summaryrefslogtreecommitdiffstats
path: root/src/main/java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java')
-rw-r--r--src/main/java/WEB-INF/reference.properties1311
-rw-r--r--src/main/java/WEB-INF/web.xml263
-rw-r--r--src/main/java/com/gitblit/.gitignore2
-rw-r--r--src/main/java/com/gitblit/AccessRestrictionFilter.java229
-rw-r--r--src/main/java/com/gitblit/AddIndexedBranch.java131
-rw-r--r--src/main/java/com/gitblit/AuthenticationFilter.java187
-rw-r--r--src/main/java/com/gitblit/ConfigUserService.java1078
-rw-r--r--src/main/java/com/gitblit/Constants.java450
-rw-r--r--src/main/java/com/gitblit/DownloadZipFilter.java107
-rw-r--r--src/main/java/com/gitblit/DownloadZipServlet.java210
-rw-r--r--src/main/java/com/gitblit/EnforceAuthenticationFilter.java102
-rw-r--r--src/main/java/com/gitblit/FederationClient.java133
-rw-r--r--src/main/java/com/gitblit/FederationPullExecutor.java523
-rw-r--r--src/main/java/com/gitblit/FederationServlet.java264
-rw-r--r--src/main/java/com/gitblit/FileSettings.java128
-rw-r--r--src/main/java/com/gitblit/FileUserService.java1146
-rw-r--r--src/main/java/com/gitblit/GCExecutor.java237
-rw-r--r--src/main/java/com/gitblit/GitBlit.java3396
-rw-r--r--src/main/java/com/gitblit/GitBlitException.java89
-rw-r--r--src/main/java/com/gitblit/GitBlitServer.java626
-rw-r--r--src/main/java/com/gitblit/GitFilter.java253
-rw-r--r--src/main/java/com/gitblit/GitServlet.java405
-rw-r--r--src/main/java/com/gitblit/GitblitSslContextFactory.java94
-rw-r--r--src/main/java/com/gitblit/GitblitTrustManager.java125
-rw-r--r--src/main/java/com/gitblit/GitblitUserService.java339
-rw-r--r--src/main/java/com/gitblit/IStoredSettings.java299
-rw-r--r--src/main/java/com/gitblit/IUserService.java325
-rw-r--r--src/main/java/com/gitblit/JsonServlet.java129
-rw-r--r--src/main/java/com/gitblit/LdapUserService.java496
-rw-r--r--src/main/java/com/gitblit/LuceneExecutor.java1375
-rw-r--r--src/main/java/com/gitblit/MailExecutor.java238
-rw-r--r--src/main/java/com/gitblit/PagesFilter.java126
-rw-r--r--src/main/java/com/gitblit/PagesServlet.java249
-rw-r--r--src/main/java/com/gitblit/RedmineUserService.java187
-rw-r--r--src/main/java/com/gitblit/RobotsTxtServlet.java64
-rw-r--r--src/main/java/com/gitblit/RpcFilter.java146
-rw-r--r--src/main/java/com/gitblit/RpcServlet.java348
-rw-r--r--src/main/java/com/gitblit/ServletRequestWrapper.java400
-rw-r--r--src/main/java/com/gitblit/SyndicationFilter.java144
-rw-r--r--src/main/java/com/gitblit/SyndicationServlet.java324
-rw-r--r--src/main/java/com/gitblit/WebXmlSettings.java112
-rw-r--r--src/main/java/com/gitblit/authority/AuthorityWorker.java58
-rw-r--r--src/main/java/com/gitblit/authority/CertificateStatus.java20
-rw-r--r--src/main/java/com/gitblit/authority/CertificateStatusRenderer.java82
-rw-r--r--src/main/java/com/gitblit/authority/CertificatesTableModel.java166
-rw-r--r--src/main/java/com/gitblit/authority/DefaultOidsPanel.java80
-rw-r--r--src/main/java/com/gitblit/authority/GitblitAuthority.java921
-rw-r--r--src/main/java/com/gitblit/authority/Launcher.java165
-rw-r--r--src/main/java/com/gitblit/authority/NewCertificateConfig.java93
-rw-r--r--src/main/java/com/gitblit/authority/NewClientCertificateDialog.java177
-rw-r--r--src/main/java/com/gitblit/authority/NewSSLCertificateDialog.java140
-rw-r--r--src/main/java/com/gitblit/authority/RequestFocusListener.java79
-rw-r--r--src/main/java/com/gitblit/authority/UserCertificateConfig.java70
-rw-r--r--src/main/java/com/gitblit/authority/UserCertificateModel.java151
-rw-r--r--src/main/java/com/gitblit/authority/UserCertificatePanel.java298
-rw-r--r--src/main/java/com/gitblit/authority/UserCertificateTableModel.java131
-rw-r--r--src/main/java/com/gitblit/authority/UserOidsPanel.java95
-rw-r--r--src/main/java/com/gitblit/authority/Utils.java123
-rw-r--r--src/main/java/com/gitblit/authority/X509CertificateViewer.java129
-rw-r--r--src/main/java/com/gitblit/build/Build.java967
-rw-r--r--src/main/java/com/gitblit/build/BuildGhPages.java259
-rw-r--r--src/main/java/com/gitblit/build/BuildSite.java385
-rw-r--r--src/main/java/com/gitblit/build/BuildThumbnails.java162
-rw-r--r--src/main/java/com/gitblit/build/BuildWebXml.java160
-rw-r--r--src/main/java/com/gitblit/client/BooleanCellRenderer.java50
-rw-r--r--src/main/java/com/gitblit/client/BranchRenderer.java78
-rw-r--r--src/main/java/com/gitblit/client/ClosableTabComponent.java149
-rw-r--r--src/main/java/com/gitblit/client/DateCellRenderer.java76
-rw-r--r--src/main/java/com/gitblit/client/EditRegistrationDialog.java196
-rw-r--r--src/main/java/com/gitblit/client/EditRepositoryDialog.java804
-rw-r--r--src/main/java/com/gitblit/client/EditTeamDialog.java397
-rw-r--r--src/main/java/com/gitblit/client/EditUserDialog.java466
-rw-r--r--src/main/java/com/gitblit/client/FeedEntryTableModel.java123
-rw-r--r--src/main/java/com/gitblit/client/FeedsPanel.java405
-rw-r--r--src/main/java/com/gitblit/client/FeedsTableModel.java122
-rw-r--r--src/main/java/com/gitblit/client/GitblitClient.java717
-rw-r--r--src/main/java/com/gitblit/client/GitblitManager.java458
-rw-r--r--src/main/java/com/gitblit/client/GitblitManagerLauncher.java164
-rw-r--r--src/main/java/com/gitblit/client/GitblitPanel.java228
-rw-r--r--src/main/java/com/gitblit/client/GitblitRegistration.java86
-rw-r--r--src/main/java/com/gitblit/client/GitblitWorker.java89
-rw-r--r--src/main/java/com/gitblit/client/HeaderPanel.java93
-rw-r--r--src/main/java/com/gitblit/client/IndicatorsRenderer.java150
-rw-r--r--src/main/java/com/gitblit/client/JPalette.java224
-rw-r--r--src/main/java/com/gitblit/client/MessageRenderer.java205
-rw-r--r--src/main/java/com/gitblit/client/NameRenderer.java91
-rw-r--r--src/main/java/com/gitblit/client/PropertiesTableModel.java106
-rw-r--r--src/main/java/com/gitblit/client/RegistrantPermissionsPanel.java235
-rw-r--r--src/main/java/com/gitblit/client/RegistrantPermissionsTableModel.java134
-rw-r--r--src/main/java/com/gitblit/client/RegistrationsDialog.java228
-rw-r--r--src/main/java/com/gitblit/client/RegistrationsTableModel.java102
-rw-r--r--src/main/java/com/gitblit/client/RepositoriesPanel.java560
-rw-r--r--src/main/java/com/gitblit/client/RepositoriesTableModel.java128
-rw-r--r--src/main/java/com/gitblit/client/SearchDialog.java404
-rw-r--r--src/main/java/com/gitblit/client/SettingCellRenderer.java67
-rw-r--r--src/main/java/com/gitblit/client/SettingPanel.java120
-rw-r--r--src/main/java/com/gitblit/client/SettingsPanel.java274
-rw-r--r--src/main/java/com/gitblit/client/SettingsTableModel.java124
-rw-r--r--src/main/java/com/gitblit/client/StatusPanel.java169
-rw-r--r--src/main/java/com/gitblit/client/SubscribedRepositoryRenderer.java62
-rw-r--r--src/main/java/com/gitblit/client/SubscriptionsDialog.java158
-rw-r--r--src/main/java/com/gitblit/client/TeamsPanel.java385
-rw-r--r--src/main/java/com/gitblit/client/TeamsTableModel.java105
-rw-r--r--src/main/java/com/gitblit/client/Translation.java59
-rw-r--r--src/main/java/com/gitblit/client/UsersPanel.java385
-rw-r--r--src/main/java/com/gitblit/client/UsersTableModel.java125
-rw-r--r--src/main/java/com/gitblit/client/Utils.java173
-rw-r--r--src/main/java/com/gitblit/client/splash.pngbin0 -> 11593 bytes
-rw-r--r--src/main/java/com/gitblit/fanout/FanoutClient.java413
-rw-r--r--src/main/java/com/gitblit/fanout/FanoutConstants.java36
-rw-r--r--src/main/java/com/gitblit/fanout/FanoutNioService.java332
-rw-r--r--src/main/java/com/gitblit/fanout/FanoutService.java563
-rw-r--r--src/main/java/com/gitblit/fanout/FanoutServiceConnection.java105
-rw-r--r--src/main/java/com/gitblit/fanout/FanoutSocketService.java234
-rw-r--r--src/main/java/com/gitblit/fanout/FanoutStats.java98
-rw-r--r--src/main/java/com/gitblit/models/Activity.java129
-rw-r--r--src/main/java/com/gitblit/models/AnnotatedLine.java47
-rw-r--r--src/main/java/com/gitblit/models/FederationModel.java206
-rw-r--r--src/main/java/com/gitblit/models/FederationProposal.java82
-rw-r--r--src/main/java/com/gitblit/models/FederationSet.java58
-rw-r--r--src/main/java/com/gitblit/models/FeedEntryModel.java61
-rw-r--r--src/main/java/com/gitblit/models/FeedModel.java91
-rw-r--r--src/main/java/com/gitblit/models/ForkModel.java77
-rw-r--r--src/main/java/com/gitblit/models/GitNote.java39
-rw-r--r--src/main/java/com/gitblit/models/GravatarProfile.java83
-rw-r--r--src/main/java/com/gitblit/models/IssueModel.java532
-rw-r--r--src/main/java/com/gitblit/models/Metric.java50
-rw-r--r--src/main/java/com/gitblit/models/PathModel.java127
-rw-r--r--src/main/java/com/gitblit/models/ProjectModel.java101
-rw-r--r--src/main/java/com/gitblit/models/PushLogEntry.java208
-rw-r--r--src/main/java/com/gitblit/models/RefModel.java148
-rw-r--r--src/main/java/com/gitblit/models/RegistrantAccessPermission.java149
-rw-r--r--src/main/java/com/gitblit/models/RepositoryCommit.java112
-rw-r--r--src/main/java/com/gitblit/models/RepositoryModel.java243
-rw-r--r--src/main/java/com/gitblit/models/SearchResult.java70
-rw-r--r--src/main/java/com/gitblit/models/ServerSettings.java69
-rw-r--r--src/main/java/com/gitblit/models/ServerStatus.java83
-rw-r--r--src/main/java/com/gitblit/models/SettingModel.java162
-rw-r--r--src/main/java/com/gitblit/models/SubmoduleModel.java47
-rw-r--r--src/main/java/com/gitblit/models/TeamModel.java310
-rw-r--r--src/main/java/com/gitblit/models/TicketModel.java119
-rw-r--r--src/main/java/com/gitblit/models/UserModel.java613
-rw-r--r--src/main/java/com/gitblit/utils/ActivityUtils.java205
-rw-r--r--src/main/java/com/gitblit/utils/ArrayUtils.java74
-rw-r--r--src/main/java/com/gitblit/utils/Base64.java311
-rw-r--r--src/main/java/com/gitblit/utils/ByteFormat.java65
-rw-r--r--src/main/java/com/gitblit/utils/ClientLogger.java77
-rw-r--r--src/main/java/com/gitblit/utils/CompressionUtils.java299
-rw-r--r--src/main/java/com/gitblit/utils/ConnectionUtils.java214
-rw-r--r--src/main/java/com/gitblit/utils/ContainerUtils.java135
-rw-r--r--src/main/java/com/gitblit/utils/DeepCopier.java135
-rw-r--r--src/main/java/com/gitblit/utils/DiffUtils.java281
-rw-r--r--src/main/java/com/gitblit/utils/FederationUtils.java349
-rw-r--r--src/main/java/com/gitblit/utils/FileUtils.java292
-rw-r--r--src/main/java/com/gitblit/utils/GitBlitDiffFormatter.java164
-rw-r--r--src/main/java/com/gitblit/utils/GitWebDiffFormatter.java155
-rw-r--r--src/main/java/com/gitblit/utils/HttpUtils.java204
-rw-r--r--src/main/java/com/gitblit/utils/IssueUtils.java829
-rw-r--r--src/main/java/com/gitblit/utils/JGitUtils.java1775
-rw-r--r--src/main/java/com/gitblit/utils/JsonUtils.java346
-rw-r--r--src/main/java/com/gitblit/utils/MarkdownUtils.java87
-rw-r--r--src/main/java/com/gitblit/utils/MetricUtils.java233
-rw-r--r--src/main/java/com/gitblit/utils/ObjectCache.java98
-rw-r--r--src/main/java/com/gitblit/utils/PatchFormatter.java143
-rw-r--r--src/main/java/com/gitblit/utils/PushLogUtils.java344
-rw-r--r--src/main/java/com/gitblit/utils/RpcUtils.java637
-rw-r--r--src/main/java/com/gitblit/utils/StringUtils.java736
-rw-r--r--src/main/java/com/gitblit/utils/SyndicationUtils.java262
-rw-r--r--src/main/java/com/gitblit/utils/TicgitUtils.java148
-rw-r--r--src/main/java/com/gitblit/utils/TimeUtils.java341
-rw-r--r--src/main/java/com/gitblit/utils/X509Utils.java1136
-rw-r--r--src/main/java/com/gitblit/wicket/AuthorizationStrategy.java86
-rw-r--r--src/main/java/com/gitblit/wicket/ExternalImage.java35
-rw-r--r--src/main/java/com/gitblit/wicket/GitBlitWebApp.java160
-rw-r--r--src/main/java/com/gitblit/wicket/GitBlitWebApp.properties447
-rw-r--r--src/main/java/com/gitblit/wicket/GitBlitWebApp_es.properties445
-rw-r--r--src/main/java/com/gitblit/wicket/GitBlitWebApp_ja.properties318
-rw-r--r--src/main/java/com/gitblit/wicket/GitBlitWebApp_ko.properties445
-rw-r--r--src/main/java/com/gitblit/wicket/GitBlitWebApp_nl.properties445
-rw-r--r--src/main/java/com/gitblit/wicket/GitBlitWebApp_pl.properties323
-rw-r--r--src/main/java/com/gitblit/wicket/GitBlitWebApp_pt_BR.properties447
-rw-r--r--src/main/java/com/gitblit/wicket/GitBlitWebApp_zh_CN.properties446
-rw-r--r--src/main/java/com/gitblit/wicket/GitBlitWebSession.java157
-rw-r--r--src/main/java/com/gitblit/wicket/GitblitParamUrlCodingStrategy.java109
-rw-r--r--src/main/java/com/gitblit/wicket/GitblitRedirectException.java49
-rw-r--r--src/main/java/com/gitblit/wicket/PageRegistration.java215
-rw-r--r--src/main/java/com/gitblit/wicket/RequiresAdminRole.java26
-rw-r--r--src/main/java/com/gitblit/wicket/SessionlessForm.java148
-rw-r--r--src/main/java/com/gitblit/wicket/StringChoiceRenderer.java43
-rw-r--r--src/main/java/com/gitblit/wicket/WicketUtils.java601
-rw-r--r--src/main/java/com/gitblit/wicket/charting/GoogleChart.java101
-rw-r--r--src/main/java/com/gitblit/wicket/charting/GoogleCharts.java68
-rw-r--r--src/main/java/com/gitblit/wicket/charting/GoogleLineChart.java60
-rw-r--r--src/main/java/com/gitblit/wicket/charting/GooglePieChart.java75
-rw-r--r--src/main/java/com/gitblit/wicket/pages/ActivityPage.html23
-rw-r--r--src/main/java/com/gitblit/wicket/pages/ActivityPage.java207
-rw-r--r--src/main/java/com/gitblit/wicket/pages/BasePage.html62
-rw-r--r--src/main/java/com/gitblit/wicket/pages/BasePage.java460
-rw-r--r--src/main/java/com/gitblit/wicket/pages/BlamePage.html39
-rw-r--r--src/main/java/com/gitblit/wicket/pages/BlamePage.java129
-rw-r--r--src/main/java/com/gitblit/wicket/pages/BlobDiffPage.html26
-rw-r--r--src/main/java/com/gitblit/wicket/pages/BlobDiffPage.java85
-rw-r--r--src/main/java/com/gitblit/wicket/pages/BlobPage.html38
-rw-r--r--src/main/java/com/gitblit/wicket/pages/BlobPage.java194
-rw-r--r--src/main/java/com/gitblit/wicket/pages/BranchesPage.html15
-rw-r--r--src/main/java/com/gitblit/wicket/pages/BranchesPage.java34
-rw-r--r--src/main/java/com/gitblit/wicket/pages/ChangePasswordPage.html31
-rw-r--r--src/main/java/com/gitblit/wicket/pages/ChangePasswordPage.java139
-rw-r--r--src/main/java/com/gitblit/wicket/pages/CommitDiffPage.html43
-rw-r--r--src/main/java/com/gitblit/wicket/pages/CommitDiffPage.java192
-rw-r--r--src/main/java/com/gitblit/wicket/pages/CommitPage.html99
-rw-r--r--src/main/java/com/gitblit/wicket/pages/CommitPage.java223
-rw-r--r--src/main/java/com/gitblit/wicket/pages/DocsPage.html28
-rw-r--r--src/main/java/com/gitblit/wicket/pages/DocsPage.java82
-rw-r--r--src/main/java/com/gitblit/wicket/pages/EditRepositoryPage.html111
-rw-r--r--src/main/java/com/gitblit/wicket/pages/EditRepositoryPage.java696
-rw-r--r--src/main/java/com/gitblit/wicket/pages/EditTeamPage.html66
-rw-r--r--src/main/java/com/gitblit/wicket/pages/EditTeamPage.java250
-rw-r--r--src/main/java/com/gitblit/wicket/pages/EditUserPage.html78
-rw-r--r--src/main/java/com/gitblit/wicket/pages/EditUserPage.java261
-rw-r--r--src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage.html53
-rw-r--r--src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage.java67
-rw-r--r--src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_es.html56
-rw-r--r--src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_ko.html57
-rw-r--r--src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_nl.html53
-rw-r--r--src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_pl.html56
-rw-r--r--src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_pt_BR.html53
-rw-r--r--src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_zh_CN.html55
-rw-r--r--src/main/java/com/gitblit/wicket/pages/FederationPage.html17
-rw-r--r--src/main/java/com/gitblit/wicket/pages/FederationPage.java52
-rw-r--r--src/main/java/com/gitblit/wicket/pages/FederationRegistrationPage.html39
-rw-r--r--src/main/java/com/gitblit/wicket/pages/FederationRegistrationPage.java95
-rw-r--r--src/main/java/com/gitblit/wicket/pages/ForkPage.html38
-rw-r--r--src/main/java/com/gitblit/wicket/pages/ForkPage.java107
-rw-r--r--src/main/java/com/gitblit/wicket/pages/ForksPage.html20
-rw-r--r--src/main/java/com/gitblit/wicket/pages/ForksPage.java156
-rw-r--r--src/main/java/com/gitblit/wicket/pages/GitSearchPage.html25
-rw-r--r--src/main/java/com/gitblit/wicket/pages/GitSearchPage.java69
-rw-r--r--src/main/java/com/gitblit/wicket/pages/GravatarProfilePage.html20
-rw-r--r--src/main/java/com/gitblit/wicket/pages/GravatarProfilePage.java64
-rw-r--r--src/main/java/com/gitblit/wicket/pages/HistoryPage.html25
-rw-r--r--src/main/java/com/gitblit/wicket/pages/HistoryPage.java65
-rw-r--r--src/main/java/com/gitblit/wicket/pages/LogPage.html25
-rw-r--r--src/main/java/com/gitblit/wicket/pages/LogPage.java69
-rw-r--r--src/main/java/com/gitblit/wicket/pages/LogoutPage.html33
-rw-r--r--src/main/java/com/gitblit/wicket/pages/LogoutPage.java51
-rw-r--r--src/main/java/com/gitblit/wicket/pages/LuceneSearchPage.html92
-rw-r--r--src/main/java/com/gitblit/wicket/pages/LuceneSearchPage.java257
-rw-r--r--src/main/java/com/gitblit/wicket/pages/MarkdownPage.html18
-rw-r--r--src/main/java/com/gitblit/wicket/pages/MarkdownPage.java73
-rw-r--r--src/main/java/com/gitblit/wicket/pages/MetricsPage.html44
-rw-r--r--src/main/java/com/gitblit/wicket/pages/MetricsPage.java184
-rw-r--r--src/main/java/com/gitblit/wicket/pages/PatchPage.html13
-rw-r--r--src/main/java/com/gitblit/wicket/pages/PatchPage.java69
-rw-r--r--src/main/java/com/gitblit/wicket/pages/ProjectPage.html70
-rw-r--r--src/main/java/com/gitblit/wicket/pages/ProjectPage.java355
-rw-r--r--src/main/java/com/gitblit/wicket/pages/ProjectsPage.html37
-rw-r--r--src/main/java/com/gitblit/wicket/pages/ProjectsPage.java235
-rw-r--r--src/main/java/com/gitblit/wicket/pages/RawPage.java161
-rw-r--r--src/main/java/com/gitblit/wicket/pages/RepositoriesPage.html14
-rw-r--r--src/main/java/com/gitblit/wicket/pages/RepositoriesPage.java186
-rw-r--r--src/main/java/com/gitblit/wicket/pages/RepositoryPage.html82
-rw-r--r--src/main/java/com/gitblit/wicket/pages/RepositoryPage.java608
-rw-r--r--src/main/java/com/gitblit/wicket/pages/ReviewProposalPage.html23
-rw-r--r--src/main/java/com/gitblit/wicket/pages/ReviewProposalPage.java102
-rw-r--r--src/main/java/com/gitblit/wicket/pages/RootPage.html41
-rw-r--r--src/main/java/com/gitblit/wicket/pages/RootPage.java454
-rw-r--r--src/main/java/com/gitblit/wicket/pages/RootSubPage.html18
-rw-r--r--src/main/java/com/gitblit/wicket/pages/RootSubPage.java109
-rw-r--r--src/main/java/com/gitblit/wicket/pages/SendProposalPage.html24
-rw-r--r--src/main/java/com/gitblit/wicket/pages/SendProposalPage.java152
-rw-r--r--src/main/java/com/gitblit/wicket/pages/SummaryPage.html54
-rw-r--r--src/main/java/com/gitblit/wicket/pages/SummaryPage.java238
-rw-r--r--src/main/java/com/gitblit/wicket/pages/TagPage.html38
-rw-r--r--src/main/java/com/gitblit/wicket/pages/TagPage.java99
-rw-r--r--src/main/java/com/gitblit/wicket/pages/TagsPage.html15
-rw-r--r--src/main/java/com/gitblit/wicket/pages/TagsPage.java35
-rw-r--r--src/main/java/com/gitblit/wicket/pages/TicketPage.html39
-rw-r--r--src/main/java/com/gitblit/wicket/pages/TicketPage.java81
-rw-r--r--src/main/java/com/gitblit/wicket/pages/TicketsPage.html27
-rw-r--r--src/main/java/com/gitblit/wicket/pages/TicketsPage.java76
-rw-r--r--src/main/java/com/gitblit/wicket/pages/TreePage.html55
-rw-r--r--src/main/java/com/gitblit/wicket/pages/TreePage.java186
-rw-r--r--src/main/java/com/gitblit/wicket/pages/UserPage.html50
-rw-r--r--src/main/java/com/gitblit/wicket/pages/UserPage.java155
-rw-r--r--src/main/java/com/gitblit/wicket/pages/UsersPage.html13
-rw-r--r--src/main/java/com/gitblit/wicket/pages/UsersPage.java33
-rw-r--r--src/main/java/com/gitblit/wicket/pages/prettify/lang-apollo.js2
-rw-r--r--src/main/java/com/gitblit/wicket/pages/prettify/lang-css.js2
-rw-r--r--src/main/java/com/gitblit/wicket/pages/prettify/lang-hs.js2
-rw-r--r--src/main/java/com/gitblit/wicket/pages/prettify/lang-lisp.js2
-rw-r--r--src/main/java/com/gitblit/wicket/pages/prettify/lang-lua.js2
-rw-r--r--src/main/java/com/gitblit/wicket/pages/prettify/lang-ml.js2
-rw-r--r--src/main/java/com/gitblit/wicket/pages/prettify/lang-proto.js1
-rw-r--r--src/main/java/com/gitblit/wicket/pages/prettify/lang-scala.js2
-rw-r--r--src/main/java/com/gitblit/wicket/pages/prettify/lang-sql.js2
-rw-r--r--src/main/java/com/gitblit/wicket/pages/prettify/lang-vb.js2
-rw-r--r--src/main/java/com/gitblit/wicket/pages/prettify/lang-vhdl.js3
-rw-r--r--src/main/java/com/gitblit/wicket/pages/prettify/lang-wiki.js2
-rw-r--r--src/main/java/com/gitblit/wicket/pages/prettify/lang-yaml.js2
-rw-r--r--src/main/java/com/gitblit/wicket/pages/prettify/prettify.css1
-rw-r--r--src/main/java/com/gitblit/wicket/pages/prettify/prettify.js33
-rw-r--r--src/main/java/com/gitblit/wicket/panels/ActivityPanel.html37
-rw-r--r--src/main/java/com/gitblit/wicket/panels/ActivityPanel.java148
-rw-r--r--src/main/java/com/gitblit/wicket/panels/BasePanel.java105
-rw-r--r--src/main/java/com/gitblit/wicket/panels/BranchesPanel.html52
-rw-r--r--src/main/java/com/gitblit/wicket/panels/BranchesPanel.java211
-rw-r--r--src/main/java/com/gitblit/wicket/panels/BulletListPanel.html13
-rw-r--r--src/main/java/com/gitblit/wicket/panels/BulletListPanel.java48
-rw-r--r--src/main/java/com/gitblit/wicket/panels/CommitHeaderPanel.html24
-rw-r--r--src/main/java/com/gitblit/wicket/panels/CommitHeaderPanel.java48
-rw-r--r--src/main/java/com/gitblit/wicket/panels/CommitLegendPanel.html13
-rw-r--r--src/main/java/com/gitblit/wicket/panels/CommitLegendPanel.java88
-rw-r--r--src/main/java/com/gitblit/wicket/panels/CompressedDownloadsPanel.html12
-rw-r--r--src/main/java/com/gitblit/wicket/panels/CompressedDownloadsPanel.java77
-rw-r--r--src/main/java/com/gitblit/wicket/panels/DropDownMenu.html13
-rw-r--r--src/main/java/com/gitblit/wicket/panels/DropDownMenu.java61
-rw-r--r--src/main/java/com/gitblit/wicket/panels/FederationProposalsPanel.html34
-rw-r--r--src/main/java/com/gitblit/wicket/panels/FederationProposalsPanel.java92
-rw-r--r--src/main/java/com/gitblit/wicket/panels/FederationRegistrationsPanel.html38
-rw-r--r--src/main/java/com/gitblit/wicket/panels/FederationRegistrationsPanel.java83
-rw-r--r--src/main/java/com/gitblit/wicket/panels/FederationTokensPanel.html35
-rw-r--r--src/main/java/com/gitblit/wicket/panels/FederationTokensPanel.java110
-rw-r--r--src/main/java/com/gitblit/wicket/panels/GravatarImage.html9
-rw-r--r--src/main/java/com/gitblit/wicket/panels/GravatarImage.java73
-rw-r--r--src/main/java/com/gitblit/wicket/panels/HistoryPanel.html48
-rw-r--r--src/main/java/com/gitblit/wicket/panels/HistoryPanel.java335
-rw-r--r--src/main/java/com/gitblit/wicket/panels/LinkPanel.html9
-rw-r--r--src/main/java/com/gitblit/wicket/panels/LinkPanel.java110
-rw-r--r--src/main/java/com/gitblit/wicket/panels/LogPanel.html32
-rw-r--r--src/main/java/com/gitblit/wicket/panels/LogPanel.java176
-rw-r--r--src/main/java/com/gitblit/wicket/panels/NavigationPanel.html12
-rw-r--r--src/main/java/com/gitblit/wicket/panels/NavigationPanel.java74
-rw-r--r--src/main/java/com/gitblit/wicket/panels/ObjectContainer.java158
-rw-r--r--src/main/java/com/gitblit/wicket/panels/PagerPanel.html13
-rw-r--r--src/main/java/com/gitblit/wicket/panels/PagerPanel.java95
-rw-r--r--src/main/java/com/gitblit/wicket/panels/PathBreadcrumbsPanel.html17
-rw-r--r--src/main/java/com/gitblit/wicket/panels/PathBreadcrumbsPanel.java92
-rw-r--r--src/main/java/com/gitblit/wicket/panels/ProjectRepositoryPanel.html80
-rw-r--r--src/main/java/com/gitblit/wicket/panels/ProjectRepositoryPanel.java229
-rw-r--r--src/main/java/com/gitblit/wicket/panels/RefsPanel.html12
-rw-r--r--src/main/java/com/gitblit/wicket/panels/RefsPanel.java156
-rw-r--r--src/main/java/com/gitblit/wicket/panels/RegistrantPermissionsPanel.html44
-rw-r--r--src/main/java/com/gitblit/wicket/panels/RegistrantPermissionsPanel.java340
-rw-r--r--src/main/java/com/gitblit/wicket/panels/RepositoriesPanel.html107
-rw-r--r--src/main/java/com/gitblit/wicket/panels/RepositoriesPanel.java557
-rw-r--r--src/main/java/com/gitblit/wicket/panels/RepositoryUrlPanel.html30
-rw-r--r--src/main/java/com/gitblit/wicket/panels/RepositoryUrlPanel.java51
-rw-r--r--src/main/java/com/gitblit/wicket/panels/SearchPanel.html33
-rw-r--r--src/main/java/com/gitblit/wicket/panels/SearchPanel.java142
-rw-r--r--src/main/java/com/gitblit/wicket/panels/ShockWaveComponent.java205
-rw-r--r--src/main/java/com/gitblit/wicket/panels/TagsPanel.html51
-rw-r--r--src/main/java/com/gitblit/wicket/panels/TagsPanel.java170
-rw-r--r--src/main/java/com/gitblit/wicket/panels/TeamsPanel.html48
-rw-r--r--src/main/java/com/gitblit/wicket/panels/TeamsPanel.java96
-rw-r--r--src/main/java/com/gitblit/wicket/panels/UsersPanel.html54
-rw-r--r--src/main/java/com/gitblit/wicket/panels/UsersPanel.java117
-rw-r--r--src/main/java/log4j.properties66
358 files changed, 67185 insertions, 0 deletions
diff --git a/src/main/java/WEB-INF/reference.properties b/src/main/java/WEB-INF/reference.properties
new file mode 100644
index 00000000..a05f5d2d
--- /dev/null
+++ b/src/main/java/WEB-INF/reference.properties
@@ -0,0 +1,1311 @@
+#
+# Gitblit Settings
+#
+
+# This settings file supports parameterization from the command-line for the
+# following command-line parameters:
+#
+# --baseFolder ${baseFolder} SINCE 1.2.1
+#
+# Settings that support ${baseFolder} parameter substitution are indicated with the
+# BASEFOLDER attribute. If the --baseFolder argument is unspecified, ${baseFolder}
+# and it's trailing / will be discarded from the setting value leaving a relative
+# path that is equivalent to pre-1.2.1 releases.
+#
+# e.g. "${baseFolder}/git" becomes "git", if --baseFolder is unspecified
+#
+# Git Servlet Settings
+#
+
+# Base folder for repositories.
+# This folder may contain bare and non-bare repositories but Gitblit will only
+# allow you to push to bare repositories.
+# Use forward slashes even on Windows!!
+# e.g. c:/gitrepos
+#
+# SINCE 0.5.0
+# RESTART REQUIRED
+# BASEFOLDER
+git.repositoriesFolder = ${baseFolder}/git
+
+# Build the available repository list at startup and cache this list for reuse.
+# This reduces disk io when presenting the repositories page, responding to rpcs,
+# etc, but it means that Gitblit will not automatically identify repositories
+# added or deleted by external tools.
+#
+# For this case you can use curl, wget, etc to issue an rpc request to clear the
+# cache (e.g. https://localhost/rpc?req=CLEAR_REPOSITORY_CACHE)
+#
+# SINCE 1.1.0
+git.cacheRepositoryList = true
+
+# Search the repositories folder subfolders for other repositories.
+# Repositories MAY NOT be nested (i.e. one repository within another)
+# but they may be grouped together in subfolders.
+# e.g. c:/gitrepos/libraries/mylibrary.git
+# c:/gitrepos/libraries/myotherlibrary.git
+#
+# SINCE 0.5.0
+git.searchRepositoriesSubfolders = true
+
+# Maximum number of folders to recurse into when searching for repositories.
+# The default value, -1, disables depth limits.
+#
+# SINCE 1.1.0
+git.searchRecursionDepth = -1
+
+# List of regex exclusion patterns to match against folders found in
+# *git.repositoriesFolder*.
+# Use forward slashes even on Windows!!
+# e.g. test/jgit\.git
+#
+# SPACE-DELIMITED
+# CASE-SENSITIVE
+# SINCE 1.1.0
+git.searchExclusions =
+
+# List of regex url patterns for extracting a repository name when locating
+# submodules.
+# e.g. git.submoduleUrlPatterns = .*?://github.com/(.*) will extract
+# *gitblit/gitblit.git* from *git://github.com/gitblit/gitblit.git*
+# If no matches are found then the submodule repository name is assumed to be
+# whatever trails the last / character. (e.g. gitblit.git).
+#
+# SPACE-DELIMITED
+# CASE-SENSITIVE
+# SINCE 1.1.0
+git.submoduleUrlPatterns = .*?://github.com/(.*)
+
+# Allow push/pull over http/https with JGit servlet.
+# If you do NOT want to allow Git clients to clone/push to Gitblit set this
+# to false. You might want to do this if you are only using ssh:// or git://.
+# If you set this false, consider changing the *web.otherUrls* setting to
+# indicate your clone/push urls.
+#
+# SINCE 0.5.0
+git.enableGitServlet = true
+
+# If you want to restrict all git servlet access to those with valid X509 client
+# certificates then set this value to true.
+#
+# SINCE 1.2.0
+git.requiresClientCertificate = false
+
+# Enforce date checks on client certificates to ensure that they are not being
+# used prematurely and that they have not expired.
+#
+# SINCE 1.2.0
+git.enforceCertificateValidity = true
+
+# List of OIDs to extract from a client certificate DN to map a certificate to
+# an account username.
+#
+# e.g. git.certificateUsernameOIDs = CN
+# e.g. git.certificateUsernameOIDs = FirstName LastName
+#
+# SPACE-DELIMITED
+# SINCE 1.2.0
+git.certificateUsernameOIDs = CN
+
+# Only serve/display bare repositories.
+# If there are non-bare repositories in git.repositoriesFolder and this setting
+# is true, they will be excluded from the ui.
+#
+# SINCE 0.9.0
+git.onlyAccessBareRepositories = false
+
+# Allow an authenticated user to create a destination repository on a push if
+# the repository does not already exist.
+#
+# Administrator accounts can create a repository in any project.
+# These repositories are created with the default access restriction and authorization
+# control values. The pushing account is set as the owner.
+#
+# Non-administrator accounts with the CREATE role may create personal repositories.
+# These repositories are created as VIEW restricted for NAMED users.
+# The pushing account is set as the owner.
+#
+# SINCE 1.2.0
+git.allowCreateOnPush = true
+
+# The default access restriction for new repositories.
+# Valid values are NONE, PUSH, CLONE, VIEW
+# NONE = anonymous view, clone, & push
+# PUSH = anonymous view & clone and authenticated push
+# CLONE = anonymous view, authenticated clone & push
+# VIEW = authenticated view, clone, & push
+#
+# SINCE 1.0.0
+git.defaultAccessRestriction = NONE
+
+# The default authorization control for new repositories.
+# Valid values are AUTHENTICATED and NAMED
+# AUTHENTICATED = any authenticated user is granted restricted access
+# NAMED = only named users/teams are granted restricted access
+#
+# SINCE 1.1.0
+git.defaultAuthorizationControl = NAMED
+
+# Enable JGit-based garbage collection. (!!EXPERIMENTAL!!)
+#
+# USE AT YOUR OWN RISK!
+#
+# If enabled, the garbage collection executor scans all repositories once a day
+# at the hour of your choosing. The GC executor will take each repository "offline",
+# one-at-a-time, to check if the repository satisfies it's GC trigger requirements.
+#
+# While the repository is offline it will be inaccessible from the web UI or from
+# any of the other services (git, rpc, rss, etc).
+#
+# Gitblit's GC Executor MAY NOT PLAY NICE with the other Git kids on the block,
+# especially on Windows systems, so if you are using other tools please coordinate
+# their usage with your GC Executor schedule or do not use this feature.
+#
+# The GC algorithm complex and the JGit team advises caution when using their
+# young implementation of GC.
+#
+# http://wiki.eclipse.org/EGit/New_and_Noteworthy/2.1#Garbage_Collector_and_Repository_Storage_Statistics
+#
+# EXPERIMENTAL
+# SINCE 1.2.0
+# RESTART REQUIRED
+git.enableGarbageCollection = false
+
+# Hour of the day for the GC Executor to scan repositories.
+# This value is in 24-hour time.
+#
+# SINCE 1.2.0
+git.garbageCollectionHour = 0
+
+# The default minimum total filesize of loose objects to trigger early garbage
+# collection.
+#
+# You may specify a custom threshold for a repository in the repository's settings.
+# Common unit suffixes of k, m, or g are supported.
+#
+# SINCE 1.2.0
+git.defaultGarbageCollectionThreshold = 500k
+
+# The default period, in days, between GCs for a repository. If the total filesize
+# of the loose object exceeds *git.garbageCollectionThreshold* or the repository's
+# custom threshold, this period will be short-circuited.
+#
+# e.g. if a repository collects 100KB of loose objects every day with a 500KB
+# threshold and a period of 7 days, it will take 5 days for the loose objects to
+# be collected, packed, and pruned.
+#
+# OR
+#
+# if a repository collects 10KB of loose objects every day with a 500KB threshold
+# and a period of 7 days, it will take the full 7 days for the loose objects to be
+# collected, packed, and pruned.
+#
+# You may specify a custom period for a repository in the repository's settings.
+#
+# The minimum value is 1 day since the GC Executor only runs once a day.
+#
+# SINCE 1.2.0
+git.defaultGarbageCollectionPeriod = 7
+
+# Number of bytes of a pack file to load into memory in a single read operation.
+# This is the "page size" of the JGit buffer cache, used for all pack access
+# operations. All disk IO occurs as single window reads. Setting this too large
+# may cause the process to load more data than is required; setting this too small
+# may increase the frequency of read() system calls.
+#
+# Default on JGit is 8 KiB on all platforms.
+#
+# Common unit suffixes of k, m, or g are supported.
+# Documentation courtesy of the Gerrit project.
+#
+# SINCE 1.0.0
+# RESTART REQUIRED
+git.packedGitWindowSize = 8k
+
+# Maximum number of bytes to load and cache in memory from pack files. If JGit
+# needs to access more than this many bytes it will unload less frequently used
+# windows to reclaim memory space within the process. As this buffer must be shared
+# with the rest of the JVM heap, it should be a fraction of the total memory available.
+#
+# The JGit team recommends setting this value larger than the size of your biggest
+# repository. This ensures you can serve most requests from memory.
+#
+# Default on JGit is 10 MiB on all platforms.
+#
+# Common unit suffixes of k, m, or g are supported.
+# Documentation courtesy of the Gerrit project.
+#
+# SINCE 1.0.0
+# RESTART REQUIRED
+git.packedGitLimit = 10m
+
+# Maximum number of bytes to reserve for caching base objects that multiple deltafied
+# objects reference. By storing the entire decompressed base object in a cache Git
+# is able to avoid unpacking and decompressing frequently used base objects multiple times.
+#
+# Default on JGit is 10 MiB on all platforms. You probably do not need to adjust
+# this value.
+#
+# Common unit suffixes of k, m, or g are supported.
+# Documentation courtesy of the Gerrit project.
+#
+# SINCE 1.0.0
+# RESTART REQUIRED
+git.deltaBaseCacheLimit = 10m
+
+# Maximum number of pack files to have open at once. A pack file must be opened
+# in order for any of its data to be available in a cached window.
+#
+# If you increase this to a larger setting you may need to also adjust the ulimit
+# on file descriptors for the host JVM, as Gitblit needs additional file descriptors
+# available for network sockets and other repository data manipulation.
+#
+# Default on JGit is 128 file descriptors on all platforms.
+# Documentation courtesy of the Gerrit project.
+#
+# SINCE 1.0.0
+# RESTART REQUIRED
+git.packedGitOpenFiles = 128
+
+# Largest object size, in bytes, that JGit will allocate as a contiguous byte
+# array. Any file revision larger than this threshold will have to be streamed,
+# typically requiring the use of temporary files under $GIT_DIR/objects to implement
+# psuedo-random access during delta decompression.
+#
+# Servers with very high traffic should set this to be larger than the size of
+# their common big files. For example a server managing the Android platform
+# typically has to deal with ~10-12 MiB XML files, so 15 m would be a reasonable
+# setting in that environment. Setting this too high may cause the JVM to run out
+# of heap space when handling very big binary files, such as device firmware or
+# CD-ROM ISO images. Make sure to adjust your JVM heap accordingly.
+#
+# Default is 50 MiB on all platforms.
+#
+# Common unit suffixes of k, m, or g are supported.
+# Documentation courtesy of the Gerrit project.
+#
+# SINCE 1.0.0
+# RESTART REQUIRED
+git.streamFileThreshold = 50m
+
+# When true, JGit will use mmap() rather than malloc()+read() to load data from
+# pack files. The use of mmap can be problematic on some JVMs as the garbage
+# collector must deduce that a memory mapped segment is no longer in use before
+# a call to munmap() can be made by the JVM native code.
+#
+# In server applications (such as Gitblit) that need to access many pack files,
+# setting this to true risks artificially running out of virtual address space,
+# as the garbage collector cannot reclaim unused mapped spaces fast enough.
+#
+# Default on JGit is false. Although potentially slower, it yields much more
+# predictable behavior.
+# Documentation courtesy of the Gerrit project.
+#
+# SINCE 1.0.0
+# RESTART REQUIRED
+git.packedGitMmap = false
+
+#
+# Groovy Integration
+#
+
+# Location of Groovy scripts to use for Pre and Post receive hooks.
+# Use forward slashes even on Windows!!
+# e.g. c:/groovy
+#
+# RESTART REQUIRED
+# SINCE 0.8.0
+# BASEFOLDER
+groovy.scriptsFolder = ${baseFolder}/groovy
+
+# Specify the directory Grape uses for downloading libraries.
+# http://groovy.codehaus.org/Grape
+#
+# RESTART REQUIRED
+# SINCE 1.0.0
+# BASEFOLDER
+groovy.grapeFolder = ${baseFolder}/groovy/grape
+
+# Scripts to execute on Pre-Receive.
+#
+# These scripts execute after an incoming push has been parsed and validated
+# but BEFORE the changes are applied to the repository. You might reject a
+# push in this script based on the repository and branch the push is attempting
+# to change.
+#
+# Script names are case-sensitive on case-sensitive file systems. You may omit
+# the traditional ".groovy" from this list if your file extension is ".groovy"
+#
+# NOTE:
+# These scripts are only executed when pushing to *Gitblit*, not to other Git
+# tooling you may be using. Also note that these scripts are shared between
+# repositories. These are NOT repository-specific scripts! Within the script
+# you may customize the control-flow for a specific repository by checking the
+# *repository* variable.
+#
+# SPACE-DELIMITED
+# CASE-SENSITIVE
+# SINCE 0.8.0
+groovy.preReceiveScripts =
+
+# Scripts to execute on Post-Receive.
+#
+# These scripts execute AFTER an incoming push has been applied to a repository.
+# You might trigger a continuous-integration build here or send a notification.
+#
+# Script names are case-sensitive on case-sensitive file systems. You may omit
+# the traditional ".groovy" from this list if your file extension is ".groovy"
+#
+# NOTE:
+# These scripts are only executed when pushing to *Gitblit*, not to other Git
+# tooling you may be using. Also note that these scripts are shared between
+# repositories. These are NOT repository-specific scripts! Within the script
+# you may customize the control-flow for a specific repository by checking the
+# *repository* variable.
+#
+# SPACE-DELIMITED
+# CASE-SENSITIVE
+# SINCE 0.8.0
+groovy.postReceiveScripts =
+
+# Repository custom fields for Groovy Hook mechanism
+#
+# List of key=label pairs of custom fields to prompt for in the Edit Repository
+# page. These keys are stored in the repository's git config file in the
+# section [gitblit "customFields"]. Key names are alphanumeric only. These
+# fields are intended to be used for the Groovy hook mechanism where a script
+# can adjust it's execution based on the custom fields stored in the repository
+# config.
+#
+# e.g. "commitMsgRegex=Commit Message Regular Expression" anotherProperty=Another
+#
+# SPACE-DELIMITED
+# SINCE 1.0.0
+groovy.customFields =
+
+#
+# Fanout Settings
+#
+
+# Fanout is a PubSub notification service that can be used by Sparkleshare
+# to eliminate repository change polling. The fanout service runs in a separate
+# thread on a separate port from the Gitblit http/https application.
+# This service is provided so that Sparkleshare may be used with Gitblit in
+# firewalled environments or where reliance on Sparkleshare's default notifications
+# server (notifications.sparkleshare.org) is unwanted.
+#
+# This service maintains an open socket connection from the client to the
+# Fanout PubSub service. This service may not work properly behind a proxy server.
+
+# Specify the interface for Fanout to bind it's service.
+# You may specify an ip or an empty value to bind to all interfaces.
+# Specifying localhost will result in Gitblit ONLY listening to requests to
+# localhost.
+#
+# SINCE 1.2.1
+# RESTART REQUIRED
+fanout.bindInterface = localhost
+
+# port for serving the Fanout PubSub service. <= 0 disables this service.
+# On Unix/Linux systems, ports < 1024 require root permissions.
+# Recommended value: 17000
+#
+# SINCE 1.2.1
+# RESTART REQUIRED
+fanout.port = 0
+
+# Use Fanout NIO service. If false, a multi-threaded socket service will be used.
+# Be advised, the socket implementation spawns a thread per connection plus the
+# connection acceptor thread. The NIO implementation is completely single-threaded.
+#
+# SINCE 1.2.1
+# RESTART REQUIRED
+fanout.useNio = true
+
+# Concurrent connection limit. <= 0 disables concurrent connection throttling.
+# If > 0, only the specified number of concurrent connections will be allowed
+# and all other connections will be rejected.
+#
+# SINCE 1.2.1
+# RESTART REQUIRED
+fanout.connectionLimit = 0
+
+#
+# Authentication Settings
+#
+
+# Require authentication to see everything but the admin pages
+#
+# SINCE 0.5.0
+# RESTART REQUIRED
+web.authenticateViewPages = false
+
+# If web.authenticateViewPages=true you may optionally require a client-side
+# basic authentication prompt instead of the standard form-based login.
+#
+# SINCE 1.3.0
+web.enforceHttpBasicAuthentication = false
+
+# Require admin authentication for the admin functions and pages
+#
+# SINCE 0.5.0
+# RESTART REQUIRED
+web.authenticateAdminPages = true
+
+# Allow Gitblit to store a cookie in the user's browser for automatic
+# authentication. The cookie is generated by the user service.
+#
+# SINCE 0.5.0
+web.allowCookieAuthentication = true
+
+# Config file for storing project metadata
+#
+# SINCE 1.2.0
+# BASEFOLDER
+web.projectsFile = ${baseFolder}/projects.conf
+
+# Either the full path to a user config file (users.conf)
+# OR the full path to a simple user properties file (users.properties)
+# OR a fully qualified class name that implements the IUserService interface.
+#
+# Alternative user services:
+# com.gitblit.LdapUserService
+# com.gitblit.RedmineUserService
+#
+# Any custom user service implementation must have a public default constructor.
+#
+# SINCE 0.5.0
+# RESTART REQUIRED
+# BASEFOLDER
+realm.userService = ${baseFolder}/users.conf
+
+# How to store passwords.
+# Valid values are plain, md5, or combined-md5. md5 is the hash of password.
+# combined-md5 is the hash of username.toLowerCase()+password.
+# Default is md5.
+#
+# SINCE 0.5.0
+realm.passwordStorage = md5
+
+# Minimum valid length for a plain text password.
+# Default value is 5. Absolute minimum is 4.
+#
+# SINCE 0.5.0
+realm.minPasswordLength = 5
+
+#
+# Gitblit Web Settings
+#
+# If blank Gitblit is displayed.
+#
+# SINCE 0.5.0
+web.siteName =
+
+# If *web.authenticateAdminPages*=true, users with "admin" role can create
+# repositories, create users, and edit repository metadata.
+#
+# If *web.authenticateAdminPages*=false, any user can execute the aforementioned
+# functions.
+#
+# SINCE 0.5.0
+web.allowAdministration = true
+
+# Allows rpc clients to list repositories and possibly manage or administer the
+# Gitblit server, if the authenticated account has administrator permissions.
+# See *web.enableRpcManagement* and *web.enableRpcAdministration*.
+#
+# SINCE 0.7.0
+web.enableRpcServlet = true
+
+# Allows rpc clients to manage repositories and users of the Gitblit instance,
+# if the authenticated account has administrator permissions.
+# Requires *web.enableRpcServlet=true*.
+#
+# SINCE 0.7.0
+web.enableRpcManagement = false
+
+# Allows rpc clients to control the server settings and monitor the health of this
+# this Gitblit instance, if the authenticated account has administrator permissions.
+# Requires *web.enableRpcServlet=true* and *web.enableRpcManagement*.
+#
+# SINCE 0.7.0
+web.enableRpcAdministration = false
+
+# Full path to a configurable robots.txt file. With this file you can control
+# what parts of your Gitblit server respectable robots are allowed to traverse.
+# http://googlewebmastercentral.blogspot.com/2008/06/improving-on-robots-exclusion-protocol.html
+#
+# SINCE 1.0.0
+# BASEFOLDER
+web.robots.txt = ${baseFolder}/robots.txt
+
+# If true, the web ui layout will respond and adapt to the browser's dimensions.
+# if false, the web ui will use a 940px fixed-width layout.
+# http://twitter.github.com/bootstrap/scaffolding.html#responsive
+#
+# SINCE 1.0.0
+web.useResponsiveLayout = true
+
+# Allow Gravatar images to be displayed in Gitblit pages.
+#
+# SINCE 0.8.0
+web.allowGravatar = true
+
+# Allow dynamic zip downloads.
+#
+# SINCE 0.5.0
+web.allowZipDownloads = true
+
+# If *web.allowZipDownloads=true* the following formats will be displayed for
+# download compressed archive links:
+#
+# zip = standard .zip
+# tar = standard tar format (preserves *nix permissions and symlinks)
+# gz = gz-compressed tar
+# xz = xz-compressed tar
+# bzip2 = bzip2-compressed tar
+#
+# SPACE-DELIMITED
+# SINCE 1.2.0
+web.compressedDownloads = zip gz
+
+# Allow optional Lucene integration. Lucene indexing is an opt-in feature.
+# A repository may specify branches to index with Lucene instead of using Git
+# commit traversal. There are scenarios where you may want to completely disable
+# Lucene indexing despite a repository specifying indexed branches. One such
+# scenario is on a resource-constrained federated Gitblit mirror.
+#
+# SINCE 0.9.0
+web.allowLuceneIndexing = true
+
+# Allows an authenticated user to create forks of a repository
+#
+# set this to false if you want to disable all fork controls on the web site
+#
+web.allowForking = true
+
+# Controls the length of shortened commit hash ids
+#
+# SINCE 1.2.0
+web.shortCommitIdLength = 6
+
+# Use Clippy (Flash solution) to provide a copy-to-clipboard button.
+# If false, a button with a more primitive JavaScript-based prompt box will
+# offer a 3-step (click, ctrl+c, enter) copy-to-clipboard alternative.
+#
+# SINCE 0.8.0
+web.allowFlashCopyToClipboard = true
+
+# Default maximum number of commits that a repository may contribute to the
+# activity page, regardless of the selected duration. This setting may be valuable
+# for an extremely busy server. This value may also be configed per-repository
+# in Edit Repository. 0 disables this throttle.
+#
+# SINCE 1.2.0
+web.maxActivityCommits = 0
+
+# Default number of entries to include in RSS Syndication links
+#
+# SINCE 0.5.0
+web.syndicationEntries = 25
+
+# Show the size of each repository on the repositories page.
+# This requires recursive traversal of each repository folder. This may be
+# non-performant on some operating systems and/or filesystems.
+#
+# SINCE 0.5.2
+web.showRepositorySizes = true
+
+# List of custom regex expressions that can be displayed in the Filters menu
+# of the Repositories and Activity pages. Keep them very simple because you
+# are likely to run into encoding issues if they are too complex.
+#
+# Use !!! to separate the filters
+#
+# SINCE 0.8.0
+web.customFilters =
+
+# Show federation registrations (without token) and the current pull status
+# to non-administrator users.
+#
+# SINCE 0.6.0
+web.showFederationRegistrations = false
+
+# This is the message displayed when *web.authenticateViewPages=true*.
+# This can point to a file with Markdown content.
+# Specifying "gitblit" uses the internal login message.
+#
+# SINCE 0.7.0
+# BASEFOLDER
+web.loginMessage = gitblit
+
+# This is the message displayed above the repositories table.
+# This can point to a file with Markdown content.
+# Specifying "gitblit" uses the internal welcome message.
+#
+# SINCE 0.5.0
+# BASEFOLDER
+web.repositoriesMessage = gitblit
+
+# Ordered list of charsets/encodings to use when trying to display a blob.
+# If empty, UTF-8 and ISO-8859-1 are used. The server's default charset
+# is always appended to the encoding list. If all encodings fail to cleanly
+# decode the blob content, UTF-8 will be used with the standard malformed
+# input/unmappable character replacement strings.
+#
+# SPACE-DELIMITED
+# SINCE 1.0.0
+web.blobEncodings = UTF-8 ISO-8859-1
+
+# Manually set the default timezone to be used by Gitblit for display in the
+# web ui. This value is independent of the JVM timezone. Specifying a blank
+# value will default to the JVM timezone.
+# e.g. America/New_York, US/Pacific, UTC, Europe/Berlin
+#
+# SINCE 0.9.0
+# RESTART REQUIRED
+web.timezone =
+
+# Use the client timezone when formatting dates.
+# This uses AJAX to determine the browser's timezone and may require more
+# server overhead because a Wicket session is created. All Gitblit pages
+# attempt to be stateless, if possible.
+#
+# SINCE 0.5.0
+# RESTART REQUIRED
+web.useClientTimezone = false
+
+# Time format
+# <http://download.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html>
+#
+# SINCE 0.8.0
+web.timeFormat = HH:mm
+
+# Short date format
+# <http://download.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html>
+#
+# SINCE 0.5.0
+web.datestampShortFormat = yyyy-MM-dd
+
+# Long date format
+#
+# SINCE 0.8.0
+web.datestampLongFormat = EEEE, MMMM d, yyyy
+
+# Long timestamp format
+# <http://download.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html>
+#
+# SINCE 0.5.0
+web.datetimestampLongFormat = EEEE, MMMM d, yyyy HH:mm Z
+
+# Mount URL parameters
+# This setting controls if pretty or parameter URLs are used.
+# i.e.
+# if true:
+# http://localhost/commit/myrepo/abcdef
+# if false:
+# http://localhost/commit/?r=myrepo&h=abcdef
+#
+# SINCE 0.5.0
+# RESTART REQUIRED
+web.mountParameters = true
+
+# Some servlet containers (e.g. Tomcat >= 6.0.10) disallow '/' (%2F) encoding
+# in URLs as a security precaution for proxies. This setting tells Gitblit
+# to preemptively replace '/' with '*' or '!' for url string parameters.
+#
+# <https://issues.apache.org/jira/browse/WICKET-1303>
+# <http://tomcat.apache.org/security-6.html#Fixed_in_Apache_Tomcat_6.0.10>
+# Add *-Dorg.apache.tomcat.util.buf.UDecoder.ALLOW_ENCODED_SLASH=true* to your
+# *CATALINA_OPTS* or to your JVM launch parameters
+#
+# SINCE 0.5.2
+web.forwardSlashCharacter = /
+
+# Show other URLs on the summary page for accessing your git repositories
+# Use spaces to separate urls. {0} is the token for the repository name.
+# e.g.
+# web.otherUrls = ssh://localhost/git/{0} git://localhost/git/{0}
+#
+# SPACE-DELIMITED
+# SINCE 0.5.0
+web.otherUrls =
+
+# Choose how to present the repositories list.
+# grouped = group nested/subfolder repositories together (no sorting)
+# flat = flat list of repositories (sorting allowed)
+#
+# SINCE 0.5.0
+web.repositoryListType = grouped
+
+# If using a grouped repository list and there are repositories at the
+# root level of your repositories folder, you may specify the displayed
+# group name with this setting. This value is only used for web presentation.
+#
+# SINCE 0.5.0
+web.repositoryRootGroupName = main
+
+# Display the repository swatch color next to the repository name link in the
+# repositories list.
+#
+# SINCE 0.8.0
+web.repositoryListSwatches = true
+
+# Choose the diff presentation style: gitblt, gitweb, or plain
+#
+# SINCE 0.5.0
+web.diffStyle = gitblit
+
+# Control if email addresses are shown in web ui
+#
+# SINCE 0.5.0
+web.showEmailAddresses = true
+
+# Shows a combobox in the page links header with commit, committer, and author
+# search selection. Default search is commit.
+#
+# SINCE 0.5.0
+web.showSearchTypeSelection = false
+
+# Generates a line graph of repository activity over time on the Summary page.
+# This uses the Google Charts API.
+#
+# SINCE 0.5.0
+web.generateActivityGraph = true
+
+# The number of days to show on the activity page.
+# Value must exceed 0 else default of 14 is used
+#
+# SINCE 0.8.0
+web.activityDuration = 14
+
+# The number of commits to display on the summary page
+# Value must exceed 0 else default of 20 is used
+#
+# SINCE 0.5.0
+web.summaryCommitCount = 16
+
+# The number of tags/branches to display on the summary page.
+# -1 = all tags/branches
+# 0 = hide tags/branches
+# N = N tags/branches
+#
+# SINCE 0.5.0
+web.summaryRefsCount = 5
+
+# The number of items to show on a page before showing the first, prev, next
+# pagination links. A default if 50 is used for any invalid value.
+#
+# SINCE 0.5.0
+web.itemsPerPage = 50
+
+# Registered file extensions to ignore during Lucene indexing
+#
+# SPACE-DELIMITED
+# SINCE 0.9.0
+web.luceneIgnoreExtensions = 7z arc arj bin bmp dll doc docx exe gif gz jar jpg lib lzh odg odf odt pdf ppt png so swf xcf xls xlsx zip
+
+# Registered extensions for google-code-prettify
+#
+# SPACE-DELIMITED
+# SINCE 0.5.0
+web.prettyPrintExtensions = c cpp cs css frm groovy htm html java js php pl prefs properties py rb scala sh sql xml vb
+
+# Registered extensions for markdown transformation
+#
+# SPACE-DELIMITED
+# CASE-SENSITIVE
+# SINCE 0.5.0
+web.markdownExtensions = md mkd markdown MD MKD
+
+# Image extensions
+#
+# SPACE-DELIMITED
+# SINCE 0.5.0
+web.imageExtensions = bmp jpg gif png
+
+# Registered extensions for binary blobs
+#
+# SPACE-DELIMITED
+# SINCE 0.5.0
+web.binaryExtensions = jar pdf tar.gz zip
+
+# Aggressive heap management will run the garbage collector on every generated
+# page. This slows down page generation a little but improves heap consumption.
+#
+# SINCE 0.5.0
+web.aggressiveHeapManagement = false
+
+# Run the webapp in debug mode
+#
+# SINCE 0.5.0
+# RESTART REQUIRED
+web.debugMode = false
+
+# Enable/disable global regex substitutions (i.e. shared across repositories)
+#
+# SINCE 0.5.0
+regex.global = true
+
+# Example global regex substitutions
+# Use !!! to separate the search pattern and the replace pattern
+# searchpattern!!!replacepattern
+# SINCE 0.5.0
+regex.global.bug = \\b(Bug:)(\\s*[#]?|-){0,1}(\\d+)\\b!!!<a href="http://somehost/bug/$3">Bug-Id: $3</a>
+# SINCE 0.5.0
+regex.global.changeid = \\b(Change-Id:\\s*)([A-Za-z0-9]*)\\b!!!<a href="http://somehost/changeid/$2">Change-Id: $2</a>
+
+# Example per-repository regex substitutions overrides global
+# SINCE 0.5.0
+regex.myrepository.bug = \\b(Bug:)(\\s*[#]?|-){0,1}(\\d+)\\b!!!<a href="http://elsewhere/bug/$3">Bug-Id: $3</a>
+
+#
+# Mail Settings
+# SINCE 0.6.0
+#
+# Mail settings are used to notify administrators of received federation proposals
+#
+
+# ip or hostname of smtp server
+#
+# SINCE 0.6.0
+mail.server =
+
+# port to use for smtp requests
+#
+# SINCE 0.6.0
+mail.port = 25
+
+# debug the mail executor
+#
+# SINCE 0.6.0
+mail.debug = false
+
+# if your smtp server requires authentication, supply the credentials here
+#
+# SINCE 0.6.0
+mail.username =
+# SINCE 0.6.0
+mail.password =
+
+# from address for generated emails
+#
+# SINCE 0.6.0
+mail.fromAddress =
+
+# List of email addresses for the Gitblit administrators
+#
+# SPACE-DELIMITED
+# SINCE 0.6.0
+mail.adminAddresses =
+
+# List of email addresses for sending push email notifications.
+#
+# This key currently requires use of the sendemail.groovy hook script.
+# If you set sendemail.groovy in *groovy.postReceiveScripts* then email
+# notifications for all repositories (regardless of access restrictions!)
+# will be sent to these addresses.
+#
+# SPACE-DELIMITED
+# SINCE 0.8.0
+mail.mailingLists =
+
+#
+# Federation Settings
+# SINCE 0.6.0
+#
+# A Gitblit federation is a way to backup one Gitblit instance to another.
+#
+# *git.enableGitServlet* must be true to use this feature.
+
+# Your federation name is used for federation status acknowledgments. If it is
+# unset, and you elect to send a status acknowledgment, your Gitblit instance
+# will be identified by its hostname, if available, else your internal ip address.
+# The source Gitblit instance will also append your external IP address to your
+# identification to differentiate multiple pulling systems behind a single proxy.
+#
+# SINCE 0.6.0
+federation.name =
+
+# Specify the passphrase of this Gitblit instance.
+#
+# An unspecified (empty) passphrase disables processing federation requests.
+#
+# This value can be anything you want: an integer, a sentence, an haiku, etc.
+# Keep the value simple, though, to avoid Java properties file encoding issues.
+#
+# Changing your passphrase will break any registrations you have established with other
+# Gitblit instances.
+#
+# CASE-SENSITIVE
+# SINCE 0.6.0
+# RESTART REQUIRED *(only to enable or disable federation)*
+federation.passphrase =
+
+# Control whether or not this Gitblit instance can receive federation proposals
+# from another Gitblit instance. Registering a federated Gitblit is a manual
+# process. Proposals help to simplify that process by allowing a remote Gitblit
+# instance to send your Gitblit instance the federation pull data.
+#
+# SINCE 0.6.0
+federation.allowProposals = false
+
+# The destination folder for cached federation proposals.
+# Use forward slashes even on Windows!!
+#
+# SINCE 0.6.0
+# BASEFOLDER
+federation.proposalsFolder = ${baseFolder}/proposals
+
+# The default pull frequency if frequency is unspecified on a registration
+#
+# SINCE 0.6.0
+federation.defaultFrequency = 60 mins
+
+# Federation Sets are named groups of repositories. The Federation Sets are
+# available for selection in the repository settings page. You can assign a
+# repository to one or more sets and then distribute the token for the set.
+# This allows you to grant federation pull access to a subset of your available
+# repositories. Tokens for federation sets only grant repository pull access.
+#
+# SPACE-DELIMITED
+# CASE-SENSITIVE
+# SINCE 0.6.0
+federation.sets =
+
+# Federation pull registrations
+# Registrations are read once, at startup.
+#
+# RESTART REQUIRED
+#
+# frequency:
+# The shortest frequency allowed is every 5 minutes
+# Decimal frequency values are cast to integers
+# Frequency values may be specified in mins, hours, or days
+# Values that can not be parsed or are unspecified default to *federation.defaultFrequency*
+#
+# folder:
+# if unspecified, the folder is *git.repositoriesFolder*
+# if specified, the folder is relative to *git.repositoriesFolder*
+#
+# bare:
+# if true, each repository will be created as a *bare* repository and will not
+# have a working directory.
+#
+# if false, each repository will be created as a normal repository suitable
+# for local work.
+#
+# mirror:
+# if true, each repository HEAD is reset to *origin/master* after each pull.
+# The repository will be flagged *isFrozen* after the initial clone.
+#
+# if false, each repository HEAD will point to the FETCH_HEAD of the initial
+# clone from the origin until pushed to or otherwise manipulated.
+#
+# mergeAccounts:
+# if true, remote accounts and their permissions are merged into your
+# users.properties file
+#
+# notifyOnError:
+# if true and the mail configuration is properly set, administrators will be
+# notified by email of pull failures
+#
+# include and exclude:
+# Space-delimited list of repositories to include or exclude from pull
+# may be * wildcard to include or exclude all
+# may use fuzzy match (e.g. org.eclipse.*)
+
+#
+# (Nearly) Perfect Mirror example
+#
+
+#federation.example1.url = https://go.gitblit.com
+#federation.example1.token = 6f3b8a24bf970f17289b234284c94f43eb42f0e4
+#federation.example1.frequency = 120 mins
+#federation.example1.folder =
+#federation.example1.bare = true
+#federation.example1.mirror = true
+#federation.example1.mergeAccounts = true
+
+#
+# Advanced Realm Settings
+#
+
+# URL of the LDAP server.
+# To use encrypted transport, use either ldaps:// URL for SSL or ldap+tls:// to
+# send StartTLS command.
+#
+# SINCE 1.0.0
+realm.ldap.server = ldap://localhost
+
+# Login username for LDAP searches.
+# If this value is unspecified, anonymous LDAP login will be used.
+#
+# e.g. mydomain\\username
+#
+# SINCE 1.0.0
+realm.ldap.username = cn=Directory Manager
+
+# Login password for LDAP searches.
+#
+# SINCE 1.0.0
+realm.ldap.password = password
+
+# The LdapUserService must be backed by another user service for standard user
+# and team management.
+# default: users.conf
+#
+# SINCE 1.0.0
+# RESTART REQUIRED
+# BASEFOLDER
+realm.ldap.backingUserService = ${baseFolder}/users.conf
+
+# Delegate team membership control to LDAP.
+#
+# If true, team user memberships will be specified by LDAP groups. This will
+# disable team selection in Edit User and user selection in Edit Team.
+#
+# If false, LDAP will only be used for authentication and Gitblit will maintain
+# team memberships with the *realm.ldap.backingUserService*.
+#
+# SINCE 1.0.0
+realm.ldap.maintainTeams = false
+
+# Root node for all LDAP users
+#
+# This is the root node from which subtree user searches will begin.
+# If blank, Gitblit will search ALL nodes.
+#
+# SINCE 1.0.0
+realm.ldap.accountBase = OU=Users,OU=UserControl,OU=MyOrganization,DC=MyDomain
+
+# Filter criteria for LDAP users
+#
+# Query pattern to use when searching for a user account. This may be any valid
+# LDAP query expression, including the standard (&) and (|) operators.
+#
+# Variables may be injected via the ${variableName} syntax.
+# Recognized variables are:
+# ${username} - The text entered as the user name
+#
+# SINCE 1.0.0
+realm.ldap.accountPattern = (&(objectClass=person)(sAMAccountName=${username}))
+
+# Root node for all LDAP groups to be used as Gitblit Teams
+#
+# This is the root node from which subtree team searches will begin.
+# If blank, Gitblit will search ALL nodes.
+#
+# SINCE 1.0.0
+realm.ldap.groupBase = OU=Groups,OU=UserControl,OU=MyOrganization,DC=MyDomain
+
+# Filter criteria for LDAP groups
+#
+# Query pattern to use when searching for a team. This may be any valid
+# LDAP query expression, including the standard (&) and (|) operators.
+#
+# Variables may be injected via the ${variableName} syntax.
+# Recognized variables are:
+# ${username} - The text entered as the user name
+# ${dn} - The Distinguished Name of the user logged in
+#
+# All attributes from the LDAP User record are available. For example, if a user
+# has an attribute "fullName" set to "John", "(fn=${fullName})" will be
+# translated to "(fn=John)".
+#
+# SINCE 1.0.0
+realm.ldap.groupMemberPattern = (&(objectClass=group)(member=${dn}))
+
+# LDAP users or groups that should be given administrator privileges.
+#
+# Teams are specified with a leading '@' character. Groups with spaces in the
+# name can be entered as "@team name".
+#
+# e.g. realm.ldap.admins = john @git_admins "@git admins"
+#
+# SPACE-DELIMITED
+# SINCE 1.0.0
+realm.ldap.admins = @Git_Admins
+
+# Attribute(s) on the USER record that indicate their display (or full) name.
+# Leave blank for no mapping available in LDAP.
+#
+# This may be a single attribute, or a string of multiple attributes. Examples:
+# displayName - Uses the attribute 'displayName' on the user record
+# ${personalTitle}. ${givenName} ${surname} - Will concatenate the 3
+# attributes together, with a '.' after personalTitle
+#
+# SINCE 1.0.0
+realm.ldap.displayName = displayName
+
+# Attribute(s) on the USER record that indicate their email address.
+# Leave blank for no mapping available in LDAP.
+#
+# This may be a single attribute, or a string of multiple attributes. Examples:
+# email - Uses the attribute 'email' on the user record
+# ${givenName}.${surname}@gitblit.com -Will concatenate the 2 attributes
+# together with a '.' and '@' creating something like first.last@gitblit.com
+#
+# SINCE 1.0.0
+realm.ldap.email = email
+
+# Defines the cache period to be used when caching LDAP queries. This is currently
+# only used for LDAP user synchronization.
+#
+# Must be of the form '<long> <TimeUnit>' where <TimeUnit> is one of 'MILLISECONDS', 'SECONDS', 'MINUTES', 'HOURS', 'DAYS'
+# default: 2 MINUTES
+#
+# RESTART REQUIRED
+realm.ldap.ldapCachePeriod = 2 MINUTES
+
+# Defines whether to synchronize all LDAP users into the backing user service
+#
+# Valid values: true, false
+# If left blank, false is assumed
+realm.ldap.synchronizeUsers.enable = false
+
+# Defines whether to delete non-existent LDAP users from the backing user service
+# during synchronization. depends on realm.ldap.synchronizeUsers.enable = true
+#
+# Valid values: true, false
+# If left blank, true is assumed
+realm.ldap.synchronizeUsers.removeDeleted = true
+
+# Attribute on the USER record that indicate their username to be used in gitblit
+# when synchronizing users from LDAP
+# if blank, Gitblit will use uid
+# For MS Active Directory this may be sAMAccountName
+realm.ldap.uid = uid
+
+# The RedmineUserService must be backed by another user service for standard user
+# and team management.
+# default: users.conf
+#
+# RESTART REQUIRED
+# BASEFOLDER
+realm.redmine.backingUserService = ${baseFolder}/users.conf
+
+# URL of the Redmine.
+realm.redmine.url = http://example.com/redmine
+
+#
+# Server Settings
+#
+
+# The temporary folder to decompress the embedded gitblit webapp.
+#
+# SINCE 0.5.0
+# RESTART REQUIRED
+# BASEFOLDER
+server.tempFolder = ${baseFolder}/temp
+
+# Use Jetty NIO connectors. If false, Jetty Socket connectors will be used.
+#
+# SINCE 0.5.0
+# RESTART REQUIRED
+server.useNio = true
+
+# Context path for the GO application. You might want to change the context
+# path if running Gitblit behind a proxy layer such as mod_proxy.
+#
+# SINCE 0.7.0
+# RESTART REQUIRED
+server.contextPath = /
+
+# Standard http port to serve. <= 0 disables this connector.
+# On Unix/Linux systems, ports < 1024 require root permissions.
+# Recommended value: 80 or 8080
+#
+# SINCE 0.5.0
+# RESTART REQUIRED
+server.httpPort = 0
+
+# Secure/SSL https port to serve. <= 0 disables this connector.
+# On Unix/Linux systems, ports < 1024 require root permissions.
+# Recommended value: 443 or 8443
+#
+# SINCE 0.5.0
+# RESTART REQUIRED
+server.httpsPort = 8443
+
+# Port for serving an Apache JServ Protocol (AJP) 1.3 connector for integrating
+# Gitblit GO into an Apache HTTP server setup. <= 0 disables this connector.
+# Recommended value: 8009
+#
+# SINCE 0.9.0
+# RESTART REQUIRED
+server.ajpPort = 0
+
+# Specify the interface for Jetty to bind the standard connector.
+# You may specify an ip or an empty value to bind to all interfaces.
+# Specifying localhost will result in Gitblit ONLY listening to requests to
+# localhost.
+#
+# SINCE 0.5.0
+# RESTART REQUIRED
+server.httpBindInterface = localhost
+
+# Specify the interface for Jetty to bind the secure connector.
+# You may specify an ip or an empty value to bind to all interfaces.
+# Specifying localhost will result in Gitblit ONLY listening to requests to
+# localhost.
+#
+# SINCE 0.5.0
+# RESTART REQUIRED
+server.httpsBindInterface = localhost
+
+# Specify the interface for Jetty to bind the AJP connector.
+# You may specify an ip or an empty value to bind to all interfaces.
+# Specifying localhost will result in Gitblit ONLY listening to requests to
+# localhost.
+#
+# SINCE 0.9.0
+# RESTART REQUIRED
+server.ajpBindInterface = localhost
+
+# Alias of certificate to use for https/SSL serving. If blank the first
+# certificate found in the keystore will be used.
+#
+# SINCE 1.2.0
+# RESTART REQUIRED
+server.certificateAlias = localhost
+
+# Password for SSL keystore.
+# Keystore password and certificate password must match.
+# This is provided for convenience, its probably more secure to set this value
+# using the --storePassword command line parameter.
+#
+# If you are using the official JRE or JDK from Oracle you may not have the
+# JCE Unlimited Strength Jurisdiction Policy files bundled with your JVM. Because
+# of this, your store/key password can not exceed 7 characters. If you require
+# longer passwords you may need to install the JCE Unlimited Strength Jurisdiction
+# Policy files from Oracle.
+#
+# http://www.oracle.com/technetwork/java/javase/downloads/index.html
+#
+# Gitblit and the Gitblit Certificate Authority will both indicate if Unlimited
+# Strength encryption is available.
+#
+# SINCE 0.5.0
+# RESTART REQUIRED
+server.storePassword = gitblit
+
+# If serving over https (recommended) you might consider requiring clients to
+# authenticate with ssl certificates. If enabled, only https clients with the
+# a valid client certificate will be able to access Gitblit.
+#
+# If disabled, client certificate authentication is optional and will be tried
+# first before falling-back to form authentication or basic authentication.
+#
+# Requiring client certificates to access any of Gitblit may be too extreme,
+# consider this carefully.
+#
+# SINCE 1.2.0
+# RESTART REQUIRED
+server.requireClientCertificates = false
+
+# Port for shutdown monitor to listen on.
+#
+# SINCE 0.5.0
+# RESTART REQUIRED
+server.shutdownPort = 8081
diff --git a/src/main/java/WEB-INF/web.xml b/src/main/java/WEB-INF/web.xml
new file mode 100644
index 00000000..a9436886
--- /dev/null
+++ b/src/main/java/WEB-INF/web.xml
@@ -0,0 +1,263 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<web-app version="2.4"
+ xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
+
+ <!-- The base folder is used to specify the root location of your Gitblit data.
+
+ ${baseFolder}/gitblit.properties
+ ${baseFolder}/users.conf
+ ${baseFolder}/projects.conf
+ ${baseFolder}/robots.txt
+ ${baseFolder}/git
+ ${baseFolder}/groovy
+ ${baseFolder}/groovy/grape
+ ${baseFolder}/proposals
+
+ By default, this location is WEB-INF/data. It is recommended to set this
+ path to a location outside your webapps folder that is writable by your
+ servlet container. Gitblit will copy the WEB-INF/data files to that
+ location for you when it restarts. This approach makes upgrading simpler.
+ All you have to do is set this parameter for the new release and then
+ review the defaults for any new settings. Settings are always versioned
+ with a SINCE x.y.z attribute and also noted in the release changelog.
+ -->
+ <context-param>
+ <param-name>baseFolder</param-name>
+ <param-value>${contextFolder}/WEB-INF/data</param-value>
+ </context-param>
+
+ <!-- Gitblit Displayname -->
+ <display-name>
+ Gitblit - @gb.version@
+ </display-name>
+
+ <!-- PARAMS -->
+
+ <!-- Gitblit Context Listener --><!-- STRIP
+ <listener>
+ <listener-class>com.gitblit.GitBlit</listener-class>
+ </listener>STRIP -->
+
+
+ <!-- Git Servlet
+ <url-pattern> MUST match:
+ * GitFilter
+ * com.gitblit.Constants.GIT_PATH
+ * Wicket Filter ignorePaths parameter -->
+ <servlet>
+ <servlet-name>GitServlet</servlet-name>
+ <servlet-class>com.gitblit.GitServlet</servlet-class>
+ </servlet>
+ <servlet-mapping>
+ <servlet-name>GitServlet</servlet-name>
+ <url-pattern>/git/*</url-pattern>
+ </servlet-mapping>
+
+
+ <!-- Syndication Servlet
+ <url-pattern> MUST match:
+ * SyndicationFilter
+ * com.gitblit.Constants.SYNDICATION_PATH
+ * Wicket Filter ignorePaths parameter -->
+ <servlet>
+ <servlet-name>SyndicationServlet</servlet-name>
+ <servlet-class>com.gitblit.SyndicationServlet</servlet-class>
+ </servlet>
+ <servlet-mapping>
+ <servlet-name>SyndicationServlet</servlet-name>
+ <url-pattern>/feed/*</url-pattern>
+ </servlet-mapping>
+
+
+ <!-- Zip Servlet
+ <url-pattern> MUST match:
+ * ZipServlet
+ * com.gitblit.Constants.ZIP_PATH
+ * Wicket Filter ignorePaths parameter -->
+ <servlet>
+ <servlet-name>ZipServlet</servlet-name>
+ <servlet-class>com.gitblit.DownloadZipServlet</servlet-class>
+ </servlet>
+ <servlet-mapping>
+ <servlet-name>ZipServlet</servlet-name>
+ <url-pattern>/zip/*</url-pattern>
+ </servlet-mapping>
+
+
+ <!-- Federation Servlet
+ <url-pattern> MUST match:
+ * com.gitblit.Constants.FEDERATION_PATH
+ * Wicket Filter ignorePaths parameter -->
+ <servlet>
+ <servlet-name>FederationServlet</servlet-name>
+ <servlet-class>com.gitblit.FederationServlet</servlet-class>
+ </servlet>
+ <servlet-mapping>
+ <servlet-name>FederationServlet</servlet-name>
+ <url-pattern>/federation/*</url-pattern>
+ </servlet-mapping>
+
+
+ <!-- Rpc Servlet
+ <url-pattern> MUST match:
+ * com.gitblit.Constants.RPC_PATH
+ * Wicket Filter ignorePaths parameter -->
+ <servlet>
+ <servlet-name>RpcServlet</servlet-name>
+ <servlet-class>com.gitblit.RpcServlet</servlet-class>
+ </servlet>
+ <servlet-mapping>
+ <servlet-name>RpcServlet</servlet-name>
+ <url-pattern>/rpc/*</url-pattern>
+ </servlet-mapping>
+
+
+ <!-- Pages Servlet
+ <url-pattern> MUST match:
+ * PagesFilter
+ * com.gitblit.Constants.PAGES_PATH
+ * Wicket Filter ignorePaths parameter -->
+ <servlet>
+ <servlet-name>PagesServlet</servlet-name>
+ <servlet-class>com.gitblit.PagesServlet</servlet-class>
+ </servlet>
+ <servlet-mapping>
+ <servlet-name>PagesServlet</servlet-name>
+ <url-pattern>/pages/*</url-pattern>
+ </servlet-mapping>
+
+
+ <!-- Robots.txt Servlet
+ <url-pattern> MUST match:
+ * Wicket Filter ignorePaths parameter -->
+ <servlet>
+ <servlet-name>RobotsTxtServlet</servlet-name>
+ <servlet-class>com.gitblit.RobotsTxtServlet</servlet-class>
+ </servlet>
+ <servlet-mapping>
+ <servlet-name>RobotsTxtServlet</servlet-name>
+ <url-pattern>/robots.txt</url-pattern>
+ </servlet-mapping>
+
+
+ <!-- Git Access Restriction Filter
+ <url-pattern> MUST match:
+ * GitServlet
+ * com.gitblit.Constants.GIT_PATH
+ * Wicket Filter ignorePaths parameter -->
+ <filter>
+ <filter-name>GitFilter</filter-name>
+ <filter-class>com.gitblit.GitFilter</filter-class>
+ </filter>
+ <filter-mapping>
+ <filter-name>GitFilter</filter-name>
+ <url-pattern>/git/*</url-pattern>
+ </filter-mapping>
+
+
+ <!-- Syndication Restriction Filter
+ <url-pattern> MUST match:
+ * SyndicationServlet
+ * com.gitblit.Constants.SYNDICATION_PATH
+ * Wicket Filter ignorePaths parameter -->
+ <filter>
+ <filter-name>SyndicationFilter</filter-name>
+ <filter-class>com.gitblit.SyndicationFilter</filter-class>
+ </filter>
+ <filter-mapping>
+ <filter-name>SyndicationFilter</filter-name>
+ <url-pattern>/feed/*</url-pattern>
+ </filter-mapping>
+
+
+ <!-- Download Zip Restriction Filter
+ <url-pattern> MUST match:
+ * DownloadZipServlet
+ * com.gitblit.Constants.ZIP_PATH
+ * Wicket Filter ignorePaths parameter -->
+ <filter>
+ <filter-name>ZipFilter</filter-name>
+ <filter-class>com.gitblit.DownloadZipFilter</filter-class>
+ </filter>
+ <filter-mapping>
+ <filter-name>ZipFilter</filter-name>
+ <url-pattern>/zip/*</url-pattern>
+ </filter-mapping>
+
+
+ <!-- Rpc Restriction Filter
+ <url-pattern> MUST match:
+ * RpcServlet
+ * com.gitblit.Constants.RPC_PATH
+ * Wicket Filter ignorePaths parameter -->
+ <filter>
+ <filter-name>RpcFilter</filter-name>
+ <filter-class>com.gitblit.RpcFilter</filter-class>
+ </filter>
+ <filter-mapping>
+ <filter-name>RpcFilter</filter-name>
+ <url-pattern>/rpc/*</url-pattern>
+ </filter-mapping>
+
+
+ <!-- Pges Restriction Filter
+ <url-pattern> MUST match:
+ * PagesServlet
+ * com.gitblit.Constants.PAGES_PATH
+ * Wicket Filter ignorePaths parameter -->
+ <filter>
+ <filter-name>PagesFilter</filter-name>
+ <filter-class>com.gitblit.PagesFilter</filter-class>
+ </filter>
+ <filter-mapping>
+ <filter-name>PagesFilter</filter-name>
+ <url-pattern>/pages/*</url-pattern>
+ </filter-mapping>
+
+ <filter>
+ <filter-name>EnforceAuthenticationFilter</filter-name>
+ <filter-class>com.gitblit.EnforceAuthenticationFilter</filter-class>
+ </filter>
+ <filter-mapping>
+ <filter-name>EnforceAuthenticationFilter</filter-name>
+ <url-pattern>/*</url-pattern>
+ </filter-mapping>
+
+
+ <!-- Wicket Filter -->
+ <filter>
+ <filter-name>wicketFilter</filter-name>
+ <filter-class>
+ org.apache.wicket.protocol.http.WicketFilter
+ </filter-class>
+ <init-param>
+ <param-name>applicationClassName</param-name>
+ <param-value>com.gitblit.wicket.GitBlitWebApp</param-value>
+ </init-param>
+ <init-param>
+ <param-name>ignorePaths</param-name>
+ <!-- Paths should match
+ * SyndicationFilter <url-pattern>
+ * SyndicationServlet <url-pattern>
+ * com.gitblit.Constants.SYNDICATION_PATH
+ * GitFilter <url-pattern>
+ * GitServlet <url-pattern>
+ * com.gitblit.Constants.GIT_PATH
+ * Zipfilter <url-pattern>
+ * ZipServlet <url-pattern>
+ * com.gitblit.Constants.ZIP_PATH
+ * FederationServlet <url-pattern>
+ * RpcFilter <url-pattern>
+ * RpcServlet <url-pattern>
+ * PagesFilter <url-pattern>
+ * PagesServlet <url-pattern>
+ * com.gitblit.Constants.PAGES_PATH -->
+ <param-value>git/,feed/,zip/,federation/,rpc/,pages/,robots.txt</param-value>
+ </init-param>
+ </filter>
+ <filter-mapping>
+ <filter-name>wicketFilter</filter-name>
+ <url-pattern>/*</url-pattern>
+ </filter-mapping>
+</web-app> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/.gitignore b/src/main/java/com/gitblit/.gitignore
new file mode 100644
index 00000000..c6298604
--- /dev/null
+++ b/src/main/java/com/gitblit/.gitignore
@@ -0,0 +1,2 @@
+/SettingKeys.java
+/Keys.java
diff --git a/src/main/java/com/gitblit/AccessRestrictionFilter.java b/src/main/java/com/gitblit/AccessRestrictionFilter.java
new file mode 100644
index 00000000..495d3438
--- /dev/null
+++ b/src/main/java/com/gitblit/AccessRestrictionFilter.java
@@ -0,0 +1,229 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit;
+
+import java.io.IOException;
+import java.text.MessageFormat;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.UserModel;
+import com.gitblit.utils.StringUtils;
+
+/**
+ * The AccessRestrictionFilter is an AuthenticationFilter that confirms that the
+ * requested repository can be accessed by the anonymous or named user.
+ *
+ * The filter extracts the name of the repository from the url and determines if
+ * the requested action for the repository requires a Basic authentication
+ * prompt. If authentication is required and no credentials are stored in the
+ * "Authorization" header, then a basic authentication challenge is issued.
+ *
+ * http://en.wikipedia.org/wiki/Basic_access_authentication
+ *
+ * @author James Moger
+ *
+ */
+public abstract class AccessRestrictionFilter extends AuthenticationFilter {
+
+ /**
+ * Extract the repository name from the url.
+ *
+ * @param url
+ * @return repository name
+ */
+ protected abstract String extractRepositoryName(String url);
+
+ /**
+ * Analyze the url and returns the action of the request.
+ *
+ * @param url
+ * @return action of the request
+ */
+ protected abstract String getUrlRequestAction(String url);
+
+ /**
+ * Determine if a non-existing repository can be created using this filter.
+ *
+ * @return true if the filter allows repository creation
+ */
+ protected abstract boolean isCreationAllowed();
+
+ /**
+ * Determine if the action may be executed on the repository.
+ *
+ * @param repository
+ * @param action
+ * @return true if the action may be performed
+ */
+ protected abstract boolean isActionAllowed(RepositoryModel repository, String action);
+
+ /**
+ * Determine if the repository requires authentication.
+ *
+ * @param repository
+ * @param action
+ * @return true if authentication required
+ */
+ protected abstract boolean requiresAuthentication(RepositoryModel repository, String action);
+
+ /**
+ * Determine if the user can access the repository and perform the specified
+ * action.
+ *
+ * @param repository
+ * @param user
+ * @param action
+ * @return true if user may execute the action on the repository
+ */
+ protected abstract boolean canAccess(RepositoryModel repository, UserModel user, String action);
+
+ /**
+ * Allows a filter to create a repository, if one does not exist.
+ *
+ * @param user
+ * @param repository
+ * @param action
+ * @return the repository model, if it is created, null otherwise
+ */
+ protected RepositoryModel createRepository(UserModel user, String repository, String action) {
+ return null;
+ }
+
+ /**
+ * doFilter does the actual work of preprocessing the request to ensure that
+ * the user may proceed.
+ *
+ * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest,
+ * javax.servlet.ServletResponse, javax.servlet.FilterChain)
+ */
+ @Override
+ public void doFilter(final ServletRequest request, final ServletResponse response,
+ final FilterChain chain) throws IOException, ServletException {
+
+ HttpServletRequest httpRequest = (HttpServletRequest) request;
+ HttpServletResponse httpResponse = (HttpServletResponse) response;
+
+ String fullUrl = getFullUrl(httpRequest);
+ String repository = extractRepositoryName(fullUrl);
+
+ if (GitBlit.self().isCollectingGarbage(repository)) {
+ logger.info(MessageFormat.format("ARF: Rejecting request for {0}, busy collecting garbage!", repository));
+ httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN);
+ return;
+ }
+
+ // Determine if the request URL is restricted
+ String fullSuffix = fullUrl.substring(repository.length());
+ String urlRequestType = getUrlRequestAction(fullSuffix);
+
+ UserModel user = getUser(httpRequest);
+
+ // Load the repository model
+ RepositoryModel model = GitBlit.self().getRepositoryModel(repository);
+ if (model == null) {
+ if (isCreationAllowed()) {
+ if (user == null) {
+ // challenge client to provide credentials for creation. send 401.
+ if (GitBlit.isDebugMode()) {
+ logger.info(MessageFormat.format("ARF: CREATE CHALLENGE {0}", fullUrl));
+ }
+ httpResponse.setHeader("WWW-Authenticate", CHALLENGE);
+ httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED);
+ return;
+ } else {
+ // see if we can create a repository for this request
+ model = createRepository(user, repository, urlRequestType);
+ }
+ }
+
+ if (model == null) {
+ // repository not found. send 404.
+ logger.info(MessageFormat.format("ARF: {0} ({1})", fullUrl,
+ HttpServletResponse.SC_NOT_FOUND));
+ httpResponse.sendError(HttpServletResponse.SC_NOT_FOUND);
+ return;
+ }
+ }
+
+ // Confirm that the action may be executed on the repository
+ if (!isActionAllowed(model, urlRequestType)) {
+ logger.info(MessageFormat.format("ARF: action {0} on {1} forbidden ({2})",
+ urlRequestType, model, HttpServletResponse.SC_FORBIDDEN));
+ httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN);
+ return;
+ }
+
+ // Wrap the HttpServletRequest with the AccessRestrictionRequest which
+ // overrides the servlet container user principal methods.
+ // JGit requires either:
+ //
+ // 1. servlet container authenticated user
+ // 2. http.receivepack = true in each repository's config
+ //
+ // Gitblit must conditionally authenticate users per-repository so just
+ // enabling http.receivepack is insufficient.
+ AuthenticatedRequest authenticatedRequest = new AuthenticatedRequest(httpRequest);
+ if (user != null) {
+ authenticatedRequest.setUser(user);
+ }
+
+ // BASIC authentication challenge and response processing
+ if (!StringUtils.isEmpty(urlRequestType) && requiresAuthentication(model, urlRequestType)) {
+ if (user == null) {
+ // challenge client to provide credentials. send 401.
+ if (GitBlit.isDebugMode()) {
+ logger.info(MessageFormat.format("ARF: CHALLENGE {0}", fullUrl));
+ }
+ httpResponse.setHeader("WWW-Authenticate", CHALLENGE);
+ httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED);
+ return;
+ } else {
+ // check user access for request
+ if (user.canAdmin() || canAccess(model, user, urlRequestType)) {
+ // authenticated request permitted.
+ // pass processing to the restricted servlet.
+ newSession(authenticatedRequest, httpResponse);
+ logger.info(MessageFormat.format("ARF: {0} ({1}) authenticated", fullUrl,
+ HttpServletResponse.SC_CONTINUE));
+ chain.doFilter(authenticatedRequest, httpResponse);
+ return;
+ }
+ // valid user, but not for requested access. send 403.
+ if (GitBlit.isDebugMode()) {
+ logger.info(MessageFormat.format("ARF: {0} forbidden to access {1}",
+ user.username, fullUrl));
+ }
+ httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN);
+ return;
+ }
+ }
+
+ if (GitBlit.isDebugMode()) {
+ logger.info(MessageFormat.format("ARF: {0} ({1}) unauthenticated", fullUrl,
+ HttpServletResponse.SC_CONTINUE));
+ }
+ // unauthenticated request permitted.
+ // pass processing to the restricted servlet.
+ chain.doFilter(authenticatedRequest, httpResponse);
+ }
+} \ No newline at end of file
diff --git a/src/main/java/com/gitblit/AddIndexedBranch.java b/src/main/java/com/gitblit/AddIndexedBranch.java
new file mode 100644
index 00000000..66997060
--- /dev/null
+++ b/src/main/java/com/gitblit/AddIndexedBranch.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2012 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit;
+
+import java.io.File;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
+
+import org.eclipse.jgit.lib.RepositoryCache.FileKey;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.storage.file.FileRepository;
+import org.eclipse.jgit.util.FS;
+
+import com.beust.jcommander.JCommander;
+import com.beust.jcommander.Parameter;
+import com.beust.jcommander.ParameterException;
+import com.beust.jcommander.Parameters;
+import com.gitblit.utils.ArrayUtils;
+import com.gitblit.utils.JGitUtils;
+import com.gitblit.utils.StringUtils;
+
+/**
+ * Utility class to add an indexBranch setting to matching repositories.
+ *
+ * @author James Moger
+ *
+ */
+public class AddIndexedBranch {
+
+ public static void main(String... args) {
+ Params params = new Params();
+ JCommander jc = new JCommander(params);
+ try {
+ jc.parse(args);
+ } catch (ParameterException t) {
+ System.err.println(t.getMessage());
+ jc.usage();
+ return;
+ }
+
+ // create a lowercase set of excluded repositories
+ Set<String> exclusions = new TreeSet<String>();
+ for (String exclude : params.exclusions) {
+ exclusions.add(exclude.toLowerCase());
+ }
+
+ // determine available repositories
+ File folder = new File(params.folder);
+ List<String> repoList = JGitUtils.getRepositoryList(folder, false, true, -1, null);
+
+ int modCount = 0;
+ int skipCount = 0;
+ for (String repo : repoList) {
+ boolean skip = false;
+ for (String exclusion : exclusions) {
+ if (StringUtils.fuzzyMatch(repo, exclusion)) {
+ skip = true;
+ break;
+ }
+ }
+
+ if (skip) {
+ System.out.println("skipping " + repo);
+ skipCount++;
+ continue;
+ }
+
+ System.out.println(MessageFormat.format("adding [gitblit] indexBranch={0} for {1}", params.branch, repo));
+ try {
+ // load repository config
+ File gitDir = FileKey.resolve(new File(folder, repo), FS.DETECTED);
+ FileRepository repository = new FileRepository(gitDir);
+ FileBasedConfig config = repository.getConfig();
+ config.load();
+
+ Set<String> indexedBranches = new LinkedHashSet<String>();
+ indexedBranches.add(Constants.DEFAULT_BRANCH);
+
+ String [] branches = config.getStringList("gitblit", null, "indexBranch");
+ if (!ArrayUtils.isEmpty(branches)) {
+ for (String branch : branches) {
+ indexedBranches.add(branch);
+ }
+ }
+ config.setStringList("gitblit", null, "indexBranch", new ArrayList<String>(indexedBranches));
+ config.save();
+ modCount++;
+ } catch (Exception e) {
+ System.err.println(repo);
+ e.printStackTrace();
+ }
+ }
+
+ System.out.println(MessageFormat.format("updated {0} repository configurations, skipped {1}", modCount, skipCount));
+ }
+
+
+
+ /**
+ * JCommander Parameters class for AddIndexedBranch.
+ */
+ @Parameters(separators = " ")
+ private static class Params {
+
+ @Parameter(names = { "--repositoriesFolder" }, description = "The root repositories folder ", required = true)
+ public String folder;
+
+ @Parameter(names = { "--branch" }, description = "The branch to index", required = true)
+ public String branch = "default";
+
+ @Parameter(names = { "--skip" }, description = "Skip the named repository (simple fizzy matching is supported)", required = false)
+ public List<String> exclusions = new ArrayList<String>();
+ }
+}
diff --git a/src/main/java/com/gitblit/AuthenticationFilter.java b/src/main/java/com/gitblit/AuthenticationFilter.java
new file mode 100644
index 00000000..eb6e95b7
--- /dev/null
+++ b/src/main/java/com/gitblit/AuthenticationFilter.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit;
+
+import java.io.IOException;
+import java.security.Principal;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.models.UserModel;
+import com.gitblit.utils.StringUtils;
+
+/**
+ * The AuthenticationFilter is a servlet filter that preprocesses requests that
+ * match its url pattern definition in the web.xml file.
+ *
+ * http://en.wikipedia.org/wiki/Basic_access_authentication
+ *
+ * @author James Moger
+ *
+ */
+public abstract class AuthenticationFilter implements Filter {
+
+ protected static final String CHALLENGE = "Basic realm=\"" + Constants.NAME + "\"";
+
+ protected static final String SESSION_SECURED = "com.gitblit.secured";
+
+ protected transient Logger logger = LoggerFactory.getLogger(getClass());
+
+ /**
+ * doFilter does the actual work of preprocessing the request to ensure that
+ * the user may proceed.
+ *
+ * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest,
+ * javax.servlet.ServletResponse, javax.servlet.FilterChain)
+ */
+ @Override
+ public abstract void doFilter(final ServletRequest request, final ServletResponse response,
+ final FilterChain chain) throws IOException, ServletException;
+
+ /**
+ * Allow the filter to require a client certificate to continue processing.
+ *
+ * @return true, if a client certificate is required
+ */
+ protected boolean requiresClientCertificate() {
+ return false;
+ }
+
+ /**
+ * Returns the full relative url of the request.
+ *
+ * @param httpRequest
+ * @return url
+ */
+ protected String getFullUrl(HttpServletRequest httpRequest) {
+ String servletUrl = httpRequest.getContextPath() + httpRequest.getServletPath();
+ String url = httpRequest.getRequestURI().substring(servletUrl.length());
+ String params = httpRequest.getQueryString();
+ if (url.length() > 0 && url.charAt(0) == '/') {
+ url = url.substring(1);
+ }
+ String fullUrl = url + (StringUtils.isEmpty(params) ? "" : ("?" + params));
+ return fullUrl;
+ }
+
+ /**
+ * Returns the user making the request, if the user has authenticated.
+ *
+ * @param httpRequest
+ * @return user
+ */
+ protected UserModel getUser(HttpServletRequest httpRequest) {
+ UserModel user = GitBlit.self().authenticate(httpRequest, requiresClientCertificate());
+ return user;
+ }
+
+ /**
+ * Taken from Jetty's LoginAuthenticator.renewSessionOnAuthentication()
+ */
+ @SuppressWarnings("unchecked")
+ protected void newSession(HttpServletRequest request, HttpServletResponse response) {
+ HttpSession oldSession = request.getSession(false);
+ if (oldSession != null && oldSession.getAttribute(SESSION_SECURED) == null) {
+ synchronized (this) {
+ Map<String, Object> attributes = new HashMap<String, Object>();
+ Enumeration<String> e = oldSession.getAttributeNames();
+ while (e.hasMoreElements()) {
+ String name = e.nextElement();
+ attributes.put(name, oldSession.getAttribute(name));
+ oldSession.removeAttribute(name);
+ }
+ oldSession.invalidate();
+
+ HttpSession newSession = request.getSession(true);
+ newSession.setAttribute(SESSION_SECURED, Boolean.TRUE);
+ for (Map.Entry<String, Object> entry : attributes.entrySet()) {
+ newSession.setAttribute(entry.getKey(), entry.getValue());
+ }
+ }
+ }
+ }
+
+ /**
+ * @see javax.servlet.Filter#init(javax.servlet.FilterConfig)
+ */
+ @Override
+ public void init(final FilterConfig config) throws ServletException {
+ }
+
+ /**
+ * @see javax.servlet.Filter#destroy()
+ */
+ @Override
+ public void destroy() {
+ }
+
+ /**
+ * Wraps a standard HttpServletRequest and overrides user principal methods.
+ */
+ public static class AuthenticatedRequest extends ServletRequestWrapper {
+
+ private UserModel user;
+
+ public AuthenticatedRequest(HttpServletRequest req) {
+ super(req);
+ user = new UserModel("anonymous");
+ user.isAuthenticated = false;
+ }
+
+ UserModel getUser() {
+ return user;
+ }
+
+ void setUser(UserModel user) {
+ this.user = user;
+ }
+
+ @Override
+ public String getRemoteUser() {
+ return user.username;
+ }
+
+ @Override
+ public boolean isUserInRole(String role) {
+ if (role.equals(Constants.ADMIN_ROLE)) {
+ return user.canAdmin();
+ }
+ // Gitblit does not currently use actual roles in the traditional
+ // servlet container sense. That is the reason this is marked
+ // deprecated, but I may want to revisit this.
+ return user.canAccessRepository(role);
+ }
+
+ @Override
+ public Principal getUserPrincipal() {
+ return user;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main/java/com/gitblit/ConfigUserService.java b/src/main/java/com/gitblit/ConfigUserService.java
new file mode 100644
index 00000000..7aa09985
--- /dev/null
+++ b/src/main/java/com/gitblit/ConfigUserService.java
@@ -0,0 +1,1078 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit;
+
+import java.io.File;
+import java.io.IOException;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.eclipse.jgit.lib.StoredConfig;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.Constants.AccessPermission;
+import com.gitblit.models.TeamModel;
+import com.gitblit.models.UserModel;
+import com.gitblit.utils.ArrayUtils;
+import com.gitblit.utils.DeepCopier;
+import com.gitblit.utils.StringUtils;
+
+/**
+ * ConfigUserService is Gitblit's default user service implementation since
+ * version 0.8.0.
+ *
+ * Users and their repository memberships are stored in a git-style config file
+ * which is cached and dynamically reloaded when modified. This file is
+ * plain-text, human-readable, and may be edited with a text editor.
+ *
+ * Additionally, this format allows for expansion of the user model without
+ * bringing in the complexity of a database.
+ *
+ * @author James Moger
+ *
+ */
+public class ConfigUserService implements IUserService {
+
+ private static final String TEAM = "team";
+
+ private static final String USER = "user";
+
+ private static final String PASSWORD = "password";
+
+ private static final String DISPLAYNAME = "displayName";
+
+ private static final String EMAILADDRESS = "emailAddress";
+
+ private static final String ORGANIZATIONALUNIT = "organizationalUnit";
+
+ private static final String ORGANIZATION = "organization";
+
+ private static final String LOCALITY = "locality";
+
+ private static final String STATEPROVINCE = "stateProvince";
+
+ private static final String COUNTRYCODE = "countryCode";
+
+ private static final String COOKIE = "cookie";
+
+ private static final String REPOSITORY = "repository";
+
+ private static final String ROLE = "role";
+
+ private static final String MAILINGLIST = "mailingList";
+
+ private static final String PRERECEIVE = "preReceiveScript";
+
+ private static final String POSTRECEIVE = "postReceiveScript";
+
+ private final File realmFile;
+
+ private final Logger logger = LoggerFactory.getLogger(ConfigUserService.class);
+
+ private final Map<String, UserModel> users = new ConcurrentHashMap<String, UserModel>();
+
+ private final Map<String, UserModel> cookies = new ConcurrentHashMap<String, UserModel>();
+
+ private final Map<String, TeamModel> teams = new ConcurrentHashMap<String, TeamModel>();
+
+ private volatile long lastModified;
+
+ private volatile boolean forceReload;
+
+ public ConfigUserService(File realmFile) {
+ this.realmFile = realmFile;
+ }
+
+ /**
+ * Setup the user service.
+ *
+ * @param settings
+ * @since 0.7.0
+ */
+ @Override
+ public void setup(IStoredSettings settings) {
+ }
+
+ /**
+ * Does the user service support changes to credentials?
+ *
+ * @return true or false
+ * @since 1.0.0
+ */
+ @Override
+ public boolean supportsCredentialChanges() {
+ return true;
+ }
+
+ /**
+ * Does the user service support changes to user display name?
+ *
+ * @return true or false
+ * @since 1.0.0
+ */
+ @Override
+ public boolean supportsDisplayNameChanges() {
+ return true;
+ }
+
+ /**
+ * Does the user service support changes to user email address?
+ *
+ * @return true or false
+ * @since 1.0.0
+ */
+ @Override
+ public boolean supportsEmailAddressChanges() {
+ return true;
+ }
+
+ /**
+ * Does the user service support changes to team memberships?
+ *
+ * @return true or false
+ * @since 1.0.0
+ */
+ public boolean supportsTeamMembershipChanges() {
+ return true;
+ }
+
+ /**
+ * Does the user service support cookie authentication?
+ *
+ * @return true or false
+ */
+ @Override
+ public boolean supportsCookies() {
+ return true;
+ }
+
+ /**
+ * Returns the cookie value for the specified user.
+ *
+ * @param model
+ * @return cookie value
+ */
+ @Override
+ public String getCookie(UserModel model) {
+ if (!StringUtils.isEmpty(model.cookie)) {
+ return model.cookie;
+ }
+ read();
+ UserModel storedModel = users.get(model.username.toLowerCase());
+ return storedModel.cookie;
+ }
+
+ /**
+ * Authenticate a user based on their cookie.
+ *
+ * @param cookie
+ * @return a user object or null
+ */
+ @Override
+ public UserModel authenticate(char[] cookie) {
+ String hash = new String(cookie);
+ if (StringUtils.isEmpty(hash)) {
+ return null;
+ }
+ read();
+ UserModel model = null;
+ if (cookies.containsKey(hash)) {
+ model = cookies.get(hash);
+ }
+ return model;
+ }
+
+ /**
+ * Authenticate a user based on a username and password.
+ *
+ * @param username
+ * @param password
+ * @return a user object or null
+ */
+ @Override
+ public UserModel authenticate(String username, char[] password) {
+ read();
+ UserModel returnedUser = null;
+ UserModel user = getUserModel(username);
+ if (user == null) {
+ return null;
+ }
+ if (user.password.startsWith(StringUtils.MD5_TYPE)) {
+ // password digest
+ String md5 = StringUtils.MD5_TYPE + StringUtils.getMD5(new String(password));
+ if (user.password.equalsIgnoreCase(md5)) {
+ returnedUser = user;
+ }
+ } else if (user.password.startsWith(StringUtils.COMBINED_MD5_TYPE)) {
+ // username+password digest
+ String md5 = StringUtils.COMBINED_MD5_TYPE
+ + StringUtils.getMD5(username.toLowerCase() + new String(password));
+ if (user.password.equalsIgnoreCase(md5)) {
+ returnedUser = user;
+ }
+ } else if (user.password.equals(new String(password))) {
+ // plain-text password
+ returnedUser = user;
+ }
+ return returnedUser;
+ }
+
+ /**
+ * Logout a user.
+ *
+ * @param user
+ */
+ @Override
+ public void logout(UserModel user) {
+ }
+
+ /**
+ * Retrieve the user object for the specified username.
+ *
+ * @param username
+ * @return a user object or null
+ */
+ @Override
+ public UserModel getUserModel(String username) {
+ read();
+ UserModel model = users.get(username.toLowerCase());
+ if (model != null) {
+ // clone the model, otherwise all changes to this object are
+ // live and unpersisted
+ model = DeepCopier.copy(model);
+ }
+ return model;
+ }
+
+ /**
+ * Updates/writes a complete user object.
+ *
+ * @param model
+ * @return true if update is successful
+ */
+ @Override
+ public boolean updateUserModel(UserModel model) {
+ return updateUserModel(model.username, model);
+ }
+
+ /**
+ * Updates/writes all specified user objects.
+ *
+ * @param models a list of user models
+ * @return true if update is successful
+ * @since 1.2.0
+ */
+ @Override
+ public boolean updateUserModels(Collection<UserModel> models) {
+ try {
+ read();
+ for (UserModel model : models) {
+ UserModel originalUser = users.remove(model.username.toLowerCase());
+ users.put(model.username.toLowerCase(), model);
+ // null check on "final" teams because JSON-sourced UserModel
+ // can have a null teams object
+ if (model.teams != null) {
+ for (TeamModel team : model.teams) {
+ TeamModel t = teams.get(team.name.toLowerCase());
+ if (t == null) {
+ // new team
+ team.addUser(model.username);
+ teams.put(team.name.toLowerCase(), team);
+ } else {
+ // do not clobber existing team definition
+ // maybe because this is a federated user
+ t.addUser(model.username);
+ }
+ }
+
+ // check for implicit team removal
+ if (originalUser != null) {
+ for (TeamModel team : originalUser.teams) {
+ if (!model.isTeamMember(team.name)) {
+ team.removeUser(model.username);
+ }
+ }
+ }
+ }
+ }
+ write();
+ return true;
+ } catch (Throwable t) {
+ logger.error(MessageFormat.format("Failed to update user {0} models!", models.size()),
+ t);
+ }
+ return false;
+ }
+
+ /**
+ * Updates/writes and replaces a complete user object keyed by username.
+ * This method allows for renaming a user.
+ *
+ * @param username
+ * the old username
+ * @param model
+ * the user object to use for username
+ * @return true if update is successful
+ */
+ @Override
+ public boolean updateUserModel(String username, UserModel model) {
+ UserModel originalUser = null;
+ try {
+ read();
+ originalUser = users.remove(username.toLowerCase());
+ users.put(model.username.toLowerCase(), model);
+ // null check on "final" teams because JSON-sourced UserModel
+ // can have a null teams object
+ if (model.teams != null) {
+ for (TeamModel team : model.teams) {
+ TeamModel t = teams.get(team.name.toLowerCase());
+ if (t == null) {
+ // new team
+ team.addUser(username);
+ teams.put(team.name.toLowerCase(), team);
+ } else {
+ // do not clobber existing team definition
+ // maybe because this is a federated user
+ t.removeUser(username);
+ t.addUser(model.username);
+ }
+ }
+
+ // check for implicit team removal
+ if (originalUser != null) {
+ for (TeamModel team : originalUser.teams) {
+ if (!model.isTeamMember(team.name)) {
+ team.removeUser(username);
+ }
+ }
+ }
+ }
+ write();
+ return true;
+ } catch (Throwable t) {
+ if (originalUser != null) {
+ // restore original user
+ users.put(originalUser.username.toLowerCase(), originalUser);
+ } else {
+ // drop attempted add
+ users.remove(model.username.toLowerCase());
+ }
+ logger.error(MessageFormat.format("Failed to update user model {0}!", model.username),
+ t);
+ }
+ return false;
+ }
+
+ /**
+ * Deletes the user object from the user service.
+ *
+ * @param model
+ * @return true if successful
+ */
+ @Override
+ public boolean deleteUserModel(UserModel model) {
+ return deleteUser(model.username);
+ }
+
+ /**
+ * Delete the user object with the specified username
+ *
+ * @param username
+ * @return true if successful
+ */
+ @Override
+ public boolean deleteUser(String username) {
+ try {
+ // Read realm file
+ read();
+ UserModel model = users.remove(username.toLowerCase());
+ if (model == null) {
+ // user does not exist
+ return false;
+ }
+ // remove user from team
+ for (TeamModel team : model.teams) {
+ TeamModel t = teams.get(team.name);
+ if (t == null) {
+ // new team
+ team.removeUser(username);
+ teams.put(team.name.toLowerCase(), team);
+ } else {
+ // existing team
+ t.removeUser(username);
+ }
+ }
+ write();
+ return true;
+ } catch (Throwable t) {
+ logger.error(MessageFormat.format("Failed to delete user {0}!", username), t);
+ }
+ return false;
+ }
+
+ /**
+ * Returns the list of all teams available to the login service.
+ *
+ * @return list of all teams
+ * @since 0.8.0
+ */
+ @Override
+ public List<String> getAllTeamNames() {
+ read();
+ List<String> list = new ArrayList<String>(teams.keySet());
+ Collections.sort(list);
+ return list;
+ }
+
+ /**
+ * Returns the list of all teams available to the login service.
+ *
+ * @return list of all teams
+ * @since 0.8.0
+ */
+ @Override
+ public List<TeamModel> getAllTeams() {
+ read();
+ List<TeamModel> list = new ArrayList<TeamModel>(teams.values());
+ list = DeepCopier.copy(list);
+ Collections.sort(list);
+ return list;
+ }
+
+ /**
+ * Returns the list of all users who are allowed to bypass the access
+ * restriction placed on the specified repository.
+ *
+ * @param role
+ * the repository name
+ * @return list of all usernames that can bypass the access restriction
+ */
+ @Override
+ public List<String> getTeamnamesForRepositoryRole(String role) {
+ List<String> list = new ArrayList<String>();
+ try {
+ read();
+ for (Map.Entry<String, TeamModel> entry : teams.entrySet()) {
+ TeamModel model = entry.getValue();
+ if (model.hasRepositoryPermission(role)) {
+ list.add(model.name);
+ }
+ }
+ } catch (Throwable t) {
+ logger.error(MessageFormat.format("Failed to get teamnames for role {0}!", role), t);
+ }
+ Collections.sort(list);
+ return list;
+ }
+
+ /**
+ * Sets the list of all teams who are allowed to bypass the access
+ * restriction placed on the specified repository.
+ *
+ * @param role
+ * the repository name
+ * @param teamnames
+ * @return true if successful
+ */
+ @Override
+ public boolean setTeamnamesForRepositoryRole(String role, List<String> teamnames) {
+ try {
+ Set<String> specifiedTeams = new HashSet<String>();
+ for (String teamname : teamnames) {
+ specifiedTeams.add(teamname.toLowerCase());
+ }
+
+ read();
+
+ // identify teams which require add or remove role
+ for (TeamModel team : teams.values()) {
+ // team has role, check against revised team list
+ if (specifiedTeams.contains(team.name.toLowerCase())) {
+ team.addRepositoryPermission(role);
+ } else {
+ // remove role from team
+ team.removeRepositoryPermission(role);
+ }
+ }
+
+ // persist changes
+ write();
+ return true;
+ } catch (Throwable t) {
+ logger.error(MessageFormat.format("Failed to set teams for role {0}!", role), t);
+ }
+ return false;
+ }
+
+ /**
+ * Retrieve the team object for the specified team name.
+ *
+ * @param teamname
+ * @return a team object or null
+ * @since 0.8.0
+ */
+ @Override
+ public TeamModel getTeamModel(String teamname) {
+ read();
+ TeamModel model = teams.get(teamname.toLowerCase());
+ if (model != null) {
+ // clone the model, otherwise all changes to this object are
+ // live and unpersisted
+ model = DeepCopier.copy(model);
+ }
+ return model;
+ }
+
+ /**
+ * Updates/writes a complete team object.
+ *
+ * @param model
+ * @return true if update is successful
+ * @since 0.8.0
+ */
+ @Override
+ public boolean updateTeamModel(TeamModel model) {
+ return updateTeamModel(model.name, model);
+ }
+
+ /**
+ * Updates/writes all specified team objects.
+ *
+ * @param models a list of team models
+ * @return true if update is successful
+ * @since 1.2.0
+ */
+ @Override
+ public boolean updateTeamModels(Collection<TeamModel> models) {
+ try {
+ read();
+ for (TeamModel team : models) {
+ teams.put(team.name.toLowerCase(), team);
+ }
+ write();
+ return true;
+ } catch (Throwable t) {
+ logger.error(MessageFormat.format("Failed to update team {0} models!", models.size()), t);
+ }
+ return false;
+ }
+
+ /**
+ * Updates/writes and replaces a complete team object keyed by teamname.
+ * This method allows for renaming a team.
+ *
+ * @param teamname
+ * the old teamname
+ * @param model
+ * the team object to use for teamname
+ * @return true if update is successful
+ * @since 0.8.0
+ */
+ @Override
+ public boolean updateTeamModel(String teamname, TeamModel model) {
+ TeamModel original = null;
+ try {
+ read();
+ original = teams.remove(teamname.toLowerCase());
+ teams.put(model.name.toLowerCase(), model);
+ write();
+ return true;
+ } catch (Throwable t) {
+ if (original != null) {
+ // restore original team
+ teams.put(original.name.toLowerCase(), original);
+ } else {
+ // drop attempted add
+ teams.remove(model.name.toLowerCase());
+ }
+ logger.error(MessageFormat.format("Failed to update team model {0}!", model.name), t);
+ }
+ return false;
+ }
+
+ /**
+ * Deletes the team object from the user service.
+ *
+ * @param model
+ * @return true if successful
+ * @since 0.8.0
+ */
+ @Override
+ public boolean deleteTeamModel(TeamModel model) {
+ return deleteTeam(model.name);
+ }
+
+ /**
+ * Delete the team object with the specified teamname
+ *
+ * @param teamname
+ * @return true if successful
+ * @since 0.8.0
+ */
+ @Override
+ public boolean deleteTeam(String teamname) {
+ try {
+ // Read realm file
+ read();
+ teams.remove(teamname.toLowerCase());
+ write();
+ return true;
+ } catch (Throwable t) {
+ logger.error(MessageFormat.format("Failed to delete team {0}!", teamname), t);
+ }
+ return false;
+ }
+
+ /**
+ * Returns the list of all users available to the login service.
+ *
+ * @return list of all usernames
+ */
+ @Override
+ public List<String> getAllUsernames() {
+ read();
+ List<String> list = new ArrayList<String>(users.keySet());
+ Collections.sort(list);
+ return list;
+ }
+
+ /**
+ * Returns the list of all users available to the login service.
+ *
+ * @return list of all usernames
+ */
+ @Override
+ public List<UserModel> getAllUsers() {
+ read();
+ List<UserModel> list = new ArrayList<UserModel>(users.values());
+ list = DeepCopier.copy(list);
+ Collections.sort(list);
+ return list;
+ }
+
+ /**
+ * Returns the list of all users who are allowed to bypass the access
+ * restriction placed on the specified repository.
+ *
+ * @param role
+ * the repository name
+ * @return list of all usernames that can bypass the access restriction
+ */
+ @Override
+ public List<String> getUsernamesForRepositoryRole(String role) {
+ List<String> list = new ArrayList<String>();
+ try {
+ read();
+ for (Map.Entry<String, UserModel> entry : users.entrySet()) {
+ UserModel model = entry.getValue();
+ if (model.hasRepositoryPermission(role)) {
+ list.add(model.username);
+ }
+ }
+ } catch (Throwable t) {
+ logger.error(MessageFormat.format("Failed to get usernames for role {0}!", role), t);
+ }
+ Collections.sort(list);
+ return list;
+ }
+
+ /**
+ * Sets the list of all uses who are allowed to bypass the access
+ * restriction placed on the specified repository.
+ *
+ * @param role
+ * the repository name
+ * @param usernames
+ * @return true if successful
+ */
+ @Override
+ @Deprecated
+ public boolean setUsernamesForRepositoryRole(String role, List<String> usernames) {
+ try {
+ Set<String> specifiedUsers = new HashSet<String>();
+ for (String username : usernames) {
+ specifiedUsers.add(username.toLowerCase());
+ }
+
+ read();
+
+ // identify users which require add or remove role
+ for (UserModel user : users.values()) {
+ // user has role, check against revised user list
+ if (specifiedUsers.contains(user.username.toLowerCase())) {
+ user.addRepositoryPermission(role);
+ } else {
+ // remove role from user
+ user.removeRepositoryPermission(role);
+ }
+ }
+
+ // persist changes
+ write();
+ return true;
+ } catch (Throwable t) {
+ logger.error(MessageFormat.format("Failed to set usernames for role {0}!", role), t);
+ }
+ return false;
+ }
+
+ /**
+ * Renames a repository role.
+ *
+ * @param oldRole
+ * @param newRole
+ * @return true if successful
+ */
+ @Override
+ public boolean renameRepositoryRole(String oldRole, String newRole) {
+ try {
+ read();
+ // identify users which require role rename
+ for (UserModel model : users.values()) {
+ if (model.hasRepositoryPermission(oldRole)) {
+ AccessPermission permission = model.removeRepositoryPermission(oldRole);
+ model.setRepositoryPermission(newRole, permission);
+ }
+ }
+
+ // identify teams which require role rename
+ for (TeamModel model : teams.values()) {
+ if (model.hasRepositoryPermission(oldRole)) {
+ AccessPermission permission = model.removeRepositoryPermission(oldRole);
+ model.setRepositoryPermission(newRole, permission);
+ }
+ }
+ // persist changes
+ write();
+ return true;
+ } catch (Throwable t) {
+ logger.error(
+ MessageFormat.format("Failed to rename role {0} to {1}!", oldRole, newRole), t);
+ }
+ return false;
+ }
+
+ /**
+ * Removes a repository role from all users.
+ *
+ * @param role
+ * @return true if successful
+ */
+ @Override
+ public boolean deleteRepositoryRole(String role) {
+ try {
+ read();
+
+ // identify users which require role rename
+ for (UserModel user : users.values()) {
+ user.removeRepositoryPermission(role);
+ }
+
+ // identify teams which require role rename
+ for (TeamModel team : teams.values()) {
+ team.removeRepositoryPermission(role);
+ }
+
+ // persist changes
+ write();
+ return true;
+ } catch (Throwable t) {
+ logger.error(MessageFormat.format("Failed to delete role {0}!", role), t);
+ }
+ return false;
+ }
+
+ /**
+ * Writes the properties file.
+ *
+ * @throws IOException
+ */
+ private synchronized void write() throws IOException {
+ // Write a temporary copy of the users file
+ File realmFileCopy = new File(realmFile.getAbsolutePath() + ".tmp");
+
+ StoredConfig config = new FileBasedConfig(realmFileCopy, FS.detect());
+
+ // write users
+ for (UserModel model : users.values()) {
+ if (!StringUtils.isEmpty(model.password)) {
+ config.setString(USER, model.username, PASSWORD, model.password);
+ }
+ if (!StringUtils.isEmpty(model.cookie)) {
+ config.setString(USER, model.username, COOKIE, model.cookie);
+ }
+ if (!StringUtils.isEmpty(model.displayName)) {
+ config.setString(USER, model.username, DISPLAYNAME, model.displayName);
+ }
+ if (!StringUtils.isEmpty(model.emailAddress)) {
+ config.setString(USER, model.username, EMAILADDRESS, model.emailAddress);
+ }
+ if (!StringUtils.isEmpty(model.organizationalUnit)) {
+ config.setString(USER, model.username, ORGANIZATIONALUNIT, model.organizationalUnit);
+ }
+ if (!StringUtils.isEmpty(model.organization)) {
+ config.setString(USER, model.username, ORGANIZATION, model.organization);
+ }
+ if (!StringUtils.isEmpty(model.locality)) {
+ config.setString(USER, model.username, LOCALITY, model.locality);
+ }
+ if (!StringUtils.isEmpty(model.stateProvince)) {
+ config.setString(USER, model.username, STATEPROVINCE, model.stateProvince);
+ }
+ if (!StringUtils.isEmpty(model.countryCode)) {
+ config.setString(USER, model.username, COUNTRYCODE, model.countryCode);
+ }
+
+ // user roles
+ List<String> roles = new ArrayList<String>();
+ if (model.canAdmin) {
+ roles.add(Constants.ADMIN_ROLE);
+ }
+ if (model.canFork) {
+ roles.add(Constants.FORK_ROLE);
+ }
+ if (model.canCreate) {
+ roles.add(Constants.CREATE_ROLE);
+ }
+ if (model.excludeFromFederation) {
+ roles.add(Constants.NOT_FEDERATED_ROLE);
+ }
+ if (roles.size() == 0) {
+ // we do this to ensure that user record with no password
+ // is written. otherwise, StoredConfig optimizes that account
+ // away. :(
+ roles.add(Constants.NO_ROLE);
+ }
+ config.setStringList(USER, model.username, ROLE, roles);
+
+ // discrete repository permissions
+ if (model.permissions != null && !model.canAdmin) {
+ List<String> permissions = new ArrayList<String>();
+ for (Map.Entry<String, AccessPermission> entry : model.permissions.entrySet()) {
+ if (entry.getValue().exceeds(AccessPermission.NONE)) {
+ permissions.add(entry.getValue().asRole(entry.getKey()));
+ }
+ }
+ config.setStringList(USER, model.username, REPOSITORY, permissions);
+ }
+ }
+
+ // write teams
+ for (TeamModel model : teams.values()) {
+ // team roles
+ List<String> roles = new ArrayList<String>();
+ if (model.canAdmin) {
+ roles.add(Constants.ADMIN_ROLE);
+ }
+ if (model.canFork) {
+ roles.add(Constants.FORK_ROLE);
+ }
+ if (model.canCreate) {
+ roles.add(Constants.CREATE_ROLE);
+ }
+ if (roles.size() == 0) {
+ // we do this to ensure that team record is written.
+ // Otherwise, StoredConfig might optimizes that record away.
+ roles.add(Constants.NO_ROLE);
+ }
+ config.setStringList(TEAM, model.name, ROLE, roles);
+
+ if (!model.canAdmin) {
+ // write team permission for non-admin teams
+ if (model.permissions == null) {
+ // null check on "final" repositories because JSON-sourced TeamModel
+ // can have a null repositories object
+ if (!ArrayUtils.isEmpty(model.repositories)) {
+ config.setStringList(TEAM, model.name, REPOSITORY, new ArrayList<String>(
+ model.repositories));
+ }
+ } else {
+ // discrete repository permissions
+ List<String> permissions = new ArrayList<String>();
+ for (Map.Entry<String, AccessPermission> entry : model.permissions.entrySet()) {
+ if (entry.getValue().exceeds(AccessPermission.NONE)) {
+ // code:repository (e.g. RW+:~james/myrepo.git
+ permissions.add(entry.getValue().asRole(entry.getKey()));
+ }
+ }
+ config.setStringList(TEAM, model.name, REPOSITORY, permissions);
+ }
+ }
+
+ // null check on "final" users because JSON-sourced TeamModel
+ // can have a null users object
+ if (!ArrayUtils.isEmpty(model.users)) {
+ config.setStringList(TEAM, model.name, USER, new ArrayList<String>(model.users));
+ }
+
+ // null check on "final" mailing lists because JSON-sourced
+ // TeamModel can have a null users object
+ if (!ArrayUtils.isEmpty(model.mailingLists)) {
+ config.setStringList(TEAM, model.name, MAILINGLIST, new ArrayList<String>(
+ model.mailingLists));
+ }
+
+ // null check on "final" preReceiveScripts because JSON-sourced
+ // TeamModel can have a null preReceiveScripts object
+ if (!ArrayUtils.isEmpty(model.preReceiveScripts)) {
+ config.setStringList(TEAM, model.name, PRERECEIVE, model.preReceiveScripts);
+ }
+
+ // null check on "final" postReceiveScripts because JSON-sourced
+ // TeamModel can have a null postReceiveScripts object
+ if (!ArrayUtils.isEmpty(model.postReceiveScripts)) {
+ config.setStringList(TEAM, model.name, POSTRECEIVE, model.postReceiveScripts);
+ }
+ }
+
+ config.save();
+ // manually set the forceReload flag because not all JVMs support real
+ // millisecond resolution of lastModified. (issue-55)
+ forceReload = true;
+
+ // If the write is successful, delete the current file and rename
+ // the temporary copy to the original filename.
+ if (realmFileCopy.exists() && realmFileCopy.length() > 0) {
+ if (realmFile.exists()) {
+ if (!realmFile.delete()) {
+ throw new IOException(MessageFormat.format("Failed to delete {0}!",
+ realmFile.getAbsolutePath()));
+ }
+ }
+ if (!realmFileCopy.renameTo(realmFile)) {
+ throw new IOException(MessageFormat.format("Failed to rename {0} to {1}!",
+ realmFileCopy.getAbsolutePath(), realmFile.getAbsolutePath()));
+ }
+ } else {
+ throw new IOException(MessageFormat.format("Failed to save {0}!",
+ realmFileCopy.getAbsolutePath()));
+ }
+ }
+
+ /**
+ * Reads the realm file and rebuilds the in-memory lookup tables.
+ */
+ protected synchronized void read() {
+ if (realmFile.exists() && (forceReload || (realmFile.lastModified() != lastModified))) {
+ forceReload = false;
+ lastModified = realmFile.lastModified();
+ users.clear();
+ cookies.clear();
+ teams.clear();
+
+ try {
+ StoredConfig config = new FileBasedConfig(realmFile, FS.detect());
+ config.load();
+ Set<String> usernames = config.getSubsections(USER);
+ for (String username : usernames) {
+ UserModel user = new UserModel(username.toLowerCase());
+ user.password = config.getString(USER, username, PASSWORD);
+ user.displayName = config.getString(USER, username, DISPLAYNAME);
+ user.emailAddress = config.getString(USER, username, EMAILADDRESS);
+ user.organizationalUnit = config.getString(USER, username, ORGANIZATIONALUNIT);
+ user.organization = config.getString(USER, username, ORGANIZATION);
+ user.locality = config.getString(USER, username, LOCALITY);
+ user.stateProvince = config.getString(USER, username, STATEPROVINCE);
+ user.countryCode = config.getString(USER, username, COUNTRYCODE);
+ user.cookie = config.getString(USER, username, COOKIE);
+ if (StringUtils.isEmpty(user.cookie) && !StringUtils.isEmpty(user.password)) {
+ user.cookie = StringUtils.getSHA1(user.username + user.password);
+ }
+
+ // user roles
+ Set<String> roles = new HashSet<String>(Arrays.asList(config.getStringList(
+ USER, username, ROLE)));
+ user.canAdmin = roles.contains(Constants.ADMIN_ROLE);
+ user.canFork = roles.contains(Constants.FORK_ROLE);
+ user.canCreate = roles.contains(Constants.CREATE_ROLE);
+ user.excludeFromFederation = roles.contains(Constants.NOT_FEDERATED_ROLE);
+
+ // repository memberships
+ if (!user.canAdmin) {
+ // non-admin, read permissions
+ Set<String> repositories = new HashSet<String>(Arrays.asList(config
+ .getStringList(USER, username, REPOSITORY)));
+ for (String repository : repositories) {
+ user.addRepositoryPermission(repository);
+ }
+ }
+
+ // update cache
+ users.put(user.username, user);
+ if (!StringUtils.isEmpty(user.cookie)) {
+ cookies.put(user.cookie, user);
+ }
+ }
+
+ // load the teams
+ Set<String> teamnames = config.getSubsections(TEAM);
+ for (String teamname : teamnames) {
+ TeamModel team = new TeamModel(teamname);
+ Set<String> roles = new HashSet<String>(Arrays.asList(config.getStringList(
+ TEAM, teamname, ROLE)));
+ team.canAdmin = roles.contains(Constants.ADMIN_ROLE);
+ team.canFork = roles.contains(Constants.FORK_ROLE);
+ team.canCreate = roles.contains(Constants.CREATE_ROLE);
+
+ if (!team.canAdmin) {
+ // non-admin team, read permissions
+ team.addRepositoryPermissions(Arrays.asList(config.getStringList(TEAM, teamname,
+ REPOSITORY)));
+ }
+ team.addUsers(Arrays.asList(config.getStringList(TEAM, teamname, USER)));
+ team.addMailingLists(Arrays.asList(config.getStringList(TEAM, teamname,
+ MAILINGLIST)));
+ team.preReceiveScripts.addAll(Arrays.asList(config.getStringList(TEAM,
+ teamname, PRERECEIVE)));
+ team.postReceiveScripts.addAll(Arrays.asList(config.getStringList(TEAM,
+ teamname, POSTRECEIVE)));
+
+ teams.put(team.name.toLowerCase(), team);
+
+ // set the teams on the users
+ for (String user : team.users) {
+ UserModel model = users.get(user);
+ if (model != null) {
+ model.teams.add(team);
+ }
+ }
+ }
+ } catch (Exception e) {
+ logger.error(MessageFormat.format("Failed to read {0}", realmFile), e);
+ }
+ }
+ }
+
+ protected long lastModified() {
+ return lastModified;
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + "(" + realmFile.getAbsolutePath() + ")";
+ }
+}
diff --git a/src/main/java/com/gitblit/Constants.java b/src/main/java/com/gitblit/Constants.java
new file mode 100644
index 00000000..7663f8bd
--- /dev/null
+++ b/src/main/java/com/gitblit/Constants.java
@@ -0,0 +1,450 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.net.URL;
+import java.util.jar.Attributes;
+import java.util.jar.Manifest;
+
+/**
+ * Constant values used by Gitblit.
+ *
+ * @author James Moger
+ *
+ */
+public class Constants {
+
+ public static final String NAME = "Gitblit";
+
+ public static final String FULL_NAME = "Gitblit - a pure Java Git solution";
+
+ public static final String ADMIN_ROLE = "#admin";
+
+ public static final String FORK_ROLE = "#fork";
+
+ public static final String CREATE_ROLE = "#create";
+
+ public static final String NOT_FEDERATED_ROLE = "#notfederated";
+
+ public static final String NO_ROLE = "#none";
+
+ public static final String PROPERTIES_FILE = "gitblit.properties";
+
+ public static final String GIT_PATH = "/git/";
+
+ public static final String ZIP_PATH = "/zip/";
+
+ public static final String SYNDICATION_PATH = "/feed/";
+
+ public static final String FEDERATION_PATH = "/federation/";
+
+ public static final String RPC_PATH = "/rpc/";
+
+ public static final String PAGES= "/pages/";
+
+ public static final String BORDER = "***********************************************************";
+
+ public static final String FEDERATION_USER = "$gitblit";
+
+ public static final String PROPOSAL_EXT = ".json";
+
+ public static final String ENCODING = "UTF-8";
+
+ public static final int LEN_SHORTLOG = 78;
+
+ public static final int LEN_SHORTLOG_REFS = 60;
+
+ public static final String DEFAULT_BRANCH = "default";
+
+ public static final String CONFIG_GITBLIT = "gitblit";
+
+ public static final String CONFIG_CUSTOM_FIELDS = "customFields";
+
+ public static final String ISO8601 = "yyyy-MM-dd'T'HH:mm:ssZ";
+
+ public static final String R_GITBLIT = "refs/gitblit/";
+
+ public static final String baseFolder = "baseFolder";
+
+ public static final String baseFolder$ = "${" + baseFolder + "}";
+
+ public static final String contextFolder$ = "${contextFolder}";
+
+ public static String getVersion() {
+ String v = Constants.class.getPackage().getImplementationVersion();
+ if (v == null) {
+ return "0.0.0-SNAPSHOT";
+ }
+ return v;
+ }
+
+ public static String getGitBlitVersion() {
+ return NAME + " v" + getVersion();
+ }
+
+ public static String getBuildDate() {
+ return getManifestValue("build-date", "PENDING");
+ }
+
+ private static String getManifestValue(String attrib, String defaultValue) {
+ Class<?> clazz = Constants.class;
+ String className = clazz.getSimpleName() + ".class";
+ String classPath = clazz.getResource(className).toString();
+ if (!classPath.startsWith("jar")) {
+ // Class not from JAR
+ return defaultValue;
+ }
+ try {
+ String manifestPath = classPath.substring(0, classPath.lastIndexOf("!") + 1) + "/META-INF/MANIFEST.MF";
+ Manifest manifest = new Manifest(new URL(manifestPath).openStream());
+ Attributes attr = manifest.getMainAttributes();
+ String value = attr.getValue(attrib);
+ return value;
+ } catch (Exception e) {
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Enumeration representing the four access restriction levels.
+ */
+ public static enum AccessRestrictionType {
+ NONE, PUSH, CLONE, VIEW;
+
+ public static AccessRestrictionType fromName(String name) {
+ for (AccessRestrictionType type : values()) {
+ if (type.name().equalsIgnoreCase(name)) {
+ return type;
+ }
+ }
+ return NONE;
+ }
+
+ public boolean exceeds(AccessRestrictionType type) {
+ return this.ordinal() > type.ordinal();
+ }
+
+ public boolean atLeast(AccessRestrictionType type) {
+ return this.ordinal() >= type.ordinal();
+ }
+
+ public String toString() {
+ return name();
+ }
+ }
+
+ /**
+ * Enumeration representing the types of authorization control for an
+ * access restricted resource.
+ */
+ public static enum AuthorizationControl {
+ AUTHENTICATED, NAMED;
+
+ public static AuthorizationControl fromName(String name) {
+ for (AuthorizationControl type : values()) {
+ if (type.name().equalsIgnoreCase(name)) {
+ return type;
+ }
+ }
+ return NAMED;
+ }
+
+ public String toString() {
+ return name();
+ }
+ }
+
+
+ /**
+ * Enumeration representing the types of federation tokens.
+ */
+ public static enum FederationToken {
+ ALL, USERS_AND_REPOSITORIES, REPOSITORIES;
+
+ public static FederationToken fromName(String name) {
+ for (FederationToken type : values()) {
+ if (type.name().equalsIgnoreCase(name)) {
+ return type;
+ }
+ }
+ return REPOSITORIES;
+ }
+
+ public String toString() {
+ return name();
+ }
+ }
+
+ /**
+ * Enumeration representing the types of federation requests.
+ */
+ public static enum FederationRequest {
+ POKE, PROPOSAL, PULL_REPOSITORIES, PULL_USERS, PULL_TEAMS, PULL_SETTINGS, PULL_SCRIPTS, STATUS;
+
+ public static FederationRequest fromName(String name) {
+ for (FederationRequest type : values()) {
+ if (type.name().equalsIgnoreCase(name)) {
+ return type;
+ }
+ }
+ return PULL_REPOSITORIES;
+ }
+
+ public String toString() {
+ return name();
+ }
+ }
+
+ /**
+ * Enumeration representing the statii of federation requests.
+ */
+ public static enum FederationPullStatus {
+ PENDING, FAILED, SKIPPED, PULLED, MIRRORED, NOCHANGE, EXCLUDED;
+
+ public static FederationPullStatus fromName(String name) {
+ for (FederationPullStatus type : values()) {
+ if (type.name().equalsIgnoreCase(name)) {
+ return type;
+ }
+ }
+ return PENDING;
+ }
+
+ @Override
+ public String toString() {
+ return name();
+ }
+ }
+
+ /**
+ * Enumeration representing the federation types.
+ */
+ public static enum FederationStrategy {
+ EXCLUDE, FEDERATE_THIS, FEDERATE_ORIGIN;
+
+ public static FederationStrategy fromName(String name) {
+ for (FederationStrategy type : values()) {
+ if (type.name().equalsIgnoreCase(name)) {
+ return type;
+ }
+ }
+ return FEDERATE_THIS;
+ }
+
+ public boolean exceeds(FederationStrategy type) {
+ return this.ordinal() > type.ordinal();
+ }
+
+ public boolean atLeast(FederationStrategy type) {
+ return this.ordinal() >= type.ordinal();
+ }
+
+ @Override
+ public String toString() {
+ return name();
+ }
+ }
+
+ /**
+ * Enumeration representing the possible results of federation proposal
+ * requests.
+ */
+ public static enum FederationProposalResult {
+ ERROR, FEDERATION_DISABLED, MISSING_DATA, NO_PROPOSALS, NO_POKE, ACCEPTED;
+
+ @Override
+ public String toString() {
+ return name();
+ }
+ }
+
+ /**
+ * Enumeration representing the possible remote procedure call requests from
+ * a client.
+ */
+ public static enum RpcRequest {
+ // Order is important here. anything above LIST_SETTINGS requires
+ // administrator privileges and web.allowRpcManagement.
+ CLEAR_REPOSITORY_CACHE, GET_PROTOCOL, LIST_REPOSITORIES, LIST_BRANCHES, LIST_SETTINGS,
+ CREATE_REPOSITORY, EDIT_REPOSITORY, DELETE_REPOSITORY,
+ LIST_USERS, CREATE_USER, EDIT_USER, DELETE_USER,
+ LIST_TEAMS, CREATE_TEAM, EDIT_TEAM, DELETE_TEAM,
+ LIST_REPOSITORY_MEMBERS, SET_REPOSITORY_MEMBERS, LIST_REPOSITORY_TEAMS, SET_REPOSITORY_TEAMS,
+ LIST_REPOSITORY_MEMBER_PERMISSIONS, SET_REPOSITORY_MEMBER_PERMISSIONS, LIST_REPOSITORY_TEAM_PERMISSIONS, SET_REPOSITORY_TEAM_PERMISSIONS,
+ LIST_FEDERATION_REGISTRATIONS, LIST_FEDERATION_RESULTS, LIST_FEDERATION_PROPOSALS, LIST_FEDERATION_SETS,
+ EDIT_SETTINGS, LIST_STATUS;
+
+ public static RpcRequest fromName(String name) {
+ for (RpcRequest type : values()) {
+ if (type.name().equalsIgnoreCase(name)) {
+ return type;
+ }
+ }
+ return null;
+ }
+
+ public boolean exceeds(RpcRequest type) {
+ return this.ordinal() > type.ordinal();
+ }
+
+ @Override
+ public String toString() {
+ return name();
+ }
+ }
+
+ /**
+ * Enumeration of the search types.
+ */
+ public static enum SearchType {
+ AUTHOR, COMMITTER, COMMIT;
+
+ public static SearchType forName(String name) {
+ for (SearchType type : values()) {
+ if (type.name().equalsIgnoreCase(name)) {
+ return type;
+ }
+ }
+ return COMMIT;
+ }
+
+ @Override
+ public String toString() {
+ return name().toLowerCase();
+ }
+ }
+
+ /**
+ * The types of objects that can be indexed and queried.
+ */
+ public static enum SearchObjectType {
+ commit, blob, issue;
+
+ static SearchObjectType fromName(String name) {
+ for (SearchObjectType value : values()) {
+ if (value.name().equals(name)) {
+ return value;
+ }
+ }
+ return null;
+ }
+ }
+
+ /**
+ * The access permissions available for a repository.
+ */
+ public static enum AccessPermission {
+ NONE("N"), EXCLUDE("X"), VIEW("V"), CLONE("R"), PUSH("RW"), CREATE("RWC"), DELETE("RWD"), REWIND("RW+"), OWNER("RW+");
+
+ public static final AccessPermission [] NEWPERMISSIONS = { EXCLUDE, VIEW, CLONE, PUSH, CREATE, DELETE, REWIND };
+
+ public static AccessPermission LEGACY = REWIND;
+
+ public final String code;
+
+ private AccessPermission(String code) {
+ this.code = code;
+ }
+
+ public boolean atLeast(AccessPermission perm) {
+ return ordinal() >= perm.ordinal();
+ }
+
+ public boolean exceeds(AccessPermission perm) {
+ return ordinal() > perm.ordinal();
+ }
+
+ public String asRole(String repository) {
+ return code + ":" + repository;
+ }
+
+ @Override
+ public String toString() {
+ return code;
+ }
+
+ public static AccessPermission permissionFromRole(String role) {
+ String [] fields = role.split(":", 2);
+ if (fields.length == 1) {
+ // legacy/undefined assume full permissions
+ return AccessPermission.LEGACY;
+ } else {
+ // code:repository
+ return AccessPermission.fromCode(fields[0]);
+ }
+ }
+
+ public static String repositoryFromRole(String role) {
+ String [] fields = role.split(":", 2);
+ if (fields.length == 1) {
+ // legacy/undefined assume full permissions
+ return role;
+ } else {
+ // code:repository
+ return fields[1];
+ }
+ }
+
+ public static AccessPermission fromCode(String code) {
+ for (AccessPermission perm : values()) {
+ if (perm.code.equalsIgnoreCase(code)) {
+ return perm;
+ }
+ }
+ return AccessPermission.NONE;
+ }
+ }
+
+ public static enum RegistrantType {
+ REPOSITORY, USER, TEAM;
+ }
+
+ public static enum PermissionType {
+ MISSING, EXPLICIT, TEAM, REGEX, OWNER, ADMINISTRATOR;
+ }
+
+ public static enum GCStatus {
+ READY, COLLECTING;
+
+ public boolean exceeds(GCStatus s) {
+ return ordinal() > s.ordinal();
+ }
+ }
+
+ public static enum AuthenticationType {
+ CREDENTIALS, COOKIE, CERTIFICATE, CONTAINER;
+
+ public boolean isStandard() {
+ return ordinal() <= COOKIE.ordinal();
+ }
+ }
+
+ public static enum AccountType {
+ LOCAL, LDAP, REDMINE;
+
+ public boolean isLocal() {
+ return this == LOCAL;
+ }
+ }
+
+ @Documented
+ @Retention(RetentionPolicy.RUNTIME)
+ public @interface Unused {
+ }
+}
diff --git a/src/main/java/com/gitblit/DownloadZipFilter.java b/src/main/java/com/gitblit/DownloadZipFilter.java
new file mode 100644
index 00000000..90a76493
--- /dev/null
+++ b/src/main/java/com/gitblit/DownloadZipFilter.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit;
+
+import com.gitblit.Constants.AccessRestrictionType;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.UserModel;
+
+/**
+ * The DownloadZipFilter is an AccessRestrictionFilter which ensures that zip
+ * requests for view-restricted repositories have proper authentication
+ * credentials and are authorized.
+ *
+ * @author James Moger
+ *
+ */
+public class DownloadZipFilter extends AccessRestrictionFilter {
+
+ /**
+ * Extract the repository name from the url.
+ *
+ * @param url
+ * @return repository name
+ */
+ @Override
+ protected String extractRepositoryName(String url) {
+ int a = url.indexOf("r=");
+ String repository = url.substring(a + 2);
+ if (repository.indexOf('&') > -1) {
+ repository = repository.substring(0, repository.indexOf('&'));
+ }
+ return repository;
+ }
+
+ /**
+ * Analyze the url and returns the action of the request.
+ *
+ * @param url
+ * @return action of the request
+ */
+ @Override
+ protected String getUrlRequestAction(String url) {
+ return "DOWNLOAD";
+ }
+
+ /**
+ * Determine if a non-existing repository can be created using this filter.
+ *
+ * @return true if the filter allows repository creation
+ */
+ @Override
+ protected boolean isCreationAllowed() {
+ return false;
+ }
+
+ /**
+ * Determine if the action may be executed on the repository.
+ *
+ * @param repository
+ * @param action
+ * @return true if the action may be performed
+ */
+ @Override
+ protected boolean isActionAllowed(RepositoryModel repository, String action) {
+ return true;
+ }
+
+ /**
+ * Determine if the repository requires authentication.
+ *
+ * @param repository
+ * @param action
+ * @return true if authentication required
+ */
+ @Override
+ protected boolean requiresAuthentication(RepositoryModel repository, String action) {
+ return repository.accessRestriction.atLeast(AccessRestrictionType.VIEW);
+ }
+
+ /**
+ * Determine if the user can access the repository and perform the specified
+ * action.
+ *
+ * @param repository
+ * @param user
+ * @param action
+ * @return true if user may execute the action on the repository
+ */
+ @Override
+ protected boolean canAccess(RepositoryModel repository, UserModel user, String action) {
+ return user.canView(repository);
+ }
+
+}
diff --git a/src/main/java/com/gitblit/DownloadZipServlet.java b/src/main/java/com/gitblit/DownloadZipServlet.java
new file mode 100644
index 00000000..0feee879
--- /dev/null
+++ b/src/main/java/com/gitblit/DownloadZipServlet.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit;
+
+import java.io.IOException;
+import java.text.MessageFormat;
+import java.text.ParseException;
+import java.util.Date;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.utils.CompressionUtils;
+import com.gitblit.utils.JGitUtils;
+import com.gitblit.utils.MarkdownUtils;
+import com.gitblit.utils.StringUtils;
+
+/**
+ * Streams out a zip file from the specified repository for any tree path at any
+ * revision.
+ *
+ * @author James Moger
+ *
+ */
+public class DownloadZipServlet extends HttpServlet {
+
+ private static final long serialVersionUID = 1L;
+
+ private transient Logger logger = LoggerFactory.getLogger(DownloadZipServlet.class);
+
+ public static enum Format {
+ zip(".zip"), tar(".tar"), gz(".tar.gz"), xz(".tar.xz"), bzip2(".tar.bzip2");
+
+ public final String extension;
+
+ Format(String ext) {
+ this.extension = ext;
+ }
+
+ public static Format fromName(String name) {
+ for (Format format : values()) {
+ if (format.name().equalsIgnoreCase(name)) {
+ return format;
+ }
+ }
+ return zip;
+ }
+ }
+
+ public DownloadZipServlet() {
+ super();
+ }
+
+ /**
+ * Returns an url to this servlet for the specified parameters.
+ *
+ * @param baseURL
+ * @param repository
+ * @param objectId
+ * @param path
+ * @param format
+ * @return an url
+ */
+ public static String asLink(String baseURL, String repository, String objectId, String path, Format format) {
+ if (baseURL.length() > 0 && baseURL.charAt(baseURL.length() - 1) == '/') {
+ baseURL = baseURL.substring(0, baseURL.length() - 1);
+ }
+ return baseURL + Constants.ZIP_PATH + "?r=" + repository
+ + (path == null ? "" : ("&p=" + path))
+ + (objectId == null ? "" : ("&h=" + objectId))
+ + (format == null ? "" : ("&format=" + format.name()));
+ }
+
+ /**
+ * Creates a zip stream from the repository of the requested data.
+ *
+ * @param request
+ * @param response
+ * @throws javax.servlet.ServletException
+ * @throws java.io.IOException
+ */
+ private void processRequest(javax.servlet.http.HttpServletRequest request,
+ javax.servlet.http.HttpServletResponse response) throws javax.servlet.ServletException,
+ java.io.IOException {
+ if (!GitBlit.getBoolean(Keys.web.allowZipDownloads, true)) {
+ logger.warn("Zip downloads are disabled");
+ response.sendError(HttpServletResponse.SC_FORBIDDEN);
+ return;
+ }
+
+ Format format = Format.zip;
+ String repository = request.getParameter("r");
+ String basePath = request.getParameter("p");
+ String objectId = request.getParameter("h");
+ String f = request.getParameter("format");
+ if (!StringUtils.isEmpty(f)) {
+ format = Format.fromName(f);
+ }
+
+ try {
+ String name = repository;
+ if (name.indexOf('/') > -1) {
+ name = name.substring(name.lastIndexOf('/') + 1);
+ }
+ name = StringUtils.stripDotGit(name);
+
+ if (!StringUtils.isEmpty(basePath)) {
+ name += "-" + basePath.replace('/', '_');
+ }
+ if (!StringUtils.isEmpty(objectId)) {
+ name += "-" + objectId;
+ }
+
+ Repository r = GitBlit.self().getRepository(repository);
+ if (r == null) {
+ if (GitBlit.self().isCollectingGarbage(repository)) {
+ error(response, MessageFormat.format("# Error\nGitblit is busy collecting garbage in {0}", repository));
+ return;
+ } else {
+ error(response, MessageFormat.format("# Error\nFailed to find repository {0}", repository));
+ return;
+ }
+ }
+ RevCommit commit = JGitUtils.getCommit(r, objectId);
+ if (commit == null) {
+ error(response, MessageFormat.format("# Error\nFailed to find commit {0}", objectId));
+ r.close();
+ return;
+ }
+ Date date = JGitUtils.getCommitDate(commit);
+
+ String contentType = "application/octet-stream";
+ response.setContentType(contentType + "; charset=" + response.getCharacterEncoding());
+ response.setHeader("Content-Disposition", "attachment; filename=\"" + name + format.extension + "\"");
+ response.setDateHeader("Last-Modified", date.getTime());
+ response.setHeader("Cache-Control", "no-cache");
+ response.setHeader("Pragma", "no-cache");
+ response.setDateHeader("Expires", 0);
+
+ try {
+ switch (format) {
+ case zip:
+ CompressionUtils.zip(r, basePath, objectId, response.getOutputStream());
+ break;
+ case tar:
+ CompressionUtils.tar(r, basePath, objectId, response.getOutputStream());
+ break;
+ case gz:
+ CompressionUtils.gz(r, basePath, objectId, response.getOutputStream());
+ break;
+ case xz:
+ CompressionUtils.xz(r, basePath, objectId, response.getOutputStream());
+ break;
+ case bzip2:
+ CompressionUtils.bzip2(r, basePath, objectId, response.getOutputStream());
+ break;
+ }
+
+ response.flushBuffer();
+ } catch (Throwable t) {
+ logger.error("Failed to write attachment to client", t);
+ }
+
+ // close the repository
+ r.close();
+ } catch (Throwable t) {
+ logger.error("Failed to write attachment to client", t);
+ }
+ }
+
+ private void error(HttpServletResponse response, String mkd) throws ServletException,
+ IOException, ParseException {
+ String content = MarkdownUtils.transformMarkdown(mkd);
+ response.setContentType("text/html; charset=" + Constants.ENCODING);
+ response.getWriter().write(content);
+ }
+
+ @Override
+ protected void doPost(javax.servlet.http.HttpServletRequest request,
+ javax.servlet.http.HttpServletResponse response) throws javax.servlet.ServletException,
+ java.io.IOException {
+ processRequest(request, response);
+ }
+
+ @Override
+ protected void doGet(javax.servlet.http.HttpServletRequest request,
+ javax.servlet.http.HttpServletResponse response) throws javax.servlet.ServletException,
+ java.io.IOException {
+ processRequest(request, response);
+ }
+}
diff --git a/src/main/java/com/gitblit/EnforceAuthenticationFilter.java b/src/main/java/com/gitblit/EnforceAuthenticationFilter.java
new file mode 100644
index 00000000..2a17996e
--- /dev/null
+++ b/src/main/java/com/gitblit/EnforceAuthenticationFilter.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2013 Laurens Vrijnsen
+ * Copyright 2013 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */package com.gitblit;
+
+import java.io.IOException;
+import java.text.MessageFormat;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.models.UserModel;
+
+/**
+ * This filter enforces authentication via HTTP Basic Authentication, if the settings indicate so.
+ * It looks at the settings "web.authenticateViewPages" and "web.enforceHttpBasicAuthentication"; if
+ * both are true, any unauthorized access will be met with a HTTP Basic Authentication header.
+ *
+ * @author Laurens Vrijnsen
+ *
+ */
+public class EnforceAuthenticationFilter implements Filter {
+
+ protected transient Logger logger = LoggerFactory.getLogger(getClass());
+
+ /*
+ * @see javax.servlet.Filter#init(javax.servlet.FilterConfig)
+ */
+ @Override
+ public void init(FilterConfig filterConfig) throws ServletException {
+ // nothing to be done
+
+ } //init
+
+
+ /*
+ * This does the actual filtering: is the user authenticated? If not, enforce HTTP authentication (401)
+ *
+ * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain)
+ */
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+
+ /*
+ * Determine whether to enforce the BASIC authentication:
+ */
+ @SuppressWarnings("static-access")
+ Boolean mustForceAuth = GitBlit.self().getBoolean(Keys.web.authenticateViewPages, false)
+ && GitBlit.self().getBoolean(Keys.web.enforceHttpBasicAuthentication, false);
+
+ HttpServletRequest HttpRequest = (HttpServletRequest)request;
+ HttpServletResponse HttpResponse = (HttpServletResponse)response;
+ UserModel user = GitBlit.self().authenticate(HttpRequest);
+
+ if (mustForceAuth && (user == null)) {
+ // not authenticated, enforce now:
+ logger.debug(MessageFormat.format("EnforceAuthFilter: user not authenticated for URL {0}!", request.toString()));
+ @SuppressWarnings("static-access")
+ String CHALLENGE = MessageFormat.format("Basic realm=\"{0}\"", GitBlit.self().getString("web.siteName",""));
+ HttpResponse.setHeader("WWW-Authenticate", CHALLENGE);
+ HttpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED);
+ return;
+
+ } else {
+ // user is authenticated, or don't care, continue handling
+ chain.doFilter( request, response );
+
+ } // authenticated
+ } // doFilter
+
+
+ /*
+ * @see javax.servlet.Filter#destroy()
+ */
+ @Override
+ public void destroy() {
+ // Nothing to be done
+
+ } // destroy
+
+}
diff --git a/src/main/java/com/gitblit/FederationClient.java b/src/main/java/com/gitblit/FederationClient.java
new file mode 100644
index 00000000..4aeb2111
--- /dev/null
+++ b/src/main/java/com/gitblit/FederationClient.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+import com.beust.jcommander.JCommander;
+import com.beust.jcommander.Parameter;
+import com.beust.jcommander.ParameterException;
+import com.beust.jcommander.Parameters;
+import com.gitblit.models.FederationModel;
+import com.gitblit.utils.FederationUtils;
+import com.gitblit.utils.StringUtils;
+
+/**
+ * Command-line client to pull federated Gitblit repositories.
+ *
+ * @author James Moger
+ *
+ */
+public class FederationClient {
+
+ public static void main(String[] args) {
+ Params params = new Params();
+ JCommander jc = new JCommander(params);
+ try {
+ jc.parse(args);
+ } catch (ParameterException t) {
+ usage(jc, t);
+ }
+
+ IStoredSettings settings = new FileSettings(params.registrationsFile);
+ List<FederationModel> registrations = new ArrayList<FederationModel>();
+ if (StringUtils.isEmpty(params.url)) {
+ registrations.addAll(FederationUtils.getFederationRegistrations(settings));
+ } else {
+ if (StringUtils.isEmpty(params.token)) {
+ System.out.println("Must specify --token parameter!");
+ System.exit(0);
+ }
+ FederationModel model = new FederationModel("Gitblit");
+ model.url = params.url;
+ model.token = params.token;
+ model.mirror = params.mirror;
+ model.bare = params.bare;
+ model.frequency = params.frequency;
+ model.folder = "";
+ registrations.add(model);
+ }
+ if (registrations.size() == 0) {
+ System.out.println("No Federation Registrations! Nothing to do.");
+ System.exit(0);
+ }
+
+ System.out.println("Gitblit Federation Client v" + Constants.getVersion() + " (" + Constants.getBuildDate() + ")");
+
+ // command-line specified repositories folder
+ if (!StringUtils.isEmpty(params.repositoriesFolder)) {
+ settings.overrideSetting(Keys.git.repositoriesFolder, new File(
+ params.repositoriesFolder).getAbsolutePath());
+ }
+
+ // configure the Gitblit singleton for minimal, non-server operation
+ GitBlit.self().configureContext(settings, null, false);
+ FederationPullExecutor executor = new FederationPullExecutor(registrations, params.isDaemon);
+ executor.run();
+ if (!params.isDaemon) {
+ System.out.println("Finished.");
+ System.exit(0);
+ }
+ }
+
+ private static void usage(JCommander jc, ParameterException t) {
+ System.out.println(Constants.getGitBlitVersion());
+ System.out.println();
+ if (t != null) {
+ System.out.println(t.getMessage());
+ System.out.println();
+ }
+
+ if (jc != null) {
+ jc.usage();
+ }
+ System.exit(0);
+ }
+
+ /**
+ * JCommander Parameters class for FederationClient.
+ */
+ @Parameters(separators = " ")
+ private static class Params {
+
+ @Parameter(names = { "--registrations" }, description = "Gitblit Federation Registrations File", required = false)
+ public String registrationsFile = "federation.properties";
+
+ @Parameter(names = { "--daemon" }, description = "Runs in daemon mode to schedule and pull repositories", required = false)
+ public boolean isDaemon;
+
+ @Parameter(names = { "--url" }, description = "URL of Gitblit instance to mirror from", required = false)
+ public String url;
+
+ @Parameter(names = { "--mirror" }, description = "Mirror repositories", required = false)
+ public boolean mirror;
+
+ @Parameter(names = { "--bare" }, description = "Create bare repositories", required = false)
+ public boolean bare;
+
+ @Parameter(names = { "--token" }, description = "Federation Token", required = false)
+ public String token;
+
+ @Parameter(names = { "--frequency" }, description = "Period to wait between pull attempts (requires --daemon)", required = false)
+ public String frequency = "60 mins";
+
+ @Parameter(names = { "--repositoriesFolder" }, description = "Destination folder for cloned repositories", required = false)
+ public String repositoriesFolder;
+
+ }
+}
diff --git a/src/main/java/com/gitblit/FederationPullExecutor.java b/src/main/java/com/gitblit/FederationPullExecutor.java
new file mode 100644
index 00000000..ad1022cf
--- /dev/null
+++ b/src/main/java/com/gitblit/FederationPullExecutor.java
@@ -0,0 +1,523 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit;
+
+import static org.eclipse.jgit.lib.Constants.DOT_GIT_EXT;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.StoredConfig;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.transport.CredentialsProvider;
+import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.Constants.AccessPermission;
+import com.gitblit.Constants.FederationPullStatus;
+import com.gitblit.Constants.FederationStrategy;
+import com.gitblit.GitBlitException.ForbiddenException;
+import com.gitblit.models.FederationModel;
+import com.gitblit.models.RefModel;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.TeamModel;
+import com.gitblit.models.UserModel;
+import com.gitblit.utils.ArrayUtils;
+import com.gitblit.utils.FederationUtils;
+import com.gitblit.utils.FileUtils;
+import com.gitblit.utils.JGitUtils;
+import com.gitblit.utils.JGitUtils.CloneResult;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.utils.TimeUtils;
+
+/**
+ * FederationPullExecutor pulls repository updates and, optionally, user
+ * accounts and server settings from registered Gitblit instances.
+ */
+public class FederationPullExecutor implements Runnable {
+
+ private final Logger logger = LoggerFactory.getLogger(FederationPullExecutor.class);
+
+ private final List<FederationModel> registrations;
+
+ private final boolean isDaemon;
+
+ /**
+ * Constructor for specifying a single federation registration. This
+ * constructor is used to schedule the next pull execution.
+ *
+ * @param registration
+ */
+ private FederationPullExecutor(FederationModel registration) {
+ this(Arrays.asList(registration), true);
+ }
+
+ /**
+ * Constructor to specify a group of federation registrations. This is
+ * normally used at startup to pull and then schedule the next update based
+ * on each registrations frequency setting.
+ *
+ * @param registrations
+ * @param isDaemon
+ * if true, registrations are rescheduled in perpetuity. if
+ * false, the federation pull operation is executed once.
+ */
+ public FederationPullExecutor(List<FederationModel> registrations, boolean isDaemon) {
+ this.registrations = registrations;
+ this.isDaemon = isDaemon;
+ }
+
+ /**
+ * Run method for this pull executor.
+ */
+ @Override
+ public void run() {
+ for (FederationModel registration : registrations) {
+ FederationPullStatus was = registration.getLowestStatus();
+ try {
+ Date now = new Date(System.currentTimeMillis());
+ pull(registration);
+ sendStatusAcknowledgment(registration);
+ registration.lastPull = now;
+ FederationPullStatus is = registration.getLowestStatus();
+ if (is.ordinal() < was.ordinal()) {
+ // the status for this registration has downgraded
+ logger.warn("Federation pull status of {0} is now {1}", registration.name,
+ is.name());
+ if (registration.notifyOnError) {
+ String message = "Federation pull of " + registration.name + " @ "
+ + registration.url + " is now at " + is.name();
+ GitBlit.self()
+ .sendMailToAdministrators(
+ "Pull Status of " + registration.name + " is " + is.name(),
+ message);
+ }
+ }
+ } catch (Throwable t) {
+ logger.error(MessageFormat.format(
+ "Failed to pull from federated gitblit ({0} @ {1})", registration.name,
+ registration.url), t);
+ } finally {
+ if (isDaemon) {
+ schedule(registration);
+ }
+ }
+ }
+ }
+
+ /**
+ * Mirrors a repository and, optionally, the server's users, and/or
+ * configuration settings from a origin Gitblit instance.
+ *
+ * @param registration
+ * @throws Exception
+ */
+ private void pull(FederationModel registration) throws Exception {
+ Map<String, RepositoryModel> repositories = FederationUtils.getRepositories(registration,
+ true);
+ String registrationFolder = registration.folder.toLowerCase().trim();
+ // confirm valid characters in server alias
+ Character c = StringUtils.findInvalidCharacter(registrationFolder);
+ if (c != null) {
+ logger.error(MessageFormat
+ .format("Illegal character ''{0}'' in folder name ''{1}'' of federation registration {2}!",
+ c, registrationFolder, registration.name));
+ return;
+ }
+ File repositoriesFolder = new File(GitBlit.getString(Keys.git.repositoriesFolder, "git"));
+ File registrationFolderFile = new File(repositoriesFolder, registrationFolder);
+ registrationFolderFile.mkdirs();
+
+ // Clone/Pull the repository
+ for (Map.Entry<String, RepositoryModel> entry : repositories.entrySet()) {
+ String cloneUrl = entry.getKey();
+ RepositoryModel repository = entry.getValue();
+ if (!repository.hasCommits) {
+ logger.warn(MessageFormat.format(
+ "Skipping federated repository {0} from {1} @ {2}. Repository is EMPTY.",
+ repository.name, registration.name, registration.url));
+ registration.updateStatus(repository, FederationPullStatus.SKIPPED);
+ continue;
+ }
+
+ // Determine local repository name
+ String repositoryName;
+ if (StringUtils.isEmpty(registrationFolder)) {
+ repositoryName = repository.name;
+ } else {
+ repositoryName = registrationFolder + "/" + repository.name;
+ }
+
+ if (registration.bare) {
+ // bare repository, ensure .git suffix
+ if (!repositoryName.toLowerCase().endsWith(DOT_GIT_EXT)) {
+ repositoryName += DOT_GIT_EXT;
+ }
+ } else {
+ // normal repository, strip .git suffix
+ if (repositoryName.toLowerCase().endsWith(DOT_GIT_EXT)) {
+ repositoryName = repositoryName.substring(0,
+ repositoryName.indexOf(DOT_GIT_EXT));
+ }
+ }
+
+ // confirm that the origin of any pre-existing repository matches
+ // the clone url
+ String fetchHead = null;
+ Repository existingRepository = GitBlit.self().getRepository(repositoryName);
+
+ if (existingRepository == null && GitBlit.self().isCollectingGarbage(repositoryName)) {
+ logger.warn(MessageFormat.format("Skipping local repository {0}, busy collecting garbage", repositoryName));
+ continue;
+ }
+
+ if (existingRepository != null) {
+ StoredConfig config = existingRepository.getConfig();
+ config.load();
+ String origin = config.getString("remote", "origin", "url");
+ RevCommit commit = JGitUtils.getCommit(existingRepository,
+ org.eclipse.jgit.lib.Constants.FETCH_HEAD);
+ if (commit != null) {
+ fetchHead = commit.getName();
+ }
+ existingRepository.close();
+ if (!origin.startsWith(registration.url)) {
+ logger.warn(MessageFormat
+ .format("Skipping federated repository {0} from {1} @ {2}. Origin does not match, consider EXCLUDING.",
+ repository.name, registration.name, registration.url));
+ registration.updateStatus(repository, FederationPullStatus.SKIPPED);
+ continue;
+ }
+ }
+
+ // clone/pull this repository
+ CredentialsProvider credentials = new UsernamePasswordCredentialsProvider(
+ Constants.FEDERATION_USER, registration.token);
+ logger.info(MessageFormat.format("Pulling federated repository {0} from {1} @ {2}",
+ repository.name, registration.name, registration.url));
+
+ CloneResult result = JGitUtils.cloneRepository(registrationFolderFile, repository.name,
+ cloneUrl, registration.bare, credentials);
+ Repository r = GitBlit.self().getRepository(repositoryName);
+ RepositoryModel rm = GitBlit.self().getRepositoryModel(repositoryName);
+ repository.isFrozen = registration.mirror;
+ if (result.createdRepository) {
+ // default local settings
+ repository.federationStrategy = FederationStrategy.EXCLUDE;
+ repository.isFrozen = registration.mirror;
+ repository.showRemoteBranches = !registration.mirror;
+ logger.info(MessageFormat.format(" cloning {0}", repository.name));
+ registration.updateStatus(repository, FederationPullStatus.MIRRORED);
+ } else {
+ // fetch and update
+ boolean fetched = false;
+ RevCommit commit = JGitUtils.getCommit(r, org.eclipse.jgit.lib.Constants.FETCH_HEAD);
+ String newFetchHead = commit.getName();
+ fetched = fetchHead == null || !fetchHead.equals(newFetchHead);
+
+ if (registration.mirror) {
+ // mirror
+ if (fetched) {
+ // update local branches to match the remote tracking branches
+ for (RefModel ref : JGitUtils.getRemoteBranches(r, false, -1)) {
+ if (ref.displayName.startsWith("origin/")) {
+ String branch = org.eclipse.jgit.lib.Constants.R_HEADS
+ + ref.displayName.substring(ref.displayName.indexOf('/') + 1);
+ String hash = ref.getReferencedObjectId().getName();
+
+ JGitUtils.setBranchRef(r, branch, hash);
+ logger.info(MessageFormat.format(" resetting {0} of {1} to {2}", branch,
+ repository.name, hash));
+ }
+ }
+
+ String newHead;
+ if (StringUtils.isEmpty(repository.HEAD)) {
+ newHead = newFetchHead;
+ } else {
+ newHead = repository.HEAD;
+ }
+ JGitUtils.setHEADtoRef(r, newHead);
+ logger.info(MessageFormat.format(" resetting HEAD of {0} to {1}",
+ repository.name, newHead));
+ registration.updateStatus(repository, FederationPullStatus.MIRRORED);
+ } else {
+ // indicate no commits pulled
+ registration.updateStatus(repository, FederationPullStatus.NOCHANGE);
+ }
+ } else {
+ // non-mirror
+ if (fetched) {
+ // indicate commits pulled to origin/master
+ registration.updateStatus(repository, FederationPullStatus.PULLED);
+ } else {
+ // indicate no commits pulled
+ registration.updateStatus(repository, FederationPullStatus.NOCHANGE);
+ }
+ }
+
+ // preserve local settings
+ repository.isFrozen = rm.isFrozen;
+ repository.federationStrategy = rm.federationStrategy;
+
+ // merge federation sets
+ Set<String> federationSets = new HashSet<String>();
+ if (rm.federationSets != null) {
+ federationSets.addAll(rm.federationSets);
+ }
+ if (repository.federationSets != null) {
+ federationSets.addAll(repository.federationSets);
+ }
+ repository.federationSets = new ArrayList<String>(federationSets);
+
+ // merge indexed branches
+ Set<String> indexedBranches = new HashSet<String>();
+ if (rm.indexedBranches != null) {
+ indexedBranches.addAll(rm.indexedBranches);
+ }
+ if (repository.indexedBranches != null) {
+ indexedBranches.addAll(repository.indexedBranches);
+ }
+ repository.indexedBranches = new ArrayList<String>(indexedBranches);
+
+ }
+ // only repositories that are actually _cloned_ from the origin
+ // Gitblit repository are marked as federated. If the origin
+ // is from somewhere else, these repositories are not considered
+ // "federated" repositories.
+ repository.isFederated = cloneUrl.startsWith(registration.url);
+
+ GitBlit.self().updateConfiguration(r, repository);
+ r.close();
+ }
+
+ IUserService userService = null;
+
+ try {
+ // Pull USERS
+ // TeamModels are automatically pulled because they are contained
+ // within the UserModel. The UserService creates unknown teams
+ // and updates existing teams.
+ Collection<UserModel> users = FederationUtils.getUsers(registration);
+ if (users != null && users.size() > 0) {
+ File realmFile = new File(registrationFolderFile, registration.name + "_users.conf");
+ realmFile.delete();
+ userService = new ConfigUserService(realmFile);
+ for (UserModel user : users) {
+ userService.updateUserModel(user.username, user);
+
+ // merge the origin permissions and origin accounts into
+ // the user accounts of this Gitblit instance
+ if (registration.mergeAccounts) {
+ // reparent all repository permissions if the local
+ // repositories are stored within subfolders
+ if (!StringUtils.isEmpty(registrationFolder)) {
+ if (user.permissions != null) {
+ // pulling from >= 1.2 version
+ Map<String, AccessPermission> copy = new HashMap<String, AccessPermission>(user.permissions);
+ user.permissions.clear();
+ for (Map.Entry<String, AccessPermission> entry : copy.entrySet()) {
+ user.setRepositoryPermission(registrationFolder + "/" + entry.getKey(), entry.getValue());
+ }
+ } else {
+ // pulling from <= 1.1 version
+ List<String> permissions = new ArrayList<String>(user.repositories);
+ user.repositories.clear();
+ for (String permission : permissions) {
+ user.addRepositoryPermission(registrationFolder + "/" + permission);
+ }
+ }
+ }
+
+ // insert new user or update local user
+ UserModel localUser = GitBlit.self().getUserModel(user.username);
+ if (localUser == null) {
+ // create new local user
+ GitBlit.self().updateUserModel(user.username, user, true);
+ } else {
+ // update repository permissions of local user
+ if (user.permissions != null) {
+ // pulling from >= 1.2 version
+ Map<String, AccessPermission> copy = new HashMap<String, AccessPermission>(user.permissions);
+ for (Map.Entry<String, AccessPermission> entry : copy.entrySet()) {
+ localUser.setRepositoryPermission(entry.getKey(), entry.getValue());
+ }
+ } else {
+ // pulling from <= 1.1 version
+ for (String repository : user.repositories) {
+ localUser.addRepositoryPermission(repository);
+ }
+ }
+ localUser.password = user.password;
+ localUser.canAdmin = user.canAdmin;
+ GitBlit.self().updateUserModel(localUser.username, localUser, false);
+ }
+
+ for (String teamname : GitBlit.self().getAllTeamnames()) {
+ TeamModel team = GitBlit.self().getTeamModel(teamname);
+ if (user.isTeamMember(teamname) && !team.hasUser(user.username)) {
+ // new team member
+ team.addUser(user.username);
+ GitBlit.self().updateTeamModel(teamname, team, false);
+ } else if (!user.isTeamMember(teamname) && team.hasUser(user.username)) {
+ // remove team member
+ team.removeUser(user.username);
+ GitBlit.self().updateTeamModel(teamname, team, false);
+ }
+
+ // update team repositories
+ TeamModel remoteTeam = user.getTeam(teamname);
+ if (remoteTeam != null) {
+ if (remoteTeam.permissions != null) {
+ // pulling from >= 1.2
+ for (Map.Entry<String, AccessPermission> entry : remoteTeam.permissions.entrySet()){
+ team.setRepositoryPermission(entry.getKey(), entry.getValue());
+ }
+ GitBlit.self().updateTeamModel(teamname, team, false);
+ } else if(!ArrayUtils.isEmpty(remoteTeam.repositories)) {
+ // pulling from <= 1.1
+ team.addRepositoryPermissions(remoteTeam.repositories);
+ GitBlit.self().updateTeamModel(teamname, team, false);
+ }
+ }
+ }
+ }
+ }
+ }
+ } catch (ForbiddenException e) {
+ // ignore forbidden exceptions
+ } catch (IOException e) {
+ logger.warn(MessageFormat.format(
+ "Failed to retrieve USERS from federated gitblit ({0} @ {1})",
+ registration.name, registration.url), e);
+ }
+
+ try {
+ // Pull TEAMS
+ // We explicitly pull these even though they are embedded in
+ // UserModels because it is possible to use teams to specify
+ // mailing lists or push scripts without specifying users.
+ if (userService != null) {
+ Collection<TeamModel> teams = FederationUtils.getTeams(registration);
+ if (teams != null && teams.size() > 0) {
+ for (TeamModel team : teams) {
+ userService.updateTeamModel(team);
+ }
+ }
+ }
+ } catch (ForbiddenException e) {
+ // ignore forbidden exceptions
+ } catch (IOException e) {
+ logger.warn(MessageFormat.format(
+ "Failed to retrieve TEAMS from federated gitblit ({0} @ {1})",
+ registration.name, registration.url), e);
+ }
+
+ try {
+ // Pull SETTINGS
+ Map<String, String> settings = FederationUtils.getSettings(registration);
+ if (settings != null && settings.size() > 0) {
+ Properties properties = new Properties();
+ properties.putAll(settings);
+ FileOutputStream os = new FileOutputStream(new File(registrationFolderFile,
+ registration.name + "_" + Constants.PROPERTIES_FILE));
+ properties.store(os, null);
+ os.close();
+ }
+ } catch (ForbiddenException e) {
+ // ignore forbidden exceptions
+ } catch (IOException e) {
+ logger.warn(MessageFormat.format(
+ "Failed to retrieve SETTINGS from federated gitblit ({0} @ {1})",
+ registration.name, registration.url), e);
+ }
+
+ try {
+ // Pull SCRIPTS
+ Map<String, String> scripts = FederationUtils.getScripts(registration);
+ if (scripts != null && scripts.size() > 0) {
+ for (Map.Entry<String, String> script : scripts.entrySet()) {
+ String scriptName = script.getKey();
+ if (scriptName.endsWith(".groovy")) {
+ scriptName = scriptName.substring(0, scriptName.indexOf(".groovy"));
+ }
+ File file = new File(registrationFolderFile, registration.name + "_"
+ + scriptName + ".groovy");
+ FileUtils.writeContent(file, script.getValue());
+ }
+ }
+ } catch (ForbiddenException e) {
+ // ignore forbidden exceptions
+ } catch (IOException e) {
+ logger.warn(MessageFormat.format(
+ "Failed to retrieve SCRIPTS from federated gitblit ({0} @ {1})",
+ registration.name, registration.url), e);
+ }
+ }
+
+ /**
+ * Sends a status acknowledgment to the origin Gitblit instance. This
+ * includes the results of the federated pull.
+ *
+ * @param registration
+ * @throws Exception
+ */
+ private void sendStatusAcknowledgment(FederationModel registration) throws Exception {
+ if (!registration.sendStatus) {
+ // skip status acknowledgment
+ return;
+ }
+ InetAddress addr = InetAddress.getLocalHost();
+ String federationName = GitBlit.getString(Keys.federation.name, null);
+ if (StringUtils.isEmpty(federationName)) {
+ federationName = addr.getHostName();
+ }
+ FederationUtils.acknowledgeStatus(addr.getHostAddress(), registration);
+ logger.info(MessageFormat.format("Pull status sent to {0}", registration.url));
+ }
+
+ /**
+ * Schedules the next check of the federated Gitblit instance.
+ *
+ * @param registration
+ */
+ private void schedule(FederationModel registration) {
+ // schedule the next pull
+ int mins = TimeUtils.convertFrequencyToMinutes(registration.frequency);
+ registration.nextPull = new Date(System.currentTimeMillis() + (mins * 60 * 1000L));
+ GitBlit.self().executor()
+ .schedule(new FederationPullExecutor(registration), mins, TimeUnit.MINUTES);
+ logger.info(MessageFormat.format(
+ "Next pull of {0} @ {1} scheduled for {2,date,yyyy-MM-dd HH:mm}",
+ registration.name, registration.url, registration.nextPull));
+ }
+}
diff --git a/src/main/java/com/gitblit/FederationServlet.java b/src/main/java/com/gitblit/FederationServlet.java
new file mode 100644
index 00000000..e7720508
--- /dev/null
+++ b/src/main/java/com/gitblit/FederationServlet.java
@@ -0,0 +1,264 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit;
+
+import java.io.File;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.servlet.http.HttpServletResponse;
+
+import com.gitblit.Constants.FederationRequest;
+import com.gitblit.models.FederationModel;
+import com.gitblit.models.FederationProposal;
+import com.gitblit.models.TeamModel;
+import com.gitblit.models.UserModel;
+import com.gitblit.utils.FederationUtils;
+import com.gitblit.utils.FileUtils;
+import com.gitblit.utils.HttpUtils;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.utils.TimeUtils;
+
+/**
+ * Handles federation requests.
+ *
+ * @author James Moger
+ *
+ */
+public class FederationServlet extends JsonServlet {
+
+ private static final long serialVersionUID = 1L;
+
+ public FederationServlet() {
+ super();
+ }
+
+ /**
+ * Processes a federation request.
+ *
+ * @param request
+ * @param response
+ * @throws javax.servlet.ServletException
+ * @throws java.io.IOException
+ */
+
+ @Override
+ protected void processRequest(javax.servlet.http.HttpServletRequest request,
+ javax.servlet.http.HttpServletResponse response) throws javax.servlet.ServletException,
+ java.io.IOException {
+ FederationRequest reqType = FederationRequest.fromName(request.getParameter("req"));
+ logger.info(MessageFormat.format("Federation {0} request from {1}", reqType,
+ request.getRemoteAddr()));
+
+ if (FederationRequest.POKE.equals(reqType)) {
+ // Gitblit always responds to POKE requests to verify a connection
+ logger.info("Received federation POKE from " + request.getRemoteAddr());
+ return;
+ }
+
+ if (!GitBlit.getBoolean(Keys.git.enableGitServlet, true)) {
+ logger.warn(Keys.git.enableGitServlet + " must be set TRUE for federation requests.");
+ response.sendError(HttpServletResponse.SC_FORBIDDEN);
+ return;
+ }
+
+ String uuid = GitBlit.getString(Keys.federation.passphrase, "");
+ if (StringUtils.isEmpty(uuid)) {
+ logger.warn(Keys.federation.passphrase
+ + " is not properly set! Federation request denied.");
+ response.sendError(HttpServletResponse.SC_FORBIDDEN);
+ return;
+ }
+
+ if (FederationRequest.PROPOSAL.equals(reqType)) {
+ // Receive a gitblit federation proposal
+ FederationProposal proposal = deserialize(request, response, FederationProposal.class);
+ if (proposal == null) {
+ return;
+ }
+
+ // reject proposal, if not receipt prohibited
+ if (!GitBlit.getBoolean(Keys.federation.allowProposals, false)) {
+ logger.error(MessageFormat.format("Rejected {0} federation proposal from {1}",
+ proposal.tokenType.name(), proposal.url));
+ response.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
+ return;
+ }
+
+ // poke the origin Gitblit instance that is proposing federation
+ boolean poked = false;
+ try {
+ poked = FederationUtils.poke(proposal.url);
+ } catch (Exception e) {
+ logger.error("Failed to poke origin", e);
+ }
+ if (!poked) {
+ logger.error(MessageFormat.format("Failed to send federation poke to {0}",
+ proposal.url));
+ response.setStatus(HttpServletResponse.SC_NOT_ACCEPTABLE);
+ return;
+ }
+
+ String url = HttpUtils.getGitblitURL(request);
+ GitBlit.self().submitFederationProposal(proposal, url);
+ logger.info(MessageFormat.format(
+ "Submitted {0} federation proposal to pull {1} repositories from {2}",
+ proposal.tokenType.name(), proposal.repositories.size(), proposal.url));
+ response.setStatus(HttpServletResponse.SC_OK);
+ return;
+ }
+
+ if (FederationRequest.STATUS.equals(reqType)) {
+ // Receive a gitblit federation status acknowledgment
+ String remoteId = StringUtils.decodeFromHtml(request.getParameter("url"));
+ String identification = MessageFormat.format("{0} ({1})", remoteId,
+ request.getRemoteAddr());
+
+ // deserialize the status data
+ FederationModel results = deserialize(request, response, FederationModel.class);
+ if (results == null) {
+ return;
+ }
+
+ // setup the last and netx pull dates
+ results.lastPull = new Date();
+ int mins = TimeUtils.convertFrequencyToMinutes(results.frequency);
+ results.nextPull = new Date(System.currentTimeMillis() + (mins * 60 * 1000L));
+
+ // acknowledge the receipt of status
+ GitBlit.self().acknowledgeFederationStatus(identification, results);
+ logger.info(MessageFormat.format(
+ "Received status of {0} federated repositories from {1}", results
+ .getStatusList().size(), identification));
+ response.setStatus(HttpServletResponse.SC_OK);
+ return;
+ }
+
+ // Determine the federation tokens for this gitblit instance
+ String token = request.getParameter("token");
+ List<String> tokens = GitBlit.self().getFederationTokens();
+ if (!tokens.contains(token)) {
+ logger.warn(MessageFormat.format(
+ "Received Federation token ''{0}'' does not match the server tokens", token));
+ response.sendError(HttpServletResponse.SC_FORBIDDEN);
+ return;
+ }
+
+ Object result = null;
+ if (FederationRequest.PULL_REPOSITORIES.equals(reqType)) {
+ String gitblitUrl = HttpUtils.getGitblitURL(request);
+ result = GitBlit.self().getRepositories(gitblitUrl, token);
+ } else {
+ if (FederationRequest.PULL_SETTINGS.equals(reqType)) {
+ // pull settings
+ if (!GitBlit.self().validateFederationRequest(reqType, token)) {
+ // invalid token to pull users or settings
+ logger.warn(MessageFormat.format(
+ "Federation token from {0} not authorized to pull SETTINGS",
+ request.getRemoteAddr()));
+ response.sendError(HttpServletResponse.SC_FORBIDDEN);
+ return;
+ }
+ Map<String, String> settings = new HashMap<String, String>();
+ List<String> keys = GitBlit.getAllKeys(null);
+ for (String key : keys) {
+ settings.put(key, GitBlit.getString(key, ""));
+ }
+ result = settings;
+ } else if (FederationRequest.PULL_USERS.equals(reqType)) {
+ // pull users
+ if (!GitBlit.self().validateFederationRequest(reqType, token)) {
+ // invalid token to pull users or settings
+ logger.warn(MessageFormat.format(
+ "Federation token from {0} not authorized to pull USERS",
+ request.getRemoteAddr()));
+ response.sendError(HttpServletResponse.SC_FORBIDDEN);
+ return;
+ }
+ List<String> usernames = GitBlit.self().getAllUsernames();
+ List<UserModel> users = new ArrayList<UserModel>();
+ for (String username : usernames) {
+ UserModel user = GitBlit.self().getUserModel(username);
+ if (!user.excludeFromFederation) {
+ users.add(user);
+ }
+ }
+ result = users;
+ } else if (FederationRequest.PULL_TEAMS.equals(reqType)) {
+ // pull teams
+ if (!GitBlit.self().validateFederationRequest(reqType, token)) {
+ // invalid token to pull teams
+ logger.warn(MessageFormat.format(
+ "Federation token from {0} not authorized to pull TEAMS",
+ request.getRemoteAddr()));
+ response.sendError(HttpServletResponse.SC_FORBIDDEN);
+ return;
+ }
+ List<String> teamnames = GitBlit.self().getAllTeamnames();
+ List<TeamModel> teams = new ArrayList<TeamModel>();
+ for (String teamname : teamnames) {
+ TeamModel user = GitBlit.self().getTeamModel(teamname);
+ teams.add(user);
+ }
+ result = teams;
+ } else if (FederationRequest.PULL_SCRIPTS.equals(reqType)) {
+ // pull scripts
+ if (!GitBlit.self().validateFederationRequest(reqType, token)) {
+ // invalid token to pull script
+ logger.warn(MessageFormat.format(
+ "Federation token from {0} not authorized to pull SCRIPTS",
+ request.getRemoteAddr()));
+ response.sendError(HttpServletResponse.SC_FORBIDDEN);
+ return;
+ }
+ Map<String, String> scripts = new HashMap<String, String>();
+
+ Set<String> names = new HashSet<String>();
+ names.addAll(GitBlit.getStrings(Keys.groovy.preReceiveScripts));
+ names.addAll(GitBlit.getStrings(Keys.groovy.postReceiveScripts));
+ for (TeamModel team : GitBlit.self().getAllTeams()) {
+ names.addAll(team.preReceiveScripts);
+ names.addAll(team.postReceiveScripts);
+ }
+ File scriptsFolder = GitBlit.getFileOrFolder(Keys.groovy.scriptsFolder, "groovy");
+ for (String name : names) {
+ File file = new File(scriptsFolder, name);
+ if (!file.exists() && !file.getName().endsWith(".groovy")) {
+ file = new File(scriptsFolder, name + ".groovy");
+ }
+ if (file.exists()) {
+ // read the script
+ String content = FileUtils.readContent(file, "\n");
+ scripts.put(name, content);
+ } else {
+ // missing script?!
+ logger.warn(MessageFormat.format("Failed to find push script \"{0}\"", name));
+ }
+ }
+ result = scripts;
+ }
+ }
+
+ // send the result of the request
+ serialize(response, result);
+ }
+}
diff --git a/src/main/java/com/gitblit/FileSettings.java b/src/main/java/com/gitblit/FileSettings.java
new file mode 100644
index 00000000..3a42cad0
--- /dev/null
+++ b/src/main/java/com/gitblit/FileSettings.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.util.Map;
+import java.util.Properties;
+
+import com.gitblit.utils.FileUtils;
+
+/**
+ * Dynamically loads and reloads a properties file by keeping track of the last
+ * modification date.
+ *
+ * @author James Moger
+ *
+ */
+public class FileSettings extends IStoredSettings {
+
+ protected final File propertiesFile;
+
+ private final Properties properties = new Properties();
+
+ private volatile long lastModified;
+
+ private volatile boolean forceReload;
+
+ public FileSettings(String file) {
+ super(FileSettings.class);
+ this.propertiesFile = new File(file);
+ }
+
+ /**
+ * Returns a properties object which contains the most recent contents of
+ * the properties file.
+ */
+ @Override
+ protected synchronized Properties read() {
+ if (propertiesFile.exists() && (forceReload || (propertiesFile.lastModified() > lastModified))) {
+ FileInputStream is = null;
+ try {
+ Properties props = new Properties();
+ is = new FileInputStream(propertiesFile);
+ props.load(is);
+
+ // load properties after we have successfully read file
+ properties.clear();
+ properties.putAll(props);
+ lastModified = propertiesFile.lastModified();
+ forceReload = false;
+ } catch (FileNotFoundException f) {
+ // IGNORE - won't happen because file.exists() check above
+ } catch (Throwable t) {
+ logger.error("Failed to read " + propertiesFile.getName(), t);
+ } finally {
+ if (is != null) {
+ try {
+ is.close();
+ } catch (Throwable t) {
+ // IGNORE
+ }
+ }
+ }
+ }
+ return properties;
+ }
+
+ /**
+ * Updates the specified settings in the settings file.
+ */
+ public synchronized boolean saveSettings(Map<String, String> settings) {
+ String content = FileUtils.readContent(propertiesFile, "\n");
+ for (Map.Entry<String, String> setting:settings.entrySet()) {
+ String regex = "(?m)^(" + regExEscape(setting.getKey()) + "\\s*+=\\s*+)"
+ + "(?:[^\r\n\\\\]++|\\\\(?:\r?\n|\r|.))*+$";
+ String oldContent = content;
+ content = content.replaceAll(regex, setting.getKey() + " = " + setting.getValue());
+ if (content.equals(oldContent)) {
+ // did not replace value because it does not exist in the file
+ // append new setting to content (issue-85)
+ content += "\n" + setting.getKey() + " = " + setting.getValue();
+ }
+ }
+ FileUtils.writeContent(propertiesFile, content);
+ // manually set the forceReload flag because not all JVMs support real
+ // millisecond resolution of lastModified. (issue-55)
+ forceReload = true;
+ return true;
+ }
+
+ private String regExEscape(String input) {
+ return input.replace(".", "\\.").replace("$", "\\$").replace("{", "\\{");
+ }
+
+ /**
+ * @return the last modification date of the properties file
+ */
+ protected long lastModified() {
+ return lastModified;
+ }
+
+ /**
+ * @return the state of the force reload flag
+ */
+ protected boolean forceReload() {
+ return forceReload;
+ }
+
+ @Override
+ public String toString() {
+ return propertiesFile.getAbsolutePath();
+ }
+}
diff --git a/src/main/java/com/gitblit/FileUserService.java b/src/main/java/com/gitblit/FileUserService.java
new file mode 100644
index 00000000..32c24cc4
--- /dev/null
+++ b/src/main/java/com/gitblit/FileUserService.java
@@ -0,0 +1,1146 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.Constants.AccessPermission;
+import com.gitblit.models.TeamModel;
+import com.gitblit.models.UserModel;
+import com.gitblit.utils.ArrayUtils;
+import com.gitblit.utils.DeepCopier;
+import com.gitblit.utils.StringUtils;
+
+/**
+ * FileUserService is Gitblit's original default user service implementation.
+ *
+ * Users and their repository memberships are stored in a simple properties file
+ * which is cached and dynamically reloaded when modified.
+ *
+ * This class was deprecated in Gitblit 0.8.0 in favor of ConfigUserService
+ * which is still a human-readable, editable, plain-text file but it is more
+ * flexible for storing additional fields.
+ *
+ * @author James Moger
+ *
+ */
+@Deprecated
+public class FileUserService extends FileSettings implements IUserService {
+
+ private final Logger logger = LoggerFactory.getLogger(FileUserService.class);
+
+ private final Map<String, String> cookies = new ConcurrentHashMap<String, String>();
+
+ private final Map<String, TeamModel> teams = new ConcurrentHashMap<String, TeamModel>();
+
+ public FileUserService(File realmFile) {
+ super(realmFile.getAbsolutePath());
+ }
+
+ /**
+ * Setup the user service.
+ *
+ * @param settings
+ * @since 0.7.0
+ */
+ @Override
+ public void setup(IStoredSettings settings) {
+ }
+
+ /**
+ * Does the user service support changes to credentials?
+ *
+ * @return true or false
+ * @since 1.0.0
+ */
+ @Override
+ public boolean supportsCredentialChanges() {
+ return true;
+ }
+
+ /**
+ * Does the user service support changes to user display name?
+ *
+ * @return true or false
+ * @since 1.0.0
+ */
+ @Override
+ public boolean supportsDisplayNameChanges() {
+ return false;
+ }
+
+ /**
+ * Does the user service support changes to user email address?
+ *
+ * @return true or false
+ * @since 1.0.0
+ */
+ @Override
+ public boolean supportsEmailAddressChanges() {
+ return false;
+ }
+
+ /**
+ * Does the user service support changes to team memberships?
+ *
+ * @return true or false
+ * @since 1.0.0
+ */
+ public boolean supportsTeamMembershipChanges() {
+ return true;
+ }
+
+ /**
+ * Does the user service support cookie authentication?
+ *
+ * @return true or false
+ */
+ @Override
+ public boolean supportsCookies() {
+ return true;
+ }
+
+ /**
+ * Returns the cookie value for the specified user.
+ *
+ * @param model
+ * @return cookie value
+ */
+ @Override
+ public String getCookie(UserModel model) {
+ if (!StringUtils.isEmpty(model.cookie)) {
+ return model.cookie;
+ }
+ Properties allUsers = super.read();
+ String value = allUsers.getProperty(model.username);
+ String[] roles = value.split(",");
+ String password = roles[0];
+ String cookie = StringUtils.getSHA1(model.username + password);
+ return cookie;
+ }
+
+ /**
+ * Authenticate a user based on their cookie.
+ *
+ * @param cookie
+ * @return a user object or null
+ */
+ @Override
+ public UserModel authenticate(char[] cookie) {
+ String hash = new String(cookie);
+ if (StringUtils.isEmpty(hash)) {
+ return null;
+ }
+ read();
+ UserModel model = null;
+ if (cookies.containsKey(hash)) {
+ String username = cookies.get(hash);
+ model = getUserModel(username);
+ }
+ return model;
+ }
+
+ /**
+ * Authenticate a user based on a username and password.
+ *
+ * @param username
+ * @param password
+ * @return a user object or null
+ */
+ @Override
+ public UserModel authenticate(String username, char[] password) {
+ Properties allUsers = read();
+ String userInfo = allUsers.getProperty(username);
+ if (StringUtils.isEmpty(userInfo)) {
+ return null;
+ }
+ UserModel returnedUser = null;
+ UserModel user = getUserModel(username);
+ if (user.password.startsWith(StringUtils.MD5_TYPE)) {
+ // password digest
+ String md5 = StringUtils.MD5_TYPE + StringUtils.getMD5(new String(password));
+ if (user.password.equalsIgnoreCase(md5)) {
+ returnedUser = user;
+ }
+ } else if (user.password.startsWith(StringUtils.COMBINED_MD5_TYPE)) {
+ // username+password digest
+ String md5 = StringUtils.COMBINED_MD5_TYPE
+ + StringUtils.getMD5(username.toLowerCase() + new String(password));
+ if (user.password.equalsIgnoreCase(md5)) {
+ returnedUser = user;
+ }
+ } else if (user.password.equals(new String(password))) {
+ // plain-text password
+ returnedUser = user;
+ }
+ return returnedUser;
+ }
+
+ /**
+ * Logout a user.
+ *
+ * @param user
+ */
+ @Override
+ public void logout(UserModel user) {
+ }
+
+ /**
+ * Retrieve the user object for the specified username.
+ *
+ * @param username
+ * @return a user object or null
+ */
+ @Override
+ public UserModel getUserModel(String username) {
+ Properties allUsers = read();
+ String userInfo = allUsers.getProperty(username.toLowerCase());
+ if (userInfo == null) {
+ return null;
+ }
+ UserModel model = new UserModel(username.toLowerCase());
+ String[] userValues = userInfo.split(",");
+ model.password = userValues[0];
+ for (int i = 1; i < userValues.length; i++) {
+ String role = userValues[i];
+ switch (role.charAt(0)) {
+ case '#':
+ // Permissions
+ if (role.equalsIgnoreCase(Constants.ADMIN_ROLE)) {
+ model.canAdmin = true;
+ } else if (role.equalsIgnoreCase(Constants.FORK_ROLE)) {
+ model.canFork = true;
+ } else if (role.equalsIgnoreCase(Constants.CREATE_ROLE)) {
+ model.canCreate = true;
+ } else if (role.equalsIgnoreCase(Constants.NOT_FEDERATED_ROLE)) {
+ model.excludeFromFederation = true;
+ }
+ break;
+ default:
+ model.addRepositoryPermission(role);
+ }
+ }
+ // set the teams for the user
+ for (TeamModel team : teams.values()) {
+ if (team.hasUser(username)) {
+ model.teams.add(DeepCopier.copy(team));
+ }
+ }
+ return model;
+ }
+
+ /**
+ * Updates/writes a complete user object.
+ *
+ * @param model
+ * @return true if update is successful
+ */
+ @Override
+ public boolean updateUserModel(UserModel model) {
+ return updateUserModel(model.username, model);
+ }
+
+ /**
+ * Updates/writes all specified user objects.
+ *
+ * @param models a list of user models
+ * @return true if update is successful
+ * @since 1.2.0
+ */
+ @Override
+ public boolean updateUserModels(Collection<UserModel> models) {
+ try {
+ Properties allUsers = read();
+ for (UserModel model : models) {
+ updateUserCache(allUsers, model.username, model);
+ }
+ write(allUsers);
+ return true;
+ } catch (Throwable t) {
+ logger.error(MessageFormat.format("Failed to update {0} user models!", models.size()),
+ t);
+ }
+ return false;
+ }
+
+ /**
+ * Updates/writes and replaces a complete user object keyed by username.
+ * This method allows for renaming a user.
+ *
+ * @param username
+ * the old username
+ * @param model
+ * the user object to use for username
+ * @return true if update is successful
+ */
+ @Override
+ public boolean updateUserModel(String username, UserModel model) {
+ try {
+ Properties allUsers = read();
+ updateUserCache(allUsers, username, model);
+ write(allUsers);
+ return true;
+ } catch (Throwable t) {
+ logger.error(MessageFormat.format("Failed to update user model {0}!", model.username),
+ t);
+ }
+ return false;
+ }
+
+ /**
+ * Updates/writes and replaces a complete user object keyed by username.
+ * This method allows for renaming a user.
+ *
+ * @param username
+ * the old username
+ * @param model
+ * the user object to use for username
+ * @return true if update is successful
+ */
+ private boolean updateUserCache(Properties allUsers, String username, UserModel model) {
+ try {
+ UserModel oldUser = getUserModel(username);
+ List<String> roles;
+ if (model.permissions == null) {
+ roles = new ArrayList<String>();
+ } else {
+ // discrete repository permissions
+ roles = new ArrayList<String>();
+ for (Map.Entry<String, AccessPermission> entry : model.permissions.entrySet()) {
+ if (entry.getValue().exceeds(AccessPermission.NONE)) {
+ // code:repository (e.g. RW+:~james/myrepo.git
+ roles.add(entry.getValue().asRole(entry.getKey()));
+ }
+ }
+ }
+
+ // Permissions
+ if (model.canAdmin) {
+ roles.add(Constants.ADMIN_ROLE);
+ }
+ if (model.canFork) {
+ roles.add(Constants.FORK_ROLE);
+ }
+ if (model.canCreate) {
+ roles.add(Constants.CREATE_ROLE);
+ }
+ if (model.excludeFromFederation) {
+ roles.add(Constants.NOT_FEDERATED_ROLE);
+ }
+
+ StringBuilder sb = new StringBuilder();
+ if (!StringUtils.isEmpty(model.password)) {
+ sb.append(model.password);
+ }
+ sb.append(',');
+ for (String role : roles) {
+ sb.append(role);
+ sb.append(',');
+ }
+ // trim trailing comma
+ sb.setLength(sb.length() - 1);
+ allUsers.remove(username.toLowerCase());
+ allUsers.put(model.username.toLowerCase(), sb.toString());
+
+ // null check on "final" teams because JSON-sourced UserModel
+ // can have a null teams object
+ if (model.teams != null) {
+ // update team cache
+ for (TeamModel team : model.teams) {
+ TeamModel t = getTeamModel(team.name);
+ if (t == null) {
+ // new team
+ t = team;
+ }
+ t.removeUser(username);
+ t.addUser(model.username);
+ updateTeamCache(allUsers, t.name, t);
+ }
+
+ // check for implicit team removal
+ if (oldUser != null) {
+ for (TeamModel team : oldUser.teams) {
+ if (!model.isTeamMember(team.name)) {
+ team.removeUser(username);
+ updateTeamCache(allUsers, team.name, team);
+ }
+ }
+ }
+ }
+ return true;
+ } catch (Throwable t) {
+ logger.error(MessageFormat.format("Failed to update user model {0}!", model.username),
+ t);
+ }
+ return false;
+ }
+
+ /**
+ * Deletes the user object from the user service.
+ *
+ * @param model
+ * @return true if successful
+ */
+ @Override
+ public boolean deleteUserModel(UserModel model) {
+ return deleteUser(model.username);
+ }
+
+ /**
+ * Delete the user object with the specified username
+ *
+ * @param username
+ * @return true if successful
+ */
+ @Override
+ public boolean deleteUser(String username) {
+ try {
+ // Read realm file
+ Properties allUsers = read();
+ UserModel user = getUserModel(username);
+ allUsers.remove(username);
+ for (TeamModel team : user.teams) {
+ TeamModel t = getTeamModel(team.name);
+ if (t == null) {
+ // new team
+ t = team;
+ }
+ t.removeUser(username);
+ updateTeamCache(allUsers, t.name, t);
+ }
+ write(allUsers);
+ return true;
+ } catch (Throwable t) {
+ logger.error(MessageFormat.format("Failed to delete user {0}!", username), t);
+ }
+ return false;
+ }
+
+ /**
+ * Returns the list of all users available to the login service.
+ *
+ * @return list of all usernames
+ */
+ @Override
+ public List<String> getAllUsernames() {
+ Properties allUsers = read();
+ List<String> list = new ArrayList<String>();
+ for (String user : allUsers.stringPropertyNames()) {
+ if (user.charAt(0) == '@') {
+ // skip team user definitions
+ continue;
+ }
+ list.add(user);
+ }
+ Collections.sort(list);
+ return list;
+ }
+
+ /**
+ * Returns the list of all users available to the login service.
+ *
+ * @return list of all usernames
+ */
+ @Override
+ public List<UserModel> getAllUsers() {
+ read();
+ List<UserModel> list = new ArrayList<UserModel>();
+ for (String username : getAllUsernames()) {
+ list.add(getUserModel(username));
+ }
+ Collections.sort(list);
+ return list;
+ }
+
+ /**
+ * Returns the list of all users who are allowed to bypass the access
+ * restriction placed on the specified repository.
+ *
+ * @param role
+ * the repository name
+ * @return list of all usernames that can bypass the access restriction
+ */
+ @Override
+ public List<String> getUsernamesForRepositoryRole(String role) {
+ List<String> list = new ArrayList<String>();
+ try {
+ Properties allUsers = read();
+ for (String username : allUsers.stringPropertyNames()) {
+ if (username.charAt(0) == '@') {
+ continue;
+ }
+ String value = allUsers.getProperty(username);
+ String[] values = value.split(",");
+ // skip first value (password)
+ for (int i = 1; i < values.length; i++) {
+ String r = values[i];
+ if (r.equalsIgnoreCase(role)) {
+ list.add(username);
+ break;
+ }
+ }
+ }
+ } catch (Throwable t) {
+ logger.error(MessageFormat.format("Failed to get usernames for role {0}!", role), t);
+ }
+ Collections.sort(list);
+ return list;
+ }
+
+ /**
+ * Sets the list of all users who are allowed to bypass the access
+ * restriction placed on the specified repository.
+ *
+ * @param role
+ * the repository name
+ * @param usernames
+ * @return true if successful
+ */
+ @Override
+ public boolean setUsernamesForRepositoryRole(String role, List<String> usernames) {
+ try {
+ Set<String> specifiedUsers = new HashSet<String>(usernames);
+ Set<String> needsAddRole = new HashSet<String>(specifiedUsers);
+ Set<String> needsRemoveRole = new HashSet<String>();
+
+ // identify users which require add and remove role
+ Properties allUsers = read();
+ for (String username : allUsers.stringPropertyNames()) {
+ String value = allUsers.getProperty(username);
+ String[] values = value.split(",");
+ // skip first value (password)
+ for (int i = 1; i < values.length; i++) {
+ String r = values[i];
+ if (r.equalsIgnoreCase(role)) {
+ // user has role, check against revised user list
+ if (specifiedUsers.contains(username)) {
+ needsAddRole.remove(username);
+ } else {
+ // remove role from user
+ needsRemoveRole.add(username);
+ }
+ break;
+ }
+ }
+ }
+
+ // add roles to users
+ for (String user : needsAddRole) {
+ String userValues = allUsers.getProperty(user);
+ userValues += "," + role;
+ allUsers.put(user, userValues);
+ }
+
+ // remove role from user
+ for (String user : needsRemoveRole) {
+ String[] values = allUsers.getProperty(user).split(",");
+ String password = values[0];
+ StringBuilder sb = new StringBuilder();
+ sb.append(password);
+ sb.append(',');
+
+ // skip first value (password)
+ for (int i = 1; i < values.length; i++) {
+ String value = values[i];
+ if (!value.equalsIgnoreCase(role)) {
+ sb.append(value);
+ sb.append(',');
+ }
+ }
+ sb.setLength(sb.length() - 1);
+
+ // update properties
+ allUsers.put(user, sb.toString());
+ }
+
+ // persist changes
+ write(allUsers);
+ return true;
+ } catch (Throwable t) {
+ logger.error(MessageFormat.format("Failed to set usernames for role {0}!", role), t);
+ }
+ return false;
+ }
+
+ /**
+ * Renames a repository role.
+ *
+ * @param oldRole
+ * @param newRole
+ * @return true if successful
+ */
+ @Override
+ public boolean renameRepositoryRole(String oldRole, String newRole) {
+ try {
+ Properties allUsers = read();
+ Set<String> needsRenameRole = new HashSet<String>();
+
+ // identify users which require role rename
+ for (String username : allUsers.stringPropertyNames()) {
+ String value = allUsers.getProperty(username);
+ String[] roles = value.split(",");
+ // skip first value (password)
+ for (int i = 1; i < roles.length; i++) {
+ String repository = AccessPermission.repositoryFromRole(roles[i]);
+ if (repository.equalsIgnoreCase(oldRole)) {
+ needsRenameRole.add(username);
+ break;
+ }
+ }
+ }
+
+ // rename role for identified users
+ for (String user : needsRenameRole) {
+ String userValues = allUsers.getProperty(user);
+ String[] values = userValues.split(",");
+ String password = values[0];
+ StringBuilder sb = new StringBuilder();
+ sb.append(password);
+ sb.append(',');
+ sb.append(newRole);
+ sb.append(',');
+
+ // skip first value (password)
+ for (int i = 1; i < values.length; i++) {
+ String repository = AccessPermission.repositoryFromRole(values[i]);
+ if (repository.equalsIgnoreCase(oldRole)) {
+ AccessPermission permission = AccessPermission.permissionFromRole(values[i]);
+ sb.append(permission.asRole(newRole));
+ sb.append(',');
+ } else {
+ sb.append(values[i]);
+ sb.append(',');
+ }
+ }
+ sb.setLength(sb.length() - 1);
+
+ // update properties
+ allUsers.put(user, sb.toString());
+ }
+
+ // persist changes
+ write(allUsers);
+ return true;
+ } catch (Throwable t) {
+ logger.error(
+ MessageFormat.format("Failed to rename role {0} to {1}!", oldRole, newRole), t);
+ }
+ return false;
+ }
+
+ /**
+ * Removes a repository role from all users.
+ *
+ * @param role
+ * @return true if successful
+ */
+ @Override
+ public boolean deleteRepositoryRole(String role) {
+ try {
+ Properties allUsers = read();
+ Set<String> needsDeleteRole = new HashSet<String>();
+
+ // identify users which require role rename
+ for (String username : allUsers.stringPropertyNames()) {
+ String value = allUsers.getProperty(username);
+ String[] roles = value.split(",");
+ // skip first value (password)
+ for (int i = 1; i < roles.length; i++) {
+ String repository = AccessPermission.repositoryFromRole(roles[i]);
+ if (repository.equalsIgnoreCase(role)) {
+ needsDeleteRole.add(username);
+ break;
+ }
+ }
+ }
+
+ // delete role for identified users
+ for (String user : needsDeleteRole) {
+ String userValues = allUsers.getProperty(user);
+ String[] values = userValues.split(",");
+ String password = values[0];
+ StringBuilder sb = new StringBuilder();
+ sb.append(password);
+ sb.append(',');
+ // skip first value (password)
+ for (int i = 1; i < values.length; i++) {
+ String repository = AccessPermission.repositoryFromRole(values[i]);
+ if (!repository.equalsIgnoreCase(role)) {
+ sb.append(values[i]);
+ sb.append(',');
+ }
+ }
+ sb.setLength(sb.length() - 1);
+
+ // update properties
+ allUsers.put(user, sb.toString());
+ }
+
+ // persist changes
+ write(allUsers);
+ return true;
+ } catch (Throwable t) {
+ logger.error(MessageFormat.format("Failed to delete role {0}!", role), t);
+ }
+ return false;
+ }
+
+ /**
+ * Writes the properties file.
+ *
+ * @param properties
+ * @throws IOException
+ */
+ private void write(Properties properties) throws IOException {
+ // Write a temporary copy of the users file
+ File realmFileCopy = new File(propertiesFile.getAbsolutePath() + ".tmp");
+ FileWriter writer = new FileWriter(realmFileCopy);
+ properties
+ .store(writer,
+ " Gitblit realm file format:\n username=password,\\#permission,repository1,repository2...\n @teamname=!username1,!username2,!username3,repository1,repository2...");
+ writer.close();
+ // If the write is successful, delete the current file and rename
+ // the temporary copy to the original filename.
+ if (realmFileCopy.exists() && realmFileCopy.length() > 0) {
+ if (propertiesFile.exists()) {
+ if (!propertiesFile.delete()) {
+ throw new IOException(MessageFormat.format("Failed to delete {0}!",
+ propertiesFile.getAbsolutePath()));
+ }
+ }
+ if (!realmFileCopy.renameTo(propertiesFile)) {
+ throw new IOException(MessageFormat.format("Failed to rename {0} to {1}!",
+ realmFileCopy.getAbsolutePath(), propertiesFile.getAbsolutePath()));
+ }
+ } else {
+ throw new IOException(MessageFormat.format("Failed to save {0}!",
+ realmFileCopy.getAbsolutePath()));
+ }
+ }
+
+ /**
+ * Reads the properties file and rebuilds the in-memory cookie lookup table.
+ */
+ @Override
+ protected synchronized Properties read() {
+ long lastRead = lastModified();
+ boolean reload = forceReload();
+ Properties allUsers = super.read();
+ if (reload || (lastRead != lastModified())) {
+ // reload hash cache
+ cookies.clear();
+ teams.clear();
+
+ for (String username : allUsers.stringPropertyNames()) {
+ String value = allUsers.getProperty(username);
+ String[] roles = value.split(",");
+ if (username.charAt(0) == '@') {
+ // team definition
+ TeamModel team = new TeamModel(username.substring(1));
+ List<String> repositories = new ArrayList<String>();
+ List<String> users = new ArrayList<String>();
+ List<String> mailingLists = new ArrayList<String>();
+ List<String> preReceive = new ArrayList<String>();
+ List<String> postReceive = new ArrayList<String>();
+ for (String role : roles) {
+ if (role.charAt(0) == '!') {
+ users.add(role.substring(1));
+ } else if (role.charAt(0) == '&') {
+ mailingLists.add(role.substring(1));
+ } else if (role.charAt(0) == '^') {
+ preReceive.add(role.substring(1));
+ } else if (role.charAt(0) == '%') {
+ postReceive.add(role.substring(1));
+ } else {
+ switch (role.charAt(0)) {
+ case '#':
+ // Permissions
+ if (role.equalsIgnoreCase(Constants.ADMIN_ROLE)) {
+ team.canAdmin = true;
+ } else if (role.equalsIgnoreCase(Constants.FORK_ROLE)) {
+ team.canFork = true;
+ } else if (role.equalsIgnoreCase(Constants.CREATE_ROLE)) {
+ team.canCreate = true;
+ }
+ break;
+ default:
+ repositories.add(role);
+ }
+ repositories.add(role);
+ }
+ }
+ if (!team.canAdmin) {
+ // only read permissions for non-admin teams
+ team.addRepositoryPermissions(repositories);
+ }
+ team.addUsers(users);
+ team.addMailingLists(mailingLists);
+ team.preReceiveScripts.addAll(preReceive);
+ team.postReceiveScripts.addAll(postReceive);
+ teams.put(team.name.toLowerCase(), team);
+ } else {
+ // user definition
+ String password = roles[0];
+ cookies.put(StringUtils.getSHA1(username.toLowerCase() + password), username.toLowerCase());
+ }
+ }
+ }
+ return allUsers;
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + "(" + propertiesFile.getAbsolutePath() + ")";
+ }
+
+ /**
+ * Returns the list of all teams available to the login service.
+ *
+ * @return list of all teams
+ * @since 0.8.0
+ */
+ @Override
+ public List<String> getAllTeamNames() {
+ List<String> list = new ArrayList<String>(teams.keySet());
+ Collections.sort(list);
+ return list;
+ }
+
+ /**
+ * Returns the list of all teams available to the login service.
+ *
+ * @return list of all teams
+ * @since 0.8.0
+ */
+ @Override
+ public List<TeamModel> getAllTeams() {
+ List<TeamModel> list = new ArrayList<TeamModel>(teams.values());
+ list = DeepCopier.copy(list);
+ Collections.sort(list);
+ return list;
+ }
+
+ /**
+ * Returns the list of all teams who are allowed to bypass the access
+ * restriction placed on the specified repository.
+ *
+ * @param role
+ * the repository name
+ * @return list of all teamnames that can bypass the access restriction
+ */
+ @Override
+ public List<String> getTeamnamesForRepositoryRole(String role) {
+ List<String> list = new ArrayList<String>();
+ try {
+ Properties allUsers = read();
+ for (String team : allUsers.stringPropertyNames()) {
+ if (team.charAt(0) != '@') {
+ // skip users
+ continue;
+ }
+ String value = allUsers.getProperty(team);
+ String[] values = value.split(",");
+ for (int i = 0; i < values.length; i++) {
+ String r = values[i];
+ if (r.equalsIgnoreCase(role)) {
+ // strip leading @
+ list.add(team.substring(1));
+ break;
+ }
+ }
+ }
+ } catch (Throwable t) {
+ logger.error(MessageFormat.format("Failed to get teamnames for role {0}!", role), t);
+ }
+ Collections.sort(list);
+ return list;
+ }
+
+ /**
+ * Sets the list of all teams who are allowed to bypass the access
+ * restriction placed on the specified repository.
+ *
+ * @param role
+ * the repository name
+ * @param teamnames
+ * @return true if successful
+ */
+ @Override
+ public boolean setTeamnamesForRepositoryRole(String role, List<String> teamnames) {
+ try {
+ Set<String> specifiedTeams = new HashSet<String>(teamnames);
+ Set<String> needsAddRole = new HashSet<String>(specifiedTeams);
+ Set<String> needsRemoveRole = new HashSet<String>();
+
+ // identify teams which require add and remove role
+ Properties allUsers = read();
+ for (String team : allUsers.stringPropertyNames()) {
+ if (team.charAt(0) != '@') {
+ // skip users
+ continue;
+ }
+ String name = team.substring(1);
+ String value = allUsers.getProperty(team);
+ String[] values = value.split(",");
+ for (int i = 0; i < values.length; i++) {
+ String r = values[i];
+ if (r.equalsIgnoreCase(role)) {
+ // team has role, check against revised team list
+ if (specifiedTeams.contains(name)) {
+ needsAddRole.remove(name);
+ } else {
+ // remove role from team
+ needsRemoveRole.add(name);
+ }
+ break;
+ }
+ }
+ }
+
+ // add roles to teams
+ for (String name : needsAddRole) {
+ String team = "@" + name;
+ String teamValues = allUsers.getProperty(team);
+ teamValues += "," + role;
+ allUsers.put(team, teamValues);
+ }
+
+ // remove role from team
+ for (String name : needsRemoveRole) {
+ String team = "@" + name;
+ String[] values = allUsers.getProperty(team).split(",");
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < values.length; i++) {
+ String value = values[i];
+ if (!value.equalsIgnoreCase(role)) {
+ sb.append(value);
+ sb.append(',');
+ }
+ }
+ sb.setLength(sb.length() - 1);
+
+ // update properties
+ allUsers.put(team, sb.toString());
+ }
+
+ // persist changes
+ write(allUsers);
+ return true;
+ } catch (Throwable t) {
+ logger.error(MessageFormat.format("Failed to set teamnames for role {0}!", role), t);
+ }
+ return false;
+ }
+
+ /**
+ * Retrieve the team object for the specified team name.
+ *
+ * @param teamname
+ * @return a team object or null
+ * @since 0.8.0
+ */
+ @Override
+ public TeamModel getTeamModel(String teamname) {
+ read();
+ TeamModel team = teams.get(teamname.toLowerCase());
+ if (team != null) {
+ // clone the model, otherwise all changes to this object are
+ // live and unpersisted
+ team = DeepCopier.copy(team);
+ }
+ return team;
+ }
+
+ /**
+ * Updates/writes a complete team object.
+ *
+ * @param model
+ * @return true if update is successful
+ * @since 0.8.0
+ */
+ @Override
+ public boolean updateTeamModel(TeamModel model) {
+ return updateTeamModel(model.name, model);
+ }
+
+ /**
+ * Updates/writes all specified team objects.
+ *
+ * @param models a list of team models
+ * @return true if update is successful
+ * @since 1.2.0
+ */
+ public boolean updateTeamModels(Collection<TeamModel> models) {
+ try {
+ Properties allUsers = read();
+ for (TeamModel model : models) {
+ updateTeamCache(allUsers, model.name, model);
+ }
+ write(allUsers);
+ return true;
+ } catch (Throwable t) {
+ logger.error(MessageFormat.format("Failed to update {0} team models!", models.size()), t);
+ }
+ return false;
+ }
+
+ /**
+ * Updates/writes and replaces a complete team object keyed by teamname.
+ * This method allows for renaming a team.
+ *
+ * @param teamname
+ * the old teamname
+ * @param model
+ * the team object to use for teamname
+ * @return true if update is successful
+ * @since 0.8.0
+ */
+ @Override
+ public boolean updateTeamModel(String teamname, TeamModel model) {
+ try {
+ Properties allUsers = read();
+ updateTeamCache(allUsers, teamname, model);
+ write(allUsers);
+ return true;
+ } catch (Throwable t) {
+ logger.error(MessageFormat.format("Failed to update team model {0}!", model.name), t);
+ }
+ return false;
+ }
+
+ private void updateTeamCache(Properties allUsers, String teamname, TeamModel model) {
+ StringBuilder sb = new StringBuilder();
+ List<String> roles;
+ if (model.permissions == null) {
+ // legacy, use repository list
+ if (model.repositories != null) {
+ roles = new ArrayList<String>(model.repositories);
+ } else {
+ roles = new ArrayList<String>();
+ }
+ } else {
+ // discrete repository permissions
+ roles = new ArrayList<String>();
+ for (Map.Entry<String, AccessPermission> entry : model.permissions.entrySet()) {
+ if (entry.getValue().exceeds(AccessPermission.NONE)) {
+ // code:repository (e.g. RW+:~james/myrepo.git
+ roles.add(entry.getValue().asRole(entry.getKey()));
+ }
+ }
+ }
+
+ // Permissions
+ if (model.canAdmin) {
+ roles.add(Constants.ADMIN_ROLE);
+ }
+ if (model.canFork) {
+ roles.add(Constants.FORK_ROLE);
+ }
+ if (model.canCreate) {
+ roles.add(Constants.CREATE_ROLE);
+ }
+
+ for (String role : roles) {
+ sb.append(role);
+ sb.append(',');
+ }
+
+ if (!ArrayUtils.isEmpty(model.users)) {
+ for (String user : model.users) {
+ sb.append('!');
+ sb.append(user);
+ sb.append(',');
+ }
+ }
+ if (!ArrayUtils.isEmpty(model.mailingLists)) {
+ for (String address : model.mailingLists) {
+ sb.append('&');
+ sb.append(address);
+ sb.append(',');
+ }
+ }
+ if (!ArrayUtils.isEmpty(model.preReceiveScripts)) {
+ for (String script : model.preReceiveScripts) {
+ sb.append('^');
+ sb.append(script);
+ sb.append(',');
+ }
+ }
+ if (!ArrayUtils.isEmpty(model.postReceiveScripts)) {
+ for (String script : model.postReceiveScripts) {
+ sb.append('%');
+ sb.append(script);
+ sb.append(',');
+ }
+ }
+ // trim trailing comma
+ sb.setLength(sb.length() - 1);
+ allUsers.remove("@" + teamname);
+ allUsers.put("@" + model.name, sb.toString());
+
+ // update team cache
+ teams.remove(teamname.toLowerCase());
+ teams.put(model.name.toLowerCase(), model);
+ }
+
+ /**
+ * Deletes the team object from the user service.
+ *
+ * @param model
+ * @return true if successful
+ * @since 0.8.0
+ */
+ @Override
+ public boolean deleteTeamModel(TeamModel model) {
+ return deleteTeam(model.name);
+ }
+
+ /**
+ * Delete the team object with the specified teamname
+ *
+ * @param teamname
+ * @return true if successful
+ * @since 0.8.0
+ */
+ @Override
+ public boolean deleteTeam(String teamname) {
+ Properties allUsers = read();
+ teams.remove(teamname.toLowerCase());
+ allUsers.remove("@" + teamname);
+ try {
+ write(allUsers);
+ return true;
+ } catch (Throwable t) {
+ logger.error(MessageFormat.format("Failed to delete team {0}!", teamname), t);
+ }
+ return false;
+ }
+}
diff --git a/src/main/java/com/gitblit/GCExecutor.java b/src/main/java/com/gitblit/GCExecutor.java
new file mode 100644
index 00000000..312baf5b
--- /dev/null
+++ b/src/main/java/com/gitblit/GCExecutor.java
@@ -0,0 +1,237 @@
+/*
+ * Copyright 2012 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit;
+
+import java.lang.reflect.Field;
+import java.text.MessageFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.storage.file.FileRepository;
+import org.eclipse.jgit.storage.file.GC;
+import org.eclipse.jgit.storage.file.GC.RepoStatistics;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.utils.FileUtils;
+
+/**
+ * The GC executor handles periodic garbage collection in repositories.
+ *
+ * @author James Moger
+ *
+ */
+public class GCExecutor implements Runnable {
+
+ public static enum GCStatus {
+ READY, COLLECTING;
+
+ public boolean exceeds(GCStatus s) {
+ return ordinal() > s.ordinal();
+ }
+ }
+ private final Logger logger = LoggerFactory.getLogger(GCExecutor.class);
+
+ private final IStoredSettings settings;
+
+ private AtomicBoolean running = new AtomicBoolean(false);
+
+ private AtomicBoolean forceClose = new AtomicBoolean(false);
+
+ private final Map<String, GCStatus> gcCache = new ConcurrentHashMap<String, GCStatus>();
+
+ public GCExecutor(IStoredSettings settings) {
+ this.settings = settings;
+ }
+
+ /**
+ * Indicates if the GC executor is ready to process repositories.
+ *
+ * @return true if the GC executor is ready to process repositories
+ */
+ public boolean isReady() {
+ return settings.getBoolean(Keys.git.enableGarbageCollection, false);
+ }
+
+ public boolean isRunning() {
+ return running.get();
+ }
+
+ public boolean lock(String repositoryName) {
+ return setGCStatus(repositoryName, GCStatus.COLLECTING);
+ }
+
+ /**
+ * Tries to set a GCStatus for the specified repository.
+ *
+ * @param repositoryName
+ * @return true if the status has been set
+ */
+ private boolean setGCStatus(String repositoryName, GCStatus status) {
+ String key = repositoryName.toLowerCase();
+ if (gcCache.containsKey(key)) {
+ if (gcCache.get(key).exceeds(GCStatus.READY)) {
+ // already collecting or blocked
+ return false;
+ }
+ }
+ gcCache.put(key, status);
+ return true;
+ }
+
+ /**
+ * Returns true if Gitblit is actively collecting garbage in this repository.
+ *
+ * @param repositoryName
+ * @return true if actively collecting garbage
+ */
+ public boolean isCollectingGarbage(String repositoryName) {
+ String key = repositoryName.toLowerCase();
+ return gcCache.containsKey(key) && GCStatus.COLLECTING.equals(gcCache.get(key));
+ }
+
+ /**
+ * Resets the GC status to ready.
+ *
+ * @param repositoryName
+ */
+ public void releaseLock(String repositoryName) {
+ gcCache.put(repositoryName.toLowerCase(), GCStatus.READY);
+ }
+
+ public void close() {
+ forceClose.set(true);
+ }
+
+ @Override
+ public void run() {
+ if (!isReady()) {
+ return;
+ }
+
+ running.set(true);
+ Date now = new Date();
+
+ for (String repositoryName : GitBlit.self().getRepositoryList()) {
+ if (forceClose.get()) {
+ break;
+ }
+ if (isCollectingGarbage(repositoryName)) {
+ logger.warn(MessageFormat.format("Already collecting garbage from {0}?!?", repositoryName));
+ continue;
+ }
+ boolean garbageCollected = false;
+ RepositoryModel model = null;
+ FileRepository repository = null;
+ try {
+ model = GitBlit.self().getRepositoryModel(repositoryName);
+ repository = (FileRepository) GitBlit.self().getRepository(repositoryName);
+ if (repository == null) {
+ logger.warn(MessageFormat.format("GCExecutor is missing repository {0}?!?", repositoryName));
+ continue;
+ }
+
+ if (!isRepositoryIdle(repository)) {
+ logger.debug(MessageFormat.format("GCExecutor is skipping {0} because it is not idle", repositoryName));
+ continue;
+ }
+
+ // By setting the GCStatus to COLLECTING we are
+ // disabling *all* access to this repository from Gitblit.
+ // Think of this as a clutch in a manual transmission vehicle.
+ if (!setGCStatus(repositoryName, GCStatus.COLLECTING)) {
+ logger.warn(MessageFormat.format("Can not acquire GC lock for {0}, skipping", repositoryName));
+ continue;
+ }
+
+ logger.debug(MessageFormat.format("GCExecutor locked idle repository {0}", repositoryName));
+
+ GC gc = new GC(repository);
+ RepoStatistics stats = gc.getStatistics();
+
+ // determine if this is a scheduled GC
+ Calendar cal = Calendar.getInstance();
+ cal.setTime(model.lastGC);
+ cal.set(Calendar.HOUR_OF_DAY, 0);
+ cal.set(Calendar.MINUTE, 0);
+ cal.set(Calendar.SECOND, 0);
+ cal.set(Calendar.MILLISECOND, 0);
+ cal.add(Calendar.DATE, model.gcPeriod);
+ Date gcDate = cal.getTime();
+ boolean shouldCollectGarbage = now.after(gcDate);
+
+ // determine if filesize triggered GC
+ long gcThreshold = FileUtils.convertSizeToLong(model.gcThreshold, 500*1024L);
+ boolean hasEnoughGarbage = stats.sizeOfLooseObjects >= gcThreshold;
+
+ // if we satisfy one of the requirements, GC
+ boolean hasGarbage = stats.sizeOfLooseObjects > 0;
+ if (hasGarbage && (hasEnoughGarbage || shouldCollectGarbage)) {
+ long looseKB = stats.sizeOfLooseObjects/1024L;
+ logger.info(MessageFormat.format("Collecting {1} KB of loose objects from {0}", repositoryName, looseKB));
+
+ // do the deed
+ gc.gc();
+
+ garbageCollected = true;
+ }
+ } catch (Exception e) {
+ logger.error("Error collecting garbage in " + repositoryName, e);
+ } finally {
+ // cleanup
+ if (repository != null) {
+ if (garbageCollected) {
+ // update the last GC date
+ model.lastGC = new Date();
+ GitBlit.self().updateConfiguration(repository, model);
+ }
+
+ repository.close();
+ }
+
+ // reset the GC lock
+ releaseLock(repositoryName);
+ logger.debug(MessageFormat.format("GCExecutor released GC lock for {0}", repositoryName));
+ }
+ }
+
+ running.set(false);
+ }
+
+ private boolean isRepositoryIdle(FileRepository repository) {
+ try {
+ // Read the use count.
+ // An idle use count is 2:
+ // +1 for being in the cache
+ // +1 for the repository parameter in this method
+ Field useCnt = Repository.class.getDeclaredField("useCnt");
+ useCnt.setAccessible(true);
+ int useCount = ((AtomicInteger) useCnt.get(repository)).get();
+ return useCount == 2;
+ } catch (Exception e) {
+ logger.warn(MessageFormat
+ .format("Failed to reflectively determine use count for repository {0}",
+ repository.getDirectory().getPath()), e);
+ }
+ return false;
+ }
+}
diff --git a/src/main/java/com/gitblit/GitBlit.java b/src/main/java/com/gitblit/GitBlit.java
new file mode 100644
index 00000000..4cfd61e4
--- /dev/null
+++ b/src/main/java/com/gitblit/GitBlit.java
@@ -0,0 +1,3396 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileFilter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.lang.reflect.Field;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.charset.Charset;
+import java.security.Principal;
+import java.text.MessageFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.TimeZone;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+import javax.mail.Message;
+import javax.mail.MessagingException;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletContextEvent;
+import javax.servlet.ServletContextListener;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.wicket.RequestCycle;
+import org.apache.wicket.protocol.http.WebResponse;
+import org.apache.wicket.resource.ContextRelativeResource;
+import org.apache.wicket.util.resource.ResourceStreamNotFoundException;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.RepositoryCache;
+import org.eclipse.jgit.lib.RepositoryCache.FileKey;
+import org.eclipse.jgit.lib.StoredConfig;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.storage.file.WindowCache;
+import org.eclipse.jgit.storage.file.WindowCacheConfig;
+import org.eclipse.jgit.util.FS;
+import org.eclipse.jgit.util.FileUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.Constants.AccessPermission;
+import com.gitblit.Constants.AccessRestrictionType;
+import com.gitblit.Constants.AuthenticationType;
+import com.gitblit.Constants.AuthorizationControl;
+import com.gitblit.Constants.FederationRequest;
+import com.gitblit.Constants.FederationStrategy;
+import com.gitblit.Constants.FederationToken;
+import com.gitblit.Constants.PermissionType;
+import com.gitblit.Constants.RegistrantType;
+import com.gitblit.fanout.FanoutNioService;
+import com.gitblit.fanout.FanoutService;
+import com.gitblit.fanout.FanoutSocketService;
+import com.gitblit.models.FederationModel;
+import com.gitblit.models.FederationProposal;
+import com.gitblit.models.FederationSet;
+import com.gitblit.models.ForkModel;
+import com.gitblit.models.Metric;
+import com.gitblit.models.ProjectModel;
+import com.gitblit.models.RegistrantAccessPermission;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.SearchResult;
+import com.gitblit.models.ServerSettings;
+import com.gitblit.models.ServerStatus;
+import com.gitblit.models.SettingModel;
+import com.gitblit.models.TeamModel;
+import com.gitblit.models.UserModel;
+import com.gitblit.utils.ArrayUtils;
+import com.gitblit.utils.Base64;
+import com.gitblit.utils.ByteFormat;
+import com.gitblit.utils.ContainerUtils;
+import com.gitblit.utils.DeepCopier;
+import com.gitblit.utils.FederationUtils;
+import com.gitblit.utils.HttpUtils;
+import com.gitblit.utils.JGitUtils;
+import com.gitblit.utils.JsonUtils;
+import com.gitblit.utils.MetricUtils;
+import com.gitblit.utils.ObjectCache;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.utils.TimeUtils;
+import com.gitblit.utils.X509Utils.X509Metadata;
+import com.gitblit.wicket.GitBlitWebSession;
+import com.gitblit.wicket.WicketUtils;
+
+/**
+ * GitBlit is the servlet context listener singleton that acts as the core for
+ * the web ui and the servlets. This class is either directly instantiated by
+ * the GitBlitServer class (Gitblit GO) or is reflectively instantiated from the
+ * definition in the web.xml file (Gitblit WAR).
+ *
+ * This class is the central logic processor for Gitblit. All settings, user
+ * object, and repository object operations pass through this class.
+ *
+ * Repository Resolution. There are two pathways for finding repositories. One
+ * pathway, for web ui display and repository authentication & authorization, is
+ * within this class. The other pathway is through the standard GitServlet.
+ *
+ * @author James Moger
+ *
+ */
+public class GitBlit implements ServletContextListener {
+
+ private static GitBlit gitblit;
+
+ private final Logger logger = LoggerFactory.getLogger(GitBlit.class);
+
+ private final ScheduledExecutorService scheduledExecutor = Executors.newScheduledThreadPool(5);
+
+ private final List<FederationModel> federationRegistrations = Collections
+ .synchronizedList(new ArrayList<FederationModel>());
+
+ private final Map<String, FederationModel> federationPullResults = new ConcurrentHashMap<String, FederationModel>();
+
+ private final ObjectCache<Long> repositorySizeCache = new ObjectCache<Long>();
+
+ private final ObjectCache<List<Metric>> repositoryMetricsCache = new ObjectCache<List<Metric>>();
+
+ private final Map<String, RepositoryModel> repositoryListCache = new ConcurrentHashMap<String, RepositoryModel>();
+
+ private final Map<String, ProjectModel> projectCache = new ConcurrentHashMap<String, ProjectModel>();
+
+ private final AtomicReference<String> repositoryListSettingsChecksum = new AtomicReference<String>("");
+
+ private final ObjectCache<String> projectMarkdownCache = new ObjectCache<String>();
+
+ private final ObjectCache<String> projectRepositoriesMarkdownCache = new ObjectCache<String>();
+
+ private ServletContext servletContext;
+
+ private File baseFolder;
+
+ private File repositoriesFolder;
+
+ private IUserService userService;
+
+ private IStoredSettings settings;
+
+ private ServerSettings settingsModel;
+
+ private ServerStatus serverStatus;
+
+ private MailExecutor mailExecutor;
+
+ private LuceneExecutor luceneExecutor;
+
+ private GCExecutor gcExecutor;
+
+ private TimeZone timezone;
+
+ private FileBasedConfig projectConfigs;
+
+ private FanoutService fanoutService;
+
+ public GitBlit() {
+ if (gitblit == null) {
+ // set the static singleton reference
+ gitblit = this;
+ }
+ }
+
+ public GitBlit(final IUserService userService) {
+ this.userService = userService;
+ gitblit = this;
+ }
+
+ /**
+ * Returns the Gitblit singleton.
+ *
+ * @return gitblit singleton
+ */
+ public static GitBlit self() {
+ if (gitblit == null) {
+ new GitBlit();
+ }
+ return gitblit;
+ }
+
+ /**
+ * Determine if this is the GO variant of Gitblit.
+ *
+ * @return true if this is the GO variant of Gitblit.
+ */
+ public static boolean isGO() {
+ return self().settings instanceof FileSettings;
+ }
+
+ /**
+ * Returns the preferred timezone for the Gitblit instance.
+ *
+ * @return a timezone
+ */
+ public static TimeZone getTimezone() {
+ if (self().timezone == null) {
+ String tzid = getString("web.timezone", null);
+ if (StringUtils.isEmpty(tzid)) {
+ self().timezone = TimeZone.getDefault();
+ return self().timezone;
+ }
+ self().timezone = TimeZone.getTimeZone(tzid);
+ }
+ return self().timezone;
+ }
+
+ /**
+ * Returns the user-defined blob encodings.
+ *
+ * @return an array of encodings, may be empty
+ */
+ public static String [] getEncodings() {
+ return getStrings(Keys.web.blobEncodings).toArray(new String[0]);
+ }
+
+
+ /**
+ * Returns the boolean value for the specified key. If the key does not
+ * exist or the value for the key can not be interpreted as a boolean, the
+ * defaultValue is returned.
+ *
+ * @see IStoredSettings.getBoolean(String, boolean)
+ * @param key
+ * @param defaultValue
+ * @return key value or defaultValue
+ */
+ public static boolean getBoolean(String key, boolean defaultValue) {
+ return self().settings.getBoolean(key, defaultValue);
+ }
+
+ /**
+ * Returns the integer value for the specified key. If the key does not
+ * exist or the value for the key can not be interpreted as an integer, the
+ * defaultValue is returned.
+ *
+ * @see IStoredSettings.getInteger(String key, int defaultValue)
+ * @param key
+ * @param defaultValue
+ * @return key value or defaultValue
+ */
+ public static int getInteger(String key, int defaultValue) {
+ return self().settings.getInteger(key, defaultValue);
+ }
+
+ /**
+ * Returns the value in bytes for the specified key. If the key does not
+ * exist or the value for the key can not be interpreted as an integer, the
+ * defaultValue is returned.
+ *
+ * @see IStoredSettings.getFilesize(String key, int defaultValue)
+ * @param key
+ * @param defaultValue
+ * @return key value or defaultValue
+ */
+ public static int getFilesize(String key, int defaultValue) {
+ return self().settings.getFilesize(key, defaultValue);
+ }
+
+ /**
+ * Returns the value in bytes for the specified key. If the key does not
+ * exist or the value for the key can not be interpreted as a long, the
+ * defaultValue is returned.
+ *
+ * @see IStoredSettings.getFilesize(String key, long defaultValue)
+ * @param key
+ * @param defaultValue
+ * @return key value or defaultValue
+ */
+ public static long getFilesize(String key, long defaultValue) {
+ return self().settings.getFilesize(key, defaultValue);
+ }
+
+ /**
+ * Returns the char value for the specified key. If the key does not exist
+ * or the value for the key can not be interpreted as a character, the
+ * defaultValue is returned.
+ *
+ * @see IStoredSettings.getChar(String key, char defaultValue)
+ * @param key
+ * @param defaultValue
+ * @return key value or defaultValue
+ */
+ public static char getChar(String key, char defaultValue) {
+ return self().settings.getChar(key, defaultValue);
+ }
+
+ /**
+ * Returns the string value for the specified key. If the key does not exist
+ * or the value for the key can not be interpreted as a string, the
+ * defaultValue is returned.
+ *
+ * @see IStoredSettings.getString(String key, String defaultValue)
+ * @param key
+ * @param defaultValue
+ * @return key value or defaultValue
+ */
+ public static String getString(String key, String defaultValue) {
+ return self().settings.getString(key, defaultValue);
+ }
+
+ /**
+ * Returns a list of space-separated strings from the specified key.
+ *
+ * @see IStoredSettings.getStrings(String key)
+ * @param name
+ * @return list of strings
+ */
+ public static List<String> getStrings(String key) {
+ return self().settings.getStrings(key);
+ }
+
+ /**
+ * Returns a map of space-separated key-value pairs from the specified key.
+ *
+ * @see IStoredSettings.getStrings(String key)
+ * @param name
+ * @return map of string, string
+ */
+ public static Map<String, String> getMap(String key) {
+ return self().settings.getMap(key);
+ }
+
+ /**
+ * Returns the list of keys whose name starts with the specified prefix. If
+ * the prefix is null or empty, all key names are returned.
+ *
+ * @see IStoredSettings.getAllKeys(String key)
+ * @param startingWith
+ * @return list of keys
+ */
+
+ public static List<String> getAllKeys(String startingWith) {
+ return self().settings.getAllKeys(startingWith);
+ }
+
+ /**
+ * Is Gitblit running in debug mode?
+ *
+ * @return true if Gitblit is running in debug mode
+ */
+ public static boolean isDebugMode() {
+ return self().settings.getBoolean(Keys.web.debugMode, false);
+ }
+
+ /**
+ * Returns the file object for the specified configuration key.
+ *
+ * @return the file
+ */
+ public static File getFileOrFolder(String key, String defaultFileOrFolder) {
+ String fileOrFolder = GitBlit.getString(key, defaultFileOrFolder);
+ return getFileOrFolder(fileOrFolder);
+ }
+
+ /**
+ * Returns the file object which may have it's base-path determined by
+ * environment variables for running on a cloud hosting service. All Gitblit
+ * file or folder retrievals are (at least initially) funneled through this
+ * method so it is the correct point to globally override/alter filesystem
+ * access based on environment or some other indicator.
+ *
+ * @return the file
+ */
+ public static File getFileOrFolder(String fileOrFolder) {
+ return com.gitblit.utils.FileUtils.resolveParameter(Constants.baseFolder$,
+ self().baseFolder, fileOrFolder);
+ }
+
+ /**
+ * Returns the path of the repositories folder. This method checks to see if
+ * Gitblit is running on a cloud service and may return an adjusted path.
+ *
+ * @return the repositories folder path
+ */
+ public static File getRepositoriesFolder() {
+ return getFileOrFolder(Keys.git.repositoriesFolder, "${baseFolder}/git");
+ }
+
+ /**
+ * Returns the path of the proposals folder. This method checks to see if
+ * Gitblit is running on a cloud service and may return an adjusted path.
+ *
+ * @return the proposals folder path
+ */
+ public static File getProposalsFolder() {
+ return getFileOrFolder(Keys.federation.proposalsFolder, "${baseFolder}/proposals");
+ }
+
+ /**
+ * Returns the path of the Groovy folder. This method checks to see if
+ * Gitblit is running on a cloud service and may return an adjusted path.
+ *
+ * @return the Groovy scripts folder path
+ */
+ public static File getGroovyScriptsFolder() {
+ return getFileOrFolder(Keys.groovy.scriptsFolder, "${baseFolder}/groovy");
+ }
+
+ /**
+ * Updates the list of server settings.
+ *
+ * @param settings
+ * @return true if the update succeeded
+ */
+ public boolean updateSettings(Map<String, String> updatedSettings) {
+ return settings.saveSettings(updatedSettings);
+ }
+
+ public ServerStatus getStatus() {
+ // update heap memory status
+ serverStatus.heapAllocated = Runtime.getRuntime().totalMemory();
+ serverStatus.heapFree = Runtime.getRuntime().freeMemory();
+ return serverStatus;
+ }
+
+ /**
+ * Returns the list of non-Gitblit clone urls. This allows Gitblit to
+ * advertise alternative urls for Git client repository access.
+ *
+ * @param repositoryName
+ * @return list of non-gitblit clone urls
+ */
+ public List<String> getOtherCloneUrls(String repositoryName) {
+ List<String> cloneUrls = new ArrayList<String>();
+ for (String url : settings.getStrings(Keys.web.otherUrls)) {
+ cloneUrls.add(MessageFormat.format(url, repositoryName));
+ }
+ return cloneUrls;
+ }
+
+ /**
+ * Set the user service. The user service authenticates all users and is
+ * responsible for managing user permissions.
+ *
+ * @param userService
+ */
+ public void setUserService(IUserService userService) {
+ logger.info("Setting up user service " + userService.toString());
+ this.userService = userService;
+ this.userService.setup(settings);
+ }
+
+ public boolean supportsAddUser() {
+ return supportsCredentialChanges(new UserModel(""));
+ }
+
+ /**
+ * Returns true if the user's credentials can be changed.
+ *
+ * @param user
+ * @return true if the user service supports credential changes
+ */
+ public boolean supportsCredentialChanges(UserModel user) {
+ return (user != null && user.isLocalAccount()) || userService.supportsCredentialChanges();
+ }
+
+ /**
+ * Returns true if the user's display name can be changed.
+ *
+ * @param user
+ * @return true if the user service supports display name changes
+ */
+ public boolean supportsDisplayNameChanges(UserModel user) {
+ return (user != null && user.isLocalAccount()) || userService.supportsDisplayNameChanges();
+ }
+
+ /**
+ * Returns true if the user's email address can be changed.
+ *
+ * @param user
+ * @return true if the user service supports email address changes
+ */
+ public boolean supportsEmailAddressChanges(UserModel user) {
+ return (user != null && user.isLocalAccount()) || userService.supportsEmailAddressChanges();
+ }
+
+ /**
+ * Returns true if the user's team memberships can be changed.
+ *
+ * @param user
+ * @return true if the user service supports team membership changes
+ */
+ public boolean supportsTeamMembershipChanges(UserModel user) {
+ return (user != null && user.isLocalAccount()) || userService.supportsTeamMembershipChanges();
+ }
+
+ /**
+ * Authenticate a user based on a username and password.
+ *
+ * @see IUserService.authenticate(String, char[])
+ * @param username
+ * @param password
+ * @return a user object or null
+ */
+ public UserModel authenticate(String username, char[] password) {
+ if (StringUtils.isEmpty(username)) {
+ // can not authenticate empty username
+ return null;
+ }
+ String pw = new String(password);
+ if (StringUtils.isEmpty(pw)) {
+ // can not authenticate empty password
+ return null;
+ }
+
+ // check to see if this is the federation user
+ if (canFederate()) {
+ if (username.equalsIgnoreCase(Constants.FEDERATION_USER)) {
+ List<String> tokens = getFederationTokens();
+ if (tokens.contains(pw)) {
+ // the federation user is an administrator
+ UserModel federationUser = new UserModel(Constants.FEDERATION_USER);
+ federationUser.canAdmin = true;
+ return federationUser;
+ }
+ }
+ }
+
+ // delegate authentication to the user service
+ if (userService == null) {
+ return null;
+ }
+ return userService.authenticate(username, password);
+ }
+
+ /**
+ * Authenticate a user based on their cookie.
+ *
+ * @param cookies
+ * @return a user object or null
+ */
+ protected UserModel authenticate(Cookie[] cookies) {
+ if (userService == null) {
+ return null;
+ }
+ if (userService.supportsCookies()) {
+ if (cookies != null && cookies.length > 0) {
+ for (Cookie cookie : cookies) {
+ if (cookie.getName().equals(Constants.NAME)) {
+ String value = cookie.getValue();
+ return userService.authenticate(value.toCharArray());
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Authenticate a user based on HTTP request parameters.
+ *
+ * Authentication by X509Certificate is tried first and then by cookie.
+ *
+ * @param httpRequest
+ * @return a user object or null
+ */
+ public UserModel authenticate(HttpServletRequest httpRequest) {
+ return authenticate(httpRequest, false);
+ }
+
+ /**
+ * Authenticate a user based on HTTP request parameters.
+ *
+ * Authentication by X509Certificate, servlet container principal, cookie,
+ * and BASIC header.
+ *
+ * @param httpRequest
+ * @param requiresCertificate
+ * @return a user object or null
+ */
+ public UserModel authenticate(HttpServletRequest httpRequest, boolean requiresCertificate) {
+ // try to authenticate by certificate
+ boolean checkValidity = settings.getBoolean(Keys.git.enforceCertificateValidity, true);
+ String [] oids = getStrings(Keys.git.certificateUsernameOIDs).toArray(new String[0]);
+ UserModel model = HttpUtils.getUserModelFromCertificate(httpRequest, checkValidity, oids);
+ if (model != null) {
+ // grab real user model and preserve certificate serial number
+ UserModel user = getUserModel(model.username);
+ X509Metadata metadata = HttpUtils.getCertificateMetadata(httpRequest);
+ if (user != null) {
+ flagWicketSession(AuthenticationType.CERTIFICATE);
+ logger.info(MessageFormat.format("{0} authenticated by client certificate {1} from {2}",
+ user.username, metadata.serialNumber, httpRequest.getRemoteAddr()));
+ return user;
+ } else {
+ logger.warn(MessageFormat.format("Failed to find UserModel for {0}, attempted client certificate ({1}) authentication from {2}",
+ model.username, metadata.serialNumber, httpRequest.getRemoteAddr()));
+ }
+ }
+
+ if (requiresCertificate) {
+ // caller requires client certificate authentication (e.g. git servlet)
+ return null;
+ }
+
+ // try to authenticate by servlet container principal
+ Principal principal = httpRequest.getUserPrincipal();
+ if (principal != null) {
+ UserModel user = getUserModel(principal.getName());
+ if (user != null) {
+ flagWicketSession(AuthenticationType.CONTAINER);
+ logger.info(MessageFormat.format("{0} authenticated by servlet container principal from {1}",
+ user.username, httpRequest.getRemoteAddr()));
+ return user;
+ } else {
+ logger.warn(MessageFormat.format("Failed to find UserModel for {0}, attempted servlet container authentication from {1}",
+ principal.getName(), httpRequest.getRemoteAddr()));
+ }
+ }
+
+ // try to authenticate by cookie
+ if (allowCookieAuthentication()) {
+ UserModel user = authenticate(httpRequest.getCookies());
+ if (user != null) {
+ flagWicketSession(AuthenticationType.COOKIE);
+ logger.info(MessageFormat.format("{0} authenticated by cookie from {1}",
+ user.username, httpRequest.getRemoteAddr()));
+ return user;
+ }
+ }
+
+ // try to authenticate by BASIC
+ final String authorization = httpRequest.getHeader("Authorization");
+ if (authorization != null && authorization.startsWith("Basic")) {
+ // Authorization: Basic base64credentials
+ String base64Credentials = authorization.substring("Basic".length()).trim();
+ String credentials = new String(Base64.decode(base64Credentials),
+ Charset.forName("UTF-8"));
+ // credentials = username:password
+ final String[] values = credentials.split(":",2);
+
+ if (values.length == 2) {
+ String username = values[0];
+ char[] password = values[1].toCharArray();
+ UserModel user = authenticate(username, password);
+ if (user != null) {
+ flagWicketSession(AuthenticationType.CREDENTIALS);
+ logger.info(MessageFormat.format("{0} authenticated by BASIC request header from {1}",
+ user.username, httpRequest.getRemoteAddr()));
+ return user;
+ } else {
+ logger.warn(MessageFormat.format("Failed login attempt for {0}, invalid credentials ({1}) from {2}",
+ username, credentials, httpRequest.getRemoteAddr()));
+ }
+ }
+ }
+ return null;
+ }
+
+ protected void flagWicketSession(AuthenticationType authenticationType) {
+ RequestCycle requestCycle = RequestCycle.get();
+ if (requestCycle != null) {
+ // flag the Wicket session, if this is a Wicket request
+ GitBlitWebSession session = GitBlitWebSession.get();
+ session.authenticationType = authenticationType;
+ }
+ }
+
+ /**
+ * Open a file resource using the Servlet container.
+ * @param file to open
+ * @return InputStream of the opened file
+ * @throws ResourceStreamNotFoundException
+ */
+ public InputStream getResourceAsStream(String file) throws ResourceStreamNotFoundException {
+ ContextRelativeResource res = WicketUtils.getResource(file);
+ return res.getResourceStream().getInputStream();
+ }
+
+ /**
+ * Sets a cookie for the specified user.
+ *
+ * @param response
+ * @param user
+ */
+ public void setCookie(WebResponse response, UserModel user) {
+ if (userService == null) {
+ return;
+ }
+ if (userService.supportsCookies()) {
+ Cookie userCookie;
+ if (user == null) {
+ // clear cookie for logout
+ userCookie = new Cookie(Constants.NAME, "");
+ } else {
+ // set cookie for login
+ String cookie = userService.getCookie(user);
+ if (StringUtils.isEmpty(cookie)) {
+ // create empty cookie
+ userCookie = new Cookie(Constants.NAME, "");
+ } else {
+ // create real cookie
+ userCookie = new Cookie(Constants.NAME, cookie);
+ userCookie.setMaxAge(Integer.MAX_VALUE);
+ }
+ }
+ userCookie.setPath("/");
+ response.addCookie(userCookie);
+ }
+ }
+
+ /**
+ * Logout a user.
+ *
+ * @param user
+ */
+ public void logout(UserModel user) {
+ if (userService == null) {
+ return;
+ }
+ userService.logout(user);
+ }
+
+ /**
+ * Returns the list of all users available to the login service.
+ *
+ * @see IUserService.getAllUsernames()
+ * @return list of all usernames
+ */
+ public List<String> getAllUsernames() {
+ List<String> names = new ArrayList<String>(userService.getAllUsernames());
+ return names;
+ }
+
+ /**
+ * Returns the list of all users available to the login service.
+ *
+ * @see IUserService.getAllUsernames()
+ * @return list of all usernames
+ */
+ public List<UserModel> getAllUsers() {
+ List<UserModel> users = userService.getAllUsers();
+ return users;
+ }
+
+ /**
+ * Delete the user object with the specified username
+ *
+ * @see IUserService.deleteUser(String)
+ * @param username
+ * @return true if successful
+ */
+ public boolean deleteUser(String username) {
+ if (StringUtils.isEmpty(username)) {
+ return false;
+ }
+ return userService.deleteUser(username);
+ }
+
+ /**
+ * Retrieve the user object for the specified username.
+ *
+ * @see IUserService.getUserModel(String)
+ * @param username
+ * @return a user object or null
+ */
+ public UserModel getUserModel(String username) {
+ if (StringUtils.isEmpty(username)) {
+ return null;
+ }
+ UserModel user = userService.getUserModel(username);
+ return user;
+ }
+
+ /**
+ * Returns the effective list of permissions for this user, taking into account
+ * team memberships, ownerships.
+ *
+ * @param user
+ * @return the effective list of permissions for the user
+ */
+ public List<RegistrantAccessPermission> getUserAccessPermissions(UserModel user) {
+ if (StringUtils.isEmpty(user.username)) {
+ // new user
+ return new ArrayList<RegistrantAccessPermission>();
+ }
+ Set<RegistrantAccessPermission> set = new LinkedHashSet<RegistrantAccessPermission>();
+ set.addAll(user.getRepositoryPermissions());
+ // Flag missing repositories
+ for (RegistrantAccessPermission permission : set) {
+ if (permission.mutable && PermissionType.EXPLICIT.equals(permission.permissionType)) {
+ RepositoryModel rm = GitBlit.self().getRepositoryModel(permission.registrant);
+ if (rm == null) {
+ permission.permissionType = PermissionType.MISSING;
+ permission.mutable = false;
+ continue;
+ }
+ }
+ }
+
+ // TODO reconsider ownership as a user property
+ // manually specify personal repository ownerships
+ for (RepositoryModel rm : repositoryListCache.values()) {
+ if (rm.isUsersPersonalRepository(user.username) || rm.isOwner(user.username)) {
+ RegistrantAccessPermission rp = new RegistrantAccessPermission(rm.name, AccessPermission.REWIND,
+ PermissionType.OWNER, RegistrantType.REPOSITORY, null, false);
+ // user may be owner of a repository to which they've inherited
+ // a team permission, replace any existing perm with owner perm
+ set.remove(rp);
+ set.add(rp);
+ }
+ }
+
+ List<RegistrantAccessPermission> list = new ArrayList<RegistrantAccessPermission>(set);
+ Collections.sort(list);
+ return list;
+ }
+
+ /**
+ * Returns the list of users and their access permissions for the specified
+ * repository including permission source information such as the team or
+ * regular expression which sets the permission.
+ *
+ * @param repository
+ * @return a list of RegistrantAccessPermissions
+ */
+ public List<RegistrantAccessPermission> getUserAccessPermissions(RepositoryModel repository) {
+ List<RegistrantAccessPermission> list = new ArrayList<RegistrantAccessPermission>();
+ if (AccessRestrictionType.NONE.equals(repository.accessRestriction)) {
+ // no permissions needed, REWIND for everyone!
+ return list;
+ }
+ if (AuthorizationControl.AUTHENTICATED.equals(repository.authorizationControl)) {
+ // no permissions needed, REWIND for authenticated!
+ return list;
+ }
+ // NAMED users and teams
+ for (UserModel user : userService.getAllUsers()) {
+ RegistrantAccessPermission ap = user.getRepositoryPermission(repository);
+ if (ap.permission.exceeds(AccessPermission.NONE)) {
+ list.add(ap);
+ }
+ }
+ return list;
+ }
+
+ /**
+ * Sets the access permissions to the specified repository for the specified users.
+ *
+ * @param repository
+ * @param permissions
+ * @return true if the user models have been updated
+ */
+ public boolean setUserAccessPermissions(RepositoryModel repository, Collection<RegistrantAccessPermission> permissions) {
+ List<UserModel> users = new ArrayList<UserModel>();
+ for (RegistrantAccessPermission up : permissions) {
+ if (up.mutable) {
+ // only set editable defined permissions
+ UserModel user = userService.getUserModel(up.registrant);
+ user.setRepositoryPermission(repository.name, up.permission);
+ users.add(user);
+ }
+ }
+ return userService.updateUserModels(users);
+ }
+
+ /**
+ * Returns the list of all users who have an explicit access permission
+ * for the specified repository.
+ *
+ * @see IUserService.getUsernamesForRepositoryRole(String)
+ * @param repository
+ * @return list of all usernames that have an access permission for the repository
+ */
+ public List<String> getRepositoryUsers(RepositoryModel repository) {
+ return userService.getUsernamesForRepositoryRole(repository.name);
+ }
+
+ /**
+ * Sets the list of all uses who are allowed to bypass the access
+ * restriction placed on the specified repository.
+ *
+ * @see IUserService.setUsernamesForRepositoryRole(String, List<String>)
+ * @param repository
+ * @param usernames
+ * @return true if successful
+ */
+ @Deprecated
+ public boolean setRepositoryUsers(RepositoryModel repository, List<String> repositoryUsers) {
+ // rejects all changes since 1.2.0 because this would elevate
+ // all discrete access permissions to RW+
+ return false;
+ }
+
+ /**
+ * Adds/updates a complete user object keyed by username. This method allows
+ * for renaming a user.
+ *
+ * @see IUserService.updateUserModel(String, UserModel)
+ * @param username
+ * @param user
+ * @param isCreate
+ * @throws GitBlitException
+ */
+ public void updateUserModel(String username, UserModel user, boolean isCreate)
+ throws GitBlitException {
+ if (!username.equalsIgnoreCase(user.username)) {
+ if (userService.getUserModel(user.username) != null) {
+ throw new GitBlitException(MessageFormat.format(
+ "Failed to rename ''{0}'' because ''{1}'' already exists.", username,
+ user.username));
+ }
+
+ // rename repositories and owner fields for all repositories
+ for (RepositoryModel model : getRepositoryModels(user)) {
+ if (model.isUsersPersonalRepository(username)) {
+ // personal repository
+ model.addOwner(user.username);
+ String oldRepositoryName = model.name;
+ model.name = "~" + user.username + model.name.substring(model.projectPath.length());
+ model.projectPath = "~" + user.username;
+ updateRepositoryModel(oldRepositoryName, model, false);
+ } else if (model.isOwner(username)) {
+ // common/shared repo
+ model.addOwner(user.username);
+ updateRepositoryModel(model.name, model, false);
+ }
+ }
+ }
+ if (!userService.updateUserModel(username, user)) {
+ throw new GitBlitException(isCreate ? "Failed to add user!" : "Failed to update user!");
+ }
+ }
+
+ /**
+ * Returns the list of available teams that a user or repository may be
+ * assigned to.
+ *
+ * @return the list of teams
+ */
+ public List<String> getAllTeamnames() {
+ List<String> teams = new ArrayList<String>(userService.getAllTeamNames());
+ return teams;
+ }
+
+ /**
+ * Returns the list of available teams that a user or repository may be
+ * assigned to.
+ *
+ * @return the list of teams
+ */
+ public List<TeamModel> getAllTeams() {
+ List<TeamModel> teams = userService.getAllTeams();
+ return teams;
+ }
+
+ /**
+ * Returns the TeamModel object for the specified name.
+ *
+ * @param teamname
+ * @return a TeamModel object or null
+ */
+ public TeamModel getTeamModel(String teamname) {
+ return userService.getTeamModel(teamname);
+ }
+
+ /**
+ * Returns the list of teams and their access permissions for the specified
+ * repository including the source of the permission such as the admin flag
+ * or a regular expression.
+ *
+ * @param repository
+ * @return a list of RegistrantAccessPermissions
+ */
+ public List<RegistrantAccessPermission> getTeamAccessPermissions(RepositoryModel repository) {
+ List<RegistrantAccessPermission> list = new ArrayList<RegistrantAccessPermission>();
+ for (TeamModel team : userService.getAllTeams()) {
+ RegistrantAccessPermission ap = team.getRepositoryPermission(repository);
+ if (ap.permission.exceeds(AccessPermission.NONE)) {
+ list.add(ap);
+ }
+ }
+ Collections.sort(list);
+ return list;
+ }
+
+ /**
+ * Sets the access permissions to the specified repository for the specified teams.
+ *
+ * @param repository
+ * @param permissions
+ * @return true if the team models have been updated
+ */
+ public boolean setTeamAccessPermissions(RepositoryModel repository, Collection<RegistrantAccessPermission> permissions) {
+ List<TeamModel> teams = new ArrayList<TeamModel>();
+ for (RegistrantAccessPermission tp : permissions) {
+ if (tp.mutable) {
+ // only set explicitly defined access permissions
+ TeamModel team = userService.getTeamModel(tp.registrant);
+ team.setRepositoryPermission(repository.name, tp.permission);
+ teams.add(team);
+ }
+ }
+ return userService.updateTeamModels(teams);
+ }
+
+ /**
+ * Returns the list of all teams who have an explicit access permission for
+ * the specified repository.
+ *
+ * @see IUserService.getTeamnamesForRepositoryRole(String)
+ * @param repository
+ * @return list of all teamnames with explicit access permissions to the repository
+ */
+ public List<String> getRepositoryTeams(RepositoryModel repository) {
+ return userService.getTeamnamesForRepositoryRole(repository.name);
+ }
+
+ /**
+ * Sets the list of all uses who are allowed to bypass the access
+ * restriction placed on the specified repository.
+ *
+ * @see IUserService.setTeamnamesForRepositoryRole(String, List<String>)
+ * @param repository
+ * @param teamnames
+ * @return true if successful
+ */
+ @Deprecated
+ public boolean setRepositoryTeams(RepositoryModel repository, List<String> repositoryTeams) {
+ // rejects all changes since 1.2.0 because this would elevate
+ // all discrete access permissions to RW+
+ return false;
+ }
+
+ /**
+ * Updates the TeamModel object for the specified name.
+ *
+ * @param teamname
+ * @param team
+ * @param isCreate
+ */
+ public void updateTeamModel(String teamname, TeamModel team, boolean isCreate)
+ throws GitBlitException {
+ if (!teamname.equalsIgnoreCase(team.name)) {
+ if (userService.getTeamModel(team.name) != null) {
+ throw new GitBlitException(MessageFormat.format(
+ "Failed to rename ''{0}'' because ''{1}'' already exists.", teamname,
+ team.name));
+ }
+ }
+ if (!userService.updateTeamModel(teamname, team)) {
+ throw new GitBlitException(isCreate ? "Failed to add team!" : "Failed to update team!");
+ }
+ }
+
+ /**
+ * Delete the team object with the specified teamname
+ *
+ * @see IUserService.deleteTeam(String)
+ * @param teamname
+ * @return true if successful
+ */
+ public boolean deleteTeam(String teamname) {
+ return userService.deleteTeam(teamname);
+ }
+
+ /**
+ * Adds the repository to the list of cached repositories if Gitblit is
+ * configured to cache the repository list.
+ *
+ * @param model
+ */
+ private void addToCachedRepositoryList(RepositoryModel model) {
+ if (settings.getBoolean(Keys.git.cacheRepositoryList, true)) {
+ repositoryListCache.put(model.name.toLowerCase(), model);
+
+ // update the fork origin repository with this repository clone
+ if (!StringUtils.isEmpty(model.originRepository)) {
+ if (repositoryListCache.containsKey(model.originRepository)) {
+ RepositoryModel origin = repositoryListCache.get(model.originRepository);
+ origin.addFork(model.name);
+ }
+ }
+ }
+ }
+
+ /**
+ * Removes the repository from the list of cached repositories.
+ *
+ * @param name
+ * @return the model being removed
+ */
+ private RepositoryModel removeFromCachedRepositoryList(String name) {
+ if (StringUtils.isEmpty(name)) {
+ return null;
+ }
+ return repositoryListCache.remove(name.toLowerCase());
+ }
+
+ /**
+ * Clears all the cached metadata for the specified repository.
+ *
+ * @param repositoryName
+ */
+ private void clearRepositoryMetadataCache(String repositoryName) {
+ repositorySizeCache.remove(repositoryName);
+ repositoryMetricsCache.remove(repositoryName);
+ }
+
+ /**
+ * Resets the repository list cache.
+ *
+ */
+ public void resetRepositoryListCache() {
+ logger.info("Repository cache manually reset");
+ repositoryListCache.clear();
+ }
+
+ /**
+ * Calculate the checksum of settings that affect the repository list cache.
+ * @return a checksum
+ */
+ private String getRepositoryListSettingsChecksum() {
+ StringBuilder ns = new StringBuilder();
+ ns.append(settings.getString(Keys.git.cacheRepositoryList, "")).append('\n');
+ ns.append(settings.getString(Keys.git.onlyAccessBareRepositories, "")).append('\n');
+ ns.append(settings.getString(Keys.git.searchRepositoriesSubfolders, "")).append('\n');
+ ns.append(settings.getString(Keys.git.searchRecursionDepth, "")).append('\n');
+ ns.append(settings.getString(Keys.git.searchExclusions, "")).append('\n');
+ String checksum = StringUtils.getSHA1(ns.toString());
+ return checksum;
+ }
+
+ /**
+ * Compare the last repository list setting checksum to the current checksum.
+ * If different then clear the cache so that it may be rebuilt.
+ *
+ * @return true if the cached repository list is valid since the last check
+ */
+ private boolean isValidRepositoryList() {
+ String newChecksum = getRepositoryListSettingsChecksum();
+ boolean valid = newChecksum.equals(repositoryListSettingsChecksum.get());
+ repositoryListSettingsChecksum.set(newChecksum);
+ if (!valid && settings.getBoolean(Keys.git.cacheRepositoryList, true)) {
+ logger.info("Repository list settings have changed. Clearing repository list cache.");
+ repositoryListCache.clear();
+ }
+ return valid;
+ }
+
+ /**
+ * Returns the list of all repositories available to Gitblit. This method
+ * does not consider user access permissions.
+ *
+ * @return list of all repositories
+ */
+ public List<String> getRepositoryList() {
+ if (repositoryListCache.size() == 0 || !isValidRepositoryList()) {
+ // we are not caching OR we have not yet cached OR the cached list is invalid
+ long startTime = System.currentTimeMillis();
+ List<String> repositories = JGitUtils.getRepositoryList(repositoriesFolder,
+ settings.getBoolean(Keys.git.onlyAccessBareRepositories, false),
+ settings.getBoolean(Keys.git.searchRepositoriesSubfolders, true),
+ settings.getInteger(Keys.git.searchRecursionDepth, -1),
+ settings.getStrings(Keys.git.searchExclusions));
+
+ if (!settings.getBoolean(Keys.git.cacheRepositoryList, true)) {
+ // we are not caching
+ StringUtils.sortRepositorynames(repositories);
+ return repositories;
+ } else {
+ // we are caching this list
+ String msg = "{0} repositories identified in {1} msecs";
+
+ // optionally (re)calculate repository sizes
+ if (getBoolean(Keys.web.showRepositorySizes, true)) {
+ msg = "{0} repositories identified with calculated folder sizes in {1} msecs";
+ for (String repository : repositories) {
+ RepositoryModel model = getRepositoryModel(repository);
+ if (!model.skipSizeCalculation) {
+ calculateSize(model);
+ }
+ }
+ } else {
+ // update cache
+ for (String repository : repositories) {
+ getRepositoryModel(repository);
+ }
+ }
+
+ // rebuild fork networks
+ for (RepositoryModel model : repositoryListCache.values()) {
+ if (!StringUtils.isEmpty(model.originRepository)) {
+ if (repositoryListCache.containsKey(model.originRepository)) {
+ RepositoryModel origin = repositoryListCache.get(model.originRepository);
+ origin.addFork(model.name);
+ }
+ }
+ }
+
+ long duration = System.currentTimeMillis() - startTime;
+ logger.info(MessageFormat.format(msg, repositoryListCache.size(), duration));
+ }
+ }
+
+ // return sorted copy of cached list
+ List<String> list = new ArrayList<String>(repositoryListCache.keySet());
+ StringUtils.sortRepositorynames(list);
+ return list;
+ }
+
+ /**
+ * Returns the JGit repository for the specified name.
+ *
+ * @param repositoryName
+ * @return repository or null
+ */
+ public Repository getRepository(String repositoryName) {
+ return getRepository(repositoryName, true);
+ }
+
+ /**
+ * Returns the JGit repository for the specified name.
+ *
+ * @param repositoryName
+ * @param logError
+ * @return repository or null
+ */
+ public Repository getRepository(String repositoryName, boolean logError) {
+ if (isCollectingGarbage(repositoryName)) {
+ logger.warn(MessageFormat.format("Rejecting request for {0}, busy collecting garbage!", repositoryName));
+ return null;
+ }
+
+ File dir = FileKey.resolve(new File(repositoriesFolder, repositoryName), FS.DETECTED);
+ if (dir == null)
+ return null;
+
+ Repository r = null;
+ try {
+ FileKey key = FileKey.exact(dir, FS.DETECTED);
+ r = RepositoryCache.open(key, true);
+ } catch (IOException e) {
+ if (logError) {
+ logger.error("GitBlit.getRepository(String) failed to find "
+ + new File(repositoriesFolder, repositoryName).getAbsolutePath());
+ }
+ }
+ return r;
+ }
+
+ /**
+ * Returns the list of repository models that are accessible to the user.
+ *
+ * @param user
+ * @return list of repository models accessible to user
+ */
+ public List<RepositoryModel> getRepositoryModels(UserModel user) {
+ long methodStart = System.currentTimeMillis();
+ List<String> list = getRepositoryList();
+ List<RepositoryModel> repositories = new ArrayList<RepositoryModel>();
+ for (String repo : list) {
+ RepositoryModel model = getRepositoryModel(user, repo);
+ if (model != null) {
+ repositories.add(model);
+ }
+ }
+ if (getBoolean(Keys.web.showRepositorySizes, true)) {
+ int repoCount = 0;
+ long startTime = System.currentTimeMillis();
+ ByteFormat byteFormat = new ByteFormat();
+ for (RepositoryModel model : repositories) {
+ if (!model.skipSizeCalculation) {
+ repoCount++;
+ model.size = byteFormat.format(calculateSize(model));
+ }
+ }
+ long duration = System.currentTimeMillis() - startTime;
+ if (duration > 250) {
+ // only log calcualtion time if > 250 msecs
+ logger.info(MessageFormat.format("{0} repository sizes calculated in {1} msecs",
+ repoCount, duration));
+ }
+ }
+ long duration = System.currentTimeMillis() - methodStart;
+ logger.info(MessageFormat.format("{0} repository models loaded for {1} in {2} msecs",
+ repositories.size(), user == null ? "anonymous" : user.username, duration));
+ return repositories;
+ }
+
+ /**
+ * Returns a repository model if the repository exists and the user may
+ * access the repository.
+ *
+ * @param user
+ * @param repositoryName
+ * @return repository model or null
+ */
+ public RepositoryModel getRepositoryModel(UserModel user, String repositoryName) {
+ RepositoryModel model = getRepositoryModel(repositoryName);
+ if (model == null) {
+ return null;
+ }
+ if (user == null) {
+ user = UserModel.ANONYMOUS;
+ }
+ if (user.canView(model)) {
+ return model;
+ }
+ return null;
+ }
+
+ /**
+ * Returns the repository model for the specified repository. This method
+ * does not consider user access permissions.
+ *
+ * @param repositoryName
+ * @return repository model or null
+ */
+ public RepositoryModel getRepositoryModel(String repositoryName) {
+ if (!repositoryListCache.containsKey(repositoryName)) {
+ RepositoryModel model = loadRepositoryModel(repositoryName);
+ if (model == null) {
+ return null;
+ }
+ addToCachedRepositoryList(model);
+ return model;
+ }
+
+ // cached model
+ RepositoryModel model = repositoryListCache.get(repositoryName.toLowerCase());
+
+ if (gcExecutor.isCollectingGarbage(model.name)) {
+ // Gitblit is busy collecting garbage, use our cached model
+ RepositoryModel rm = DeepCopier.copy(model);
+ rm.isCollectingGarbage = true;
+ return rm;
+ }
+
+ // check for updates
+ Repository r = getRepository(model.name);
+ if (r == null) {
+ // repository is missing
+ removeFromCachedRepositoryList(repositoryName);
+ logger.error(MessageFormat.format("Repository \"{0}\" is missing! Removing from cache.", repositoryName));
+ return null;
+ }
+
+ FileBasedConfig config = (FileBasedConfig) getRepositoryConfig(r);
+ if (config.isOutdated()) {
+ // reload model
+ logger.info(MessageFormat.format("Config for \"{0}\" has changed. Reloading model and updating cache.", repositoryName));
+ model = loadRepositoryModel(model.name);
+ removeFromCachedRepositoryList(model.name);
+ addToCachedRepositoryList(model);
+ } else {
+ // update a few repository parameters
+ if (!model.hasCommits) {
+ // update hasCommits, assume a repository only gains commits :)
+ model.hasCommits = JGitUtils.hasCommits(r);
+ }
+
+ model.lastChange = JGitUtils.getLastChange(r);
+ }
+ r.close();
+
+ // return a copy of the cached model
+ return DeepCopier.copy(model);
+ }
+
+
+ /**
+ * Returns the map of project config. This map is cached and reloaded if
+ * the underlying projects.conf file changes.
+ *
+ * @return project config map
+ */
+ private Map<String, ProjectModel> getProjectConfigs() {
+ if (projectCache.isEmpty() || projectConfigs.isOutdated()) {
+
+ try {
+ projectConfigs.load();
+ } catch (Exception e) {
+ }
+
+ // project configs
+ String rootName = GitBlit.getString(Keys.web.repositoryRootGroupName, "main");
+ ProjectModel rootProject = new ProjectModel(rootName, true);
+
+ Map<String, ProjectModel> configs = new HashMap<String, ProjectModel>();
+ // cache the root project under its alias and an empty path
+ configs.put("", rootProject);
+ configs.put(rootProject.name.toLowerCase(), rootProject);
+
+ for (String name : projectConfigs.getSubsections("project")) {
+ ProjectModel project;
+ if (name.equalsIgnoreCase(rootName)) {
+ project = rootProject;
+ } else {
+ project = new ProjectModel(name);
+ }
+ project.title = projectConfigs.getString("project", name, "title");
+ project.description = projectConfigs.getString("project", name, "description");
+
+ // project markdown
+ File pmkd = new File(getRepositoriesFolder(), (project.isRoot ? "" : name) + "/project.mkd");
+ if (pmkd.exists()) {
+ Date lm = new Date(pmkd.lastModified());
+ if (!projectMarkdownCache.hasCurrent(name, lm)) {
+ String mkd = com.gitblit.utils.FileUtils.readContent(pmkd, "\n");
+ projectMarkdownCache.updateObject(name, lm, mkd);
+ }
+ project.projectMarkdown = projectMarkdownCache.getObject(name);
+ }
+
+ // project repositories markdown
+ File rmkd = new File(getRepositoriesFolder(), (project.isRoot ? "" : name) + "/repositories.mkd");
+ if (rmkd.exists()) {
+ Date lm = new Date(rmkd.lastModified());
+ if (!projectRepositoriesMarkdownCache.hasCurrent(name, lm)) {
+ String mkd = com.gitblit.utils.FileUtils.readContent(rmkd, "\n");
+ projectRepositoriesMarkdownCache.updateObject(name, lm, mkd);
+ }
+ project.repositoriesMarkdown = projectRepositoriesMarkdownCache.getObject(name);
+ }
+
+ configs.put(name.toLowerCase(), project);
+ }
+ projectCache.clear();
+ projectCache.putAll(configs);
+ }
+ return projectCache;
+ }
+
+ /**
+ * Returns a list of project models for the user.
+ *
+ * @param user
+ * @param includeUsers
+ * @return list of projects that are accessible to the user
+ */
+ public List<ProjectModel> getProjectModels(UserModel user, boolean includeUsers) {
+ Map<String, ProjectModel> configs = getProjectConfigs();
+
+ // per-user project lists, this accounts for security and visibility
+ Map<String, ProjectModel> map = new TreeMap<String, ProjectModel>();
+ // root project
+ map.put("", configs.get(""));
+
+ for (RepositoryModel model : getRepositoryModels(user)) {
+ String rootPath = StringUtils.getRootPath(model.name).toLowerCase();
+ if (!map.containsKey(rootPath)) {
+ ProjectModel project;
+ if (configs.containsKey(rootPath)) {
+ // clone the project model because it's repository list will
+ // be tailored for the requesting user
+ project = DeepCopier.copy(configs.get(rootPath));
+ } else {
+ project = new ProjectModel(rootPath);
+ }
+ map.put(rootPath, project);
+ }
+ map.get(rootPath).addRepository(model);
+ }
+
+ // sort projects, root project first
+ List<ProjectModel> projects;
+ if (includeUsers) {
+ // all projects
+ projects = new ArrayList<ProjectModel>(map.values());
+ Collections.sort(projects);
+ projects.remove(map.get(""));
+ projects.add(0, map.get(""));
+ } else {
+ // all non-user projects
+ projects = new ArrayList<ProjectModel>();
+ ProjectModel root = map.remove("");
+ for (ProjectModel model : map.values()) {
+ if (!model.isUserProject()) {
+ projects.add(model);
+ }
+ }
+ Collections.sort(projects);
+ projects.add(0, root);
+ }
+ return projects;
+ }
+
+ /**
+ * Returns the project model for the specified user.
+ *
+ * @param name
+ * @param user
+ * @return a project model, or null if it does not exist
+ */
+ public ProjectModel getProjectModel(String name, UserModel user) {
+ for (ProjectModel project : getProjectModels(user, true)) {
+ if (project.name.equalsIgnoreCase(name)) {
+ return project;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns a project model for the Gitblit/system user.
+ *
+ * @param name a project name
+ * @return a project model or null if the project does not exist
+ */
+ public ProjectModel getProjectModel(String name) {
+ Map<String, ProjectModel> configs = getProjectConfigs();
+ ProjectModel project = configs.get(name.toLowerCase());
+ if (project == null) {
+ project = new ProjectModel(name);
+ if (name.length() > 0 && name.charAt(0) == '~') {
+ UserModel user = getUserModel(name.substring(1));
+ if (user != null) {
+ project.title = user.getDisplayName();
+ project.description = "personal repositories";
+ }
+ }
+ } else {
+ // clone the object
+ project = DeepCopier.copy(project);
+ }
+ if (StringUtils.isEmpty(name)) {
+ // get root repositories
+ for (String repository : getRepositoryList()) {
+ if (repository.indexOf('/') == -1) {
+ project.addRepository(repository);
+ }
+ }
+ } else {
+ // get repositories in subfolder
+ String folder = name.toLowerCase() + "/";
+ for (String repository : getRepositoryList()) {
+ if (repository.toLowerCase().startsWith(folder)) {
+ project.addRepository(repository);
+ }
+ }
+ }
+ if (project.repositories.size() == 0) {
+ // no repositories == no project
+ return null;
+ }
+ return project;
+ }
+
+ /**
+ * Returns the list of project models that are referenced by the supplied
+ * repository model list. This is an alternative method exists to ensure
+ * Gitblit does not call getRepositoryModels(UserModel) twice in a request.
+ *
+ * @param repositoryModels
+ * @param includeUsers
+ * @return a list of project models
+ */
+ public List<ProjectModel> getProjectModels(List<RepositoryModel> repositoryModels, boolean includeUsers) {
+ Map<String, ProjectModel> projects = new LinkedHashMap<String, ProjectModel>();
+ for (RepositoryModel repository : repositoryModels) {
+ if (!includeUsers && repository.isPersonalRepository()) {
+ // exclude personal repositories
+ continue;
+ }
+ if (!projects.containsKey(repository.projectPath)) {
+ ProjectModel project = getProjectModel(repository.projectPath);
+ if (project == null) {
+ logger.warn(MessageFormat.format("excluding project \"{0}\" from project list because it is empty!",
+ repository.projectPath));
+ continue;
+ }
+ projects.put(repository.projectPath, project);
+ // clear the repo list in the project because that is the system
+ // list, not the user-accessible list and start building the
+ // user-accessible list
+ project.repositories.clear();
+ project.repositories.add(repository.name);
+ project.lastChange = repository.lastChange;
+ } else {
+ // update the user-accessible list
+ // this is used for repository count
+ ProjectModel project = projects.get(repository.projectPath);
+ project.repositories.add(repository.name);
+ if (project.lastChange.before(repository.lastChange)) {
+ project.lastChange = repository.lastChange;
+ }
+ }
+ }
+ return new ArrayList<ProjectModel>(projects.values());
+ }
+
+ /**
+ * Workaround JGit. I need to access the raw config object directly in order
+ * to see if the config is dirty so that I can reload a repository model.
+ * If I use the stock JGit method to get the config it already reloads the
+ * config. If the config changes are made within Gitblit this is fine as
+ * the returned config will still be flagged as dirty. BUT... if the config
+ * is manipulated outside Gitblit then it fails to recognize this as dirty.
+ *
+ * @param r
+ * @return a config
+ */
+ private StoredConfig getRepositoryConfig(Repository r) {
+ try {
+ Field f = r.getClass().getDeclaredField("repoConfig");
+ f.setAccessible(true);
+ StoredConfig config = (StoredConfig) f.get(r);
+ return config;
+ } catch (Exception e) {
+ logger.error("Failed to retrieve \"repoConfig\" via reflection", e);
+ }
+ return r.getConfig();
+ }
+
+ /**
+ * Create a repository model from the configuration and repository data.
+ *
+ * @param repositoryName
+ * @return a repositoryModel or null if the repository does not exist
+ */
+ private RepositoryModel loadRepositoryModel(String repositoryName) {
+ Repository r = getRepository(repositoryName);
+ if (r == null) {
+ return null;
+ }
+ RepositoryModel model = new RepositoryModel();
+ model.isBare = r.isBare();
+ File basePath = getFileOrFolder(Keys.git.repositoriesFolder, "${baseFolder}/git");
+ if (model.isBare) {
+ model.name = com.gitblit.utils.FileUtils.getRelativePath(basePath, r.getDirectory());
+ } else {
+ model.name = com.gitblit.utils.FileUtils.getRelativePath(basePath, r.getDirectory().getParentFile());
+ }
+ model.hasCommits = JGitUtils.hasCommits(r);
+ model.lastChange = JGitUtils.getLastChange(r);
+ model.projectPath = StringUtils.getFirstPathElement(repositoryName);
+
+ StoredConfig config = r.getConfig();
+ boolean hasOrigin = !StringUtils.isEmpty(config.getString("remote", "origin", "url"));
+
+ if (config != null) {
+ model.description = getConfig(config, "description", "");
+ model.addOwners(ArrayUtils.fromString(getConfig(config, "owner", "")));
+ model.useTickets = getConfig(config, "useTickets", false);
+ model.useDocs = getConfig(config, "useDocs", false);
+ model.allowForks = getConfig(config, "allowForks", true);
+ model.accessRestriction = AccessRestrictionType.fromName(getConfig(config,
+ "accessRestriction", settings.getString(Keys.git.defaultAccessRestriction, null)));
+ model.authorizationControl = AuthorizationControl.fromName(getConfig(config,
+ "authorizationControl", settings.getString(Keys.git.defaultAuthorizationControl, null)));
+ model.verifyCommitter = getConfig(config, "verifyCommitter", false);
+ model.showRemoteBranches = getConfig(config, "showRemoteBranches", hasOrigin);
+ model.isFrozen = getConfig(config, "isFrozen", false);
+ model.showReadme = getConfig(config, "showReadme", false);
+ model.skipSizeCalculation = getConfig(config, "skipSizeCalculation", false);
+ model.skipSummaryMetrics = getConfig(config, "skipSummaryMetrics", false);
+ model.federationStrategy = FederationStrategy.fromName(getConfig(config,
+ "federationStrategy", null));
+ model.federationSets = new ArrayList<String>(Arrays.asList(config.getStringList(
+ Constants.CONFIG_GITBLIT, null, "federationSets")));
+ model.isFederated = getConfig(config, "isFederated", false);
+ model.gcThreshold = getConfig(config, "gcThreshold", settings.getString(Keys.git.defaultGarbageCollectionThreshold, "500KB"));
+ model.gcPeriod = getConfig(config, "gcPeriod", settings.getInteger(Keys.git.defaultGarbageCollectionPeriod, 7));
+ try {
+ model.lastGC = new SimpleDateFormat(Constants.ISO8601).parse(getConfig(config, "lastGC", "1970-01-01'T'00:00:00Z"));
+ } catch (Exception e) {
+ model.lastGC = new Date(0);
+ }
+ model.maxActivityCommits = getConfig(config, "maxActivityCommits", settings.getInteger(Keys.web.maxActivityCommits, 0));
+ model.origin = config.getString("remote", "origin", "url");
+ if (model.origin != null) {
+ model.origin = model.origin.replace('\\', '/');
+ }
+ model.preReceiveScripts = new ArrayList<String>(Arrays.asList(config.getStringList(
+ Constants.CONFIG_GITBLIT, null, "preReceiveScript")));
+ model.postReceiveScripts = new ArrayList<String>(Arrays.asList(config.getStringList(
+ Constants.CONFIG_GITBLIT, null, "postReceiveScript")));
+ model.mailingLists = new ArrayList<String>(Arrays.asList(config.getStringList(
+ Constants.CONFIG_GITBLIT, null, "mailingList")));
+ model.indexedBranches = new ArrayList<String>(Arrays.asList(config.getStringList(
+ Constants.CONFIG_GITBLIT, null, "indexBranch")));
+
+ // Custom defined properties
+ model.customFields = new LinkedHashMap<String, String>();
+ for (String aProperty : config.getNames(Constants.CONFIG_GITBLIT, Constants.CONFIG_CUSTOM_FIELDS)) {
+ model.customFields.put(aProperty, config.getString(Constants.CONFIG_GITBLIT, Constants.CONFIG_CUSTOM_FIELDS, aProperty));
+ }
+ }
+ model.HEAD = JGitUtils.getHEADRef(r);
+ model.availableRefs = JGitUtils.getAvailableHeadTargets(r);
+ model.sparkleshareId = JGitUtils.getSparkleshareId(r);
+ r.close();
+
+ if (model.origin != null && model.origin.startsWith("file://")) {
+ // repository was cloned locally... perhaps as a fork
+ try {
+ File folder = new File(new URI(model.origin));
+ String originRepo = com.gitblit.utils.FileUtils.getRelativePath(getRepositoriesFolder(), folder);
+ if (!StringUtils.isEmpty(originRepo)) {
+ // ensure origin still exists
+ File repoFolder = new File(getRepositoriesFolder(), originRepo);
+ if (repoFolder.exists()) {
+ model.originRepository = originRepo.toLowerCase();
+ }
+ }
+ } catch (URISyntaxException e) {
+ logger.error("Failed to determine fork for " + model, e);
+ }
+ }
+ return model;
+ }
+
+ /**
+ * Determines if this server has the requested repository.
+ *
+ * @param name
+ * @return true if the repository exists
+ */
+ public boolean hasRepository(String repositoryName) {
+ return hasRepository(repositoryName, false);
+ }
+
+ /**
+ * Determines if this server has the requested repository.
+ *
+ * @param name
+ * @param caseInsensitive
+ * @return true if the repository exists
+ */
+ public boolean hasRepository(String repositoryName, boolean caseSensitiveCheck) {
+ if (!caseSensitiveCheck && settings.getBoolean(Keys.git.cacheRepositoryList, true)) {
+ // if we are caching use the cache to determine availability
+ // otherwise we end up adding a phantom repository to the cache
+ return repositoryListCache.containsKey(repositoryName.toLowerCase());
+ }
+ Repository r = getRepository(repositoryName, false);
+ if (r == null) {
+ return false;
+ }
+ r.close();
+ return true;
+ }
+
+ /**
+ * Determines if the specified user has a fork of the specified origin
+ * repository.
+ *
+ * @param username
+ * @param origin
+ * @return true the if the user has a fork
+ */
+ public boolean hasFork(String username, String origin) {
+ return getFork(username, origin) != null;
+ }
+
+ /**
+ * Gets the name of a user's fork of the specified origin
+ * repository.
+ *
+ * @param username
+ * @param origin
+ * @return the name of the user's fork, null otherwise
+ */
+ public String getFork(String username, String origin) {
+ String userProject = "~" + username.toLowerCase();
+ if (settings.getBoolean(Keys.git.cacheRepositoryList, true)) {
+ String userPath = userProject + "/";
+
+ // collect all origin nodes in fork network
+ Set<String> roots = new HashSet<String>();
+ roots.add(origin);
+ RepositoryModel originModel = repositoryListCache.get(origin);
+ while (originModel != null) {
+ if (!ArrayUtils.isEmpty(originModel.forks)) {
+ for (String fork : originModel.forks) {
+ if (!fork.startsWith(userPath)) {
+ roots.add(fork);
+ }
+ }
+ }
+
+ if (originModel.originRepository != null) {
+ roots.add(originModel.originRepository);
+ originModel = repositoryListCache.get(originModel.originRepository);
+ } else {
+ // break
+ originModel = null;
+ }
+ }
+
+ for (String repository : repositoryListCache.keySet()) {
+ if (repository.startsWith(userPath)) {
+ RepositoryModel model = repositoryListCache.get(repository);
+ if (!StringUtils.isEmpty(model.originRepository)) {
+ if (roots.contains(model.originRepository)) {
+ // user has a fork in this graph
+ return model.name;
+ }
+ }
+ }
+ }
+ } else {
+ // not caching
+ ProjectModel project = getProjectModel(userProject);
+ for (String repository : project.repositories) {
+ if (repository.startsWith(userProject)) {
+ RepositoryModel model = getRepositoryModel(repository);
+ if (model.originRepository.equalsIgnoreCase(origin)) {
+ // user has a fork
+ return model.name;
+ }
+ }
+ }
+ }
+ // user does not have a fork
+ return null;
+ }
+
+ /**
+ * Returns the fork network for a repository by traversing up the fork graph
+ * to discover the root and then down through all children of the root node.
+ *
+ * @param repository
+ * @return a ForkModel
+ */
+ public ForkModel getForkNetwork(String repository) {
+ if (settings.getBoolean(Keys.git.cacheRepositoryList, true)) {
+ // find the root, cached
+ RepositoryModel model = repositoryListCache.get(repository.toLowerCase());
+ while (model.originRepository != null) {
+ model = repositoryListCache.get(model.originRepository);
+ }
+ ForkModel root = getForkModelFromCache(model.name);
+ return root;
+ } else {
+ // find the root, non-cached
+ RepositoryModel model = getRepositoryModel(repository.toLowerCase());
+ while (model.originRepository != null) {
+ model = getRepositoryModel(model.originRepository);
+ }
+ ForkModel root = getForkModel(model.name);
+ return root;
+ }
+ }
+
+ private ForkModel getForkModelFromCache(String repository) {
+ RepositoryModel model = repositoryListCache.get(repository.toLowerCase());
+ if (model == null) {
+ return null;
+ }
+ ForkModel fork = new ForkModel(model);
+ if (!ArrayUtils.isEmpty(model.forks)) {
+ for (String aFork : model.forks) {
+ ForkModel fm = getForkModelFromCache(aFork);
+ if (fm != null) {
+ fork.forks.add(fm);
+ }
+ }
+ }
+ return fork;
+ }
+
+ private ForkModel getForkModel(String repository) {
+ RepositoryModel model = getRepositoryModel(repository.toLowerCase());
+ if (model == null) {
+ return null;
+ }
+ ForkModel fork = new ForkModel(model);
+ if (!ArrayUtils.isEmpty(model.forks)) {
+ for (String aFork : model.forks) {
+ ForkModel fm = getForkModel(aFork);
+ if (fm != null) {
+ fork.forks.add(fm);
+ }
+ }
+ }
+ return fork;
+ }
+
+ /**
+ * Returns the size in bytes of the repository. Gitblit caches the
+ * repository sizes to reduce the performance penalty of recursive
+ * calculation. The cache is updated if the repository has been changed
+ * since the last calculation.
+ *
+ * @param model
+ * @return size in bytes
+ */
+ public long calculateSize(RepositoryModel model) {
+ if (repositorySizeCache.hasCurrent(model.name, model.lastChange)) {
+ return repositorySizeCache.getObject(model.name);
+ }
+ File gitDir = FileKey.resolve(new File(repositoriesFolder, model.name), FS.DETECTED);
+ long size = com.gitblit.utils.FileUtils.folderSize(gitDir);
+ repositorySizeCache.updateObject(model.name, model.lastChange, size);
+ return size;
+ }
+
+ /**
+ * Ensure that a cached repository is completely closed and its resources
+ * are properly released.
+ *
+ * @param repositoryName
+ */
+ private void closeRepository(String repositoryName) {
+ Repository repository = getRepository(repositoryName);
+ if (repository == null) {
+ return;
+ }
+ RepositoryCache.close(repository);
+
+ // assume 2 uses in case reflection fails
+ int uses = 2;
+ try {
+ // The FileResolver caches repositories which is very useful
+ // for performance until you want to delete a repository.
+ // I have to use reflection to call close() the correct
+ // number of times to ensure that the object and ref databases
+ // are properly closed before I can delete the repository from
+ // the filesystem.
+ Field useCnt = Repository.class.getDeclaredField("useCnt");
+ useCnt.setAccessible(true);
+ uses = ((AtomicInteger) useCnt.get(repository)).get();
+ } catch (Exception e) {
+ logger.warn(MessageFormat
+ .format("Failed to reflectively determine use count for repository {0}",
+ repositoryName), e);
+ }
+ if (uses > 0) {
+ logger.info(MessageFormat
+ .format("{0}.useCnt={1}, calling close() {2} time(s) to close object and ref databases",
+ repositoryName, uses, uses));
+ for (int i = 0; i < uses; i++) {
+ repository.close();
+ }
+ }
+
+ // close any open index writer/searcher in the Lucene executor
+ luceneExecutor.close(repositoryName);
+ }
+
+ /**
+ * Returns the metrics for the default branch of the specified repository.
+ * This method builds a metrics cache. The cache is updated if the
+ * repository is updated. A new copy of the metrics list is returned on each
+ * call so that modifications to the list are non-destructive.
+ *
+ * @param model
+ * @param repository
+ * @return a new array list of metrics
+ */
+ public List<Metric> getRepositoryDefaultMetrics(RepositoryModel model, Repository repository) {
+ if (repositoryMetricsCache.hasCurrent(model.name, model.lastChange)) {
+ return new ArrayList<Metric>(repositoryMetricsCache.getObject(model.name));
+ }
+ List<Metric> metrics = MetricUtils.getDateMetrics(repository, null, true, null, getTimezone());
+ repositoryMetricsCache.updateObject(model.name, model.lastChange, metrics);
+ return new ArrayList<Metric>(metrics);
+ }
+
+ /**
+ * Returns the gitblit string value for the specified key. If key is not
+ * set, returns defaultValue.
+ *
+ * @param config
+ * @param field
+ * @param defaultValue
+ * @return field value or defaultValue
+ */
+ private String getConfig(StoredConfig config, String field, String defaultValue) {
+ String value = config.getString(Constants.CONFIG_GITBLIT, null, field);
+ if (StringUtils.isEmpty(value)) {
+ return defaultValue;
+ }
+ return value;
+ }
+
+ /**
+ * Returns the gitblit boolean value for the specified key. If key is not
+ * set, returns defaultValue.
+ *
+ * @param config
+ * @param field
+ * @param defaultValue
+ * @return field value or defaultValue
+ */
+ private boolean getConfig(StoredConfig config, String field, boolean defaultValue) {
+ return config.getBoolean(Constants.CONFIG_GITBLIT, field, defaultValue);
+ }
+
+ /**
+ * Returns the gitblit string value for the specified key. If key is not
+ * set, returns defaultValue.
+ *
+ * @param config
+ * @param field
+ * @param defaultValue
+ * @return field value or defaultValue
+ */
+ private int getConfig(StoredConfig config, String field, int defaultValue) {
+ String value = config.getString(Constants.CONFIG_GITBLIT, null, field);
+ if (StringUtils.isEmpty(value)) {
+ return defaultValue;
+ }
+ try {
+ return Integer.parseInt(value);
+ } catch (Exception e) {
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Creates/updates the repository model keyed by reopsitoryName. Saves all
+ * repository settings in .git/config. This method allows for renaming
+ * repositories and will update user access permissions accordingly.
+ *
+ * All repositories created by this method are bare and automatically have
+ * .git appended to their names, which is the standard convention for bare
+ * repositories.
+ *
+ * @param repositoryName
+ * @param repository
+ * @param isCreate
+ * @throws GitBlitException
+ */
+ public void updateRepositoryModel(String repositoryName, RepositoryModel repository,
+ boolean isCreate) throws GitBlitException {
+ if (gcExecutor.isCollectingGarbage(repositoryName)) {
+ throw new GitBlitException(MessageFormat.format("sorry, Gitblit is busy collecting garbage in {0}",
+ repositoryName));
+ }
+ Repository r = null;
+ String projectPath = StringUtils.getFirstPathElement(repository.name);
+ if (!StringUtils.isEmpty(projectPath)) {
+ if (projectPath.equalsIgnoreCase(getString(Keys.web.repositoryRootGroupName, "main"))) {
+ // strip leading group name
+ repository.name = repository.name.substring(projectPath.length() + 1);
+ }
+ }
+ if (isCreate) {
+ // ensure created repository name ends with .git
+ if (!repository.name.toLowerCase().endsWith(org.eclipse.jgit.lib.Constants.DOT_GIT_EXT)) {
+ repository.name += org.eclipse.jgit.lib.Constants.DOT_GIT_EXT;
+ }
+ if (hasRepository(repository.name)) {
+ throw new GitBlitException(MessageFormat.format(
+ "Can not create repository ''{0}'' because it already exists.",
+ repository.name));
+ }
+ // create repository
+ logger.info("create repository " + repository.name);
+ r = JGitUtils.createRepository(repositoriesFolder, repository.name);
+ } else {
+ // rename repository
+ if (!repositoryName.equalsIgnoreCase(repository.name)) {
+ if (!repository.name.toLowerCase().endsWith(
+ org.eclipse.jgit.lib.Constants.DOT_GIT_EXT)) {
+ repository.name += org.eclipse.jgit.lib.Constants.DOT_GIT_EXT;
+ }
+ if (new File(repositoriesFolder, repository.name).exists()) {
+ throw new GitBlitException(MessageFormat.format(
+ "Failed to rename ''{0}'' because ''{1}'' already exists.",
+ repositoryName, repository.name));
+ }
+ closeRepository(repositoryName);
+ File folder = new File(repositoriesFolder, repositoryName);
+ File destFolder = new File(repositoriesFolder, repository.name);
+ if (destFolder.exists()) {
+ throw new GitBlitException(
+ MessageFormat
+ .format("Can not rename repository ''{0}'' to ''{1}'' because ''{1}'' already exists.",
+ repositoryName, repository.name));
+ }
+ File parentFile = destFolder.getParentFile();
+ if (!parentFile.exists() && !parentFile.mkdirs()) {
+ throw new GitBlitException(MessageFormat.format(
+ "Failed to create folder ''{0}''", parentFile.getAbsolutePath()));
+ }
+ if (!folder.renameTo(destFolder)) {
+ throw new GitBlitException(MessageFormat.format(
+ "Failed to rename repository ''{0}'' to ''{1}''.", repositoryName,
+ repository.name));
+ }
+ // rename the roles
+ if (!userService.renameRepositoryRole(repositoryName, repository.name)) {
+ throw new GitBlitException(MessageFormat.format(
+ "Failed to rename repository permissions ''{0}'' to ''{1}''.",
+ repositoryName, repository.name));
+ }
+
+ // rename fork origins in their configs
+ if (!ArrayUtils.isEmpty(repository.forks)) {
+ for (String fork : repository.forks) {
+ Repository rf = getRepository(fork);
+ try {
+ StoredConfig config = rf.getConfig();
+ String origin = config.getString("remote", "origin", "url");
+ origin = origin.replace(repositoryName, repository.name);
+ config.setString("remote", "origin", "url", origin);
+ config.save();
+ } catch (Exception e) {
+ logger.error("Failed to update repository fork config for " + fork, e);
+ }
+ rf.close();
+ }
+ }
+
+ // remove this repository from any origin model's fork list
+ if (!StringUtils.isEmpty(repository.originRepository)) {
+ RepositoryModel origin = repositoryListCache.get(repository.originRepository);
+ if (origin != null && !ArrayUtils.isEmpty(origin.forks)) {
+ origin.forks.remove(repositoryName);
+ }
+ }
+
+ // clear the cache
+ clearRepositoryMetadataCache(repositoryName);
+ repository.resetDisplayName();
+ }
+
+ // load repository
+ logger.info("edit repository " + repository.name);
+ r = getRepository(repository.name);
+ }
+
+ // update settings
+ if (r != null) {
+ updateConfiguration(r, repository);
+ // only update symbolic head if it changes
+ String currentRef = JGitUtils.getHEADRef(r);
+ if (!StringUtils.isEmpty(repository.HEAD) && !repository.HEAD.equals(currentRef)) {
+ logger.info(MessageFormat.format("Relinking {0} HEAD from {1} to {2}",
+ repository.name, currentRef, repository.HEAD));
+ if (JGitUtils.setHEADtoRef(r, repository.HEAD)) {
+ // clear the cache
+ clearRepositoryMetadataCache(repository.name);
+ }
+ }
+
+ // close the repository object
+ r.close();
+ }
+
+ // update repository cache
+ removeFromCachedRepositoryList(repositoryName);
+ // model will actually be replaced on next load because config is stale
+ addToCachedRepositoryList(repository);
+ }
+
+ /**
+ * Updates the Gitblit configuration for the specified repository.
+ *
+ * @param r
+ * the Git repository
+ * @param repository
+ * the Gitblit repository model
+ */
+ public void updateConfiguration(Repository r, RepositoryModel repository) {
+ StoredConfig config = r.getConfig();
+ config.setString(Constants.CONFIG_GITBLIT, null, "description", repository.description);
+ config.setString(Constants.CONFIG_GITBLIT, null, "owner", ArrayUtils.toString(repository.owners));
+ config.setBoolean(Constants.CONFIG_GITBLIT, null, "useTickets", repository.useTickets);
+ config.setBoolean(Constants.CONFIG_GITBLIT, null, "useDocs", repository.useDocs);
+ config.setBoolean(Constants.CONFIG_GITBLIT, null, "allowForks", repository.allowForks);
+ config.setString(Constants.CONFIG_GITBLIT, null, "accessRestriction", repository.accessRestriction.name());
+ config.setString(Constants.CONFIG_GITBLIT, null, "authorizationControl", repository.authorizationControl.name());
+ config.setBoolean(Constants.CONFIG_GITBLIT, null, "verifyCommitter", repository.verifyCommitter);
+ config.setBoolean(Constants.CONFIG_GITBLIT, null, "showRemoteBranches", repository.showRemoteBranches);
+ config.setBoolean(Constants.CONFIG_GITBLIT, null, "isFrozen", repository.isFrozen);
+ config.setBoolean(Constants.CONFIG_GITBLIT, null, "showReadme", repository.showReadme);
+ config.setBoolean(Constants.CONFIG_GITBLIT, null, "skipSizeCalculation", repository.skipSizeCalculation);
+ config.setBoolean(Constants.CONFIG_GITBLIT, null, "skipSummaryMetrics", repository.skipSummaryMetrics);
+ config.setString(Constants.CONFIG_GITBLIT, null, "federationStrategy",
+ repository.federationStrategy.name());
+ config.setBoolean(Constants.CONFIG_GITBLIT, null, "isFederated", repository.isFederated);
+ config.setString(Constants.CONFIG_GITBLIT, null, "gcThreshold", repository.gcThreshold);
+ if (repository.gcPeriod == settings.getInteger(Keys.git.defaultGarbageCollectionPeriod, 7)) {
+ // use default from config
+ config.unset(Constants.CONFIG_GITBLIT, null, "gcPeriod");
+ } else {
+ config.setInt(Constants.CONFIG_GITBLIT, null, "gcPeriod", repository.gcPeriod);
+ }
+ if (repository.lastGC != null) {
+ config.setString(Constants.CONFIG_GITBLIT, null, "lastGC", new SimpleDateFormat(Constants.ISO8601).format(repository.lastGC));
+ }
+ if (repository.maxActivityCommits == settings.getInteger(Keys.web.maxActivityCommits, 0)) {
+ // use default from config
+ config.unset(Constants.CONFIG_GITBLIT, null, "maxActivityCommits");
+ } else {
+ config.setInt(Constants.CONFIG_GITBLIT, null, "maxActivityCommits", repository.maxActivityCommits);
+ }
+
+ updateList(config, "federationSets", repository.federationSets);
+ updateList(config, "preReceiveScript", repository.preReceiveScripts);
+ updateList(config, "postReceiveScript", repository.postReceiveScripts);
+ updateList(config, "mailingList", repository.mailingLists);
+ updateList(config, "indexBranch", repository.indexedBranches);
+
+ // User Defined Properties
+ if (repository.customFields != null) {
+ if (repository.customFields.size() == 0) {
+ // clear section
+ config.unsetSection(Constants.CONFIG_GITBLIT, Constants.CONFIG_CUSTOM_FIELDS);
+ } else {
+ for (Entry<String, String> property : repository.customFields.entrySet()) {
+ // set field
+ String key = property.getKey();
+ String value = property.getValue();
+ config.setString(Constants.CONFIG_GITBLIT, Constants.CONFIG_CUSTOM_FIELDS, key, value);
+ }
+ }
+ }
+
+ try {
+ config.save();
+ } catch (IOException e) {
+ logger.error("Failed to save repository config!", e);
+ }
+ }
+
+ private void updateList(StoredConfig config, String field, List<String> list) {
+ // a null list is skipped, not cleared
+ // this is for RPC administration where an older manager might be used
+ if (list == null) {
+ return;
+ }
+ if (ArrayUtils.isEmpty(list)) {
+ config.unset(Constants.CONFIG_GITBLIT, null, field);
+ } else {
+ config.setStringList(Constants.CONFIG_GITBLIT, null, field, list);
+ }
+ }
+
+ /**
+ * Deletes the repository from the file system and removes the repository
+ * permission from all repository users.
+ *
+ * @param model
+ * @return true if successful
+ */
+ public boolean deleteRepositoryModel(RepositoryModel model) {
+ return deleteRepository(model.name);
+ }
+
+ /**
+ * Deletes the repository from the file system and removes the repository
+ * permission from all repository users.
+ *
+ * @param repositoryName
+ * @return true if successful
+ */
+ public boolean deleteRepository(String repositoryName) {
+ try {
+ closeRepository(repositoryName);
+ // clear the repository cache
+ clearRepositoryMetadataCache(repositoryName);
+
+ RepositoryModel model = removeFromCachedRepositoryList(repositoryName);
+ if (model != null && !ArrayUtils.isEmpty(model.forks)) {
+ resetRepositoryListCache();
+ }
+
+ File folder = new File(repositoriesFolder, repositoryName);
+ if (folder.exists() && folder.isDirectory()) {
+ FileUtils.delete(folder, FileUtils.RECURSIVE | FileUtils.RETRY);
+ if (userService.deleteRepositoryRole(repositoryName)) {
+ logger.info(MessageFormat.format("Repository \"{0}\" deleted", repositoryName));
+ return true;
+ }
+ }
+ } catch (Throwable t) {
+ logger.error(MessageFormat.format("Failed to delete repository {0}", repositoryName), t);
+ }
+ return false;
+ }
+
+ /**
+ * Returns an html version of the commit message with any global or
+ * repository-specific regular expression substitution applied.
+ *
+ * @param repositoryName
+ * @param text
+ * @return html version of the commit message
+ */
+ public String processCommitMessage(String repositoryName, String text) {
+ String html = StringUtils.breakLinesForHtml(text);
+ Map<String, String> map = new HashMap<String, String>();
+ // global regex keys
+ if (settings.getBoolean(Keys.regex.global, false)) {
+ for (String key : settings.getAllKeys(Keys.regex.global)) {
+ if (!key.equals(Keys.regex.global)) {
+ String subKey = key.substring(key.lastIndexOf('.') + 1);
+ map.put(subKey, settings.getString(key, ""));
+ }
+ }
+ }
+
+ // repository-specific regex keys
+ List<String> keys = settings.getAllKeys(Keys.regex._ROOT + "."
+ + repositoryName.toLowerCase());
+ for (String key : keys) {
+ String subKey = key.substring(key.lastIndexOf('.') + 1);
+ map.put(subKey, settings.getString(key, ""));
+ }
+
+ for (Entry<String, String> entry : map.entrySet()) {
+ String definition = entry.getValue().trim();
+ String[] chunks = definition.split("!!!");
+ if (chunks.length == 2) {
+ html = html.replaceAll(chunks[0], chunks[1]);
+ } else {
+ logger.warn(entry.getKey()
+ + " improperly formatted. Use !!! to separate match from replacement: "
+ + definition);
+ }
+ }
+ return html;
+ }
+
+ /**
+ * Returns Gitblit's scheduled executor service for scheduling tasks.
+ *
+ * @return scheduledExecutor
+ */
+ public ScheduledExecutorService executor() {
+ return scheduledExecutor;
+ }
+
+ public static boolean canFederate() {
+ String passphrase = getString(Keys.federation.passphrase, "");
+ return !StringUtils.isEmpty(passphrase);
+ }
+
+ /**
+ * Configures this Gitblit instance to pull any registered federated gitblit
+ * instances.
+ */
+ private void configureFederation() {
+ boolean validPassphrase = true;
+ String passphrase = settings.getString(Keys.federation.passphrase, "");
+ if (StringUtils.isEmpty(passphrase)) {
+ logger.warn("Federation passphrase is blank! This server can not be PULLED from.");
+ validPassphrase = false;
+ }
+ if (validPassphrase) {
+ // standard tokens
+ for (FederationToken tokenType : FederationToken.values()) {
+ logger.info(MessageFormat.format("Federation {0} token = {1}", tokenType.name(),
+ getFederationToken(tokenType)));
+ }
+
+ // federation set tokens
+ for (String set : settings.getStrings(Keys.federation.sets)) {
+ logger.info(MessageFormat.format("Federation Set {0} token = {1}", set,
+ getFederationToken(set)));
+ }
+ }
+
+ // Schedule the federation executor
+ List<FederationModel> registrations = getFederationRegistrations();
+ if (registrations.size() > 0) {
+ FederationPullExecutor executor = new FederationPullExecutor(registrations, true);
+ scheduledExecutor.schedule(executor, 1, TimeUnit.MINUTES);
+ }
+ }
+
+ /**
+ * Returns the list of federated gitblit instances that this instance will
+ * try to pull.
+ *
+ * @return list of registered gitblit instances
+ */
+ public List<FederationModel> getFederationRegistrations() {
+ if (federationRegistrations.isEmpty()) {
+ federationRegistrations.addAll(FederationUtils.getFederationRegistrations(settings));
+ }
+ return federationRegistrations;
+ }
+
+ /**
+ * Retrieve the specified federation registration.
+ *
+ * @param name
+ * the name of the registration
+ * @return a federation registration
+ */
+ public FederationModel getFederationRegistration(String url, String name) {
+ // check registrations
+ for (FederationModel r : getFederationRegistrations()) {
+ if (r.name.equals(name) && r.url.equals(url)) {
+ return r;
+ }
+ }
+
+ // check the results
+ for (FederationModel r : getFederationResultRegistrations()) {
+ if (r.name.equals(name) && r.url.equals(url)) {
+ return r;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the list of federation sets.
+ *
+ * @return list of federation sets
+ */
+ public List<FederationSet> getFederationSets(String gitblitUrl) {
+ List<FederationSet> list = new ArrayList<FederationSet>();
+ // generate standard tokens
+ for (FederationToken type : FederationToken.values()) {
+ FederationSet fset = new FederationSet(type.toString(), type, getFederationToken(type));
+ fset.repositories = getRepositories(gitblitUrl, fset.token);
+ list.add(fset);
+ }
+ // generate tokens for federation sets
+ for (String set : settings.getStrings(Keys.federation.sets)) {
+ FederationSet fset = new FederationSet(set, FederationToken.REPOSITORIES,
+ getFederationToken(set));
+ fset.repositories = getRepositories(gitblitUrl, fset.token);
+ list.add(fset);
+ }
+ return list;
+ }
+
+ /**
+ * Returns the list of possible federation tokens for this Gitblit instance.
+ *
+ * @return list of federation tokens
+ */
+ public List<String> getFederationTokens() {
+ List<String> tokens = new ArrayList<String>();
+ // generate standard tokens
+ for (FederationToken type : FederationToken.values()) {
+ tokens.add(getFederationToken(type));
+ }
+ // generate tokens for federation sets
+ for (String set : settings.getStrings(Keys.federation.sets)) {
+ tokens.add(getFederationToken(set));
+ }
+ return tokens;
+ }
+
+ /**
+ * Returns the specified federation token for this Gitblit instance.
+ *
+ * @param type
+ * @return a federation token
+ */
+ public String getFederationToken(FederationToken type) {
+ return getFederationToken(type.name());
+ }
+
+ /**
+ * Returns the specified federation token for this Gitblit instance.
+ *
+ * @param value
+ * @return a federation token
+ */
+ public String getFederationToken(String value) {
+ String passphrase = settings.getString(Keys.federation.passphrase, "");
+ return StringUtils.getSHA1(passphrase + "-" + value);
+ }
+
+ /**
+ * Compares the provided token with this Gitblit instance's tokens and
+ * determines if the requested permission may be granted to the token.
+ *
+ * @param req
+ * @param token
+ * @return true if the request can be executed
+ */
+ public boolean validateFederationRequest(FederationRequest req, String token) {
+ String all = getFederationToken(FederationToken.ALL);
+ String unr = getFederationToken(FederationToken.USERS_AND_REPOSITORIES);
+ String jur = getFederationToken(FederationToken.REPOSITORIES);
+ switch (req) {
+ case PULL_REPOSITORIES:
+ return token.equals(all) || token.equals(unr) || token.equals(jur);
+ case PULL_USERS:
+ case PULL_TEAMS:
+ return token.equals(all) || token.equals(unr);
+ case PULL_SETTINGS:
+ case PULL_SCRIPTS:
+ return token.equals(all);
+ default:
+ break;
+ }
+ return false;
+ }
+
+ /**
+ * Acknowledge and cache the status of a remote Gitblit instance.
+ *
+ * @param identification
+ * the identification of the pulling Gitblit instance
+ * @param registration
+ * the registration from the pulling Gitblit instance
+ * @return true if acknowledged
+ */
+ public boolean acknowledgeFederationStatus(String identification, FederationModel registration) {
+ // reset the url to the identification of the pulling Gitblit instance
+ registration.url = identification;
+ String id = identification;
+ if (!StringUtils.isEmpty(registration.folder)) {
+ id += "-" + registration.folder;
+ }
+ federationPullResults.put(id, registration);
+ return true;
+ }
+
+ /**
+ * Returns the list of registration results.
+ *
+ * @return the list of registration results
+ */
+ public List<FederationModel> getFederationResultRegistrations() {
+ return new ArrayList<FederationModel>(federationPullResults.values());
+ }
+
+ /**
+ * Submit a federation proposal. The proposal is cached locally and the
+ * Gitblit administrator(s) are notified via email.
+ *
+ * @param proposal
+ * the proposal
+ * @param gitblitUrl
+ * the url of your gitblit instance to send an email to
+ * administrators
+ * @return true if the proposal was submitted
+ */
+ public boolean submitFederationProposal(FederationProposal proposal, String gitblitUrl) {
+ // convert proposal to json
+ String json = JsonUtils.toJsonString(proposal);
+
+ try {
+ // make the proposals folder
+ File proposalsFolder = getProposalsFolder();
+ proposalsFolder.mkdirs();
+
+ // cache json to a file
+ File file = new File(proposalsFolder, proposal.token + Constants.PROPOSAL_EXT);
+ com.gitblit.utils.FileUtils.writeContent(file, json);
+ } catch (Exception e) {
+ logger.error(MessageFormat.format("Failed to cache proposal from {0}", proposal.url), e);
+ }
+
+ // send an email, if possible
+ try {
+ Message message = mailExecutor.createMessageForAdministrators();
+ if (message != null) {
+ message.setSubject("Federation proposal from " + proposal.url);
+ message.setText("Please review the proposal @ " + gitblitUrl + "/proposal/"
+ + proposal.token);
+ mailExecutor.queue(message);
+ }
+ } catch (Throwable t) {
+ logger.error("Failed to notify administrators of proposal", t);
+ }
+ return true;
+ }
+
+ /**
+ * Returns the list of pending federation proposals
+ *
+ * @return list of federation proposals
+ */
+ public List<FederationProposal> getPendingFederationProposals() {
+ List<FederationProposal> list = new ArrayList<FederationProposal>();
+ File folder = getProposalsFolder();
+ if (folder.exists()) {
+ File[] files = folder.listFiles(new FileFilter() {
+ @Override
+ public boolean accept(File file) {
+ return file.isFile()
+ && file.getName().toLowerCase().endsWith(Constants.PROPOSAL_EXT);
+ }
+ });
+ for (File file : files) {
+ String json = com.gitblit.utils.FileUtils.readContent(file, null);
+ FederationProposal proposal = JsonUtils.fromJsonString(json,
+ FederationProposal.class);
+ list.add(proposal);
+ }
+ }
+ return list;
+ }
+
+ /**
+ * Get repositories for the specified token.
+ *
+ * @param gitblitUrl
+ * the base url of this gitblit instance
+ * @param token
+ * the federation token
+ * @return a map of <cloneurl, RepositoryModel>
+ */
+ public Map<String, RepositoryModel> getRepositories(String gitblitUrl, String token) {
+ Map<String, String> federationSets = new HashMap<String, String>();
+ for (String set : getStrings(Keys.federation.sets)) {
+ federationSets.put(getFederationToken(set), set);
+ }
+
+ // Determine the Gitblit clone url
+ StringBuilder sb = new StringBuilder();
+ sb.append(gitblitUrl);
+ sb.append(Constants.GIT_PATH);
+ sb.append("{0}");
+ String cloneUrl = sb.toString();
+
+ // Retrieve all available repositories
+ UserModel user = new UserModel(Constants.FEDERATION_USER);
+ user.canAdmin = true;
+ List<RepositoryModel> list = getRepositoryModels(user);
+
+ // create the [cloneurl, repositoryModel] map
+ Map<String, RepositoryModel> repositories = new HashMap<String, RepositoryModel>();
+ for (RepositoryModel model : list) {
+ // by default, setup the url for THIS repository
+ String url = MessageFormat.format(cloneUrl, model.name);
+ switch (model.federationStrategy) {
+ case EXCLUDE:
+ // skip this repository
+ continue;
+ case FEDERATE_ORIGIN:
+ // federate the origin, if it is defined
+ if (!StringUtils.isEmpty(model.origin)) {
+ url = model.origin;
+ }
+ break;
+ default:
+ break;
+ }
+
+ if (federationSets.containsKey(token)) {
+ // include repositories only for federation set
+ String set = federationSets.get(token);
+ if (model.federationSets.contains(set)) {
+ repositories.put(url, model);
+ }
+ } else {
+ // standard federation token for ALL
+ repositories.put(url, model);
+ }
+ }
+ return repositories;
+ }
+
+ /**
+ * Creates a proposal from the token.
+ *
+ * @param gitblitUrl
+ * the url of this Gitblit instance
+ * @param token
+ * @return a potential proposal
+ */
+ public FederationProposal createFederationProposal(String gitblitUrl, String token) {
+ FederationToken tokenType = FederationToken.REPOSITORIES;
+ for (FederationToken type : FederationToken.values()) {
+ if (token.equals(getFederationToken(type))) {
+ tokenType = type;
+ break;
+ }
+ }
+ Map<String, RepositoryModel> repositories = getRepositories(gitblitUrl, token);
+ FederationProposal proposal = new FederationProposal(gitblitUrl, tokenType, token,
+ repositories);
+ return proposal;
+ }
+
+ /**
+ * Returns the proposal identified by the supplied token.
+ *
+ * @param token
+ * @return the specified proposal or null
+ */
+ public FederationProposal getPendingFederationProposal(String token) {
+ List<FederationProposal> list = getPendingFederationProposals();
+ for (FederationProposal proposal : list) {
+ if (proposal.token.equals(token)) {
+ return proposal;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Deletes a pending federation proposal.
+ *
+ * @param a
+ * proposal
+ * @return true if the proposal was deleted
+ */
+ public boolean deletePendingFederationProposal(FederationProposal proposal) {
+ File folder = getProposalsFolder();
+ File file = new File(folder, proposal.token + Constants.PROPOSAL_EXT);
+ return file.delete();
+ }
+
+ /**
+ * Returns the list of all Groovy push hook scripts. Script files must have
+ * .groovy extension
+ *
+ * @return list of available hook scripts
+ */
+ public List<String> getAllScripts() {
+ File groovyFolder = getGroovyScriptsFolder();
+ File[] files = groovyFolder.listFiles(new FileFilter() {
+ @Override
+ public boolean accept(File pathname) {
+ return pathname.isFile() && pathname.getName().endsWith(".groovy");
+ }
+ });
+ List<String> scripts = new ArrayList<String>();
+ if (files != null) {
+ for (File file : files) {
+ String script = file.getName().substring(0, file.getName().lastIndexOf('.'));
+ scripts.add(script);
+ }
+ }
+ return scripts;
+ }
+
+ /**
+ * Returns the list of pre-receive scripts the repository inherited from the
+ * global settings and team affiliations.
+ *
+ * @param repository
+ * if null only the globally specified scripts are returned
+ * @return a list of scripts
+ */
+ public List<String> getPreReceiveScriptsInherited(RepositoryModel repository) {
+ Set<String> scripts = new LinkedHashSet<String>();
+ // Globals
+ for (String script : getStrings(Keys.groovy.preReceiveScripts)) {
+ if (script.endsWith(".groovy")) {
+ scripts.add(script.substring(0, script.lastIndexOf('.')));
+ } else {
+ scripts.add(script);
+ }
+ }
+
+ // Team Scripts
+ if (repository != null) {
+ for (String teamname : userService.getTeamnamesForRepositoryRole(repository.name)) {
+ TeamModel team = userService.getTeamModel(teamname);
+ scripts.addAll(team.preReceiveScripts);
+ }
+ }
+ return new ArrayList<String>(scripts);
+ }
+
+ /**
+ * Returns the list of all available Groovy pre-receive push hook scripts
+ * that are not already inherited by the repository. Script files must have
+ * .groovy extension
+ *
+ * @param repository
+ * optional parameter
+ * @return list of available hook scripts
+ */
+ public List<String> getPreReceiveScriptsUnused(RepositoryModel repository) {
+ Set<String> inherited = new TreeSet<String>(getPreReceiveScriptsInherited(repository));
+
+ // create list of available scripts by excluding inherited scripts
+ List<String> scripts = new ArrayList<String>();
+ for (String script : getAllScripts()) {
+ if (!inherited.contains(script)) {
+ scripts.add(script);
+ }
+ }
+ return scripts;
+ }
+
+ /**
+ * Returns the list of post-receive scripts the repository inherited from
+ * the global settings and team affiliations.
+ *
+ * @param repository
+ * if null only the globally specified scripts are returned
+ * @return a list of scripts
+ */
+ public List<String> getPostReceiveScriptsInherited(RepositoryModel repository) {
+ Set<String> scripts = new LinkedHashSet<String>();
+ // Global Scripts
+ for (String script : getStrings(Keys.groovy.postReceiveScripts)) {
+ if (script.endsWith(".groovy")) {
+ scripts.add(script.substring(0, script.lastIndexOf('.')));
+ } else {
+ scripts.add(script);
+ }
+ }
+ // Team Scripts
+ if (repository != null) {
+ for (String teamname : userService.getTeamnamesForRepositoryRole(repository.name)) {
+ TeamModel team = userService.getTeamModel(teamname);
+ scripts.addAll(team.postReceiveScripts);
+ }
+ }
+ return new ArrayList<String>(scripts);
+ }
+
+ /**
+ * Returns the list of unused Groovy post-receive push hook scripts that are
+ * not already inherited by the repository. Script files must have .groovy
+ * extension
+ *
+ * @param repository
+ * optional parameter
+ * @return list of available hook scripts
+ */
+ public List<String> getPostReceiveScriptsUnused(RepositoryModel repository) {
+ Set<String> inherited = new TreeSet<String>(getPostReceiveScriptsInherited(repository));
+
+ // create list of available scripts by excluding inherited scripts
+ List<String> scripts = new ArrayList<String>();
+ for (String script : getAllScripts()) {
+ if (!inherited.contains(script)) {
+ scripts.add(script);
+ }
+ }
+ return scripts;
+ }
+
+ /**
+ * Search the specified repositories using the Lucene query.
+ *
+ * @param query
+ * @param page
+ * @param pageSize
+ * @param repositories
+ * @return
+ */
+ public List<SearchResult> search(String query, int page, int pageSize, List<String> repositories) {
+ List<SearchResult> srs = luceneExecutor.search(query, page, pageSize, repositories);
+ return srs;
+ }
+
+ /**
+ * Notify the administrators by email.
+ *
+ * @param subject
+ * @param message
+ */
+ public void sendMailToAdministrators(String subject, String message) {
+ try {
+ Message mail = mailExecutor.createMessageForAdministrators();
+ if (mail != null) {
+ mail.setSubject(subject);
+ mail.setText(message);
+ mailExecutor.queue(mail);
+ }
+ } catch (MessagingException e) {
+ logger.error("Messaging error", e);
+ }
+ }
+
+ /**
+ * Notify users by email of something.
+ *
+ * @param subject
+ * @param message
+ * @param toAddresses
+ */
+ public void sendMail(String subject, String message, Collection<String> toAddresses) {
+ this.sendMail(subject, message, toAddresses.toArray(new String[0]));
+ }
+
+ /**
+ * Notify users by email of something.
+ *
+ * @param subject
+ * @param message
+ * @param toAddresses
+ */
+ public void sendMail(String subject, String message, String... toAddresses) {
+ try {
+ Message mail = mailExecutor.createMessage(toAddresses);
+ if (mail != null) {
+ mail.setSubject(subject);
+ mail.setText(message);
+ mailExecutor.queue(mail);
+ }
+ } catch (MessagingException e) {
+ logger.error("Messaging error", e);
+ }
+ }
+
+ /**
+ * Notify users by email of something.
+ *
+ * @param subject
+ * @param message
+ * @param toAddresses
+ */
+ public void sendHtmlMail(String subject, String message, Collection<String> toAddresses) {
+ this.sendHtmlMail(subject, message, toAddresses.toArray(new String[0]));
+ }
+
+ /**
+ * Notify users by email of something.
+ *
+ * @param subject
+ * @param message
+ * @param toAddresses
+ */
+ public void sendHtmlMail(String subject, String message, String... toAddresses) {
+ try {
+ Message mail = mailExecutor.createMessage(toAddresses);
+ if (mail != null) {
+ mail.setSubject(subject);
+ mail.setContent(message, "text/html");
+ mailExecutor.queue(mail);
+ }
+ } catch (MessagingException e) {
+ logger.error("Messaging error", e);
+ }
+ }
+
+ /**
+ * Returns the descriptions/comments of the Gitblit config settings.
+ *
+ * @return SettingsModel
+ */
+ public ServerSettings getSettingsModel() {
+ // ensure that the current values are updated in the setting models
+ for (String key : settings.getAllKeys(null)) {
+ SettingModel setting = settingsModel.get(key);
+ if (setting == null) {
+ // unreferenced setting, create a setting model
+ setting = new SettingModel();
+ setting.name = key;
+ settingsModel.add(setting);
+ }
+ setting.currentValue = settings.getString(key, "");
+ }
+ settingsModel.pushScripts = getAllScripts();
+ return settingsModel;
+ }
+
+ /**
+ * Parse the properties file and aggregate all the comments by the setting
+ * key. A setting model tracks the current value, the default value, the
+ * description of the setting and and directives about the setting.
+ * @param referencePropertiesInputStream
+ *
+ * @return Map<String, SettingModel>
+ */
+ private ServerSettings loadSettingModels(InputStream referencePropertiesInputStream) {
+ ServerSettings settingsModel = new ServerSettings();
+ settingsModel.supportsCredentialChanges = userService.supportsCredentialChanges();
+ settingsModel.supportsDisplayNameChanges = userService.supportsDisplayNameChanges();
+ settingsModel.supportsEmailAddressChanges = userService.supportsEmailAddressChanges();
+ settingsModel.supportsTeamMembershipChanges = userService.supportsTeamMembershipChanges();
+ try {
+ // Read bundled Gitblit properties to extract setting descriptions.
+ // This copy is pristine and only used for populating the setting
+ // models map.
+ InputStream is = referencePropertiesInputStream;
+ BufferedReader propertiesReader = new BufferedReader(new InputStreamReader(is));
+ StringBuilder description = new StringBuilder();
+ SettingModel setting = new SettingModel();
+ String line = null;
+ while ((line = propertiesReader.readLine()) != null) {
+ if (line.length() == 0) {
+ description.setLength(0);
+ setting = new SettingModel();
+ } else {
+ if (line.charAt(0) == '#') {
+ if (line.length() > 1) {
+ String text = line.substring(1).trim();
+ if (SettingModel.CASE_SENSITIVE.equals(text)) {
+ setting.caseSensitive = true;
+ } else if (SettingModel.RESTART_REQUIRED.equals(text)) {
+ setting.restartRequired = true;
+ } else if (SettingModel.SPACE_DELIMITED.equals(text)) {
+ setting.spaceDelimited = true;
+ } else if (text.startsWith(SettingModel.SINCE)) {
+ try {
+ setting.since = text.split(" ")[1];
+ } catch (Exception e) {
+ setting.since = text;
+ }
+ } else {
+ description.append(text);
+ description.append('\n');
+ }
+ }
+ } else {
+ String[] kvp = line.split("=", 2);
+ String key = kvp[0].trim();
+ setting.name = key;
+ setting.defaultValue = kvp[1].trim();
+ setting.currentValue = setting.defaultValue;
+ setting.description = description.toString().trim();
+ settingsModel.add(setting);
+ description.setLength(0);
+ setting = new SettingModel();
+ }
+ }
+ }
+ propertiesReader.close();
+ } catch (NullPointerException e) {
+ logger.error("Failed to find resource copy of gitblit.properties");
+ } catch (IOException e) {
+ logger.error("Failed to load resource copy of gitblit.properties");
+ }
+ return settingsModel;
+ }
+
+ /**
+ * Configure the Gitblit singleton with the specified settings source. This
+ * source may be file settings (Gitblit GO) or may be web.xml settings
+ * (Gitblit WAR).
+ *
+ * @param settings
+ */
+ public void configureContext(IStoredSettings settings, File folder, boolean startFederation) {
+ this.settings = settings;
+ this.baseFolder = folder;
+
+ repositoriesFolder = getRepositoriesFolder();
+
+ logger.info("Gitblit base folder = " + folder.getAbsolutePath());
+ logger.info("Git repositories folder = " + repositoriesFolder.getAbsolutePath());
+ logger.info("Gitblit settings = " + settings.toString());
+
+ // prepare service executors
+ mailExecutor = new MailExecutor(settings);
+ luceneExecutor = new LuceneExecutor(settings, repositoriesFolder);
+ gcExecutor = new GCExecutor(settings);
+
+ // calculate repository list settings checksum for future config changes
+ repositoryListSettingsChecksum.set(getRepositoryListSettingsChecksum());
+
+ // build initial repository list
+ if (settings.getBoolean(Keys.git.cacheRepositoryList, true)) {
+ logger.info("Identifying available repositories...");
+ getRepositoryList();
+ }
+
+ logTimezone("JVM", TimeZone.getDefault());
+ logTimezone(Constants.NAME, getTimezone());
+
+ serverStatus = new ServerStatus(isGO());
+
+ if (this.userService == null) {
+ String realm = settings.getString(Keys.realm.userService, "${baseFolder}/users.properties");
+ IUserService loginService = null;
+ try {
+ // check to see if this "file" is a login service class
+ Class<?> realmClass = Class.forName(realm);
+ loginService = (IUserService) realmClass.newInstance();
+ } catch (Throwable t) {
+ loginService = new GitblitUserService();
+ }
+ setUserService(loginService);
+ }
+
+ // load and cache the project metadata
+ projectConfigs = new FileBasedConfig(getFileOrFolder(Keys.web.projectsFile, "${baseFolder}/projects.conf"), FS.detect());
+ getProjectConfigs();
+
+ // schedule mail engine
+ if (mailExecutor.isReady()) {
+ logger.info("Mail executor is scheduled to process the message queue every 2 minutes.");
+ scheduledExecutor.scheduleAtFixedRate(mailExecutor, 1, 2, TimeUnit.MINUTES);
+ } else {
+ logger.warn("Mail server is not properly configured. Mail services disabled.");
+ }
+
+ // schedule lucene engine
+ enableLuceneIndexing();
+
+
+ // schedule gc engine
+ if (gcExecutor.isReady()) {
+ logger.info("GC executor is scheduled to scan repositories every 24 hours.");
+ Calendar c = Calendar.getInstance();
+ c.set(Calendar.HOUR_OF_DAY, settings.getInteger(Keys.git.garbageCollectionHour, 0));
+ c.set(Calendar.MINUTE, 0);
+ c.set(Calendar.SECOND, 0);
+ c.set(Calendar.MILLISECOND, 0);
+ Date cd = c.getTime();
+ Date now = new Date();
+ int delay = 0;
+ if (cd.before(now)) {
+ c.add(Calendar.DATE, 1);
+ cd = c.getTime();
+ }
+ delay = (int) ((cd.getTime() - now.getTime())/TimeUtils.MIN);
+ String when = delay + " mins";
+ if (delay > 60) {
+ when = MessageFormat.format("{0,number,0.0} hours", ((float)delay)/60f);
+ }
+ logger.info(MessageFormat.format("Next scheculed GC scan is in {0}", when));
+ scheduledExecutor.scheduleAtFixedRate(gcExecutor, delay, 60*24, TimeUnit.MINUTES);
+ }
+
+ if (startFederation) {
+ configureFederation();
+ }
+
+ // Configure JGit
+ WindowCacheConfig cfg = new WindowCacheConfig();
+
+ cfg.setPackedGitWindowSize(settings.getFilesize(Keys.git.packedGitWindowSize, cfg.getPackedGitWindowSize()));
+ cfg.setPackedGitLimit(settings.getFilesize(Keys.git.packedGitLimit, cfg.getPackedGitLimit()));
+ cfg.setDeltaBaseCacheLimit(settings.getFilesize(Keys.git.deltaBaseCacheLimit, cfg.getDeltaBaseCacheLimit()));
+ cfg.setPackedGitOpenFiles(settings.getFilesize(Keys.git.packedGitOpenFiles, cfg.getPackedGitOpenFiles()));
+ cfg.setStreamFileThreshold(settings.getFilesize(Keys.git.streamFileThreshold, cfg.getStreamFileThreshold()));
+ cfg.setPackedGitMMAP(settings.getBoolean(Keys.git.packedGitMmap, cfg.isPackedGitMMAP()));
+
+ try {
+ WindowCache.reconfigure(cfg);
+ logger.debug(MessageFormat.format("{0} = {1,number,0}", Keys.git.packedGitWindowSize, cfg.getPackedGitWindowSize()));
+ logger.debug(MessageFormat.format("{0} = {1,number,0}", Keys.git.packedGitLimit, cfg.getPackedGitLimit()));
+ logger.debug(MessageFormat.format("{0} = {1,number,0}", Keys.git.deltaBaseCacheLimit, cfg.getDeltaBaseCacheLimit()));
+ logger.debug(MessageFormat.format("{0} = {1,number,0}", Keys.git.packedGitOpenFiles, cfg.getPackedGitOpenFiles()));
+ logger.debug(MessageFormat.format("{0} = {1,number,0}", Keys.git.streamFileThreshold, cfg.getStreamFileThreshold()));
+ logger.debug(MessageFormat.format("{0} = {1}", Keys.git.packedGitMmap, cfg.isPackedGitMMAP()));
+ } catch (IllegalArgumentException e) {
+ logger.error("Failed to configure JGit parameters!", e);
+ }
+
+ ContainerUtils.CVE_2007_0450.test();
+
+ // startup Fanout PubSub service
+ if (settings.getInteger(Keys.fanout.port, 0) > 0) {
+ String bindInterface = settings.getString(Keys.fanout.bindInterface, null);
+ int port = settings.getInteger(Keys.fanout.port, FanoutService.DEFAULT_PORT);
+ boolean useNio = settings.getBoolean(Keys.fanout.useNio, true);
+ int limit = settings.getInteger(Keys.fanout.connectionLimit, 0);
+
+ if (useNio) {
+ if (StringUtils.isEmpty(bindInterface)) {
+ fanoutService = new FanoutNioService(port);
+ } else {
+ fanoutService = new FanoutNioService(bindInterface, port);
+ }
+ } else {
+ if (StringUtils.isEmpty(bindInterface)) {
+ fanoutService = new FanoutSocketService(port);
+ } else {
+ fanoutService = new FanoutSocketService(bindInterface, port);
+ }
+ }
+
+ fanoutService.setConcurrentConnectionLimit(limit);
+ fanoutService.setAllowAllChannelAnnouncements(false);
+ fanoutService.start();
+ }
+ }
+
+ protected void enableLuceneIndexing() {
+ scheduledExecutor.scheduleAtFixedRate(luceneExecutor, 1, 2, TimeUnit.MINUTES);
+ logger.info("Lucene executor is scheduled to process indexed branches every 2 minutes.");
+ }
+
+ protected final Logger getLogger() {
+ return logger;
+ }
+
+ protected final ScheduledExecutorService getScheduledExecutor() {
+ return scheduledExecutor;
+ }
+
+ protected final LuceneExecutor getLuceneExecutor() {
+ return luceneExecutor;
+ }
+
+ private void logTimezone(String type, TimeZone zone) {
+ SimpleDateFormat df = new SimpleDateFormat("z Z");
+ df.setTimeZone(zone);
+ String offset = df.format(new Date());
+ logger.info(type + " timezone is " + zone.getID() + " (" + offset + ")");
+ }
+
+ /**
+ * Configure Gitblit from the web.xml, if no configuration has already been
+ * specified.
+ *
+ * @see ServletContextListener.contextInitialize(ServletContextEvent)
+ */
+ @Override
+ public void contextInitialized(ServletContextEvent contextEvent) {
+ contextInitialized(contextEvent, contextEvent.getServletContext().getResourceAsStream("/WEB-INF/reference.properties"));
+ }
+
+ public void contextInitialized(ServletContextEvent contextEvent, InputStream referencePropertiesInputStream) {
+ servletContext = contextEvent.getServletContext();
+ if (settings == null) {
+ // Gitblit is running in a servlet container
+ ServletContext context = contextEvent.getServletContext();
+ WebXmlSettings webxmlSettings = new WebXmlSettings(context);
+ File contextFolder = new File(context.getRealPath("/"));
+ String openShift = System.getenv("OPENSHIFT_DATA_DIR");
+
+ if (!StringUtils.isEmpty(openShift)) {
+ // Gitblit is running in OpenShift/JBoss
+ File base = new File(openShift);
+
+ // gitblit.properties setting overrides
+ File overrideFile = new File(base, "gitblit.properties");
+ webxmlSettings.applyOverrides(overrideFile);
+
+ // Copy the included scripts to the configured groovy folder
+ File localScripts = new File(base, webxmlSettings.getString(Keys.groovy.scriptsFolder, "groovy"));
+ if (!localScripts.exists()) {
+ File warScripts = new File(contextFolder, "/WEB-INF/data/groovy");
+ if (!warScripts.equals(localScripts)) {
+ try {
+ com.gitblit.utils.FileUtils.copy(localScripts, warScripts.listFiles());
+ } catch (IOException e) {
+ logger.error(MessageFormat.format(
+ "Failed to copy included Groovy scripts from {0} to {1}",
+ warScripts, localScripts));
+ }
+ }
+ }
+
+ // configure context using the web.xml
+ configureContext(webxmlSettings, base, true);
+ } else {
+ // Gitblit is running in a standard servlet container
+ logger.info("WAR contextFolder is " + contextFolder.getAbsolutePath());
+
+ String path = webxmlSettings.getString(Constants.baseFolder, Constants.contextFolder$ + "/WEB-INF/data");
+ File base = com.gitblit.utils.FileUtils.resolveParameter(Constants.contextFolder$, contextFolder, path);
+ base.mkdirs();
+
+ // try to copy the data folder contents to the baseFolder
+ File localSettings = new File(base, "gitblit.properties");
+ if (!localSettings.exists()) {
+ File contextData = new File(contextFolder, "/WEB-INF/data");
+ if (!base.equals(contextData)) {
+ try {
+ com.gitblit.utils.FileUtils.copy(base, contextData.listFiles());
+ } catch (IOException e) {
+ logger.error(MessageFormat.format(
+ "Failed to copy included data from {0} to {1}",
+ contextData, base));
+ }
+ }
+ }
+
+ // delegate all config to baseFolder/gitblit.properties file
+ FileSettings settings = new FileSettings(localSettings.getAbsolutePath());
+ configureContext(settings, base, true);
+ }
+ }
+
+ settingsModel = loadSettingModels(referencePropertiesInputStream);
+ serverStatus.servletContainer = servletContext.getServerInfo();
+ }
+
+ /**
+ * Gitblit is being shutdown either because the servlet container is
+ * shutting down or because the servlet container is re-deploying Gitblit.
+ */
+ @Override
+ public void contextDestroyed(ServletContextEvent contextEvent) {
+ logger.info("Gitblit context destroyed by servlet container.");
+ scheduledExecutor.shutdownNow();
+ luceneExecutor.close();
+ gcExecutor.close();
+ if (fanoutService != null) {
+ fanoutService.stop();
+ }
+ }
+
+ /**
+ *
+ * @return true if we are running the gc executor
+ */
+ public boolean isCollectingGarbage() {
+ return gcExecutor.isRunning();
+ }
+
+ /**
+ * Returns true if Gitblit is actively collecting garbage in this repository.
+ *
+ * @param repositoryName
+ * @return true if actively collecting garbage
+ */
+ public boolean isCollectingGarbage(String repositoryName) {
+ return gcExecutor.isCollectingGarbage(repositoryName);
+ }
+
+ /**
+ * Creates a personal fork of the specified repository. The clone is view
+ * restricted by default and the owner of the source repository is given
+ * access to the clone.
+ *
+ * @param repository
+ * @param user
+ * @return the repository model of the fork, if successful
+ * @throws GitBlitException
+ */
+ public RepositoryModel fork(RepositoryModel repository, UserModel user) throws GitBlitException {
+ String cloneName = MessageFormat.format("~{0}/{1}.git", user.username, StringUtils.stripDotGit(StringUtils.getLastPathElement(repository.name)));
+ String fromUrl = MessageFormat.format("file://{0}/{1}", repositoriesFolder.getAbsolutePath(), repository.name);
+
+ // clone the repository
+ try {
+ JGitUtils.cloneRepository(repositoriesFolder, cloneName, fromUrl, true, null);
+ } catch (Exception e) {
+ throw new GitBlitException(e);
+ }
+
+ // create a Gitblit repository model for the clone
+ RepositoryModel cloneModel = repository.cloneAs(cloneName);
+ // owner has REWIND/RW+ permissions
+ cloneModel.addOwner(user.username);
+ updateRepositoryModel(cloneName, cloneModel, false);
+
+ // add the owner of the source repository to the clone's access list
+ if (!ArrayUtils.isEmpty(repository.owners)) {
+ for (String owner : repository.owners) {
+ UserModel originOwner = getUserModel(owner);
+ if (originOwner != null) {
+ originOwner.setRepositoryPermission(cloneName, AccessPermission.CLONE);
+ updateUserModel(originOwner.username, originOwner, false);
+ }
+ }
+ }
+
+ // grant origin's user list clone permission to fork
+ List<String> users = getRepositoryUsers(repository);
+ List<UserModel> cloneUsers = new ArrayList<UserModel>();
+ for (String name : users) {
+ if (!name.equalsIgnoreCase(user.username)) {
+ UserModel cloneUser = getUserModel(name);
+ if (cloneUser.canClone(repository)) {
+ // origin user can clone origin, grant clone access to fork
+ cloneUser.setRepositoryPermission(cloneName, AccessPermission.CLONE);
+ }
+ cloneUsers.add(cloneUser);
+ }
+ }
+ userService.updateUserModels(cloneUsers);
+
+ // grant origin's team list clone permission to fork
+ List<String> teams = getRepositoryTeams(repository);
+ List<TeamModel> cloneTeams = new ArrayList<TeamModel>();
+ for (String name : teams) {
+ TeamModel cloneTeam = getTeamModel(name);
+ if (cloneTeam.canClone(repository)) {
+ // origin team can clone origin, grant clone access to fork
+ cloneTeam.setRepositoryPermission(cloneName, AccessPermission.CLONE);
+ }
+ cloneTeams.add(cloneTeam);
+ }
+ userService.updateTeamModels(cloneTeams);
+
+ // add this clone to the cached model
+ addToCachedRepositoryList(cloneModel);
+ return cloneModel;
+ }
+
+ /**
+ * Allow to understand if GitBlit supports and is configured to allow
+ * cookie-based authentication.
+ *
+ * @return status of Cookie authentication enablement.
+ */
+ public boolean allowCookieAuthentication() {
+ return GitBlit.getBoolean(Keys.web.allowCookieAuthentication, true) && userService.supportsCookies();
+ }
+}
diff --git a/src/main/java/com/gitblit/GitBlitException.java b/src/main/java/com/gitblit/GitBlitException.java
new file mode 100644
index 00000000..7ab0f99d
--- /dev/null
+++ b/src/main/java/com/gitblit/GitBlitException.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit;
+
+import java.io.IOException;
+
+/**
+ * GitBlitException is a marginally useful class. :)
+ *
+ * @author James Moger
+ *
+ */
+public class GitBlitException extends IOException {
+
+ private static final long serialVersionUID = 1L;
+
+ public GitBlitException(String message) {
+ super(message);
+ }
+
+ public GitBlitException(Throwable cause) {
+ super(cause);
+ }
+
+ /**
+ * Exception to indicate that the client should prompt for credentials
+ * because the requested action requires authentication.
+ */
+ public static class UnauthorizedException extends GitBlitException {
+
+ private static final long serialVersionUID = 1L;
+
+ public UnauthorizedException(String message) {
+ super(message);
+ }
+ }
+
+ /**
+ * Exception to indicate that the requested action can not be executed by
+ * the specified user.
+ */
+ public static class ForbiddenException extends GitBlitException {
+
+ private static final long serialVersionUID = 1L;
+
+ public ForbiddenException(String message) {
+ super(message);
+ }
+ }
+
+ /**
+ * Exception to indicate that the requested action has been disabled on the
+ * Gitblit server.
+ */
+ public static class NotAllowedException extends GitBlitException {
+
+ private static final long serialVersionUID = 1L;
+
+ public NotAllowedException(String message) {
+ super(message);
+ }
+ }
+
+ /**
+ * Exception to indicate that the requested action can not be executed by
+ * the server because it does not recognize the request type.
+ */
+ public static class UnknownRequestException extends GitBlitException {
+
+ private static final long serialVersionUID = 1L;
+
+ public UnknownRequestException(String message) {
+ super(message);
+ }
+ }
+}
diff --git a/src/main/java/com/gitblit/GitBlitServer.java b/src/main/java/com/gitblit/GitBlitServer.java
new file mode 100644
index 00000000..08c0d2a2
--- /dev/null
+++ b/src/main/java/com/gitblit/GitBlitServer.java
@@ -0,0 +1,626 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.URI;
+import java.net.URL;
+import java.net.UnknownHostException;
+import java.security.ProtectionDomain;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Scanner;
+
+import org.eclipse.jetty.ajp.Ajp13SocketConnector;
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.bio.SocketConnector;
+import org.eclipse.jetty.server.nio.SelectChannelConnector;
+import org.eclipse.jetty.server.session.HashSessionManager;
+import org.eclipse.jetty.server.ssl.SslConnector;
+import org.eclipse.jetty.server.ssl.SslSelectChannelConnector;
+import org.eclipse.jetty.server.ssl.SslSocketConnector;
+import org.eclipse.jetty.util.thread.QueuedThreadPool;
+import org.eclipse.jetty.webapp.WebAppContext;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+import org.eclipse.jgit.util.FileUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.beust.jcommander.JCommander;
+import com.beust.jcommander.Parameter;
+import com.beust.jcommander.ParameterException;
+import com.beust.jcommander.Parameters;
+import com.gitblit.authority.GitblitAuthority;
+import com.gitblit.authority.NewCertificateConfig;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.utils.TimeUtils;
+import com.gitblit.utils.X509Utils;
+import com.gitblit.utils.X509Utils.X509Log;
+import com.gitblit.utils.X509Utils.X509Metadata;
+import com.unboundid.ldap.listener.InMemoryDirectoryServer;
+import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
+import com.unboundid.ldap.listener.InMemoryListenerConfig;
+import com.unboundid.ldif.LDIFReader;
+
+/**
+ * GitBlitServer is the embedded Jetty server for Gitblit GO. This class starts
+ * and stops an instance of Jetty that is configured from a combination of the
+ * gitblit.properties file and command line parameters. JCommander is used to
+ * simplify command line parameter processing. This class also automatically
+ * generates a self-signed certificate for localhost, if the keystore does not
+ * already exist.
+ *
+ * @author James Moger
+ *
+ */
+public class GitBlitServer {
+
+ private static Logger logger;
+
+ public static void main(String... args) {
+ GitBlitServer server = new GitBlitServer();
+
+ // filter out the baseFolder parameter
+ List<String> filtered = new ArrayList<String>();
+ String folder = "data";
+ for (int i = 0; i< args.length; i++) {
+ String arg = args[i];
+ if (arg.equals("--baseFolder")) {
+ if (i + 1 == args.length) {
+ System.out.println("Invalid --baseFolder parameter!");
+ System.exit(-1);
+ } else if (args[i + 1] != ".") {
+ folder = args[i + 1];
+ }
+ i = i + 1;
+ } else {
+ filtered.add(arg);
+ }
+ }
+
+ Params.baseFolder = folder;
+ Params params = new Params();
+ JCommander jc = new JCommander(params);
+ try {
+ jc.parse(filtered.toArray(new String[filtered.size()]));
+ if (params.help) {
+ server.usage(jc, null);
+ }
+ } catch (ParameterException t) {
+ server.usage(jc, t);
+ }
+
+ if (params.stop) {
+ server.stop(params);
+ } else {
+ server.start(params);
+ }
+ }
+
+ /**
+ * Display the command line usage of Gitblit GO.
+ *
+ * @param jc
+ * @param t
+ */
+ protected final void usage(JCommander jc, ParameterException t) {
+ System.out.println(Constants.BORDER);
+ System.out.println(Constants.getGitBlitVersion());
+ System.out.println(Constants.BORDER);
+ System.out.println();
+ if (t != null) {
+ System.out.println(t.getMessage());
+ System.out.println();
+ }
+ if (jc != null) {
+ jc.usage();
+ System.out
+ .println("\nExample:\n java -server -Xmx1024M -jar gitblit.jar --repositoriesFolder c:\\git --httpPort 80 --httpsPort 443");
+ }
+ System.exit(0);
+ }
+
+ /**
+ * Stop Gitblt GO.
+ */
+ public void stop(Params params) {
+ try {
+ Socket s = new Socket(InetAddress.getByName("127.0.0.1"), params.shutdownPort);
+ OutputStream out = s.getOutputStream();
+ System.out.println("Sending Shutdown Request to " + Constants.NAME);
+ out.write("\r\n".getBytes());
+ out.flush();
+ s.close();
+ } catch (UnknownHostException e) {
+ e.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Start Gitblit GO.
+ */
+ protected final void start(Params params) {
+ final File baseFolder = new File(Params.baseFolder).getAbsoluteFile();
+ FileSettings settings = params.FILESETTINGS;
+ if (!StringUtils.isEmpty(params.settingsfile)) {
+ if (new File(params.settingsfile).exists()) {
+ settings = new FileSettings(params.settingsfile);
+ }
+ }
+ logger = LoggerFactory.getLogger(GitBlitServer.class);
+ logger.info(Constants.BORDER);
+ logger.info(" _____ _ _ _ _ _ _");
+ logger.info(" | __ \\(_)| | | | | |(_)| |");
+ logger.info(" | | \\/ _ | |_ | |__ | | _ | |_");
+ logger.info(" | | __ | || __|| '_ \\ | || || __|");
+ logger.info(" | |_\\ \\| || |_ | |_) || || || |_");
+ logger.info(" \\____/|_| \\__||_.__/ |_||_| \\__|");
+ int spacing = (Constants.BORDER.length() - Constants.getGitBlitVersion().length()) / 2;
+ StringBuilder sb = new StringBuilder();
+ while (spacing > 0) {
+ spacing--;
+ sb.append(' ');
+ }
+ logger.info(sb.toString() + Constants.getGitBlitVersion());
+ logger.info("");
+ logger.info(Constants.BORDER);
+
+ System.setProperty("java.awt.headless", "true");
+
+ String osname = System.getProperty("os.name");
+ String osversion = System.getProperty("os.version");
+ logger.info("Running on " + osname + " (" + osversion + ")");
+
+ List<Connector> connectors = new ArrayList<Connector>();
+
+ // conditionally configure the http connector
+ if (params.port > 0) {
+ Connector httpConnector = createConnector(params.useNIO, params.port);
+ String bindInterface = settings.getString(Keys.server.httpBindInterface, null);
+ if (!StringUtils.isEmpty(bindInterface)) {
+ logger.warn(MessageFormat.format("Binding connector on port {0,number,0} to {1}",
+ params.port, bindInterface));
+ httpConnector.setHost(bindInterface);
+ }
+ if (params.port < 1024 && !isWindows()) {
+ logger.warn("Gitblit needs to run with ROOT permissions for ports < 1024!");
+ }
+ connectors.add(httpConnector);
+ }
+
+ // conditionally configure the https connector
+ if (params.securePort > 0) {
+ File certificatesConf = new File(baseFolder, X509Utils.CA_CONFIG);
+ File serverKeyStore = new File(baseFolder, X509Utils.SERVER_KEY_STORE);
+ File serverTrustStore = new File(baseFolder, X509Utils.SERVER_TRUST_STORE);
+ File caRevocationList = new File(baseFolder, X509Utils.CA_REVOCATION_LIST);
+
+ // generate CA & web certificates, create certificate stores
+ X509Metadata metadata = new X509Metadata("localhost", params.storePassword);
+ // set default certificate values from config file
+ if (certificatesConf.exists()) {
+ FileBasedConfig config = new FileBasedConfig(certificatesConf, FS.detect());
+ try {
+ config.load();
+ } catch (Exception e) {
+ logger.error("Error parsing " + certificatesConf, e);
+ }
+ NewCertificateConfig certificateConfig = NewCertificateConfig.KEY.parse(config);
+ certificateConfig.update(metadata);
+ }
+
+ metadata.notAfter = new Date(System.currentTimeMillis() + 10*TimeUtils.ONEYEAR);
+ X509Utils.prepareX509Infrastructure(metadata, baseFolder, new X509Log() {
+ @Override
+ public void log(String message) {
+ BufferedWriter writer = null;
+ try {
+ writer = new BufferedWriter(new FileWriter(new File(baseFolder, X509Utils.CERTS + File.separator + "log.txt"), true));
+ writer.write(MessageFormat.format("{0,date,yyyy-MM-dd HH:mm}: {1}", new Date(), message));
+ writer.newLine();
+ writer.flush();
+ } catch (Exception e) {
+ LoggerFactory.getLogger(GitblitAuthority.class).error("Failed to append log entry!", e);
+ } finally {
+ if (writer != null) {
+ try {
+ writer.close();
+ } catch (IOException e) {
+ }
+ }
+ }
+ }
+ });
+
+ if (serverKeyStore.exists()) {
+ Connector secureConnector = createSSLConnector(params.alias, serverKeyStore, serverTrustStore, params.storePassword,
+ caRevocationList, params.useNIO, params.securePort, params.requireClientCertificates);
+ String bindInterface = settings.getString(Keys.server.httpsBindInterface, null);
+ if (!StringUtils.isEmpty(bindInterface)) {
+ logger.warn(MessageFormat.format(
+ "Binding ssl connector on port {0,number,0} to {1}", params.securePort,
+ bindInterface));
+ secureConnector.setHost(bindInterface);
+ }
+ if (params.securePort < 1024 && !isWindows()) {
+ logger.warn("Gitblit needs to run with ROOT permissions for ports < 1024!");
+ }
+ connectors.add(secureConnector);
+ } else {
+ logger.warn("Failed to find or load Keystore?");
+ logger.warn("SSL connector DISABLED.");
+ }
+ }
+
+ // conditionally configure the ajp connector
+ if (params.ajpPort > 0) {
+ Connector ajpConnector = createAJPConnector(params.ajpPort);
+ String bindInterface = settings.getString(Keys.server.ajpBindInterface, null);
+ if (!StringUtils.isEmpty(bindInterface)) {
+ logger.warn(MessageFormat.format("Binding connector on port {0,number,0} to {1}",
+ params.ajpPort, bindInterface));
+ ajpConnector.setHost(bindInterface);
+ }
+ if (params.ajpPort < 1024 && !isWindows()) {
+ logger.warn("Gitblit needs to run with ROOT permissions for ports < 1024!");
+ }
+ connectors.add(ajpConnector);
+ }
+
+ // tempDir is where the embedded Gitblit web application is expanded and
+ // where Jetty creates any necessary temporary files
+ File tempDir = com.gitblit.utils.FileUtils.resolveParameter(Constants.baseFolder$, baseFolder, params.temp);
+ if (tempDir.exists()) {
+ try {
+ FileUtils.delete(tempDir, FileUtils.RECURSIVE | FileUtils.RETRY);
+ } catch (IOException x) {
+ logger.warn("Failed to delete temp dir " + tempDir.getAbsolutePath(), x);
+ }
+ }
+ if (!tempDir.mkdirs()) {
+ logger.warn("Failed to create temp dir " + tempDir.getAbsolutePath());
+ }
+
+ Server server = new Server();
+ server.setStopAtShutdown(true);
+ server.setConnectors(connectors.toArray(new Connector[connectors.size()]));
+
+ // Get the execution path of this class
+ // We use this to set the WAR path.
+ ProtectionDomain protectionDomain = GitBlitServer.class.getProtectionDomain();
+ URL location = protectionDomain.getCodeSource().getLocation();
+
+ // Root WebApp Context
+ WebAppContext rootContext = new WebAppContext();
+ rootContext.setContextPath(settings.getString(Keys.server.contextPath, "/"));
+ rootContext.setServer(server);
+ rootContext.setWar(location.toExternalForm());
+ rootContext.setTempDirectory(tempDir);
+
+ // Set cookies HttpOnly so they are not accessible to JavaScript engines
+ HashSessionManager sessionManager = new HashSessionManager();
+ sessionManager.setHttpOnly(true);
+ // Use secure cookies if only serving https
+ sessionManager.setSecureCookies(params.port <= 0 && params.securePort > 0);
+ rootContext.getSessionHandler().setSessionManager(sessionManager);
+
+ // Ensure there is a defined User Service
+ String realmUsers = params.userService;
+ if (StringUtils.isEmpty(realmUsers)) {
+ logger.error(MessageFormat.format("PLEASE SPECIFY {0}!!", Keys.realm.userService));
+ return;
+ }
+
+ // Override settings from the command-line
+ settings.overrideSetting(Keys.realm.userService, params.userService);
+ settings.overrideSetting(Keys.git.repositoriesFolder, params.repositoriesFolder);
+
+ // Start up an in-memory LDAP server, if configured
+ try {
+ if (StringUtils.isEmpty(params.ldapLdifFile) == false) {
+ File ldifFile = new File(params.ldapLdifFile);
+ if (ldifFile != null && ldifFile.exists()) {
+ URI ldapUrl = new URI(settings.getRequiredString(Keys.realm.ldap.server));
+ String firstLine = new Scanner(ldifFile).nextLine();
+ String rootDN = firstLine.substring(4);
+ String bindUserName = settings.getString(Keys.realm.ldap.username, "");
+ String bindPassword = settings.getString(Keys.realm.ldap.password, "");
+
+ // Get the port
+ int port = ldapUrl.getPort();
+ if (port == -1)
+ port = 389;
+
+ InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(rootDN);
+ config.addAdditionalBindCredentials(bindUserName, bindPassword);
+ config.setListenerConfigs(InMemoryListenerConfig.createLDAPConfig("default", port));
+ config.setSchema(null);
+
+ InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
+ ds.importFromLDIF(true, new LDIFReader(ldifFile));
+ ds.startListening();
+
+ logger.info("LDAP Server started at ldap://localhost:" + port);
+ }
+ }
+ } catch (Exception e) {
+ // Completely optional, just show a warning
+ logger.warn("Unable to start LDAP server", e);
+ }
+
+ // Set the server's contexts
+ server.setHandler(rootContext);
+
+ // Setup the GitBlit context
+ GitBlit gitblit = getGitBlitInstance();
+ gitblit.configureContext(settings, baseFolder, true);
+ rootContext.addEventListener(gitblit);
+
+ try {
+ // start the shutdown monitor
+ if (params.shutdownPort > 0) {
+ Thread shutdownMonitor = new ShutdownMonitorThread(server, params);
+ shutdownMonitor.start();
+ }
+
+ // start Jetty
+ server.start();
+ server.join();
+ } catch (Exception e) {
+ e.printStackTrace();
+ System.exit(100);
+ }
+ }
+
+ protected GitBlit getGitBlitInstance() {
+ return GitBlit.self();
+ }
+
+ /**
+ * Creates an http connector.
+ *
+ * @param useNIO
+ * @param port
+ * @return an http connector
+ */
+ private Connector createConnector(boolean useNIO, int port) {
+ Connector connector;
+ if (useNIO) {
+ logger.info("Setting up NIO SelectChannelConnector on port " + port);
+ SelectChannelConnector nioconn = new SelectChannelConnector();
+ nioconn.setSoLingerTime(-1);
+ nioconn.setThreadPool(new QueuedThreadPool(20));
+ connector = nioconn;
+ } else {
+ logger.info("Setting up SocketConnector on port " + port);
+ SocketConnector sockconn = new SocketConnector();
+ connector = sockconn;
+ }
+
+ connector.setPort(port);
+ connector.setMaxIdleTime(30000);
+ return connector;
+ }
+
+ /**
+ * Creates an https connector.
+ *
+ * SSL renegotiation will be enabled if the JVM is 1.6.0_22 or later.
+ * oracle.com/technetwork/java/javase/documentation/tlsreadme2-176330.html
+ *
+ * @param certAlias
+ * @param keyStore
+ * @param clientTrustStore
+ * @param storePassword
+ * @param caRevocationList
+ * @param useNIO
+ * @param port
+ * @param requireClientCertificates
+ * @return an https connector
+ */
+ private Connector createSSLConnector(String certAlias, File keyStore, File clientTrustStore,
+ String storePassword, File caRevocationList, boolean useNIO, int port,
+ boolean requireClientCertificates) {
+ GitblitSslContextFactory factory = new GitblitSslContextFactory(certAlias,
+ keyStore, clientTrustStore, storePassword, caRevocationList);
+ SslConnector connector;
+ if (useNIO) {
+ logger.info("Setting up NIO SslSelectChannelConnector on port " + port);
+ SslSelectChannelConnector ssl = new SslSelectChannelConnector(factory);
+ ssl.setSoLingerTime(-1);
+ if (requireClientCertificates) {
+ factory.setNeedClientAuth(true);
+ } else {
+ factory.setWantClientAuth(true);
+ }
+ ssl.setThreadPool(new QueuedThreadPool(20));
+ connector = ssl;
+ } else {
+ logger.info("Setting up NIO SslSocketConnector on port " + port);
+ SslSocketConnector ssl = new SslSocketConnector(factory);
+ connector = ssl;
+ }
+ connector.setPort(port);
+ connector.setMaxIdleTime(30000);
+
+ return connector;
+ }
+
+ /**
+ * Creates an ajp connector.
+ *
+ * @param port
+ * @return an ajp connector
+ */
+ private Connector createAJPConnector(int port) {
+ logger.info("Setting up AJP Connector on port " + port);
+ Ajp13SocketConnector ajp = new Ajp13SocketConnector();
+ ajp.setPort(port);
+ if (port < 1024 && !isWindows()) {
+ logger.warn("Gitblit needs to run with ROOT permissions for ports < 1024!");
+ }
+ return ajp;
+ }
+
+ /**
+ * Tests to see if the operating system is Windows.
+ *
+ * @return true if this is a windows machine
+ */
+ private boolean isWindows() {
+ return System.getProperty("os.name").toLowerCase().indexOf("windows") > -1;
+ }
+
+ /**
+ * The ShutdownMonitorThread opens a socket on a specified port and waits
+ * for an incoming connection. When that connection is accepted a shutdown
+ * message is issued to the running Jetty server.
+ *
+ * @author James Moger
+ *
+ */
+ private static class ShutdownMonitorThread extends Thread {
+
+ private final ServerSocket socket;
+
+ private final Server server;
+
+ private final Logger logger = LoggerFactory.getLogger(ShutdownMonitorThread.class);
+
+ public ShutdownMonitorThread(Server server, Params params) {
+ this.server = server;
+ setDaemon(true);
+ setName(Constants.NAME + " Shutdown Monitor");
+ ServerSocket skt = null;
+ try {
+ skt = new ServerSocket(params.shutdownPort, 1, InetAddress.getByName("127.0.0.1"));
+ } catch (Exception e) {
+ logger.warn("Could not open shutdown monitor on port " + params.shutdownPort, e);
+ }
+ socket = skt;
+ }
+
+ @Override
+ public void run() {
+ logger.info("Shutdown Monitor listening on port " + socket.getLocalPort());
+ Socket accept;
+ try {
+ accept = socket.accept();
+ BufferedReader reader = new BufferedReader(new InputStreamReader(
+ accept.getInputStream()));
+ reader.readLine();
+ logger.info(Constants.BORDER);
+ logger.info("Stopping " + Constants.NAME);
+ logger.info(Constants.BORDER);
+ server.stop();
+ server.setStopAtShutdown(false);
+ accept.close();
+ socket.close();
+ } catch (Exception e) {
+ logger.warn("Failed to shutdown Jetty", e);
+ }
+ }
+ }
+
+ /**
+ * JCommander Parameters class for GitBlitServer.
+ */
+ @Parameters(separators = " ")
+ public static class Params {
+
+ public static String baseFolder;
+
+ private final FileSettings FILESETTINGS = new FileSettings(new File(baseFolder, Constants.PROPERTIES_FILE).getAbsolutePath());
+
+ /*
+ * Server parameters
+ */
+ @Parameter(names = { "-h", "--help" }, description = "Show this help")
+ public Boolean help = false;
+
+ @Parameter(names = { "--stop" }, description = "Stop Server")
+ public Boolean stop = false;
+
+ @Parameter(names = { "--tempFolder" }, description = "Folder for server to extract built-in webapp")
+ public String temp = FILESETTINGS.getString(Keys.server.tempFolder, "temp");
+
+ /*
+ * GIT Servlet Parameters
+ */
+ @Parameter(names = { "--repositoriesFolder" }, description = "Git Repositories Folder")
+ public String repositoriesFolder = FILESETTINGS.getString(Keys.git.repositoriesFolder,
+ "git");
+
+ /*
+ * Authentication Parameters
+ */
+ @Parameter(names = { "--userService" }, description = "Authentication and Authorization Service (filename or fully qualified classname)")
+ public String userService = FILESETTINGS.getString(Keys.realm.userService,
+ "users.conf");
+
+ /*
+ * JETTY Parameters
+ */
+ @Parameter(names = { "--useNio" }, description = "Use NIO Connector else use Socket Connector.")
+ public Boolean useNIO = FILESETTINGS.getBoolean(Keys.server.useNio, true);
+
+ @Parameter(names = "--httpPort", description = "HTTP port for to serve. (port <= 0 will disable this connector)")
+ public Integer port = FILESETTINGS.getInteger(Keys.server.httpPort, 0);
+
+ @Parameter(names = "--httpsPort", description = "HTTPS port to serve. (port <= 0 will disable this connector)")
+ public Integer securePort = FILESETTINGS.getInteger(Keys.server.httpsPort, 8443);
+
+ @Parameter(names = "--ajpPort", description = "AJP port to serve. (port <= 0 will disable this connector)")
+ public Integer ajpPort = FILESETTINGS.getInteger(Keys.server.ajpPort, 0);
+
+ @Parameter(names = "--alias", description = "Alias of SSL certificate in keystore for serving https.")
+ public String alias = FILESETTINGS.getString(Keys.server.certificateAlias, "");
+
+ @Parameter(names = "--storePassword", description = "Password for SSL (https) keystore.")
+ public String storePassword = FILESETTINGS.getString(Keys.server.storePassword, "");
+
+ @Parameter(names = "--shutdownPort", description = "Port for Shutdown Monitor to listen on. (port <= 0 will disable this monitor)")
+ public Integer shutdownPort = FILESETTINGS.getInteger(Keys.server.shutdownPort, 8081);
+
+ @Parameter(names = "--requireClientCertificates", description = "Require client X509 certificates for https connections.")
+ public Boolean requireClientCertificates = FILESETTINGS.getBoolean(Keys.server.requireClientCertificates, false);
+
+ /*
+ * Setting overrides
+ */
+ @Parameter(names = { "--settings" }, description = "Path to alternative settings")
+ public String settingsfile;
+
+ @Parameter(names = { "--ldapLdifFile" }, description = "Path to LDIF file. This will cause an in-memory LDAP server to be started according to gitblit settings")
+ public String ldapLdifFile;
+
+ }
+} \ No newline at end of file
diff --git a/src/main/java/com/gitblit/GitFilter.java b/src/main/java/com/gitblit/GitFilter.java
new file mode 100644
index 00000000..a0d395b1
--- /dev/null
+++ b/src/main/java/com/gitblit/GitFilter.java
@@ -0,0 +1,253 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit;
+
+import java.text.MessageFormat;
+
+import com.gitblit.Constants.AccessRestrictionType;
+import com.gitblit.Constants.AuthorizationControl;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.UserModel;
+import com.gitblit.utils.StringUtils;
+
+/**
+ * The GitFilter is an AccessRestrictionFilter which ensures that Git client
+ * requests for push, clone, or view restricted repositories are authenticated
+ * and authorized.
+ *
+ * @author James Moger
+ *
+ */
+public class GitFilter extends AccessRestrictionFilter {
+
+ protected static final String gitReceivePack = "/git-receive-pack";
+
+ protected static final String gitUploadPack = "/git-upload-pack";
+
+ protected static final String[] suffixes = { gitReceivePack, gitUploadPack, "/info/refs", "/HEAD",
+ "/objects" };
+
+ /**
+ * Extract the repository name from the url.
+ *
+ * @param url
+ * @return repository name
+ */
+ public static String getRepositoryName(String value) {
+ String repository = value;
+ // get the repository name from the url by finding a known url suffix
+ for (String urlSuffix : suffixes) {
+ if (repository.indexOf(urlSuffix) > -1) {
+ repository = repository.substring(0, repository.indexOf(urlSuffix));
+ }
+ }
+ return repository;
+ }
+
+ /**
+ * Extract the repository name from the url.
+ *
+ * @param url
+ * @return repository name
+ */
+ @Override
+ protected String extractRepositoryName(String url) {
+ return GitFilter.getRepositoryName(url);
+ }
+
+ /**
+ * Analyze the url and returns the action of the request. Return values are
+ * either "/git-receive-pack" or "/git-upload-pack".
+ *
+ * @param serverUrl
+ * @return action of the request
+ */
+ @Override
+ protected String getUrlRequestAction(String suffix) {
+ if (!StringUtils.isEmpty(suffix)) {
+ if (suffix.startsWith(gitReceivePack)) {
+ return gitReceivePack;
+ } else if (suffix.startsWith(gitUploadPack)) {
+ return gitUploadPack;
+ } else if (suffix.contains("?service=git-receive-pack")) {
+ return gitReceivePack;
+ } else if (suffix.contains("?service=git-upload-pack")) {
+ return gitUploadPack;
+ } else {
+ return gitUploadPack;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Determine if a non-existing repository can be created using this filter.
+ *
+ * @return true if the server allows repository creation on-push
+ */
+ @Override
+ protected boolean isCreationAllowed() {
+ return GitBlit.getBoolean(Keys.git.allowCreateOnPush, true);
+ }
+
+ /**
+ * Determine if the repository can receive pushes.
+ *
+ * @param repository
+ * @param action
+ * @return true if the action may be performed
+ */
+ @Override
+ protected boolean isActionAllowed(RepositoryModel repository, String action) {
+ if (!StringUtils.isEmpty(action)) {
+ if (action.equals(gitReceivePack)) {
+ // Push request
+ if (!repository.isBare) {
+ logger.warn("Gitblit does not allow pushes to repositories with a working copy");
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ @Override
+ protected boolean requiresClientCertificate() {
+ return GitBlit.getBoolean(Keys.git.requiresClientCertificate, false);
+ }
+
+ /**
+ * Determine if the repository requires authentication.
+ *
+ * @param repository
+ * @param action
+ * @return true if authentication required
+ */
+ @Override
+ protected boolean requiresAuthentication(RepositoryModel repository, String action) {
+ if (gitUploadPack.equals(action)) {
+ // send to client
+ return repository.accessRestriction.atLeast(AccessRestrictionType.CLONE);
+ } else if (gitReceivePack.equals(action)) {
+ // receive from client
+ return repository.accessRestriction.atLeast(AccessRestrictionType.PUSH);
+ }
+ return false;
+ }
+
+ /**
+ * Determine if the user can access the repository and perform the specified
+ * action.
+ *
+ * @param repository
+ * @param user
+ * @param action
+ * @return true if user may execute the action on the repository
+ */
+ @Override
+ protected boolean canAccess(RepositoryModel repository, UserModel user, String action) {
+ if (!GitBlit.getBoolean(Keys.git.enableGitServlet, true)) {
+ // Git Servlet disabled
+ return false;
+ }
+ if (action.equals(gitReceivePack)) {
+ // Push request
+ if (user.canPush(repository)) {
+ return true;
+ } else {
+ // user is unauthorized to push to this repository
+ logger.warn(MessageFormat.format("user {0} is not authorized to push to {1}",
+ user.username, repository));
+ return false;
+ }
+ } else if (action.equals(gitUploadPack)) {
+ // Clone request
+ if (user.canClone(repository)) {
+ return true;
+ } else {
+ // user is unauthorized to clone this repository
+ logger.warn(MessageFormat.format("user {0} is not authorized to clone {1}",
+ user.username, repository));
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * An authenticated user with the CREATE role can create a repository on
+ * push.
+ *
+ * @param user
+ * @param repository
+ * @param action
+ * @return the repository model, if it is created, null otherwise
+ */
+ @Override
+ protected RepositoryModel createRepository(UserModel user, String repository, String action) {
+ boolean isPush = !StringUtils.isEmpty(action) && gitReceivePack.equals(action);
+ if (isPush) {
+ if (user.canCreate(repository)) {
+ // user is pushing to a new repository
+ // validate name
+ if (repository.startsWith("../")) {
+ logger.error(MessageFormat.format("Illegal relative path in repository name! {0}", repository));
+ return null;
+ }
+ if (repository.contains("/../")) {
+ logger.error(MessageFormat.format("Illegal relative path in repository name! {0}", repository));
+ return null;
+ }
+
+ // confirm valid characters in repository name
+ Character c = StringUtils.findInvalidCharacter(repository);
+ if (c != null) {
+ logger.error(MessageFormat.format("Invalid character '{0}' in repository name {1}!", c, repository));
+ return null;
+ }
+
+ // create repository
+ RepositoryModel model = new RepositoryModel();
+ model.name = repository;
+ model.addOwner(user.username);
+ model.projectPath = StringUtils.getFirstPathElement(repository);
+ if (model.isUsersPersonalRepository(user.username)) {
+ // personal repository, default to private for user
+ model.authorizationControl = AuthorizationControl.NAMED;
+ model.accessRestriction = AccessRestrictionType.VIEW;
+ } else {
+ // common repository, user default server settings
+ model.authorizationControl = AuthorizationControl.fromName(GitBlit.getString(Keys.git.defaultAuthorizationControl, ""));
+ model.accessRestriction = AccessRestrictionType.fromName(GitBlit.getString(Keys.git.defaultAccessRestriction, ""));
+ }
+
+ // create the repository
+ try {
+ GitBlit.self().updateRepositoryModel(model.name, model, true);
+ logger.info(MessageFormat.format("{0} created {1} ON-PUSH", user.username, model.name));
+ return GitBlit.self().getRepositoryModel(model.name);
+ } catch (GitBlitException e) {
+ logger.error(MessageFormat.format("{0} failed to create repository {1} ON-PUSH!", user.username, model.name), e);
+ }
+ } else {
+ logger.warn(MessageFormat.format("{0} is not permitted to create repository {1} ON-PUSH!", user.username, repository));
+ }
+ }
+
+ // repository could not be created or action was not a push
+ return null;
+ }
+}
diff --git a/src/main/java/com/gitblit/GitServlet.java b/src/main/java/com/gitblit/GitServlet.java
new file mode 100644
index 00000000..77be963f
--- /dev/null
+++ b/src/main/java/com/gitblit/GitServlet.java
@@ -0,0 +1,405 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit;
+
+import groovy.lang.Binding;
+import groovy.util.GroovyScriptEngine;
+
+import java.io.File;
+import java.io.IOException;
+import java.text.MessageFormat;
+import java.util.Collection;
+import java.util.Enumeration;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+
+import org.eclipse.jgit.http.server.resolver.DefaultReceivePackFactory;
+import org.eclipse.jgit.http.server.resolver.DefaultUploadPackFactory;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.transport.PostReceiveHook;
+import org.eclipse.jgit.transport.PreReceiveHook;
+import org.eclipse.jgit.transport.ReceiveCommand;
+import org.eclipse.jgit.transport.ReceiveCommand.Result;
+import org.eclipse.jgit.transport.ReceivePack;
+import org.eclipse.jgit.transport.RefFilter;
+import org.eclipse.jgit.transport.UploadPack;
+import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException;
+import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.Constants.AccessRestrictionType;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.UserModel;
+import com.gitblit.utils.ClientLogger;
+import com.gitblit.utils.HttpUtils;
+import com.gitblit.utils.IssueUtils;
+import com.gitblit.utils.JGitUtils;
+import com.gitblit.utils.PushLogUtils;
+import com.gitblit.utils.StringUtils;
+
+/**
+ * The GitServlet exists to force configuration of the JGit GitServlet based on
+ * the Gitblit settings from either gitblit.properties or from context
+ * parameters in the web.xml file.
+ *
+ * It also implements and registers the Groovy hook mechanism.
+ *
+ * Access to this servlet is protected by the GitFilter.
+ *
+ * @author James Moger
+ *
+ */
+public class GitServlet extends org.eclipse.jgit.http.server.GitServlet {
+
+ private static final long serialVersionUID = 1L;
+
+ private GroovyScriptEngine gse;
+
+ private File groovyDir;
+
+ @Override
+ public void init(ServletConfig config) throws ServletException {
+ groovyDir = GitBlit.getGroovyScriptsFolder();
+ try {
+ // set Grape root
+ File grapeRoot = GitBlit.getFileOrFolder(Keys.groovy.grapeFolder, "${baseFolder}/groovy/grape").getAbsoluteFile();
+ grapeRoot.mkdirs();
+ System.setProperty("grape.root", grapeRoot.getAbsolutePath());
+
+ gse = new GroovyScriptEngine(groovyDir.getAbsolutePath());
+ } catch (IOException e) {
+ throw new ServletException("Failed to instantiate Groovy Script Engine!", e);
+ }
+
+ // set the Gitblit receive hook
+ setReceivePackFactory(new DefaultReceivePackFactory() {
+ @Override
+ public ReceivePack create(HttpServletRequest req, Repository db)
+ throws ServiceNotEnabledException, ServiceNotAuthorizedException {
+
+ // determine repository name from request
+ String repositoryName = req.getPathInfo().substring(1);
+ repositoryName = GitFilter.getRepositoryName(repositoryName);
+
+ GitblitReceiveHook hook = new GitblitReceiveHook();
+ hook.repositoryName = repositoryName;
+ hook.gitblitUrl = HttpUtils.getGitblitURL(req);
+
+ ReceivePack rp = super.create(req, db);
+ rp.setPreReceiveHook(hook);
+ rp.setPostReceiveHook(hook);
+
+ // determine pushing user
+ PersonIdent person = rp.getRefLogIdent();
+ UserModel user = GitBlit.self().getUserModel(person.getName());
+ if (user == null) {
+ // anonymous push, create a temporary usermodel
+ user = new UserModel(person.getName());
+ }
+
+ // enforce advanced ref permissions
+ RepositoryModel repository = GitBlit.self().getRepositoryModel(repositoryName);
+ rp.setAllowCreates(user.canCreateRef(repository));
+ rp.setAllowDeletes(user.canDeleteRef(repository));
+ rp.setAllowNonFastForwards(user.canRewindRef(repository));
+
+ if (repository.isFrozen) {
+ throw new ServiceNotEnabledException();
+ }
+
+ return rp;
+ }
+ });
+
+ // override the default upload pack to exclude gitblit refs
+ setUploadPackFactory(new DefaultUploadPackFactory() {
+ @Override
+ public UploadPack create(final HttpServletRequest req, final Repository db)
+ throws ServiceNotEnabledException, ServiceNotAuthorizedException {
+ UploadPack up = super.create(req, db);
+ RefFilter refFilter = new RefFilter() {
+ @Override
+ public Map<String, Ref> filter(Map<String, Ref> refs) {
+ // admin accounts can access all refs
+ UserModel user = GitBlit.self().authenticate(req);
+ if (user == null) {
+ user = UserModel.ANONYMOUS;
+ }
+ if (user.canAdmin()) {
+ return refs;
+ }
+
+ // normal users can not clone gitblit refs
+ refs.remove(IssueUtils.GB_ISSUES);
+ refs.remove(PushLogUtils.GB_PUSHES);
+ return refs;
+ }
+ };
+ up.setRefFilter(refFilter);
+ return up;
+ }
+ });
+ super.init(new GitblitServletConfig(config));
+ }
+
+ /**
+ * Transitional wrapper class to configure the JGit 1.2 GitFilter. This
+ * GitServlet will probably be replaced by a GitFilter so that Gitblit can
+ * serve Git repositories on the root URL and not a /git sub-url.
+ *
+ * @author James Moger
+ *
+ */
+ private class GitblitServletConfig implements ServletConfig {
+ final ServletConfig config;
+
+ GitblitServletConfig(ServletConfig config) {
+ this.config = config;
+ }
+
+ @Override
+ public String getServletName() {
+ return config.getServletName();
+ }
+
+ @Override
+ public ServletContext getServletContext() {
+ return config.getServletContext();
+ }
+
+ @Override
+ public String getInitParameter(String name) {
+ if (name.equals("base-path")) {
+ return GitBlit.getRepositoriesFolder().getAbsolutePath();
+ } else if (name.equals("export-all")) {
+ return "1";
+ }
+ return config.getInitParameter(name);
+ }
+
+ @Override
+ public Enumeration<String> getInitParameterNames() {
+ return config.getInitParameterNames();
+ }
+ }
+
+ /**
+ * The Gitblit receive hook allows for special processing on push events.
+ * That might include rejecting writes to specific branches or executing a
+ * script.
+ *
+ * @author James Moger
+ *
+ */
+ private class GitblitReceiveHook implements PreReceiveHook, PostReceiveHook {
+
+ protected final Logger logger = LoggerFactory.getLogger(GitblitReceiveHook.class);
+
+ protected String repositoryName;
+
+ protected String gitblitUrl;
+
+ /**
+ * Instrumentation point where the incoming push event has been parsed,
+ * validated, objects created BUT refs have not been updated. You might
+ * use this to enforce a branch-write permissions model.
+ */
+ @Override
+ public void onPreReceive(ReceivePack rp, Collection<ReceiveCommand> commands) {
+ RepositoryModel repository = GitBlit.self().getRepositoryModel(repositoryName);
+ UserModel user = getUserModel(rp);
+
+ if (repository.accessRestriction.atLeast(AccessRestrictionType.PUSH) && repository.verifyCommitter) {
+ if (StringUtils.isEmpty(user.emailAddress)) {
+ // emit warning if user does not have an email address
+ logger.warn(MessageFormat.format("Consider setting an email address for {0} ({1}) to improve committer verification.", user.getDisplayName(), user.username));
+ }
+
+ // Optionally enforce that the committer of the left parent chain
+ // match the account being used to push the commits.
+ //
+ // This requires all merge commits are executed with the "--no-ff"
+ // option to force a merge commit even if fast-forward is possible.
+ // This ensures that the chain of left parents has the commit
+ // identity of the merging user.
+ for (ReceiveCommand cmd : commands) {
+ try {
+ List<RevCommit> commits = JGitUtils.getRevLog(rp.getRepository(), cmd.getOldId().name(), cmd.getNewId().name());
+ for (RevCommit commit : commits) {
+ PersonIdent committer = commit.getCommitterIdent();
+ if (!user.is(committer.getName(), committer.getEmailAddress())) {
+ String reason;
+ if (StringUtils.isEmpty(user.emailAddress)) {
+ // account does not have en email address
+ reason = MessageFormat.format("{0} by {1} <{2}> was not committed by {3} ({4})", commit.getId().name(), committer.getName(), StringUtils.isEmpty(committer.getEmailAddress()) ? "?":committer.getEmailAddress(), user.getDisplayName(), user.username);
+ } else {
+ // account has an email address
+ reason = MessageFormat.format("{0} by {1} <{2}> was not committed by {3} ({4}) <{5}>", commit.getId().name(), committer.getName(), StringUtils.isEmpty(committer.getEmailAddress()) ? "?":committer.getEmailAddress(), user.getDisplayName(), user.username, user.emailAddress);
+ }
+ cmd.setResult(Result.REJECTED_OTHER_REASON, reason);
+ break;
+ }
+ }
+ } catch (Exception e) {
+ logger.error("Failed to verify commits were made by pushing user", e);
+ }
+ }
+ }
+
+ Set<String> scripts = new LinkedHashSet<String>();
+ scripts.addAll(GitBlit.self().getPreReceiveScriptsInherited(repository));
+ scripts.addAll(repository.preReceiveScripts);
+ runGroovy(repository, user, commands, rp, scripts);
+ for (ReceiveCommand cmd : commands) {
+ if (!Result.NOT_ATTEMPTED.equals(cmd.getResult())) {
+ logger.warn(MessageFormat.format("{0} {1} because \"{2}\"", cmd.getNewId()
+ .getName(), cmd.getResult(), cmd.getMessage()));
+ }
+ }
+ }
+
+ /**
+ * Instrumentation point where the incoming push has been applied to the
+ * repository. This is the point where we would trigger a Jenkins build
+ * or send an email.
+ */
+ @Override
+ public void onPostReceive(ReceivePack rp, Collection<ReceiveCommand> commands) {
+ if (commands.size() == 0) {
+ logger.info("skipping post-receive hooks, no refs created, updated, or removed");
+ return;
+ }
+
+ UserModel user = getUserModel(rp);
+ RepositoryModel repository = GitBlit.self().getRepositoryModel(repositoryName);
+
+ // log ref changes
+ for (ReceiveCommand cmd : commands) {
+ if (Result.OK.equals(cmd.getResult())) {
+ // add some logging for important ref changes
+ switch (cmd.getType()) {
+ case DELETE:
+ logger.info(MessageFormat.format("{0} DELETED {1} in {2} ({3})", user.username, cmd.getRefName(), repository.name, cmd.getOldId().name()));
+ break;
+ case CREATE:
+ logger.info(MessageFormat.format("{0} CREATED {1} in {2}", user.username, cmd.getRefName(), repository.name));
+ break;
+ case UPDATE_NONFASTFORWARD:
+ logger.info(MessageFormat.format("{0} UPDATED NON-FAST-FORWARD {1} in {2} (from {3} to {4})", user.username, cmd.getRefName(), repository.name, cmd.getOldId().name(), cmd.getNewId().name()));
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ // update push log
+ try {
+ PushLogUtils.updatePushLog(user, rp.getRepository(), commands);
+ logger.info(MessageFormat.format("{0} push log updated", repository.name));
+ } catch (Exception e) {
+ logger.error(MessageFormat.format("Failed to update {0} pushlog", repository.name), e);
+ }
+
+ // run Groovy hook scripts
+ Set<String> scripts = new LinkedHashSet<String>();
+ scripts.addAll(GitBlit.self().getPostReceiveScriptsInherited(repository));
+ scripts.addAll(repository.postReceiveScripts);
+ runGroovy(repository, user, commands, rp, scripts);
+ }
+
+ /**
+ * Returns the UserModel for the user pushing the changes.
+ *
+ * @param rp
+ * @return a UserModel
+ */
+ protected UserModel getUserModel(ReceivePack rp) {
+ PersonIdent person = rp.getRefLogIdent();
+ UserModel user = GitBlit.self().getUserModel(person.getName());
+ if (user == null) {
+ // anonymous push, create a temporary usermodel
+ user = new UserModel(person.getName());
+ user.isAuthenticated = false;
+ }
+ return user;
+ }
+
+ /**
+ * Runs the specified Groovy hook scripts.
+ *
+ * @param repository
+ * @param user
+ * @param commands
+ * @param scripts
+ */
+ protected void runGroovy(RepositoryModel repository, UserModel user,
+ Collection<ReceiveCommand> commands, ReceivePack rp, Set<String> scripts) {
+ if (scripts == null || scripts.size() == 0) {
+ // no Groovy scripts to execute
+ return;
+ }
+
+ Binding binding = new Binding();
+ binding.setVariable("gitblit", GitBlit.self());
+ binding.setVariable("repository", repository);
+ binding.setVariable("receivePack", rp);
+ binding.setVariable("user", user);
+ binding.setVariable("commands", commands);
+ binding.setVariable("url", gitblitUrl);
+ binding.setVariable("logger", logger);
+ binding.setVariable("clientLogger", new ClientLogger(rp));
+ for (String script : scripts) {
+ if (StringUtils.isEmpty(script)) {
+ continue;
+ }
+ // allow script to be specified without .groovy extension
+ // this is easier to read in the settings
+ File file = new File(groovyDir, script);
+ if (!file.exists() && !script.toLowerCase().endsWith(".groovy")) {
+ file = new File(groovyDir, script + ".groovy");
+ if (file.exists()) {
+ script = file.getName();
+ }
+ }
+ try {
+ Object result = gse.run(script, binding);
+ if (result instanceof Boolean) {
+ if (!((Boolean) result)) {
+ logger.error(MessageFormat.format(
+ "Groovy script {0} has failed! Hook scripts aborted.", script));
+ break;
+ }
+ }
+ } catch (Exception e) {
+ logger.error(
+ MessageFormat.format("Failed to execute Groovy script {0}", script), e);
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/gitblit/GitblitSslContextFactory.java b/src/main/java/com/gitblit/GitblitSslContextFactory.java
new file mode 100644
index 00000000..f025c452
--- /dev/null
+++ b/src/main/java/com/gitblit/GitblitSslContextFactory.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2012 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit;
+
+import java.io.File;
+import java.security.KeyStore;
+import java.security.cert.CRL;
+import java.util.Collection;
+
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.utils.StringUtils;
+
+/**
+ * Special SSL context factory that configures Gitblit GO and replaces the
+ * primary trustmanager with a GitblitTrustManager.
+ *
+ * @author James Moger
+ */
+public class GitblitSslContextFactory extends SslContextFactory {
+
+ private static final Logger logger = LoggerFactory.getLogger(GitblitSslContextFactory.class);
+
+ private final File caRevocationList;
+
+ public GitblitSslContextFactory(String certAlias, File keyStore, File clientTrustStore,
+ String storePassword, File caRevocationList) {
+ super(keyStore.getAbsolutePath());
+
+ this.caRevocationList = caRevocationList;
+
+ // disable renegotiation unless this is a patched JVM
+ boolean allowRenegotiation = false;
+ String v = System.getProperty("java.version");
+ if (v.startsWith("1.7")) {
+ allowRenegotiation = true;
+ } else if (v.startsWith("1.6")) {
+ // 1.6.0_22 was first release with RFC-5746 implemented fix.
+ if (v.indexOf('_') > -1) {
+ String b = v.substring(v.indexOf('_') + 1);
+ if (Integer.parseInt(b) >= 22) {
+ allowRenegotiation = true;
+ }
+ }
+ }
+ if (allowRenegotiation) {
+ logger.info(" allowing SSL renegotiation on Java " + v);
+ setAllowRenegotiate(allowRenegotiation);
+ }
+
+
+ if (!StringUtils.isEmpty(certAlias)) {
+ logger.info(" certificate alias = " + certAlias);
+ setCertAlias(certAlias);
+ }
+ setKeyStorePassword(storePassword);
+ setTrustStore(clientTrustStore.getAbsolutePath());
+ setTrustStorePassword(storePassword);
+
+ logger.info(" keyStorePath = " + keyStore.getAbsolutePath());
+ logger.info(" trustStorePath = " + clientTrustStore.getAbsolutePath());
+ logger.info(" crlPath = " + caRevocationList.getAbsolutePath());
+ }
+
+ @Override
+ protected TrustManager[] getTrustManagers(KeyStore trustStore, Collection<? extends CRL> crls)
+ throws Exception {
+ TrustManager[] managers = super.getTrustManagers(trustStore, crls);
+ X509TrustManager delegate = (X509TrustManager) managers[0];
+ GitblitTrustManager root = new GitblitTrustManager(delegate, caRevocationList);
+
+ // replace first manager with the GitblitTrustManager
+ managers[0] = root;
+ return managers;
+ }
+}
diff --git a/src/main/java/com/gitblit/GitblitTrustManager.java b/src/main/java/com/gitblit/GitblitTrustManager.java
new file mode 100644
index 00000000..4127caf4
--- /dev/null
+++ b/src/main/java/com/gitblit/GitblitTrustManager.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2012 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509CRL;
+import java.security.cert.X509CRLEntry;
+import java.security.cert.X509Certificate;
+import java.text.MessageFormat;
+import java.util.concurrent.atomic.AtomicLong;
+
+import javax.net.ssl.X509TrustManager;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * GitblitTrustManager is a wrapper trust manager that hot-reloads a local file
+ * CRL and enforces client certificate revocations. The GitblitTrustManager
+ * also implements fuzzy revocation enforcement in case of issuer mismatch BUT
+ * serial number match. These rejecions are specially noted in the log.
+ *
+ * @author James Moger
+ */
+public class GitblitTrustManager implements X509TrustManager {
+
+ private static final Logger logger = LoggerFactory.getLogger(GitblitTrustManager.class);
+
+ private final X509TrustManager delegate;
+ private final File caRevocationList;
+
+ private final AtomicLong lastModified = new AtomicLong(0);
+ private volatile X509CRL crl;
+
+ public GitblitTrustManager(X509TrustManager delegate, File crlFile) {
+ this.delegate = delegate;
+ this.caRevocationList = crlFile;
+ }
+
+ @Override
+ public void checkClientTrusted(X509Certificate[] chain, String authType)
+ throws CertificateException {
+ X509Certificate cert = chain[0];
+ if (isRevoked(cert)) {
+ String message = MessageFormat.format("Rejecting revoked certificate {0,number,0} for {1}",
+ cert.getSerialNumber(), cert.getSubjectDN().getName());
+ logger.warn(message);
+ throw new CertificateException(message);
+ }
+ delegate.checkClientTrusted(chain, authType);
+ }
+
+ @Override
+ public void checkServerTrusted(X509Certificate[] chain, String authType)
+ throws CertificateException {
+ delegate.checkServerTrusted(chain, authType);
+ }
+
+ @Override
+ public X509Certificate[] getAcceptedIssuers() {
+ return delegate.getAcceptedIssuers();
+ }
+
+ protected boolean isRevoked(X509Certificate cert) {
+ if (!caRevocationList.exists()) {
+ return false;
+ }
+ read();
+
+ if (crl.isRevoked(cert)) {
+ // exact cert is revoked
+ return true;
+ }
+
+ X509CRLEntry entry = crl.getRevokedCertificate(cert.getSerialNumber());
+ if (entry != null) {
+ logger.warn("Certificate issuer does not match CRL issuer, but serial number has been revoked!");
+ logger.warn(" cert issuer = " + cert.getIssuerX500Principal());
+ logger.warn(" crl issuer = " + crl.getIssuerX500Principal());
+ return true;
+ }
+
+ return false;
+ }
+
+ protected synchronized void read() {
+ if (lastModified.get() == caRevocationList.lastModified()) {
+ return;
+ }
+ logger.info("Reloading CRL from " + caRevocationList.getAbsolutePath());
+ InputStream inStream = null;
+ try {
+ inStream = new FileInputStream(caRevocationList);
+ CertificateFactory cf = CertificateFactory.getInstance("X.509");
+ X509CRL list = (X509CRL)cf.generateCRL(inStream);
+ crl = list;
+ lastModified.set(caRevocationList.lastModified());
+ } catch (Exception e) {
+ } finally {
+ if (inStream != null) {
+ try {
+ inStream.close();
+ } catch (Exception e) {
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main/java/com/gitblit/GitblitUserService.java b/src/main/java/com/gitblit/GitblitUserService.java
new file mode 100644
index 00000000..fe35db94
--- /dev/null
+++ b/src/main/java/com/gitblit/GitblitUserService.java
@@ -0,0 +1,339 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit;
+
+import java.io.File;
+import java.io.IOException;
+import java.text.MessageFormat;
+import java.util.Collection;
+import java.util.List;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.Constants.AccountType;
+import com.gitblit.models.TeamModel;
+import com.gitblit.models.UserModel;
+import com.gitblit.utils.DeepCopier;
+import com.gitblit.utils.StringUtils;
+
+/**
+ * This class wraps the default user service and is recommended as the starting
+ * point for custom user service implementations.
+ *
+ * This does seem a little convoluted, but the idea is to allow IUserService to
+ * evolve with new methods and implementations without breaking custom
+ * authentication implementations.
+ *
+ * The most common implementation of a custom IUserService is to only override
+ * authentication and then delegate all other functionality to one of Gitblit's
+ * user services. This class optimizes that use-case.
+ *
+ * Extending GitblitUserService allows for authentication customization without
+ * having to keep-up-with IUSerService API changes.
+ *
+ * @author James Moger
+ *
+ */
+public class GitblitUserService implements IUserService {
+
+ protected IUserService serviceImpl;
+
+ protected final String ExternalAccount = "#externalAccount";
+
+ private final Logger logger = LoggerFactory.getLogger(GitblitUserService.class);
+
+ public GitblitUserService() {
+ }
+
+ @Override
+ public void setup(IStoredSettings settings) {
+ File realmFile = GitBlit.getFileOrFolder(Keys.realm.userService, "${baseFolder}/users.conf");
+ serviceImpl = createUserService(realmFile);
+ logger.info("GUS delegating to " + serviceImpl.toString());
+ }
+
+ @SuppressWarnings("deprecation")
+ protected IUserService createUserService(File realmFile) {
+ IUserService service = null;
+ if (realmFile.getName().toLowerCase().endsWith(".properties")) {
+ // v0.5.0 - v0.7.0 properties-based realm file
+ service = new FileUserService(realmFile);
+ } else if (realmFile.getName().toLowerCase().endsWith(".conf")) {
+ // v0.8.0+ config-based realm file
+ service = new ConfigUserService(realmFile);
+ }
+
+ assert service != null;
+
+ if (!realmFile.exists()) {
+ // Create the Administrator account for a new realm file
+ try {
+ realmFile.createNewFile();
+ } catch (IOException x) {
+ logger.error(MessageFormat.format("COULD NOT CREATE REALM FILE {0}!", realmFile), x);
+ }
+ UserModel admin = new UserModel("admin");
+ admin.password = "admin";
+ admin.canAdmin = true;
+ admin.excludeFromFederation = true;
+ service.updateUserModel(admin);
+ }
+
+ if (service instanceof FileUserService) {
+ // automatically create a users.conf realm file from the original
+ // users.properties file
+ File usersConfig = new File(realmFile.getParentFile(), "users.conf");
+ if (!usersConfig.exists()) {
+ logger.info(MessageFormat.format("Automatically creating {0} based on {1}",
+ usersConfig.getAbsolutePath(), realmFile.getAbsolutePath()));
+ ConfigUserService configService = new ConfigUserService(usersConfig);
+ for (String username : service.getAllUsernames()) {
+ UserModel userModel = service.getUserModel(username);
+ configService.updateUserModel(userModel);
+ }
+ }
+ // issue suggestion about switching to users.conf
+ logger.warn("Please consider using \"users.conf\" instead of the deprecated \"users.properties\" file");
+ }
+ return service;
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName();
+ }
+
+ @Override
+ public boolean supportsCredentialChanges() {
+ return serviceImpl.supportsCredentialChanges();
+ }
+
+ @Override
+ public boolean supportsDisplayNameChanges() {
+ return serviceImpl.supportsDisplayNameChanges();
+ }
+
+ @Override
+ public boolean supportsEmailAddressChanges() {
+ return serviceImpl.supportsEmailAddressChanges();
+ }
+
+ @Override
+ public boolean supportsTeamMembershipChanges() {
+ return serviceImpl.supportsTeamMembershipChanges();
+ }
+
+ @Override
+ public boolean supportsCookies() {
+ return serviceImpl.supportsCookies();
+ }
+
+ @Override
+ public String getCookie(UserModel model) {
+ return serviceImpl.getCookie(model);
+ }
+
+ @Override
+ public UserModel authenticate(char[] cookie) {
+ UserModel user = serviceImpl.authenticate(cookie);
+ setAccountType(user);
+ return user;
+ }
+
+ @Override
+ public UserModel authenticate(String username, char[] password) {
+ UserModel user = serviceImpl.authenticate(username, password);
+ setAccountType(user);
+ return user;
+ }
+
+ @Override
+ public void logout(UserModel user) {
+ serviceImpl.logout(user);
+ }
+
+ @Override
+ public UserModel getUserModel(String username) {
+ UserModel user = serviceImpl.getUserModel(username);
+ setAccountType(user);
+ return user;
+ }
+
+ @Override
+ public boolean updateUserModel(UserModel model) {
+ return serviceImpl.updateUserModel(model);
+ }
+
+ @Override
+ public boolean updateUserModels(Collection<UserModel> models) {
+ return serviceImpl.updateUserModels(models);
+ }
+
+ @Override
+ public boolean updateUserModel(String username, UserModel model) {
+ if (model.isLocalAccount() || supportsCredentialChanges()) {
+ if (!model.isLocalAccount() && !supportsTeamMembershipChanges()) {
+ // teams are externally controlled - copy from original model
+ UserModel existingModel = getUserModel(username);
+
+ model = DeepCopier.copy(model);
+ model.teams.clear();
+ model.teams.addAll(existingModel.teams);
+ }
+ return serviceImpl.updateUserModel(username, model);
+ }
+ if (model.username.equals(username)) {
+ // passwords are not persisted by the backing user service
+ model.password = null;
+ if (!model.isLocalAccount() && !supportsTeamMembershipChanges()) {
+ // teams are externally controlled- copy from original model
+ UserModel existingModel = getUserModel(username);
+
+ model = DeepCopier.copy(model);
+ model.teams.clear();
+ model.teams.addAll(existingModel.teams);
+ }
+ return serviceImpl.updateUserModel(username, model);
+ }
+ logger.error("Users can not be renamed!");
+ return false;
+ }
+ @Override
+ public boolean deleteUserModel(UserModel model) {
+ return serviceImpl.deleteUserModel(model);
+ }
+
+ @Override
+ public boolean deleteUser(String username) {
+ return serviceImpl.deleteUser(username);
+ }
+
+ @Override
+ public List<String> getAllUsernames() {
+ return serviceImpl.getAllUsernames();
+ }
+
+ @Override
+ public List<UserModel> getAllUsers() {
+ List<UserModel> users = serviceImpl.getAllUsers();
+ for (UserModel user : users) {
+ setAccountType(user);
+ }
+ return users;
+ }
+
+ @Override
+ public List<String> getAllTeamNames() {
+ return serviceImpl.getAllTeamNames();
+ }
+
+ @Override
+ public List<TeamModel> getAllTeams() {
+ return serviceImpl.getAllTeams();
+ }
+
+ @Override
+ public List<String> getTeamnamesForRepositoryRole(String role) {
+ return serviceImpl.getTeamnamesForRepositoryRole(role);
+ }
+
+ @Override
+ @Deprecated
+ public boolean setTeamnamesForRepositoryRole(String role, List<String> teamnames) {
+ return serviceImpl.setTeamnamesForRepositoryRole(role, teamnames);
+ }
+
+ @Override
+ public TeamModel getTeamModel(String teamname) {
+ return serviceImpl.getTeamModel(teamname);
+ }
+
+ @Override
+ public boolean updateTeamModel(TeamModel model) {
+ return serviceImpl.updateTeamModel(model);
+ }
+
+ @Override
+ public boolean updateTeamModels(Collection<TeamModel> models) {
+ return serviceImpl.updateTeamModels(models);
+ }
+
+ @Override
+ public boolean updateTeamModel(String teamname, TeamModel model) {
+ if (!supportsTeamMembershipChanges()) {
+ // teams are externally controlled - copy from original model
+ TeamModel existingModel = getTeamModel(teamname);
+
+ model = DeepCopier.copy(model);
+ model.users.clear();
+ model.users.addAll(existingModel.users);
+ }
+ return serviceImpl.updateTeamModel(teamname, model);
+ }
+
+ @Override
+ public boolean deleteTeamModel(TeamModel model) {
+ return serviceImpl.deleteTeamModel(model);
+ }
+
+ @Override
+ public boolean deleteTeam(String teamname) {
+ return serviceImpl.deleteTeam(teamname);
+ }
+
+ @Override
+ public List<String> getUsernamesForRepositoryRole(String role) {
+ return serviceImpl.getUsernamesForRepositoryRole(role);
+ }
+
+ @Override
+ @Deprecated
+ public boolean setUsernamesForRepositoryRole(String role, List<String> usernames) {
+ return serviceImpl.setUsernamesForRepositoryRole(role, usernames);
+ }
+
+ @Override
+ public boolean renameRepositoryRole(String oldRole, String newRole) {
+ return serviceImpl.renameRepositoryRole(oldRole, newRole);
+ }
+
+ @Override
+ public boolean deleteRepositoryRole(String role) {
+ return serviceImpl.deleteRepositoryRole(role);
+ }
+
+ protected boolean isLocalAccount(String username) {
+ UserModel user = getUserModel(username);
+ return user != null && user.isLocalAccount();
+ }
+
+ protected void setAccountType(UserModel user) {
+ if (user != null) {
+ if (!StringUtils.isEmpty(user.password)
+ && !ExternalAccount.equalsIgnoreCase(user.password)
+ && !"StoredInLDAP".equalsIgnoreCase(user.password)) {
+ user.accountType = AccountType.LOCAL;
+ } else {
+ user.accountType = getAccountType();
+ }
+ }
+ }
+
+ protected AccountType getAccountType() {
+ return AccountType.LOCAL;
+ }
+}
diff --git a/src/main/java/com/gitblit/IStoredSettings.java b/src/main/java/com/gitblit/IStoredSettings.java
new file mode 100644
index 00000000..790f8b68
--- /dev/null
+++ b/src/main/java/com/gitblit/IStoredSettings.java
@@ -0,0 +1,299 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.utils.StringUtils;
+
+/**
+ * Base class for stored settings implementations.
+ *
+ * @author James Moger
+ *
+ */
+public abstract class IStoredSettings {
+
+ protected final Logger logger;
+
+ protected final Properties overrides = new Properties();
+
+ public IStoredSettings(Class<? extends IStoredSettings> clazz) {
+ logger = LoggerFactory.getLogger(clazz);
+ }
+
+ protected abstract Properties read();
+
+ private Properties getSettings() {
+ Properties props = read();
+ props.putAll(overrides);
+ return props;
+ }
+
+ /**
+ * Returns the list of keys whose name starts with the specified prefix. If
+ * the prefix is null or empty, all key names are returned.
+ *
+ * @param startingWith
+ * @return list of keys
+ */
+ public List<String> getAllKeys(String startingWith) {
+ List<String> keys = new ArrayList<String>();
+ Properties props = getSettings();
+ if (StringUtils.isEmpty(startingWith)) {
+ keys.addAll(props.stringPropertyNames());
+ } else {
+ startingWith = startingWith.toLowerCase();
+ for (Object o : props.keySet()) {
+ String key = o.toString();
+ if (key.toLowerCase().startsWith(startingWith)) {
+ keys.add(key);
+ }
+ }
+ }
+ return keys;
+ }
+
+ /**
+ * Returns the boolean value for the specified key. If the key does not
+ * exist or the value for the key can not be interpreted as a boolean, the
+ * defaultValue is returned.
+ *
+ * @param key
+ * @param defaultValue
+ * @return key value or defaultValue
+ */
+ public boolean getBoolean(String name, boolean defaultValue) {
+ Properties props = getSettings();
+ if (props.containsKey(name)) {
+ String value = props.getProperty(name);
+ if (!StringUtils.isEmpty(value)) {
+ return Boolean.parseBoolean(value.trim());
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Returns the integer value for the specified key. If the key does not
+ * exist or the value for the key can not be interpreted as an integer, the
+ * defaultValue is returned.
+ *
+ * @param key
+ * @param defaultValue
+ * @return key value or defaultValue
+ */
+ public int getInteger(String name, int defaultValue) {
+ Properties props = getSettings();
+ if (props.containsKey(name)) {
+ try {
+ String value = props.getProperty(name);
+ if (!StringUtils.isEmpty(value)) {
+ return Integer.parseInt(value.trim());
+ }
+ } catch (NumberFormatException e) {
+ logger.warn("Failed to parse integer for " + name + " using default of "
+ + defaultValue);
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Returns the long value for the specified key. If the key does not
+ * exist or the value for the key can not be interpreted as an long, the
+ * defaultValue is returned.
+ *
+ * @param key
+ * @param defaultValue
+ * @return key value or defaultValue
+ */
+ public long getLong(String name, long defaultValue) {
+ Properties props = getSettings();
+ if (props.containsKey(name)) {
+ try {
+ String value = props.getProperty(name);
+ if (!StringUtils.isEmpty(value)) {
+ return Long.parseLong(value.trim());
+ }
+ } catch (NumberFormatException e) {
+ logger.warn("Failed to parse long for " + name + " using default of "
+ + defaultValue);
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Returns an int filesize from a string value such as 50m or 50mb
+ * @param name
+ * @param defaultValue
+ * @return an int filesize or defaultValue if the key does not exist or can
+ * not be parsed
+ */
+ public int getFilesize(String name, int defaultValue) {
+ String val = getString(name, null);
+ if (StringUtils.isEmpty(val)) {
+ return defaultValue;
+ }
+ return com.gitblit.utils.FileUtils.convertSizeToInt(val, defaultValue);
+ }
+
+ /**
+ * Returns an long filesize from a string value such as 50m or 50mb
+ * @param name
+ * @param defaultValue
+ * @return a long filesize or defaultValue if the key does not exist or can
+ * not be parsed
+ */
+ public long getFilesize(String key, long defaultValue) {
+ String val = getString(key, null);
+ if (StringUtils.isEmpty(val)) {
+ return defaultValue;
+ }
+ return com.gitblit.utils.FileUtils.convertSizeToLong(val, defaultValue);
+ }
+
+ /**
+ * Returns the char value for the specified key. If the key does not exist
+ * or the value for the key can not be interpreted as a char, the
+ * defaultValue is returned.
+ *
+ * @param key
+ * @param defaultValue
+ * @return key value or defaultValue
+ */
+ public char getChar(String name, char defaultValue) {
+ Properties props = getSettings();
+ if (props.containsKey(name)) {
+ String value = props.getProperty(name);
+ if (!StringUtils.isEmpty(value)) {
+ return value.trim().charAt(0);
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Returns the string value for the specified key. If the key does not exist
+ * or the value for the key can not be interpreted as a string, the
+ * defaultValue is returned.
+ *
+ * @param key
+ * @param defaultValue
+ * @return key value or defaultValue
+ */
+ public String getString(String name, String defaultValue) {
+ Properties props = getSettings();
+ if (props.containsKey(name)) {
+ String value = props.getProperty(name);
+ if (value != null) {
+ return value.trim();
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Returns the string value for the specified key. If the key does not
+ * exist an exception is thrown.
+ *
+ * @param key
+ * @return key value
+ */
+ public String getRequiredString(String name) {
+ Properties props = getSettings();
+ if (props.containsKey(name)) {
+ String value = props.getProperty(name);
+ if (value != null) {
+ return value.trim();
+ }
+ }
+ throw new RuntimeException("Property (" + name + ") does not exist");
+ }
+
+ /**
+ * Returns a list of space-separated strings from the specified key.
+ *
+ * @param name
+ * @return list of strings
+ */
+ public List<String> getStrings(String name) {
+ return getStrings(name, " ");
+ }
+
+ /**
+ * Returns a list of strings from the specified key using the specified
+ * string separator.
+ *
+ * @param name
+ * @param separator
+ * @return list of strings
+ */
+ public List<String> getStrings(String name, String separator) {
+ List<String> strings = new ArrayList<String>();
+ Properties props = getSettings();
+ if (props.containsKey(name)) {
+ String value = props.getProperty(name);
+ strings = StringUtils.getStringsFromValue(value, separator);
+ }
+ return strings;
+ }
+
+ /**
+ * Returns a map of strings from the specified key.
+ *
+ * @param name
+ * @return map of string, string
+ */
+ public Map<String, String> getMap(String name) {
+ Map<String, String> map = new LinkedHashMap<String, String>();
+ for (String string : getStrings(name)) {
+ String[] kvp = string.split("=", 2);
+ String key = kvp[0];
+ String value = kvp[1];
+ map.put(key, value);
+ }
+ return map;
+ }
+
+ /**
+ * Override the specified key with the specified value.
+ *
+ * @param key
+ * @param value
+ */
+ public void overrideSetting(String key, String value) {
+ overrides.put(key, value);
+ }
+
+ /**
+ * Updates the values for the specified keys and persists the entire
+ * configuration file.
+ *
+ * @param map
+ * of key, value pairs
+ * @return true if successful
+ */
+ public abstract boolean saveSettings(Map<String, String> updatedSettings);
+} \ No newline at end of file
diff --git a/src/main/java/com/gitblit/IUserService.java b/src/main/java/com/gitblit/IUserService.java
new file mode 100644
index 00000000..a57b0da6
--- /dev/null
+++ b/src/main/java/com/gitblit/IUserService.java
@@ -0,0 +1,325 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit;
+
+import java.util.Collection;
+import java.util.List;
+
+import com.gitblit.models.TeamModel;
+import com.gitblit.models.UserModel;
+
+/**
+ * Implementations of IUserService control all aspects of UserModel objects and
+ * user authentication.
+ *
+ * @author James Moger
+ *
+ */
+public interface IUserService {
+
+ /**
+ * Setup the user service. This method allows custom implementations to
+ * retrieve settings from gitblit.properties or the web.xml file without
+ * relying on the GitBlit static singleton.
+ *
+ * @param settings
+ * @since 0.7.0
+ */
+ void setup(IStoredSettings settings);
+
+ /**
+ * Does the user service support changes to credentials?
+ *
+ * @return true or false
+ * @since 1.0.0
+ */
+ boolean supportsCredentialChanges();
+
+ /**
+ * Does the user service support changes to user display name?
+ *
+ * @return true or false
+ * @since 1.0.0
+ */
+ boolean supportsDisplayNameChanges();
+
+ /**
+ * Does the user service support changes to user email address?
+ *
+ * @return true or false
+ * @since 1.0.0
+ */
+ boolean supportsEmailAddressChanges();
+
+ /**
+ * Does the user service support changes to team memberships?
+ *
+ * @return true or false
+ * @since 1.0.0
+ */
+ boolean supportsTeamMembershipChanges();
+
+ /**
+ * Does the user service support cookie authentication?
+ *
+ * @return true or false
+ */
+ boolean supportsCookies();
+
+ /**
+ * Returns the cookie value for the specified user.
+ *
+ * @param model
+ * @return cookie value
+ */
+ String getCookie(UserModel model);
+
+ /**
+ * Authenticate a user based on their cookie.
+ *
+ * @param cookie
+ * @return a user object or null
+ */
+ UserModel authenticate(char[] cookie);
+
+ /**
+ * Authenticate a user based on a username and password.
+ *
+ * @param username
+ * @param password
+ * @return a user object or null
+ */
+ UserModel authenticate(String username, char[] password);
+
+ /**
+ * Logout a user.
+ *
+ * @param user
+ */
+ void logout(UserModel user);
+
+ /**
+ * Retrieve the user object for the specified username.
+ *
+ * @param username
+ * @return a user object or null
+ */
+ UserModel getUserModel(String username);
+
+ /**
+ * Updates/writes a complete user object.
+ *
+ * @param model
+ * @return true if update is successful
+ */
+ boolean updateUserModel(UserModel model);
+
+ /**
+ * Updates/writes all specified user objects.
+ *
+ * @param models a list of user models
+ * @return true if update is successful
+ * @since 1.2.0
+ */
+ boolean updateUserModels(Collection<UserModel> models);
+
+ /**
+ * Adds/updates a user object keyed by username. This method allows for
+ * renaming a user.
+ *
+ * @param username
+ * the old username
+ * @param model
+ * the user object to use for username
+ * @return true if update is successful
+ */
+ boolean updateUserModel(String username, UserModel model);
+
+ /**
+ * Deletes the user object from the user service.
+ *
+ * @param model
+ * @return true if successful
+ */
+ boolean deleteUserModel(UserModel model);
+
+ /**
+ * Delete the user object with the specified username
+ *
+ * @param username
+ * @return true if successful
+ */
+ boolean deleteUser(String username);
+
+ /**
+ * Returns the list of all users available to the login service.
+ *
+ * @return list of all usernames
+ */
+ List<String> getAllUsernames();
+
+ /**
+ * Returns the list of all users available to the login service.
+ *
+ * @return list of all users
+ * @since 0.8.0
+ */
+ List<UserModel> getAllUsers();
+
+ /**
+ * Returns the list of all teams available to the login service.
+ *
+ * @return list of all teams
+ * @since 0.8.0
+ */
+ List<String> getAllTeamNames();
+
+ /**
+ * Returns the list of all teams available to the login service.
+ *
+ * @return list of all teams
+ * @since 0.8.0
+ */
+ List<TeamModel> getAllTeams();
+
+ /**
+ * Returns the list of all users who are allowed to bypass the access
+ * restriction placed on the specified repository.
+ *
+ * @param role
+ * the repository name
+ * @return list of all usernames that can bypass the access restriction
+ * @since 0.8.0
+ */
+ List<String> getTeamnamesForRepositoryRole(String role);
+
+ /**
+ * Sets the list of all teams who are allowed to bypass the access
+ * restriction placed on the specified repository.
+ *
+ * @param role
+ * the repository name
+ * @param teamnames
+ * @return true if successful
+ * @since 0.8.0
+ */
+ @Deprecated
+ boolean setTeamnamesForRepositoryRole(String role, List<String> teamnames);
+
+ /**
+ * Retrieve the team object for the specified team name.
+ *
+ * @param teamname
+ * @return a team object or null
+ * @since 0.8.0
+ */
+ TeamModel getTeamModel(String teamname);
+
+ /**
+ * Updates/writes a complete team object.
+ *
+ * @param model
+ * @return true if update is successful
+ * @since 0.8.0
+ */
+ boolean updateTeamModel(TeamModel model);
+
+ /**
+ * Updates/writes all specified team objects.
+ *
+ * @param models a list of team models
+ * @return true if update is successful
+ * @since 1.2.0
+ */
+ boolean updateTeamModels(Collection<TeamModel> models);
+
+ /**
+ * Updates/writes and replaces a complete team object keyed by teamname.
+ * This method allows for renaming a team.
+ *
+ * @param teamname
+ * the old teamname
+ * @param model
+ * the team object to use for teamname
+ * @return true if update is successful
+ * @since 0.8.0
+ */
+ boolean updateTeamModel(String teamname, TeamModel model);
+
+ /**
+ * Deletes the team object from the user service.
+ *
+ * @param model
+ * @return true if successful
+ * @since 0.8.0
+ */
+ boolean deleteTeamModel(TeamModel model);
+
+ /**
+ * Delete the team object with the specified teamname
+ *
+ * @param teamname
+ * @return true if successful
+ * @since 0.8.0
+ */
+ boolean deleteTeam(String teamname);
+
+ /**
+ * Returns the list of all users who are allowed to bypass the access
+ * restriction placed on the specified repository.
+ *
+ * @param role
+ * the repository name
+ * @return list of all usernames that can bypass the access restriction
+ * @since 0.8.0
+ */
+ List<String> getUsernamesForRepositoryRole(String role);
+
+ /**
+ * Sets the list of all uses who are allowed to bypass the access
+ * restriction placed on the specified repository.
+ *
+ * @param role
+ * the repository name
+ * @param usernames
+ * @return true if successful
+ */
+ @Deprecated
+ boolean setUsernamesForRepositoryRole(String role, List<String> usernames);
+
+ /**
+ * Renames a repository role.
+ *
+ * @param oldRole
+ * @param newRole
+ * @return true if successful
+ */
+ boolean renameRepositoryRole(String oldRole, String newRole);
+
+ /**
+ * Removes a repository role from all users.
+ *
+ * @param role
+ * @return true if successful
+ */
+ boolean deleteRepositoryRole(String role);
+
+ /**
+ * @See java.lang.Object.toString();
+ * @return string representation of the login service
+ */
+ String toString();
+}
diff --git a/src/main/java/com/gitblit/JsonServlet.java b/src/main/java/com/gitblit/JsonServlet.java
new file mode 100644
index 00000000..3ad2b7d2
--- /dev/null
+++ b/src/main/java/com/gitblit/JsonServlet.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.lang.reflect.Type;
+import java.text.MessageFormat;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.utils.JsonUtils;
+import com.gitblit.utils.StringUtils;
+
+/**
+ * Servlet class for interpreting json requests.
+ *
+ * @author James Moger
+ *
+ */
+public abstract class JsonServlet extends HttpServlet {
+
+ private static final long serialVersionUID = 1L;
+
+ protected final int forbiddenCode = HttpServletResponse.SC_FORBIDDEN;
+
+ protected final int notAllowedCode = HttpServletResponse.SC_METHOD_NOT_ALLOWED;
+
+ protected final int failureCode = HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
+
+ protected final Logger logger;
+
+ public JsonServlet() {
+ super();
+ logger = LoggerFactory.getLogger(getClass());
+ }
+
+ /**
+ * Processes an gson request.
+ *
+ * @param request
+ * @param response
+ * @throws javax.servlet.ServletException
+ * @throws java.io.IOException
+ */
+ protected abstract void processRequest(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException;
+
+ @Override
+ protected void doPost(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, java.io.IOException {
+ processRequest(request, response);
+ }
+
+ @Override
+ protected void doGet(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ processRequest(request, response);
+ }
+
+ protected <X> X deserialize(HttpServletRequest request, HttpServletResponse response,
+ Class<X> clazz) throws IOException {
+ String json = readJson(request, response);
+ if (StringUtils.isEmpty(json)) {
+ return null;
+ }
+
+ X object = JsonUtils.fromJsonString(json.toString(), clazz);
+ return object;
+ }
+
+ protected <X> X deserialize(HttpServletRequest request, HttpServletResponse response, Type type)
+ throws IOException {
+ String json = readJson(request, response);
+ if (StringUtils.isEmpty(json)) {
+ return null;
+ }
+
+ X object = JsonUtils.fromJsonString(json.toString(), type);
+ return object;
+ }
+
+ private String readJson(HttpServletRequest request, HttpServletResponse response)
+ throws IOException {
+ BufferedReader reader = request.getReader();
+ StringBuilder json = new StringBuilder();
+ String line = null;
+ while ((line = reader.readLine()) != null) {
+ json.append(line);
+ }
+ reader.close();
+
+ if (json.length() == 0) {
+ logger.error(MessageFormat.format("Failed to receive json data from {0}",
+ request.getRemoteAddr()));
+ response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+ return null;
+ }
+ return json.toString();
+ }
+
+ protected void serialize(HttpServletResponse response, Object o) throws IOException {
+ if (o != null) {
+ // Send JSON response
+ String json = JsonUtils.toJsonString(o);
+ response.setCharacterEncoding(Constants.ENCODING);
+ response.getWriter().append(json);
+ }
+ }
+}
diff --git a/src/main/java/com/gitblit/LdapUserService.java b/src/main/java/com/gitblit/LdapUserService.java
new file mode 100644
index 00000000..2867b88f
--- /dev/null
+++ b/src/main/java/com/gitblit/LdapUserService.java
@@ -0,0 +1,496 @@
+/*
+ * Copyright 2012 John Crygier
+ * Copyright 2012 gitblit.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit;
+
+import java.io.File;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.security.GeneralSecurityException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.Constants.AccountType;
+import com.gitblit.models.TeamModel;
+import com.gitblit.models.UserModel;
+import com.gitblit.utils.ArrayUtils;
+import com.gitblit.utils.StringUtils;
+import com.unboundid.ldap.sdk.Attribute;
+import com.unboundid.ldap.sdk.ExtendedResult;
+import com.unboundid.ldap.sdk.LDAPConnection;
+import com.unboundid.ldap.sdk.LDAPException;
+import com.unboundid.ldap.sdk.LDAPSearchException;
+import com.unboundid.ldap.sdk.ResultCode;
+import com.unboundid.ldap.sdk.SearchResult;
+import com.unboundid.ldap.sdk.SearchResultEntry;
+import com.unboundid.ldap.sdk.SearchScope;
+import com.unboundid.ldap.sdk.extensions.StartTLSExtendedRequest;
+import com.unboundid.util.ssl.SSLUtil;
+import com.unboundid.util.ssl.TrustAllTrustManager;
+
+/**
+ * Implementation of an LDAP user service.
+ *
+ * @author John Crygier
+ */
+public class LdapUserService extends GitblitUserService {
+
+ public static final Logger logger = LoggerFactory.getLogger(LdapUserService.class);
+
+ private IStoredSettings settings;
+ private AtomicLong lastLdapUserSync = new AtomicLong(0L);
+
+ public LdapUserService() {
+ super();
+ }
+
+ private long getSynchronizationPeriod() {
+ final String cacheDuration = settings.getString(Keys.realm.ldap.ldapCachePeriod, "2 MINUTES");
+ try {
+ final String[] s = cacheDuration.split(" ", 2);
+ long duration = Long.parseLong(s[0]);
+ TimeUnit timeUnit = TimeUnit.valueOf(s[1]);
+ return timeUnit.toMillis(duration);
+ } catch (RuntimeException ex) {
+ throw new IllegalArgumentException(Keys.realm.ldap.ldapCachePeriod + " must have format '<long> <TimeUnit>' where <TimeUnit> is one of 'MILLISECONDS', 'SECONDS', 'MINUTES', 'HOURS', 'DAYS'");
+ }
+ }
+
+ @Override
+ public void setup(IStoredSettings settings) {
+ this.settings = settings;
+ String file = settings.getString(Keys.realm.ldap.backingUserService, "${baseFolder}/users.conf");
+ File realmFile = GitBlit.getFileOrFolder(file);
+
+ serviceImpl = createUserService(realmFile);
+ logger.info("LDAP User Service backed by " + serviceImpl.toString());
+
+ synchronizeLdapUsers();
+ }
+
+ protected synchronized void synchronizeLdapUsers() {
+ final boolean enabled = settings.getBoolean(Keys.realm.ldap.synchronizeUsers.enable, false);
+ if (enabled) {
+ if (System.currentTimeMillis() > (lastLdapUserSync.get() + getSynchronizationPeriod())) {
+ logger.info("Synchronizing with LDAP @ " + settings.getRequiredString(Keys.realm.ldap.server));
+ final boolean deleteRemovedLdapUsers = settings.getBoolean(Keys.realm.ldap.synchronizeUsers.removeDeleted, true);
+ LDAPConnection ldapConnection = getLdapConnection();
+ if (ldapConnection != null) {
+ try {
+ String accountBase = settings.getString(Keys.realm.ldap.accountBase, "");
+ String uidAttribute = settings.getString(Keys.realm.ldap.uid, "uid");
+ String accountPattern = settings.getString(Keys.realm.ldap.accountPattern, "(&(objectClass=person)(sAMAccountName=${username}))");
+ accountPattern = StringUtils.replace(accountPattern, "${username}", "*");
+
+ SearchResult result = doSearch(ldapConnection, accountBase, accountPattern);
+ if (result != null && result.getEntryCount() > 0) {
+ final Map<String, UserModel> ldapUsers = new HashMap<String, UserModel>();
+
+ for (SearchResultEntry loggingInUser : result.getSearchEntries()) {
+
+ final String username = loggingInUser.getAttribute(uidAttribute).getValue();
+ logger.debug("LDAP synchronizing: " + username);
+
+ UserModel user = getUserModel(username);
+ if (user == null) {
+ user = new UserModel(username);
+ }
+
+ if (!supportsTeamMembershipChanges())
+ getTeamsFromLdap(ldapConnection, username, loggingInUser, user);
+
+ // Get User Attributes
+ setUserAttributes(user, loggingInUser);
+
+ // store in map
+ ldapUsers.put(username.toLowerCase(), user);
+ }
+
+ if (deleteRemovedLdapUsers) {
+ logger.debug("detecting removed LDAP users...");
+
+ for (UserModel userModel : super.getAllUsers()) {
+ if (ExternalAccount.equals(userModel.password)) {
+ if (! ldapUsers.containsKey(userModel.username)) {
+ logger.info("deleting removed LDAP user " + userModel.username + " from backing user service");
+ super.deleteUser(userModel.username);
+ }
+ }
+ }
+ }
+
+ super.updateUserModels(ldapUsers.values());
+
+ if (!supportsTeamMembershipChanges()) {
+ final Map<String, TeamModel> userTeams = new HashMap<String, TeamModel>();
+ for (UserModel user : ldapUsers.values()) {
+ for (TeamModel userTeam : user.teams) {
+ userTeams.put(userTeam.name, userTeam);
+ }
+ }
+ updateTeamModels(userTeams.values());
+ }
+ }
+ lastLdapUserSync.set(System.currentTimeMillis());
+ } finally {
+ ldapConnection.close();
+ }
+ }
+ }
+ }
+ }
+
+ private LDAPConnection getLdapConnection() {
+ try {
+ URI ldapUrl = new URI(settings.getRequiredString(Keys.realm.ldap.server));
+ String bindUserName = settings.getString(Keys.realm.ldap.username, "");
+ String bindPassword = settings.getString(Keys.realm.ldap.password, "");
+ int ldapPort = ldapUrl.getPort();
+
+ if (ldapUrl.getScheme().equalsIgnoreCase("ldaps")) { // SSL
+ if (ldapPort == -1) // Default Port
+ ldapPort = 636;
+
+ SSLUtil sslUtil = new SSLUtil(new TrustAllTrustManager());
+ return new LDAPConnection(sslUtil.createSSLSocketFactory(), ldapUrl.getHost(), ldapPort, bindUserName, bindPassword);
+ } else {
+ if (ldapPort == -1) // Default Port
+ ldapPort = 389;
+
+ LDAPConnection conn = new LDAPConnection(ldapUrl.getHost(), ldapPort, bindUserName, bindPassword);
+
+ if (ldapUrl.getScheme().equalsIgnoreCase("ldap+tls")) {
+ SSLUtil sslUtil = new SSLUtil(new TrustAllTrustManager());
+
+ ExtendedResult extendedResult = conn.processExtendedOperation(
+ new StartTLSExtendedRequest(sslUtil.createSSLContext()));
+
+ if (extendedResult.getResultCode() != ResultCode.SUCCESS) {
+ throw new LDAPException(extendedResult.getResultCode());
+ }
+ }
+ return conn;
+ }
+ } catch (URISyntaxException e) {
+ logger.error("Bad LDAP URL, should be in the form: ldap(s|+tls)://<server>:<port>", e);
+ } catch (GeneralSecurityException e) {
+ logger.error("Unable to create SSL Connection", e);
+ } catch (LDAPException e) {
+ logger.error("Error Connecting to LDAP", e);
+ }
+
+ return null;
+ }
+
+ /**
+ * Credentials are defined in the LDAP server and can not be manipulated
+ * from Gitblit.
+ *
+ * @return false
+ * @since 1.0.0
+ */
+ @Override
+ public boolean supportsCredentialChanges() {
+ return false;
+ }
+
+ /**
+ * If no displayName pattern is defined then Gitblit can manage the display name.
+ *
+ * @return true if Gitblit can manage the user display name
+ * @since 1.0.0
+ */
+ @Override
+ public boolean supportsDisplayNameChanges() {
+ return StringUtils.isEmpty(settings.getString(Keys.realm.ldap.displayName, ""));
+ }
+
+ /**
+ * If no email pattern is defined then Gitblit can manage the email address.
+ *
+ * @return true if Gitblit can manage the user email address
+ * @since 1.0.0
+ */
+ @Override
+ public boolean supportsEmailAddressChanges() {
+ return StringUtils.isEmpty(settings.getString(Keys.realm.ldap.email, ""));
+ }
+
+
+ /**
+ * If the LDAP server will maintain team memberships then LdapUserService
+ * will not allow team membership changes. In this scenario all team
+ * changes must be made on the LDAP server by the LDAP administrator.
+ *
+ * @return true or false
+ * @since 1.0.0
+ */
+ public boolean supportsTeamMembershipChanges() {
+ return !settings.getBoolean(Keys.realm.ldap.maintainTeams, false);
+ }
+
+ @Override
+ protected AccountType getAccountType() {
+ return AccountType.LDAP;
+ }
+
+ @Override
+ public UserModel authenticate(String username, char[] password) {
+ if (isLocalAccount(username)) {
+ // local account, bypass LDAP authentication
+ return super.authenticate(username, password);
+ }
+
+ String simpleUsername = getSimpleUsername(username);
+
+ LDAPConnection ldapConnection = getLdapConnection();
+ if (ldapConnection != null) {
+ try {
+ // Find the logging in user's DN
+ String accountBase = settings.getString(Keys.realm.ldap.accountBase, "");
+ String accountPattern = settings.getString(Keys.realm.ldap.accountPattern, "(&(objectClass=person)(sAMAccountName=${username}))");
+ accountPattern = StringUtils.replace(accountPattern, "${username}", escapeLDAPSearchFilter(simpleUsername));
+
+ SearchResult result = doSearch(ldapConnection, accountBase, accountPattern);
+ if (result != null && result.getEntryCount() == 1) {
+ SearchResultEntry loggingInUser = result.getSearchEntries().get(0);
+ String loggingInUserDN = loggingInUser.getDN();
+
+ if (isAuthenticated(ldapConnection, loggingInUserDN, new String(password))) {
+ logger.debug("LDAP authenticated: " + username);
+
+ UserModel user = null;
+ synchronized (this) {
+ user = getUserModel(simpleUsername);
+ if (user == null) // create user object for new authenticated user
+ user = new UserModel(simpleUsername);
+
+ // create a user cookie
+ if (StringUtils.isEmpty(user.cookie) && !ArrayUtils.isEmpty(password)) {
+ user.cookie = StringUtils.getSHA1(user.username + new String(password));
+ }
+
+ if (!supportsTeamMembershipChanges())
+ getTeamsFromLdap(ldapConnection, simpleUsername, loggingInUser, user);
+
+ // Get User Attributes
+ setUserAttributes(user, loggingInUser);
+
+ // Push the ldap looked up values to backing file
+ super.updateUserModel(user);
+ if (!supportsTeamMembershipChanges()) {
+ for (TeamModel userTeam : user.teams)
+ updateTeamModel(userTeam);
+ }
+ }
+
+ return user;
+ }
+ }
+ } finally {
+ ldapConnection.close();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Set the admin attribute from team memberships retrieved from LDAP.
+ * If we are not storing teams in LDAP and/or we have not defined any
+ * administrator teams, then do not change the admin flag.
+ *
+ * @param user
+ */
+ private void setAdminAttribute(UserModel user) {
+ if (!supportsTeamMembershipChanges()) {
+ List<String> admins = settings.getStrings(Keys.realm.ldap.admins);
+ // if we have defined administrative teams, then set admin flag
+ // otherwise leave admin flag unchanged
+ if (!ArrayUtils.isEmpty(admins)) {
+ user.canAdmin = false;
+ for (String admin : admins) {
+ if (admin.startsWith("@")) { // Team
+ if (user.getTeam(admin.substring(1)) != null)
+ user.canAdmin = true;
+ } else
+ if (user.getName().equalsIgnoreCase(admin))
+ user.canAdmin = true;
+ }
+ }
+ }
+ }
+
+ private void setUserAttributes(UserModel user, SearchResultEntry userEntry) {
+ // Is this user an admin?
+ setAdminAttribute(user);
+
+ // Don't want visibility into the real password, make up a dummy
+ user.password = ExternalAccount;
+ user.accountType = getAccountType();
+
+ // Get full name Attribute
+ String displayName = settings.getString(Keys.realm.ldap.displayName, "");
+ if (!StringUtils.isEmpty(displayName)) {
+ // Replace embedded ${} with attributes
+ if (displayName.contains("${")) {
+ for (Attribute userAttribute : userEntry.getAttributes())
+ displayName = StringUtils.replace(displayName, "${" + userAttribute.getName() + "}", userAttribute.getValue());
+
+ user.displayName = displayName;
+ } else {
+ Attribute attribute = userEntry.getAttribute(displayName);
+ if (attribute != null && attribute.hasValue()) {
+ user.displayName = attribute.getValue();
+ }
+ }
+ }
+
+ // Get email address Attribute
+ String email = settings.getString(Keys.realm.ldap.email, "");
+ if (!StringUtils.isEmpty(email)) {
+ if (email.contains("${")) {
+ for (Attribute userAttribute : userEntry.getAttributes())
+ email = StringUtils.replace(email, "${" + userAttribute.getName() + "}", userAttribute.getValue());
+
+ user.emailAddress = email;
+ } else {
+ Attribute attribute = userEntry.getAttribute(email);
+ if (attribute != null && attribute.hasValue()) {
+ user.emailAddress = attribute.getValue();
+ }
+ }
+ }
+ }
+
+ private void getTeamsFromLdap(LDAPConnection ldapConnection, String simpleUsername, SearchResultEntry loggingInUser, UserModel user) {
+ String loggingInUserDN = loggingInUser.getDN();
+
+ user.teams.clear(); // Clear the users team memberships - we're going to get them from LDAP
+ String groupBase = settings.getString(Keys.realm.ldap.groupBase, "");
+ String groupMemberPattern = settings.getString(Keys.realm.ldap.groupMemberPattern, "(&(objectClass=group)(member=${dn}))");
+
+ groupMemberPattern = StringUtils.replace(groupMemberPattern, "${dn}", escapeLDAPSearchFilter(loggingInUserDN));
+ groupMemberPattern = StringUtils.replace(groupMemberPattern, "${username}", escapeLDAPSearchFilter(simpleUsername));
+
+ // Fill in attributes into groupMemberPattern
+ for (Attribute userAttribute : loggingInUser.getAttributes())
+ groupMemberPattern = StringUtils.replace(groupMemberPattern, "${" + userAttribute.getName() + "}", escapeLDAPSearchFilter(userAttribute.getValue()));
+
+ SearchResult teamMembershipResult = doSearch(ldapConnection, groupBase, groupMemberPattern);
+ if (teamMembershipResult != null && teamMembershipResult.getEntryCount() > 0) {
+ for (int i = 0; i < teamMembershipResult.getEntryCount(); i++) {
+ SearchResultEntry teamEntry = teamMembershipResult.getSearchEntries().get(i);
+ String teamName = teamEntry.getAttribute("cn").getValue();
+
+ TeamModel teamModel = getTeamModel(teamName);
+ if (teamModel == null)
+ teamModel = createTeamFromLdap(teamEntry);
+
+ user.teams.add(teamModel);
+ teamModel.addUser(user.getName());
+ }
+ }
+ }
+
+ private TeamModel createTeamFromLdap(SearchResultEntry teamEntry) {
+ TeamModel answer = new TeamModel(teamEntry.getAttributeValue("cn"));
+ // potentially retrieve other attributes here in the future
+
+ return answer;
+ }
+
+ private SearchResult doSearch(LDAPConnection ldapConnection, String base, String filter) {
+ try {
+ return ldapConnection.search(base, SearchScope.SUB, filter);
+ } catch (LDAPSearchException e) {
+ logger.error("Problem Searching LDAP", e);
+
+ return null;
+ }
+ }
+
+ private boolean isAuthenticated(LDAPConnection ldapConnection, String userDn, String password) {
+ try {
+ // Binding will stop any LDAP-Injection Attacks since the searched-for user needs to bind to that DN
+ ldapConnection.bind(userDn, password);
+ return true;
+ } catch (LDAPException e) {
+ logger.error("Error authenticating user", e);
+ return false;
+ }
+ }
+
+ @Override
+ public List<String> getAllUsernames() {
+ synchronizeLdapUsers();
+ return super.getAllUsernames();
+ }
+
+ @Override
+ public List<UserModel> getAllUsers() {
+ synchronizeLdapUsers();
+ return super.getAllUsers();
+ }
+
+ /**
+ * Returns a simple username without any domain prefixes.
+ *
+ * @param username
+ * @return a simple username
+ */
+ protected String getSimpleUsername(String username) {
+ int lastSlash = username.lastIndexOf('\\');
+ if (lastSlash > -1) {
+ username = username.substring(lastSlash + 1);
+ }
+
+ return username;
+ }
+
+ // From: https://www.owasp.org/index.php/Preventing_LDAP_Injection_in_Java
+ public static final String escapeLDAPSearchFilter(String filter) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < filter.length(); i++) {
+ char curChar = filter.charAt(i);
+ switch (curChar) {
+ case '\\':
+ sb.append("\\5c");
+ break;
+ case '*':
+ sb.append("\\2a");
+ break;
+ case '(':
+ sb.append("\\28");
+ break;
+ case ')':
+ sb.append("\\29");
+ break;
+ case '\u0000':
+ sb.append("\\00");
+ break;
+ default:
+ sb.append(curChar);
+ }
+ }
+ return sb.toString();
+ }
+}
diff --git a/src/main/java/com/gitblit/LuceneExecutor.java b/src/main/java/com/gitblit/LuceneExecutor.java
new file mode 100644
index 00000000..0e4baae9
--- /dev/null
+++ b/src/main/java/com/gitblit/LuceneExecutor.java
@@ -0,0 +1,1375 @@
+/*
+ * Copyright 2012 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit;
+
+import static org.eclipse.jgit.treewalk.filter.TreeFilter.ANY_DIFF;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Method;
+import java.text.MessageFormat;
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.apache.lucene.analysis.Analyzer;
+import org.apache.lucene.analysis.standard.StandardAnalyzer;
+import org.apache.lucene.document.DateTools;
+import org.apache.lucene.document.DateTools.Resolution;
+import org.apache.lucene.document.Document;
+import org.apache.lucene.document.Field;
+import org.apache.lucene.document.Field.Index;
+import org.apache.lucene.document.Field.Store;
+import org.apache.lucene.index.IndexReader;
+import org.apache.lucene.index.IndexWriter;
+import org.apache.lucene.index.IndexWriterConfig;
+import org.apache.lucene.index.IndexWriterConfig.OpenMode;
+import org.apache.lucene.index.MultiReader;
+import org.apache.lucene.index.Term;
+import org.apache.lucene.queryParser.QueryParser;
+import org.apache.lucene.search.BooleanClause.Occur;
+import org.apache.lucene.search.BooleanQuery;
+import org.apache.lucene.search.IndexSearcher;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.ScoreDoc;
+import org.apache.lucene.search.TermQuery;
+import org.apache.lucene.search.TopScoreDocCollector;
+import org.apache.lucene.search.highlight.Fragmenter;
+import org.apache.lucene.search.highlight.Highlighter;
+import org.apache.lucene.search.highlight.InvalidTokenOffsetsException;
+import org.apache.lucene.search.highlight.QueryScorer;
+import org.apache.lucene.search.highlight.SimpleHTMLFormatter;
+import org.apache.lucene.search.highlight.SimpleSpanFragmenter;
+import org.apache.lucene.store.Directory;
+import org.apache.lucene.store.FSDirectory;
+import org.apache.lucene.util.Version;
+import org.eclipse.jgit.diff.DiffEntry.ChangeType;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectLoader;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.RepositoryCache.FileKey;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevTree;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.treewalk.EmptyTreeIterator;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.util.FS;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.Constants.SearchObjectType;
+import com.gitblit.models.IssueModel;
+import com.gitblit.models.IssueModel.Attachment;
+import com.gitblit.models.PathModel.PathChangeModel;
+import com.gitblit.models.RefModel;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.SearchResult;
+import com.gitblit.utils.ArrayUtils;
+import com.gitblit.utils.IssueUtils;
+import com.gitblit.utils.JGitUtils;
+import com.gitblit.utils.StringUtils;
+
+/**
+ * The Lucene executor handles indexing and searching repositories.
+ *
+ * @author James Moger
+ *
+ */
+public class LuceneExecutor implements Runnable {
+
+
+ private static final int INDEX_VERSION = 5;
+
+ private static final String FIELD_OBJECT_TYPE = "type";
+ private static final String FIELD_ISSUE = "issue";
+ private static final String FIELD_PATH = "path";
+ private static final String FIELD_COMMIT = "commit";
+ private static final String FIELD_BRANCH = "branch";
+ private static final String FIELD_SUMMARY = "summary";
+ private static final String FIELD_CONTENT = "content";
+ private static final String FIELD_AUTHOR = "author";
+ private static final String FIELD_COMMITTER = "committer";
+ private static final String FIELD_DATE = "date";
+ private static final String FIELD_TAG = "tag";
+ private static final String FIELD_LABEL = "label";
+ private static final String FIELD_ATTACHMENT = "attachment";
+
+ private static final String CONF_FILE = "lucene.conf";
+ private static final String LUCENE_DIR = "lucene";
+ private static final String CONF_INDEX = "index";
+ private static final String CONF_VERSION = "version";
+ private static final String CONF_ALIAS = "aliases";
+ private static final String CONF_BRANCH = "branches";
+
+ private static final Version LUCENE_VERSION = Version.LUCENE_35;
+
+ private final Logger logger = LoggerFactory.getLogger(LuceneExecutor.class);
+
+ private final IStoredSettings storedSettings;
+ private final File repositoriesFolder;
+
+ private final Map<String, IndexSearcher> searchers = new ConcurrentHashMap<String, IndexSearcher>();
+ private final Map<String, IndexWriter> writers = new ConcurrentHashMap<String, IndexWriter>();
+
+ private final String luceneIgnoreExtensions = "7z arc arj bin bmp dll doc docx exe gif gz jar jpg lib lzh odg odf odt pdf ppt png so swf xcf xls xlsx zip";
+ private Set<String> excludedExtensions;
+
+ public LuceneExecutor(IStoredSettings settings, File repositoriesFolder) {
+ this.storedSettings = settings;
+ this.repositoriesFolder = repositoriesFolder;
+ String exts = luceneIgnoreExtensions;
+ if (settings != null) {
+ exts = settings.getString(Keys.web.luceneIgnoreExtensions, exts);
+ }
+ excludedExtensions = new TreeSet<String>(StringUtils.getStringsFromValue(exts));
+ }
+
+ /**
+ * Run is executed by the Gitblit executor service. Because this is called
+ * by an executor service, calls will queue - i.e. there can never be
+ * concurrent execution of repository index updates.
+ */
+ @Override
+ public void run() {
+ if (!storedSettings.getBoolean(Keys.web.allowLuceneIndexing, true)) {
+ // Lucene indexing is disabled
+ return;
+ }
+ // reload the excluded extensions
+ String exts = storedSettings.getString(Keys.web.luceneIgnoreExtensions, luceneIgnoreExtensions);
+ excludedExtensions = new TreeSet<String>(StringUtils.getStringsFromValue(exts));
+
+ if (GitBlit.self().isCollectingGarbage()) {
+ // busy collecting garbage, try again later
+ return;
+ }
+
+ for (String repositoryName: GitBlit.self().getRepositoryList()) {
+ RepositoryModel model = GitBlit.self().getRepositoryModel(repositoryName);
+ if (model.hasCommits && !ArrayUtils.isEmpty(model.indexedBranches)) {
+ Repository repository = GitBlit.self().getRepository(model.name);
+ if (repository == null) {
+ if (GitBlit.self().isCollectingGarbage(model.name)) {
+ logger.info(MessageFormat.format("Skipping Lucene index of {0}, busy garbage collecting", repositoryName));
+ }
+ continue;
+ }
+ index(model, repository);
+ repository.close();
+ System.gc();
+ }
+ }
+ }
+
+ /**
+ * Synchronously indexes a repository. This may build a complete index of a
+ * repository or it may update an existing index.
+ *
+ * @param name
+ * the name of the repository
+ * @param repository
+ * the repository object
+ */
+ private void index(RepositoryModel model, Repository repository) {
+ try {
+ if (shouldReindex(repository)) {
+ // (re)build the entire index
+ IndexResult result = reindex(model, repository);
+
+ if (result.success) {
+ if (result.commitCount > 0) {
+ String msg = "Built {0} Lucene index from {1} commits and {2} files across {3} branches in {4} secs";
+ logger.info(MessageFormat.format(msg, model.name, result.commitCount,
+ result.blobCount, result.branchCount, result.duration()));
+ }
+ } else {
+ String msg = "Could not build {0} Lucene index!";
+ logger.error(MessageFormat.format(msg, model.name));
+ }
+ } else {
+ // update the index with latest commits
+ IndexResult result = updateIndex(model, repository);
+ if (result.success) {
+ if (result.commitCount > 0) {
+ String msg = "Updated {0} Lucene index with {1} commits and {2} files across {3} branches in {4} secs";
+ logger.info(MessageFormat.format(msg, model.name, result.commitCount,
+ result.blobCount, result.branchCount, result.duration()));
+ }
+ } else {
+ String msg = "Could not update {0} Lucene index!";
+ logger.error(MessageFormat.format(msg, model.name));
+ }
+ }
+ } catch (Throwable t) {
+ logger.error(MessageFormat.format("Lucene indexing failure for {0}", model.name), t);
+ }
+ }
+
+ /**
+ * Close the writer/searcher objects for a repository.
+ *
+ * @param repositoryName
+ */
+ public synchronized void close(String repositoryName) {
+ try {
+ IndexSearcher searcher = searchers.remove(repositoryName);
+ if (searcher != null) {
+ searcher.getIndexReader().close();
+ }
+ } catch (Exception e) {
+ logger.error("Failed to close index searcher for " + repositoryName, e);
+ }
+
+ try {
+ IndexWriter writer = writers.remove(repositoryName);
+ if (writer != null) {
+ writer.close();
+ }
+ } catch (Exception e) {
+ logger.error("Failed to close index writer for " + repositoryName, e);
+ }
+ }
+
+ /**
+ * Close all Lucene indexers.
+ *
+ */
+ public synchronized void close() {
+ // close all writers
+ for (String writer : writers.keySet()) {
+ try {
+ writers.get(writer).close(true);
+ } catch (Throwable t) {
+ logger.error("Failed to close Lucene writer for " + writer, t);
+ }
+ }
+ writers.clear();
+
+ // close all searchers
+ for (String searcher : searchers.keySet()) {
+ try {
+ searchers.get(searcher).getIndexReader().close();
+ } catch (Throwable t) {
+ logger.error("Failed to close Lucene searcher for " + searcher, t);
+ }
+ }
+ searchers.clear();
+ }
+
+
+ /**
+ * Deletes the Lucene index for the specified repository.
+ *
+ * @param repositoryName
+ * @return true, if successful
+ */
+ public boolean deleteIndex(String repositoryName) {
+ try {
+ // close any open writer/searcher
+ close(repositoryName);
+
+ // delete the index folder
+ File repositoryFolder = FileKey.resolve(new File(repositoriesFolder, repositoryName), FS.DETECTED);
+ File luceneIndex = new File(repositoryFolder, LUCENE_DIR);
+ if (luceneIndex.exists()) {
+ org.eclipse.jgit.util.FileUtils.delete(luceneIndex,
+ org.eclipse.jgit.util.FileUtils.RECURSIVE);
+ }
+ // delete the config file
+ File luceneConfig = new File(repositoryFolder, CONF_FILE);
+ if (luceneConfig.exists()) {
+ luceneConfig.delete();
+ }
+ return true;
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Returns the author for the commit, if this information is available.
+ *
+ * @param commit
+ * @return an author or unknown
+ */
+ private String getAuthor(RevCommit commit) {
+ String name = "unknown";
+ try {
+ name = commit.getAuthorIdent().getName();
+ if (StringUtils.isEmpty(name)) {
+ name = commit.getAuthorIdent().getEmailAddress();
+ }
+ } catch (NullPointerException n) {
+ }
+ return name;
+ }
+
+ /**
+ * Returns the committer for the commit, if this information is available.
+ *
+ * @param commit
+ * @return an committer or unknown
+ */
+ private String getCommitter(RevCommit commit) {
+ String name = "unknown";
+ try {
+ name = commit.getCommitterIdent().getName();
+ if (StringUtils.isEmpty(name)) {
+ name = commit.getCommitterIdent().getEmailAddress();
+ }
+ } catch (NullPointerException n) {
+ }
+ return name;
+ }
+
+ /**
+ * Get the tree associated with the given commit.
+ *
+ * @param walk
+ * @param commit
+ * @return tree
+ * @throws IOException
+ */
+ private RevTree getTree(final RevWalk walk, final RevCommit commit)
+ throws IOException {
+ final RevTree tree = commit.getTree();
+ if (tree != null) {
+ return tree;
+ }
+ walk.parseHeaders(commit);
+ return commit.getTree();
+ }
+
+ /**
+ * Construct a keyname from the branch.
+ *
+ * @param branchName
+ * @return a keyname appropriate for the Git config file format
+ */
+ private String getBranchKey(String branchName) {
+ return StringUtils.getSHA1(branchName);
+ }
+
+ /**
+ * Returns the Lucene configuration for the specified repository.
+ *
+ * @param repository
+ * @return a config object
+ */
+ private FileBasedConfig getConfig(Repository repository) {
+ File file = new File(repository.getDirectory(), CONF_FILE);
+ FileBasedConfig config = new FileBasedConfig(file, FS.detect());
+ return config;
+ }
+
+ /**
+ * Reads the Lucene config file for the repository to check the index
+ * version. If the index version is different, then rebuild the repository
+ * index.
+ *
+ * @param repository
+ * @return true of the on-disk index format is different than INDEX_VERSION
+ */
+ private boolean shouldReindex(Repository repository) {
+ try {
+ FileBasedConfig config = getConfig(repository);
+ config.load();
+ int indexVersion = config.getInt(CONF_INDEX, CONF_VERSION, 0);
+ // reindex if versions do not match
+ return indexVersion != INDEX_VERSION;
+ } catch (Throwable t) {
+ }
+ return true;
+ }
+
+
+ /**
+ * This completely indexes the repository and will destroy any existing
+ * index.
+ *
+ * @param repositoryName
+ * @param repository
+ * @return IndexResult
+ */
+ public IndexResult reindex(RepositoryModel model, Repository repository) {
+ IndexResult result = new IndexResult();
+ if (!deleteIndex(model.name)) {
+ return result;
+ }
+ try {
+ String [] encodings = storedSettings.getStrings(Keys.web.blobEncodings).toArray(new String[0]);
+ FileBasedConfig config = getConfig(repository);
+ Set<String> indexedCommits = new TreeSet<String>();
+ IndexWriter writer = getIndexWriter(model.name);
+ // build a quick lookup of tags
+ Map<String, List<String>> tags = new HashMap<String, List<String>>();
+ for (RefModel tag : JGitUtils.getTags(repository, false, -1)) {
+ if (!tag.isAnnotatedTag()) {
+ // skip non-annotated tags
+ continue;
+ }
+ if (!tags.containsKey(tag.getObjectId())) {
+ tags.put(tag.getReferencedObjectId().getName(), new ArrayList<String>());
+ }
+ tags.get(tag.getReferencedObjectId().getName()).add(tag.displayName);
+ }
+
+ ObjectReader reader = repository.newObjectReader();
+
+ // get the local branches
+ List<RefModel> branches = JGitUtils.getLocalBranches(repository, true, -1);
+
+ // sort them by most recently updated
+ Collections.sort(branches, new Comparator<RefModel>() {
+ @Override
+ public int compare(RefModel ref1, RefModel ref2) {
+ return ref2.getDate().compareTo(ref1.getDate());
+ }
+ });
+
+ // reorder default branch to first position
+ RefModel defaultBranch = null;
+ ObjectId defaultBranchId = JGitUtils.getDefaultBranch(repository);
+ for (RefModel branch : branches) {
+ if (branch.getObjectId().equals(defaultBranchId)) {
+ defaultBranch = branch;
+ break;
+ }
+ }
+ branches.remove(defaultBranch);
+ branches.add(0, defaultBranch);
+
+ // walk through each branch
+ for (RefModel branch : branches) {
+
+ boolean indexBranch = false;
+ if (model.indexedBranches.contains(com.gitblit.Constants.DEFAULT_BRANCH)
+ && branch.equals(defaultBranch)) {
+ // indexing "default" branch
+ indexBranch = true;
+ } else if (IssueUtils.GB_ISSUES.equals(branch)) {
+ // skip the GB_ISSUES branch because it is indexed later
+ // note: this is different than updateIndex
+ indexBranch = false;
+ } else {
+ // normal explicit branch check
+ indexBranch = model.indexedBranches.contains(branch.getName());
+ }
+
+ // if this branch is not specifically indexed then skip
+ if (!indexBranch) {
+ continue;
+ }
+
+ String branchName = branch.getName();
+ RevWalk revWalk = new RevWalk(reader);
+ RevCommit tip = revWalk.parseCommit(branch.getObjectId());
+ String tipId = tip.getId().getName();
+
+ String keyName = getBranchKey(branchName);
+ config.setString(CONF_ALIAS, null, keyName, branchName);
+ config.setString(CONF_BRANCH, null, keyName, tipId);
+
+ // index the blob contents of the tree
+ TreeWalk treeWalk = new TreeWalk(repository);
+ treeWalk.addTree(tip.getTree());
+ treeWalk.setRecursive(true);
+
+ Map<String, ObjectId> paths = new TreeMap<String, ObjectId>();
+ while (treeWalk.next()) {
+ // ensure path is not in a submodule
+ if (treeWalk.getFileMode(0) != FileMode.GITLINK) {
+ paths.put(treeWalk.getPathString(), treeWalk.getObjectId(0));
+ }
+ }
+
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ byte[] tmp = new byte[32767];
+
+ RevWalk commitWalk = new RevWalk(reader);
+ commitWalk.markStart(tip);
+
+ RevCommit commit;
+ while ((paths.size() > 0) && (commit = commitWalk.next()) != null) {
+ TreeWalk diffWalk = new TreeWalk(reader);
+ int parentCount = commit.getParentCount();
+ switch (parentCount) {
+ case 0:
+ diffWalk.addTree(new EmptyTreeIterator());
+ break;
+ case 1:
+ diffWalk.addTree(getTree(commitWalk, commit.getParent(0)));
+ break;
+ default:
+ // skip merge commits
+ continue;
+ }
+ diffWalk.addTree(getTree(commitWalk, commit));
+ diffWalk.setFilter(ANY_DIFF);
+ diffWalk.setRecursive(true);
+ while ((paths.size() > 0) && diffWalk.next()) {
+ String path = diffWalk.getPathString();
+ if (!paths.containsKey(path)) {
+ continue;
+ }
+
+ // remove path from set
+ ObjectId blobId = paths.remove(path);
+ result.blobCount++;
+
+ // index the blob metadata
+ String blobAuthor = getAuthor(commit);
+ String blobCommitter = getCommitter(commit);
+ String blobDate = DateTools.timeToString(commit.getCommitTime() * 1000L,
+ Resolution.MINUTE);
+
+ Document doc = new Document();
+ doc.add(new Field(FIELD_OBJECT_TYPE, SearchObjectType.blob.name(), Store.YES, Index.NOT_ANALYZED_NO_NORMS));
+ doc.add(new Field(FIELD_BRANCH, branchName, Store.YES, Index.ANALYZED));
+ doc.add(new Field(FIELD_COMMIT, commit.getName(), Store.YES, Index.ANALYZED));
+ doc.add(new Field(FIELD_PATH, path, Store.YES, Index.ANALYZED));
+ doc.add(new Field(FIELD_DATE, blobDate, Store.YES, Index.NO));
+ doc.add(new Field(FIELD_AUTHOR, blobAuthor, Store.YES, Index.ANALYZED));
+ doc.add(new Field(FIELD_COMMITTER, blobCommitter, Store.YES, Index.ANALYZED));
+
+ // determine extension to compare to the extension
+ // blacklist
+ String ext = null;
+ String name = path.toLowerCase();
+ if (name.indexOf('.') > -1) {
+ ext = name.substring(name.lastIndexOf('.') + 1);
+ }
+
+ // index the blob content
+ if (StringUtils.isEmpty(ext) || !excludedExtensions.contains(ext)) {
+ ObjectLoader ldr = repository.open(blobId, Constants.OBJ_BLOB);
+ InputStream in = ldr.openStream();
+ int n;
+ while ((n = in.read(tmp)) > 0) {
+ os.write(tmp, 0, n);
+ }
+ in.close();
+ byte[] content = os.toByteArray();
+ String str = StringUtils.decodeString(content, encodings);
+ doc.add(new Field(FIELD_CONTENT, str, Store.YES, Index.ANALYZED));
+ os.reset();
+ }
+
+ // add the blob to the index
+ writer.addDocument(doc);
+ }
+ }
+
+ os.close();
+
+ // index the tip commit object
+ if (indexedCommits.add(tipId)) {
+ Document doc = createDocument(tip, tags.get(tipId));
+ doc.add(new Field(FIELD_BRANCH, branchName, Store.YES, Index.ANALYZED));
+ writer.addDocument(doc);
+ result.commitCount += 1;
+ result.branchCount += 1;
+ }
+
+ // traverse the log and index the previous commit objects
+ RevWalk historyWalk = new RevWalk(reader);
+ historyWalk.markStart(historyWalk.parseCommit(tip.getId()));
+ RevCommit rev;
+ while ((rev = historyWalk.next()) != null) {
+ String hash = rev.getId().getName();
+ if (indexedCommits.add(hash)) {
+ Document doc = createDocument(rev, tags.get(hash));
+ doc.add(new Field(FIELD_BRANCH, branchName, Store.YES, Index.ANALYZED));
+ writer.addDocument(doc);
+ result.commitCount += 1;
+ }
+ }
+ }
+
+ // finished
+ reader.release();
+
+ // this repository has a gb-issues branch, index all issues
+ if (IssueUtils.getIssuesBranch(repository) != null) {
+ List<IssueModel> issues = IssueUtils.getIssues(repository, null);
+ if (issues.size() > 0) {
+ result.branchCount += 1;
+ }
+ for (IssueModel issue : issues) {
+ result.issueCount++;
+ Document doc = createDocument(issue);
+ writer.addDocument(doc);
+ }
+ }
+
+ // commit all changes and reset the searcher
+ config.setInt(CONF_INDEX, null, CONF_VERSION, INDEX_VERSION);
+ config.save();
+ writer.commit();
+ resetIndexSearcher(model.name);
+ result.success();
+ } catch (Exception e) {
+ logger.error("Exception while reindexing " + model.name, e);
+ }
+ return result;
+ }
+
+ /**
+ * Incrementally update the index with the specified commit for the
+ * repository.
+ *
+ * @param repositoryName
+ * @param repository
+ * @param branch
+ * the fully qualified branch name (e.g. refs/heads/master)
+ * @param commit
+ * @return true, if successful
+ */
+ private IndexResult index(String repositoryName, Repository repository,
+ String branch, RevCommit commit) {
+ IndexResult result = new IndexResult();
+ try {
+ String [] encodings = storedSettings.getStrings(Keys.web.blobEncodings).toArray(new String[0]);
+ List<PathChangeModel> changedPaths = JGitUtils.getFilesInCommit(repository, commit);
+ String revDate = DateTools.timeToString(commit.getCommitTime() * 1000L,
+ Resolution.MINUTE);
+ IndexWriter writer = getIndexWriter(repositoryName);
+ for (PathChangeModel path : changedPaths) {
+ if (path.isSubmodule()) {
+ continue;
+ }
+ // delete the indexed blob
+ deleteBlob(repositoryName, branch, path.name);
+
+ // re-index the blob
+ if (!ChangeType.DELETE.equals(path.changeType)) {
+ result.blobCount++;
+ Document doc = new Document();
+ doc.add(new Field(FIELD_OBJECT_TYPE, SearchObjectType.blob.name(), Store.YES,
+ Index.NOT_ANALYZED));
+ doc.add(new Field(FIELD_BRANCH, branch, Store.YES, Index.ANALYZED));
+ doc.add(new Field(FIELD_COMMIT, commit.getName(), Store.YES, Index.ANALYZED));
+ doc.add(new Field(FIELD_PATH, path.path, Store.YES, Index.ANALYZED));
+ doc.add(new Field(FIELD_DATE, revDate, Store.YES, Index.NO));
+ doc.add(new Field(FIELD_AUTHOR, getAuthor(commit), Store.YES, Index.ANALYZED));
+ doc.add(new Field(FIELD_COMMITTER, getCommitter(commit), Store.YES, Index.ANALYZED));
+
+ // determine extension to compare to the extension
+ // blacklist
+ String ext = null;
+ String name = path.name.toLowerCase();
+ if (name.indexOf('.') > -1) {
+ ext = name.substring(name.lastIndexOf('.') + 1);
+ }
+
+ if (StringUtils.isEmpty(ext) || !excludedExtensions.contains(ext)) {
+ // read the blob content
+ String str = JGitUtils.getStringContent(repository, commit.getTree(),
+ path.path, encodings);
+ if (str != null) {
+ doc.add(new Field(FIELD_CONTENT, str, Store.YES, Index.ANALYZED));
+ writer.addDocument(doc);
+ }
+ }
+ }
+ }
+ writer.commit();
+
+ // get any annotated commit tags
+ List<String> commitTags = new ArrayList<String>();
+ for (RefModel ref : JGitUtils.getTags(repository, false, -1)) {
+ if (ref.isAnnotatedTag() && ref.getReferencedObjectId().equals(commit.getId())) {
+ commitTags.add(ref.displayName);
+ }
+ }
+
+ // create and write the Lucene document
+ Document doc = createDocument(commit, commitTags);
+ doc.add(new Field(FIELD_BRANCH, branch, Store.YES, Index.ANALYZED));
+ result.commitCount++;
+ result.success = index(repositoryName, doc);
+ } catch (Exception e) {
+ logger.error(MessageFormat.format("Exception while indexing commit {0} in {1}", commit.getId().getName(), repositoryName), e);
+ }
+ return result;
+ }
+
+ /**
+ * Incrementally update the index with the specified issue for the
+ * repository.
+ *
+ * @param repositoryName
+ * @param issue
+ * @return true, if successful
+ */
+ public boolean index(String repositoryName, IssueModel issue) {
+ try {
+ // delete the old issue from the index, if exists
+ deleteIssue(repositoryName, issue.id);
+ Document doc = createDocument(issue);
+ return index(repositoryName, doc);
+ } catch (Exception e) {
+ logger.error(MessageFormat.format("Error while indexing issue {0} in {1}", issue.id, repositoryName), e);
+ }
+ return false;
+ }
+
+ /**
+ * Delete an issue from the repository index.
+ *
+ * @param repositoryName
+ * @param issueId
+ * @throws Exception
+ * @return true, if deleted, false if no record was deleted
+ */
+ private boolean deleteIssue(String repositoryName, String issueId) throws Exception {
+ BooleanQuery query = new BooleanQuery();
+ Term objectTerm = new Term(FIELD_OBJECT_TYPE, SearchObjectType.issue.name());
+ query.add(new TermQuery(objectTerm), Occur.MUST);
+ Term issueidTerm = new Term(FIELD_ISSUE, issueId);
+ query.add(new TermQuery(issueidTerm), Occur.MUST);
+
+ IndexWriter writer = getIndexWriter(repositoryName);
+ int numDocsBefore = writer.numDocs();
+ writer.deleteDocuments(query);
+ writer.commit();
+ int numDocsAfter = writer.numDocs();
+ if (numDocsBefore == numDocsAfter) {
+ logger.debug(MessageFormat.format("no records found to delete {0}", query.toString()));
+ return false;
+ } else {
+ logger.debug(MessageFormat.format("deleted {0} records with {1}", numDocsBefore - numDocsAfter, query.toString()));
+ return true;
+ }
+ }
+
+ /**
+ * Delete a blob from the specified branch of the repository index.
+ *
+ * @param repositoryName
+ * @param branch
+ * @param path
+ * @throws Exception
+ * @return true, if deleted, false if no record was deleted
+ */
+ public boolean deleteBlob(String repositoryName, String branch, String path) throws Exception {
+ String pattern = MessageFormat.format("{0}:'{'0} AND {1}:\"'{'1'}'\" AND {2}:\"'{'2'}'\"", FIELD_OBJECT_TYPE, FIELD_BRANCH, FIELD_PATH);
+ String q = MessageFormat.format(pattern, SearchObjectType.blob.name(), branch, path);
+
+ BooleanQuery query = new BooleanQuery();
+ StandardAnalyzer analyzer = new StandardAnalyzer(LUCENE_VERSION);
+ QueryParser qp = new QueryParser(LUCENE_VERSION, FIELD_SUMMARY, analyzer);
+ query.add(qp.parse(q), Occur.MUST);
+
+ IndexWriter writer = getIndexWriter(repositoryName);
+ int numDocsBefore = writer.numDocs();
+ writer.deleteDocuments(query);
+ writer.commit();
+ int numDocsAfter = writer.numDocs();
+ if (numDocsBefore == numDocsAfter) {
+ logger.debug(MessageFormat.format("no records found to delete {0}", query.toString()));
+ return false;
+ } else {
+ logger.debug(MessageFormat.format("deleted {0} records with {1}", numDocsBefore - numDocsAfter, query.toString()));
+ return true;
+ }
+ }
+
+ /**
+ * Updates a repository index incrementally from the last indexed commits.
+ *
+ * @param model
+ * @param repository
+ * @return IndexResult
+ */
+ private IndexResult updateIndex(RepositoryModel model, Repository repository) {
+ IndexResult result = new IndexResult();
+ try {
+ FileBasedConfig config = getConfig(repository);
+ config.load();
+
+ // build a quick lookup of annotated tags
+ Map<String, List<String>> tags = new HashMap<String, List<String>>();
+ for (RefModel tag : JGitUtils.getTags(repository, false, -1)) {
+ if (!tag.isAnnotatedTag()) {
+ // skip non-annotated tags
+ continue;
+ }
+ if (!tags.containsKey(tag.getObjectId())) {
+ tags.put(tag.getReferencedObjectId().getName(), new ArrayList<String>());
+ }
+ tags.get(tag.getReferencedObjectId().getName()).add(tag.displayName);
+ }
+
+ // detect branch deletion
+ // first assume all branches are deleted and then remove each
+ // existing branch from deletedBranches during indexing
+ Set<String> deletedBranches = new TreeSet<String>();
+ for (String alias : config.getNames(CONF_ALIAS)) {
+ String branch = config.getString(CONF_ALIAS, null, alias);
+ deletedBranches.add(branch);
+ }
+
+ // get the local branches
+ List<RefModel> branches = JGitUtils.getLocalBranches(repository, true, -1);
+
+ // sort them by most recently updated
+ Collections.sort(branches, new Comparator<RefModel>() {
+ @Override
+ public int compare(RefModel ref1, RefModel ref2) {
+ return ref2.getDate().compareTo(ref1.getDate());
+ }
+ });
+
+ // reorder default branch to first position
+ RefModel defaultBranch = null;
+ ObjectId defaultBranchId = JGitUtils.getDefaultBranch(repository);
+ for (RefModel branch : branches) {
+ if (branch.getObjectId().equals(defaultBranchId)) {
+ defaultBranch = branch;
+ break;
+ }
+ }
+ branches.remove(defaultBranch);
+ branches.add(0, defaultBranch);
+
+ // walk through each branches
+ for (RefModel branch : branches) {
+ String branchName = branch.getName();
+
+ boolean indexBranch = false;
+ if (model.indexedBranches.contains(com.gitblit.Constants.DEFAULT_BRANCH)
+ && branch.equals(defaultBranch)) {
+ // indexing "default" branch
+ indexBranch = true;
+ } else if (IssueUtils.GB_ISSUES.equals(branch)) {
+ // update issues modified on the GB_ISSUES branch
+ // note: this is different than reindex
+ indexBranch = true;
+ } else {
+ // normal explicit branch check
+ indexBranch = model.indexedBranches.contains(branch.getName());
+ }
+
+ // if this branch is not specifically indexed then skip
+ if (!indexBranch) {
+ continue;
+ }
+
+ // remove this branch from the deletedBranches set
+ deletedBranches.remove(branchName);
+
+ // determine last commit
+ String keyName = getBranchKey(branchName);
+ String lastCommit = config.getString(CONF_BRANCH, null, keyName);
+
+ List<RevCommit> revs;
+ if (StringUtils.isEmpty(lastCommit)) {
+ // new branch/unindexed branch, get all commits on branch
+ revs = JGitUtils.getRevLog(repository, branchName, 0, -1);
+ } else {
+ // pre-existing branch, get changes since last commit
+ revs = JGitUtils.getRevLog(repository, lastCommit, branchName);
+ }
+
+ if (revs.size() > 0) {
+ result.branchCount += 1;
+ }
+
+ // track the issue ids that we have already indexed
+ Set<String> indexedIssues = new TreeSet<String>();
+
+ // reverse the list of commits so we start with the first commit
+ Collections.reverse(revs);
+ for (RevCommit commit : revs) {
+ if (IssueUtils.GB_ISSUES.equals(branch)) {
+ // only index an issue once during updateIndex
+ String issueId = commit.getShortMessage().substring(2).trim();
+ if (indexedIssues.contains(issueId)) {
+ continue;
+ }
+ indexedIssues.add(issueId);
+
+ IssueModel issue = IssueUtils.getIssue(repository, issueId);
+ if (issue == null) {
+ // issue was deleted, remove from index
+ if (!deleteIssue(model.name, issueId)) {
+ logger.error(MessageFormat.format("Failed to delete issue {0} from Lucene index!", issueId));
+ }
+ } else {
+ // issue was updated
+ index(model.name, issue);
+ result.issueCount++;
+ }
+ } else {
+ // index a commit
+ result.add(index(model.name, repository, branchName, commit));
+ }
+ }
+
+ // update the config
+ config.setInt(CONF_INDEX, null, CONF_VERSION, INDEX_VERSION);
+ config.setString(CONF_ALIAS, null, keyName, branchName);
+ config.setString(CONF_BRANCH, null, keyName, branch.getObjectId().getName());
+ config.save();
+ }
+
+ // the deletedBranches set will normally be empty by this point
+ // unless a branch really was deleted and no longer exists
+ if (deletedBranches.size() > 0) {
+ for (String branch : deletedBranches) {
+ IndexWriter writer = getIndexWriter(model.name);
+ writer.deleteDocuments(new Term(FIELD_BRANCH, branch));
+ writer.commit();
+ }
+ }
+ result.success = true;
+ } catch (Throwable t) {
+ logger.error(MessageFormat.format("Exception while updating {0} Lucene index", model.name), t);
+ }
+ return result;
+ }
+
+ /**
+ * Creates a Lucene document from an issue.
+ *
+ * @param issue
+ * @return a Lucene document
+ */
+ private Document createDocument(IssueModel issue) {
+ Document doc = new Document();
+ doc.add(new Field(FIELD_OBJECT_TYPE, SearchObjectType.issue.name(), Store.YES,
+ Field.Index.NOT_ANALYZED));
+ doc.add(new Field(FIELD_ISSUE, issue.id, Store.YES, Index.ANALYZED));
+ doc.add(new Field(FIELD_BRANCH, IssueUtils.GB_ISSUES, Store.YES, Index.ANALYZED));
+ doc.add(new Field(FIELD_DATE, DateTools.dateToString(issue.created, Resolution.MINUTE),
+ Store.YES, Field.Index.NO));
+ doc.add(new Field(FIELD_AUTHOR, issue.reporter, Store.YES, Index.ANALYZED));
+ List<String> attachments = new ArrayList<String>();
+ for (Attachment attachment : issue.getAttachments()) {
+ attachments.add(attachment.name.toLowerCase());
+ }
+ doc.add(new Field(FIELD_ATTACHMENT, StringUtils.flattenStrings(attachments), Store.YES,
+ Index.ANALYZED));
+ doc.add(new Field(FIELD_SUMMARY, issue.summary, Store.YES, Index.ANALYZED));
+ doc.add(new Field(FIELD_CONTENT, issue.toString(), Store.YES, Index.ANALYZED));
+ doc.add(new Field(FIELD_LABEL, StringUtils.flattenStrings(issue.getLabels()), Store.YES,
+ Index.ANALYZED));
+ return doc;
+ }
+
+ /**
+ * Creates a Lucene document for a commit
+ *
+ * @param commit
+ * @param tags
+ * @return a Lucene document
+ */
+ private Document createDocument(RevCommit commit, List<String> tags) {
+ Document doc = new Document();
+ doc.add(new Field(FIELD_OBJECT_TYPE, SearchObjectType.commit.name(), Store.YES,
+ Index.NOT_ANALYZED));
+ doc.add(new Field(FIELD_COMMIT, commit.getName(), Store.YES, Index.ANALYZED));
+ doc.add(new Field(FIELD_DATE, DateTools.timeToString(commit.getCommitTime() * 1000L,
+ Resolution.MINUTE), Store.YES, Index.NO));
+ doc.add(new Field(FIELD_AUTHOR, getAuthor(commit), Store.YES, Index.ANALYZED));
+ doc.add(new Field(FIELD_COMMITTER, getCommitter(commit), Store.YES, Index.ANALYZED));
+ doc.add(new Field(FIELD_SUMMARY, commit.getShortMessage(), Store.YES, Index.ANALYZED));
+ doc.add(new Field(FIELD_CONTENT, commit.getFullMessage(), Store.YES, Index.ANALYZED));
+ if (!ArrayUtils.isEmpty(tags)) {
+ doc.add(new Field(FIELD_TAG, StringUtils.flattenStrings(tags), Store.YES, Index.ANALYZED));
+ }
+ return doc;
+ }
+
+ /**
+ * Incrementally index an object for the repository.
+ *
+ * @param repositoryName
+ * @param doc
+ * @return true, if successful
+ */
+ private boolean index(String repositoryName, Document doc) {
+ try {
+ IndexWriter writer = getIndexWriter(repositoryName);
+ writer.addDocument(doc);
+ writer.commit();
+ resetIndexSearcher(repositoryName);
+ return true;
+ } catch (Exception e) {
+ logger.error(MessageFormat.format("Exception while incrementally updating {0} Lucene index", repositoryName), e);
+ }
+ return false;
+ }
+
+ private SearchResult createSearchResult(Document doc, float score, int hitId, int totalHits) throws ParseException {
+ SearchResult result = new SearchResult();
+ result.hitId = hitId;
+ result.totalHits = totalHits;
+ result.score = score;
+ result.date = DateTools.stringToDate(doc.get(FIELD_DATE));
+ result.summary = doc.get(FIELD_SUMMARY);
+ result.author = doc.get(FIELD_AUTHOR);
+ result.committer = doc.get(FIELD_COMMITTER);
+ result.type = SearchObjectType.fromName(doc.get(FIELD_OBJECT_TYPE));
+ result.branch = doc.get(FIELD_BRANCH);
+ result.commitId = doc.get(FIELD_COMMIT);
+ result.issueId = doc.get(FIELD_ISSUE);
+ result.path = doc.get(FIELD_PATH);
+ if (doc.get(FIELD_TAG) != null) {
+ result.tags = StringUtils.getStringsFromValue(doc.get(FIELD_TAG));
+ }
+ if (doc.get(FIELD_LABEL) != null) {
+ result.labels = StringUtils.getStringsFromValue(doc.get(FIELD_LABEL));
+ }
+ return result;
+ }
+
+ private synchronized void resetIndexSearcher(String repository) throws IOException {
+ IndexSearcher searcher = searchers.remove(repository);
+ if (searcher != null) {
+ searcher.getIndexReader().close();
+ }
+ }
+
+ /**
+ * Gets an index searcher for the repository.
+ *
+ * @param repository
+ * @return
+ * @throws IOException
+ */
+ private IndexSearcher getIndexSearcher(String repository) throws IOException {
+ IndexSearcher searcher = searchers.get(repository);
+ if (searcher == null) {
+ IndexWriter writer = getIndexWriter(repository);
+ searcher = new IndexSearcher(IndexReader.open(writer, true));
+ searchers.put(repository, searcher);
+ }
+ return searcher;
+ }
+
+ /**
+ * Gets an index writer for the repository. The index will be created if it
+ * does not already exist or if forceCreate is specified.
+ *
+ * @param repository
+ * @return an IndexWriter
+ * @throws IOException
+ */
+ private IndexWriter getIndexWriter(String repository) throws IOException {
+ IndexWriter indexWriter = writers.get(repository);
+ File repositoryFolder = FileKey.resolve(new File(repositoriesFolder, repository), FS.DETECTED);
+ File indexFolder = new File(repositoryFolder, LUCENE_DIR);
+ Directory directory = FSDirectory.open(indexFolder);
+
+ if (indexWriter == null) {
+ if (!indexFolder.exists()) {
+ indexFolder.mkdirs();
+ }
+ StandardAnalyzer analyzer = new StandardAnalyzer(LUCENE_VERSION);
+ IndexWriterConfig config = new IndexWriterConfig(LUCENE_VERSION, analyzer);
+ config.setOpenMode(OpenMode.CREATE_OR_APPEND);
+ indexWriter = new IndexWriter(directory, config);
+ writers.put(repository, indexWriter);
+ }
+ return indexWriter;
+ }
+
+ /**
+ * Searches the specified repositories for the given text or query
+ *
+ * @param text
+ * if the text is null or empty, null is returned
+ * @param page
+ * the page number to retrieve. page is 1-indexed.
+ * @param pageSize
+ * the number of elements to return for this page
+ * @param repositories
+ * a list of repositories to search. if no repositories are
+ * specified null is returned.
+ * @return a list of SearchResults in order from highest to the lowest score
+ *
+ */
+ public List<SearchResult> search(String text, int page, int pageSize, List<String> repositories) {
+ if (ArrayUtils.isEmpty(repositories)) {
+ return null;
+ }
+ return search(text, page, pageSize, repositories.toArray(new String[0]));
+ }
+
+ /**
+ * Searches the specified repositories for the given text or query
+ *
+ * @param text
+ * if the text is null or empty, null is returned
+ * @param page
+ * the page number to retrieve. page is 1-indexed.
+ * @param pageSize
+ * the number of elements to return for this page
+ * @param repositories
+ * a list of repositories to search. if no repositories are
+ * specified null is returned.
+ * @return a list of SearchResults in order from highest to the lowest score
+ *
+ */
+ public List<SearchResult> search(String text, int page, int pageSize, String... repositories) {
+ if (StringUtils.isEmpty(text)) {
+ return null;
+ }
+ if (ArrayUtils.isEmpty(repositories)) {
+ return null;
+ }
+ Set<SearchResult> results = new LinkedHashSet<SearchResult>();
+ StandardAnalyzer analyzer = new StandardAnalyzer(LUCENE_VERSION);
+ try {
+ // default search checks summary and content
+ BooleanQuery query = new BooleanQuery();
+ QueryParser qp;
+ qp = new QueryParser(LUCENE_VERSION, FIELD_SUMMARY, analyzer);
+ qp.setAllowLeadingWildcard(true);
+ query.add(qp.parse(text), Occur.SHOULD);
+
+ qp = new QueryParser(LUCENE_VERSION, FIELD_CONTENT, analyzer);
+ qp.setAllowLeadingWildcard(true);
+ query.add(qp.parse(text), Occur.SHOULD);
+
+ IndexSearcher searcher;
+ if (repositories.length == 1) {
+ // single repository search
+ searcher = getIndexSearcher(repositories[0]);
+ } else {
+ // multiple repository search
+ List<IndexReader> readers = new ArrayList<IndexReader>();
+ for (String repository : repositories) {
+ IndexSearcher repositoryIndex = getIndexSearcher(repository);
+ readers.add(repositoryIndex.getIndexReader());
+ }
+ IndexReader[] rdrs = readers.toArray(new IndexReader[readers.size()]);
+ MultiSourceReader reader = new MultiSourceReader(rdrs);
+ searcher = new IndexSearcher(reader);
+ }
+
+ Query rewrittenQuery = searcher.rewrite(query);
+ logger.debug(rewrittenQuery.toString());
+
+ TopScoreDocCollector collector = TopScoreDocCollector.create(5000, true);
+ searcher.search(rewrittenQuery, collector);
+ int offset = Math.max(0, (page - 1) * pageSize);
+ ScoreDoc[] hits = collector.topDocs(offset, pageSize).scoreDocs;
+ int totalHits = collector.getTotalHits();
+ for (int i = 0; i < hits.length; i++) {
+ int docId = hits[i].doc;
+ Document doc = searcher.doc(docId);
+ SearchResult result = createSearchResult(doc, hits[i].score, offset + i + 1, totalHits);
+ if (repositories.length == 1) {
+ // single repository search
+ result.repository = repositories[0];
+ } else {
+ // multi-repository search
+ MultiSourceReader reader = (MultiSourceReader) searcher.getIndexReader();
+ int index = reader.getSourceIndex(docId);
+ result.repository = repositories[index];
+ }
+ String content = doc.get(FIELD_CONTENT);
+ result.fragment = getHighlightedFragment(analyzer, query, content, result);
+ results.add(result);
+ }
+ } catch (Exception e) {
+ logger.error(MessageFormat.format("Exception while searching for {0}", text), e);
+ }
+ return new ArrayList<SearchResult>(results);
+ }
+
+ /**
+ *
+ * @param analyzer
+ * @param query
+ * @param content
+ * @param result
+ * @return
+ * @throws IOException
+ * @throws InvalidTokenOffsetsException
+ */
+ private String getHighlightedFragment(Analyzer analyzer, Query query,
+ String content, SearchResult result) throws IOException, InvalidTokenOffsetsException {
+ if (content == null) {
+ content = "";
+ }
+
+ int fragmentLength = SearchObjectType.commit == result.type ? 512 : 150;
+
+ QueryScorer scorer = new QueryScorer(query, "content");
+ Fragmenter fragmenter = new SimpleSpanFragmenter(scorer, fragmentLength);
+
+ // use an artificial delimiter for the token
+ String termTag = "!!--[";
+ String termTagEnd = "]--!!";
+ SimpleHTMLFormatter formatter = new SimpleHTMLFormatter(termTag, termTagEnd);
+ Highlighter highlighter = new Highlighter(formatter, scorer);
+ highlighter.setTextFragmenter(fragmenter);
+
+ String [] fragments = highlighter.getBestFragments(analyzer, "content", content, 3);
+ if (ArrayUtils.isEmpty(fragments)) {
+ if (SearchObjectType.blob == result.type) {
+ return "";
+ }
+ // clip commit message
+ String fragment = content;
+ if (fragment.length() > fragmentLength) {
+ fragment = fragment.substring(0, fragmentLength) + "...";
+ }
+ return "<pre class=\"text\">" + StringUtils.escapeForHtml(fragment, true) + "</pre>";
+ }
+
+ // make sure we have unique fragments
+ Set<String> uniqueFragments = new LinkedHashSet<String>();
+ for (String fragment : fragments) {
+ uniqueFragments.add(fragment);
+ }
+ fragments = uniqueFragments.toArray(new String[uniqueFragments.size()]);
+
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0, len = fragments.length; i < len; i++) {
+ String fragment = fragments[i];
+ String tag = "<pre class=\"text\">";
+
+ // resurrect the raw fragment from removing the artificial delimiters
+ String raw = fragment.replace(termTag, "").replace(termTagEnd, "");
+
+ // determine position of the raw fragment in the content
+ int pos = content.indexOf(raw);
+
+ // restore complete first line of fragment
+ int c = pos;
+ while (c > 0) {
+ c--;
+ if (content.charAt(c) == '\n') {
+ break;
+ }
+ }
+ if (c > 0) {
+ // inject leading chunk of first fragment line
+ fragment = content.substring(c + 1, pos) + fragment;
+ }
+
+ if (SearchObjectType.blob == result.type) {
+ // count lines as offset into the content for this fragment
+ int line = Math.max(1, StringUtils.countLines(content.substring(0, pos)));
+
+ // create fragment tag with line number and language
+ String lang = "";
+ String ext = StringUtils.getFileExtension(result.path).toLowerCase();
+ if (!StringUtils.isEmpty(ext)) {
+ // maintain leading space!
+ lang = " lang-" + ext;
+ }
+ tag = MessageFormat.format("<pre class=\"prettyprint linenums:{0,number,0}{1}\">", line, lang);
+
+ }
+
+ sb.append(tag);
+
+ // replace the artificial delimiter with html tags
+ String html = StringUtils.escapeForHtml(fragment, false);
+ html = html.replace(termTag, "<span class=\"highlight\">").replace(termTagEnd, "</span>");
+ sb.append(html);
+ sb.append("</pre>");
+ if (i < len - 1) {
+ sb.append("<span class=\"ellipses\">...</span><br/>");
+ }
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Simple class to track the results of an index update.
+ */
+ private class IndexResult {
+ long startTime = System.currentTimeMillis();
+ long endTime = startTime;
+ boolean success;
+ int branchCount;
+ int commitCount;
+ int blobCount;
+ int issueCount;
+
+ void add(IndexResult result) {
+ this.branchCount += result.branchCount;
+ this.commitCount += result.commitCount;
+ this.blobCount += result.blobCount;
+ this.issueCount += result.issueCount;
+ }
+
+ void success() {
+ success = true;
+ endTime = System.currentTimeMillis();
+ }
+
+ float duration() {
+ return (endTime - startTime)/1000f;
+ }
+ }
+
+ /**
+ * Custom subclass of MultiReader to identify the source index for a given
+ * doc id. This would not be necessary of there was a public method to
+ * obtain this information.
+ *
+ */
+ private class MultiSourceReader extends MultiReader {
+
+ final Method method;
+
+ MultiSourceReader(IndexReader[] subReaders) {
+ super(subReaders);
+ Method m = null;
+ try {
+ m = MultiReader.class.getDeclaredMethod("readerIndex", int.class);
+ m.setAccessible(true);
+ } catch (Exception e) {
+ logger.error("Error getting readerIndex method", e);
+ }
+ method = m;
+ }
+
+ int getSourceIndex(int docId) {
+ int index = -1;
+ try {
+ Object o = method.invoke(this, docId);
+ index = (Integer) o;
+ } catch (Exception e) {
+ logger.error("Error getting source index", e);
+ }
+ return index;
+ }
+ }
+}
diff --git a/src/main/java/com/gitblit/MailExecutor.java b/src/main/java/com/gitblit/MailExecutor.java
new file mode 100644
index 00000000..9001e836
--- /dev/null
+++ b/src/main/java/com/gitblit/MailExecutor.java
@@ -0,0 +1,238 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Properties;
+import java.util.Queue;
+import java.util.Set;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.regex.Pattern;
+
+import javax.mail.Authenticator;
+import javax.mail.Message;
+import javax.mail.PasswordAuthentication;
+import javax.mail.Session;
+import javax.mail.Transport;
+import javax.mail.internet.InternetAddress;
+import javax.mail.internet.MimeMessage;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.utils.StringUtils;
+
+/**
+ * The mail executor handles sending email messages asynchronously from queue.
+ *
+ * @author James Moger
+ *
+ */
+public class MailExecutor implements Runnable {
+
+ private final Logger logger = LoggerFactory.getLogger(MailExecutor.class);
+
+ private final Queue<Message> queue = new ConcurrentLinkedQueue<Message>();
+
+ private final Session session;
+
+ private final IStoredSettings settings;
+
+ public MailExecutor(IStoredSettings settings) {
+ this.settings = settings;
+
+ final String mailUser = settings.getString(Keys.mail.username, null);
+ final String mailPassword = settings.getString(Keys.mail.password, null);
+ boolean authenticate = !StringUtils.isEmpty(mailUser) && !StringUtils.isEmpty(mailPassword);
+ String server = settings.getString(Keys.mail.server, "");
+ if (StringUtils.isEmpty(server)) {
+ session = null;
+ return;
+ }
+ int port = settings.getInteger(Keys.mail.port, 25);
+ boolean isGMail = false;
+ if (server.equals("smtp.gmail.com")) {
+ port = 465;
+ isGMail = true;
+ }
+
+ Properties props = new Properties();
+ props.setProperty("mail.smtp.host", server);
+ props.setProperty("mail.smtp.port", String.valueOf(port));
+ props.setProperty("mail.smtp.auth", String.valueOf(authenticate));
+ props.setProperty("mail.smtp.auths", String.valueOf(authenticate));
+
+ if (isGMail) {
+ props.setProperty("mail.smtp.starttls.enable", "true");
+ props.put("mail.smtp.socketFactory.port", String.valueOf(port));
+ props.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
+ props.put("mail.smtp.socketFactory.fallback", "false");
+ }
+
+ if (!StringUtils.isEmpty(mailUser) && !StringUtils.isEmpty(mailPassword)) {
+ // SMTP requires authentication
+ session = Session.getInstance(props, new Authenticator() {
+ protected PasswordAuthentication getPasswordAuthentication() {
+ PasswordAuthentication passwordAuthentication = new PasswordAuthentication(
+ mailUser, mailPassword);
+ return passwordAuthentication;
+ }
+ });
+ } else {
+ // SMTP does not require authentication
+ session = Session.getInstance(props);
+ }
+ }
+
+ /**
+ * Indicates if the mail executor can send emails.
+ *
+ * @return true if the mail executor is ready to send emails
+ */
+ public boolean isReady() {
+ return session != null;
+ }
+
+ /**
+ * Creates a message for the administrators.
+ *
+ * @returna message
+ */
+ public Message createMessageForAdministrators() {
+ List<String> toAddresses = settings.getStrings(Keys.mail.adminAddresses);
+ if (toAddresses.size() == 0) {
+ logger.warn("Can not notify administrators because no email addresses are defined!");
+ return null;
+ }
+ return createMessage(toAddresses);
+ }
+
+ /**
+ * Create a message.
+ *
+ * @param toAddresses
+ * @return a message
+ */
+ public Message createMessage(String... toAddresses) {
+ return createMessage(Arrays.asList(toAddresses));
+ }
+
+ /**
+ * Create a message.
+ *
+ * @param toAddresses
+ * @return a message
+ */
+ public Message createMessage(List<String> toAddresses) {
+ MimeMessage message = new MimeMessage(session);
+ try {
+ String fromAddress = settings.getString(Keys.mail.fromAddress, null);
+ if (StringUtils.isEmpty(fromAddress)) {
+ fromAddress = "gitblit@gitblit.com";
+ }
+ InternetAddress from = new InternetAddress(fromAddress, "Gitblit");
+ message.setFrom(from);
+
+ // determine unique set of addresses
+ Set<String> uniques = new HashSet<String>();
+ for (String address : toAddresses) {
+ uniques.add(address.toLowerCase());
+ }
+
+ Pattern validEmail = Pattern
+ .compile("^([a-zA-Z0-9_\\-\\.]+)@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.)|(([a-zA-Z0-9\\-]+\\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\\]?)$");
+ List<InternetAddress> tos = new ArrayList<InternetAddress>();
+ for (String address : uniques) {
+ if (StringUtils.isEmpty(address)) {
+ continue;
+ }
+ if (validEmail.matcher(address).find()) {
+ try {
+ tos.add(new InternetAddress(address));
+ } catch (Throwable t) {
+ }
+ }
+ }
+ message.setRecipients(Message.RecipientType.BCC,
+ tos.toArray(new InternetAddress[tos.size()]));
+ message.setSentDate(new Date());
+ } catch (Exception e) {
+ logger.error("Failed to properly create message", e);
+ }
+ return message;
+ }
+
+ /**
+ * Returns the status of the mail queue.
+ *
+ * @return true, if the queue is empty
+ */
+ public boolean hasEmptyQueue() {
+ return queue.isEmpty();
+ }
+
+ /**
+ * Queue's an email message to be sent.
+ *
+ * @param message
+ * @return true if the message was queued
+ */
+ public boolean queue(Message message) {
+ if (!isReady()) {
+ return false;
+ }
+ try {
+ message.saveChanges();
+ } catch (Throwable t) {
+ logger.error("Failed to save changes to message!", t);
+ }
+ queue.add(message);
+ return true;
+ }
+
+ @Override
+ public void run() {
+ if (!queue.isEmpty()) {
+ if (session != null) {
+ // send message via mail server
+ List<Message> failures = new ArrayList<Message>();
+ Message message = null;
+ while ((message = queue.poll()) != null) {
+ try {
+ if (settings.getBoolean(Keys.mail.debug, false)) {
+ logger.info("send: " + StringUtils.trimString(message.getSubject(), 60));
+ }
+ Transport.send(message);
+ } catch (Throwable e) {
+ logger.error("Failed to send message", e);
+ failures.add(message);
+ }
+ }
+
+ // push the failures back onto the queue for the next cycle
+ queue.addAll(failures);
+ }
+ }
+ }
+
+ public void sendNow(Message message) throws Exception {
+ Transport.send(message);
+ }
+}
diff --git a/src/main/java/com/gitblit/PagesFilter.java b/src/main/java/com/gitblit/PagesFilter.java
new file mode 100644
index 00000000..f88624e1
--- /dev/null
+++ b/src/main/java/com/gitblit/PagesFilter.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2012 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit;
+
+import org.eclipse.jgit.lib.Repository;
+
+import com.gitblit.Constants.AccessRestrictionType;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.UserModel;
+
+/**
+ * The PagesFilter is an AccessRestrictionFilter which ensures the gh-pages
+ * requests for a view-restricted repository are authenticated and authorized.
+ *
+ * @author James Moger
+ *
+ */
+public class PagesFilter extends AccessRestrictionFilter {
+
+ /**
+ * Extract the repository name from the url.
+ *
+ * @param url
+ * @return repository name
+ */
+ @Override
+ protected String extractRepositoryName(String url) {
+ // get the repository name from the url by finding a known url suffix
+ String repository = "";
+ Repository r = null;
+ int offset = 0;
+ while (r == null) {
+ int slash = url.indexOf('/', offset);
+ if (slash == -1) {
+ repository = url;
+ } else {
+ repository = url.substring(0, slash);
+ }
+ r = GitBlit.self().getRepository(repository, false);
+ if (r == null) {
+ // try again
+ offset = slash + 1;
+ } else {
+ // close the repo
+ r.close();
+ }
+ if (repository.equals(url)) {
+ // either only repository in url or no repository found
+ break;
+ }
+ }
+ return repository;
+ }
+
+ /**
+ * Analyze the url and returns the action of the request.
+ *
+ * @param url
+ * @return action of the request
+ */
+ @Override
+ protected String getUrlRequestAction(String suffix) {
+ return "VIEW";
+ }
+
+ /**
+ * Determine if a non-existing repository can be created using this filter.
+ *
+ * @return true if the filter allows repository creation
+ */
+ @Override
+ protected boolean isCreationAllowed() {
+ return false;
+ }
+
+ /**
+ * Determine if the action may be executed on the repository.
+ *
+ * @param repository
+ * @param action
+ * @return true if the action may be performed
+ */
+ @Override
+ protected boolean isActionAllowed(RepositoryModel repository, String action) {
+ return true;
+ }
+
+ /**
+ * Determine if the repository requires authentication.
+ *
+ * @param repository
+ * @param action
+ * @return true if authentication required
+ */
+ @Override
+ protected boolean requiresAuthentication(RepositoryModel repository, String action) {
+ return repository.accessRestriction.atLeast(AccessRestrictionType.VIEW);
+ }
+
+ /**
+ * Determine if the user can access the repository and perform the specified
+ * action.
+ *
+ * @param repository
+ * @param user
+ * @param action
+ * @return true if user may execute the action on the repository
+ */
+ @Override
+ protected boolean canAccess(RepositoryModel repository, UserModel user, String action) {
+ return user.canView(repository);
+ }
+}
diff --git a/src/main/java/com/gitblit/PagesServlet.java b/src/main/java/com/gitblit/PagesServlet.java
new file mode 100644
index 00000000..91f25b70
--- /dev/null
+++ b/src/main/java/com/gitblit/PagesServlet.java
@@ -0,0 +1,249 @@
+/*
+ * Copyright 2012 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit;
+
+import java.io.IOException;
+import java.text.MessageFormat;
+import java.text.ParseException;
+
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevTree;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.models.RefModel;
+import com.gitblit.utils.ArrayUtils;
+import com.gitblit.utils.JGitUtils;
+import com.gitblit.utils.MarkdownUtils;
+import com.gitblit.utils.StringUtils;
+
+/**
+ * Serves the content of a gh-pages branch.
+ *
+ * @author James Moger
+ *
+ */
+public class PagesServlet extends HttpServlet {
+
+ private static final long serialVersionUID = 1L;
+
+ private transient Logger logger = LoggerFactory.getLogger(PagesServlet.class);
+
+ public PagesServlet() {
+ super();
+ }
+
+ /**
+ * Returns an url to this servlet for the specified parameters.
+ *
+ * @param baseURL
+ * @param repository
+ * @param path
+ * @return an url
+ */
+ public static String asLink(String baseURL, String repository, String path) {
+ if (baseURL.length() > 0 && baseURL.charAt(baseURL.length() - 1) == '/') {
+ baseURL = baseURL.substring(0, baseURL.length() - 1);
+ }
+ return baseURL + Constants.PAGES + repository + "/" + (path == null ? "" : ("/" + path));
+ }
+
+ /**
+ * Retrieves the specified resource from the gh-pages branch of the
+ * repository.
+ *
+ * @param request
+ * @param response
+ * @throws javax.servlet.ServletException
+ * @throws java.io.IOException
+ */
+ private void processRequest(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ String path = request.getPathInfo();
+ if (path.toLowerCase().endsWith(".git")) {
+ // forward to url with trailing /
+ // this is important for relative pages links
+ response.sendRedirect(request.getServletPath() + path + "/");
+ return;
+ }
+ if (path.charAt(0) == '/') {
+ // strip leading /
+ path = path.substring(1);
+ }
+
+ // determine repository and resource from url
+ String repository = "";
+ String resource = "";
+ Repository r = null;
+ int offset = 0;
+ while (r == null) {
+ int slash = path.indexOf('/', offset);
+ if (slash == -1) {
+ repository = path;
+ } else {
+ repository = path.substring(0, slash);
+ }
+ r = GitBlit.self().getRepository(repository, false);
+ offset = slash + 1;
+ if (offset > 0) {
+ resource = path.substring(offset);
+ }
+ if (repository.equals(path)) {
+ // either only repository in url or no repository found
+ break;
+ }
+ }
+
+ ServletContext context = request.getSession().getServletContext();
+
+ try {
+ if (r == null) {
+ // repository not found!
+ String mkd = MessageFormat.format(
+ "# Error\nSorry, no valid **repository** specified in this url: {0}!",
+ repository);
+ error(response, mkd);
+ return;
+ }
+
+ // retrieve the content from the repository
+ RefModel pages = JGitUtils.getPagesBranch(r);
+ RevCommit commit = JGitUtils.getCommit(r, pages.getObjectId().getName());
+
+ if (commit == null) {
+ // branch not found!
+ String mkd = MessageFormat.format(
+ "# Error\nSorry, the repository {0} does not have a **gh-pages** branch!",
+ repository);
+ error(response, mkd);
+ r.close();
+ return;
+ }
+ response.setDateHeader("Last-Modified", JGitUtils.getCommitDate(commit).getTime());
+
+ String [] encodings = GitBlit.getEncodings();
+
+ RevTree tree = commit.getTree();
+ byte[] content = null;
+ if (StringUtils.isEmpty(resource)) {
+ // find resource
+ String[] files = { "index.html", "index.htm", "index.mkd" };
+ for (String file : files) {
+ content = JGitUtils.getStringContent(r, tree, file, encodings)
+ .getBytes(Constants.ENCODING);
+ if (content != null) {
+ resource = file;
+ // assume text/html unless the servlet container
+ // overrides
+ response.setContentType("text/html; charset=" + Constants.ENCODING);
+ break;
+ }
+ }
+ } else {
+ // specific resource
+ try {
+ String contentType = context.getMimeType(resource);
+ if (contentType == null) {
+ contentType = "text/plain";
+ }
+ if (contentType.startsWith("text")) {
+ content = JGitUtils.getStringContent(r, tree, resource, encodings).getBytes(
+ Constants.ENCODING);
+ } else {
+ content = JGitUtils.getByteContent(r, tree, resource, false);
+ }
+ response.setContentType(contentType);
+ } catch (Exception e) {
+ }
+ }
+
+ // no content, try custom 404 page
+ if (ArrayUtils.isEmpty(content)) {
+ String custom404 = JGitUtils.getStringContent(r, tree, "404.html", encodings);
+ if (!StringUtils.isEmpty(custom404)) {
+ content = custom404.getBytes(Constants.ENCODING);
+ }
+
+ // still no content
+ if (ArrayUtils.isEmpty(content)) {
+ String str = MessageFormat.format(
+ "# Error\nSorry, the requested resource **{0}** was not found.",
+ resource);
+ content = MarkdownUtils.transformMarkdown(str).getBytes(Constants.ENCODING);
+ }
+
+ try {
+ // output the content
+ logger.warn("Pages 404: " + resource);
+ response.setStatus(HttpServletResponse.SC_NOT_FOUND);
+ response.getOutputStream().write(content);
+ response.flushBuffer();
+ } catch (Throwable t) {
+ logger.error("Failed to write page to client", t);
+ }
+ return;
+ }
+
+ // check to see if we should transform markdown files
+ for (String ext : GitBlit.getStrings(Keys.web.markdownExtensions)) {
+ if (resource.endsWith(ext)) {
+ String mkd = new String(content, Constants.ENCODING);
+ content = MarkdownUtils.transformMarkdown(mkd).getBytes(Constants.ENCODING);
+ break;
+ }
+ }
+
+ try {
+ // output the content
+ response.getOutputStream().write(content);
+ response.flushBuffer();
+ } catch (Throwable t) {
+ logger.error("Failed to write page to client", t);
+ }
+
+ // close the repository
+ r.close();
+ } catch (Throwable t) {
+ logger.error("Failed to write page to client", t);
+ }
+ }
+
+ private void error(HttpServletResponse response, String mkd) throws ServletException,
+ IOException, ParseException {
+ String content = MarkdownUtils.transformMarkdown(mkd);
+ response.setContentType("text/html; charset=" + Constants.ENCODING);
+ response.getWriter().write(content);
+ }
+
+ @Override
+ protected void doPost(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ processRequest(request, response);
+ }
+
+ @Override
+ protected void doGet(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ processRequest(request, response);
+ }
+}
diff --git a/src/main/java/com/gitblit/RedmineUserService.java b/src/main/java/com/gitblit/RedmineUserService.java
new file mode 100644
index 00000000..9d571e37
--- /dev/null
+++ b/src/main/java/com/gitblit/RedmineUserService.java
@@ -0,0 +1,187 @@
+package com.gitblit;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+
+import org.apache.wicket.util.io.IOUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.Constants.AccountType;
+import com.gitblit.models.UserModel;
+import com.gitblit.utils.ArrayUtils;
+import com.gitblit.utils.ConnectionUtils;
+import com.gitblit.utils.StringUtils;
+import com.google.gson.Gson;
+
+/**
+ * Implementation of an Redmine user service.<br>
+ * you can login to gitblit with Redmine user id and api key.
+ */
+public class RedmineUserService extends GitblitUserService {
+
+ private final Logger logger = LoggerFactory.getLogger(RedmineUserService.class);
+
+ private IStoredSettings settings;
+
+ private String testingJson;
+
+ private class RedmineCurrent {
+ private class RedmineUser {
+ public String login;
+ public String firstname;
+ public String lastname;
+ public String mail;
+ }
+
+ public RedmineUser user;
+ }
+
+ public RedmineUserService() {
+ super();
+ }
+
+ @Override
+ public void setup(IStoredSettings settings) {
+ this.settings = settings;
+
+ String file = settings.getString(Keys.realm.redmine.backingUserService, "${baseFolder}/users.conf");
+ File realmFile = GitBlit.getFileOrFolder(file);
+
+ serviceImpl = createUserService(realmFile);
+ logger.info("Redmine User Service backed by " + serviceImpl.toString());
+ }
+
+ @Override
+ public boolean supportsCredentialChanges() {
+ return false;
+ }
+
+ @Override
+ public boolean supportsDisplayNameChanges() {
+ return false;
+ }
+
+ @Override
+ public boolean supportsEmailAddressChanges() {
+ return false;
+ }
+
+ @Override
+ public boolean supportsTeamMembershipChanges() {
+ return false;
+ }
+
+ @Override
+ protected AccountType getAccountType() {
+ return AccountType.REDMINE;
+ }
+
+ @Override
+ public UserModel authenticate(String username, char[] password) {
+ if (isLocalAccount(username)) {
+ // local account, bypass Redmine authentication
+ return super.authenticate(username, password);
+ }
+
+ String jsonString = null;
+ try {
+ // first attempt by username/password
+ jsonString = getCurrentUserAsJson(username, password);
+ } catch (Exception e1) {
+ logger.warn("Failed to authenticate via username/password against Redmine");
+ try {
+ // second attempt is by apikey
+ jsonString = getCurrentUserAsJson(null, password);
+ username = null;
+ } catch (Exception e2) {
+ logger.error("Failed to authenticate via apikey against Redmine", e2);
+ return null;
+ }
+ }
+
+ if (StringUtils.isEmpty(jsonString)) {
+ logger.error("Received empty authentication response from Redmine");
+ return null;
+ }
+
+ RedmineCurrent current = null;
+ try {
+ current = new Gson().fromJson(jsonString, RedmineCurrent.class);
+ } catch (Exception e) {
+ logger.error("Failed to deserialize Redmine json response: " + jsonString, e);
+ return null;
+ }
+
+ if (StringUtils.isEmpty(username)) {
+ // if the username has been reset because of apikey authentication
+ // then use the email address of the user. this is the original
+ // behavior as contributed by github/mallowlabs
+ username = current.user.mail;
+ }
+
+ UserModel user = getUserModel(username);
+ if (user == null) // create user object for new authenticated user
+ user = new UserModel(username.toLowerCase());
+
+ // create a user cookie
+ if (StringUtils.isEmpty(user.cookie) && !ArrayUtils.isEmpty(password)) {
+ user.cookie = StringUtils.getSHA1(user.username + new String(password));
+ }
+
+ // update user attributes from Redmine
+ user.accountType = getAccountType();
+ user.displayName = current.user.firstname + " " + current.user.lastname;
+ user.emailAddress = current.user.mail;
+ user.password = ExternalAccount;
+ if (!StringUtils.isEmpty(current.user.login)) {
+ // only admin users can get login name
+ // evidently this is an undocumented behavior of Redmine
+ user.canAdmin = true;
+ }
+
+ // TODO consider Redmine group mapping for team membership
+ // http://www.redmine.org/projects/redmine/wiki/Rest_Users
+
+ // push the changes to the backing user service
+ super.updateUserModel(user);
+
+ return user;
+ }
+
+ private String getCurrentUserAsJson(String username, char [] password) throws IOException {
+ if (testingJson != null) { // for testing
+ return testingJson;
+ }
+
+ String url = this.settings.getString(Keys.realm.redmine.url, "");
+ if (!url.endsWith("/")) {
+ url.concat("/");
+ }
+ HttpURLConnection http;
+ if (username == null) {
+ // apikey authentication
+ String apiKey = String.valueOf(password);
+ String apiUrl = url + "users/current.json?key=" + apiKey;
+ http = (HttpURLConnection) ConnectionUtils.openConnection(apiUrl, null, null);
+ } else {
+ // username/password BASIC authentication
+ String apiUrl = url + "users/current.json";
+ http = (HttpURLConnection) ConnectionUtils.openConnection(apiUrl, username, password);
+ }
+ http.setRequestMethod("GET");
+ http.connect();
+ InputStreamReader reader = new InputStreamReader(http.getInputStream());
+ return IOUtils.toString(reader);
+ }
+
+ /**
+ * set json response. do NOT invoke from production code.
+ * @param json json
+ */
+ public void setTestingCurrentUserAsJson(String json) {
+ this.testingJson = json;
+ }
+}
diff --git a/src/main/java/com/gitblit/RobotsTxtServlet.java b/src/main/java/com/gitblit/RobotsTxtServlet.java
new file mode 100644
index 00000000..d66ebf43
--- /dev/null
+++ b/src/main/java/com/gitblit/RobotsTxtServlet.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2012 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit;
+
+import java.io.File;
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import com.gitblit.utils.FileUtils;
+
+/**
+ * Handles requests for robots.txt
+ *
+ * @author James Moger
+ *
+ */
+public class RobotsTxtServlet extends HttpServlet {
+
+ private static final long serialVersionUID = 1L;
+
+ public RobotsTxtServlet() {
+ super();
+ }
+
+ @Override
+ protected void doPost(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, java.io.IOException {
+ processRequest(request, response);
+ }
+
+ @Override
+ protected void doGet(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ processRequest(request, response);
+ }
+
+ protected void processRequest(javax.servlet.http.HttpServletRequest request,
+ javax.servlet.http.HttpServletResponse response) throws javax.servlet.ServletException,
+ java.io.IOException {
+ File file = GitBlit.getFileOrFolder(Keys.web.robots.txt, null);
+ String content = "";
+ if (file.exists()) {
+ content = FileUtils.readContent(file, "\n");
+ }
+ response.getWriter().append(content);
+ }
+}
diff --git a/src/main/java/com/gitblit/RpcFilter.java b/src/main/java/com/gitblit/RpcFilter.java
new file mode 100644
index 00000000..1de9fcc4
--- /dev/null
+++ b/src/main/java/com/gitblit/RpcFilter.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit;
+
+import java.io.IOException;
+import java.text.MessageFormat;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import com.gitblit.Constants.RpcRequest;
+import com.gitblit.models.UserModel;
+
+/**
+ * The RpcFilter is a servlet filter that secures the RpcServlet.
+ *
+ * The filter extracts the rpc request type from the url and determines if the
+ * requested action requires a Basic authentication prompt. If authentication is
+ * required and no credentials are stored in the "Authorization" header, then a
+ * basic authentication challenge is issued.
+ *
+ * http://en.wikipedia.org/wiki/Basic_access_authentication
+ *
+ * @author James Moger
+ *
+ */
+public class RpcFilter extends AuthenticationFilter {
+
+ /**
+ * doFilter does the actual work of preprocessing the request to ensure that
+ * the user may proceed.
+ *
+ * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest,
+ * javax.servlet.ServletResponse, javax.servlet.FilterChain)
+ */
+ @Override
+ public void doFilter(final ServletRequest request, final ServletResponse response,
+ final FilterChain chain) throws IOException, ServletException {
+
+ HttpServletRequest httpRequest = (HttpServletRequest) request;
+ HttpServletResponse httpResponse = (HttpServletResponse) response;
+
+ String fullUrl = getFullUrl(httpRequest);
+ RpcRequest requestType = RpcRequest.fromName(httpRequest.getParameter("req"));
+ if (requestType == null) {
+ httpResponse.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED);
+ return;
+ }
+
+ boolean adminRequest = requestType.exceeds(RpcRequest.LIST_SETTINGS);
+
+ // conditionally reject all rpc requests
+ if (!GitBlit.getBoolean(Keys.web.enableRpcServlet, true)) {
+ logger.warn(Keys.web.enableRpcServlet + " must be set TRUE for rpc requests.");
+ httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN);
+ return;
+ }
+
+ boolean authenticateView = GitBlit.getBoolean(Keys.web.authenticateViewPages, false);
+ boolean authenticateAdmin = GitBlit.getBoolean(Keys.web.authenticateAdminPages, true);
+
+ // Wrap the HttpServletRequest with the RpcServletRequest which
+ // overrides the servlet container user principal methods.
+ AuthenticatedRequest authenticatedRequest = new AuthenticatedRequest(httpRequest);
+ UserModel user = getUser(httpRequest);
+ if (user != null) {
+ authenticatedRequest.setUser(user);
+ }
+
+ // conditionally reject rpc management/administration requests
+ if (adminRequest && !GitBlit.getBoolean(Keys.web.enableRpcManagement, false)) {
+ logger.warn(MessageFormat.format("{0} must be set TRUE for {1} rpc requests.",
+ Keys.web.enableRpcManagement, requestType.toString()));
+ httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN);
+ return;
+ }
+
+ // BASIC authentication challenge and response processing
+ if ((adminRequest && authenticateAdmin) || (!adminRequest && authenticateView)) {
+ if (user == null) {
+ // challenge client to provide credentials. send 401.
+ if (GitBlit.isDebugMode()) {
+ logger.info(MessageFormat.format("RPC: CHALLENGE {0}", fullUrl));
+
+ }
+ httpResponse.setHeader("WWW-Authenticate", CHALLENGE);
+ httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED);
+ return;
+ } else {
+ // check user access for request
+ if (user.canAdmin() || canAccess(user, requestType)) {
+ // authenticated request permitted.
+ // pass processing to the restricted servlet.
+ newSession(authenticatedRequest, httpResponse);
+ logger.info(MessageFormat.format("RPC: {0} ({1}) authenticated", fullUrl,
+ HttpServletResponse.SC_CONTINUE));
+ chain.doFilter(authenticatedRequest, httpResponse);
+ return;
+ }
+ // valid user, but not for requested access. send 403.
+ if (GitBlit.isDebugMode()) {
+ logger.info(MessageFormat.format("RPC: {0} forbidden to access {1}",
+ user.username, fullUrl));
+ }
+ httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN);
+ return;
+ }
+ }
+
+ if (GitBlit.isDebugMode()) {
+ logger.info(MessageFormat.format("RPC: {0} ({1}) unauthenticated", fullUrl,
+ HttpServletResponse.SC_CONTINUE));
+ }
+ // unauthenticated request permitted.
+ // pass processing to the restricted servlet.
+ chain.doFilter(authenticatedRequest, httpResponse);
+ }
+
+ private boolean canAccess(UserModel user, RpcRequest requestType) {
+ switch (requestType) {
+ case GET_PROTOCOL:
+ return true;
+ case LIST_REPOSITORIES:
+ return true;
+ default:
+ return user.canAdmin();
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main/java/com/gitblit/RpcServlet.java b/src/main/java/com/gitblit/RpcServlet.java
new file mode 100644
index 00000000..f6368dd0
--- /dev/null
+++ b/src/main/java/com/gitblit/RpcServlet.java
@@ -0,0 +1,348 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit;
+
+import java.io.IOException;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jgit.lib.Repository;
+
+import com.gitblit.Constants.RpcRequest;
+import com.gitblit.models.RegistrantAccessPermission;
+import com.gitblit.models.RefModel;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.ServerSettings;
+import com.gitblit.models.TeamModel;
+import com.gitblit.models.UserModel;
+import com.gitblit.utils.HttpUtils;
+import com.gitblit.utils.JGitUtils;
+import com.gitblit.utils.RpcUtils;
+
+/**
+ * Handles remote procedure calls.
+ *
+ * @author James Moger
+ *
+ */
+public class RpcServlet extends JsonServlet {
+
+ private static final long serialVersionUID = 1L;
+
+ public static final int PROTOCOL_VERSION = 5;
+
+ public RpcServlet() {
+ super();
+ }
+
+ /**
+ * Processes an rpc request.
+ *
+ * @param request
+ * @param response
+ * @throws javax.servlet.ServletException
+ * @throws java.io.IOException
+ */
+ @Override
+ protected void processRequest(HttpServletRequest request, HttpServletResponse response)
+ throws ServletException, IOException {
+ RpcRequest reqType = RpcRequest.fromName(request.getParameter("req"));
+ String objectName = request.getParameter("name");
+ logger.info(MessageFormat.format("Rpc {0} request from {1}", reqType,
+ request.getRemoteAddr()));
+
+ UserModel user = (UserModel) request.getUserPrincipal();
+
+ boolean allowManagement = user != null && user.canAdmin()
+ && GitBlit.getBoolean(Keys.web.enableRpcManagement, false);
+
+ boolean allowAdmin = user != null && user.canAdmin()
+ && GitBlit.getBoolean(Keys.web.enableRpcAdministration, false);
+
+ Object result = null;
+ if (RpcRequest.GET_PROTOCOL.equals(reqType)) {
+ // Return the protocol version
+ result = PROTOCOL_VERSION;
+ } else if (RpcRequest.LIST_REPOSITORIES.equals(reqType)) {
+ // Determine the Gitblit clone url
+ String gitblitUrl = HttpUtils.getGitblitURL(request);
+ StringBuilder sb = new StringBuilder();
+ sb.append(gitblitUrl);
+ sb.append(Constants.GIT_PATH);
+ sb.append("{0}");
+ String cloneUrl = sb.toString();
+
+ // list repositories
+ List<RepositoryModel> list = GitBlit.self().getRepositoryModels(user);
+ Map<String, RepositoryModel> repositories = new HashMap<String, RepositoryModel>();
+ for (RepositoryModel model : list) {
+ String url = MessageFormat.format(cloneUrl, model.name);
+ repositories.put(url, model);
+ }
+ result = repositories;
+ } else if (RpcRequest.LIST_BRANCHES.equals(reqType)) {
+ // list all local branches in all repositories accessible to user
+ Map<String, List<String>> localBranches = new HashMap<String, List<String>>();
+ List<RepositoryModel> models = GitBlit.self().getRepositoryModels(user);
+ for (RepositoryModel model : models) {
+ if (!model.hasCommits) {
+ // skip empty repository
+ continue;
+ }
+ if (model.isCollectingGarbage) {
+ // skip garbage collecting repository
+ logger.warn(MessageFormat.format("Temporarily excluding {0} from RPC, busy collecting garbage", model.name));
+ continue;
+ }
+ // get local branches
+ Repository repository = GitBlit.self().getRepository(model.name);
+ List<RefModel> refs = JGitUtils.getLocalBranches(repository, false, -1);
+ if (model.showRemoteBranches) {
+ // add remote branches if repository displays them
+ refs.addAll(JGitUtils.getRemoteBranches(repository, false, -1));
+ }
+ if (refs.size() > 0) {
+ List<String> branches = new ArrayList<String>();
+ for (RefModel ref : refs) {
+ branches.add(ref.getName());
+ }
+ localBranches.put(model.name, branches);
+ }
+ repository.close();
+ }
+ result = localBranches;
+ } else if (RpcRequest.LIST_USERS.equals(reqType)) {
+ // list users
+ List<String> names = GitBlit.self().getAllUsernames();
+ List<UserModel> users = new ArrayList<UserModel>();
+ for (String name : names) {
+ users.add(GitBlit.self().getUserModel(name));
+ }
+ result = users;
+ } else if (RpcRequest.LIST_TEAMS.equals(reqType)) {
+ // list teams
+ List<String> names = GitBlit.self().getAllTeamnames();
+ List<TeamModel> teams = new ArrayList<TeamModel>();
+ for (String name : names) {
+ teams.add(GitBlit.self().getTeamModel(name));
+ }
+ result = teams;
+ } else if (RpcRequest.CREATE_REPOSITORY.equals(reqType)) {
+ // create repository
+ RepositoryModel model = deserialize(request, response, RepositoryModel.class);
+ try {
+ GitBlit.self().updateRepositoryModel(model.name, model, true);
+ } catch (GitBlitException e) {
+ response.setStatus(failureCode);
+ }
+ } else if (RpcRequest.EDIT_REPOSITORY.equals(reqType)) {
+ // edit repository
+ RepositoryModel model = deserialize(request, response, RepositoryModel.class);
+ // name specifies original repository name in event of rename
+ String repoName = objectName;
+ if (repoName == null) {
+ repoName = model.name;
+ }
+ try {
+ GitBlit.self().updateRepositoryModel(repoName, model, false);
+ } catch (GitBlitException e) {
+ response.setStatus(failureCode);
+ }
+ } else if (RpcRequest.DELETE_REPOSITORY.equals(reqType)) {
+ // delete repository
+ RepositoryModel model = deserialize(request, response, RepositoryModel.class);
+ GitBlit.self().deleteRepositoryModel(model);
+ } else if (RpcRequest.CREATE_USER.equals(reqType)) {
+ // create user
+ UserModel model = deserialize(request, response, UserModel.class);
+ try {
+ GitBlit.self().updateUserModel(model.username, model, true);
+ } catch (GitBlitException e) {
+ response.setStatus(failureCode);
+ }
+ } else if (RpcRequest.EDIT_USER.equals(reqType)) {
+ // edit user
+ UserModel model = deserialize(request, response, UserModel.class);
+ // name parameter specifies original user name in event of rename
+ String username = objectName;
+ if (username == null) {
+ username = model.username;
+ }
+ try {
+ GitBlit.self().updateUserModel(username, model, false);
+ } catch (GitBlitException e) {
+ response.setStatus(failureCode);
+ }
+ } else if (RpcRequest.DELETE_USER.equals(reqType)) {
+ // delete user
+ UserModel model = deserialize(request, response, UserModel.class);
+ if (!GitBlit.self().deleteUser(model.username)) {
+ response.setStatus(failureCode);
+ }
+ } else if (RpcRequest.CREATE_TEAM.equals(reqType)) {
+ // create team
+ TeamModel model = deserialize(request, response, TeamModel.class);
+ try {
+ GitBlit.self().updateTeamModel(model.name, model, true);
+ } catch (GitBlitException e) {
+ response.setStatus(failureCode);
+ }
+ } else if (RpcRequest.EDIT_TEAM.equals(reqType)) {
+ // edit team
+ TeamModel model = deserialize(request, response, TeamModel.class);
+ // name parameter specifies original team name in event of rename
+ String teamname = objectName;
+ if (teamname == null) {
+ teamname = model.name;
+ }
+ try {
+ GitBlit.self().updateTeamModel(teamname, model, false);
+ } catch (GitBlitException e) {
+ response.setStatus(failureCode);
+ }
+ } else if (RpcRequest.DELETE_TEAM.equals(reqType)) {
+ // delete team
+ TeamModel model = deserialize(request, response, TeamModel.class);
+ if (!GitBlit.self().deleteTeam(model.name)) {
+ response.setStatus(failureCode);
+ }
+ } else if (RpcRequest.LIST_REPOSITORY_MEMBERS.equals(reqType)) {
+ // get repository members
+ RepositoryModel model = GitBlit.self().getRepositoryModel(objectName);
+ result = GitBlit.self().getRepositoryUsers(model);
+ } else if (RpcRequest.SET_REPOSITORY_MEMBERS.equals(reqType)) {
+ // rejected since 1.2.0
+ response.setStatus(failureCode);
+ } else if (RpcRequest.LIST_REPOSITORY_MEMBER_PERMISSIONS.equals(reqType)) {
+ // get repository member permissions
+ RepositoryModel model = GitBlit.self().getRepositoryModel(objectName);
+ result = GitBlit.self().getUserAccessPermissions(model);
+ } else if (RpcRequest.SET_REPOSITORY_MEMBER_PERMISSIONS.equals(reqType)) {
+ // set the repository permissions for the specified users
+ RepositoryModel model = GitBlit.self().getRepositoryModel(objectName);
+ Collection<RegistrantAccessPermission> permissions = deserialize(request, response, RpcUtils.REGISTRANT_PERMISSIONS_TYPE);
+ result = GitBlit.self().setUserAccessPermissions(model, permissions);
+ } else if (RpcRequest.LIST_REPOSITORY_TEAMS.equals(reqType)) {
+ // get repository teams
+ RepositoryModel model = GitBlit.self().getRepositoryModel(objectName);
+ result = GitBlit.self().getRepositoryTeams(model);
+ } else if (RpcRequest.SET_REPOSITORY_TEAMS.equals(reqType)) {
+ // rejected since 1.2.0
+ response.setStatus(failureCode);
+ } else if (RpcRequest.LIST_REPOSITORY_TEAM_PERMISSIONS.equals(reqType)) {
+ // get repository team permissions
+ RepositoryModel model = GitBlit.self().getRepositoryModel(objectName);
+ result = GitBlit.self().getTeamAccessPermissions(model);
+ } else if (RpcRequest.SET_REPOSITORY_TEAM_PERMISSIONS.equals(reqType)) {
+ // set the repository permissions for the specified teams
+ RepositoryModel model = GitBlit.self().getRepositoryModel(objectName);
+ Collection<RegistrantAccessPermission> permissions = deserialize(request, response, RpcUtils.REGISTRANT_PERMISSIONS_TYPE);
+ result = GitBlit.self().setTeamAccessPermissions(model, permissions);
+ } else if (RpcRequest.LIST_FEDERATION_REGISTRATIONS.equals(reqType)) {
+ // return the list of federation registrations
+ if (allowAdmin) {
+ result = GitBlit.self().getFederationRegistrations();
+ } else {
+ response.sendError(notAllowedCode);
+ }
+ } else if (RpcRequest.LIST_FEDERATION_RESULTS.equals(reqType)) {
+ // return the list of federation result registrations
+ if (allowAdmin && GitBlit.canFederate()) {
+ result = GitBlit.self().getFederationResultRegistrations();
+ } else {
+ response.sendError(notAllowedCode);
+ }
+ } else if (RpcRequest.LIST_FEDERATION_PROPOSALS.equals(reqType)) {
+ // return the list of federation proposals
+ if (allowAdmin && GitBlit.canFederate()) {
+ result = GitBlit.self().getPendingFederationProposals();
+ } else {
+ response.sendError(notAllowedCode);
+ }
+ } else if (RpcRequest.LIST_FEDERATION_SETS.equals(reqType)) {
+ // return the list of federation sets
+ if (allowAdmin && GitBlit.canFederate()) {
+ String gitblitUrl = HttpUtils.getGitblitURL(request);
+ result = GitBlit.self().getFederationSets(gitblitUrl);
+ } else {
+ response.sendError(notAllowedCode);
+ }
+ } else if (RpcRequest.LIST_SETTINGS.equals(reqType)) {
+ // return the server's settings
+ ServerSettings settings = GitBlit.self().getSettingsModel();
+ if (allowAdmin) {
+ // return all settings
+ result = settings;
+ } else {
+ // anonymous users get a few settings to allow browser launching
+ List<String> keys = new ArrayList<String>();
+ keys.add(Keys.web.siteName);
+ keys.add(Keys.web.mountParameters);
+ keys.add(Keys.web.syndicationEntries);
+
+ if (allowManagement) {
+ // keys necessary for repository and/or user management
+ keys.add(Keys.realm.minPasswordLength);
+ keys.add(Keys.realm.passwordStorage);
+ keys.add(Keys.federation.sets);
+ }
+ // build the settings
+ ServerSettings managementSettings = new ServerSettings();
+ for (String key : keys) {
+ managementSettings.add(settings.get(key));
+ }
+ if (allowManagement) {
+ managementSettings.pushScripts = settings.pushScripts;
+ }
+ result = managementSettings;
+ }
+ } else if (RpcRequest.EDIT_SETTINGS.equals(reqType)) {
+ // update settings on the server
+ if (allowAdmin) {
+ Map<String, String> settings = deserialize(request, response,
+ RpcUtils.SETTINGS_TYPE);
+ GitBlit.self().updateSettings(settings);
+ } else {
+ response.sendError(notAllowedCode);
+ }
+ } else if (RpcRequest.LIST_STATUS.equals(reqType)) {
+ // return the server's status information
+ if (allowAdmin) {
+ result = GitBlit.self().getStatus();
+ } else {
+ response.sendError(notAllowedCode);
+ }
+ } else if (RpcRequest.CLEAR_REPOSITORY_CACHE.equals(reqType)) {
+ // clear the repository list cache
+ if (allowManagement) {
+ GitBlit.self().resetRepositoryListCache();
+ } else {
+ response.sendError(notAllowedCode);
+ }
+ }
+
+ // send the result of the request
+ serialize(response, result);
+ }
+}
diff --git a/src/main/java/com/gitblit/ServletRequestWrapper.java b/src/main/java/com/gitblit/ServletRequestWrapper.java
new file mode 100644
index 00000000..d74a9ecb
--- /dev/null
+++ b/src/main/java/com/gitblit/ServletRequestWrapper.java
@@ -0,0 +1,400 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.security.Principal;
+import java.util.Collection;
+import java.util.Enumeration;
+import java.util.Locale;
+import java.util.Map;
+
+import javax.servlet.AsyncContext;
+import javax.servlet.DispatcherType;
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.ServletInputStream;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+import javax.servlet.http.Part;
+
+/**
+ * ServletRequestWrapper is a pass-through/delegate wrapper class for a servlet
+ * request. This class is used in conjunction with ServletFilters, such as the
+ * AccessRestrictionFilter.
+ *
+ * The original request is wrapped by instances of this class and this class is
+ * set as the servlet request in the filter. This allows for specialized
+ * implementations of request methods, like getUserPrincipal() with delegation
+ * to the original request for any method not overridden.
+ *
+ * This class, by itself, is not altogether interesting. Subclasses of this
+ * class, however, are of interest.
+ *
+ * @author James Moger
+ *
+ */
+public abstract class ServletRequestWrapper implements HttpServletRequest {
+
+ protected final HttpServletRequest req;
+
+ public ServletRequestWrapper(HttpServletRequest req) {
+ this.req = req;
+ }
+
+ @Override
+ public Object getAttribute(String name) {
+ return req.getAttribute(name);
+ }
+
+ @Override
+ public Enumeration getAttributeNames() {
+ return req.getAttributeNames();
+ }
+
+ @Override
+ public String getCharacterEncoding() {
+ return req.getCharacterEncoding();
+ }
+
+ @Override
+ public void setCharacterEncoding(String env) throws UnsupportedEncodingException {
+ req.setCharacterEncoding(env);
+ }
+
+ @Override
+ public int getContentLength() {
+ return req.getContentLength();
+ }
+
+ @Override
+ public String getContentType() {
+ return req.getContentType();
+ }
+
+ @Override
+ public ServletInputStream getInputStream() throws IOException {
+ return req.getInputStream();
+ }
+
+ @Override
+ public String getParameter(String name) {
+ return req.getParameter(name);
+ }
+
+ @Override
+ public Enumeration getParameterNames() {
+ return req.getParameterNames();
+ }
+
+ @Override
+ public String[] getParameterValues(String name) {
+ return req.getParameterValues(name);
+ }
+
+ @Override
+ public Map getParameterMap() {
+ return req.getParameterMap();
+ }
+
+ @Override
+ public String getProtocol() {
+ return req.getProtocol();
+ }
+
+ @Override
+ public String getScheme() {
+ return req.getScheme();
+ }
+
+ @Override
+ public String getServerName() {
+ return req.getServerName();
+ }
+
+ @Override
+ public int getServerPort() {
+ return req.getServerPort();
+ }
+
+ @Override
+ public BufferedReader getReader() throws IOException {
+ return req.getReader();
+ }
+
+ @Override
+ public String getRemoteAddr() {
+ return req.getRemoteAddr();
+ }
+
+ @Override
+ public String getRemoteHost() {
+ return req.getRemoteHost();
+ }
+
+ @Override
+ public void setAttribute(String name, Object o) {
+ req.setAttribute(name, o);
+ }
+
+ @Override
+ public void removeAttribute(String name) {
+ req.removeAttribute(name);
+ }
+
+ @Override
+ public Locale getLocale() {
+ return req.getLocale();
+ }
+
+ @Override
+ public Enumeration getLocales() {
+ return req.getLocales();
+ }
+
+ @Override
+ public boolean isSecure() {
+ return req.isSecure();
+ }
+
+ @Override
+ public RequestDispatcher getRequestDispatcher(String path) {
+ return req.getRequestDispatcher(path);
+ }
+
+ @Override
+ @Deprecated
+ public String getRealPath(String path) {
+ return req.getRealPath(path);
+ }
+
+ @Override
+ public int getRemotePort() {
+ return req.getRemotePort();
+ }
+
+ @Override
+ public String getLocalName() {
+ return req.getLocalName();
+ }
+
+ @Override
+ public String getLocalAddr() {
+ return req.getLocalAddr();
+ }
+
+ @Override
+ public int getLocalPort() {
+ return req.getLocalPort();
+ }
+
+ @Override
+ public String getAuthType() {
+ return req.getAuthType();
+ }
+
+ @Override
+ public Cookie[] getCookies() {
+ return req.getCookies();
+ }
+
+ @Override
+ public long getDateHeader(String name) {
+ return req.getDateHeader(name);
+ }
+
+ @Override
+ public String getHeader(String name) {
+ return req.getHeader(name);
+ }
+
+ @Override
+ public Enumeration getHeaders(String name) {
+ return req.getHeaders(name);
+ }
+
+ @Override
+ public Enumeration getHeaderNames() {
+ return req.getHeaderNames();
+ }
+
+ @Override
+ public int getIntHeader(String name) {
+ return req.getIntHeader(name);
+ }
+
+ @Override
+ public String getMethod() {
+ return req.getMethod();
+ }
+
+ @Override
+ public String getPathInfo() {
+ return req.getPathInfo();
+ }
+
+ @Override
+ public String getPathTranslated() {
+ return req.getPathTranslated();
+ }
+
+ @Override
+ public String getContextPath() {
+ return req.getContextPath();
+ }
+
+ @Override
+ public String getQueryString() {
+ return req.getQueryString();
+ }
+
+ @Override
+ public String getRemoteUser() {
+ return req.getRemoteUser();
+ }
+
+ @Override
+ public boolean isUserInRole(String role) {
+ return req.isUserInRole(role);
+ }
+
+ @Override
+ public Principal getUserPrincipal() {
+ return req.getUserPrincipal();
+ }
+
+ @Override
+ public String getRequestedSessionId() {
+ return req.getRequestedSessionId();
+ }
+
+ @Override
+ public String getRequestURI() {
+ return req.getRequestURI();
+ }
+
+ @Override
+ public StringBuffer getRequestURL() {
+ return req.getRequestURL();
+ }
+
+ @Override
+ public String getServletPath() {
+ return req.getServletPath();
+ }
+
+ @Override
+ public HttpSession getSession(boolean create) {
+ return req.getSession(create);
+ }
+
+ @Override
+ public HttpSession getSession() {
+ return req.getSession();
+ }
+
+ @Override
+ public boolean isRequestedSessionIdValid() {
+ return req.isRequestedSessionIdValid();
+ }
+
+ @Override
+ public boolean isRequestedSessionIdFromCookie() {
+ return req.isRequestedSessionIdFromCookie();
+ }
+
+ @Override
+ public boolean isRequestedSessionIdFromURL() {
+ return req.isRequestedSessionIdFromURL();
+ }
+
+ @Override
+ @Deprecated
+ public boolean isRequestedSessionIdFromUrl() {
+ return req.isRequestedSessionIdFromUrl();
+ }
+
+ /*
+ * Servlet 3.0 Methods
+ */
+
+ @Override
+ public boolean authenticate(HttpServletResponse response) throws IOException, ServletException {
+ return false;
+ }
+
+ @Override
+ public void login(String username, String password) throws ServletException {
+ }
+
+ @Override
+ public void logout() throws ServletException {
+ }
+
+
+ @Override
+ public Part getPart(String arg0) throws IOException, ServletException {
+ return req.getPart(arg0);
+ }
+
+ @Override
+ public Collection<Part> getParts() throws IOException, ServletException {
+ return req.getParts();
+ }
+
+ @Override
+ public AsyncContext getAsyncContext() {
+ return req.getAsyncContext();
+ }
+
+ @Override
+ public DispatcherType getDispatcherType() {
+ return req.getDispatcherType();
+ }
+
+ @Override
+ public ServletContext getServletContext() {
+ return req.getServletContext();
+ }
+
+ @Override
+ public boolean isAsyncStarted() {
+ return req.isAsyncStarted();
+ }
+
+ @Override
+ public boolean isAsyncSupported() {
+ return req.isAsyncStarted();
+ }
+
+ @Override
+ public AsyncContext startAsync() throws IllegalStateException {
+ return req.startAsync();
+ }
+
+ @Override
+ public AsyncContext startAsync(ServletRequest arg0, ServletResponse arg1)
+ throws IllegalStateException {
+ return req.startAsync(arg0, arg1);
+ }
+} \ No newline at end of file
diff --git a/src/main/java/com/gitblit/SyndicationFilter.java b/src/main/java/com/gitblit/SyndicationFilter.java
new file mode 100644
index 00000000..61bf2258
--- /dev/null
+++ b/src/main/java/com/gitblit/SyndicationFilter.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit;
+
+import java.io.IOException;
+import java.text.MessageFormat;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import com.gitblit.Constants.AccessRestrictionType;
+import com.gitblit.models.ProjectModel;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.UserModel;
+
+/**
+ * The SyndicationFilter is an AuthenticationFilter which ensures that feed
+ * requests for projects or view-restricted repositories have proper authentication
+ * credentials and are authorized for the requested feed.
+ *
+ * @author James Moger
+ *
+ */
+public class SyndicationFilter extends AuthenticationFilter {
+
+ /**
+ * Extract the repository name from the url.
+ *
+ * @param url
+ * @return repository name
+ */
+ protected String extractRequestedName(String url) {
+ if (url.indexOf('?') > -1) {
+ return url.substring(0, url.indexOf('?'));
+ }
+ return url;
+ }
+
+ /**
+ * doFilter does the actual work of preprocessing the request to ensure that
+ * the user may proceed.
+ *
+ * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest,
+ * javax.servlet.ServletResponse, javax.servlet.FilterChain)
+ */
+ @Override
+ public void doFilter(final ServletRequest request, final ServletResponse response,
+ final FilterChain chain) throws IOException, ServletException {
+
+ HttpServletRequest httpRequest = (HttpServletRequest) request;
+ HttpServletResponse httpResponse = (HttpServletResponse) response;
+
+ String fullUrl = getFullUrl(httpRequest);
+ String name = extractRequestedName(fullUrl);
+
+ ProjectModel project = GitBlit.self().getProjectModel(name);
+ RepositoryModel model = null;
+
+ if (project == null) {
+ // try loading a repository model
+ model = GitBlit.self().getRepositoryModel(name);
+ if (model == null) {
+ // repository not found. send 404.
+ logger.info(MessageFormat.format("ARF: {0} ({1})", fullUrl,
+ HttpServletResponse.SC_NOT_FOUND));
+ httpResponse.sendError(HttpServletResponse.SC_NOT_FOUND);
+ return;
+ }
+ }
+
+ // Wrap the HttpServletRequest with the AccessRestrictionRequest which
+ // overrides the servlet container user principal methods.
+ // JGit requires either:
+ //
+ // 1. servlet container authenticated user
+ // 2. http.receivepack = true in each repository's config
+ //
+ // Gitblit must conditionally authenticate users per-repository so just
+ // enabling http.receivepack is insufficient.
+ AuthenticatedRequest authenticatedRequest = new AuthenticatedRequest(httpRequest);
+ UserModel user = getUser(httpRequest);
+ if (user != null) {
+ authenticatedRequest.setUser(user);
+ }
+
+ // BASIC authentication challenge and response processing
+ if (model != null) {
+ if (model.accessRestriction.atLeast(AccessRestrictionType.VIEW)) {
+ if (user == null) {
+ // challenge client to provide credentials. send 401.
+ if (GitBlit.isDebugMode()) {
+ logger.info(MessageFormat.format("ARF: CHALLENGE {0}", fullUrl));
+ }
+ httpResponse.setHeader("WWW-Authenticate", CHALLENGE);
+ httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED);
+ return;
+ } else {
+ // check user access for request
+ if (user.canView(model)) {
+ // authenticated request permitted.
+ // pass processing to the restricted servlet.
+ newSession(authenticatedRequest, httpResponse);
+ logger.info(MessageFormat.format("ARF: {0} ({1}) authenticated", fullUrl,
+ HttpServletResponse.SC_CONTINUE));
+ chain.doFilter(authenticatedRequest, httpResponse);
+ return;
+ }
+ // valid user, but not for requested access. send 403.
+ if (GitBlit.isDebugMode()) {
+ logger.info(MessageFormat.format("ARF: {0} forbidden to access {1}",
+ user.username, fullUrl));
+ }
+ httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN);
+ return;
+ }
+ }
+ }
+
+ if (GitBlit.isDebugMode()) {
+ logger.info(MessageFormat.format("ARF: {0} ({1}) unauthenticated", fullUrl,
+ HttpServletResponse.SC_CONTINUE));
+ }
+ // unauthenticated request permitted.
+ // pass processing to the restricted servlet.
+ chain.doFilter(authenticatedRequest, httpResponse);
+ }
+}
diff --git a/src/main/java/com/gitblit/SyndicationServlet.java b/src/main/java/com/gitblit/SyndicationServlet.java
new file mode 100644
index 00000000..baaf7eb7
--- /dev/null
+++ b/src/main/java/com/gitblit/SyndicationServlet.java
@@ -0,0 +1,324 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit;
+
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.http.HttpServlet;
+
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.AuthenticationFilter.AuthenticatedRequest;
+import com.gitblit.models.FeedEntryModel;
+import com.gitblit.models.ProjectModel;
+import com.gitblit.models.RefModel;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.UserModel;
+import com.gitblit.utils.HttpUtils;
+import com.gitblit.utils.JGitUtils;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.utils.SyndicationUtils;
+
+/**
+ * SyndicationServlet generates RSS 2.0 feeds and feed links.
+ *
+ * Access to this servlet is protected by the SyndicationFilter.
+ *
+ * @author James Moger
+ *
+ */
+public class SyndicationServlet extends HttpServlet {
+
+ private static final long serialVersionUID = 1L;
+
+ private transient Logger logger = LoggerFactory.getLogger(SyndicationServlet.class);
+
+ /**
+ * Create a feed link for the specified repository and branch/tag/commit id.
+ *
+ * @param baseURL
+ * @param repository
+ * the repository name
+ * @param objectId
+ * the branch, tag, or first commit for the feed
+ * @param length
+ * the number of commits to include in the feed
+ * @return an RSS feed url
+ */
+ public static String asLink(String baseURL, String repository, String objectId, int length) {
+ if (baseURL.length() > 0 && baseURL.charAt(baseURL.length() - 1) == '/') {
+ baseURL = baseURL.substring(0, baseURL.length() - 1);
+ }
+ StringBuilder url = new StringBuilder();
+ url.append(baseURL);
+ url.append(Constants.SYNDICATION_PATH);
+ url.append(repository);
+ if (!StringUtils.isEmpty(objectId) || length > 0) {
+ StringBuilder parameters = new StringBuilder("?");
+ if (StringUtils.isEmpty(objectId)) {
+ parameters.append("l=");
+ parameters.append(length);
+ } else {
+ parameters.append("h=");
+ parameters.append(objectId);
+ if (length > 0) {
+ parameters.append("&l=");
+ parameters.append(length);
+ }
+ }
+ url.append(parameters);
+ }
+ return url.toString();
+ }
+
+ /**
+ * Determines the appropriate title for a feed.
+ *
+ * @param repository
+ * @param objectId
+ * @return title of the feed
+ */
+ public static String getTitle(String repository, String objectId) {
+ String id = objectId;
+ if (!StringUtils.isEmpty(id)) {
+ if (id.startsWith(org.eclipse.jgit.lib.Constants.R_HEADS)) {
+ id = id.substring(org.eclipse.jgit.lib.Constants.R_HEADS.length());
+ } else if (id.startsWith(org.eclipse.jgit.lib.Constants.R_REMOTES)) {
+ id = id.substring(org.eclipse.jgit.lib.Constants.R_REMOTES.length());
+ } else if (id.startsWith(org.eclipse.jgit.lib.Constants.R_TAGS)) {
+ id = id.substring(org.eclipse.jgit.lib.Constants.R_TAGS.length());
+ }
+ }
+ return MessageFormat.format("{0} ({1})", repository, id);
+ }
+
+ /**
+ * Generates the feed content.
+ *
+ * @param request
+ * @param response
+ * @throws javax.servlet.ServletException
+ * @throws java.io.IOException
+ */
+ private void processRequest(javax.servlet.http.HttpServletRequest request,
+ javax.servlet.http.HttpServletResponse response) throws javax.servlet.ServletException,
+ java.io.IOException {
+
+ String servletUrl = request.getContextPath() + request.getServletPath();
+ String url = request.getRequestURI().substring(servletUrl.length());
+ if (url.charAt(0) == '/' && url.length() > 1) {
+ url = url.substring(1);
+ }
+ String repositoryName = url;
+ String objectId = request.getParameter("h");
+ String l = request.getParameter("l");
+ String page = request.getParameter("pg");
+ String searchString = request.getParameter("s");
+ Constants.SearchType searchType = Constants.SearchType.COMMIT;
+ if (!StringUtils.isEmpty(request.getParameter("st"))) {
+ Constants.SearchType type = Constants.SearchType.forName(request.getParameter("st"));
+ if (type != null) {
+ searchType = type;
+ }
+ }
+ int length = GitBlit.getInteger(Keys.web.syndicationEntries, 25);
+ if (StringUtils.isEmpty(objectId)) {
+ objectId = org.eclipse.jgit.lib.Constants.HEAD;
+ }
+ if (!StringUtils.isEmpty(l)) {
+ try {
+ length = Integer.parseInt(l);
+ } catch (NumberFormatException x) {
+ }
+ }
+ int offset = 0;
+ if (!StringUtils.isEmpty(page)) {
+ try {
+ offset = length * Integer.parseInt(page);
+ } catch (NumberFormatException x) {
+ }
+ }
+
+ response.setContentType("application/rss+xml; charset=UTF-8");
+
+ boolean isProjectFeed = false;
+ String feedName = null;
+ String feedTitle = null;
+ String feedDescription = null;
+
+ List<String> repositories = null;
+ if (repositoryName.indexOf('/') == -1 && !repositoryName.toLowerCase().endsWith(".git")) {
+ // try to find a project
+ UserModel user = null;
+ if (request instanceof AuthenticatedRequest) {
+ user = ((AuthenticatedRequest) request).getUser();
+ }
+ ProjectModel project = GitBlit.self().getProjectModel(repositoryName, user);
+ if (project != null) {
+ isProjectFeed = true;
+ repositories = new ArrayList<String>(project.repositories);
+
+ // project feed
+ feedName = project.name;
+ feedTitle = project.title;
+ feedDescription = project.description;
+ }
+ }
+
+ if (repositories == null) {
+ // could not find project, assume this is a repository
+ repositories = Arrays.asList(repositoryName);
+ }
+
+
+ boolean mountParameters = GitBlit.getBoolean(Keys.web.mountParameters, true);
+ String urlPattern;
+ if (mountParameters) {
+ // mounted parameters
+ urlPattern = "{0}/commit/{1}/{2}";
+ } else {
+ // parameterized parameters
+ urlPattern = "{0}/commit/?r={1}&h={2}";
+ }
+ String gitblitUrl = HttpUtils.getGitblitURL(request);
+ char fsc = GitBlit.getChar(Keys.web.forwardSlashCharacter, '/');
+
+ List<FeedEntryModel> entries = new ArrayList<FeedEntryModel>();
+
+ for (String name : repositories) {
+ Repository repository = GitBlit.self().getRepository(name);
+ RepositoryModel model = GitBlit.self().getRepositoryModel(name);
+
+ if (repository == null) {
+ if (model.isCollectingGarbage) {
+ logger.warn(MessageFormat.format("Temporarily excluding {0} from feed, busy collecting garbage", name));
+ }
+ continue;
+ }
+ if (!isProjectFeed) {
+ // single-repository feed
+ feedName = model.name;
+ feedTitle = model.name;
+ feedDescription = model.description;
+ }
+
+ List<RevCommit> commits;
+ if (StringUtils.isEmpty(searchString)) {
+ // standard log/history lookup
+ commits = JGitUtils.getRevLog(repository, objectId, offset, length);
+ } else {
+ // repository search
+ commits = JGitUtils.searchRevlogs(repository, objectId, searchString, searchType,
+ offset, length);
+ }
+ Map<ObjectId, List<RefModel>> allRefs = JGitUtils.getAllRefs(repository, model.showRemoteBranches);
+
+ // convert RevCommit to SyndicatedEntryModel
+ for (RevCommit commit : commits) {
+ FeedEntryModel entry = new FeedEntryModel();
+ entry.title = commit.getShortMessage();
+ entry.author = commit.getAuthorIdent().getName();
+ entry.link = MessageFormat.format(urlPattern, gitblitUrl,
+ StringUtils.encodeURL(model.name.replace('/', fsc)), commit.getName());
+ entry.published = commit.getCommitterIdent().getWhen();
+ entry.contentType = "text/html";
+ String message = GitBlit.self().processCommitMessage(model.name,
+ commit.getFullMessage());
+ entry.content = message;
+ entry.repository = model.name;
+ entry.branch = objectId;
+ entry.tags = new ArrayList<String>();
+
+ // add commit id and parent commit ids
+ entry.tags.add("commit:" + commit.getName());
+ for (RevCommit parent : commit.getParents()) {
+ entry.tags.add("parent:" + parent.getName());
+ }
+
+ // add refs to tabs list
+ List<RefModel> refs = allRefs.get(commit.getId());
+ if (refs != null && refs.size() > 0) {
+ for (RefModel ref : refs) {
+ entry.tags.add("ref:" + ref.getName());
+ }
+ }
+ entries.add(entry);
+ }
+ }
+
+ // sort & truncate the feed
+ Collections.sort(entries);
+ if (entries.size() > length) {
+ // clip the list
+ entries = entries.subList(0, length);
+ }
+
+ String feedLink;
+ if (isProjectFeed) {
+ // project feed
+ if (mountParameters) {
+ // mounted url
+ feedLink = MessageFormat.format("{0}/project/{1}", gitblitUrl,
+ StringUtils.encodeURL(feedName));
+ } else {
+ // parameterized url
+ feedLink = MessageFormat.format("{0}/project/?p={1}", gitblitUrl,
+ StringUtils.encodeURL(feedName));
+ }
+ } else {
+ // repository feed
+ if (mountParameters) {
+ // mounted url
+ feedLink = MessageFormat.format("{0}/summary/{1}", gitblitUrl,
+ StringUtils.encodeURL(feedName));
+ } else {
+ // parameterized url
+ feedLink = MessageFormat.format("{0}/summary/?r={1}", gitblitUrl,
+ StringUtils.encodeURL(feedName));
+ }
+ }
+
+ try {
+ SyndicationUtils.toRSS(gitblitUrl, feedLink, getTitle(feedTitle, objectId),
+ feedDescription, entries, response.getOutputStream());
+ } catch (Exception e) {
+ logger.error("An error occurred during feed generation", e);
+ }
+ }
+
+ @Override
+ protected void doPost(javax.servlet.http.HttpServletRequest request,
+ javax.servlet.http.HttpServletResponse response) throws javax.servlet.ServletException,
+ java.io.IOException {
+ processRequest(request, response);
+ }
+
+ @Override
+ protected void doGet(javax.servlet.http.HttpServletRequest request,
+ javax.servlet.http.HttpServletResponse response) throws javax.servlet.ServletException,
+ java.io.IOException {
+ processRequest(request, response);
+ }
+}
diff --git a/src/main/java/com/gitblit/WebXmlSettings.java b/src/main/java/com/gitblit/WebXmlSettings.java
new file mode 100644
index 00000000..7c8120b7
--- /dev/null
+++ b/src/main/java/com/gitblit/WebXmlSettings.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.text.MessageFormat;
+import java.util.Enumeration;
+import java.util.Map;
+import java.util.Properties;
+
+import javax.servlet.ServletContext;
+
+import com.gitblit.utils.StringUtils;
+
+/**
+ * Loads Gitblit settings from the context-parameter values of a web.xml file.
+ *
+ * @author James Moger
+ *
+ */
+public class WebXmlSettings extends IStoredSettings {
+
+ private final Properties properties = new Properties();
+
+ private File overrideFile;
+
+ public WebXmlSettings(ServletContext context) {
+ super(WebXmlSettings.class);
+ Enumeration<?> keys = context.getInitParameterNames();
+ while (keys.hasMoreElements()) {
+ String key = keys.nextElement().toString();
+ String value = context.getInitParameter(key);
+ properties.put(key, decodeValue(value));
+ logger.debug(key + "=" + properties.getProperty(key));
+ }
+ }
+
+ public void applyOverrides(File overrideFile) {
+ this.overrideFile = overrideFile;
+
+ // apply any web-configured overrides
+ if (overrideFile.exists()) {
+ try {
+ InputStream is = new FileInputStream(overrideFile);
+ properties.load(is);
+ is.close();
+ } catch (Throwable t) {
+ logger.error(
+ MessageFormat.format("Failed to apply {0} setting overrides",
+ overrideFile.getAbsolutePath()), t);
+ }
+ }
+ }
+
+ private String decodeValue(String value) {
+ // decode escaped backslashes and HTML entities
+ return StringUtils.decodeFromHtml(value).replace("\\\\", "\\");
+ }
+
+ @Override
+ protected Properties read() {
+ return properties;
+ }
+
+ @Override
+ public synchronized boolean saveSettings(Map<String, String> settings) {
+ try {
+ Properties props = new Properties();
+ // load pre-existing web-configuration
+ if (overrideFile.exists()) {
+ InputStream is = new FileInputStream(overrideFile);
+ props.load(is);
+ is.close();
+ }
+
+ // put all new settings and persist
+ props.putAll(settings);
+ OutputStream os = new FileOutputStream(overrideFile);
+ props.store(os, null);
+ os.close();
+
+ // override current runtime settings
+ properties.putAll(settings);
+ return true;
+ } catch (Throwable t) {
+ logger.error("Failed to save settings!", t);
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return "WEB.XML";
+ }
+}
diff --git a/src/main/java/com/gitblit/authority/AuthorityWorker.java b/src/main/java/com/gitblit/authority/AuthorityWorker.java
new file mode 100644
index 00000000..262bbb53
--- /dev/null
+++ b/src/main/java/com/gitblit/authority/AuthorityWorker.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2012 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.authority;
+
+import java.awt.Component;
+import java.awt.Cursor;
+import java.io.IOException;
+
+import javax.swing.SwingWorker;
+
+public abstract class AuthorityWorker extends SwingWorker<Boolean, Void> {
+
+ private final Component parent;
+
+ public AuthorityWorker(Component parent) {
+ this.parent = parent;
+ parent.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
+ }
+
+ @Override
+ protected Boolean doInBackground() throws IOException {
+ return doRequest();
+ }
+
+ protected void done() {
+ parent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
+ try {
+ Boolean success = get();
+ if (success) {
+ onSuccess();
+ } else {
+ onFailure();
+ }
+ } catch (Throwable t) {
+ Utils.showException(parent, t);
+ }
+ }
+
+ protected abstract Boolean doRequest() throws IOException;
+
+ protected abstract void onSuccess();
+
+ protected void onFailure() {
+ }
+}
diff --git a/src/main/java/com/gitblit/authority/CertificateStatus.java b/src/main/java/com/gitblit/authority/CertificateStatus.java
new file mode 100644
index 00000000..79c51628
--- /dev/null
+++ b/src/main/java/com/gitblit/authority/CertificateStatus.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2012 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.authority;
+
+public enum CertificateStatus {
+ unknown, ok, expiring, expired, revoked
+}
diff --git a/src/main/java/com/gitblit/authority/CertificateStatusRenderer.java b/src/main/java/com/gitblit/authority/CertificateStatusRenderer.java
new file mode 100644
index 00000000..7a708ea4
--- /dev/null
+++ b/src/main/java/com/gitblit/authority/CertificateStatusRenderer.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2012 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.authority;
+
+import java.awt.Component;
+
+import javax.swing.ImageIcon;
+import javax.swing.JTable;
+import javax.swing.table.DefaultTableCellRenderer;
+
+import com.gitblit.client.Translation;
+
+/**
+ * Displays a subscribed icon on the left of the repository name, if there is at
+ * least one subscribed branch.
+ *
+ * @author James Moger
+ *
+ */
+public class CertificateStatusRenderer extends DefaultTableCellRenderer {
+
+ private static final long serialVersionUID = 1L;
+
+ private final ImageIcon unknownIcon;
+ private final ImageIcon revokedIcon;
+ private final ImageIcon expiredIcon;
+ private final ImageIcon expiringIcon;
+ private final ImageIcon okIcon;
+
+ public CertificateStatusRenderer() {
+ super();
+ unknownIcon = new ImageIcon(getClass().getResource("/bullet_white.png"));
+ revokedIcon = new ImageIcon(getClass().getResource("/bullet_delete.png"));
+ expiredIcon = new ImageIcon(getClass().getResource("/bullet_red.png"));
+ expiringIcon = new ImageIcon(getClass().getResource("/bullet_orange.png"));
+ okIcon = new ImageIcon(getClass().getResource("/bullet_green.png"));
+ }
+
+ public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,
+ boolean hasFocus, int row, int column) {
+ super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
+ if (value instanceof CertificateStatus) {
+ CertificateStatus status = (CertificateStatus) value;
+ switch(status) {
+ case revoked:
+ setText(Translation.get("gb.revoked"));
+ setIcon(revokedIcon);
+ break;
+ case expiring:
+ setText(Translation.get("gb.expiring"));
+ setIcon(expiringIcon);
+ break;
+ case expired:
+ setText(Translation.get("gb.expired"));
+ setIcon(expiredIcon);
+ break;
+ case unknown:
+ setText("");
+ setIcon(unknownIcon);
+ break;
+ default:
+ setText(Translation.get("gb.ok"));
+ setIcon(okIcon);
+ break;
+ }
+ }
+ return this;
+ }
+}
diff --git a/src/main/java/com/gitblit/authority/CertificatesTableModel.java b/src/main/java/com/gitblit/authority/CertificatesTableModel.java
new file mode 100644
index 00000000..44d80e3a
--- /dev/null
+++ b/src/main/java/com/gitblit/authority/CertificatesTableModel.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright 2012 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.authority;
+
+import java.math.BigInteger;
+import java.security.cert.X509Certificate;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+
+import javax.swing.table.AbstractTableModel;
+
+import com.gitblit.client.Translation;
+import com.gitblit.utils.X509Utils.RevocationReason;
+
+/**
+ * Table model of a list of user certificate models.
+ *
+ * @author James Moger
+ *
+ */
+public class CertificatesTableModel extends AbstractTableModel {
+
+ private static final long serialVersionUID = 1L;
+
+ UserCertificateModel ucm;
+
+ enum Columns {
+ SerialNumber, Status, Reason, Issued, Expires;
+
+ @Override
+ public String toString() {
+ return name().replace('_', ' ');
+ }
+ }
+
+ public CertificatesTableModel() {
+ }
+
+ @Override
+ public int getRowCount() {
+ return ucm == null || ucm.certs == null ? 0 : ucm.certs.size();
+ }
+
+ @Override
+ public int getColumnCount() {
+ return Columns.values().length;
+ }
+
+ @Override
+ public String getColumnName(int column) {
+ Columns col = Columns.values()[column];
+ switch (col) {
+ case SerialNumber:
+ return Translation.get("gb.serialNumber");
+ case Issued:
+ return Translation.get("gb.issued");
+ case Expires:
+ return Translation.get("gb.expires");
+ case Status:
+ return Translation.get("gb.status");
+ case Reason:
+ return Translation.get("gb.reason");
+ }
+ return "";
+ }
+
+ /**
+ * Returns <code>Object.class</code> regardless of <code>columnIndex</code>.
+ *
+ * @param columnIndex
+ * the column being queried
+ * @return the Object.class
+ */
+ public Class<?> getColumnClass(int columnIndex) {
+ Columns col = Columns.values()[columnIndex];
+ switch (col) {
+ case Status:
+ return CertificateStatus.class;
+ case Issued:
+ return Date.class;
+ case Expires:
+ return Date.class;
+ case SerialNumber:
+ return BigInteger.class;
+ default:
+ return String.class;
+ }
+ }
+
+ @Override
+ public boolean isCellEditable(int rowIndex, int columnIndex) {
+ Columns col = Columns.values()[columnIndex];
+ switch (col) {
+ default:
+ return false;
+ }
+ }
+
+ @Override
+ public Object getValueAt(int rowIndex, int columnIndex) {
+ X509Certificate cert = ucm.certs.get(rowIndex);
+ Columns col = Columns.values()[columnIndex];
+ switch (col) {
+ case Status:
+ return ucm.getStatus(cert);
+ case SerialNumber:
+ return cert.getSerialNumber();
+ case Issued:
+ return cert.getNotBefore();
+ case Expires:
+ return cert.getNotAfter();
+ case Reason:
+ if (ucm.getStatus(cert).equals(CertificateStatus.revoked)) {
+ RevocationReason r = ucm.getRevocationReason(cert.getSerialNumber());
+ return Translation.get("gb." + r.name());
+ }
+ }
+ return null;
+ }
+
+ public X509Certificate get(int modelRow) {
+ return ucm.certs.get(modelRow);
+ }
+
+ public void setUserCertificateModel(UserCertificateModel ucm) {
+ this.ucm = ucm;
+ Collections.sort(ucm.certs, new Comparator<X509Certificate>() {
+ @Override
+ public int compare(X509Certificate o1, X509Certificate o2) {
+ // sort by issue date in reverse chronological order
+ int result = o2.getNotBefore().compareTo(o1.getNotBefore());
+ if (result == 0) {
+ // same issue date, show expiring first
+ boolean r1 = CertificatesTableModel.this.ucm.isRevoked(o1.getSerialNumber());
+ boolean r2 = CertificatesTableModel.this.ucm.isRevoked(o2.getSerialNumber());
+ if ((r1 && r2) || (!r1 && !r2)) {
+ // both revoked or both not revoked
+ // chronlogical order by expiration dates
+ result = o1.getNotAfter().compareTo(o2.getNotAfter());
+ } else if (r1) {
+ // r1 is revoked, r2 first
+ return 1;
+ } else {
+ // r2 is revoked, r1 first
+ return -1;
+ }
+ }
+ return result;
+ }
+ });
+ }
+}
diff --git a/src/main/java/com/gitblit/authority/DefaultOidsPanel.java b/src/main/java/com/gitblit/authority/DefaultOidsPanel.java
new file mode 100644
index 00000000..12b919fa
--- /dev/null
+++ b/src/main/java/com/gitblit/authority/DefaultOidsPanel.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2012 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.authority;
+
+import java.awt.GridLayout;
+
+import javax.swing.JPanel;
+import javax.swing.JTextField;
+
+import com.gitblit.client.Translation;
+import com.gitblit.utils.X509Utils.X509Metadata;
+
+public class DefaultOidsPanel extends JPanel {
+
+ private static final long serialVersionUID = 1L;
+
+ private JTextField organizationalUnit;
+ private JTextField organization;
+ private JTextField locality;
+ private JTextField stateProvince;
+ private JTextField countryCode;
+
+ public DefaultOidsPanel(X509Metadata metadata) {
+ super();
+
+ organizationalUnit = new JTextField(metadata.getOID("OU", ""), 20);
+ organization = new JTextField(metadata.getOID("O", ""), 20);
+ locality = new JTextField(metadata.getOID("L", ""), 20);
+ stateProvince = new JTextField(metadata.getOID("ST", ""), 20);
+ countryCode = new JTextField(metadata.getOID("C", ""), 20);
+
+ setLayout(new GridLayout(0, 1, Utils.MARGIN, Utils.MARGIN));
+ add(Utils.newFieldPanel(Translation.get("gb.organizationalUnit") + " (OU)", organizationalUnit));
+ add(Utils.newFieldPanel(Translation.get("gb.organization") + " (O)", organization));
+ add(Utils.newFieldPanel(Translation.get("gb.locality") + " (L)", locality));
+ add(Utils.newFieldPanel(Translation.get("gb.stateProvince") + " (ST)", stateProvince));
+ add(Utils.newFieldPanel(Translation.get("gb.countryCode") + " (C)", countryCode));
+ }
+
+ public void update(X509Metadata metadata) {
+ metadata.setOID("OU", organizationalUnit.getText());
+ metadata.setOID("O", organization.getText());
+ metadata.setOID("L", locality.getText());
+ metadata.setOID("ST", stateProvince.getText());
+ metadata.setOID("C", countryCode.getText());
+ }
+
+ public String getOrganizationalUnit() {
+ return organizationalUnit.getText();
+ }
+
+ public String getOrganization() {
+ return organization.getText();
+ }
+
+ public String getLocality() {
+ return locality.getText();
+ }
+
+ public String getStateProvince() {
+ return stateProvince.getText();
+ }
+
+ public String getCountryCode() {
+ return countryCode.getText();
+ }
+}
diff --git a/src/main/java/com/gitblit/authority/GitblitAuthority.java b/src/main/java/com/gitblit/authority/GitblitAuthority.java
new file mode 100644
index 00000000..1a1f96db
--- /dev/null
+++ b/src/main/java/com/gitblit/authority/GitblitAuthority.java
@@ -0,0 +1,921 @@
+/*
+ * Copyright 2012 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.authority;
+
+import java.awt.BorderLayout;
+import java.awt.Container;
+import java.awt.Desktop;
+import java.awt.Dimension;
+import java.awt.EventQueue;
+import java.awt.FlowLayout;
+import java.awt.GridLayout;
+import java.awt.Insets;
+import java.awt.Point;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.KeyAdapter;
+import java.awt.event.KeyEvent;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.io.BufferedInputStream;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileWriter;
+import java.io.FilenameFilter;
+import java.io.IOException;
+import java.net.URI;
+import java.security.PrivateKey;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.activation.DataHandler;
+import javax.activation.FileDataSource;
+import javax.mail.Message;
+import javax.mail.Multipart;
+import javax.mail.internet.MimeBodyPart;
+import javax.mail.internet.MimeMultipart;
+import javax.swing.ImageIcon;
+import javax.swing.InputVerifier;
+import javax.swing.JButton;
+import javax.swing.JComponent;
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JPasswordField;
+import javax.swing.JScrollPane;
+import javax.swing.JSplitPane;
+import javax.swing.JTable;
+import javax.swing.JTextArea;
+import javax.swing.JTextField;
+import javax.swing.JToolBar;
+import javax.swing.RowFilter;
+import javax.swing.SwingConstants;
+import javax.swing.UIManager;
+import javax.swing.event.ListSelectionEvent;
+import javax.swing.event.ListSelectionListener;
+import javax.swing.table.TableRowSorter;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.StoredConfig;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.ConfigUserService;
+import com.gitblit.Constants;
+import com.gitblit.FileSettings;
+import com.gitblit.IStoredSettings;
+import com.gitblit.IUserService;
+import com.gitblit.Keys;
+import com.gitblit.MailExecutor;
+import com.gitblit.client.HeaderPanel;
+import com.gitblit.client.Translation;
+import com.gitblit.models.UserModel;
+import com.gitblit.utils.ArrayUtils;
+import com.gitblit.utils.FileUtils;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.utils.TimeUtils;
+import com.gitblit.utils.X509Utils;
+import com.gitblit.utils.X509Utils.RevocationReason;
+import com.gitblit.utils.X509Utils.X509Log;
+import com.gitblit.utils.X509Utils.X509Metadata;
+
+/**
+ * Simple GUI tool for administering Gitblit client certificates.
+ *
+ * @author James Moger
+ *
+ */
+public class GitblitAuthority extends JFrame implements X509Log {
+
+ private static final long serialVersionUID = 1L;
+
+ private final UserCertificateTableModel tableModel;
+
+ private UserCertificatePanel userCertificatePanel;
+
+ private File folder;
+
+ private IStoredSettings gitblitSettings;
+
+ private IUserService userService;
+
+ private String caKeystorePassword;
+
+ private JTable table;
+
+ private int defaultDuration;
+
+ private TableRowSorter<UserCertificateTableModel> defaultSorter;
+
+ private MailExecutor mail;
+
+ private JButton certificateDefaultsButton;
+
+ private JButton newSSLCertificate;
+
+ public static void main(String... args) {
+ // filter out the baseFolder parameter
+ String folder = "data";
+ for (int i = 0; i< args.length; i++) {
+ String arg = args[i];
+ if (arg.equals("--baseFolder")) {
+ if (i + 1 == args.length) {
+ System.out.println("Invalid --baseFolder parameter!");
+ System.exit(-1);
+ } else if (args[i + 1] != ".") {
+ folder = args[i+1];
+ }
+ break;
+ }
+ }
+ final String baseFolder = folder;
+ EventQueue.invokeLater(new Runnable() {
+ public void run() {
+ try {
+ UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
+ } catch (Exception e) {
+ }
+ GitblitAuthority authority = new GitblitAuthority();
+ authority.initialize(baseFolder);
+ authority.setLocationRelativeTo(null);
+ authority.setVisible(true);
+ }
+ });
+ }
+
+ public GitblitAuthority() {
+ super();
+ tableModel = new UserCertificateTableModel();
+ defaultSorter = new TableRowSorter<UserCertificateTableModel>(tableModel);
+ }
+
+ public void initialize(String baseFolder) {
+ setIconImage(new ImageIcon(getClass().getResource("/gitblt-favicon.png")).getImage());
+ setTitle("Gitblit Certificate Authority v" + Constants.getVersion() + " (" + Constants.getBuildDate() + ")");
+ setContentPane(getUI());
+ setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+ addWindowListener(new WindowAdapter() {
+ @Override
+ public void windowClosing(WindowEvent event) {
+ saveSizeAndPosition();
+ }
+
+ @Override
+ public void windowOpened(WindowEvent event) {
+ }
+ });
+
+ File folder = new File(baseFolder).getAbsoluteFile();
+ load(folder);
+
+ setSizeAndPosition();
+ }
+
+ private void setSizeAndPosition() {
+ String sz = null;
+ String pos = null;
+ try {
+ StoredConfig config = getConfig();
+ sz = config.getString("ui", null, "size");
+ pos = config.getString("ui", null, "position");
+ defaultDuration = config.getInt("new", "duration", 365);
+ } catch (Throwable t) {
+ t.printStackTrace();
+ }
+
+ // try to restore saved window size
+ if (StringUtils.isEmpty(sz)) {
+ setSize(900, 600);
+ } else {
+ String[] chunks = sz.split("x");
+ int width = Integer.parseInt(chunks[0]);
+ int height = Integer.parseInt(chunks[1]);
+ setSize(width, height);
+ }
+
+ // try to restore saved window position
+ if (StringUtils.isEmpty(pos)) {
+ setLocationRelativeTo(null);
+ } else {
+ String[] chunks = pos.split(",");
+ int x = Integer.parseInt(chunks[0]);
+ int y = Integer.parseInt(chunks[1]);
+ setLocation(x, y);
+ }
+ }
+
+ private void saveSizeAndPosition() {
+ try {
+ // save window size and position
+ StoredConfig config = getConfig();
+ Dimension sz = GitblitAuthority.this.getSize();
+ config.setString("ui", null, "size",
+ MessageFormat.format("{0,number,0}x{1,number,0}", sz.width, sz.height));
+ Point pos = GitblitAuthority.this.getLocationOnScreen();
+ config.setString("ui", null, "position",
+ MessageFormat.format("{0,number,0},{1,number,0}", pos.x, pos.y));
+ config.save();
+ } catch (Throwable t) {
+ Utils.showException(GitblitAuthority.this, t);
+ }
+ }
+
+ private StoredConfig getConfig() throws IOException, ConfigInvalidException {
+ File configFile = new File(folder, X509Utils.CA_CONFIG);
+ FileBasedConfig config = new FileBasedConfig(configFile, FS.detect());
+ config.load();
+ return config;
+ }
+
+ private IUserService loadUsers(File folder) {
+ File file = new File(folder, "gitblit.properties");
+ if (!file.exists()) {
+ return null;
+ }
+ gitblitSettings = new FileSettings(file.getAbsolutePath());
+ mail = new MailExecutor(gitblitSettings);
+ String us = gitblitSettings.getString(Keys.realm.userService, "${baseFolder}/users.conf");
+ String ext = us.substring(us.lastIndexOf(".") + 1).toLowerCase();
+ IUserService service = null;
+ if (!ext.equals("conf") && !ext.equals("properties")) {
+ if (us.equals("com.gitblit.LdapUserService")) {
+ us = gitblitSettings.getString(Keys.realm.ldap.backingUserService, "${baseFolder}/users.conf");
+ } else if (us.equals("com.gitblit.LdapUserService")) {
+ us = gitblitSettings.getString(Keys.realm.redmine.backingUserService, "${baseFolder}/users.conf");
+ }
+ }
+
+ if (us.endsWith(".conf")) {
+ service = new ConfigUserService(FileUtils.resolveParameter(Constants.baseFolder$, folder, us));
+ } else {
+ throw new RuntimeException("Unsupported user service: " + us);
+ }
+
+ service = new ConfigUserService(FileUtils.resolveParameter(Constants.baseFolder$, folder, us));
+ return service;
+ }
+
+ private void load(File folder) {
+ this.folder = folder;
+ this.userService = loadUsers(folder);
+ System.out.println(Constants.baseFolder$ + " set to " + folder);
+ if (userService == null) {
+ JOptionPane.showMessageDialog(this, MessageFormat.format("Sorry, {0} doesn't look like a Gitblit GO installation.", folder));
+ } else {
+ // build empty certificate model for all users
+ Map<String, UserCertificateModel> map = new HashMap<String, UserCertificateModel>();
+ for (String user : userService.getAllUsernames()) {
+ UserModel model = userService.getUserModel(user);
+ UserCertificateModel ucm = new UserCertificateModel(model);
+ map.put(user, ucm);
+ }
+ File certificatesConfigFile = new File(folder, X509Utils.CA_CONFIG);
+ FileBasedConfig config = new FileBasedConfig(certificatesConfigFile, FS.detect());
+ if (certificatesConfigFile.exists()) {
+ try {
+ config.load();
+ // replace user certificate model with actual data
+ List<UserCertificateModel> list = UserCertificateConfig.KEY.parse(config).list;
+ for (UserCertificateModel ucm : list) {
+ ucm.user = userService.getUserModel(ucm.user.username);
+ map.put(ucm.user.username, ucm);
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ } catch (ConfigInvalidException e) {
+ e.printStackTrace();
+ }
+ }
+
+ tableModel.list = new ArrayList<UserCertificateModel>(map.values());
+ Collections.sort(tableModel.list);
+ tableModel.fireTableDataChanged();
+ Utils.packColumns(table, Utils.MARGIN);
+
+ File caKeystore = new File(folder, X509Utils.CA_KEY_STORE);
+ if (!caKeystore.exists()) {
+
+ if (!X509Utils.unlimitedStrength) {
+ // prompt to confirm user understands JCE Standard Strength encryption
+ int res = JOptionPane.showConfirmDialog(GitblitAuthority.this, Translation.get("gb.jceWarning"),
+ Translation.get("gb.warning"), JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
+ if (res != JOptionPane.YES_OPTION) {
+ if (Desktop.isDesktopSupported()) {
+ if (Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
+ try {
+ Desktop.getDesktop().browse(URI.create("http://www.oracle.com/technetwork/java/javase/downloads/index.html"));
+ } catch (IOException e) {
+ }
+ }
+ }
+ System.exit(1);
+ }
+ }
+
+ // show certificate defaults dialog
+ certificateDefaultsButton.doClick();
+
+ // create "localhost" ssl certificate
+ prepareX509Infrastructure();
+ }
+ }
+ }
+
+ private boolean prepareX509Infrastructure() {
+ if (caKeystorePassword == null) {
+ JPasswordField pass = new JPasswordField(10);
+ pass.setText(caKeystorePassword);
+ pass.addAncestorListener(new RequestFocusListener());
+ JPanel panel = new JPanel(new BorderLayout());
+ panel.add(new JLabel(Translation.get("gb.enterKeystorePassword")), BorderLayout.NORTH);
+ panel.add(pass, BorderLayout.CENTER);
+ int result = JOptionPane.showConfirmDialog(GitblitAuthority.this, panel, Translation.get("gb.password"), JOptionPane.OK_CANCEL_OPTION);
+ if (result == JOptionPane.OK_OPTION) {
+ caKeystorePassword = new String(pass.getPassword());
+ } else {
+ return false;
+ }
+ }
+
+ X509Metadata metadata = new X509Metadata("localhost", caKeystorePassword);
+ setMetadataDefaults(metadata);
+ metadata.notAfter = new Date(System.currentTimeMillis() + 10*TimeUtils.ONEYEAR);
+ X509Utils.prepareX509Infrastructure(metadata, folder, this);
+ return true;
+ }
+
+ private List<X509Certificate> findCerts(File folder, String username) {
+ List<X509Certificate> list = new ArrayList<X509Certificate>();
+ File userFolder = new File(folder, X509Utils.CERTS + File.separator + username);
+ if (!userFolder.exists()) {
+ return list;
+ }
+ File [] certs = userFolder.listFiles(new FilenameFilter() {
+ @Override
+ public boolean accept(File dir, String name) {
+ return name.toLowerCase().endsWith(".cer") || name.toLowerCase().endsWith(".crt");
+ }
+ });
+ try {
+ CertificateFactory factory = CertificateFactory.getInstance("X.509");
+ for (File cert : certs) {
+ BufferedInputStream is = new BufferedInputStream(new FileInputStream(cert));
+ X509Certificate x509 = (X509Certificate) factory.generateCertificate(is);
+ is.close();
+ list.add(x509);
+ }
+ } catch (Exception e) {
+ Utils.showException(GitblitAuthority.this, e);
+ }
+ return list;
+ }
+
+ private Container getUI() {
+ userCertificatePanel = new UserCertificatePanel(this) {
+
+ private static final long serialVersionUID = 1L;
+ @Override
+ public Insets getInsets() {
+ return Utils.INSETS;
+ }
+
+ @Override
+ public boolean isAllowEmail() {
+ return mail.isReady();
+ }
+
+ @Override
+ public Date getDefaultExpiration() {
+ Calendar c = Calendar.getInstance();
+ c.add(Calendar.DATE, defaultDuration);
+ c.set(Calendar.HOUR_OF_DAY, 0);
+ c.set(Calendar.MINUTE, 0);
+ c.set(Calendar.SECOND, 0);
+ c.set(Calendar.MILLISECOND, 0);
+ return c.getTime();
+ }
+
+ @Override
+ public boolean saveUser(String username, UserCertificateModel ucm) {
+ return userService.updateUserModel(username, ucm.user);
+ }
+
+ @Override
+ public boolean newCertificate(UserCertificateModel ucm, X509Metadata metadata, boolean sendEmail) {
+ if (!prepareX509Infrastructure()) {
+ return false;
+ }
+
+ Date notAfter = metadata.notAfter;
+ setMetadataDefaults(metadata);
+ metadata.notAfter = notAfter;
+
+ // set user's specified OID values
+ UserModel user = ucm.user;
+ if (!StringUtils.isEmpty(user.organizationalUnit)) {
+ metadata.oids.put("OU", user.organizationalUnit);
+ }
+ if (!StringUtils.isEmpty(user.organization)) {
+ metadata.oids.put("O", user.organization);
+ }
+ if (!StringUtils.isEmpty(user.locality)) {
+ metadata.oids.put("L", user.locality);
+ }
+ if (!StringUtils.isEmpty(user.stateProvince)) {
+ metadata.oids.put("ST", user.stateProvince);
+ }
+ if (!StringUtils.isEmpty(user.countryCode)) {
+ metadata.oids.put("C", user.countryCode);
+ }
+
+ File caKeystoreFile = new File(folder, X509Utils.CA_KEY_STORE);
+ File zip = X509Utils.newClientBundle(metadata, caKeystoreFile, caKeystorePassword, GitblitAuthority.this);
+
+ // save latest expiration date
+ if (ucm.expires == null || metadata.notAfter.before(ucm.expires)) {
+ ucm.expires = metadata.notAfter;
+ }
+
+ updateAuthorityConfig(ucm);
+
+ // refresh user
+ ucm.certs = null;
+ int modelIndex = table.convertRowIndexToModel(table.getSelectedRow());
+ tableModel.fireTableDataChanged();
+ table.getSelectionModel().setSelectionInterval(modelIndex, modelIndex);
+
+ if (sendEmail) {
+ sendEmail(user, metadata, zip);
+ }
+ return true;
+ }
+
+ @Override
+ public boolean revoke(UserCertificateModel ucm, X509Certificate cert, RevocationReason reason) {
+ if (!prepareX509Infrastructure()) {
+ return false;
+ }
+
+ File caRevocationList = new File(folder, X509Utils.CA_REVOCATION_LIST);
+ File caKeystoreFile = new File(folder, X509Utils.CA_KEY_STORE);
+ if (X509Utils.revoke(cert, reason, caRevocationList, caKeystoreFile, caKeystorePassword, GitblitAuthority.this)) {
+ File certificatesConfigFile = new File(folder, X509Utils.CA_CONFIG);
+ FileBasedConfig config = new FileBasedConfig(certificatesConfigFile, FS.detect());
+ if (certificatesConfigFile.exists()) {
+ try {
+ config.load();
+ } catch (Exception e) {
+ Utils.showException(GitblitAuthority.this, e);
+ }
+ }
+ // add serial to revoked list
+ ucm.revoke(cert.getSerialNumber(), reason);
+ ucm.update(config);
+ try {
+ config.save();
+ } catch (Exception e) {
+ Utils.showException(GitblitAuthority.this, e);
+ }
+
+ // refresh user
+ ucm.certs = null;
+ int modelIndex = table.convertRowIndexToModel(table.getSelectedRow());
+ tableModel.fireTableDataChanged();
+ table.getSelectionModel().setSelectionInterval(modelIndex, modelIndex);
+
+ return true;
+ }
+
+ return false;
+ }
+ };
+
+ table = Utils.newTable(tableModel, Utils.DATE_FORMAT);
+ table.setRowSorter(defaultSorter);
+ table.setDefaultRenderer(CertificateStatus.class, new CertificateStatusRenderer());
+ table.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
+
+ @Override
+ public void valueChanged(ListSelectionEvent e) {
+ if (e.getValueIsAdjusting()) {
+ return;
+ }
+ int row = table.getSelectedRow();
+ if (row < 0) {
+ return;
+ }
+ int modelIndex = table.convertRowIndexToModel(row);
+ UserCertificateModel ucm = tableModel.get(modelIndex);
+ if (ucm.certs == null) {
+ ucm.certs = findCerts(folder, ucm.user.username);
+ }
+ userCertificatePanel.setUserCertificateModel(ucm);
+ }
+ });
+
+ JPanel usersPanel = new JPanel(new BorderLayout()) {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public Insets getInsets() {
+ return Utils.INSETS;
+ }
+ };
+ usersPanel.add(new HeaderPanel(Translation.get("gb.users"), "users_16x16.png"), BorderLayout.NORTH);
+ usersPanel.add(new JScrollPane(table), BorderLayout.CENTER);
+ usersPanel.setMinimumSize(new Dimension(400, 10));
+
+ certificateDefaultsButton = new JButton(new ImageIcon(getClass().getResource("/settings_16x16.png")));
+ certificateDefaultsButton.setFocusable(false);
+ certificateDefaultsButton.setToolTipText(Translation.get("gb.newCertificateDefaults"));
+ certificateDefaultsButton.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ X509Metadata metadata = new X509Metadata("whocares", "whocares");
+ File certificatesConfigFile = new File(folder, X509Utils.CA_CONFIG);
+ FileBasedConfig config = new FileBasedConfig(certificatesConfigFile, FS.detect());
+ NewCertificateConfig certificateConfig = null;
+ if (certificatesConfigFile.exists()) {
+ try {
+ config.load();
+ } catch (Exception x) {
+ Utils.showException(GitblitAuthority.this, x);
+ }
+ certificateConfig = NewCertificateConfig.KEY.parse(config);
+ certificateConfig.update(metadata);
+ }
+ InputVerifier verifier = new InputVerifier() {
+ public boolean verify(JComponent comp) {
+ boolean returnValue;
+ JTextField textField = (JTextField) comp;
+ try {
+ Integer.parseInt(textField.getText());
+ returnValue = true;
+ } catch (NumberFormatException e) {
+ returnValue = false;
+ }
+ return returnValue;
+ }
+ };
+
+ JTextField siteNameTF = new JTextField(20);
+ siteNameTF.setText(gitblitSettings.getString(Keys.web.siteName, "Gitblit"));
+ JPanel siteNamePanel = Utils.newFieldPanel(Translation.get("gb.siteName"),
+ siteNameTF, Translation.get("gb.siteNameDescription"));
+
+ JTextField validityTF = new JTextField(4);
+ validityTF.setInputVerifier(verifier);
+ validityTF.setVerifyInputWhenFocusTarget(true);
+ validityTF.setText("" + certificateConfig.duration);
+ JPanel validityPanel = Utils.newFieldPanel(Translation.get("gb.validity"),
+ validityTF, Translation.get("gb.duration.days").replace("{0}", "").trim());
+
+ JPanel p1 = new JPanel(new GridLayout(0, 1, 5, 2));
+ p1.add(siteNamePanel);
+ p1.add(validityPanel);
+
+ DefaultOidsPanel oids = new DefaultOidsPanel(metadata);
+
+ JPanel panel = new JPanel(new BorderLayout());
+ panel.add(p1, BorderLayout.NORTH);
+ panel.add(oids, BorderLayout.CENTER);
+
+ int result = JOptionPane.showConfirmDialog(GitblitAuthority.this,
+ panel, Translation.get("gb.newCertificateDefaults"), JOptionPane.OK_CANCEL_OPTION,
+ JOptionPane.QUESTION_MESSAGE, new ImageIcon(getClass().getResource("/settings_32x32.png")));
+ if (result == JOptionPane.OK_OPTION) {
+ try {
+ oids.update(metadata);
+ certificateConfig.duration = Integer.parseInt(validityTF.getText());
+ certificateConfig.store(config, metadata);
+ config.save();
+
+ Map<String, String> updates = new HashMap<String, String>();
+ updates.put(Keys.web.siteName, siteNameTF.getText());
+ gitblitSettings.saveSettings(updates);
+ } catch (Exception e1) {
+ Utils.showException(GitblitAuthority.this, e1);
+ }
+ }
+ }
+ });
+
+ newSSLCertificate = new JButton(new ImageIcon(getClass().getResource("/rosette_16x16.png")));
+ newSSLCertificate.setFocusable(false);
+ newSSLCertificate.setToolTipText(Translation.get("gb.newSSLCertificate"));
+ newSSLCertificate.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ Date defaultExpiration = new Date(System.currentTimeMillis() + 10*TimeUtils.ONEYEAR);
+ NewSSLCertificateDialog dialog = new NewSSLCertificateDialog(GitblitAuthority.this, defaultExpiration);
+ dialog.setModal(true);
+ dialog.setVisible(true);
+ if (dialog.isCanceled()) {
+ return;
+ }
+ final Date expires = dialog.getExpiration();
+ final String hostname = dialog.getHostname();
+ final boolean serveCertificate = dialog.isServeCertificate();
+
+ AuthorityWorker worker = new AuthorityWorker(GitblitAuthority.this) {
+
+ @Override
+ protected Boolean doRequest() throws IOException {
+ if (!prepareX509Infrastructure()) {
+ return false;
+ }
+
+ // read CA private key and certificate
+ File caKeystoreFile = new File(folder, X509Utils.CA_KEY_STORE);
+ PrivateKey caPrivateKey = X509Utils.getPrivateKey(X509Utils.CA_ALIAS, caKeystoreFile, caKeystorePassword);
+ X509Certificate caCert = X509Utils.getCertificate(X509Utils.CA_ALIAS, caKeystoreFile, caKeystorePassword);
+
+ // generate new SSL certificate
+ X509Metadata metadata = new X509Metadata(hostname, caKeystorePassword);
+ setMetadataDefaults(metadata);
+ metadata.notAfter = expires;
+ File serverKeystoreFile = new File(folder, X509Utils.SERVER_KEY_STORE);
+ X509Certificate cert = X509Utils.newSSLCertificate(metadata, caPrivateKey, caCert, serverKeystoreFile, GitblitAuthority.this);
+ boolean hasCert = cert != null;
+ if (hasCert && serveCertificate) {
+ // update Gitblit https connector alias
+ Map<String, String> updates = new HashMap<String, String>();
+ updates.put(Keys.server.certificateAlias, metadata.commonName);
+ gitblitSettings.saveSettings(updates);
+ }
+ return hasCert;
+ }
+
+ @Override
+ protected void onSuccess() {
+ if (serveCertificate) {
+ JOptionPane.showMessageDialog(GitblitAuthority.this,
+ MessageFormat.format(Translation.get("gb.sslCertificateGeneratedRestart"), hostname),
+ Translation.get("gb.newSSLCertificate"), JOptionPane.INFORMATION_MESSAGE);
+ } else {
+ JOptionPane.showMessageDialog(GitblitAuthority.this,
+ MessageFormat.format(Translation.get("gb.sslCertificateGenerated"), hostname),
+ Translation.get("gb.newSSLCertificate"), JOptionPane.INFORMATION_MESSAGE);
+ }
+ }
+ };
+
+ worker.execute();
+ }
+ });
+
+ JButton emailBundle = new JButton(new ImageIcon(getClass().getResource("/mail_16x16.png")));
+ emailBundle.setFocusable(false);
+ emailBundle.setToolTipText(Translation.get("gb.emailCertificateBundle"));
+ emailBundle.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ int row = table.getSelectedRow();
+ if (row < 0) {
+ return;
+ }
+ int modelIndex = table.convertRowIndexToModel(row);
+ final UserCertificateModel ucm = tableModel.get(modelIndex);
+ if (ArrayUtils.isEmpty(ucm.certs)) {
+ JOptionPane.showMessageDialog(GitblitAuthority.this, MessageFormat.format(Translation.get("gb.pleaseGenerateClientCertificate"), ucm.user.getDisplayName()));
+ }
+ final File zip = new File(folder, X509Utils.CERTS + File.separator + ucm.user.username + File.separator + ucm.user.username + ".zip");
+ if (!zip.exists()) {
+ return;
+ }
+
+ AuthorityWorker worker = new AuthorityWorker(GitblitAuthority.this) {
+ @Override
+ protected Boolean doRequest() throws IOException {
+ X509Metadata metadata = new X509Metadata(ucm.user.username, "whocares");
+ metadata.serverHostname = gitblitSettings.getString(Keys.web.siteName, Constants.NAME);
+ if (StringUtils.isEmpty(metadata.serverHostname)) {
+ metadata.serverHostname = Constants.NAME;
+ }
+ metadata.userDisplayname = ucm.user.getDisplayName();
+ return sendEmail(ucm.user, metadata, zip);
+ }
+
+ @Override
+ protected void onSuccess() {
+ JOptionPane.showMessageDialog(GitblitAuthority.this, MessageFormat.format(Translation.get("gb.clientCertificateBundleSent"),
+ ucm.user.getDisplayName()));
+ }
+
+ };
+ worker.execute();
+ }
+ });
+
+ JButton logButton = new JButton(new ImageIcon(getClass().getResource("/script_16x16.png")));
+ logButton.setFocusable(false);
+ logButton.setToolTipText(Translation.get("gb.log"));
+ logButton.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ File log = new File(folder, X509Utils.CERTS + File.separator + "log.txt");
+ if (log.exists()) {
+ String content = FileUtils.readContent(log, "\n");
+ JTextArea textarea = new JTextArea(content);
+ JScrollPane scrollPane = new JScrollPane(textarea);
+ scrollPane.setPreferredSize(new Dimension(700, 400));
+ JOptionPane.showMessageDialog(GitblitAuthority.this, scrollPane, log.getAbsolutePath(), JOptionPane.INFORMATION_MESSAGE);
+ }
+ }
+ });
+
+ final JTextField filterTextfield = new JTextField(15);
+ filterTextfield.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ filterUsers(filterTextfield.getText());
+ }
+ });
+ filterTextfield.addKeyListener(new KeyAdapter() {
+ public void keyReleased(KeyEvent e) {
+ filterUsers(filterTextfield.getText());
+ }
+ });
+
+ JToolBar buttonControls = new JToolBar(JToolBar.HORIZONTAL);
+ buttonControls.setFloatable(false);
+ buttonControls.add(certificateDefaultsButton);
+ buttonControls.add(newSSLCertificate);
+ buttonControls.add(emailBundle);
+ buttonControls.add(logButton);
+
+ JPanel userControls = new JPanel(new FlowLayout(FlowLayout.RIGHT, Utils.MARGIN, Utils.MARGIN));
+ userControls.add(new JLabel(Translation.get("gb.filter")));
+ userControls.add(filterTextfield);
+
+ JPanel topPanel = new JPanel(new BorderLayout(0, 0));
+ topPanel.add(buttonControls, BorderLayout.WEST);
+ topPanel.add(userControls, BorderLayout.EAST);
+
+ JPanel leftPanel = new JPanel(new BorderLayout());
+ leftPanel.add(topPanel, BorderLayout.NORTH);
+ leftPanel.add(usersPanel, BorderLayout.CENTER);
+
+ userCertificatePanel.setMinimumSize(new Dimension(375, 10));
+
+ JLabel statusLabel = new JLabel();
+ statusLabel.setHorizontalAlignment(SwingConstants.RIGHT);
+ if (X509Utils.unlimitedStrength) {
+ statusLabel.setText("JCE Unlimited Strength Jurisdiction Policy");
+ } else {
+ statusLabel.setText("JCE Standard Encryption Policy");
+ }
+
+ JPanel root = new JPanel(new BorderLayout()) {
+ private static final long serialVersionUID = 1L;
+ public Insets getInsets() {
+ return Utils.INSETS;
+ }
+ };
+ JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftPanel, userCertificatePanel);
+ splitPane.setDividerLocation(1d);
+ root.add(splitPane, BorderLayout.CENTER);
+ root.add(statusLabel, BorderLayout.SOUTH);
+ return root;
+ }
+
+ private void filterUsers(final String fragment) {
+ if (StringUtils.isEmpty(fragment)) {
+ table.setRowSorter(defaultSorter);
+ return;
+ }
+ RowFilter<UserCertificateTableModel, Object> containsFilter = new RowFilter<UserCertificateTableModel, Object>() {
+ public boolean include(Entry<? extends UserCertificateTableModel, ? extends Object> entry) {
+ for (int i = entry.getValueCount() - 1; i >= 0; i--) {
+ if (entry.getStringValue(i).toLowerCase().contains(fragment.toLowerCase())) {
+ return true;
+ }
+ }
+ return false;
+ }
+ };
+ TableRowSorter<UserCertificateTableModel> sorter = new TableRowSorter<UserCertificateTableModel>(
+ tableModel);
+ sorter.setRowFilter(containsFilter);
+ table.setRowSorter(sorter);
+ }
+
+ @Override
+ public void log(String message) {
+ BufferedWriter writer = null;
+ try {
+ writer = new BufferedWriter(new FileWriter(new File(folder, X509Utils.CERTS + File.separator + "log.txt"), true));
+ writer.write(MessageFormat.format("{0,date,yyyy-MM-dd HH:mm}: {1}", new Date(), message));
+ writer.newLine();
+ writer.flush();
+ } catch (Exception e) {
+ LoggerFactory.getLogger(GitblitAuthority.class).error("Failed to append log entry!", e);
+ } finally {
+ if (writer != null) {
+ try {
+ writer.close();
+ } catch (IOException e) {
+ }
+ }
+ }
+ }
+
+ private boolean sendEmail(UserModel user, X509Metadata metadata, File zip) {
+ // send email
+ try {
+ if (mail.isReady()) {
+ Message message = mail.createMessage(user.emailAddress);
+ message.setSubject("Your Gitblit client certificate for " + metadata.serverHostname);
+
+ // body of email
+ String body = X509Utils.processTemplate(new File(folder, X509Utils.CERTS + File.separator + "mail.tmpl"), metadata);
+ if (StringUtils.isEmpty(body)) {
+ body = MessageFormat.format("Hi {0}\n\nHere is your client certificate bundle.\nInside the zip file are installation instructions.", user.getDisplayName());
+ }
+ Multipart mp = new MimeMultipart();
+ MimeBodyPart messagePart = new MimeBodyPart();
+ messagePart.setText(body);
+ mp.addBodyPart(messagePart);
+
+ // attach zip
+ MimeBodyPart filePart = new MimeBodyPart();
+ FileDataSource fds = new FileDataSource(zip);
+ filePart.setDataHandler(new DataHandler(fds));
+ filePart.setFileName(fds.getName());
+ mp.addBodyPart(filePart);
+
+ message.setContent(mp);
+
+ mail.sendNow(message);
+ return true;
+ } else {
+ JOptionPane.showMessageDialog(GitblitAuthority.this, "Sorry, the mail server settings are not configured properly.\nCan not send email.", Translation.get("gb.error"), JOptionPane.ERROR_MESSAGE);
+ }
+ } catch (Exception e) {
+ Utils.showException(GitblitAuthority.this, e);
+ }
+ return false;
+ }
+
+ private void setMetadataDefaults(X509Metadata metadata) {
+ metadata.serverHostname = gitblitSettings.getString(Keys.web.siteName, Constants.NAME);
+ if (StringUtils.isEmpty(metadata.serverHostname)) {
+ metadata.serverHostname = Constants.NAME;
+ }
+
+ // set default values from config file
+ File certificatesConfigFile = new File(folder, X509Utils.CA_CONFIG);
+ FileBasedConfig config = new FileBasedConfig(certificatesConfigFile, FS.detect());
+ if (certificatesConfigFile.exists()) {
+ try {
+ config.load();
+ } catch (Exception e) {
+ Utils.showException(GitblitAuthority.this, e);
+ }
+ NewCertificateConfig certificateConfig = NewCertificateConfig.KEY.parse(config);
+ certificateConfig.update(metadata);
+ }
+ }
+
+ private void updateAuthorityConfig(UserCertificateModel ucm) {
+ File certificatesConfigFile = new File(folder, X509Utils.CA_CONFIG);
+ FileBasedConfig config = new FileBasedConfig(certificatesConfigFile, FS.detect());
+ if (certificatesConfigFile.exists()) {
+ try {
+ config.load();
+ } catch (Exception e) {
+ Utils.showException(GitblitAuthority.this, e);
+ }
+ }
+ ucm.update(config);
+ try {
+ config.save();
+ } catch (Exception e) {
+ Utils.showException(GitblitAuthority.this, e);
+ }
+ }
+}
diff --git a/src/main/java/com/gitblit/authority/Launcher.java b/src/main/java/com/gitblit/authority/Launcher.java
new file mode 100644
index 00000000..1da97149
--- /dev/null
+++ b/src/main/java/com/gitblit/authority/Launcher.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright 2012 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.authority;
+
+import java.awt.Color;
+import java.awt.EventQueue;
+import java.awt.FontMetrics;
+import java.awt.Graphics2D;
+import java.awt.SplashScreen;
+import java.io.File;
+import java.io.FileFilter;
+import java.io.IOException;
+import java.lang.reflect.Method;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import com.gitblit.Constants;
+import com.gitblit.client.Translation;
+
+/**
+ * Downloads dependencies and launches Gitblit Authority.
+ *
+ * @author James Moger
+ *
+ */
+public class Launcher {
+
+ public static final boolean DEBUG = false;
+
+ /**
+ * Parameters of the method to add an URL to the System classes.
+ */
+ private static final Class<?>[] PARAMETERS = new Class[] { URL.class };
+
+
+ public static void main(String[] args) {
+ final SplashScreen splash = SplashScreen.getSplashScreen();
+
+ File libFolder = new File("ext");
+ List<File> jars = findJars(libFolder.getAbsoluteFile());
+
+ // sort the jars by name and then reverse the order so the newer version
+ // of the library gets loaded in the event that this is an upgrade
+ Collections.sort(jars);
+ Collections.reverse(jars);
+ for (File jar : jars) {
+ try {
+ updateSplash(splash, Translation.get("gb.loading") + " " + jar.getName() + "...");
+ addJarFile(jar);
+ } catch (IOException e) {
+
+ }
+ }
+
+ updateSplash(splash, Translation.get("gb.starting") + " Gitblit Authority...");
+ GitblitAuthority.main(args);
+ }
+
+ private static void updateSplash(final SplashScreen splash, final String string) {
+ if (splash == null) {
+ return;
+ }
+ try {
+ EventQueue.invokeAndWait(new Runnable() {
+ public void run() {
+ Graphics2D g = splash.createGraphics();
+ if (g != null) {
+ // Splash is 320x120
+ FontMetrics fm = g.getFontMetrics();
+
+ // paint startup status
+ g.setColor(Color.darkGray);
+ int h = fm.getHeight() + fm.getMaxDescent();
+ int x = 5;
+ int y = 115;
+ int w = 320 - 2 * x;
+ g.fillRect(x, y - h, w, h);
+ g.setColor(Color.lightGray);
+ g.drawRect(x, y - h, w, h);
+ g.setColor(Color.WHITE);
+ int xw = fm.stringWidth(string);
+ g.drawString(string, x + ((w - xw) / 2), y - 5);
+
+ // paint version
+ String ver = "v" + Constants.getVersion();
+ int vw = g.getFontMetrics().stringWidth(ver);
+ g.drawString(ver, 320 - vw - 5, 34);
+ g.dispose();
+ splash.update();
+ }
+ }
+ });
+ } catch (Throwable t) {
+ t.printStackTrace();
+ }
+ }
+
+ public static List<File> findJars(File folder) {
+ List<File> jars = new ArrayList<File>();
+ if (folder.exists()) {
+ File[] libs = folder.listFiles(new FileFilter() {
+ @Override
+ public boolean accept(File file) {
+ return !file.isDirectory() && file.getName().toLowerCase().endsWith(".jar");
+ }
+ });
+ if (libs != null && libs.length > 0) {
+ jars.addAll(Arrays.asList(libs));
+ if (DEBUG) {
+ for (File jar : jars) {
+ System.out.println("found " + jar);
+ }
+ }
+ }
+ }
+
+ return jars;
+ }
+
+ /**
+ * Adds a file to the classpath
+ *
+ * @param f
+ * the file to be added
+ * @throws IOException
+ */
+ public static void addJarFile(File f) throws IOException {
+ if (f.getName().indexOf("-sources") > -1 || f.getName().indexOf("-javadoc") > -1) {
+ // don't add source or javadoc jars to runtime classpath
+ return;
+ }
+ URL u = f.toURI().toURL();
+ if (DEBUG) {
+ System.out.println("load=" + u.toExternalForm());
+ }
+ URLClassLoader sysloader = (URLClassLoader) ClassLoader.getSystemClassLoader();
+ Class<?> sysclass = URLClassLoader.class;
+ try {
+ Method method = sysclass.getDeclaredMethod("addURL", PARAMETERS);
+ method.setAccessible(true);
+ method.invoke(sysloader, new Object[] { u });
+ } catch (Throwable t) {
+ throw new IOException(MessageFormat.format(
+ "Error, could not add {0} to system classloader", f.getPath()), t);
+ }
+ }
+}
diff --git a/src/main/java/com/gitblit/authority/NewCertificateConfig.java b/src/main/java/com/gitblit/authority/NewCertificateConfig.java
new file mode 100644
index 00000000..ca047c82
--- /dev/null
+++ b/src/main/java/com/gitblit/authority/NewCertificateConfig.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2012 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.authority;
+
+import java.util.Date;
+
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Config.SectionParser;
+
+import com.gitblit.utils.StringUtils;
+import com.gitblit.utils.TimeUtils;
+import com.gitblit.utils.X509Utils.X509Metadata;
+
+/**
+ * Certificate config file parser.
+ *
+ * @author James Moger
+ */
+public class NewCertificateConfig {
+ public static final SectionParser<NewCertificateConfig> KEY = new SectionParser<NewCertificateConfig>() {
+ public NewCertificateConfig parse(final Config cfg) {
+ return new NewCertificateConfig(cfg);
+ }
+ };
+
+ public String OU;
+ public String O;
+ public String L;
+ public String ST;
+ public String C;
+
+ public int duration;
+
+ private NewCertificateConfig(final Config c) {
+ duration = c.getInt("new", null, "duration", 0);
+ OU = c.getString("new", null, "organizationalUnit");
+ O = c.getString("new", null, "organization");
+ L = c.getString("new", null, "locality");
+ ST = c.getString("new", null, "stateProvince");
+ C = c.getString("new", null, "countryCode");
+ }
+
+ public void update(X509Metadata metadata) {
+ update(metadata, "OU", OU);
+ update(metadata, "O", O);
+ update(metadata, "L", L);
+ update(metadata, "ST", ST);
+ update(metadata, "C", C);
+ if (duration > 0) {
+ metadata.notAfter = new Date(System.currentTimeMillis() + duration*TimeUtils.ONEDAY);
+ }
+ }
+
+ private void update(X509Metadata metadata, String oid, String value) {
+ if (!StringUtils.isEmpty(value)) {
+ metadata.oids.put(oid, value);
+ }
+ }
+
+ public void store(Config c, X509Metadata metadata) {
+ store(c, "new", "organizationalUnit", metadata.getOID("OU", null));
+ store(c, "new", "organization", metadata.getOID("O", null));
+ store(c, "new", "locality", metadata.getOID("L", null));
+ store(c, "new", "stateProvince", metadata.getOID("ST", null));
+ store(c, "new", "countryCode", metadata.getOID("C", null));
+ if (duration <= 0) {
+ c.unset("new", null, "duration");
+ } else {
+ c.setInt("new", null, "duration", duration);
+ }
+ }
+
+ private void store(Config c, String section, String name, String value) {
+ if (StringUtils.isEmpty(value)) {
+ c.unset(section, null, name);
+ } else {
+ c.setString(section, null, name, value);
+ }
+ }
+ } \ No newline at end of file
diff --git a/src/main/java/com/gitblit/authority/NewClientCertificateDialog.java b/src/main/java/com/gitblit/authority/NewClientCertificateDialog.java
new file mode 100644
index 00000000..3d214390
--- /dev/null
+++ b/src/main/java/com/gitblit/authority/NewClientCertificateDialog.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright 2012 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.authority;
+
+import java.awt.BorderLayout;
+import java.awt.Dimension;
+import java.awt.Frame;
+import java.awt.GridLayout;
+import java.awt.Insets;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.util.Date;
+
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JDialog;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JPasswordField;
+import javax.swing.JScrollPane;
+import javax.swing.JTextArea;
+import javax.swing.JTextField;
+
+import org.bouncycastle.util.Arrays;
+
+import com.gitblit.client.HeaderPanel;
+import com.gitblit.client.Translation;
+import com.gitblit.utils.StringUtils;
+import com.toedter.calendar.JDateChooser;
+
+public class NewClientCertificateDialog extends JDialog {
+
+ private static final long serialVersionUID = 1L;
+
+ JDateChooser expirationDate;
+ JPasswordField pw1;
+ JPasswordField pw2;
+ JTextField hint;
+ JCheckBox sendEmail;
+ boolean isCanceled = true;
+
+ public NewClientCertificateDialog(Frame owner, String displayname, Date defaultExpiration, boolean allowEmail) {
+ super(owner);
+
+ setTitle(Translation.get("gb.newCertificate"));
+
+ JPanel content = new JPanel(new BorderLayout(Utils.MARGIN, Utils.MARGIN)) {
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public Insets getInsets() {
+
+ return Utils.INSETS;
+ }
+ };
+
+ expirationDate = new JDateChooser(defaultExpiration);
+ pw1 = new JPasswordField(20);
+ pw2 = new JPasswordField(20);
+ hint = new JTextField(20);
+ sendEmail = new JCheckBox(Translation.get("gb.sendEmail"));
+
+ JPanel panel = new JPanel(new GridLayout(0, 2, Utils.MARGIN, Utils.MARGIN));
+
+ panel.add(new JLabel(Translation.get("gb.expires")));
+ panel.add(expirationDate);
+
+ panel.add(new JLabel(Translation.get("gb.password")));
+ panel.add(pw1);
+
+ panel.add(new JLabel(Translation.get("gb.confirmPassword")));
+ panel.add(pw2);
+
+ panel.add(new JLabel(Translation.get("gb.passwordHint")));
+ panel.add(hint);
+
+ if (allowEmail) {
+ panel.add(new JLabel(""));
+ panel.add(sendEmail);
+ }
+
+
+ JButton ok = new JButton(Translation.get("gb.ok"));
+ ok.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ if (validateInputs()) {
+ isCanceled = false;
+ setVisible(false);
+ }
+ }
+ });
+ JButton cancel = new JButton(Translation.get("gb.cancel"));
+ cancel.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ isCanceled = true;
+ setVisible(false);
+ }
+ });
+
+ JPanel controls = new JPanel();
+ controls.add(ok);
+ controls.add(cancel);
+
+ JTextArea message = new JTextArea(Translation.get("gb.newClientCertificateMessage"));
+ message.setLineWrap(true);
+ message.setWrapStyleWord(true);
+ message.setEditable(false);
+ message.setRows(6);
+ message.setPreferredSize(new Dimension(300, 100));
+
+ content.add(new JScrollPane(message), BorderLayout.CENTER);
+ content.add(panel, BorderLayout.NORTH);
+ content.add(controls, BorderLayout.SOUTH);
+
+ getContentPane().add(new HeaderPanel(Translation.get("gb.newCertificate") + " (" + displayname + ")", "rosette_16x16.png"), BorderLayout.NORTH);
+ getContentPane().add(content, BorderLayout.CENTER);
+ pack();
+
+ setLocationRelativeTo(owner);
+ }
+
+ private boolean validateInputs() {
+ if (getExpiration().getTime() < System.currentTimeMillis()) {
+ // expires before now
+ JOptionPane.showMessageDialog(this, Translation.get("gb.invalidExpirationDate"),
+ Translation.get("gb.error"), JOptionPane.ERROR_MESSAGE);
+ return false;
+ }
+ if (pw1.getPassword().length == 0 || !Arrays.areEqual(pw1.getPassword(), pw2.getPassword())) {
+ // password mismatch
+ JOptionPane.showMessageDialog(this, Translation.get("gb.passwordsDoNotMatch"),
+ Translation.get("gb.error"), JOptionPane.ERROR_MESSAGE);
+ return false;
+ }
+ if (StringUtils.isEmpty(getPasswordHint())) {
+ // must have hint
+ JOptionPane.showMessageDialog(this, Translation.get("gb.passwordHintRequired"),
+ Translation.get("gb.error"), JOptionPane.ERROR_MESSAGE);
+ return false;
+ }
+ return true;
+ }
+
+ public String getPassword() {
+ return new String(pw1.getPassword());
+ }
+
+ public String getPasswordHint() {
+ return hint.getText();
+ }
+
+ public Date getExpiration() {
+ return expirationDate.getDate();
+ }
+
+ public boolean sendEmail() {
+ return sendEmail.isSelected();
+ }
+
+ public boolean isCanceled() {
+ return isCanceled;
+ }
+}
diff --git a/src/main/java/com/gitblit/authority/NewSSLCertificateDialog.java b/src/main/java/com/gitblit/authority/NewSSLCertificateDialog.java
new file mode 100644
index 00000000..821e9e9f
--- /dev/null
+++ b/src/main/java/com/gitblit/authority/NewSSLCertificateDialog.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright 2012 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.authority;
+
+import java.awt.BorderLayout;
+import java.awt.Frame;
+import java.awt.GridLayout;
+import java.awt.Insets;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.util.Date;
+
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JDialog;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JTextField;
+
+import com.gitblit.client.HeaderPanel;
+import com.gitblit.client.Translation;
+import com.gitblit.utils.StringUtils;
+import com.toedter.calendar.JDateChooser;
+
+public class NewSSLCertificateDialog extends JDialog {
+
+ private static final long serialVersionUID = 1L;
+
+ JDateChooser expirationDate;
+ JTextField hostname;
+ JCheckBox serveCertificate;
+ boolean isCanceled = true;
+
+ public NewSSLCertificateDialog(Frame owner, Date defaultExpiration) {
+ super(owner);
+
+ setTitle(Translation.get("gb.newSSLCertificate"));
+
+ JPanel content = new JPanel(new BorderLayout(Utils.MARGIN, Utils.MARGIN)) {
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public Insets getInsets() {
+
+ return Utils.INSETS;
+ }
+ };
+
+ expirationDate = new JDateChooser(defaultExpiration);
+ hostname = new JTextField(20);
+ serveCertificate = new JCheckBox(Translation.get("gb.serveCertificate"), true);
+
+ JPanel panel = new JPanel(new GridLayout(0, 2, Utils.MARGIN, Utils.MARGIN));
+
+ panel.add(new JLabel(Translation.get("gb.hostname")));
+ panel.add(hostname);
+
+ panel.add(new JLabel(Translation.get("gb.expires")));
+ panel.add(expirationDate);
+
+ panel.add(new JLabel(""));
+ panel.add(serveCertificate);
+
+ JButton ok = new JButton(Translation.get("gb.ok"));
+ ok.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ if (validateInputs()) {
+ isCanceled = false;
+ setVisible(false);
+ }
+ }
+ });
+ JButton cancel = new JButton(Translation.get("gb.cancel"));
+ cancel.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ isCanceled = true;
+ setVisible(false);
+ }
+ });
+
+ JPanel controls = new JPanel();
+ controls.add(ok);
+ controls.add(cancel);
+
+ content.add(panel, BorderLayout.CENTER);
+ content.add(controls, BorderLayout.SOUTH);
+
+ getContentPane().add(new HeaderPanel(Translation.get("gb.newSSLCertificate"), "rosette_16x16.png"), BorderLayout.NORTH);
+ getContentPane().add(content, BorderLayout.CENTER);
+ pack();
+
+ setLocationRelativeTo(owner);
+ }
+
+ private boolean validateInputs() {
+ if (getExpiration().getTime() < System.currentTimeMillis()) {
+ // expires before now
+ JOptionPane.showMessageDialog(this, Translation.get("gb.invalidExpirationDate"),
+ Translation.get("gb.error"), JOptionPane.ERROR_MESSAGE);
+ return false;
+ }
+ if (StringUtils.isEmpty(getHostname())) {
+ // must have hostname
+ JOptionPane.showMessageDialog(this, Translation.get("gb.hostnameRequired"),
+ Translation.get("gb.error"), JOptionPane.ERROR_MESSAGE);
+ return false;
+ }
+ return true;
+ }
+
+ public String getHostname() {
+ return hostname.getText();
+ }
+
+ public Date getExpiration() {
+ return expirationDate.getDate();
+ }
+
+ public boolean isServeCertificate() {
+ return serveCertificate.isSelected();
+ }
+
+ public boolean isCanceled() {
+ return isCanceled;
+ }
+}
diff --git a/src/main/java/com/gitblit/authority/RequestFocusListener.java b/src/main/java/com/gitblit/authority/RequestFocusListener.java
new file mode 100644
index 00000000..e9368686
--- /dev/null
+++ b/src/main/java/com/gitblit/authority/RequestFocusListener.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2012 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.authority;
+import javax.swing.*;
+import javax.swing.event.*;
+
+/**
+ * Convenience class to request focus on a component.
+ *
+ * When the component is added to a realized Window then component will
+ * request focus immediately, since the ancestorAdded event is fired
+ * immediately.
+ *
+ * When the component is added to a non realized Window, then the focus
+ * request will be made once the window is realized, since the
+ * ancestorAdded event will not be fired until then.
+ *
+ * Using the default constructor will cause the listener to be removed
+ * from the component once the AncestorEvent is generated. A second constructor
+ * allows you to specify a boolean value of false to prevent the
+ * AncestorListener from being removed when the event is generated. This will
+ * allow you to reuse the listener each time the event is generated.
+ *
+ * @author Rob Camick
+ */
+public class RequestFocusListener implements AncestorListener
+{
+ private boolean removeListener;
+
+ /*
+ * Convenience constructor. The listener is only used once and then it is
+ * removed from the component.
+ */
+ public RequestFocusListener()
+ {
+ this(true);
+ }
+
+ /*
+ * Constructor that controls whether this listen can be used once or
+ * multiple times.
+ *
+ * @param removeListener when true this listener is only invoked once
+ * otherwise it can be invoked multiple times.
+ */
+ public RequestFocusListener(boolean removeListener)
+ {
+ this.removeListener = removeListener;
+ }
+
+ @Override
+ public void ancestorAdded(AncestorEvent e)
+ {
+ JComponent component = e.getComponent();
+ component.requestFocusInWindow();
+
+ if (removeListener)
+ component.removeAncestorListener( this );
+ }
+
+ @Override
+ public void ancestorMoved(AncestorEvent e) {}
+
+ @Override
+ public void ancestorRemoved(AncestorEvent e) {}
+}
diff --git a/src/main/java/com/gitblit/authority/UserCertificateConfig.java b/src/main/java/com/gitblit/authority/UserCertificateConfig.java
new file mode 100644
index 00000000..5ec76f77
--- /dev/null
+++ b/src/main/java/com/gitblit/authority/UserCertificateConfig.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2012 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.authority;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Config.SectionParser;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.Constants;
+import com.gitblit.models.UserModel;
+
+/**
+ * User certificate config section parser.
+ *
+ * @author James Moger
+ */
+public class UserCertificateConfig {
+ public static final SectionParser<UserCertificateConfig> KEY = new SectionParser<UserCertificateConfig>() {
+ public UserCertificateConfig parse(final Config cfg) {
+ return new UserCertificateConfig(cfg);
+ }
+ };
+
+ public final List<UserCertificateModel> list;
+
+ private UserCertificateConfig(final Config c) {
+ SimpleDateFormat df = new SimpleDateFormat(Constants.ISO8601);
+ list = new ArrayList<UserCertificateModel>();
+ for (String username : c.getSubsections("user")) {
+ UserCertificateModel uc = new UserCertificateModel(new UserModel(username));
+ try {
+ uc.expires = df.parse(c.getString("user", username, "expires"));
+ } catch (ParseException e) {
+ LoggerFactory.getLogger(UserCertificateConfig.class).error("Failed to parse date!", e);
+ } catch (NullPointerException e) {
+ }
+ uc.notes = c.getString("user", username, "notes");
+ uc.revoked = new ArrayList<String>(Arrays.asList(c.getStringList("user", username, "revoked")));
+ list.add(uc);
+ }
+ }
+
+ public UserCertificateModel getUserCertificateModel(String username) {
+ for (UserCertificateModel ucm : list) {
+ if (ucm.user.username.equalsIgnoreCase(username)) {
+ return ucm;
+ }
+ }
+ return null;
+ }
+} \ No newline at end of file
diff --git a/src/main/java/com/gitblit/authority/UserCertificateModel.java b/src/main/java/com/gitblit/authority/UserCertificateModel.java
new file mode 100644
index 00000000..6c69a93b
--- /dev/null
+++ b/src/main/java/com/gitblit/authority/UserCertificateModel.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2012 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.authority;
+
+import java.math.BigInteger;
+import java.security.cert.X509Certificate;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+import org.eclipse.jgit.lib.Config;
+
+import com.gitblit.Constants;
+import com.gitblit.models.UserModel;
+import com.gitblit.utils.ArrayUtils;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.utils.TimeUtils;
+import com.gitblit.utils.X509Utils.RevocationReason;
+
+public class UserCertificateModel implements Comparable<UserCertificateModel> {
+ public UserModel user;
+ public Date expires;
+ public List<X509Certificate> certs;
+ public List<String> revoked;
+ public String notes;
+
+ public UserCertificateModel(UserModel user) {
+ this.user = user;
+ }
+
+ public void update(Config config) {
+ if (expires == null) {
+ config.unset("user", user.username, "expires");
+ } else {
+ SimpleDateFormat df = new SimpleDateFormat(Constants.ISO8601);
+ config.setString("user", user.username, "expires", df.format(expires));
+ }
+ if (StringUtils.isEmpty(notes)) {
+ config.unset("user", user.username, "notes");
+ } else {
+ config.setString("user", user.username, "notes", notes);
+ }
+ if (ArrayUtils.isEmpty(revoked)) {
+ config.unset("user", user.username, "revoked");
+ } else {
+ config.setStringList("user", user.username, "revoked", revoked);
+ }
+ }
+
+ @Override
+ public int compareTo(UserCertificateModel o) {
+ return user.compareTo(o.user);
+ }
+
+ public void revoke(BigInteger serial, RevocationReason reason) {
+ if (revoked == null) {
+ revoked = new ArrayList<String>();
+ }
+ revoked.add(serial.toString() + ":" + reason.ordinal());
+ expires = null;
+ for (X509Certificate cert : certs) {
+ if (!isRevoked(cert.getSerialNumber())) {
+ if (!isExpired(cert.getNotAfter())) {
+ if (expires == null || cert.getNotAfter().after(expires)) {
+ expires = cert.getNotAfter();
+ }
+ }
+ }
+ }
+ }
+
+ public boolean isRevoked(BigInteger serial) {
+ return isRevoked(serial.toString());
+ }
+
+ public boolean isRevoked(String serial) {
+ if (ArrayUtils.isEmpty(revoked)) {
+ return false;
+ }
+ String sn = serial + ":";
+ for (String s : revoked) {
+ if (s.startsWith(sn)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public RevocationReason getRevocationReason(BigInteger serial) {
+ try {
+ String sn = serial + ":";
+ for (String s : revoked) {
+ if (s.startsWith(sn)) {
+ String r = s.substring(sn.length());
+ int i = Integer.parseInt(r);
+ return RevocationReason.values()[i];
+ }
+ }
+ } catch (Exception e) {
+ }
+ return RevocationReason.unspecified;
+ }
+
+ public CertificateStatus getStatus() {
+ if (expires == null) {
+ return CertificateStatus.unknown;
+ } else if (isExpired(expires)) {
+ return CertificateStatus.expired;
+ } else if (isExpiring(expires)) {
+ return CertificateStatus.expiring;
+ }
+ return CertificateStatus.ok;
+ }
+
+ public boolean hasExpired() {
+ return expires != null && isExpiring(expires);
+ }
+
+ public CertificateStatus getStatus(X509Certificate cert) {
+ if (isRevoked(cert.getSerialNumber())) {
+ return CertificateStatus.revoked;
+ } else if (isExpired(cert.getNotAfter())) {
+ return CertificateStatus.expired;
+ } else if (isExpiring(cert.getNotAfter())) {
+ return CertificateStatus.expiring;
+ }
+ return CertificateStatus.ok;
+ }
+
+ private boolean isExpiring(Date date) {
+ return (date.getTime() - System.currentTimeMillis()) <= TimeUtils.ONEDAY * 30;
+ }
+
+ private boolean isExpired(Date date) {
+ return date.getTime() < System.currentTimeMillis();
+ }
+ } \ No newline at end of file
diff --git a/src/main/java/com/gitblit/authority/UserCertificatePanel.java b/src/main/java/com/gitblit/authority/UserCertificatePanel.java
new file mode 100644
index 00000000..0c49252c
--- /dev/null
+++ b/src/main/java/com/gitblit/authority/UserCertificatePanel.java
@@ -0,0 +1,298 @@
+/*
+ * Copyright 2012 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.authority;
+
+import java.awt.BorderLayout;
+import java.awt.FlowLayout;
+import java.awt.Frame;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.io.IOException;
+import java.security.cert.X509Certificate;
+import java.text.MessageFormat;
+import java.util.Date;
+
+import javax.swing.ImageIcon;
+import javax.swing.JButton;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTable;
+import javax.swing.event.ListSelectionEvent;
+import javax.swing.event.ListSelectionListener;
+import javax.swing.table.TableRowSorter;
+
+import com.gitblit.client.HeaderPanel;
+import com.gitblit.client.Translation;
+import com.gitblit.models.UserModel;
+import com.gitblit.utils.X509Utils.RevocationReason;
+import com.gitblit.utils.X509Utils.X509Metadata;
+
+public abstract class UserCertificatePanel extends JPanel {
+
+ private static final long serialVersionUID = 1L;
+
+ private Frame owner;
+
+ private UserCertificateModel ucm;
+
+ private UserOidsPanel oidsPanel;
+
+ private CertificatesTableModel tableModel;
+
+ private JButton saveUserButton;
+
+ private JButton editUserButton;
+
+ private JButton newCertificateButton;
+
+ private JButton revokeCertificateButton;
+
+ private JTable table;
+
+ public UserCertificatePanel(Frame owner) {
+ super(new BorderLayout());
+
+ this.owner = owner;
+ oidsPanel = new UserOidsPanel();
+
+ JPanel fp = new JPanel(new BorderLayout(Utils.MARGIN, Utils.MARGIN));
+ fp.add(oidsPanel, BorderLayout.NORTH);
+
+ JPanel fieldsPanel = new JPanel(new BorderLayout());
+ fieldsPanel.add(new HeaderPanel(Translation.get("gb.properties"), "vcard_16x16.png"), BorderLayout.NORTH);
+ fieldsPanel.add(fp, BorderLayout.CENTER);
+
+ saveUserButton = new JButton(Translation.get("gb.save"));
+ saveUserButton.setEnabled(false);
+ saveUserButton.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ setEditable(false);
+ String username = ucm.user.username;
+ oidsPanel.updateUser(ucm);
+ saveUser(username, ucm);
+ }
+ });
+
+ editUserButton = new JButton(Translation.get("gb.edit"));
+ editUserButton.setEnabled(false);
+ editUserButton.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ setEditable(true);
+ }
+ });
+
+ JPanel userControls = new JPanel(new FlowLayout(FlowLayout.LEFT));
+ userControls.add(editUserButton);
+ userControls.add(saveUserButton);
+ fieldsPanel.add(userControls, BorderLayout.SOUTH);
+
+ JPanel certificatesPanel = new JPanel(new BorderLayout());
+ certificatesPanel.add(new HeaderPanel(Translation.get("gb.certificates"), "rosette_16x16.png"), BorderLayout.NORTH);
+ tableModel = new CertificatesTableModel();
+ table = Utils.newTable(tableModel, Utils.DATE_FORMAT);
+ table.setRowSorter(new TableRowSorter<CertificatesTableModel>(tableModel));
+ table.setDefaultRenderer(CertificateStatus.class, new CertificateStatusRenderer());
+ table.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
+ @Override
+ public void valueChanged(ListSelectionEvent e) {
+ if (e.getValueIsAdjusting()) {
+ return;
+ }
+ boolean enable = false;
+ int row = table.getSelectedRow();
+ if (row > -1) {
+ int modelIndex = table.convertRowIndexToModel(row);
+ X509Certificate cert = tableModel.get(modelIndex);
+ enable = !ucm.isRevoked(cert.getSerialNumber());
+ }
+ revokeCertificateButton.setEnabled(enable);
+ }
+ });
+ table.addMouseListener(new MouseAdapter() {
+ public void mouseClicked(MouseEvent e) {
+ if (e.getClickCount() == 2) {
+ int row = table.rowAtPoint(e.getPoint());
+ int modelIndex = table.convertRowIndexToModel(row);
+ X509Certificate cert = tableModel.get(modelIndex);
+ X509CertificateViewer viewer = new X509CertificateViewer(UserCertificatePanel.this.owner, cert);
+ viewer.setVisible(true);
+ }
+ }
+ });
+ certificatesPanel.add(new JScrollPane(table), BorderLayout.CENTER);
+
+ newCertificateButton = new JButton(Translation.get("gb.newCertificate"));
+ newCertificateButton.setEnabled(false);
+ newCertificateButton.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ try {
+ if (saveUserButton.isEnabled()) {
+ // save changes
+ String username = ucm.user.username;
+ setEditable(false);
+ oidsPanel.updateUser(ucm);
+ saveUser(username, ucm);
+ }
+
+ NewClientCertificateDialog dialog = new NewClientCertificateDialog(UserCertificatePanel.this.owner,
+ ucm.user.getDisplayName(), getDefaultExpiration(), isAllowEmail());
+ dialog.setModal(true);
+ dialog.setVisible(true);
+ if (dialog.isCanceled()) {
+ return;
+ }
+
+ final boolean sendEmail = dialog.sendEmail();
+ final UserModel user = ucm.user;
+ final X509Metadata metadata = new X509Metadata(user.username, dialog.getPassword());
+ metadata.userDisplayname = user.getDisplayName();
+ metadata.emailAddress = user.emailAddress;
+ metadata.passwordHint = dialog.getPasswordHint();
+ metadata.notAfter = dialog.getExpiration();
+
+ AuthorityWorker worker = new AuthorityWorker(UserCertificatePanel.this.owner) {
+ @Override
+ protected Boolean doRequest() throws IOException {
+ return newCertificate(ucm, metadata, sendEmail);
+ }
+
+ @Override
+ protected void onSuccess() {
+ JOptionPane.showMessageDialog(UserCertificatePanel.this.owner,
+ MessageFormat.format(Translation.get("gb.clientCertificateGenerated"), user.getDisplayName()),
+ Translation.get("gb.newCertificate"), JOptionPane.INFORMATION_MESSAGE);
+ }
+ };
+ worker.execute();
+ } catch (Exception x) {
+ Utils.showException(UserCertificatePanel.this, x);
+ }
+ }
+ });
+
+ revokeCertificateButton = new JButton(Translation.get("gb.revokeCertificate"));
+ revokeCertificateButton.setEnabled(false);
+ revokeCertificateButton.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ try {
+ int row = table.getSelectedRow();
+ if (row < 0) {
+ return;
+ }
+ int modelIndex = table.convertRowIndexToModel(row);
+ final X509Certificate cert = tableModel.get(modelIndex);
+
+ String [] choices = new String[RevocationReason.reasons.length];
+ for (int i = 0; i < choices.length; i++) {
+ choices[i] = Translation.get("gb." + RevocationReason.reasons[i].name());
+ }
+
+ Object choice = JOptionPane.showInputDialog(UserCertificatePanel.this.owner,
+ Translation.get("gb.revokeCertificateReason"), Translation.get("gb.revokeCertificate"),
+ JOptionPane.PLAIN_MESSAGE, new ImageIcon(getClass().getResource("/rosette_32x32.png")), choices, Translation.get("gb.unspecified"));
+ if (choice == null) {
+ return;
+ }
+ RevocationReason selection = RevocationReason.unspecified;
+ for (int i = 0 ; i < choices.length; i++) {
+ if (choices[i].equals(choice)) {
+ selection = RevocationReason.reasons[i];
+ break;
+ }
+ }
+ final RevocationReason reason = selection;
+ if (!ucm.isRevoked(cert.getSerialNumber())) {
+ if (ucm.certs.size() == 1) {
+ // no other certificates
+ ucm.expires = null;
+ } else {
+ // determine new expires date for user
+ Date newExpires = null;
+ for (X509Certificate c : ucm.certs) {
+ if (!c.equals(cert)) {
+ if (!ucm.isRevoked(c.getSerialNumber())) {
+ if (newExpires == null || c.getNotAfter().after(newExpires)) {
+ newExpires = c.getNotAfter();
+ }
+ }
+ }
+ }
+ ucm.expires = newExpires;
+ }
+
+ AuthorityWorker worker = new AuthorityWorker(UserCertificatePanel.this.owner) {
+
+ @Override
+ protected Boolean doRequest() throws IOException {
+ return revoke(ucm, cert, reason);
+ }
+
+ @Override
+ protected void onSuccess() {
+ JOptionPane.showMessageDialog(UserCertificatePanel.this.owner,
+ MessageFormat.format(Translation.get("gb.certificateRevoked"), cert.getSerialNumber(), cert.getIssuerDN().getName()),
+ Translation.get("gb.revokeCertificate"), JOptionPane.INFORMATION_MESSAGE);
+ }
+
+ };
+ worker.execute();
+ }
+ } catch (Exception x) {
+ Utils.showException(UserCertificatePanel.this, x);
+ }
+ }
+ });
+
+ JPanel certificateControls = new JPanel(new FlowLayout(FlowLayout.LEFT));
+ certificateControls.add(newCertificateButton);
+ certificateControls.add(revokeCertificateButton);
+ certificatesPanel.add(certificateControls, BorderLayout.SOUTH);
+
+ add(fieldsPanel, BorderLayout.NORTH);
+ add(certificatesPanel, BorderLayout.CENTER);
+ setEditable(false);
+ }
+
+ public void setUserCertificateModel(UserCertificateModel ucm) {
+ this.ucm = ucm;
+ setEditable(false);
+ oidsPanel.setUserCertificateModel(ucm);
+
+ tableModel.setUserCertificateModel(ucm);
+ tableModel.fireTableDataChanged();
+ Utils.packColumns(table, Utils.MARGIN);
+ }
+
+ public void setEditable(boolean editable) {
+ oidsPanel.setEditable(editable);
+
+ editUserButton.setEnabled(!editable && ucm != null);
+ saveUserButton.setEnabled(editable && ucm != null);
+
+ newCertificateButton.setEnabled(ucm != null);
+ revokeCertificateButton.setEnabled(false);
+ }
+
+ public abstract Date getDefaultExpiration();
+ public abstract boolean isAllowEmail();
+
+ public abstract boolean saveUser(String username, UserCertificateModel ucm);
+ public abstract boolean newCertificate(UserCertificateModel ucm, X509Metadata metadata, boolean sendEmail);
+ public abstract boolean revoke(UserCertificateModel ucm, X509Certificate cert, RevocationReason reason);
+}
diff --git a/src/main/java/com/gitblit/authority/UserCertificateTableModel.java b/src/main/java/com/gitblit/authority/UserCertificateTableModel.java
new file mode 100644
index 00000000..dde73fc0
--- /dev/null
+++ b/src/main/java/com/gitblit/authority/UserCertificateTableModel.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2012 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.authority;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+
+import javax.swing.table.AbstractTableModel;
+
+import com.gitblit.client.Translation;
+
+/**
+ * Table model of a list of user certificate models.
+ *
+ * @author James Moger
+ *
+ */
+public class UserCertificateTableModel extends AbstractTableModel {
+
+ private static final long serialVersionUID = 1L;
+
+ List<UserCertificateModel> list;
+
+ enum Columns {
+ Username, DisplayName, Status, Expires;
+
+ @Override
+ public String toString() {
+ return name().replace('_', ' ');
+ }
+ }
+
+ public UserCertificateTableModel() {
+ this(new ArrayList<UserCertificateModel>());
+ }
+
+ public UserCertificateTableModel(List<UserCertificateModel> list) {
+ this.list = list;
+ Collections.sort(this.list);
+ }
+
+ @Override
+ public int getRowCount() {
+ return list.size();
+ }
+
+ @Override
+ public int getColumnCount() {
+ return Columns.values().length;
+ }
+
+ @Override
+ public String getColumnName(int column) {
+ Columns col = Columns.values()[column];
+ switch (col) {
+ case Username:
+ return Translation.get("gb.username");
+ case DisplayName:
+ return Translation.get("gb.displayName");
+ case Expires:
+ return Translation.get("gb.expires");
+ case Status:
+ return Translation.get("gb.status");
+ }
+ return "";
+ }
+
+ /**
+ * Returns <code>Object.class</code> regardless of <code>columnIndex</code>.
+ *
+ * @param columnIndex
+ * the column being queried
+ * @return the Object.class
+ */
+ public Class<?> getColumnClass(int columnIndex) {
+ Columns col = Columns.values()[columnIndex];
+ switch (col) {
+ case Expires:
+ return Date.class;
+ case Status:
+ return CertificateStatus.class;
+ default:
+ return String.class;
+ }
+ }
+
+ @Override
+ public boolean isCellEditable(int rowIndex, int columnIndex) {
+ Columns col = Columns.values()[columnIndex];
+ switch (col) {
+ default:
+ return false;
+ }
+ }
+
+ @Override
+ public Object getValueAt(int rowIndex, int columnIndex) {
+ UserCertificateModel model = list.get(rowIndex);
+ Columns col = Columns.values()[columnIndex];
+ switch (col) {
+ case Username:
+ return model.user.username;
+ case DisplayName:
+ return model.user.getDisplayName();
+ case Expires:
+ return model.expires;
+ case Status:
+ return model.getStatus();
+ }
+ return null;
+ }
+
+ public UserCertificateModel get(int modelRow) {
+ return list.get(modelRow);
+ }
+}
diff --git a/src/main/java/com/gitblit/authority/UserOidsPanel.java b/src/main/java/com/gitblit/authority/UserOidsPanel.java
new file mode 100644
index 00000000..8c3adf66
--- /dev/null
+++ b/src/main/java/com/gitblit/authority/UserOidsPanel.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2012 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.authority;
+
+import java.awt.GridLayout;
+
+import javax.swing.JPanel;
+import javax.swing.JTextField;
+
+import com.gitblit.client.Translation;
+
+public class UserOidsPanel extends JPanel {
+
+ private static final long serialVersionUID = 1L;
+
+ private JTextField displayname;
+ private JTextField username;
+ private JTextField emailAddress;
+ private JTextField organizationalUnit;
+ private JTextField organization;
+ private JTextField locality;
+ private JTextField stateProvince;
+ private JTextField countryCode;
+
+ public UserOidsPanel() {
+ super();
+
+ displayname = new JTextField(20);
+ username = new JTextField(20);
+ username.setEditable(false);
+ emailAddress = new JTextField(20);
+ organizationalUnit = new JTextField(20);
+ organization = new JTextField(20);
+ locality = new JTextField(20);
+ stateProvince = new JTextField(20);
+ countryCode = new JTextField(20);
+
+ setLayout(new GridLayout(0, 1, Utils.MARGIN, Utils.MARGIN));
+ add(Utils.newFieldPanel(Translation.get("gb.displayName"), displayname));
+ add(Utils.newFieldPanel(Translation.get("gb.username") + " (CN)", username));
+ add(Utils.newFieldPanel(Translation.get("gb.emailAddress") + " (E)", emailAddress));
+ add(Utils.newFieldPanel(Translation.get("gb.organizationalUnit") + " (OU)", organizationalUnit));
+ add(Utils.newFieldPanel(Translation.get("gb.organization") + " (O)", organization));
+ add(Utils.newFieldPanel(Translation.get("gb.locality") + " (L)", locality));
+ add(Utils.newFieldPanel(Translation.get("gb.stateProvince") + " (ST)", stateProvince));
+ add(Utils.newFieldPanel(Translation.get("gb.countryCode") + " (C)", countryCode));
+ }
+
+ public void setUserCertificateModel(UserCertificateModel ucm) {
+ setEditable(false);
+ displayname.setText(ucm.user.getDisplayName());
+ username.setText(ucm.user.username);
+ emailAddress.setText(ucm.user.emailAddress);
+ organizationalUnit.setText(ucm.user.organizationalUnit);
+ organization.setText(ucm.user.organization);
+ locality.setText(ucm.user.locality);
+ stateProvince.setText(ucm.user.stateProvince);
+ countryCode.setText(ucm.user.countryCode);
+ }
+
+ public void setEditable(boolean editable) {
+ displayname.setEditable(editable);
+// username.setEditable(editable);
+ emailAddress.setEditable(editable);
+ organizationalUnit.setEditable(editable);
+ organization.setEditable(editable);
+ locality.setEditable(editable);
+ stateProvince.setEditable(editable);
+ countryCode.setEditable(editable);
+ }
+
+ protected void updateUser(UserCertificateModel ucm) {
+ ucm.user.displayName = displayname.getText();
+ ucm.user.username = username.getText();
+ ucm.user.emailAddress = emailAddress.getText();
+ ucm.user.organizationalUnit = organizationalUnit.getText();
+ ucm.user.organization = organization.getText();
+ ucm.user.locality = locality.getText();
+ ucm.user.stateProvince = stateProvince.getText();
+ ucm.user.countryCode = countryCode.getText();
+ }
+}
diff --git a/src/main/java/com/gitblit/authority/Utils.java b/src/main/java/com/gitblit/authority/Utils.java
new file mode 100644
index 00000000..45e028e7
--- /dev/null
+++ b/src/main/java/com/gitblit/authority/Utils.java
@@ -0,0 +1,123 @@
+package com.gitblit.authority;
+
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.Dimension;
+import java.awt.FlowLayout;
+import java.awt.Font;
+import java.awt.Insets;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.Date;
+
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTable;
+import javax.swing.JTextArea;
+import javax.swing.table.DefaultTableColumnModel;
+import javax.swing.table.TableCellRenderer;
+import javax.swing.table.TableColumn;
+import javax.swing.table.TableModel;
+
+import com.gitblit.client.DateCellRenderer;
+import com.gitblit.client.Translation;
+import com.gitblit.utils.StringUtils;
+
+public class Utils {
+
+ public final static int LABEL_WIDTH = 175;
+
+ public final static int MARGIN = 5;
+
+ public final static Insets INSETS = new Insets(MARGIN, MARGIN, MARGIN, MARGIN);
+
+ public final static String TIMESTAMP_FORMAT = "yyyy-MM-dd HH:mm";
+
+ public final static String DATE_FORMAT = "yyyy-MM-dd";
+
+ public static JTable newTable(TableModel model, String datePattern) {
+ JTable table = new JTable(model);
+ table.setRowHeight(table.getFont().getSize() + 8);
+ table.setCellSelectionEnabled(false);
+ table.setRowSelectionAllowed(true);
+ table.getTableHeader().setReorderingAllowed(false);
+ table.setGridColor(new Color(0xd9d9d9));
+ table.setBackground(Color.white);
+ table.setDefaultRenderer(Date.class,
+ new DateCellRenderer(datePattern, Color.orange.darker()));
+ return table;
+ }
+
+ public static JPanel newFieldPanel(String label, Component c) {
+ return newFieldPanel(label, c, null);
+ }
+
+ public static JPanel newFieldPanel(String label, Component c, String trailingLabel) {
+ JLabel jlabel = new JLabel(label);
+ jlabel.setPreferredSize(new Dimension(Utils.LABEL_WIDTH, 20));
+ JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+ panel.add(jlabel);
+ panel.add(c);
+ if (!StringUtils.isEmpty(trailingLabel)) {
+ panel.add(new JLabel(trailingLabel));
+ }
+ return panel;
+ }
+
+ public static void showException(Component c, Throwable t) {
+ StringWriter writer = new StringWriter();
+ t.printStackTrace(new PrintWriter(writer));
+ String stacktrace = writer.toString();
+ try {
+ writer.close();
+ } catch (Throwable x) {
+ }
+ JTextArea textArea = new JTextArea(stacktrace);
+ textArea.setFont(new Font("monospaced", Font.PLAIN, 11));
+ JScrollPane jsp = new JScrollPane(textArea);
+ jsp.setPreferredSize(new Dimension(800, 400));
+ JOptionPane.showMessageDialog(c, jsp, Translation.get("gb.error"),
+ JOptionPane.ERROR_MESSAGE);
+ }
+
+ public static void packColumns(JTable table, int margin) {
+ for (int c = 0; c < table.getColumnCount(); c++) {
+ packColumn(table, c, 4);
+ }
+ }
+
+ // Sets the preferred width of the visible column specified by vColIndex.
+ // The column will be just wide enough to show the column head and the
+ // widest cell in the column. margin pixels are added to the left and right
+ // (resulting in an additional width of 2*margin pixels).
+ private static void packColumn(JTable table, int vColIndex, int margin) {
+ DefaultTableColumnModel colModel = (DefaultTableColumnModel) table.getColumnModel();
+ TableColumn col = colModel.getColumn(vColIndex);
+ int width = 0;
+
+ // Get width of column header
+ TableCellRenderer renderer = col.getHeaderRenderer();
+ if (renderer == null) {
+ renderer = table.getTableHeader().getDefaultRenderer();
+ }
+ Component comp = renderer.getTableCellRendererComponent(table, col.getHeaderValue(), false,
+ false, 0, 0);
+ width = comp.getPreferredSize().width;
+
+ // Get maximum width of column data
+ for (int r = 0; r < table.getRowCount(); r++) {
+ renderer = table.getCellRenderer(r, vColIndex);
+ comp = renderer.getTableCellRendererComponent(table, table.getValueAt(r, vColIndex),
+ false, false, r, vColIndex);
+ width = Math.max(width, comp.getPreferredSize().width);
+ }
+
+ // Add margin
+ width += 2 * margin;
+
+ // Set the width
+ col.setPreferredWidth(width);
+ }
+}
diff --git a/src/main/java/com/gitblit/authority/X509CertificateViewer.java b/src/main/java/com/gitblit/authority/X509CertificateViewer.java
new file mode 100644
index 00000000..797b9a81
--- /dev/null
+++ b/src/main/java/com/gitblit/authority/X509CertificateViewer.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2012 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.authority;
+
+import java.awt.BorderLayout;
+import java.awt.Dimension;
+import java.awt.FlowLayout;
+import java.awt.Frame;
+import java.awt.GridLayout;
+import java.awt.Insets;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.X509Certificate;
+import java.text.DateFormat;
+
+import javax.swing.JButton;
+import javax.swing.JDialog;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JTextField;
+import javax.swing.SwingConstants;
+
+import com.gitblit.client.HeaderPanel;
+import com.gitblit.client.Translation;
+import com.gitblit.utils.StringUtils;
+
+public class X509CertificateViewer extends JDialog {
+
+ private static final long serialVersionUID = 1L;
+
+ public X509CertificateViewer(Frame owner, X509Certificate cert) {
+ super(owner);
+
+ setTitle(Translation.get("gb.viewCertificate"));
+
+ JPanel content = new JPanel(new BorderLayout(Utils.MARGIN, Utils.MARGIN)) {
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public Insets getInsets() {
+
+ return Utils.INSETS;
+ }
+ };
+
+ DateFormat df = DateFormat.getDateTimeInstance();
+
+ int l1 = 15;
+ int l2 = 25;
+ int l3 = 45;
+ JPanel panel = new JPanel(new GridLayout(0, 1, 0, 2*Utils.MARGIN));
+ panel.add(newField(Translation.get("gb.version"), "" + cert.getVersion(), 3));
+ panel.add(newField(Translation.get("gb.subject"), cert.getSubjectDN().getName(), l3));
+ panel.add(newField(Translation.get("gb.issuer"), cert.getIssuerDN().getName(), l3));
+ panel.add(newField(Translation.get("gb.serialNumber"), "0x" + cert.getSerialNumber().toString(16), l2));
+ panel.add(newField(Translation.get("gb.serialNumber"), cert.getSerialNumber().toString(), l2));
+ panel.add(newField(Translation.get("gb.validFrom"), df.format(cert.getNotBefore()), l2));
+ panel.add(newField(Translation.get("gb.validUntil"), df.format(cert.getNotAfter()), l2));
+ panel.add(newField(Translation.get("gb.publicKey"), cert.getPublicKey().getAlgorithm(), l1));
+ panel.add(newField(Translation.get("gb.signatureAlgorithm"), cert.getSigAlgName(), l1));
+ try {
+ panel.add(newField(Translation.get("gb.sha1FingerPrint"), fingerprint(StringUtils.getSHA1(cert.getEncoded())), l3));
+ } catch (CertificateEncodingException e1) {
+ }
+ try {
+ panel.add(newField(Translation.get("gb.md5FingerPrint"), fingerprint(StringUtils.getMD5(cert.getEncoded())), l3));
+ } catch (CertificateEncodingException e1) {
+ }
+
+ content.add(panel, BorderLayout.CENTER);
+
+ JButton ok = new JButton(Translation.get("gb.ok"));
+ ok.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ setVisible(false);
+ }
+ });
+
+ JPanel controls = new JPanel();
+ controls.add(ok);
+
+ content.add(controls, BorderLayout.SOUTH);
+
+ getContentPane().add(new HeaderPanel(Translation.get("gb.certificate"), "rosette_16x16.png"), BorderLayout.NORTH);
+ getContentPane().add(content, BorderLayout.CENTER);
+ pack();
+
+ setLocationRelativeTo(owner);
+ }
+
+ private JPanel newField(String label, String value, int cols) {
+ JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT, 2*Utils.MARGIN, 0));
+ JLabel lbl = new JLabel(label);
+ lbl.setHorizontalAlignment(SwingConstants.RIGHT);
+ lbl.setPreferredSize(new Dimension(125, 20));
+ panel.add(lbl);
+ JTextField tf = new JTextField(value, cols);
+ tf.setCaretPosition(0);
+ tf.setEditable(false);
+ panel.add(tf);
+ return panel;
+ }
+
+ private String fingerprint(String value) {
+ value = value.toUpperCase();
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < value.length(); i += 2) {
+ sb.append(value.charAt(i));
+ sb.append(value.charAt(i + 1));
+ sb.append(':');
+ }
+ sb.setLength(sb.length() - 1);
+ return sb.toString();
+ }
+}
diff --git a/src/main/java/com/gitblit/build/Build.java b/src/main/java/com/gitblit/build/Build.java
new file mode 100644
index 00000000..3a9ed751
--- /dev/null
+++ b/src/main/java/com/gitblit/build/Build.java
@@ -0,0 +1,967 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.build;
+
+import java.io.BufferedInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileWriter;
+import java.io.FilenameFilter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.RandomAccessFile;
+import java.net.URL;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Properties;
+
+import com.gitblit.Constants;
+import com.gitblit.utils.StringUtils;
+
+/**
+ * The Build class downloads runtime and compile-time jar files from the Apache
+ * or Eclipse Maven repositories.
+ *
+ * It also generates the Keys class from the gitblit.properties file.
+ *
+ * Its important that this class have minimal compile dependencies since its
+ * called very early in the build script.
+ *
+ * @author James Moger
+ *
+ */
+public class Build {
+
+ private static final String osName = System.getProperty("os.name");
+
+ public interface DownloadListener {
+ public void downloading(String name);
+ }
+
+ /**
+ * BuildType enumeration representing compile-time or runtime. This is used
+ * to download dependencies either for Gitblit GO runtime or for setting up
+ * a development environment.
+ */
+ public static enum BuildType {
+ RUNTIME, COMPILETIME;
+ }
+
+ private static DownloadListener downloadListener;
+
+ public static void main(String... args) {
+ runtime();
+ compiletime();
+ buildSettingKeys();
+ delete(
+ "bcmail-jdk16-1.46.jar",
+ "bcprov-jdk16-1.46.jar",
+ "src/bcmail-jdk16-1.46-sources.jar",
+ "src/bcprov-jdk16-1.46-sources.jar");
+ }
+
+ public static void runtime() {
+ downloadFromApache(MavenObject.JCOMMANDER, BuildType.RUNTIME);
+ downloadFromApache(MavenObject.JETTY, BuildType.RUNTIME);
+ downloadFromApache(MavenObject.JETTY_AJP, BuildType.RUNTIME);
+ downloadFromApache(MavenObject.SERVLET, BuildType.RUNTIME);
+ downloadFromApache(MavenObject.SLF4JAPI, BuildType.RUNTIME);
+ downloadFromApache(MavenObject.SLF4LOG4J, BuildType.RUNTIME);
+ downloadFromApache(MavenObject.LOG4J, BuildType.RUNTIME);
+ downloadFromApache(MavenObject.WICKET, BuildType.RUNTIME);
+ downloadFromApache(MavenObject.WICKET_EXT, BuildType.RUNTIME);
+ downloadFromApache(MavenObject.WICKET_AUTH_ROLES, BuildType.RUNTIME);
+ downloadFromApache(MavenObject.WICKET_GOOGLE_CHARTS, BuildType.RUNTIME);
+ downloadFromApache(MavenObject.MARKDOWNPAPERS, BuildType.RUNTIME);
+ downloadFromApache(MavenObject.BOUNCYCASTLE, BuildType.RUNTIME);
+ downloadFromApache(MavenObject.BOUNCYCASTLE_MAIL, BuildType.RUNTIME);
+ downloadFromApache(MavenObject.BOUNCYCASTLE_PKIX, BuildType.RUNTIME);
+ downloadFromApache(MavenObject.JSCH, BuildType.RUNTIME);
+ downloadFromApache(MavenObject.ROME, BuildType.RUNTIME);
+ downloadFromApache(MavenObject.JDOM, BuildType.RUNTIME);
+ downloadFromApache(MavenObject.GSON, BuildType.RUNTIME);
+ downloadFromApache(MavenObject.MAIL, BuildType.RUNTIME);
+ downloadFromApache(MavenObject.GROOVY, BuildType.RUNTIME);
+ downloadFromApache(MavenObject.LUCENE, BuildType.RUNTIME);
+ downloadFromApache(MavenObject.LUCENE_HIGHLIGHTER, BuildType.RUNTIME);
+ downloadFromApache(MavenObject.LUCENE_MEMORY, BuildType.RUNTIME);
+ downloadFromApache(MavenObject.LUCENE_QUERIES, BuildType.RUNTIME);
+ downloadFromApache(MavenObject.JAKARTA_REGEXP, BuildType.RUNTIME);
+ downloadFromApache(MavenObject.UNBOUND_ID, BuildType.RUNTIME);
+ downloadFromApache(MavenObject.IVY, BuildType.RUNTIME);
+ downloadFromApache(MavenObject.JCALENDAR, BuildType.RUNTIME);
+ downloadFromApache(MavenObject.COMMONS_COMPRESS, BuildType.RUNTIME);
+ downloadFromApache(MavenObject.XZ, BuildType.RUNTIME);
+
+ //needed for selenium ui tests
+ downloadFromApacheToExtSelenium(MavenObject.SEL_API, BuildType.RUNTIME);
+ downloadFromApacheToExtSelenium(MavenObject.SEL_FF, BuildType.RUNTIME);
+ downloadFromApacheToExtSelenium(MavenObject.SEL_JAVA, BuildType.RUNTIME);
+ downloadFromApacheToExtSelenium(MavenObject.SEL_REMOTE, BuildType.RUNTIME);
+ downloadFromApacheToExtSelenium(MavenObject.SEL_SUPPORT, BuildType.RUNTIME);
+ downloadFromApacheToExtSelenium(MavenObject.GUAVA, BuildType.RUNTIME);
+ downloadFromApacheToExtSelenium(MavenObject.JSON, BuildType.RUNTIME);
+ downloadFromApacheToExtSelenium(MavenObject.COMMONS_EXEC, BuildType.RUNTIME);
+ downloadFromApacheToExtSelenium(MavenObject.HTTPCLIENT, BuildType.RUNTIME);
+ downloadFromApacheToExtSelenium(MavenObject.HTTPCORE, BuildType.RUNTIME);
+ downloadFromApacheToExtSelenium(MavenObject.HTTPMIME, BuildType.RUNTIME);
+ downloadFromApacheToExtSelenium(MavenObject.COMMONS_LOGGING, BuildType.RUNTIME);
+
+ downloadFromEclipse(MavenObject.JGIT, BuildType.RUNTIME);
+ downloadFromEclipse(MavenObject.JGIT_HTTP, BuildType.RUNTIME);
+ }
+
+ public static void compiletime() {
+ downloadFromApache(MavenObject.JUNIT, BuildType.RUNTIME);
+ downloadFromApache(MavenObject.HAMCREST, BuildType.RUNTIME);
+ downloadFromApache(MavenObject.JCOMMANDER, BuildType.COMPILETIME);
+ downloadFromApache(MavenObject.JETTY, BuildType.COMPILETIME);
+ downloadFromApache(MavenObject.JETTY_AJP, BuildType.COMPILETIME);
+ downloadFromApache(MavenObject.SERVLET, BuildType.COMPILETIME);
+ downloadFromApache(MavenObject.SLF4JAPI, BuildType.COMPILETIME);
+ downloadFromApache(MavenObject.SLF4LOG4J, BuildType.COMPILETIME);
+ downloadFromApache(MavenObject.LOG4J, BuildType.COMPILETIME);
+ downloadFromApache(MavenObject.WICKET, BuildType.COMPILETIME);
+ downloadFromApache(MavenObject.WICKET_EXT, BuildType.COMPILETIME);
+ downloadFromApache(MavenObject.WICKET_AUTH_ROLES, BuildType.COMPILETIME);
+ downloadFromApache(MavenObject.WICKET_GOOGLE_CHARTS, BuildType.COMPILETIME);
+ downloadFromApache(MavenObject.MARKDOWNPAPERS, BuildType.COMPILETIME);
+ downloadFromApache(MavenObject.BOUNCYCASTLE, BuildType.COMPILETIME);
+ downloadFromApache(MavenObject.BOUNCYCASTLE_MAIL, BuildType.COMPILETIME);
+ downloadFromApache(MavenObject.BOUNCYCASTLE_PKIX, BuildType.COMPILETIME);
+ downloadFromApache(MavenObject.JSCH, BuildType.COMPILETIME);
+ downloadFromApache(MavenObject.ROME, BuildType.COMPILETIME);
+ downloadFromApache(MavenObject.JDOM, BuildType.COMPILETIME);
+ downloadFromApache(MavenObject.GSON, BuildType.COMPILETIME);
+ downloadFromApache(MavenObject.MAIL, BuildType.COMPILETIME);
+ downloadFromApache(MavenObject.GROOVY, BuildType.COMPILETIME);
+ downloadFromApache(MavenObject.LUCENE, BuildType.COMPILETIME);
+ downloadFromApache(MavenObject.LUCENE_HIGHLIGHTER, BuildType.COMPILETIME);
+ downloadFromApache(MavenObject.LUCENE_MEMORY, BuildType.COMPILETIME);
+ downloadFromApache(MavenObject.LUCENE_QUERIES, BuildType.COMPILETIME);
+ downloadFromApache(MavenObject.JAKARTA_REGEXP, BuildType.COMPILETIME);
+ downloadFromApache(MavenObject.UNBOUND_ID, BuildType.COMPILETIME);
+ downloadFromApache(MavenObject.IVY, BuildType.COMPILETIME);
+ downloadFromApache(MavenObject.JCALENDAR, BuildType.COMPILETIME);
+ downloadFromApache(MavenObject.COMMONS_COMPRESS, BuildType.COMPILETIME);
+ downloadFromApache(MavenObject.XZ, BuildType.COMPILETIME);
+
+ //needed for selenium ui tests
+ downloadFromApacheToExtSelenium(MavenObject.SEL_API, BuildType.COMPILETIME);
+ downloadFromApacheToExtSelenium(MavenObject.SEL_FF, BuildType.COMPILETIME);
+ downloadFromApacheToExtSelenium(MavenObject.SEL_JAVA, BuildType.COMPILETIME);
+ downloadFromApacheToExtSelenium(MavenObject.SEL_REMOTE, BuildType.COMPILETIME);
+ downloadFromApacheToExtSelenium(MavenObject.SEL_SUPPORT, BuildType.COMPILETIME);
+ downloadFromApacheToExtSelenium(MavenObject.GUAVA, BuildType.COMPILETIME);
+ downloadFromApacheToExtSelenium(MavenObject.JSON, BuildType.COMPILETIME);
+ downloadFromApacheToExtSelenium(MavenObject.COMMONS_EXEC, BuildType.COMPILETIME);
+ downloadFromApacheToExtSelenium(MavenObject.HTTPCLIENT, BuildType.COMPILETIME);
+ downloadFromApacheToExtSelenium(MavenObject.HTTPCORE, BuildType.COMPILETIME);
+ downloadFromApacheToExtSelenium(MavenObject.HTTPMIME, BuildType.COMPILETIME);
+ downloadFromApacheToExtSelenium(MavenObject.COMMONS_LOGGING, BuildType.COMPILETIME);
+
+ downloadFromEclipse(MavenObject.JGIT, BuildType.COMPILETIME);
+ downloadFromEclipse(MavenObject.JGIT_HTTP, BuildType.COMPILETIME);
+
+ // needed for site publishing
+ downloadFromApache(MavenObject.COMMONSNET, BuildType.RUNTIME);
+ }
+
+ private static void delete(String... files) {
+ for (String name : files) {
+ File file = new File("ext", name);
+ if (file.exists()) {
+ file.delete();
+ }
+ }
+ }
+
+ public static void federationClient() {
+ downloadFromApache(MavenObject.JCOMMANDER, BuildType.RUNTIME);
+ downloadFromApache(MavenObject.SERVLET, BuildType.RUNTIME);
+ downloadFromApache(MavenObject.MAIL, BuildType.RUNTIME);
+ downloadFromApache(MavenObject.SLF4JAPI, BuildType.RUNTIME);
+ downloadFromApache(MavenObject.SLF4LOG4J, BuildType.RUNTIME);
+ downloadFromApache(MavenObject.LOG4J, BuildType.RUNTIME);
+ downloadFromApache(MavenObject.GSON, BuildType.RUNTIME);
+ downloadFromApache(MavenObject.JSCH, BuildType.RUNTIME);
+ downloadFromApache(MavenObject.LUCENE, BuildType.RUNTIME);
+ downloadFromApache(MavenObject.LUCENE_HIGHLIGHTER, BuildType.RUNTIME);
+ downloadFromApache(MavenObject.LUCENE_MEMORY, BuildType.RUNTIME);
+ downloadFromApache(MavenObject.LUCENE_QUERIES, BuildType.RUNTIME);
+ downloadFromApache(MavenObject.JAKARTA_REGEXP, BuildType.RUNTIME);
+
+ downloadFromEclipse(MavenObject.JGIT, BuildType.RUNTIME);
+ }
+
+ public static void manager(DownloadListener listener) {
+ downloadListener = listener;
+ downloadFromApache(MavenObject.GSON, BuildType.RUNTIME);
+ downloadFromApache(MavenObject.ROME, BuildType.RUNTIME);
+ downloadFromApache(MavenObject.JDOM, BuildType.RUNTIME);
+ downloadFromApache(MavenObject.JSCH, BuildType.RUNTIME);
+
+ downloadFromEclipse(MavenObject.JGIT, BuildType.RUNTIME);
+ }
+
+ public static void authority(DownloadListener listener) {
+ downloadListener = listener;
+ downloadFromApache(MavenObject.JCOMMANDER, BuildType.RUNTIME);
+ downloadFromApache(MavenObject.JSCH, BuildType.RUNTIME);
+ downloadFromApache(MavenObject.SLF4JAPI, BuildType.RUNTIME);
+ downloadFromApache(MavenObject.SLF4LOG4J, BuildType.RUNTIME);
+ downloadFromApache(MavenObject.LOG4J, BuildType.RUNTIME);
+ downloadFromApache(MavenObject.BOUNCYCASTLE, BuildType.RUNTIME);
+ downloadFromApache(MavenObject.BOUNCYCASTLE_MAIL, BuildType.RUNTIME);
+ downloadFromApache(MavenObject.BOUNCYCASTLE_PKIX, BuildType.RUNTIME);
+ downloadFromApache(MavenObject.JCALENDAR, BuildType.RUNTIME);
+ downloadFromApache(MavenObject.MAIL, BuildType.RUNTIME);
+
+ downloadFromEclipse(MavenObject.JGIT, BuildType.RUNTIME);
+ }
+
+ /**
+ * Builds the Keys class based on the gitblit.properties file and inserts
+ * the class source into the project source folder.
+ */
+ public static void buildSettingKeys() {
+ // Load all keys
+ Properties properties = new Properties();
+ FileInputStream is = null;
+ try {
+ is = new FileInputStream(new File("distrib", Constants.PROPERTIES_FILE));
+ properties.load(is);
+ } catch (Throwable t) {
+ t.printStackTrace();
+ } finally {
+ if (is != null) {
+ try {
+ is.close();
+ } catch (Throwable t) {
+ // IGNORE
+ }
+ }
+ }
+ List<String> keys = new ArrayList<String>(properties.stringPropertyNames());
+ Collections.sort(keys);
+
+ KeyGroup root = new KeyGroup();
+ for (String key : keys) {
+ root.addKey(key);
+ }
+
+ // Save Keys class definition
+ try {
+ File file = new File("src/com/gitblit/Keys.java");
+ FileWriter fw = new FileWriter(file, false);
+ fw.write(root.generateClass("com.gitblit", "Keys"));
+ fw.close();
+ } catch (Throwable t) {
+ t.printStackTrace();
+ }
+ }
+
+ private static class KeyGroup {
+ final KeyGroup parent;
+ final String namespace;
+
+ String name;
+ List<KeyGroup> children;
+ List<String> fields;
+
+ KeyGroup() {
+ this.parent = null;
+ this.namespace = "";
+ this.name = "";
+ }
+
+ KeyGroup(String namespace, KeyGroup parent) {
+ this.parent = parent;
+ this.namespace = namespace;
+ if (parent.children == null) {
+ parent.children = new ArrayList<KeyGroup>();
+ }
+ parent.children.add(this);
+ }
+
+ void addKey(String key) {
+ String keyspace = "";
+ String field = key;
+ if (key.indexOf('.') > -1) {
+ keyspace = key.substring(0, key.lastIndexOf('.'));
+ field = key.substring(key.lastIndexOf('.') + 1);
+ KeyGroup group = addKeyGroup(keyspace);
+ group.addKey(field);
+ } else {
+ if (fields == null) {
+ fields = new ArrayList<String>();
+ }
+ fields.add(key);
+ }
+ }
+
+ KeyGroup addKeyGroup(String keyspace) {
+ KeyGroup parent = this;
+ KeyGroup node = null;
+ String [] space = keyspace.split("\\.");
+ for (int i = 0; i < space.length; i++) {
+ StringBuilder namespace = new StringBuilder();
+ for (int j = 0; j <= i; j++) {
+ namespace.append(space[j]);
+ if (j < i) {
+ namespace.append('.');
+ }
+ }
+ if (parent.children != null) {
+ for (KeyGroup child : parent.children) {
+ if (child.name.equals(space[i])) {
+ node = child;
+ }
+ }
+ }
+ if (node == null) {
+ node = new KeyGroup(namespace.toString(), parent);
+ node.name = space[i];
+ }
+ parent = node;
+ node = null;
+ }
+ return parent;
+ }
+
+ String fullKey(String field) {
+ if (namespace.equals("")) {
+ return field;
+ }
+ return namespace + "." + field;
+ }
+
+ String generateClass(String packageName, String className) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("package ").append(packageName).append(";\n");
+ sb.append('\n');
+ sb.append("/*\n");
+ sb.append(" * This class is auto-generated from the properties file.\n");
+ sb.append(" * Do not version control!\n");
+ sb.append(" */\n");
+ sb.append(MessageFormat.format("public final class {0} '{'\n\n", className));
+ sb.append(generateClass(this, 0));
+ sb.append("}\n");
+ return sb.toString();
+ }
+
+ String generateClass(KeyGroup group, int level) {
+ String classIndent = StringUtils.leftPad("", level, '\t');
+ String fieldIndent = StringUtils.leftPad("", level + 1, '\t');
+
+ // begin class
+ StringBuilder sb = new StringBuilder();
+ if (!group.namespace.equals("")) {
+ sb.append(classIndent).append(MessageFormat.format("public static final class {0} '{'\n\n", group.name));
+ sb.append(fieldIndent).append(MessageFormat.format("public static final String _ROOT = \"{0}\";\n\n", group.namespace));
+ }
+
+ if (group.fields != null) {
+ // fields
+ for (String field : group.fields) {
+ sb.append(fieldIndent).append(MessageFormat.format("public static final String {0} = \"{1}\";\n\n", field, group.fullKey(field)));
+ }
+ }
+ if (group.children != null) {
+ // inner classes
+ for (KeyGroup child : group.children) {
+ sb.append(generateClass(child, level + 1));
+ }
+ }
+ // end class
+ if (!group.namespace.equals("")) {
+ sb.append(classIndent).append("}\n\n");
+ }
+ return sb.toString();
+ }
+ }
+
+ /**
+ * Download a file from the official Apache Maven repository.
+ *
+ * @param mo
+ * the maven object to download.
+ * @return
+ */
+ private static List<File> downloadFromApache(MavenObject mo, BuildType type) {
+ return downloadFromMaven("http://repo1.maven.org/maven2/", mo, type);
+ }
+
+ /**
+ * Download a file from the official Eclipse Maven repository.
+ *
+ * @param mo
+ * the maven object to download.
+ * @return
+ */
+ private static List<File> downloadFromEclipse(MavenObject mo, BuildType type) {
+ return downloadFromMaven("http://download.eclipse.org/jgit/maven/", mo, type);
+ }
+
+ /**
+ * Download a file from a Maven repository.
+ *
+ * @param mo
+ * the maven object to download.
+ * @return
+ */
+ private static List<File> downloadFromMaven(String mavenRoot, MavenObject mo, BuildType type, String targetFolder) {
+ List<File> downloads = new ArrayList<File>();
+ String[] jars = { "" };
+ if (BuildType.RUNTIME.equals(type)) {
+ jars = new String[] { "" };
+ } else if (BuildType.COMPILETIME.equals(type)) {
+ jars = new String[] { "-sources" };
+ }
+ for (String jar : jars) {
+ File targetFile = mo.getLocalFile(targetFolder, jar);
+ if ("-sources".equals(jar)) {
+ File relocated = new File(targetFolder+"/src", targetFile.getName());
+ if (targetFile.exists()) {
+ // move -sources jar to ext/src folder
+ targetFile.renameTo(relocated);
+ }
+ // -sources jars are located in ext/src
+ targetFile = relocated;
+ }
+
+ if (targetFile.exists()) {
+ downloads.add(targetFile);
+ removeObsoleteArtifacts(mo, type, targetFile.getParentFile());
+ continue;
+ }
+ String expectedSHA1 = mo.getSHA1(jar);
+ if (expectedSHA1 == null) {
+ // skip this jar
+ continue;
+ }
+ float approximateLength = mo.getApproximateLength(jar);
+ String mavenURL = mavenRoot + mo.getRepositoryPath(jar);
+ if (!targetFile.getAbsoluteFile().getParentFile().exists()) {
+ boolean success = targetFile.getAbsoluteFile().getParentFile().mkdirs();
+ if (!success) {
+ throw new RuntimeException("Failed to create destination folder structure!");
+ }
+ }
+ if (downloadListener != null) {
+ downloadListener.downloading(mo.name + "...");
+ }
+ ByteArrayOutputStream buff = new ByteArrayOutputStream();
+ try {
+ URL url = new URL(mavenURL);
+ InputStream in = new BufferedInputStream(url.openStream());
+ byte[] buffer = new byte[4096];
+ int downloadedLen = 0;
+ float lastProgress = 0f;
+
+ updateDownload(0, targetFile);
+ while (true) {
+ int len = in.read(buffer);
+ if (len < 0) {
+ break;
+ }
+ downloadedLen += len;
+ buff.write(buffer, 0, len);
+ float progress = downloadedLen / approximateLength;
+ if (progress - lastProgress >= 0.1f) {
+ lastProgress = progress;
+ updateDownload(progress, targetFile);
+ if (downloadListener != null) {
+ int percent = Math.min(100, Math.round(100 * progress));
+ downloadListener.downloading(mo.name + " (" + percent + "%)");
+ }
+ }
+ }
+ in.close();
+ updateDownload(1f, targetFile);
+ if (downloadListener != null) {
+ downloadListener.downloading(mo.name + " (100%)");
+ }
+
+ } catch (IOException e) {
+ throw new RuntimeException("Error downloading " + mavenURL + " to " + targetFile, e);
+ }
+ byte[] data = buff.toByteArray();
+ String calculatedSHA1 = StringUtils.getSHA1(data);
+
+ System.out.println();
+
+ if (expectedSHA1.length() == 0) {
+ updateProgress(0, "sha: " + calculatedSHA1);
+ System.out.println();
+ } else {
+ if (!calculatedSHA1.equals(expectedSHA1)) {
+ throw new RuntimeException("SHA1 checksum mismatch; got: " + calculatedSHA1);
+ }
+ }
+ try {
+ RandomAccessFile ra = new RandomAccessFile(targetFile, "rw");
+ ra.write(data);
+ ra.setLength(data.length);
+ ra.close();
+ } catch (IOException e) {
+ throw new RuntimeException("Error writing to file " + targetFile, e);
+ }
+ downloads.add(targetFile);
+
+ removeObsoleteArtifacts(mo, type, targetFile.getParentFile());
+ }
+ return downloads;
+ }
+
+ /**
+ * Download a file from the official Apache Maven repository.
+ *
+ * @param mo
+ * the maven object to download.
+ * @return
+ */
+ private static List<File> downloadFromApacheToExtSelenium(MavenObject mo,
+ BuildType type) {
+ return downloadFromMaven("http://repo1.maven.org/maven2/", mo, type,
+ "ext/seleniumhq");
+ }
+
+ /**
+ * Download a file from the official Apache Maven repository.
+ *
+ * @param mo
+ * the maven object to download.
+ * @return
+ */
+ private static List<File> downloadFromMaven(String mavenRoot,
+ MavenObject mo, BuildType type) {
+ return downloadFromMaven(mavenRoot, mo, type, "ext");
+ }
+
+ private static void removeObsoleteArtifacts(final MavenObject mo, final BuildType type, File folder) {
+ File [] removals = folder.listFiles(new FilenameFilter() {
+ @Override
+ public boolean accept(File dir, String name) {
+ String n = name.toLowerCase();
+ String dep = mo.artifact.toLowerCase();
+ if (n.startsWith(dep)) {
+ String suffix = "-" + mo.version;
+ if (type.equals(BuildType.COMPILETIME)) {
+ suffix += "-sources.jar";
+ } else {
+ suffix += ".jar";
+ }
+ if (!n.endsWith(suffix)) {
+ return true;
+ }
+ }
+ return false;
+ }
+ });
+
+ // delete any matches
+ if (removals != null) {
+ for (File file : removals) {
+ System.out.println("deleting " + file);
+ file.delete();
+ }
+ }
+ }
+
+ private static void updateDownload(float progress, File file) {
+ updateProgress(progress, "d/l: " + file.getName());
+ }
+
+ private static void updateProgress(float progress, String url) {
+ boolean isWindows = osName.contains("Windows");
+ String anim = "==========";
+ int width = Math.round(anim.length() * progress);
+ if (isWindows) System.out.print("\r");
+ System.out.print("[");
+ System.out.print(anim.substring(0, Math.min(width, anim.length())));
+ for (int i = 0; i < anim.length() - width; i++) {
+ System.out.print(' ');
+ }
+ System.out.print("] " + url);
+ if (!isWindows) System.out.println();
+ }
+
+ /**
+ * MavenObject represents a complete maven artifact (binary, sources, and
+ * javadoc). MavenObjects can be downloaded and checksummed to confirm
+ * authenticity.
+ */
+ private static class MavenObject {
+
+ public static final MavenObject JCOMMANDER = new MavenObject(
+ "jCommander", "com/beust", "jcommander", "1.17",
+ 34000, 32000, 141000,
+ "219a3540f3b27d7cc3b1d91d6ea046cd8723290e",
+ "0bb50eec177acf0e94d58e0cf07262fe5164331d",
+ "c7adc475ca40c288c93054e0f4fe58f3a98c0cb5");
+
+ public static final MavenObject JETTY = new MavenObject(
+ "Jetty", "org/eclipse/jetty/aggregate", "jetty-webapp", "7.6.8.v20121106",
+ 1000000, 680000, 2720000,
+ "6333969b4d509c4b681e05302ca7ebccb9c3efb5",
+ "354f2752ed6544296bc0fc92e533d68a5b03045b",
+ "");
+
+ public static final MavenObject JETTY_AJP = new MavenObject(
+ "Jetty-AJP", "org/eclipse/jetty", "jetty-ajp", "7.6.8.v20121106",
+ 32000, 22000, 97000,
+ "95bd1c89bb2afd4eeaabc6f4b0183a9f26a522d7",
+ "e1fc2539202ebb240a87a080bc44a24c93d7318b",
+ "");
+
+ public static final MavenObject SERVLET = new MavenObject(
+ "Servlet 3.0", "javax/servlet", "javax.servlet-api", "3.0.1",
+ 84000, 211000, 0,
+ "6bf0ebb7efd993e222fc1112377b5e92a13b38dd",
+ "01952f91d84016a39e31346c9d18bd8c9c4a128a",
+ null);
+
+ public static final MavenObject SLF4JAPI = new MavenObject(
+ "SLF4J API", "org/slf4j", "slf4j-api", "1.6.6",
+ 25500, 45000, 182000,
+ "ce53b0a0e2cfbb27e8a59d38f79a18a5c6a8d2b0",
+ "bcd0e21b1572960cefd449f8a16efab5b6b8e644",
+ "4253b52aabf1c5a5f20c191a261e6ada0fcf621d");
+
+ public static final MavenObject SLF4LOG4J = new MavenObject(
+ "SLF4J LOG4J", "org/slf4j", "slf4j-log4j12", "1.6.6",
+ 9800, 9500, 52400,
+ "5cd9b4fbc3ff6a97beaade3206137d76f65df805",
+ "497bfac9a678118e7ff75d1f3b8c3bcc06dc9c8c",
+ "69855e2a85d9249bb577df3c5076bc2cb34975d7");
+
+ public static final MavenObject LOG4J = new MavenObject(
+ "Apache LOG4J", "log4j", "log4j", "1.2.17",
+ 481000, 471000, 1455000,
+ "5af35056b4d257e4b64b9e8069c0746e8b08629f",
+ "677abe279b68c5e7490d6d50c6951376238d7d3e",
+ "c10c20168206896442f3192d5417815df7fcbf9a");
+
+ public static final MavenObject WICKET = new MavenObject(
+ "Apache Wicket", "org/apache/wicket", "wicket", "1.4.21",
+ 1960000, 1906000, 6818000,
+ "cce9dfd3088e18a3cdcf9be33b5b76caa48dc4c6",
+ "e8c2bfe2c96a2da7a0eca947a2f60dc3242e7229",
+ "");
+
+ public static final MavenObject WICKET_EXT = new MavenObject(
+ "Apache Wicket Extensions", "org/apache/wicket", "wicket-extensions", "1.4.21",
+ 1180000, 1118000, 1458000,
+ "fac510c7ee4399a29b927405ec3de40b67d105d8",
+ "ee3409ce9ed64ad8cc8d69abbd7d63f07e10851a",
+ "");
+
+ public static final MavenObject WICKET_AUTH_ROLES = new MavenObject(
+ "Apache Wicket Auth Roles", "org/apache/wicket", "wicket-auth-roles", "1.4.21",
+ 44000, 45000, 166000,
+ "e78df70ca942e2e15287c393f236b32fbe6f9a30",
+ "47c301212cce43a70caa72f41a9a1aefcf26a533",
+ "");
+
+ public static final MavenObject WICKET_GOOGLE_CHARTS = new MavenObject(
+ "Apache Wicket Google Charts Add-On", "org/wicketstuff", "googlecharts", "1.4.21",
+ 34000, 18750, 161000,
+ "73d7540267afc3a0e91ca6148d3073e050dba180",
+ "627b125cc6029d4d5c59c3a910c1bef347384d97",
+ "");
+
+ public static final MavenObject JUNIT = new MavenObject(
+ "JUnit", "junit", "junit", "4.10",
+ 253000, 141000, 0, "e4f1766ce7404a08f45d859fb9c226fc9e41a861",
+ "6c98d6766e72d5575f96c9479d1c1d3b865c6e25", "");
+
+ public static final MavenObject HAMCREST = new MavenObject(
+ "Hamcrest Core", "org/hamcrest", "hamcrest-core", "1.1",
+ 77000, 0, 0,
+ "860340562250678d1a344907ac75754e259cdb14",
+ null,
+ "");
+
+ public static final MavenObject MARKDOWNPAPERS = new MavenObject(
+ "MarkdownPapers", "org/tautua/markdownpapers", "markdownpapers-core", "1.3.2",
+ 92000, 60000, 268000,
+ "da22db6660e90b9a677bbdfc2c511c619ea5c249",
+ "6a7228280a229144afe6c01351a8f44675d8524d",
+ "");
+
+ public static final MavenObject BOUNCYCASTLE = new MavenObject(
+ "BouncyCastle", "org/bouncycastle", "bcprov-jdk15on", "1.47",
+ 1900000, 1400000, 4670000,
+ "b6f5d9926b0afbde9f4dbe3db88c5247be7794bb",
+ "85e6e1ad449d5a3f09624bf4038fc8d2b02de81c",
+ "");
+
+ public static final MavenObject BOUNCYCASTLE_MAIL = new MavenObject(
+ "BouncyCastle Mail", "org/bouncycastle", "bcmail-jdk15on", "1.47",
+ 502000, 420000, 482000,
+ "a35ccec640177d0de5815568529021af5546d6a7",
+ "f742330cfe1e7365dbdf773c24b92382172164a7",
+ "");
+
+ public static final MavenObject BOUNCYCASTLE_PKIX = new MavenObject(
+ "BouncyCastle PKIX", "org/bouncycastle", "bcpkix-jdk15on", "1.47",
+ 502000, 420000, 482000,
+ "cd204e6f26d2bbf65ff3a30de8831d3a1344e851",
+ "80e774a73d0e6a6b40ddf35fff613f9f30fe2a98",
+ "");
+
+ public static final MavenObject JGIT = new MavenObject(
+ "JGit", "org/eclipse/jgit", "org.eclipse.jgit", "2.2.0.201212191850-r",
+ 1600000, 1565000, 3460000,
+ "97d0761b9dd618d1f9f6c16c35c3ddf045ba536c",
+ "08dcf9546f4d61e1b8a50df5da5513006023b64b",
+ "");
+
+ public static final MavenObject JGIT_HTTP = new MavenObject(
+ "JGit", "org/eclipse/jgit", "org.eclipse.jgit.http.server", "2.2.0.201212191850-r",
+ 68000, 62000, 110000,
+ "8ad4fc4fb9529d645249bb46ad7e54d98436cb65",
+ "3385cf294957d1d34c1270b468853aea347b36ca",
+ "");
+
+ public static final MavenObject JSCH = new MavenObject(
+ "JSch", "com/jcraft", "jsch", "0.1.44-1",
+ 214000, 211000, 413000,
+ "2e9ae08de5a71bd0e0d3ba2558598181bfa71d4e",
+ "e528f593b19b04d500992606f58b87fcfded8883",
+ "d0ffadd0a4ab909d94a577b5aad43c13b617ddcb");
+
+ public static final MavenObject COMMONSNET = new MavenObject(
+ "commons-net", "commons-net", "commons-net", "1.4.0",
+ 181000, 0, 0,
+ "eb47e8cad2dd7f92fd7e77df1d1529cae87361f7",
+ "",
+ "");
+
+ public static final MavenObject ROME = new MavenObject(
+ "rome", "rome", "rome", "0.9",
+ 208000, 196000, 407000,
+ "dee2705dd01e79a5a96a17225f5a1ae30470bb18",
+ "226f851dc44fd94fe70b9c471881b71f88949cbf",
+ "8d7d867b97eeb3a9196c3926da550ad042941c1b");
+
+ public static final MavenObject JDOM = new MavenObject(
+ "jdom", "jdom", "jdom", "1.0",
+ 153000, 235000, 445000,
+ "a2ac1cd690ab4c80defe7f9bce14d35934c35cec",
+ "662abe0196cf554d4d7374f5d6382034171b652c",
+ "");
+
+ public static final MavenObject GSON = new MavenObject(
+ "gson", "com/google/code/gson", "gson", "1.7.2",
+ 174000, 142000, 247000,
+ "112366d8158749e25532ebce162232c6e0fb20a5",
+ "a6fe3006df46174a9c1c56b3c51357b9bfde5874",
+ "537f729ac63b6132a795a3c1f2e13b327e872333");
+
+ public static final MavenObject MAIL = new MavenObject(
+ "javax.mail", "javax/mail", "mail", "1.4.3",
+ 462000, 642000, 0,
+ "8154bf8d666e6db154c548dc31a8d512c273f5ee",
+ "5875e2729de83a4e46391f8f979ec8bd03810c10", null);
+
+ public static final MavenObject GROOVY = new MavenObject(
+ "groovy", "org/codehaus/groovy", "groovy-all", "1.8.8",
+ 6143000, 2290000, 4608000,
+ "98a489343d3c30da817d36cbea5de11ed07bef31",
+ "5f847ed18009f8a034bad3906e39f771c01728c1", "");
+
+ public static final MavenObject LUCENE = new MavenObject(
+ "lucene", "org/apache/lucene", "lucene-core", "3.6.1",
+ 1540000, 1431000, 3608000,
+ "6ae2c83c77a1ffa5840b9151a271ab3f451f6e0c",
+ "6925deb6b78e63bbcac382004f00b98133327057", "");
+
+ public static final MavenObject LUCENE_HIGHLIGHTER = new MavenObject(
+ "lucene highlighter", "org/apache/lucene", "lucene-highlighter", "3.6.1",
+ 89200, 85000, 0,
+ "2bd49695e9891697c5f290aa94c3412dfb95b096",
+ "20ae81816ce9c27186ef0f2e92a57812c9ee3b6c", "");
+
+ public static final MavenObject LUCENE_MEMORY = new MavenObject(
+ "lucene memory", "org/apache/lucene", "lucene-memory", "3.6.1",
+ 30000, 23000, 0,
+ "8c7ca5572edea50973dc0d26cf75c27047eebe7e",
+ "2e291e96d25132e002b1c8240e361d1272d113e1", "");
+
+ public static final MavenObject LUCENE_QUERIES = new MavenObject(
+ "lucene queries", "org/apache/lucene", "lucene-queries", "3.6.1",
+ 47400, 48600, 0,
+ "4ed6022dd4aa80b932a1546e7e39e3b8bbe7acb7",
+ "dc425c75d988e4975d314772035a46b6a17dcc8d", "");
+
+ public static final MavenObject JAKARTA_REGEXP = new MavenObject(
+ "jakarta regexp", "jakarta-regexp", "jakarta-regexp", "1.4",
+ 28500, 0, 0,
+ "0ea514a179ac1dd7e81c7e6594468b9b9910d298",
+ null, "");
+
+ public static final MavenObject UNBOUND_ID = new MavenObject(
+ "unbound id", "com/unboundid", "unboundid-ldapsdk", "2.3.0",
+ 1383417, 1439721, 0,
+ "6fde8d9fb4ee3e7e3d7e764e3ea57195971e2eb2",
+ "5276d3d29630693dba99ab9f7ea54f4c471d3af1",
+ "");
+
+ public static final MavenObject IVY = new MavenObject(
+ "ivy", "org/apache/ivy", "ivy", "2.2.0",
+ 948000, 744000, 0,
+ "f9d1e83e82fc085093510f7d2e77d81d52bc2081",
+ "0312527950ad0e8fbab37228fbed3bf41a6fe0a1", "");
+
+ public static final MavenObject JCALENDAR = new MavenObject(
+ "jcalendar", "com/toedter", "jcalendar", "1.3.2",
+ 127000, 0, 0,
+ "323a672aeacb5f5f4461be3b7f7d9d3e4bda80d4",
+ null, "");
+
+ public static final MavenObject COMMONS_COMPRESS = new MavenObject(
+ "commons-compress", "org/apache/commons", "commons-compress", "1.4.1",
+ 242000, 265000, 0,
+ "b02e84a993d88568417536240e970c4b809126fd",
+ "277d39267403965a7a192474794a29bac6760a25", "");
+
+ public static final MavenObject XZ = new MavenObject(
+ "xz", "org/tukaani", "xz", "1.0",
+ 95000, 120000, 0,
+ "ecff5cb8b1189514c9d1d8d68eb77ac372e000c9",
+ "f95e32a5d2dd8da643c4419814415b9704312993", "");
+
+ public static final MavenObject SEL_JAVA = new MavenObject(
+ "selenium-java", "org/seleniumhq/selenium", "selenium-java",
+ "2.28.0", 984098, 0, 0,
+ "7606286989ac9cb942cc206d975ffe187c18d605", "4ede08d293dc153989a337cd0d31d26421433af5", "");
+
+ public static final MavenObject SEL_API = new MavenObject(
+ "selenium-api", "org/seleniumhq/selenium", "selenium-api",
+ "2.28.0", 984098, 0, 0,
+ "c4044c40fff65cd25135a5f443638a2b1ccaeac5", "35fc6ec0804ae32b16a56627e69bdcb69995c515", "");
+
+ public static final MavenObject SEL_REMOTE = new MavenObject(
+ "selenium-remote-driver", "org/seleniumhq/selenium",
+ "selenium-remote-driver", "2.28.0", 984098, 0, 0,
+ "c67f97cd94e02afec92b0ac881844febb4fc90be", "51a9c30de3c8c203cb7a474a10842443005a5fb4", "");
+ public static final MavenObject SEL_SUPPORT = new MavenObject(
+ "selenium-support", "org/seleniumhq/selenium",
+ "selenium-support", "2.28.0", 984098, 0, 0,
+ "caf68d6310425f583bc592c08e43066b35eb94f6", "ce3831a601f5f50fda2f4604decde409b6c735a7", "");
+ public static final MavenObject SEL_FF = new MavenObject(
+ "selenium-firefox-driver", "org/seleniumhq/selenium",
+ "selenium-firefox-driver", "2.28.0", 984098, 0, 0,
+ "a7c34e45dba39e65467b900aa67611aaa039692d", "aa8cd5fb49ca75a53d5b143406ea3d81ab3eddfd", "");
+
+ public static final MavenObject GUAVA = new MavenObject("guava",
+ "com/google/guava", "guava", "12.0", 984098, 0, 0,
+ "5bc66dd95b79db1e437eb08adba124a3e4088dc0", "f8b98e61865bed3c39b978ee3bf5c7fb990c4032", "");
+
+ public static final MavenObject JSON = new MavenObject("json",
+ "org/json", "json", "20080701", 984098, 0, 0,
+ "d652f102185530c93b66158b1859f35d45687258", "71bd54221e701df9d112bf9ba2918e13b0671f3a", "");
+
+ public static final MavenObject COMMONS_EXEC = new MavenObject(
+ "commons-exec", "org/apache/commons", "commons-exec", "1.1",
+ 984098, 0, 0, "07dfdf16fade726000564386825ed6d911a44ba1", "f60bea898e18b308099862e8634d589b06a8b0be",
+ "");
+
+ public static final MavenObject HTTPCORE = new MavenObject("httpcore",
+ "org/apache/httpcomponents", "httpcore", "4.2.1", 984098, 0, 0,
+ "2d503272bf0a8b5f92d64db78b4ba9abbaccc6fd", "3f6caf5334fa83607b82e2f32dd128a9d8a0ea5e", "");
+
+ public static final MavenObject HTTPMIME = new MavenObject("httpmime",
+ "org/apache/httpcomponents", "httpmime", "4.2.1", 984098, 0, 0,
+ "7c772bace9aa31a728c39a88c6ff66a7cd177e89", "", "4e453843ae47f1c2d70e2eb2c13c037de4b614c4");
+
+ public static final MavenObject HTTPCLIENT = new MavenObject(
+ "httpclient", "org/apache/httpcomponents", "httpclient",
+ "4.2.1", 984098, 0, 0,
+ "b69bd03af60bf487b3ae1209a644ecac587bf6fc", "6b27312b9c28b59aaeb6c21f3490045690c703d3", "");
+ public static final MavenObject COMMONS_LOGGING = new MavenObject(
+ "commons-logging", "commons-logging", "commons-logging",
+ "1.1.1", 984098, 0, 0,
+ "5043bfebc3db072ed80fbd362e7caf00e885d8ae", "f3f156cbff0e0fb0d64bfce31a352cce4a33bc19", "");
+
+ public final String name;
+ public final String group;
+ public final String artifact;
+ public final String version;
+ public final int approxLibraryLen;
+ public final int approxSourcesLen;
+ public final int approxJavadocLen;
+ public final String librarySHA1;
+ public final String sourcesSHA1;
+ public final String javadocSHA1;
+
+ private MavenObject(String name, String group, String artifact, String version,
+ int approxLibraryLen, int approxSourcesLen, int approxJavadocLen,
+ String librarySHA1, String sourcesSHA1, String javadocSHA1) {
+ this.name = name;
+ this.group = group;
+ this.artifact = artifact;
+ this.version = version;
+ this.approxLibraryLen = approxLibraryLen;
+ this.approxSourcesLen = approxSourcesLen;
+ this.approxJavadocLen = approxJavadocLen;
+ this.librarySHA1 = librarySHA1;
+ this.sourcesSHA1 = sourcesSHA1;
+ this.javadocSHA1 = javadocSHA1;
+ }
+
+ private String getRepositoryPath(String jar) {
+ return group + "/" + artifact + "/" + version + "/" + artifact + "-" + version + jar
+ + ".jar";
+ }
+
+ private File getLocalFile(String basePath, String jar) {
+ return new File(basePath, artifact + "-" + version + jar + ".jar");
+ }
+
+ private String getSHA1(String jar) {
+ if (jar.equals("")) {
+ return librarySHA1;
+ } else if (jar.equals("-sources")) {
+ return sourcesSHA1;
+ } else if (jar.equals("-javadoc")) {
+ return javadocSHA1;
+ }
+ return librarySHA1;
+ }
+
+ private int getApproximateLength(String jar) {
+ if (jar.equals("")) {
+ return approxLibraryLen;
+ } else if (jar.equals("-sources")) {
+ return approxSourcesLen;
+ } else if (jar.equals("-javadoc")) {
+ return approxJavadocLen;
+ }
+ return approxLibraryLen;
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+ }
+}
diff --git a/src/main/java/com/gitblit/build/BuildGhPages.java b/src/main/java/com/gitblit/build/BuildGhPages.java
new file mode 100644
index 00000000..5982ac30
--- /dev/null
+++ b/src/main/java/com/gitblit/build/BuildGhPages.java
@@ -0,0 +1,259 @@
+/*
+ * Copyright 2012 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.build;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
+
+import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
+import org.eclipse.jgit.api.errors.JGitInternalException;
+import org.eclipse.jgit.dircache.DirCache;
+import org.eclipse.jgit.dircache.DirCacheBuilder;
+import org.eclipse.jgit.dircache.DirCacheEntry;
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.RefUpdate.Result;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.RepositoryCache.FileKey;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.storage.file.FileRepository;
+import org.eclipse.jgit.treewalk.CanonicalTreeParser;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.util.FS;
+
+import com.beust.jcommander.JCommander;
+import com.beust.jcommander.Parameter;
+import com.beust.jcommander.ParameterException;
+import com.beust.jcommander.Parameters;
+import com.gitblit.models.RefModel;
+import com.gitblit.utils.JGitUtils;
+import com.gitblit.utils.StringUtils;
+
+/**
+ * Creates or updates a gh-pages branch with the specified content.
+ *
+ * @author James Moger
+ *
+ */
+public class BuildGhPages {
+
+ public static void main(String[] args) {
+ Params params = new Params();
+ JCommander jc = new JCommander(params);
+ try {
+ jc.parse(args);
+ } catch (ParameterException t) {
+ System.err.println(t.getMessage());
+ jc.usage();
+ }
+
+ File source = new File(params.sourceFolder);
+ String ghpages = "refs/heads/gh-pages";
+ try {
+ File gitDir = FileKey.resolve(new File(params.repositoryFolder), FS.DETECTED);
+ Repository repository = new FileRepository(gitDir);
+
+ RefModel issuesBranch = JGitUtils.getPagesBranch(repository);
+ if (issuesBranch == null) {
+ JGitUtils.createOrphanBranch(repository, "gh-pages", null);
+ }
+
+ System.out.println("Updating gh-pages branch...");
+ ObjectId headId = repository.resolve(ghpages + "^{commit}");
+ ObjectInserter odi = repository.newObjectInserter();
+ try {
+ // Create the in-memory index of the new/updated issue.
+ DirCache index = createIndex(repository, headId, source, params.obliterate);
+ ObjectId indexTreeId = index.writeTree(odi);
+
+ // Create a commit object
+ PersonIdent author = new PersonIdent("Gitblit", "gitblit@localhost");
+ CommitBuilder commit = new CommitBuilder();
+ commit.setAuthor(author);
+ commit.setCommitter(author);
+ commit.setEncoding(Constants.CHARACTER_ENCODING);
+ commit.setMessage("updated pages");
+ commit.setParentId(headId);
+ commit.setTreeId(indexTreeId);
+
+ // Insert the commit into the repository
+ ObjectId commitId = odi.insert(commit);
+ odi.flush();
+
+ RevWalk revWalk = new RevWalk(repository);
+ try {
+ RevCommit revCommit = revWalk.parseCommit(commitId);
+ RefUpdate ru = repository.updateRef(ghpages);
+ ru.setNewObjectId(commitId);
+ ru.setExpectedOldObjectId(headId);
+ ru.setRefLogMessage("commit: " + revCommit.getShortMessage(), false);
+ Result rc = ru.forceUpdate();
+ switch (rc) {
+ case NEW:
+ case FORCED:
+ case FAST_FORWARD:
+ break;
+ case REJECTED:
+ case LOCK_FAILURE:
+ throw new ConcurrentRefUpdateException(JGitText.get().couldNotLockHEAD,
+ ru.getRef(), rc);
+ default:
+ throw new JGitInternalException(MessageFormat.format(
+ JGitText.get().updatingRefFailed, ghpages, commitId.toString(), rc));
+ }
+ } finally {
+ revWalk.release();
+ }
+ } finally {
+ odi.release();
+ }
+ System.out.println("gh-pages updated.");
+ } catch (Throwable t) {
+ t.printStackTrace();
+ }
+ }
+
+ /**
+ * Creates an in-memory index of the issue change.
+ *
+ * @param repo
+ * @param headId
+ * @param sourceFolder
+ * @param obliterate
+ * if true the source folder tree is used as the new tree for
+ * gh-pages and non-existent files are considered deleted
+ * @return an in-memory index
+ * @throws IOException
+ */
+ private static DirCache createIndex(Repository repo, ObjectId headId, File sourceFolder,
+ boolean obliterate) throws IOException {
+
+ DirCache inCoreIndex = DirCache.newInCore();
+ DirCacheBuilder dcBuilder = inCoreIndex.builder();
+ ObjectInserter inserter = repo.newObjectInserter();
+
+ try {
+ // Add all files to the temporary index
+ Set<String> ignorePaths = new TreeSet<String>();
+ List<File> files = listFiles(sourceFolder);
+ for (File file : files) {
+ // create an index entry for the file
+ final DirCacheEntry dcEntry = new DirCacheEntry(StringUtils.getRelativePath(
+ sourceFolder.getPath(), file.getPath()));
+ dcEntry.setLength(file.length());
+ dcEntry.setLastModified(file.lastModified());
+ dcEntry.setFileMode(FileMode.REGULAR_FILE);
+
+ // add this entry to the ignore paths set
+ ignorePaths.add(dcEntry.getPathString());
+
+ // insert object
+ InputStream inputStream = new FileInputStream(file);
+ try {
+ dcEntry.setObjectId(inserter.insert(Constants.OBJ_BLOB, file.length(),
+ inputStream));
+ } finally {
+ inputStream.close();
+ }
+
+ // add to temporary in-core index
+ dcBuilder.add(dcEntry);
+ }
+
+ if (!obliterate) {
+ // Traverse HEAD to add all other paths
+ TreeWalk treeWalk = new TreeWalk(repo);
+ int hIdx = -1;
+ if (headId != null)
+ hIdx = treeWalk.addTree(new RevWalk(repo).parseTree(headId));
+ treeWalk.setRecursive(true);
+
+ while (treeWalk.next()) {
+ String path = treeWalk.getPathString();
+ CanonicalTreeParser hTree = null;
+ if (hIdx != -1)
+ hTree = treeWalk.getTree(hIdx, CanonicalTreeParser.class);
+ if (!ignorePaths.contains(path)) {
+ // add entries from HEAD for all other paths
+ if (hTree != null) {
+ // create a new DirCacheEntry with data retrieved
+ // from
+ // HEAD
+ final DirCacheEntry dcEntry = new DirCacheEntry(path);
+ dcEntry.setObjectId(hTree.getEntryObjectId());
+ dcEntry.setFileMode(hTree.getEntryFileMode());
+
+ // add to temporary in-core index
+ dcBuilder.add(dcEntry);
+ }
+ }
+ }
+
+ // release the treewalk
+ treeWalk.release();
+ }
+
+ // finish temporary in-core index used for this commit
+ dcBuilder.finish();
+ } finally {
+ inserter.release();
+ }
+ return inCoreIndex;
+ }
+
+ private static List<File> listFiles(File folder) {
+ List<File> files = new ArrayList<File>();
+ for (File file : folder.listFiles()) {
+ if (file.isDirectory()) {
+ files.addAll(listFiles(file));
+ } else {
+ files.add(file);
+ }
+ }
+ return files;
+ }
+
+ /**
+ * JCommander Parameters class for BuildGhPages.
+ */
+ @Parameters(separators = " ")
+ private static class Params {
+
+ @Parameter(names = { "--sourceFolder" }, description = "Source folder for pages", required = true)
+ public String sourceFolder;
+
+ @Parameter(names = { "--repository" }, description = "Repository folder", required = true)
+ public String repositoryFolder;
+
+ @Parameter(names = { "--obliterate" }, description = "Replace gh-pages tree with only the content in your sourcefolder")
+ public boolean obliterate;
+
+ }
+}
diff --git a/src/main/java/com/gitblit/build/BuildSite.java b/src/main/java/com/gitblit/build/BuildSite.java
new file mode 100644
index 00000000..efff5a34
--- /dev/null
+++ b/src/main/java/com/gitblit/build/BuildSite.java
@@ -0,0 +1,385 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.build;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.FilenameFilter;
+import java.io.OutputStreamWriter;
+import java.nio.charset.Charset;
+import java.text.MessageFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Vector;
+
+import com.beust.jcommander.JCommander;
+import com.beust.jcommander.Parameter;
+import com.beust.jcommander.ParameterException;
+import com.beust.jcommander.Parameters;
+import com.gitblit.Constants;
+import com.gitblit.utils.FileUtils;
+import com.gitblit.utils.MarkdownUtils;
+import com.gitblit.utils.StringUtils;
+
+/**
+ * Builds the web site or deployment documentation from Markdown source files.
+ *
+ * All Markdown source files must have the .mkd extension.
+ *
+ * Natural string sort order of the Markdown source filenames is the order of
+ * page links. "##_" prefixes are used to control the sort order.
+ *
+ * @author James Moger
+ *
+ */
+public class BuildSite {
+
+ private static final String SPACE_DELIMITED = "SPACE-DELIMITED";
+
+ private static final String CASE_SENSITIVE = "CASE-SENSITIVE";
+
+ private static final String RESTART_REQUIRED = "RESTART REQUIRED";
+
+ private static final String SINCE = "SINCE";
+
+ public static void main(String... args) {
+ Params params = new Params();
+ JCommander jc = new JCommander(params);
+ try {
+ jc.parse(args);
+ } catch (ParameterException t) {
+ usage(jc, t);
+ }
+
+ File sourceFolder = new File(params.sourceFolder);
+ File destinationFolder = new File(params.outputFolder);
+ File[] markdownFiles = sourceFolder.listFiles(new FilenameFilter() {
+
+ @Override
+ public boolean accept(File dir, String name) {
+ return name.toLowerCase().endsWith(".mkd");
+ }
+ });
+ Arrays.sort(markdownFiles);
+
+ Map<String, String> aliasMap = new HashMap<String, String>();
+ for (String alias : params.aliases) {
+ String[] values = alias.split("=");
+ aliasMap.put(values[0], values[1]);
+ }
+
+ System.out.println(MessageFormat.format("Generating site from {0} Markdown Docs in {1} ",
+ markdownFiles.length, sourceFolder.getAbsolutePath()));
+
+ String htmlHeader = FileUtils.readContent(new File(params.pageHeader), "\n");
+
+ String htmlAdSnippet = null;
+ if (!StringUtils.isEmpty(params.adSnippet)) {
+ File snippet = new File(params.adSnippet);
+ if (snippet.exists()) {
+ htmlAdSnippet = FileUtils.readContent(snippet, "\n");
+ }
+ }
+ String htmlFooter = FileUtils.readContent(new File(params.pageFooter), "\n");
+ final String date = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
+ final String footer = MessageFormat.format(htmlFooter, "generated " + date);
+ for (File file : markdownFiles) {
+ String documentName = getDocumentName(file);
+ if (params.skips.contains(documentName)) {
+ continue;
+ }
+ try {
+ String links = createLinks(file, markdownFiles, aliasMap, params.skips);
+ String header = MessageFormat.format(htmlHeader, Constants.FULL_NAME, links);
+ if (!StringUtils.isEmpty(params.analyticsSnippet)) {
+ File snippet = new File(params.analyticsSnippet);
+ if (snippet.exists()) {
+ String htmlSnippet = FileUtils.readContent(snippet, "\n");
+ header = header.replace("<!-- ANALYTICS -->", htmlSnippet);
+ }
+ }
+
+ String fileName = documentName + ".html";
+ System.out.println(MessageFormat.format(" {0} => {1}", file.getName(), fileName));
+ String rawContent = FileUtils.readContent(file, "\n");
+ String markdownContent = rawContent;
+
+ Map<String, List<String>> nomarkdownMap = new HashMap<String, List<String>>();
+
+ // extract sections marked as no-markdown
+ int nmd = 0;
+ for (String token : params.nomarkdown) {
+ StringBuilder strippedContent = new StringBuilder();
+
+ String nomarkdownKey = "%NOMARKDOWN" + nmd + "%";
+ String[] kv = token.split(":", 2);
+ String beginToken = kv[0];
+ String endToken = kv[1];
+
+ // strip nomarkdown chunks from markdown and cache them
+ List<String> chunks = new Vector<String>();
+ int beginCode = 0;
+ int endCode = 0;
+ while ((beginCode = markdownContent.indexOf(beginToken, endCode)) > -1) {
+ if (endCode == 0) {
+ strippedContent.append(markdownContent.substring(0, beginCode));
+ } else {
+ strippedContent.append(markdownContent.substring(endCode, beginCode));
+ }
+ strippedContent.append(nomarkdownKey);
+ endCode = markdownContent.indexOf(endToken, beginCode);
+ chunks.add(markdownContent.substring(beginCode, endCode));
+ nomarkdownMap.put(nomarkdownKey, chunks);
+ }
+
+ // get remainder of text
+ if (endCode < markdownContent.length()) {
+ strippedContent.append(markdownContent.substring(endCode,
+ markdownContent.length()));
+ }
+ markdownContent = strippedContent.toString();
+ nmd++;
+ }
+
+ // transform markdown to html
+ String content = transformMarkdown(markdownContent.toString());
+
+ // reinsert nomarkdown chunks
+ for (Map.Entry<String, List<String>> nomarkdown : nomarkdownMap.entrySet()) {
+ for (String chunk : nomarkdown.getValue()) {
+ content = content.replaceFirst(nomarkdown.getKey(), chunk);
+ }
+ }
+
+ for (String token : params.substitutions) {
+ String[] kv = token.split("=", 2);
+ content = content.replace(kv[0], kv[1]);
+ }
+ for (String token : params.regex) {
+ String[] kv = token.split("!!!", 2);
+ content = content.replaceAll(kv[0], kv[1]);
+ }
+ for (String alias : params.properties) {
+ String[] kv = alias.split("=", 2);
+ String loadedContent = generatePropertiesContent(new File(kv[1]));
+ content = content.replace(kv[0], loadedContent);
+ }
+ for (String alias : params.loads) {
+ String[] kv = alias.split("=", 2);
+ String loadedContent = FileUtils.readContent(new File(kv[1]), "\n");
+ loadedContent = StringUtils.escapeForHtml(loadedContent, false);
+ loadedContent = StringUtils.breakLinesForHtml(loadedContent);
+ content = content.replace(kv[0], loadedContent);
+ }
+ OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(new File(
+ destinationFolder, fileName)), Charset.forName("UTF-8"));
+ writer.write(header);
+ if (!StringUtils.isEmpty(htmlAdSnippet)) {
+ writer.write(htmlAdSnippet);
+ }
+ writer.write(content);
+ writer.write(footer);
+ writer.close();
+ } catch (Throwable t) {
+ System.err.println("Failed to transform " + file.getName());
+ t.printStackTrace();
+ }
+ }
+ }
+
+ private static String getDocumentName(File file) {
+ String displayName = file.getName().substring(0, file.getName().lastIndexOf('.'))
+ .toLowerCase();
+ int underscore = displayName.indexOf('_') + 1;
+ if (underscore > -1) {
+ // trim leading ##_ which is to control display order
+ return displayName.substring(underscore);
+ }
+ return displayName;
+ }
+
+ private static String createLinks(File currentFile, File[] markdownFiles,
+ Map<String, String> aliasMap, List<String> skips) {
+ String linkPattern = "<li><a href=''{0}''>{1}</a></li>";
+ String currentLinkPattern = "<li class=''active''><a href=''{0}''>{1}</a></li>";
+ StringBuilder sb = new StringBuilder();
+ for (File file : markdownFiles) {
+ String documentName = getDocumentName(file);
+ if (!skips.contains(documentName)) {
+ String displayName = documentName;
+ if (aliasMap.containsKey(documentName)) {
+ displayName = aliasMap.get(documentName);
+ } else {
+ displayName = displayName.replace('_', ' ');
+ }
+ String fileName = documentName + ".html";
+ if (currentFile.getName().equals(file.getName())) {
+ sb.append(MessageFormat.format(currentLinkPattern, fileName, displayName));
+ } else {
+ sb.append(MessageFormat.format(linkPattern, fileName, displayName));
+ }
+ }
+ }
+ sb.setLength(sb.length() - 3);
+ sb.trimToSize();
+ return sb.toString();
+ }
+
+ private static String generatePropertiesContent(File propertiesFile) throws Exception {
+ // Read the current Gitblit properties
+ BufferedReader propertiesReader = new BufferedReader(new FileReader(propertiesFile));
+
+ Vector<Setting> settings = new Vector<Setting>();
+ List<String> comments = new ArrayList<String>();
+ String line = null;
+ while ((line = propertiesReader.readLine()) != null) {
+ if (line.length() == 0) {
+ Setting s = new Setting("", "", comments);
+ settings.add(s);
+ comments.clear();
+ } else {
+ if (line.charAt(0) == '#') {
+ comments.add(line.substring(1).trim());
+ } else {
+ String[] kvp = line.split("=", 2);
+ String key = kvp[0].trim();
+ Setting s = new Setting(key, kvp[1].trim(), comments);
+ settings.add(s);
+ comments.clear();
+ }
+ }
+ }
+ propertiesReader.close();
+
+ StringBuilder sb = new StringBuilder();
+ for (Setting setting : settings) {
+ for (String comment : setting.comments) {
+ if (comment.contains(SINCE) || comment.contains(RESTART_REQUIRED)
+ || comment.contains(CASE_SENSITIVE) || comment.contains(SPACE_DELIMITED)) {
+ sb.append(MessageFormat.format(
+ "<span style=\"color:#004000;\"># <i>{0}</i></span>",
+ transformMarkdown(comment)));
+ } else {
+ sb.append(MessageFormat.format("<span style=\"color:#004000;\"># {0}</span>",
+ transformMarkdown(comment)));
+ }
+ sb.append("<br/>\n");
+ }
+ if (!StringUtils.isEmpty(setting.name)) {
+ sb.append(MessageFormat
+ .format("<span style=\"color:#000080;\">{0}</span> = <span style=\"color:#800000;\">{1}</span>",
+ setting.name, StringUtils.escapeForHtml(setting.value, false)));
+ }
+ sb.append("<br/>\n");
+ }
+
+ return sb.toString();
+ }
+
+ private static String transformMarkdown(String comment) throws ParseException {
+ String md = MarkdownUtils.transformMarkdown(comment);
+ if (md.startsWith("<p>")) {
+ md = md.substring(3);
+ }
+ if (md.endsWith("</p>")) {
+ md = md.substring(0, md.length() - 4);
+ }
+ return md;
+ }
+
+ private static void usage(JCommander jc, ParameterException t) {
+ System.out.println(Constants.getGitBlitVersion());
+ System.out.println();
+ if (t != null) {
+ System.out.println(t.getMessage());
+ System.out.println();
+ }
+ if (jc != null) {
+ jc.usage();
+ }
+ System.exit(0);
+ }
+
+ /**
+ * Setting represents a setting with its comments from the properties file.
+ */
+ private static class Setting {
+ final String name;
+ final String value;
+ final List<String> comments;
+
+ Setting(String name, String value, List<String> comments) {
+ this.name = name;
+ this.value = value;
+ this.comments = new ArrayList<String>(comments);
+ }
+ }
+
+ /**
+ * JCommander Parameters class for BuildSite.
+ */
+ @Parameters(separators = " ")
+ private static class Params {
+
+ @Parameter(names = { "--sourceFolder" }, description = "Markdown Source Folder", required = true)
+ public String sourceFolder;
+
+ @Parameter(names = { "--outputFolder" }, description = "HTML Ouptut Folder", required = true)
+ public String outputFolder;
+
+ @Parameter(names = { "--pageHeader" }, description = "Page Header HTML Snippet", required = true)
+ public String pageHeader;
+
+ @Parameter(names = { "--pageFooter" }, description = "Page Footer HTML Snippet", required = true)
+ public String pageFooter;
+
+ @Parameter(names = { "--adSnippet" }, description = "Ad HTML Snippet", required = false)
+ public String adSnippet;
+
+ @Parameter(names = { "--analyticsSnippet" }, description = "Analytics HTML Snippet", required = false)
+ public String analyticsSnippet;
+
+ @Parameter(names = { "--skip" }, description = "Filename to skip", required = false)
+ public List<String> skips = new ArrayList<String>();
+
+ @Parameter(names = { "--alias" }, description = "Filename=Linkname aliases", required = false)
+ public List<String> aliases = new ArrayList<String>();
+
+ @Parameter(names = { "--substitute" }, description = "%TOKEN%=value", required = false)
+ public List<String> substitutions = new ArrayList<String>();
+
+ @Parameter(names = { "--load" }, description = "%TOKEN%=filename", required = false)
+ public List<String> loads = new ArrayList<String>();
+
+ @Parameter(names = { "--properties" }, description = "%TOKEN%=filename", required = false)
+ public List<String> properties = new ArrayList<String>();
+
+ @Parameter(names = { "--nomarkdown" }, description = "%STARTTOKEN%:%ENDTOKEN%", required = false)
+ public List<String> nomarkdown = new ArrayList<String>();
+
+ @Parameter(names = { "--regex" }, description = "searchPattern!!!replacePattern", required = false)
+ public List<String> regex = new ArrayList<String>();
+
+ }
+}
diff --git a/src/main/java/com/gitblit/build/BuildThumbnails.java b/src/main/java/com/gitblit/build/BuildThumbnails.java
new file mode 100644
index 00000000..fe06c6ca
--- /dev/null
+++ b/src/main/java/com/gitblit/build/BuildThumbnails.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.build;
+
+import java.awt.Dimension;
+import java.awt.Image;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FilenameFilter;
+import java.io.IOException;
+import java.text.MessageFormat;
+import java.util.Iterator;
+
+import javax.imageio.ImageIO;
+import javax.imageio.ImageReader;
+import javax.imageio.stream.ImageInputStream;
+
+import com.beust.jcommander.JCommander;
+import com.beust.jcommander.Parameter;
+import com.beust.jcommander.ParameterException;
+import com.beust.jcommander.Parameters;
+
+/**
+ * Generates PNG thumbnails of the PNG images from the specified source folder.
+ *
+ * @author James Moger
+ *
+ */
+public class BuildThumbnails {
+
+ public static void main(String[] args) {
+ Params params = new Params();
+ JCommander jc = new JCommander(params);
+ try {
+ jc.parse(args);
+ } catch (ParameterException t) {
+ System.err.println(t.getMessage());
+ jc.usage();
+ }
+ createImageThumbnail(params.sourceFolder, params.destinationFolder, params.maximumDimension);
+ }
+
+ /**
+ * Generates thumbnails from all PNG images in the source folder and saves
+ * them to the destination folder.
+ *
+ * @param sourceFolder
+ * @param destinationFolder
+ * @param maxDimension
+ * the maximum height or width of the image.
+ */
+ public static void createImageThumbnail(String sourceFolder, String destinationFolder,
+ int maxDimension) {
+ if (maxDimension <= 0) {
+ return;
+ }
+ File source = new File(sourceFolder);
+ File destination = new File(destinationFolder);
+ destination.mkdirs();
+ File[] sourceFiles = source.listFiles(new FilenameFilter() {
+ @Override
+ public boolean accept(File dir, String name) {
+ return name.toLowerCase().endsWith(".png");
+ }
+ });
+
+ for (File sourceFile : sourceFiles) {
+ File destinationFile = new File(destination, sourceFile.getName());
+ try {
+ Dimension sz = getImageDimensions(sourceFile);
+ int w = 0;
+ int h = 0;
+ if (sz.width > maxDimension) {
+ // Scale to Width
+ w = maxDimension;
+ float f = maxDimension;
+ // normalize height
+ h = (int) ((f / sz.width) * sz.height);
+ } else if (sz.height > maxDimension) {
+ // Scale to Height
+ h = maxDimension;
+ float f = maxDimension;
+ // normalize width
+ w = (int) ((f / sz.height) * sz.width);
+ }
+ System.out.println(MessageFormat.format(
+ "Generating thumbnail for {0} as ({1,number,#}, {2,number,#})",
+ sourceFile.getName(), w, h));
+ BufferedImage image = ImageIO.read(sourceFile);
+ Image scaledImage = image.getScaledInstance(w, h, BufferedImage.SCALE_SMOOTH);
+ BufferedImage destImage = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
+ destImage.createGraphics().drawImage(scaledImage, 0, 0, null);
+ FileOutputStream fos = new FileOutputStream(destinationFile);
+ ImageIO.write(destImage, "png", fos);
+ fos.flush();
+ fos.getFD().sync();
+ fos.close();
+ } catch (Throwable t) {
+ t.printStackTrace();
+ }
+ }
+ }
+
+ /**
+ * Return the dimensions of the specified image file.
+ *
+ * @param file
+ * @return dimensions of the image
+ * @throws IOException
+ */
+ public static Dimension getImageDimensions(File file) throws IOException {
+ ImageInputStream in = ImageIO.createImageInputStream(file);
+ try {
+ final Iterator<ImageReader> readers = ImageIO.getImageReaders(in);
+ if (readers.hasNext()) {
+ ImageReader reader = readers.next();
+ try {
+ reader.setInput(in);
+ return new Dimension(reader.getWidth(0), reader.getHeight(0));
+ } finally {
+ reader.dispose();
+ }
+ }
+ } finally {
+ if (in != null) {
+ in.close();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * JCommander Parameters class for BuildThumbnails.
+ */
+ @Parameters(separators = " ")
+ private static class Params {
+
+ @Parameter(names = { "--sourceFolder" }, description = "Source folder for raw images", required = true)
+ public String sourceFolder;
+
+ @Parameter(names = { "--destinationFolder" }, description = "Destination folder for thumbnails", required = true)
+ public String destinationFolder;
+
+ @Parameter(names = { "--maximumDimension" }, description = "Maximum width or height for thumbnail", required = true)
+ public int maximumDimension;
+
+ }
+}
diff --git a/src/main/java/com/gitblit/build/BuildWebXml.java b/src/main/java/com/gitblit/build/BuildWebXml.java
new file mode 100644
index 00000000..49a12ab2
--- /dev/null
+++ b/src/main/java/com/gitblit/build/BuildWebXml.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.build;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Vector;
+
+import com.beust.jcommander.JCommander;
+import com.beust.jcommander.Parameter;
+import com.beust.jcommander.ParameterException;
+import com.beust.jcommander.Parameters;
+import com.gitblit.Keys;
+import com.gitblit.utils.StringUtils;
+
+/**
+ * Builds the Gitblit WAR web.xml file by merging the Gitblit GO web.xml file
+ * with the gitblit.properties comments, settings, and values.
+ *
+ * @author James Moger
+ *
+ */
+public class BuildWebXml {
+ private static final String PARAMS = "<!-- PARAMS -->";
+
+ private static final String[] STRIP_TOKENS = { "<!-- STRIP", "STRIP -->" };
+
+ private static final String COMMENT_PATTERN = "\n\t<!-- {0} -->";
+
+ private static final String PARAM_PATTERN = "\n\t<context-param>\n\t\t<param-name>{0}</param-name>\n\t\t<param-value>{1}</param-value>\n\t</context-param>\n";
+
+ public static void main(String[] args) throws Exception {
+ Params params = new Params();
+ JCommander jc = new JCommander(params);
+ try {
+ jc.parse(args);
+ } catch (ParameterException t) {
+ System.err.println(t.getMessage());
+ jc.usage();
+ }
+ generateWebXml(params);
+ }
+
+ private static void generateWebXml(Params params) throws Exception {
+ StringBuilder parameters = new StringBuilder();
+ // Read the current Gitblit properties
+ if (params.propertiesFile != null) {
+ BufferedReader propertiesReader = new BufferedReader(new FileReader(new File(
+ params.propertiesFile)));
+
+ Vector<Setting> settings = new Vector<Setting>();
+ List<String> comments = new ArrayList<String>();
+ String line = null;
+ while ((line = propertiesReader.readLine()) != null) {
+ if (line.length() == 0) {
+ comments.clear();
+ } else {
+ if (line.charAt(0) == '#') {
+ if (line.length() > 1) {
+ comments.add(line.substring(1).trim());
+ }
+ } else {
+ String[] kvp = line.split("=", 2);
+ String key = kvp[0].trim();
+ if (!skipKey(key)) {
+ Setting s = new Setting(key, kvp[1].trim(), comments);
+ settings.add(s);
+ }
+ comments.clear();
+ }
+ }
+ }
+ propertiesReader.close();
+
+ for (Setting setting : settings) {
+ for (String comment : setting.comments) {
+ parameters.append(MessageFormat.format(COMMENT_PATTERN, comment));
+ }
+ parameters.append(MessageFormat.format(PARAM_PATTERN, setting.name,
+ StringUtils.escapeForHtml(setting.value, false)));
+ }
+ }
+ // Read the prototype web.xml file
+ File webxml = new File(params.sourceFile);
+ char[] buffer = new char[(int) webxml.length()];
+ FileReader webxmlReader = new FileReader(webxml);
+ webxmlReader.read(buffer);
+ webxmlReader.close();
+ String webXmlContent = new String(buffer);
+
+ // Insert the Gitblit properties into the prototype web.xml
+ for (String stripToken : STRIP_TOKENS) {
+ webXmlContent = webXmlContent.replace(stripToken, "");
+ }
+ int idx = webXmlContent.indexOf(PARAMS);
+ StringBuilder sb = new StringBuilder();
+ sb.append(webXmlContent.substring(0, idx));
+ sb.append(parameters.toString());
+ sb.append(webXmlContent.substring(idx + PARAMS.length()));
+
+ // Save the merged web.xml to the war build folder
+ FileOutputStream os = new FileOutputStream(new File(params.destinationFile), false);
+ os.write(sb.toString().getBytes());
+ os.close();
+ }
+
+ private static boolean skipKey(String key) {
+ return key.startsWith(Keys.server._ROOT);
+ }
+
+ /**
+ * Setting represents a setting and its comments from the properties file.
+ */
+ private static class Setting {
+ final String name;
+ final String value;
+ final List<String> comments;
+
+ Setting(String name, String value, List<String> comments) {
+ this.name = name;
+ this.value = value;
+ this.comments = new ArrayList<String>(comments);
+ }
+ }
+
+ /**
+ * JCommander Parameters class for BuildWebXml.
+ */
+ @Parameters(separators = " ")
+ private static class Params {
+
+ @Parameter(names = { "--sourceFile" }, description = "Source web.xml file", required = true)
+ public String sourceFile;
+
+ @Parameter(names = { "--propertiesFile" }, description = "Properties settings file")
+ public String propertiesFile;
+
+ @Parameter(names = { "--destinationFile" }, description = "Destination web.xml file", required = true)
+ public String destinationFile;
+
+ }
+}
diff --git a/src/main/java/com/gitblit/client/BooleanCellRenderer.java b/src/main/java/com/gitblit/client/BooleanCellRenderer.java
new file mode 100644
index 00000000..c8341df6
--- /dev/null
+++ b/src/main/java/com/gitblit/client/BooleanCellRenderer.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.client;
+
+import java.awt.Component;
+import java.io.Serializable;
+
+import javax.swing.JCheckBox;
+import javax.swing.JTable;
+import javax.swing.SwingConstants;
+import javax.swing.table.TableCellRenderer;
+
+/**
+ * Boolean checkbox cell renderer.
+ *
+ * @author James Moger
+ *
+ */
+public class BooleanCellRenderer extends JCheckBox implements TableCellRenderer, Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ public BooleanCellRenderer() {
+ super();
+ setOpaque(false);
+ setHorizontalAlignment(SwingConstants.CENTER);
+ }
+
+ public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,
+ boolean hasFocus, int row, int column) {
+ if (value instanceof Boolean) {
+ boolean checked = (Boolean) value;
+ this.setSelected(checked);
+ }
+ return this;
+ }
+} \ No newline at end of file
diff --git a/src/main/java/com/gitblit/client/BranchRenderer.java b/src/main/java/com/gitblit/client/BranchRenderer.java
new file mode 100644
index 00000000..9a303c38
--- /dev/null
+++ b/src/main/java/com/gitblit/client/BranchRenderer.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.client;
+
+import java.awt.Color;
+import java.awt.Component;
+
+import javax.swing.JList;
+import javax.swing.JTable;
+import javax.swing.ListCellRenderer;
+import javax.swing.table.DefaultTableCellRenderer;
+
+/**
+ * Branch renderer displays refs/heads and refs/remotes in a color similar to
+ * the site.
+ *
+ * @author James Moger
+ *
+ */
+public class BranchRenderer extends DefaultTableCellRenderer implements ListCellRenderer {
+
+ private static final long serialVersionUID = 1L;
+
+ private static final String R_HEADS = "refs/heads/";
+
+ private static final String R_REMOTES = "refs/remotes/";
+
+ public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,
+ boolean hasFocus, int row, int column) {
+ super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
+ setText(value == null ? "" : value.toString());
+ if (isSelected) {
+ setForeground(table.getSelectionForeground());
+ }
+ return this;
+ }
+
+ @Override
+ public Component getListCellRendererComponent(JList list, Object value, int index,
+ boolean isSelected, boolean cellHasFocus) {
+ setText(value == null ? "" : value.toString());
+ if (isSelected) {
+ setBackground(list.getSelectionBackground());
+ setForeground(list.getSelectionForeground());
+ } else {
+ setBackground(list.getBackground());
+ }
+ return this;
+ }
+
+ @Override
+ public void setText(String text) {
+ String name = text;
+ Color fg = getForeground();
+ if (name.startsWith(R_HEADS)) {
+ name = name.substring(R_HEADS.length());
+ fg = new Color(0, 0x80, 0);
+ } else if (name.startsWith(R_REMOTES)) {
+ name = name.substring(R_REMOTES.length());
+ fg = Color.decode("#6C6CBF");
+ }
+ setForeground(fg);
+ super.setText(name);
+ }
+} \ No newline at end of file
diff --git a/src/main/java/com/gitblit/client/ClosableTabComponent.java b/src/main/java/com/gitblit/client/ClosableTabComponent.java
new file mode 100644
index 00000000..a121806a
--- /dev/null
+++ b/src/main/java/com/gitblit/client/ClosableTabComponent.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.client;
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.Dimension;
+import java.awt.FlowLayout;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.Stroke;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseListener;
+
+import javax.swing.AbstractButton;
+import javax.swing.BorderFactory;
+import javax.swing.ImageIcon;
+import javax.swing.JButton;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JTabbedPane;
+import javax.swing.plaf.basic.BasicButtonUI;
+
+/**
+ * Closable tab control.
+ */
+public class ClosableTabComponent extends JPanel {
+
+ private static final long serialVersionUID = 1L;
+
+ private static final MouseListener BUTTON_MOUSE_LISTENER = new MouseAdapter() {
+ public void mouseEntered(MouseEvent e) {
+ Component component = e.getComponent();
+ if (component instanceof AbstractButton) {
+ AbstractButton button = (AbstractButton) component;
+ button.setBorderPainted(true);
+ }
+ }
+
+ public void mouseExited(MouseEvent e) {
+ Component component = e.getComponent();
+ if (component instanceof AbstractButton) {
+ AbstractButton button = (AbstractButton) component;
+ button.setBorderPainted(false);
+ }
+ }
+ };
+
+ private final JTabbedPane pane;
+ private final JLabel label;
+ private final JButton button = new TabButton();
+
+ private final CloseTabListener closeListener;
+
+ public interface CloseTabListener {
+ void closeTab(Component c);
+ }
+
+ public ClosableTabComponent(String title, ImageIcon icon, JTabbedPane pane,
+ CloseTabListener closeListener) {
+ super(new FlowLayout(FlowLayout.LEFT, 0, 0));
+ this.closeListener = closeListener;
+
+ if (pane == null) {
+ throw new NullPointerException("TabbedPane is null");
+ }
+ this.pane = pane;
+ setOpaque(false);
+ label = new JLabel(title);
+ if (icon != null) {
+ label.setIcon(icon);
+ }
+
+ add(label);
+ label.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 5));
+ add(button);
+ setBorder(BorderFactory.createEmptyBorder(2, 0, 0, 0));
+ }
+
+ private class TabButton extends JButton implements ActionListener {
+
+ private static final long serialVersionUID = 1L;
+
+ public TabButton() {
+ int size = 17;
+ setPreferredSize(new Dimension(size, size));
+ setToolTipText("Close");
+ setUI(new BasicButtonUI());
+ setContentAreaFilled(false);
+ setFocusable(false);
+ setBorder(BorderFactory.createEtchedBorder());
+ setBorderPainted(false);
+ addMouseListener(BUTTON_MOUSE_LISTENER);
+ setRolloverEnabled(true);
+ addActionListener(this);
+ }
+
+ public void actionPerformed(ActionEvent e) {
+ int i = pane.indexOfTabComponent(ClosableTabComponent.this);
+ Component c = pane.getComponentAt(i);
+ if (i != -1) {
+ pane.remove(i);
+ }
+ if (closeListener != null) {
+ closeListener.closeTab(c);
+ }
+ }
+
+ public void updateUI() {
+ }
+
+ @Override
+ protected void paintComponent(Graphics g) {
+ super.paintComponent(g);
+ Graphics2D g2 = (Graphics2D) g;
+ Stroke stroke = g2.getStroke();
+ g2.setStroke(new BasicStroke(2));
+ g.setColor(Color.BLACK);
+ if (getModel().isRollover()) {
+ Color highlight = new Color(0, 51, 153);
+ g.setColor(highlight);
+ }
+ int delta = 5;
+ g.drawLine(delta, delta, getWidth() - delta - 1, getHeight() - delta - 1);
+ g.drawLine(getWidth() - delta - 1, delta, delta, getHeight() - delta - 1);
+ g2.setStroke(stroke);
+
+ int i = pane.indexOfTabComponent(ClosableTabComponent.this);
+ pane.setTitleAt(i, label.getText());
+ }
+ }
+}
diff --git a/src/main/java/com/gitblit/client/DateCellRenderer.java b/src/main/java/com/gitblit/client/DateCellRenderer.java
new file mode 100644
index 00000000..751c7dbb
--- /dev/null
+++ b/src/main/java/com/gitblit/client/DateCellRenderer.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.client;
+
+import java.awt.Color;
+import java.awt.Component;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+import javax.swing.JTable;
+import javax.swing.SwingConstants;
+import javax.swing.table.DefaultTableCellRenderer;
+
+/**
+ * Time ago cell renderer with real date tooltip.
+ *
+ * @author James Moger
+ *
+ */
+public class DateCellRenderer extends DefaultTableCellRenderer {
+
+ private static final long serialVersionUID = 1L;
+
+ private final String pattern;
+
+ public DateCellRenderer(String pattern, Color foreground) {
+ this.pattern = (pattern == null ? "yyyy-MM-dd HH:mm" : pattern);
+ setForeground(foreground);
+ setHorizontalAlignment(SwingConstants.CENTER);
+ }
+
+ public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,
+ boolean hasFocus, int row, int column) {
+ super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
+ if (value instanceof Date) {
+ Date date = (Date) value;
+ String title;
+ String dateString;
+ if (date.getTime() == 0) {
+ title = "--";
+ dateString = "never";
+ } else {
+ if (date.getTime() - System.currentTimeMillis() > 0) {
+ // future
+ title = Translation.getTimeUtils().inFuture(date);
+ } else {
+ // past
+ title = Translation.getTimeUtils().timeAgo(date);
+ }
+ dateString = new SimpleDateFormat(pattern).format((Date) value);
+ }
+
+ if ((System.currentTimeMillis() - date.getTime()) > 10 * 24 * 60 * 60 * 1000L) {
+ String tmp = dateString;
+ dateString = title;
+ title = tmp;
+ }
+ this.setText(title);
+ this.setToolTipText(dateString);
+ }
+ return this;
+ }
+} \ No newline at end of file
diff --git a/src/main/java/com/gitblit/client/EditRegistrationDialog.java b/src/main/java/com/gitblit/client/EditRegistrationDialog.java
new file mode 100644
index 00000000..99cd36fa
--- /dev/null
+++ b/src/main/java/com/gitblit/client/EditRegistrationDialog.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.client;
+
+import java.awt.BorderLayout;
+import java.awt.Dimension;
+import java.awt.Font;
+import java.awt.GridLayout;
+import java.awt.Insets;
+import java.awt.Window;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.KeyEvent;
+
+import javax.swing.ImageIcon;
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JComponent;
+import javax.swing.JDialog;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JPasswordField;
+import javax.swing.JRootPane;
+import javax.swing.JTextField;
+import javax.swing.KeyStroke;
+
+import com.gitblit.utils.StringUtils;
+
+/**
+ * Dialog to create or edit a Gitblit registration.
+ *
+ * @author James Moger
+ *
+ */
+public class EditRegistrationDialog extends JDialog {
+
+ private static final long serialVersionUID = 1L;
+ private JTextField urlField;
+ private JTextField nameField;
+ private JTextField accountField;
+ private JPasswordField passwordField;
+ private JCheckBox savePassword;
+ private boolean canceled;
+ private HeaderPanel headerPanel;
+
+ public EditRegistrationDialog(Window owner) {
+ this(owner, null, false);
+ }
+
+ public EditRegistrationDialog(Window owner, GitblitRegistration reg, boolean isLogin) {
+ super(owner);
+ initialize(reg, isLogin);
+ }
+
+ @Override
+ protected JRootPane createRootPane() {
+ KeyStroke stroke = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);
+ JRootPane rootPane = new JRootPane();
+ rootPane.registerKeyboardAction(new ActionListener() {
+ public void actionPerformed(ActionEvent actionEvent) {
+ setVisible(false);
+ }
+ }, stroke, JComponent.WHEN_IN_FOCUSED_WINDOW);
+ return rootPane;
+ }
+
+ private void initialize(GitblitRegistration reg, boolean isLogin) {
+ setIconImage(new ImageIcon(getClass().getResource("/gitblt-favicon.png")).getImage());
+ canceled = true;
+ urlField = new JTextField(reg == null ? "" : reg.url, 30);
+ nameField = new JTextField(reg == null ? "" : reg.name);
+ accountField = new JTextField(reg == null ? "" : reg.account);
+ passwordField = new JPasswordField(reg == null ? "" : new String(reg.password));
+ savePassword = new JCheckBox("save password (passwords are NOT encrypted!)");
+ savePassword.setSelected(reg == null ? false
+ : (reg.password != null && reg.password.length > 0));
+
+ JPanel panel = new JPanel(new GridLayout(0, 1, 5, 5));
+ panel.add(newLabelPanel(Translation.get("gb.name"), nameField));
+ panel.add(newLabelPanel(Translation.get("gb.url"), urlField));
+ panel.add(newLabelPanel(Translation.get("gb.username"), accountField));
+ panel.add(newLabelPanel(Translation.get("gb.password"), passwordField));
+ panel.add(newLabelPanel("", savePassword));
+
+ JButton cancel = new JButton(Translation.get("gb.cancel"));
+ cancel.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent event) {
+ setVisible(false);
+ }
+ });
+
+ final JButton save = new JButton(Translation.get(isLogin ? "gb.login" : "gb.save"));
+ save.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent event) {
+ if (validateFields()) {
+ canceled = false;
+ setVisible(false);
+ }
+ }
+ });
+
+ // on enter in password field, save or login
+ passwordField.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent event) {
+ save.doClick();
+ }
+ });
+
+ JPanel controls = new JPanel();
+ controls.add(cancel);
+ controls.add(save);
+
+ if (reg == null) {
+ this.setTitle(Translation.get("gb.create"));
+ headerPanel = new HeaderPanel(Translation.get("gb.create"), null);
+ } else {
+ this.setTitle(Translation.get(isLogin ? "gb.login" : "gb.edit"));
+ headerPanel = new HeaderPanel(reg.name, null);
+ }
+
+ final Insets insets = new Insets(5, 5, 5, 5);
+ JPanel centerPanel = new JPanel(new BorderLayout(5, 5)) {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public Insets getInsets() {
+ return insets;
+ }
+ };
+ centerPanel.add(headerPanel, BorderLayout.NORTH);
+ centerPanel.add(panel, BorderLayout.CENTER);
+ centerPanel.add(controls, BorderLayout.SOUTH);
+
+ getContentPane().setLayout(new BorderLayout());
+ getContentPane().add(centerPanel, BorderLayout.CENTER);
+ pack();
+ setModal(true);
+ if (isLogin) {
+ passwordField.requestFocus();
+ }
+ }
+
+ private JPanel newLabelPanel(String text, JComponent field) {
+ JLabel label = new JLabel(text);
+ label.setFont(label.getFont().deriveFont(Font.BOLD));
+ label.setPreferredSize(new Dimension(75, 10));
+ JPanel jpanel = new JPanel(new BorderLayout());
+ jpanel.add(label, BorderLayout.WEST);
+ jpanel.add(field, BorderLayout.CENTER);
+ return jpanel;
+ }
+
+ private boolean validateFields() {
+ String name = nameField.getText();
+ if (StringUtils.isEmpty(name)) {
+ error("Please enter a name for this registration!");
+ return false;
+ }
+ String url = urlField.getText();
+ if (StringUtils.isEmpty(url)) {
+ error("Please enter a url for this registration!");
+ return false;
+ }
+ return true;
+ }
+
+ private void error(String message) {
+ JOptionPane.showMessageDialog(EditRegistrationDialog.this, message,
+ Translation.get("gb.error"), JOptionPane.ERROR_MESSAGE);
+ }
+
+ public GitblitRegistration getRegistration() {
+ if (canceled) {
+ return null;
+ }
+ GitblitRegistration reg = new GitblitRegistration(nameField.getText(), urlField.getText(),
+ accountField.getText(), passwordField.getPassword());
+ reg.savePassword = savePassword.isSelected();
+ return reg;
+ }
+}
diff --git a/src/main/java/com/gitblit/client/EditRepositoryDialog.java b/src/main/java/com/gitblit/client/EditRepositoryDialog.java
new file mode 100644
index 00000000..8851de43
--- /dev/null
+++ b/src/main/java/com/gitblit/client/EditRepositoryDialog.java
@@ -0,0 +1,804 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.client;
+
+import java.awt.BorderLayout;
+import java.awt.Component;
+import java.awt.Dimension;
+import java.awt.FlowLayout;
+import java.awt.Font;
+import java.awt.GridLayout;
+import java.awt.Insets;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.ItemEvent;
+import java.awt.event.ItemListener;
+import java.awt.event.KeyEvent;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.swing.BoxLayout;
+import javax.swing.ButtonGroup;
+import javax.swing.DefaultListCellRenderer;
+import javax.swing.ImageIcon;
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JComboBox;
+import javax.swing.JComponent;
+import javax.swing.JDialog;
+import javax.swing.JLabel;
+import javax.swing.JList;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JRadioButton;
+import javax.swing.JRootPane;
+import javax.swing.JScrollPane;
+import javax.swing.JTabbedPane;
+import javax.swing.JTextField;
+import javax.swing.KeyStroke;
+import javax.swing.ListCellRenderer;
+import javax.swing.ScrollPaneConstants;
+
+import com.gitblit.Constants.AccessRestrictionType;
+import com.gitblit.Constants.AuthorizationControl;
+import com.gitblit.Constants.FederationStrategy;
+import com.gitblit.Constants.RegistrantType;
+import com.gitblit.models.RegistrantAccessPermission;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.utils.ArrayUtils;
+import com.gitblit.utils.StringUtils;
+
+/**
+ * Dialog to create/edit a repository.
+ *
+ * @author James Moger
+ */
+public class EditRepositoryDialog extends JDialog {
+
+ private static final long serialVersionUID = 1L;
+
+ private final String repositoryName;
+
+ private final RepositoryModel repository;
+
+ private boolean isCreate;
+
+ private boolean canceled = true;
+
+ private JTextField nameField;
+
+ private JTextField descriptionField;
+
+ private JCheckBox useTickets;
+
+ private JCheckBox useDocs;
+
+ private JCheckBox showRemoteBranches;
+
+ private JCheckBox showReadme;
+
+ private JCheckBox skipSizeCalculation;
+
+ private JCheckBox skipSummaryMetrics;
+
+ private JCheckBox isFrozen;
+
+ private JTextField mailingListsField;
+
+ private JComboBox accessRestriction;
+
+ private JRadioButton allowAuthenticated;
+
+ private JRadioButton allowNamed;
+
+ private JCheckBox allowForks;
+
+ private JCheckBox verifyCommitter;
+
+ private JComboBox federationStrategy;
+
+ private JPalette<String> ownersPalette;
+
+ private JComboBox headRefField;
+
+ private JComboBox gcPeriod;
+
+ private JTextField gcThreshold;
+
+ private JComboBox maxActivityCommits;
+
+ private RegistrantPermissionsPanel usersPalette;
+
+ private JPalette<String> setsPalette;
+
+ private RegistrantPermissionsPanel teamsPalette;
+
+ private JPalette<String> indexedBranchesPalette;
+
+ private JPalette<String> preReceivePalette;
+
+ private JLabel preReceiveInherited;
+
+ private JPalette<String> postReceivePalette;
+
+ private JLabel postReceiveInherited;
+
+ private Set<String> repositoryNames;
+
+ private JPanel customFieldsPanel;
+
+ private List<JTextField> customTextfields;
+
+ public EditRepositoryDialog(int protocolVersion) {
+ this(protocolVersion, new RepositoryModel());
+ this.isCreate = true;
+ setTitle(Translation.get("gb.newRepository"));
+ }
+
+ public EditRepositoryDialog(int protocolVersion, RepositoryModel aRepository) {
+ super();
+ this.repositoryName = aRepository.name;
+ this.repository = new RepositoryModel();
+ this.repositoryNames = new HashSet<String>();
+ this.isCreate = false;
+ initialize(protocolVersion, aRepository);
+ setModal(true);
+ setResizable(false);
+ setTitle(Translation.get("gb.edit") + ": " + aRepository.name);
+ setIconImage(new ImageIcon(getClass()
+ .getResource("/gitblt-favicon.png")).getImage());
+ }
+
+ @Override
+ protected JRootPane createRootPane() {
+ KeyStroke stroke = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);
+ JRootPane rootPane = new JRootPane();
+ rootPane.registerKeyboardAction(new ActionListener() {
+ public void actionPerformed(ActionEvent actionEvent) {
+ setVisible(false);
+ }
+ }, stroke, JComponent.WHEN_IN_FOCUSED_WINDOW);
+ return rootPane;
+ }
+
+ private void initialize(int protocolVersion, RepositoryModel anRepository) {
+ nameField = new JTextField(anRepository.name == null ? ""
+ : anRepository.name, 35);
+ descriptionField = new JTextField(anRepository.description == null ? ""
+ : anRepository.description, 35);
+
+ JTextField originField = new JTextField(
+ anRepository.origin == null ? "" : anRepository.origin, 40);
+ originField.setEditable(false);
+
+ if (ArrayUtils.isEmpty(anRepository.availableRefs)) {
+ headRefField = new JComboBox();
+ headRefField.setEnabled(false);
+ } else {
+ headRefField = new JComboBox(
+ anRepository.availableRefs.toArray());
+ headRefField.setSelectedItem(anRepository.HEAD);
+ }
+
+ Integer [] gcPeriods = { 1, 2, 3, 4, 5, 7, 10, 14 };
+ gcPeriod = new JComboBox(gcPeriods);
+ gcPeriod.setSelectedItem(anRepository.gcPeriod);
+
+ gcThreshold = new JTextField(8);
+ gcThreshold.setText(anRepository.gcThreshold);
+
+ ownersPalette = new JPalette<String>(true);
+
+ useTickets = new JCheckBox(Translation.get("gb.useTicketsDescription"),
+ anRepository.useTickets);
+ useDocs = new JCheckBox(Translation.get("gb.useDocsDescription"),
+ anRepository.useDocs);
+ showRemoteBranches = new JCheckBox(
+ Translation.get("gb.showRemoteBranchesDescription"),
+ anRepository.showRemoteBranches);
+ showReadme = new JCheckBox(Translation.get("gb.showReadmeDescription"),
+ anRepository.showReadme);
+ skipSizeCalculation = new JCheckBox(
+ Translation.get("gb.skipSizeCalculationDescription"),
+ anRepository.skipSizeCalculation);
+ skipSummaryMetrics = new JCheckBox(
+ Translation.get("gb.skipSummaryMetricsDescription"),
+ anRepository.skipSummaryMetrics);
+ isFrozen = new JCheckBox(Translation.get("gb.isFrozenDescription"),
+ anRepository.isFrozen);
+
+ maxActivityCommits = new JComboBox(new Integer [] { -1, 0, 25, 50, 75, 100, 150, 250, 500 });
+ maxActivityCommits.setSelectedItem(anRepository.maxActivityCommits);
+
+ mailingListsField = new JTextField(
+ ArrayUtils.isEmpty(anRepository.mailingLists) ? ""
+ : StringUtils.flattenStrings(anRepository.mailingLists,
+ " "), 50);
+
+ accessRestriction = new JComboBox(AccessRestrictionType.values());
+ accessRestriction.setRenderer(new AccessRestrictionRenderer());
+ accessRestriction.setSelectedItem(anRepository.accessRestriction);
+ accessRestriction.addItemListener(new ItemListener() {
+ @Override
+ public void itemStateChanged(ItemEvent e) {
+ if (e.getStateChange() == ItemEvent.SELECTED) {
+ AccessRestrictionType art = (AccessRestrictionType) accessRestriction.getSelectedItem();
+ EditRepositoryDialog.this.setupAccessPermissions(art);
+ }
+ }
+ });
+
+ boolean authenticated = anRepository.authorizationControl != null
+ && AuthorizationControl.AUTHENTICATED.equals(anRepository.authorizationControl);
+ allowAuthenticated = new JRadioButton(Translation.get("gb.allowAuthenticatedDescription"));
+ allowAuthenticated.setSelected(authenticated);
+ allowAuthenticated.addItemListener(new ItemListener() {
+ @Override
+ public void itemStateChanged(ItemEvent e) {
+ if (e.getStateChange() == ItemEvent.SELECTED) {
+ usersPalette.setEnabled(false);
+ teamsPalette.setEnabled(false);
+ }
+ }
+ });
+
+ allowNamed = new JRadioButton(Translation.get("gb.allowNamedDescription"));
+ allowNamed.setSelected(!authenticated);
+ allowNamed.addItemListener(new ItemListener() {
+ @Override
+ public void itemStateChanged(ItemEvent e) {
+ if (e.getStateChange() == ItemEvent.SELECTED) {
+ usersPalette.setEnabled(true);
+ teamsPalette.setEnabled(true);
+ }
+ }
+ });
+
+ ButtonGroup group = new ButtonGroup();
+ group.add(allowAuthenticated);
+ group.add(allowNamed);
+
+ JPanel authorizationPanel = new JPanel(new GridLayout(0, 1));
+ authorizationPanel.add(allowAuthenticated);
+ authorizationPanel.add(allowNamed);
+
+ allowForks = new JCheckBox(Translation.get("gb.allowForksDescription"), anRepository.allowForks);
+ verifyCommitter = new JCheckBox(Translation.get("gb.verifyCommitterDescription"), anRepository.verifyCommitter);
+
+ // federation strategies - remove ORIGIN choice if this repository has
+ // no origin.
+ List<FederationStrategy> federationStrategies = new ArrayList<FederationStrategy>(
+ Arrays.asList(FederationStrategy.values()));
+ if (StringUtils.isEmpty(anRepository.origin)) {
+ federationStrategies.remove(FederationStrategy.FEDERATE_ORIGIN);
+ }
+ federationStrategy = new JComboBox(federationStrategies.toArray());
+ federationStrategy.setRenderer(new FederationStrategyRenderer());
+ federationStrategy.setSelectedItem(anRepository.federationStrategy);
+
+ JPanel fieldsPanel = new JPanel(new GridLayout(0, 1));
+ fieldsPanel.add(newFieldPanel(Translation.get("gb.name"), nameField));
+ fieldsPanel.add(newFieldPanel(Translation.get("gb.description"),
+ descriptionField));
+ fieldsPanel
+ .add(newFieldPanel(Translation.get("gb.origin"), originField));
+ fieldsPanel.add(newFieldPanel(Translation.get("gb.headRef"), headRefField));
+ fieldsPanel.add(newFieldPanel(Translation.get("gb.gcPeriod"), gcPeriod));
+ fieldsPanel.add(newFieldPanel(Translation.get("gb.gcThreshold"), gcThreshold));
+
+ fieldsPanel.add(newFieldPanel(Translation.get("gb.enableTickets"),
+ useTickets));
+ fieldsPanel
+ .add(newFieldPanel(Translation.get("gb.enableDocs"), useDocs));
+ fieldsPanel.add(newFieldPanel(Translation.get("gb.showRemoteBranches"),
+ showRemoteBranches));
+ fieldsPanel.add(newFieldPanel(Translation.get("gb.showReadme"),
+ showReadme));
+ fieldsPanel
+ .add(newFieldPanel(Translation.get("gb.skipSizeCalculation"),
+ skipSizeCalculation));
+ fieldsPanel.add(newFieldPanel(Translation.get("gb.skipSummaryMetrics"),
+ skipSummaryMetrics));
+ fieldsPanel.add(newFieldPanel(Translation.get("gb.maxActivityCommits"),
+ maxActivityCommits));
+ fieldsPanel.add(newFieldPanel(Translation.get("gb.mailingLists"),
+ mailingListsField));
+
+ JPanel clonePushPanel = new JPanel(new GridLayout(0, 1));
+ clonePushPanel
+ .add(newFieldPanel(Translation.get("gb.isFrozen"), isFrozen));
+ clonePushPanel
+ .add(newFieldPanel(Translation.get("gb.allowForks"), allowForks));
+ clonePushPanel
+ .add(newFieldPanel(Translation.get("gb.verifyCommitter"), verifyCommitter));
+
+ usersPalette = new RegistrantPermissionsPanel(RegistrantType.USER);
+
+ JPanel northFieldsPanel = new JPanel(new BorderLayout(0, 5));
+ northFieldsPanel.add(newFieldPanel(Translation.get("gb.owners"), ownersPalette), BorderLayout.NORTH);
+ northFieldsPanel.add(newFieldPanel(Translation.get("gb.accessRestriction"),
+ accessRestriction), BorderLayout.CENTER);
+
+ JPanel northAccessPanel = new JPanel(new BorderLayout(5, 5));
+ northAccessPanel.add(northFieldsPanel, BorderLayout.NORTH);
+ northAccessPanel.add(newFieldPanel(Translation.get("gb.authorizationControl"),
+ authorizationPanel), BorderLayout.CENTER);
+ northAccessPanel.add(clonePushPanel, BorderLayout.SOUTH);
+
+ JPanel accessPanel = new JPanel(new BorderLayout(5, 5));
+ accessPanel.add(northAccessPanel, BorderLayout.NORTH);
+ accessPanel.add(newFieldPanel(Translation.get("gb.userPermissions"),
+ usersPalette), BorderLayout.CENTER);
+
+ teamsPalette = new RegistrantPermissionsPanel(RegistrantType.TEAM);
+ JPanel teamsPanel = new JPanel(new BorderLayout(5, 5));
+ teamsPanel.add(
+ newFieldPanel(Translation.get("gb.teamPermissions"),
+ teamsPalette), BorderLayout.CENTER);
+
+ setsPalette = new JPalette<String>();
+ JPanel federationPanel = new JPanel(new BorderLayout(5, 5));
+ federationPanel.add(
+ newFieldPanel(Translation.get("gb.federationStrategy"),
+ federationStrategy), BorderLayout.NORTH);
+ federationPanel
+ .add(newFieldPanel(Translation.get("gb.federationSets"),
+ setsPalette), BorderLayout.CENTER);
+
+ indexedBranchesPalette = new JPalette<String>();
+ JPanel indexedBranchesPanel = new JPanel(new BorderLayout(5, 5));
+ indexedBranchesPanel
+ .add(newFieldPanel(Translation.get("gb.indexedBranches"),
+ indexedBranchesPalette), BorderLayout.CENTER);
+
+ preReceivePalette = new JPalette<String>(true);
+ preReceiveInherited = new JLabel();
+ JPanel preReceivePanel = new JPanel(new BorderLayout(5, 5));
+ preReceivePanel.add(preReceivePalette, BorderLayout.CENTER);
+ preReceivePanel.add(preReceiveInherited, BorderLayout.WEST);
+
+ postReceivePalette = new JPalette<String>(true);
+ postReceiveInherited = new JLabel();
+ JPanel postReceivePanel = new JPanel(new BorderLayout(5, 5));
+ postReceivePanel.add(postReceivePalette, BorderLayout.CENTER);
+ postReceivePanel.add(postReceiveInherited, BorderLayout.WEST);
+
+ customFieldsPanel = new JPanel();
+ customFieldsPanel.setLayout(new BoxLayout(customFieldsPanel, BoxLayout.Y_AXIS));
+ JScrollPane customFieldsScrollPane = new JScrollPane(customFieldsPanel);
+ customFieldsScrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
+ customFieldsScrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED);
+
+ JTabbedPane panel = new JTabbedPane(JTabbedPane.TOP);
+ panel.addTab(Translation.get("gb.general"), fieldsPanel);
+ panel.addTab(Translation.get("gb.accessRestriction"), accessPanel);
+ if (protocolVersion >= 2) {
+ panel.addTab(Translation.get("gb.teams"), teamsPanel);
+ }
+ panel.addTab(Translation.get("gb.federation"), federationPanel);
+ if (protocolVersion >= 3) {
+ panel.addTab(Translation.get("gb.indexedBranches"), indexedBranchesPanel);
+ }
+ panel.addTab(Translation.get("gb.preReceiveScripts"), preReceivePanel);
+ panel.addTab(Translation.get("gb.postReceiveScripts"), postReceivePanel);
+
+ panel.addTab(Translation.get("gb.customFields"), customFieldsScrollPane);
+
+
+ setupAccessPermissions(anRepository.accessRestriction);
+
+ JButton createButton = new JButton(Translation.get("gb.save"));
+ createButton.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent event) {
+ if (validateFields()) {
+ canceled = false;
+ setVisible(false);
+ }
+ }
+ });
+
+ JButton cancelButton = new JButton(Translation.get("gb.cancel"));
+ cancelButton.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent event) {
+ canceled = true;
+ setVisible(false);
+ }
+ });
+
+ JPanel controls = new JPanel();
+ controls.add(cancelButton);
+ controls.add(createButton);
+
+ final Insets _insets = new Insets(5, 5, 5, 5);
+ JPanel centerPanel = new JPanel(new BorderLayout(5, 5)) {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public Insets getInsets() {
+ return _insets;
+ }
+ };
+ centerPanel.add(panel, BorderLayout.CENTER);
+ centerPanel.add(controls, BorderLayout.SOUTH);
+
+ getContentPane().setLayout(new BorderLayout(5, 5));
+ getContentPane().add(centerPanel, BorderLayout.CENTER);
+ pack();
+ nameField.requestFocus();
+ }
+
+ private JPanel newFieldPanel(String label, JComponent comp) {
+ return newFieldPanel(label, 150, comp);
+ }
+
+ private JPanel newFieldPanel(String label, int labelSize, JComponent comp) {
+ JLabel fieldLabel = new JLabel(label);
+ fieldLabel.setFont(fieldLabel.getFont().deriveFont(Font.BOLD));
+ fieldLabel.setPreferredSize(new Dimension(labelSize, 20));
+ JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 0));
+ panel.add(fieldLabel);
+ panel.add(comp);
+ return panel;
+ }
+
+ private void setupAccessPermissions(AccessRestrictionType art) {
+ if (AccessRestrictionType.NONE.equals(art)) {
+ usersPalette.setEnabled(false);
+ teamsPalette.setEnabled(false);
+
+ allowAuthenticated.setEnabled(false);
+ allowNamed.setEnabled(false);
+ verifyCommitter.setEnabled(false);
+ } else {
+ allowAuthenticated.setEnabled(true);
+ allowNamed.setEnabled(true);
+ verifyCommitter.setEnabled(true);
+
+ if (allowNamed.isSelected()) {
+ usersPalette.setEnabled(true);
+ teamsPalette.setEnabled(true);
+ }
+ }
+
+ }
+
+ private boolean validateFields() {
+ String rname = nameField.getText();
+ if (StringUtils.isEmpty(rname)) {
+ error("Please enter a repository name!");
+ return false;
+ }
+
+ // automatically convert backslashes to forward slashes
+ rname = rname.replace('\\', '/');
+ // Automatically replace // with /
+ rname = rname.replace("//", "/");
+
+ // prohibit folder paths
+ if (rname.startsWith("/")) {
+ error("Leading root folder references (/) are prohibited.");
+ return false;
+ }
+ if (rname.startsWith("../")) {
+ error("Relative folder references (../) are prohibited.");
+ return false;
+ }
+ if (rname.contains("/../")) {
+ error("Relative folder references (../) are prohibited.");
+ return false;
+ }
+ if (rname.endsWith("/")) {
+ rname = rname.substring(0, rname.length() - 1);
+ }
+
+ // confirm valid characters in repository name
+ Character c = StringUtils.findInvalidCharacter(rname);
+ if (c != null) {
+ error(MessageFormat.format(
+ "Illegal character ''{0}'' in repository name!", c));
+ return false;
+ }
+
+ // verify repository name uniqueness on create
+ if (isCreate) {
+ // force repo names to lowercase
+ // this means that repository name checking for rpc creation
+ // is case-insensitive, regardless of the Gitblit server's
+ // filesystem
+ if (repositoryNames.contains(rname.toLowerCase())) {
+ error(MessageFormat
+ .format("Can not create repository ''{0}'' because it already exists.",
+ rname));
+ return false;
+ }
+ } else {
+ // check rename collision
+ if (!repositoryName.equalsIgnoreCase(rname)) {
+ if (repositoryNames.contains(rname.toLowerCase())) {
+ error(MessageFormat
+ .format("Failed to rename ''{0}'' because ''{1}'' already exists.",
+ repositoryName, rname));
+ return false;
+ }
+ }
+ }
+
+ if (accessRestriction.getSelectedItem() == null) {
+ error("Please select access restriction!");
+ return false;
+ }
+
+ if (federationStrategy.getSelectedItem() == null) {
+ error("Please select federation strategy!");
+ return false;
+ }
+
+ repository.name = rname;
+ repository.description = descriptionField.getText();
+ repository.owners.clear();
+ repository.owners.addAll(ownersPalette.getSelections());
+ repository.HEAD = headRefField.getSelectedItem() == null ? null
+ : headRefField.getSelectedItem().toString();
+ repository.gcPeriod = (Integer) gcPeriod.getSelectedItem();
+ repository.gcThreshold = gcThreshold.getText();
+ repository.useTickets = useTickets.isSelected();
+ repository.useDocs = useDocs.isSelected();
+ repository.showRemoteBranches = showRemoteBranches.isSelected();
+ repository.showReadme = showReadme.isSelected();
+ repository.skipSizeCalculation = skipSizeCalculation.isSelected();
+ repository.skipSummaryMetrics = skipSummaryMetrics.isSelected();
+ repository.maxActivityCommits = (Integer) maxActivityCommits.getSelectedItem();
+
+ repository.isFrozen = isFrozen.isSelected();
+ repository.allowForks = allowForks.isSelected();
+ repository.verifyCommitter = verifyCommitter.isSelected();
+
+ String ml = mailingListsField.getText();
+ if (!StringUtils.isEmpty(ml)) {
+ Set<String> list = new HashSet<String>();
+ for (String address : ml.split("(,|\\s)")) {
+ if (StringUtils.isEmpty(address)) {
+ continue;
+ }
+ list.add(address.toLowerCase());
+ }
+ repository.mailingLists = new ArrayList<String>(list);
+ }
+
+ repository.accessRestriction = (AccessRestrictionType) accessRestriction
+ .getSelectedItem();
+ repository.authorizationControl = allowAuthenticated.isSelected() ?
+ AuthorizationControl.AUTHENTICATED : AuthorizationControl.NAMED;
+ repository.federationStrategy = (FederationStrategy) federationStrategy
+ .getSelectedItem();
+
+ if (repository.federationStrategy.exceeds(FederationStrategy.EXCLUDE)) {
+ repository.federationSets = setsPalette.getSelections();
+ }
+
+ repository.indexedBranches = indexedBranchesPalette.getSelections();
+ repository.preReceiveScripts = preReceivePalette.getSelections();
+ repository.postReceiveScripts = postReceivePalette.getSelections();
+
+ // Custom Fields
+ repository.customFields = new LinkedHashMap<String, String>();
+ if (customTextfields != null) {
+ for (JTextField field : customTextfields) {
+ String key = field.getName();
+ String value = field.getText();
+ repository.customFields.put(key, value);
+ }
+ }
+ return true;
+ }
+
+ private void error(String message) {
+ JOptionPane.showMessageDialog(EditRepositoryDialog.this, message,
+ Translation.get("gb.error"), JOptionPane.ERROR_MESSAGE);
+ }
+
+ public void setAccessRestriction(AccessRestrictionType restriction) {
+ this.accessRestriction.setSelectedItem(restriction);
+ setupAccessPermissions(restriction);
+ }
+
+ public void setAuthorizationControl(AuthorizationControl authorization) {
+ boolean authenticated = authorization != null && AuthorizationControl.AUTHENTICATED.equals(authorization);
+ this.allowAuthenticated.setSelected(authenticated);
+ this.allowNamed.setSelected(!authenticated);
+ }
+
+ public void setUsers(List<String> owners, List<String> all, List<RegistrantAccessPermission> permissions) {
+ ownersPalette.setObjects(all, owners);
+ usersPalette.setObjects(all, permissions);
+ }
+
+ public void setTeams(List<String> all, List<RegistrantAccessPermission> permissions) {
+ teamsPalette.setObjects(all, permissions);
+ }
+
+ public void setRepositories(List<RepositoryModel> repositories) {
+ repositoryNames.clear();
+ for (RepositoryModel repository : repositories) {
+ // force repo names to lowercase
+ // this means that repository name checking for rpc creation
+ // is case-insensitive, regardless of the Gitblit server's
+ // filesystem
+ repositoryNames.add(repository.name.toLowerCase());
+ }
+ }
+
+ public void setFederationSets(List<String> all, List<String> selected) {
+ setsPalette.setObjects(all, selected);
+ }
+
+ public void setIndexedBranches(List<String> all, List<String> selected) {
+ indexedBranchesPalette.setObjects(all, selected);
+ }
+
+ public void setPreReceiveScripts(List<String> all, List<String> inherited,
+ List<String> selected) {
+ preReceivePalette.setObjects(all, selected);
+ showInherited(inherited, preReceiveInherited);
+ }
+
+ public void setPostReceiveScripts(List<String> all, List<String> inherited,
+ List<String> selected) {
+ postReceivePalette.setObjects(all, selected);
+ showInherited(inherited, postReceiveInherited);
+ }
+
+ private void showInherited(List<String> list, JLabel label) {
+ StringBuilder sb = new StringBuilder();
+ if (list != null && list.size() > 0) {
+ sb.append("<html><body><b>INHERITED</b><ul style=\"margin-left:5px;list-style-type: none;\">");
+ for (String script : list) {
+ sb.append("<li>").append(script).append("</li>");
+ }
+ sb.append("</ul></body></html>");
+ }
+ label.setText(sb.toString());
+ }
+
+ public RepositoryModel getRepository() {
+ if (canceled) {
+ return null;
+ }
+ return repository;
+ }
+
+ public List<RegistrantAccessPermission> getUserAccessPermissions() {
+ return usersPalette.getPermissions();
+ }
+
+ public List<RegistrantAccessPermission> getTeamAccessPermissions() {
+ return teamsPalette.getPermissions();
+ }
+
+ public void setCustomFields(RepositoryModel repository, Map<String, String> customFields) {
+ customFieldsPanel.removeAll();
+ customTextfields = new ArrayList<JTextField>();
+
+ final Insets insets = new Insets(5, 5, 5, 5);
+ JPanel fields = new JPanel(new GridLayout(0, 1, 0, 5)) {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public Insets getInsets() {
+ return insets;
+ }
+ };
+
+ for (Map.Entry<String, String> entry : customFields.entrySet()) {
+ String field = entry.getKey();
+ String value = "";
+ if (repository.customFields != null && repository.customFields.containsKey(field)) {
+ value = repository.customFields.get(field);
+ }
+ JTextField textField = new JTextField(value);
+ textField.setName(field);
+
+ textField.setPreferredSize(new Dimension(450, 26));
+
+ fields.add(newFieldPanel(entry.getValue(), 250, textField));
+
+ customTextfields.add(textField);
+ }
+ JScrollPane jsp = new JScrollPane(fields);
+ jsp.getVerticalScrollBar().setBlockIncrement(100);
+ jsp.getVerticalScrollBar().setUnitIncrement(100);
+ jsp.setViewportBorder(null);
+ customFieldsPanel.setLayout(new FlowLayout(FlowLayout.LEFT));
+ customFieldsPanel.add(jsp);
+ }
+
+ /**
+ * ListCellRenderer to display descriptive text about the access
+ * restriction.
+ *
+ */
+ private class AccessRestrictionRenderer extends DefaultListCellRenderer {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public Component getListCellRendererComponent(JList list, Object value,
+ int index, boolean isSelected, boolean cellHasFocus) {
+ super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
+
+ if (value instanceof AccessRestrictionType) {
+ AccessRestrictionType restriction = (AccessRestrictionType) value;
+ switch (restriction) {
+ case NONE:
+ setText(Translation.get("gb.notRestricted"));
+ break;
+ case PUSH:
+ setText(Translation.get("gb.pushRestricted"));
+ break;
+ case CLONE:
+ setText(Translation.get("gb.cloneRestricted"));
+ break;
+ case VIEW:
+ setText(Translation.get("gb.viewRestricted"));
+ break;
+ }
+ } else {
+ setText(value.toString());
+ }
+ return this;
+ }
+ }
+
+ /**
+ * ListCellRenderer to display descriptive text about the federation
+ * strategy.
+ */
+ private class FederationStrategyRenderer extends JLabel implements
+ ListCellRenderer {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public Component getListCellRendererComponent(JList list, Object value,
+ int index, boolean isSelected, boolean cellHasFocus) {
+ if (value instanceof FederationStrategy) {
+ FederationStrategy strategy = (FederationStrategy) value;
+ switch (strategy) {
+ case EXCLUDE:
+ setText(Translation.get("gb.excludeFromFederation"));
+ break;
+ case FEDERATE_THIS:
+ setText(Translation.get("gb.federateThis"));
+ break;
+ case FEDERATE_ORIGIN:
+ setText(Translation.get("gb.federateOrigin"));
+ break;
+ }
+ } else {
+ setText(value.toString());
+ }
+ return this;
+ }
+ }
+}
diff --git a/src/main/java/com/gitblit/client/EditTeamDialog.java b/src/main/java/com/gitblit/client/EditTeamDialog.java
new file mode 100644
index 00000000..4d7af261
--- /dev/null
+++ b/src/main/java/com/gitblit/client/EditTeamDialog.java
@@ -0,0 +1,397 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.client;
+
+import java.awt.BorderLayout;
+import java.awt.Dimension;
+import java.awt.FlowLayout;
+import java.awt.Font;
+import java.awt.GridLayout;
+import java.awt.Insets;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.KeyEvent;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import javax.swing.ImageIcon;
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JComponent;
+import javax.swing.JDialog;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JRootPane;
+import javax.swing.JTabbedPane;
+import javax.swing.JTextField;
+import javax.swing.KeyStroke;
+
+import com.gitblit.Constants.AccessRestrictionType;
+import com.gitblit.Constants.AuthorizationControl;
+import com.gitblit.Constants.RegistrantType;
+import com.gitblit.models.RegistrantAccessPermission;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.ServerSettings;
+import com.gitblit.models.TeamModel;
+import com.gitblit.utils.StringUtils;
+
+public class EditTeamDialog extends JDialog {
+
+ private static final long serialVersionUID = 1L;
+
+ private final String teamname;
+
+ private final TeamModel team;
+
+ private final ServerSettings settings;
+
+ private boolean isCreate;
+
+ private boolean canceled = true;
+
+ private JTextField teamnameField;
+
+ private JCheckBox canAdminCheckbox;
+
+ private JCheckBox canForkCheckbox;
+
+ private JCheckBox canCreateCheckbox;
+
+ private JTextField mailingListsField;
+
+ private RegistrantPermissionsPanel repositoryPalette;
+
+ private JPalette<String> userPalette;
+
+ private JPalette<String> preReceivePalette;
+
+ private JLabel preReceiveInherited;
+
+ private JPalette<String> postReceivePalette;
+
+ private JLabel postReceiveInherited;
+
+ private Set<String> teamnames;
+
+ public EditTeamDialog(int protocolVersion, ServerSettings settings) {
+ this(protocolVersion, new TeamModel(""), settings);
+ this.isCreate = true;
+ setTitle(Translation.get("gb.newTeam"));
+ }
+
+ public EditTeamDialog(int protocolVersion, TeamModel aTeam, ServerSettings settings) {
+ super();
+ this.teamname = aTeam.name;
+ this.team = new TeamModel("");
+ this.settings = settings;
+ this.teamnames = new HashSet<String>();
+ this.isCreate = false;
+ initialize(protocolVersion, aTeam);
+ setModal(true);
+ setTitle(Translation.get("gb.edit") + ": " + aTeam.name);
+ setIconImage(new ImageIcon(getClass().getResource("/gitblt-favicon.png")).getImage());
+ }
+
+ @Override
+ protected JRootPane createRootPane() {
+ KeyStroke stroke = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);
+ JRootPane rootPane = new JRootPane();
+ rootPane.registerKeyboardAction(new ActionListener() {
+ public void actionPerformed(ActionEvent actionEvent) {
+ setVisible(false);
+ }
+ }, stroke, JComponent.WHEN_IN_FOCUSED_WINDOW);
+ return rootPane;
+ }
+
+ private void initialize(int protocolVersion, TeamModel aTeam) {
+ teamnameField = new JTextField(aTeam.name == null ? "" : aTeam.name, 25);
+
+ canAdminCheckbox = new JCheckBox(Translation.get("gb.canAdminDescription"), aTeam.canAdmin);
+ canForkCheckbox = new JCheckBox(Translation.get("gb.canForkDescription"), aTeam.canFork);
+ canCreateCheckbox = new JCheckBox(Translation.get("gb.canCreateDescription"), aTeam.canCreate);
+
+ mailingListsField = new JTextField(aTeam.mailingLists == null ? ""
+ : StringUtils.flattenStrings(aTeam.mailingLists, " "), 50);
+
+ JPanel fieldsPanel = new JPanel(new GridLayout(0, 1));
+ fieldsPanel.add(newFieldPanel(Translation.get("gb.teamName"), teamnameField));
+ fieldsPanel.add(newFieldPanel(Translation.get("gb.canAdmin"), canAdminCheckbox));
+ fieldsPanel.add(newFieldPanel(Translation.get("gb.canFork"), canForkCheckbox));
+ fieldsPanel.add(newFieldPanel(Translation.get("gb.canCreate"), canCreateCheckbox));
+
+ fieldsPanel.add(newFieldPanel(Translation.get("gb.mailingLists"), mailingListsField));
+
+ final Insets _insets = new Insets(5, 5, 5, 5);
+ repositoryPalette = new RegistrantPermissionsPanel(RegistrantType.REPOSITORY);
+ userPalette = new JPalette<String>();
+ userPalette.setEnabled(settings.supportsTeamMembershipChanges);
+
+ JPanel fieldsPanelTop = new JPanel(new BorderLayout());
+ fieldsPanelTop.add(fieldsPanel, BorderLayout.NORTH);
+
+ JPanel repositoriesPanel = new JPanel(new BorderLayout()) {
+
+ private static final long serialVersionUID = 1L;
+
+ public Insets getInsets() {
+ return _insets;
+ }
+ };
+ repositoriesPanel.add(repositoryPalette, BorderLayout.CENTER);
+
+ JPanel usersPanel = new JPanel(new BorderLayout()) {
+
+ private static final long serialVersionUID = 1L;
+
+ public Insets getInsets() {
+ return _insets;
+ }
+ };
+ usersPanel.add(userPalette, BorderLayout.CENTER);
+
+ preReceivePalette = new JPalette<String>(true);
+ preReceiveInherited = new JLabel();
+ JPanel preReceivePanel = new JPanel(new BorderLayout(5, 5));
+ preReceivePanel.add(preReceivePalette, BorderLayout.CENTER);
+ preReceivePanel.add(preReceiveInherited, BorderLayout.WEST);
+
+ postReceivePalette = new JPalette<String>(true);
+ postReceiveInherited = new JLabel();
+ JPanel postReceivePanel = new JPanel(new BorderLayout(5, 5));
+ postReceivePanel.add(postReceivePalette, BorderLayout.CENTER);
+ postReceivePanel.add(postReceiveInherited, BorderLayout.WEST);
+
+ JTabbedPane panel = new JTabbedPane(JTabbedPane.TOP);
+ panel.addTab(Translation.get("gb.general"), fieldsPanelTop);
+ panel.addTab(Translation.get("gb.teamMembers"), usersPanel);
+ panel.addTab(Translation.get("gb.restrictedRepositories"), repositoriesPanel);
+ panel.addTab(Translation.get("gb.preReceiveScripts"), preReceivePanel);
+ panel.addTab(Translation.get("gb.postReceiveScripts"), postReceivePanel);
+
+ JButton createButton = new JButton(Translation.get("gb.save"));
+ createButton.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent event) {
+ if (validateFields()) {
+ canceled = false;
+ setVisible(false);
+ }
+ }
+ });
+
+ JButton cancelButton = new JButton(Translation.get("gb.cancel"));
+ cancelButton.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent event) {
+ canceled = true;
+ setVisible(false);
+ }
+ });
+
+ JPanel controls = new JPanel();
+ controls.add(cancelButton);
+ controls.add(createButton);
+
+ JPanel centerPanel = new JPanel(new BorderLayout(5, 5)) {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public Insets getInsets() {
+ return _insets;
+ }
+ };
+ centerPanel.add(panel, BorderLayout.CENTER);
+ centerPanel.add(controls, BorderLayout.SOUTH);
+
+ getContentPane().setLayout(new BorderLayout(5, 5));
+ getContentPane().add(centerPanel, BorderLayout.CENTER);
+ pack();
+ }
+
+ private JPanel newFieldPanel(String label, JComponent comp) {
+ JLabel fieldLabel = new JLabel(label);
+ fieldLabel.setFont(fieldLabel.getFont().deriveFont(Font.BOLD));
+ fieldLabel.setPreferredSize(new Dimension(150, 20));
+ JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 0));
+ panel.add(fieldLabel);
+ panel.add(comp);
+ return panel;
+ }
+
+ private boolean validateFields() {
+ String tname = teamnameField.getText();
+ if (StringUtils.isEmpty(tname)) {
+ error("Please enter a team name!");
+ return false;
+ }
+
+ boolean rename = false;
+ // verify teamname uniqueness on create
+ if (isCreate) {
+ if (teamnames.contains(tname.toLowerCase())) {
+ error(MessageFormat.format("Team name ''{0}'' is unavailable.", tname));
+ return false;
+ }
+ } else {
+ // check rename collision
+ rename = !StringUtils.isEmpty(teamname) && !teamname.equalsIgnoreCase(tname);
+ if (rename) {
+ if (teamnames.contains(tname.toLowerCase())) {
+ error(MessageFormat.format(
+ "Failed to rename ''{0}'' because ''{1}'' already exists.", teamname,
+ tname));
+ return false;
+ }
+ }
+ }
+ team.name = tname;
+
+ team.canAdmin = canAdminCheckbox.isSelected();
+ team.canFork = canForkCheckbox.isSelected();
+ team.canCreate = canCreateCheckbox.isSelected();
+
+ String ml = mailingListsField.getText();
+ if (!StringUtils.isEmpty(ml)) {
+ Set<String> list = new HashSet<String>();
+ for (String address : ml.split("(,|\\s)")) {
+ if (StringUtils.isEmpty(address)) {
+ continue;
+ }
+ list.add(address.toLowerCase());
+ }
+ team.mailingLists.clear();
+ team.mailingLists.addAll(list);
+ }
+
+ for (RegistrantAccessPermission rp : repositoryPalette.getPermissions()) {
+ team.setRepositoryPermission(rp.registrant, rp.permission);
+ }
+
+ team.users.clear();
+ team.users.addAll(userPalette.getSelections());
+
+ team.preReceiveScripts.clear();
+ team.preReceiveScripts.addAll(preReceivePalette.getSelections());
+
+ team.postReceiveScripts.clear();
+ team.postReceiveScripts.addAll(postReceivePalette.getSelections());
+
+ return true;
+ }
+
+ private void error(String message) {
+ JOptionPane.showMessageDialog(EditTeamDialog.this, message, Translation.get("gb.error"),
+ JOptionPane.ERROR_MESSAGE);
+ }
+
+ public void setTeams(List<TeamModel> teams) {
+ teamnames.clear();
+ for (TeamModel team : teams) {
+ teamnames.add(team.name.toLowerCase());
+ }
+ }
+
+ public void setRepositories(List<RepositoryModel> repositories, List<RegistrantAccessPermission> permissions) {
+ List<String> restricted = new ArrayList<String>();
+ for (RepositoryModel repo : repositories) {
+ if (repo.accessRestriction.exceeds(AccessRestrictionType.NONE)
+ && repo.authorizationControl.equals(AuthorizationControl.NAMED)) {
+ restricted.add(repo.name);
+ }
+ }
+ StringUtils.sortRepositorynames(restricted);
+
+ List<String> list = new ArrayList<String>();
+ // repositories
+ list.add(".*");
+ // all repositories excluding personal repositories
+ list.add("[^~].*");
+ String lastProject = null;
+ for (String repo : restricted) {
+ String projectPath = StringUtils.getFirstPathElement(repo);
+ if (lastProject == null || !lastProject.equalsIgnoreCase(projectPath)) {
+ lastProject = projectPath;
+ if (!StringUtils.isEmpty(projectPath)) {
+ // regex for all repositories within a project
+ list.add(projectPath + "/.*");
+ }
+ list.add(repo);
+ }
+ }
+
+ // remove repositories for which user already has a permission
+ if (permissions == null) {
+ permissions = new ArrayList<RegistrantAccessPermission>();
+ } else {
+ for (RegistrantAccessPermission rp : permissions) {
+ list.remove(rp.registrant);
+ }
+ }
+ repositoryPalette.setObjects(list, permissions);
+ }
+
+ public void setUsers(List<String> users, List<String> selected) {
+ Collections.sort(users);
+ if (selected != null) {
+ Collections.sort(selected);
+ }
+ userPalette.setObjects(users, selected);
+ }
+
+ public void setPreReceiveScripts(List<String> unused, List<String> inherited,
+ List<String> selected) {
+ Collections.sort(unused);
+ if (selected != null) {
+ Collections.sort(selected);
+ }
+ preReceivePalette.setObjects(unused, selected);
+ showInherited(inherited, preReceiveInherited);
+ }
+
+ public void setPostReceiveScripts(List<String> unused, List<String> inherited,
+ List<String> selected) {
+ Collections.sort(unused);
+ if (selected != null) {
+ Collections.sort(selected);
+ }
+ postReceivePalette.setObjects(unused, selected);
+ showInherited(inherited, postReceiveInherited);
+ }
+
+ private void showInherited(List<String> list, JLabel label) {
+ StringBuilder sb = new StringBuilder();
+ if (list != null && list.size() > 0) {
+ sb.append("<html><body><b>INHERITED</b><ul style=\"margin-left:5px;list-style-type: none;\">");
+ for (String script : list) {
+ sb.append("<li>").append(script).append("</li>");
+ }
+ sb.append("</ul></body></html>");
+ }
+ label.setText(sb.toString());
+ }
+
+ public TeamModel getTeam() {
+ if (canceled) {
+ return null;
+ }
+ return team;
+ }
+}
diff --git a/src/main/java/com/gitblit/client/EditUserDialog.java b/src/main/java/com/gitblit/client/EditUserDialog.java
new file mode 100644
index 00000000..0400f5c9
--- /dev/null
+++ b/src/main/java/com/gitblit/client/EditUserDialog.java
@@ -0,0 +1,466 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.client;
+
+import java.awt.BorderLayout;
+import java.awt.Dimension;
+import java.awt.FlowLayout;
+import java.awt.Font;
+import java.awt.GridLayout;
+import java.awt.Insets;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.KeyEvent;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.swing.ImageIcon;
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JComponent;
+import javax.swing.JDialog;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JPasswordField;
+import javax.swing.JRootPane;
+import javax.swing.JTabbedPane;
+import javax.swing.JTextField;
+import javax.swing.KeyStroke;
+
+import com.gitblit.Constants.AccessRestrictionType;
+import com.gitblit.Constants.AuthorizationControl;
+import com.gitblit.Constants.PermissionType;
+import com.gitblit.Constants.RegistrantType;
+import com.gitblit.Keys;
+import com.gitblit.models.RegistrantAccessPermission;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.ServerSettings;
+import com.gitblit.models.TeamModel;
+import com.gitblit.models.UserModel;
+import com.gitblit.utils.StringUtils;
+
+public class EditUserDialog extends JDialog {
+
+ private static final long serialVersionUID = 1L;
+
+ private final String username;
+
+ private final UserModel user;
+
+ private final ServerSettings settings;
+
+ private boolean isCreate;
+
+ private boolean canceled = true;
+
+ private JTextField usernameField;
+
+ private JPasswordField passwordField;
+
+ private JPasswordField confirmPasswordField;
+
+ private JTextField displayNameField;
+
+ private JTextField emailAddressField;
+
+ private JCheckBox canAdminCheckbox;
+
+ private JCheckBox canForkCheckbox;
+
+ private JCheckBox canCreateCheckbox;
+
+ private JCheckBox notFederatedCheckbox;
+
+ private JTextField organizationalUnitField;
+
+ private JTextField organizationField;
+
+ private JTextField localityField;
+
+ private JTextField stateProvinceField;
+
+ private JTextField countryCodeField;
+
+ private RegistrantPermissionsPanel repositoryPalette;
+
+ private JPalette<TeamModel> teamsPalette;
+
+ private Set<String> usernames;
+
+ public EditUserDialog(int protocolVersion, ServerSettings settings) {
+ this(protocolVersion, new UserModel(""), settings);
+ this.isCreate = true;
+ setTitle(Translation.get("gb.newUser"));
+ }
+
+ public EditUserDialog(int protocolVersion, UserModel anUser, ServerSettings settings) {
+ super();
+ this.username = anUser.username;
+ this.user = new UserModel("");
+ this.settings = settings;
+ this.usernames = new HashSet<String>();
+ this.isCreate = false;
+ initialize(protocolVersion, anUser);
+ setModal(true);
+ setTitle(Translation.get("gb.edit") + ": " + anUser.username);
+ setIconImage(new ImageIcon(getClass().getResource("/gitblt-favicon.png")).getImage());
+ }
+
+ @Override
+ protected JRootPane createRootPane() {
+ KeyStroke stroke = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);
+ JRootPane rootPane = new JRootPane();
+ rootPane.registerKeyboardAction(new ActionListener() {
+ public void actionPerformed(ActionEvent actionEvent) {
+ setVisible(false);
+ }
+ }, stroke, JComponent.WHEN_IN_FOCUSED_WINDOW);
+ return rootPane;
+ }
+
+ private void initialize(int protocolVersion, UserModel anUser) {
+ usernameField = new JTextField(anUser.username == null ? "" : anUser.username, 25);
+ passwordField = new JPasswordField(anUser.password == null ? "" : anUser.password, 25);
+ confirmPasswordField = new JPasswordField(anUser.password == null ? "" : anUser.password,
+ 25);
+ displayNameField = new JTextField(anUser.displayName == null ? "" : anUser.displayName, 25);
+ emailAddressField = new JTextField(anUser.emailAddress == null ? "" : anUser.emailAddress, 25);
+ canAdminCheckbox = new JCheckBox(Translation.get("gb.canAdminDescription"), anUser.canAdmin);
+ canForkCheckbox = new JCheckBox(Translation.get("gb.canForkDescription"), anUser.canFork);
+ canCreateCheckbox = new JCheckBox(Translation.get("gb.canCreateDescription"), anUser.canCreate);
+ notFederatedCheckbox = new JCheckBox(
+ Translation.get("gb.excludeFromFederationDescription"),
+ anUser.excludeFromFederation);
+
+ organizationalUnitField = new JTextField(anUser.organizationalUnit == null ? "" : anUser.organizationalUnit, 25);
+ organizationField = new JTextField(anUser.organization == null ? "" : anUser.organization, 25);
+ localityField = new JTextField(anUser.locality == null ? "" : anUser.locality, 25);
+ stateProvinceField = new JTextField(anUser.stateProvince == null ? "" : anUser.stateProvince, 25);
+ countryCodeField = new JTextField(anUser.countryCode == null ? "" : anUser.countryCode, 15);
+
+ // credentials are optionally controlled by 3rd-party authentication
+ usernameField.setEnabled(settings.supportsCredentialChanges);
+ passwordField.setEnabled(settings.supportsCredentialChanges);
+ confirmPasswordField.setEnabled(settings.supportsCredentialChanges);
+
+ displayNameField.setEnabled(settings.supportsDisplayNameChanges);
+ emailAddressField.setEnabled(settings.supportsEmailAddressChanges);
+
+ organizationalUnitField.setEnabled(settings.supportsDisplayNameChanges);
+ organizationField.setEnabled(settings.supportsDisplayNameChanges);
+ localityField.setEnabled(settings.supportsDisplayNameChanges);
+ stateProvinceField.setEnabled(settings.supportsDisplayNameChanges);
+ countryCodeField.setEnabled(settings.supportsDisplayNameChanges);
+
+ JPanel fieldsPanel = new JPanel(new GridLayout(0, 1));
+ fieldsPanel.add(newFieldPanel(Translation.get("gb.username"), usernameField));
+ fieldsPanel.add(newFieldPanel(Translation.get("gb.password"), passwordField));
+ fieldsPanel.add(newFieldPanel(Translation.get("gb.confirmPassword"), confirmPasswordField));
+ fieldsPanel.add(newFieldPanel(Translation.get("gb.displayName"), displayNameField));
+ fieldsPanel.add(newFieldPanel(Translation.get("gb.emailAddress"), emailAddressField));
+ fieldsPanel.add(newFieldPanel(Translation.get("gb.canAdmin"), canAdminCheckbox));
+ fieldsPanel.add(newFieldPanel(Translation.get("gb.canFork"), canForkCheckbox));
+ fieldsPanel.add(newFieldPanel(Translation.get("gb.canCreate"), canCreateCheckbox));
+ fieldsPanel.add(newFieldPanel(Translation.get("gb.excludeFromFederation"),
+ notFederatedCheckbox));
+
+ JPanel attributesPanel = new JPanel(new GridLayout(0, 1, 5, 2));
+ attributesPanel.add(newFieldPanel(Translation.get("gb.organizationalUnit") + " (OU)", organizationalUnitField));
+ attributesPanel.add(newFieldPanel(Translation.get("gb.organization") + " (O)", organizationField));
+ attributesPanel.add(newFieldPanel(Translation.get("gb.locality") + " (L)", localityField));
+ attributesPanel.add(newFieldPanel(Translation.get("gb.stateProvince") + " (ST)", stateProvinceField));
+ attributesPanel.add(newFieldPanel(Translation.get("gb.countryCode") + " (C)", countryCodeField));
+
+ final Insets _insets = new Insets(5, 5, 5, 5);
+ repositoryPalette = new RegistrantPermissionsPanel(RegistrantType.REPOSITORY);
+ teamsPalette = new JPalette<TeamModel>();
+ teamsPalette.setEnabled(settings.supportsTeamMembershipChanges);
+
+ JPanel fieldsPanelTop = new JPanel(new BorderLayout());
+ fieldsPanelTop.add(fieldsPanel, BorderLayout.NORTH);
+
+ JPanel attributesPanelTop = new JPanel(new BorderLayout());
+ attributesPanelTop.add(attributesPanel, BorderLayout.NORTH);
+
+ JPanel repositoriesPanel = new JPanel(new BorderLayout()) {
+
+ private static final long serialVersionUID = 1L;
+
+ public Insets getInsets() {
+ return _insets;
+ }
+ };
+ repositoriesPanel.add(repositoryPalette, BorderLayout.CENTER);
+
+ JPanel teamsPanel = new JPanel(new BorderLayout()) {
+
+ private static final long serialVersionUID = 1L;
+
+ public Insets getInsets() {
+ return _insets;
+ }
+ };
+ teamsPanel.add(teamsPalette, BorderLayout.CENTER);
+
+ JTabbedPane panel = new JTabbedPane(JTabbedPane.TOP);
+ panel.addTab(Translation.get("gb.general"), fieldsPanelTop);
+ panel.addTab(Translation.get("gb.attributes"), attributesPanelTop);
+ if (protocolVersion > 1) {
+ panel.addTab(Translation.get("gb.teamMemberships"), teamsPanel);
+ }
+ panel.addTab(Translation.get("gb.restrictedRepositories"), repositoriesPanel);
+
+ JButton createButton = new JButton(Translation.get("gb.save"));
+ createButton.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent event) {
+ if (validateFields()) {
+ canceled = false;
+ setVisible(false);
+ }
+ }
+ });
+
+ JButton cancelButton = new JButton(Translation.get("gb.cancel"));
+ cancelButton.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent event) {
+ canceled = true;
+ setVisible(false);
+ }
+ });
+
+ JPanel controls = new JPanel();
+ controls.add(cancelButton);
+ controls.add(createButton);
+
+ JPanel centerPanel = new JPanel(new BorderLayout(5, 5)) {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public Insets getInsets() {
+ return _insets;
+ }
+ };
+ centerPanel.add(panel, BorderLayout.CENTER);
+ centerPanel.add(controls, BorderLayout.SOUTH);
+
+ getContentPane().setLayout(new BorderLayout(5, 5));
+ getContentPane().add(centerPanel, BorderLayout.CENTER);
+ pack();
+ }
+
+ private JPanel newFieldPanel(String label, JComponent comp) {
+ JLabel fieldLabel = new JLabel(label);
+ fieldLabel.setFont(fieldLabel.getFont().deriveFont(Font.BOLD));
+ fieldLabel.setPreferredSize(new Dimension(150, 20));
+ JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 0));
+ panel.add(fieldLabel);
+ panel.add(comp);
+ return panel;
+ }
+
+ private boolean validateFields() {
+ if (StringUtils.isEmpty(usernameField.getText())) {
+ error("Please enter a username!");
+ return false;
+ }
+ String uname = usernameField.getText().toLowerCase();
+ boolean rename = false;
+ // verify username uniqueness on create
+ if (isCreate) {
+ if (usernames.contains(uname)) {
+ error(MessageFormat.format("Username ''{0}'' is unavailable.", uname));
+ return false;
+ }
+ } else {
+ // check rename collision
+ rename = !StringUtils.isEmpty(username) && !username.equalsIgnoreCase(uname);
+ if (rename) {
+ if (usernames.contains(uname)) {
+ error(MessageFormat.format(
+ "Failed to rename ''{0}'' because ''{1}'' already exists.", username,
+ uname));
+ return false;
+ }
+ }
+ }
+ user.username = uname;
+
+ int minLength = settings.get(Keys.realm.minPasswordLength).getInteger(5);
+ if (minLength < 4) {
+ minLength = 4;
+ }
+
+ String password = new String(passwordField.getPassword());
+ if (StringUtils.isEmpty(password) || password.length() < minLength) {
+ error(MessageFormat.format("Password is too short. Minimum length is {0} characters.",
+ minLength));
+ return false;
+ }
+ if (!password.toUpperCase().startsWith(StringUtils.MD5_TYPE)
+ && !password.toUpperCase().startsWith(StringUtils.COMBINED_MD5_TYPE)) {
+ String cpw = new String(confirmPasswordField.getPassword());
+ if (cpw == null || cpw.length() != password.length()) {
+ error("Please confirm the password!");
+ return false;
+ }
+ if (!password.equals(cpw)) {
+ error("Passwords do not match!");
+ return false;
+ }
+
+ String type = settings.get(Keys.realm.passwordStorage).getString("md5");
+ if (type.equalsIgnoreCase("md5")) {
+ // store MD5 digest of password
+ user.password = StringUtils.MD5_TYPE + StringUtils.getMD5(password);
+ } else if (type.equalsIgnoreCase("combined-md5")) {
+ // store MD5 digest of username+password
+ user.password = StringUtils.COMBINED_MD5_TYPE
+ + StringUtils.getMD5(user.username + password);
+ } else {
+ // plain-text password
+ user.password = password;
+ }
+ } else if (rename && password.toUpperCase().startsWith(StringUtils.COMBINED_MD5_TYPE)) {
+ error("Gitblit is configured for combined-md5 password hashing. You must enter a new password on account rename.");
+ return false;
+ } else {
+ // no change in password
+ user.password = password;
+ }
+
+ user.displayName = displayNameField.getText().trim();
+ user.emailAddress = emailAddressField.getText().trim();
+
+ user.canAdmin = canAdminCheckbox.isSelected();
+ user.canFork = canForkCheckbox.isSelected();
+ user.canCreate = canCreateCheckbox.isSelected();
+ user.excludeFromFederation = notFederatedCheckbox.isSelected();
+
+ user.organizationalUnit = organizationalUnitField.getText().trim();
+ user.organization = organizationField.getText().trim();
+ user.locality = localityField.getText().trim();
+ user.stateProvince = stateProvinceField.getText().trim();
+ user.countryCode = countryCodeField.getText().trim();
+
+ for (RegistrantAccessPermission rp : repositoryPalette.getPermissions()) {
+ user.setRepositoryPermission(rp.registrant, rp.permission);
+ }
+
+ user.teams.clear();
+ user.teams.addAll(teamsPalette.getSelections());
+ return true;
+ }
+
+ private void error(String message) {
+ JOptionPane.showMessageDialog(EditUserDialog.this, message, Translation.get("gb.error"),
+ JOptionPane.ERROR_MESSAGE);
+ }
+
+ public void setUsers(List<UserModel> users) {
+ usernames.clear();
+ for (UserModel user : users) {
+ usernames.add(user.username.toLowerCase());
+ }
+ }
+
+ public void setRepositories(List<RepositoryModel> repositories, List<RegistrantAccessPermission> permissions) {
+ Map<String, RepositoryModel> repoMap = new HashMap<String, RepositoryModel>();
+ List<String> restricted = new ArrayList<String>();
+ for (RepositoryModel repo : repositories) {
+ // exclude Owner or personal repositories
+ if (!repo.isOwner(username) && !repo.isUsersPersonalRepository(username)) {
+ if (repo.accessRestriction.exceeds(AccessRestrictionType.NONE)
+ && repo.authorizationControl.equals(AuthorizationControl.NAMED)) {
+ restricted.add(repo.name);
+ }
+ }
+ repoMap.put(repo.name.toLowerCase(), repo);
+ }
+ StringUtils.sortRepositorynames(restricted);
+
+ List<String> list = new ArrayList<String>();
+ // repositories
+ list.add(".*");
+ // all repositories excluding personal repositories
+ list.add("[^~].*");
+ String lastProject = null;
+ for (String repo : restricted) {
+ String projectPath = StringUtils.getFirstPathElement(repo).toLowerCase();
+ if (lastProject == null || !lastProject.equalsIgnoreCase(projectPath)) {
+ lastProject = projectPath;
+ if (!StringUtils.isEmpty(projectPath)) {
+ // regex for all repositories within a project
+ list.add(projectPath + "/.*");
+ }
+ }
+ list.add(repo);
+ }
+
+ // remove repositories for which user already has a permission
+ if (permissions == null) {
+ permissions = new ArrayList<RegistrantAccessPermission>();
+ } else {
+ for (RegistrantAccessPermission rp : permissions) {
+ list.remove(rp.registrant.toLowerCase());
+ }
+ }
+
+ // update owner and missing permissions for editing
+ for (RegistrantAccessPermission permission : permissions) {
+ if (permission.mutable && PermissionType.EXPLICIT.equals(permission.permissionType)) {
+ // Ensure this is NOT an owner permission - which is non-editable
+ // We don't know this from within the usermodel, ownership is a
+ // property of a repository.
+ RepositoryModel rm = repoMap.get(permission.registrant.toLowerCase());
+ if (rm == null) {
+ permission.permissionType = PermissionType.MISSING;
+ permission.mutable = false;
+ continue;
+ }
+ boolean isOwner = rm.isOwner(username);
+ if (isOwner) {
+ permission.permissionType = PermissionType.OWNER;
+ permission.mutable = false;
+ }
+ }
+ }
+
+ repositoryPalette.setObjects(list, permissions);
+ }
+
+ public void setTeams(List<TeamModel> teams, List<TeamModel> selected) {
+ Collections.sort(teams);
+ if (selected != null) {
+ Collections.sort(selected);
+ }
+ teamsPalette.setObjects(teams, selected);
+ }
+
+ public UserModel getUser() {
+ if (canceled) {
+ return null;
+ }
+ return user;
+ }
+}
diff --git a/src/main/java/com/gitblit/client/FeedEntryTableModel.java b/src/main/java/com/gitblit/client/FeedEntryTableModel.java
new file mode 100644
index 00000000..0b0ef178
--- /dev/null
+++ b/src/main/java/com/gitblit/client/FeedEntryTableModel.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.client;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+
+import javax.swing.table.AbstractTableModel;
+
+import com.gitblit.models.FeedEntryModel;
+
+/**
+ * Table model for a list of retrieved feed entries.
+ *
+ * @author James Moger
+ *
+ */
+public class FeedEntryTableModel extends AbstractTableModel {
+
+ private static final long serialVersionUID = 1L;
+
+ List<FeedEntryModel> entries;
+
+ enum Columns {
+ Date, Repository, Branch, Author, Message;
+
+ @Override
+ public String toString() {
+ return name().replace('_', ' ');
+ }
+ }
+
+ public FeedEntryTableModel() {
+ this.entries = new ArrayList<FeedEntryModel>();
+ }
+
+ public void setEntries(List<FeedEntryModel> entries) {
+ this.entries = entries;
+ Collections.sort(entries);
+ }
+
+ @Override
+ public int getRowCount() {
+ return entries.size();
+ }
+
+ @Override
+ public int getColumnCount() {
+ return Columns.values().length;
+ }
+
+ @Override
+ public String getColumnName(int column) {
+ Columns col = Columns.values()[column];
+ switch (col) {
+ case Date:
+ return Translation.get("gb.date");
+ case Repository:
+ return Translation.get("gb.repository");
+ case Branch:
+ return Translation.get("gb.branch");
+ case Author:
+ return Translation.get("gb.author");
+ case Message:
+ return Translation.get("gb.message");
+ }
+ return "";
+ }
+
+ /**
+ * Returns <code>Object.class</code> regardless of <code>columnIndex</code>.
+ *
+ * @param columnIndex
+ * the column being queried
+ * @return the Object.class
+ */
+ public Class<?> getColumnClass(int columnIndex) {
+ if (Columns.Date.ordinal() == columnIndex) {
+ return Date.class;
+ } else if (Columns.Message.ordinal() == columnIndex) {
+ return FeedEntryModel.class;
+ }
+ return String.class;
+ }
+
+ @Override
+ public Object getValueAt(int rowIndex, int columnIndex) {
+ FeedEntryModel entry = entries.get(rowIndex);
+ Columns col = Columns.values()[columnIndex];
+ switch (col) {
+ case Date:
+ return entry.published;
+ case Repository:
+ return entry.repository;
+ case Branch:
+ return entry.branch;
+ case Author:
+ return entry.author;
+ case Message:
+ return entry;
+ }
+ return null;
+ }
+
+ public FeedEntryModel get(int modelRow) {
+ return entries.get(modelRow);
+ }
+}
diff --git a/src/main/java/com/gitblit/client/FeedsPanel.java b/src/main/java/com/gitblit/client/FeedsPanel.java
new file mode 100644
index 00000000..392636e4
--- /dev/null
+++ b/src/main/java/com/gitblit/client/FeedsPanel.java
@@ -0,0 +1,405 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.client;
+
+import java.awt.BorderLayout;
+import java.awt.FlowLayout;
+import java.awt.Insets;
+import java.awt.Rectangle;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import javax.swing.DefaultComboBoxModel;
+import javax.swing.JButton;
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTable;
+import javax.swing.RowFilter;
+import javax.swing.event.ListSelectionEvent;
+import javax.swing.event.ListSelectionListener;
+import javax.swing.table.TableRowSorter;
+
+import com.gitblit.models.FeedEntryModel;
+import com.gitblit.models.FeedModel;
+import com.gitblit.utils.StringUtils;
+
+/**
+ * RSS Feeds Panel displays recent entries and launches the browser to view the
+ * commit. commitdiff, or tree of a commit.
+ *
+ * @author James Moger
+ *
+ */
+public abstract class FeedsPanel extends JPanel {
+
+ private static final long serialVersionUID = 1L;
+
+ private final GitblitClient gitblit;
+
+ private final String ALL = "*";
+
+ private FeedEntryTableModel tableModel;
+
+ private TableRowSorter<FeedEntryTableModel> defaultSorter;
+
+ private HeaderPanel header;
+
+ private JTable table;
+
+ private DefaultComboBoxModel repositoryChoices;
+
+ private JComboBox repositorySelector;
+
+ private DefaultComboBoxModel authorChoices;
+
+ private JComboBox authorSelector;
+
+ private int page;
+
+ private JButton prev;
+
+ private JButton next;
+
+ public FeedsPanel(GitblitClient gitblit) {
+ super();
+ this.gitblit = gitblit;
+ initialize();
+ }
+
+ private void initialize() {
+
+ prev = new JButton("<");
+ prev.setToolTipText(Translation.get("gb.pagePrevious"));
+ prev.setEnabled(false);
+ prev.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ refreshFeeds(--page);
+ }
+ });
+
+ next = new JButton(">");
+ next.setToolTipText(Translation.get("gb.pageNext"));
+ next.setEnabled(false);
+ next.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ refreshFeeds(++page);
+ }
+ });
+
+ JButton refreshFeeds = new JButton(Translation.get("gb.refresh"));
+ refreshFeeds.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ refreshFeeds(0);
+ }
+ });
+
+ final JButton viewCommit = new JButton(Translation.get("gb.view"));
+ viewCommit.setEnabled(false);
+ viewCommit.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ viewCommit();
+ }
+ });
+
+ final JButton viewCommitDiff = new JButton(Translation.get("gb.commitdiff"));
+ viewCommitDiff.setEnabled(false);
+ viewCommitDiff.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ viewCommitDiff();
+ }
+ });
+
+ final JButton viewTree = new JButton(Translation.get("gb.tree"));
+ viewTree.setEnabled(false);
+ viewTree.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ viewTree();
+ }
+ });
+
+ JButton subscribeFeeds = new JButton(Translation.get("gb.subscribe") + "...");
+ subscribeFeeds.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ subscribeFeeds(gitblit.getAvailableFeeds());
+ }
+ });
+
+ JPanel controls = new JPanel(new FlowLayout(FlowLayout.CENTER, Utils.MARGIN, 0));
+ controls.add(refreshFeeds);
+ controls.add(subscribeFeeds);
+ controls.add(viewCommit);
+ controls.add(viewCommitDiff);
+ controls.add(viewTree);
+
+ NameRenderer nameRenderer = new NameRenderer();
+ tableModel = new FeedEntryTableModel();
+ header = new HeaderPanel(Translation.get("gb.activity"), "feed_16x16.png");
+ table = Utils.newTable(tableModel, Utils.DATE_FORMAT);
+ defaultSorter = new TableRowSorter<FeedEntryTableModel>(tableModel);
+ String name = table.getColumnName(FeedEntryTableModel.Columns.Author.ordinal());
+ table.getColumn(name).setCellRenderer(nameRenderer);
+ name = table.getColumnName(FeedEntryTableModel.Columns.Repository.ordinal());
+ table.getColumn(name).setCellRenderer(nameRenderer);
+
+ name = table.getColumnName(FeedEntryTableModel.Columns.Branch.ordinal());
+ table.getColumn(name).setCellRenderer(new BranchRenderer());
+
+ name = table.getColumnName(FeedEntryTableModel.Columns.Message.ordinal());
+ table.getColumn(name).setCellRenderer(new MessageRenderer(gitblit));
+
+ table.addMouseListener(new MouseAdapter() {
+ public void mouseClicked(MouseEvent e) {
+ if (e.getClickCount() == 2) {
+ if (e.isControlDown()) {
+ viewCommitDiff();
+ } else {
+ viewCommit();
+ }
+ }
+ }
+ });
+
+ table.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
+ @Override
+ public void valueChanged(ListSelectionEvent e) {
+ if (e.getValueIsAdjusting()) {
+ return;
+ }
+ boolean singleSelection = table.getSelectedRowCount() == 1;
+ viewCommit.setEnabled(singleSelection);
+ viewCommitDiff.setEnabled(singleSelection);
+ viewTree.setEnabled(singleSelection);
+ }
+ });
+
+ repositoryChoices = new DefaultComboBoxModel();
+ repositorySelector = new JComboBox(repositoryChoices);
+ repositorySelector.setRenderer(nameRenderer);
+ repositorySelector.setForeground(nameRenderer.getForeground());
+ repositorySelector.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent event) {
+ // repopulate the author list based on repository selection
+ // preserve author selection, if possible
+ String selectedAuthor = null;
+ if (authorSelector.getSelectedIndex() > -1) {
+ selectedAuthor = authorSelector.getSelectedItem().toString();
+ }
+ updateAuthors();
+ if (selectedAuthor != null) {
+ if (authorChoices.getIndexOf(selectedAuthor) > -1) {
+ authorChoices.setSelectedItem(selectedAuthor);
+ }
+ }
+ filterFeeds();
+ }
+ });
+ authorChoices = new DefaultComboBoxModel();
+ authorSelector = new JComboBox(authorChoices);
+ authorSelector.setRenderer(nameRenderer);
+ authorSelector.setForeground(nameRenderer.getForeground());
+ authorSelector.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent event) {
+ filterFeeds();
+ }
+ });
+ JPanel northControls = new JPanel(new FlowLayout(FlowLayout.LEFT, Utils.MARGIN, 0));
+ northControls.add(new JLabel(Translation.get("gb.repository")));
+ northControls.add(repositorySelector);
+ northControls.add(new JLabel(Translation.get("gb.author")));
+ northControls.add(authorSelector);
+// northControls.add(prev);
+// northControls.add(next);
+
+ JPanel northPanel = new JPanel(new BorderLayout(0, Utils.MARGIN));
+ northPanel.add(header, BorderLayout.NORTH);
+ northPanel.add(northControls, BorderLayout.CENTER);
+
+ setLayout(new BorderLayout(Utils.MARGIN, Utils.MARGIN));
+ add(northPanel, BorderLayout.NORTH);
+ add(new JScrollPane(table), BorderLayout.CENTER);
+ add(controls, BorderLayout.SOUTH);
+ }
+
+ @Override
+ public Insets getInsets() {
+ return Utils.INSETS;
+ }
+
+ protected void refreshFeeds(final int page) {
+ this.page = page;
+ GitblitWorker worker = new GitblitWorker(FeedsPanel.this, null) {
+ @Override
+ protected Boolean doRequest() throws IOException {
+ gitblit.refreshSubscribedFeeds(page);
+ return true;
+ }
+
+ @Override
+ protected void onSuccess() {
+ updateTable(false);
+ }
+ };
+ worker.execute();
+ }
+
+ protected abstract void subscribeFeeds(List<FeedModel> feeds);
+
+ protected void updateTable(boolean pack) {
+ tableModel.entries.clear();
+ tableModel.entries.addAll(gitblit.getSyndicatedEntries());
+ tableModel.fireTableDataChanged();
+ header.setText(Translation.get("gb.activity") + " ("
+ + gitblit.getSyndicatedEntries().size() + (page > 0 ? (", pg " + (page + 1)) : "")
+ + ")");
+ if (pack) {
+ Utils.packColumns(table, Utils.MARGIN);
+ }
+ table.scrollRectToVisible(new Rectangle(table.getCellRect(0, 0, true)));
+
+ if (page == 0) {
+ // determine unique repositories
+ Set<String> uniqueRepositories = new HashSet<String>();
+ for (FeedEntryModel entry : tableModel.entries) {
+ uniqueRepositories.add(entry.repository);
+ }
+
+ // repositories
+ List<String> sortedRespositories = new ArrayList<String>(uniqueRepositories);
+ StringUtils.sortRepositorynames(sortedRespositories);
+ repositoryChoices.removeAllElements();
+ repositoryChoices.addElement(ALL);
+ for (String repo : sortedRespositories) {
+ repositoryChoices.addElement(repo);
+ }
+ }
+
+ // update pagination buttons
+ next.setEnabled(tableModel.entries.size() > 0);
+ prev.setEnabled(page > 0);
+ }
+
+ private void updateAuthors() {
+ String repository = ALL;
+ if (repositorySelector.getSelectedIndex() > -1) {
+ repository = repositorySelector.getSelectedItem().toString();
+ }
+
+ // determine unique repositories and authors
+ Set<String> uniqueAuthors = new HashSet<String>();
+ for (FeedEntryModel entry : tableModel.entries) {
+ if (repository.equals(ALL) || entry.repository.equalsIgnoreCase(repository)) {
+ uniqueAuthors.add(entry.author);
+ }
+ }
+ // authors
+ List<String> sortedAuthors = new ArrayList<String>(uniqueAuthors);
+ Collections.sort(sortedAuthors);
+ authorChoices.removeAllElements();
+ authorChoices.addElement(ALL);
+ for (String author : sortedAuthors) {
+ authorChoices.addElement(author);
+ }
+ }
+
+ protected FeedEntryModel getSelectedSyndicatedEntry() {
+ int viewRow = table.getSelectedRow();
+ int modelRow = table.convertRowIndexToModel(viewRow);
+ FeedEntryModel entry = tableModel.get(modelRow);
+ return entry;
+ }
+
+ protected void viewCommit() {
+ FeedEntryModel entry = getSelectedSyndicatedEntry();
+ Utils.browse(entry.link);
+ }
+
+ protected void viewCommitDiff() {
+ FeedEntryModel entry = getSelectedSyndicatedEntry();
+ Utils.browse(entry.link.replace("/commit/", "/commitdiff/"));
+ }
+
+ protected void viewTree() {
+ FeedEntryModel entry = getSelectedSyndicatedEntry();
+ Utils.browse(entry.link.replace("/commit/", "/tree/"));
+ }
+
+ protected void filterFeeds() {
+ final String repository;
+ if (repositorySelector.getSelectedIndex() > -1) {
+ repository = repositorySelector.getSelectedItem().toString();
+ } else {
+ repository = ALL;
+ }
+
+ final String author;
+ if (authorSelector.getSelectedIndex() > -1) {
+ author = authorSelector.getSelectedItem().toString();
+ } else {
+ author = ALL;
+ }
+
+ if (repository.equals(ALL) && author.equals(ALL)) {
+ table.setRowSorter(defaultSorter);
+ return;
+ }
+ final int repositoryIndex = FeedEntryTableModel.Columns.Repository.ordinal();
+ final int authorIndex = FeedEntryTableModel.Columns.Author.ordinal();
+ RowFilter<FeedEntryTableModel, Object> containsFilter;
+ if (repository.equals(ALL)) {
+ // author filter
+ containsFilter = new RowFilter<FeedEntryTableModel, Object>() {
+ public boolean include(
+ Entry<? extends FeedEntryTableModel, ? extends Object> entry) {
+ return entry.getStringValue(authorIndex).equalsIgnoreCase(author);
+ }
+ };
+ } else if (author.equals(ALL)) {
+ // repository filter
+ containsFilter = new RowFilter<FeedEntryTableModel, Object>() {
+ public boolean include(
+ Entry<? extends FeedEntryTableModel, ? extends Object> entry) {
+ return entry.getStringValue(repositoryIndex).equalsIgnoreCase(repository);
+ }
+ };
+ } else {
+ // repository-author filter
+ containsFilter = new RowFilter<FeedEntryTableModel, Object>() {
+ public boolean include(
+ Entry<? extends FeedEntryTableModel, ? extends Object> entry) {
+ boolean authorMatch = entry.getStringValue(authorIndex)
+ .equalsIgnoreCase(author);
+ boolean repositoryMatch = entry.getStringValue(repositoryIndex)
+ .equalsIgnoreCase(repository);
+ return authorMatch && repositoryMatch;
+ }
+ };
+ }
+ TableRowSorter<FeedEntryTableModel> sorter = new TableRowSorter<FeedEntryTableModel>(
+ tableModel);
+ sorter.setRowFilter(containsFilter);
+ table.setRowSorter(sorter);
+ }
+}
diff --git a/src/main/java/com/gitblit/client/FeedsTableModel.java b/src/main/java/com/gitblit/client/FeedsTableModel.java
new file mode 100644
index 00000000..0979a4c9
--- /dev/null
+++ b/src/main/java/com/gitblit/client/FeedsTableModel.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.client;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import javax.swing.table.AbstractTableModel;
+
+import com.gitblit.models.FeedModel;
+
+/**
+ * Table model of a list of available feeds.
+ *
+ * @author James Moger
+ *
+ */
+public class FeedsTableModel extends AbstractTableModel {
+
+ private static final long serialVersionUID = 1L;
+
+ List<FeedModel> list;
+
+ enum Columns {
+ Subscribed, Repository, Branch;
+
+ @Override
+ public String toString() {
+ return name().replace('_', ' ');
+ }
+ }
+
+ public FeedsTableModel() {
+ this(new ArrayList<FeedModel>());
+ }
+
+ public FeedsTableModel(List<FeedModel> feeds) {
+ this.list = feeds;
+ Collections.sort(this.list);
+ }
+
+ @Override
+ public int getRowCount() {
+ return list.size();
+ }
+
+ @Override
+ public int getColumnCount() {
+ return Columns.values().length;
+ }
+
+ @Override
+ public String getColumnName(int column) {
+ Columns col = Columns.values()[column];
+ switch (col) {
+ case Repository:
+ return Translation.get("gb.repository");
+ case Branch:
+ return Translation.get("gb.branch");
+ }
+ return "";
+ }
+
+ /**
+ * Returns <code>Object.class</code> regardless of <code>columnIndex</code>.
+ *
+ * @param columnIndex
+ * the column being queried
+ * @return the Object.class
+ */
+ public Class<?> getColumnClass(int columnIndex) {
+ Columns col = Columns.values()[columnIndex];
+ switch (col) {
+ case Subscribed:
+ return Boolean.class;
+ }
+ return String.class;
+ }
+
+ @Override
+ public boolean isCellEditable(int rowIndex, int columnIndex) {
+ Columns col = Columns.values()[columnIndex];
+ switch (col) {
+ case Subscribed:
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public Object getValueAt(int rowIndex, int columnIndex) {
+ FeedModel model = list.get(rowIndex);
+ Columns col = Columns.values()[columnIndex];
+ switch (col) {
+ case Repository:
+ return model.repository;
+ case Branch:
+ return model.branch;
+ case Subscribed:
+ return model.subscribed;
+ }
+ return null;
+ }
+
+ public FeedModel get(int modelRow) {
+ return list.get(modelRow);
+ }
+}
diff --git a/src/main/java/com/gitblit/client/GitblitClient.java b/src/main/java/com/gitblit/client/GitblitClient.java
new file mode 100644
index 00000000..cc7d58a6
--- /dev/null
+++ b/src/main/java/com/gitblit/client/GitblitClient.java
@@ -0,0 +1,717 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.client;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+
+import com.gitblit.Constants;
+import com.gitblit.Constants.AccessPermission;
+import com.gitblit.Constants.AccessRestrictionType;
+import com.gitblit.Constants.AuthorizationControl;
+import com.gitblit.Constants.PermissionType;
+import com.gitblit.Constants.RegistrantType;
+import com.gitblit.GitBlitException.ForbiddenException;
+import com.gitblit.GitBlitException.NotAllowedException;
+import com.gitblit.GitBlitException.UnauthorizedException;
+import com.gitblit.GitBlitException.UnknownRequestException;
+import com.gitblit.Keys;
+import com.gitblit.models.FederationModel;
+import com.gitblit.models.FeedEntryModel;
+import com.gitblit.models.FeedModel;
+import com.gitblit.models.RegistrantAccessPermission;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.ServerSettings;
+import com.gitblit.models.ServerStatus;
+import com.gitblit.models.TeamModel;
+import com.gitblit.models.UserModel;
+import com.gitblit.utils.ArrayUtils;
+import com.gitblit.utils.RpcUtils;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.utils.SyndicationUtils;
+
+/**
+ * GitblitClient is a object that retrieves data from a Gitblit server, caches
+ * it for local operations, and allows updating or creating Gitblit objects.
+ *
+ * @author James Moger
+ *
+ */
+public class GitblitClient implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ private static final Date NEVER = new Date(0);
+
+ protected final GitblitRegistration reg;
+
+ public final String url;
+
+ public final String account;
+
+ private final char[] password;
+
+ private volatile int protocolVersion;
+
+ private volatile boolean allowManagement;
+
+ private volatile boolean allowAdministration;
+
+ private volatile ServerSettings settings;
+
+ private final List<RepositoryModel> allRepositories;
+
+ private final List<UserModel> allUsers;
+
+ private final List<TeamModel> allTeams;
+
+ private final List<FederationModel> federationRegistrations;
+
+ private final List<FeedModel> availableFeeds;
+
+ private final List<FeedEntryModel> syndicatedEntries;
+
+ private final Set<String> subscribedRepositories;
+
+ private ServerStatus status;
+
+ public GitblitClient(GitblitRegistration reg) {
+ this.reg = reg;
+ this.url = reg.url;
+ this.account = reg.account;
+ this.password = reg.password;
+
+ this.allUsers = new ArrayList<UserModel>();
+ this.allTeams = new ArrayList<TeamModel>();
+ this.allRepositories = new ArrayList<RepositoryModel>();
+ this.federationRegistrations = new ArrayList<FederationModel>();
+ this.availableFeeds = new ArrayList<FeedModel>();
+ this.syndicatedEntries = new ArrayList<FeedEntryModel>();
+ this.subscribedRepositories = new HashSet<String>();
+ }
+
+ public void login() throws IOException {
+ protocolVersion = RpcUtils.getProtocolVersion(url, account, password);
+ refreshSettings();
+ refreshAvailableFeeds();
+ refreshRepositories();
+ refreshSubscribedFeeds(0);
+
+ try {
+ // credentials may not have administrator access
+ // or server may have disabled rpc management
+ refreshUsers();
+ if (protocolVersion > 1) {
+ refreshTeams();
+ }
+ allowManagement = true;
+ } catch (UnauthorizedException e) {
+ } catch (ForbiddenException e) {
+ } catch (NotAllowedException e) {
+ } catch (UnknownRequestException e) {
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ try {
+ // credentials may not have administrator access
+ // or server may have disabled rpc administration
+ refreshStatus();
+ allowAdministration = true;
+ } catch (UnauthorizedException e) {
+ } catch (ForbiddenException e) {
+ } catch (NotAllowedException e) {
+ } catch (UnknownRequestException e) {
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public int getProtocolVersion() {
+ return protocolVersion;
+ }
+
+ public boolean allowManagement() {
+ return allowManagement;
+ }
+
+ public boolean allowAdministration() {
+ return allowAdministration;
+ }
+
+ public boolean isOwner(RepositoryModel model) {
+ return model.isOwner(account);
+ }
+
+ public String getURL(String action, String repository, String objectId) {
+ boolean mounted = settings.get(Keys.web.mountParameters).getBoolean(true);
+ StringBuilder sb = new StringBuilder();
+ sb.append(url);
+ sb.append('/');
+ sb.append(action);
+ sb.append('/');
+ if (mounted) {
+ // mounted url/action/repository/objectId
+ sb.append(StringUtils.encodeURL(repository));
+ if (!StringUtils.isEmpty(objectId)) {
+ sb.append('/');
+ sb.append(objectId);
+ }
+ return sb.toString();
+ } else {
+ // parameterized url/action/&r=repository&h=objectId
+ sb.append("?r=");
+ sb.append(repository);
+ if (!StringUtils.isEmpty(objectId)) {
+ sb.append("&h=");
+ sb.append(objectId);
+ }
+ return sb.toString();
+ }
+ }
+
+ public AccessRestrictionType getDefaultAccessRestriction() {
+ String restriction = null;
+ if (settings.hasKey(Keys.git.defaultAccessRestriction)) {
+ restriction = settings.get(Keys.git.defaultAccessRestriction).currentValue;
+ }
+ return AccessRestrictionType.fromName(restriction);
+ }
+
+ public AuthorizationControl getDefaultAuthorizationControl() {
+ String authorization = null;
+ if (settings.hasKey(Keys.git.defaultAuthorizationControl)) {
+ authorization = settings.get(Keys.git.defaultAuthorizationControl).currentValue;
+ }
+ return AuthorizationControl.fromName(authorization);
+ }
+
+ /**
+ * Returns the list of pre-receive scripts the repository inherited from the
+ * global settings and team affiliations.
+ *
+ * @param repository
+ * if null only the globally specified scripts are returned
+ * @return a list of scripts
+ */
+ public List<String> getPreReceiveScriptsInherited(RepositoryModel repository) {
+ Set<String> scripts = new LinkedHashSet<String>();
+ // Globals
+ for (String script : settings.get(Keys.groovy.preReceiveScripts).getStrings()) {
+ if (script.endsWith(".groovy")) {
+ scripts.add(script.substring(0, script.lastIndexOf('.')));
+ } else {
+ scripts.add(script);
+ }
+ }
+
+ // Team Scripts
+ if (repository != null) {
+ for (String teamname : getPermittedTeamnames(repository)) {
+ TeamModel team = getTeamModel(teamname);
+ if (!ArrayUtils.isEmpty(team.preReceiveScripts)) {
+ scripts.addAll(team.preReceiveScripts);
+ }
+ }
+ }
+ return new ArrayList<String>(scripts);
+ }
+
+ /**
+ * Returns the list of all available Groovy pre-receive push hook scripts
+ * that are not already inherited by the repository. Script files must have
+ * .groovy extension
+ *
+ * @param repository
+ * optional parameter
+ * @return list of available hook scripts
+ */
+ public List<String> getPreReceiveScriptsUnused(RepositoryModel repository) {
+ Set<String> inherited = new TreeSet<String>(getPreReceiveScriptsInherited(repository));
+
+ // create list of available scripts by excluding inherited scripts
+ List<String> scripts = new ArrayList<String>();
+ for (String script : settings.pushScripts) {
+ if (!inherited.contains(script)) {
+ scripts.add(script);
+ }
+ }
+ return scripts;
+ }
+
+ /**
+ * Returns the list of post-receive scripts the repository inherited from
+ * the global settings and team affiliations.
+ *
+ * @param repository
+ * if null only the globally specified scripts are returned
+ * @return a list of scripts
+ */
+ public List<String> getPostReceiveScriptsInherited(RepositoryModel repository) {
+ Set<String> scripts = new LinkedHashSet<String>();
+ // Global Scripts
+ for (String script : settings.get(Keys.groovy.postReceiveScripts).getStrings()) {
+ if (script.endsWith(".groovy")) {
+ scripts.add(script.substring(0, script.lastIndexOf('.')));
+ } else {
+ scripts.add(script);
+ }
+ }
+ // Team Scripts
+ if (repository != null) {
+ for (String teamname : getPermittedTeamnames(repository)) {
+ TeamModel team = getTeamModel(teamname);
+ if (!ArrayUtils.isEmpty(team.postReceiveScripts)) {
+ scripts.addAll(team.postReceiveScripts);
+ }
+ }
+ }
+ return new ArrayList<String>(scripts);
+ }
+
+ /**
+ * Returns the list of unused Groovy post-receive push hook scripts that are
+ * not already inherited by the repository. Script files must have .groovy
+ * extension
+ *
+ * @param repository
+ * optional parameter
+ * @return list of available hook scripts
+ */
+ public List<String> getPostReceiveScriptsUnused(RepositoryModel repository) {
+ Set<String> inherited = new TreeSet<String>(getPostReceiveScriptsInherited(repository));
+
+ // create list of available scripts by excluding inherited scripts
+ List<String> scripts = new ArrayList<String>();
+ if (!ArrayUtils.isEmpty(settings.pushScripts)) {
+ for (String script : settings.pushScripts) {
+ if (!inherited.contains(script)) {
+ scripts.add(script);
+ }
+ }
+ }
+ return scripts;
+ }
+
+ public ServerSettings getSettings() {
+ return settings;
+ }
+
+ public ServerStatus getStatus() {
+ return status;
+ }
+
+ public String getSettingDescription(String key) {
+ return settings.get(key).description;
+ }
+
+ public List<RepositoryModel> refreshRepositories() throws IOException {
+ Map<String, RepositoryModel> repositories = RpcUtils
+ .getRepositories(url, account, password);
+ allRepositories.clear();
+ allRepositories.addAll(repositories.values());
+ Collections.sort(allRepositories);
+ markSubscribedFeeds();
+ return allRepositories;
+ }
+
+ public List<UserModel> refreshUsers() throws IOException {
+ List<UserModel> users = RpcUtils.getUsers(url, account, password);
+ allUsers.clear();
+ allUsers.addAll(users);
+ Collections.sort(users);
+ return allUsers;
+ }
+
+ public List<TeamModel> refreshTeams() throws IOException {
+ List<TeamModel> teams = RpcUtils.getTeams(url, account, password);
+ allTeams.clear();
+ allTeams.addAll(teams);
+ Collections.sort(teams);
+ return allTeams;
+ }
+
+ public ServerSettings refreshSettings() throws IOException {
+ settings = RpcUtils.getSettings(url, account, password);
+ return settings;
+ }
+
+ public ServerStatus refreshStatus() throws IOException {
+ status = RpcUtils.getStatus(url, account, password);
+ return status;
+ }
+
+ public List<String> getBranches(String repository) {
+ List<FeedModel> feeds = getAvailableFeeds(repository);
+ List<String> branches = new ArrayList<String>();
+ for (FeedModel feed : feeds) {
+ branches.add(feed.branch);
+ }
+ Collections.sort(branches);
+ return branches;
+ }
+
+ public List<FeedModel> getAvailableFeeds() {
+ return availableFeeds;
+ }
+
+ public List<FeedModel> getAvailableFeeds(RepositoryModel repository) {
+ return getAvailableFeeds(repository.name);
+ }
+
+ public List<FeedModel> getAvailableFeeds(String repository) {
+ List<FeedModel> repositoryFeeds = new ArrayList<FeedModel>();
+ if (repository == null) {
+ return repositoryFeeds;
+ }
+ for (FeedModel feed : availableFeeds) {
+ if (feed.repository.equalsIgnoreCase(repository)) {
+ repositoryFeeds.add(feed);
+ }
+ }
+ return repositoryFeeds;
+ }
+
+ public List<FeedModel> refreshAvailableFeeds() throws IOException {
+ List<FeedModel> feeds = RpcUtils.getBranchFeeds(url, account, password);
+ availableFeeds.clear();
+ availableFeeds.addAll(feeds);
+ markSubscribedFeeds();
+ return availableFeeds;
+ }
+
+ public List<FeedEntryModel> refreshSubscribedFeeds(int page) throws IOException {
+ Set<FeedEntryModel> allEntries = new HashSet<FeedEntryModel>();
+ if (reg.feeds.size() > 0) {
+ for (FeedModel feed : reg.feeds) {
+ feed.lastRefreshDate = feed.currentRefreshDate;
+ feed.currentRefreshDate = new Date();
+ List<FeedEntryModel> entries = SyndicationUtils.readFeed(url, feed.repository,
+ feed.branch, -1, page, account, password);
+ allEntries.addAll(entries);
+ }
+ }
+ reg.cacheFeeds();
+ syndicatedEntries.clear();
+ syndicatedEntries.addAll(allEntries);
+ Collections.sort(syndicatedEntries);
+ return syndicatedEntries;
+ }
+
+ public void updateSubscribedFeeds(List<FeedModel> list) {
+ reg.updateSubscribedFeeds(list);
+ markSubscribedFeeds();
+ }
+
+ private void markSubscribedFeeds() {
+ subscribedRepositories.clear();
+ for (FeedModel feed : availableFeeds) {
+ // mark feed in the available list as subscribed
+ feed.subscribed = reg.feeds.contains(feed);
+ if (feed.subscribed) {
+ subscribedRepositories.add(feed.repository.toLowerCase());
+ }
+ }
+ }
+
+ public Date getLastFeedRefresh(String repository, String branch) {
+ FeedModel feed = new FeedModel();
+ feed.repository = repository;
+ feed.branch = branch;
+ if (reg.feeds.contains(feed)) {
+ int idx = reg.feeds.indexOf(feed);
+ feed = reg.feeds.get(idx);
+ return feed.lastRefreshDate;
+ }
+ return NEVER;
+ }
+
+ public boolean isSubscribed(RepositoryModel repository) {
+ return subscribedRepositories.contains(repository.name.toLowerCase());
+ }
+
+ public List<FeedEntryModel> getSyndicatedEntries() {
+ return syndicatedEntries;
+ }
+
+ public List<FeedEntryModel> log(String repository, String branch, int numberOfEntries, int page)
+ throws IOException {
+ return SyndicationUtils.readFeed(url, repository, branch, numberOfEntries, page, account,
+ password);
+ }
+
+ public List<FeedEntryModel> search(String repository, String branch, String fragment,
+ Constants.SearchType type, int numberOfEntries, int page) throws IOException {
+ return SyndicationUtils.readSearchFeed(url, repository, branch, fragment, type,
+ numberOfEntries, page, account, password);
+ }
+
+ public List<FederationModel> refreshFederationRegistrations() throws IOException {
+ List<FederationModel> list = RpcUtils.getFederationRegistrations(url, account, password);
+ federationRegistrations.clear();
+ federationRegistrations.addAll(list);
+ return federationRegistrations;
+ }
+
+ public List<UserModel> getUsers() {
+ return allUsers;
+ }
+
+ public UserModel getUser(String username) {
+ for (UserModel user : getUsers()) {
+ if (user.username.equalsIgnoreCase(username)) {
+ return user;
+ }
+ }
+ return null;
+ }
+
+ public List<String> getUsernames() {
+ List<String> usernames = new ArrayList<String>();
+ for (UserModel user : this.allUsers) {
+ usernames.add(user.username);
+ }
+ Collections.sort(usernames);
+ return usernames;
+ }
+
+ public List<String> getPermittedUsernames(RepositoryModel repository) {
+ List<String> usernames = new ArrayList<String>();
+ for (UserModel user : this.allUsers) {
+ if (user.hasRepositoryPermission(repository.name)) {
+ usernames.add(user.username);
+ }
+ }
+ return usernames;
+ }
+
+ /**
+ * Returns the effective list of permissions for this user, taking into account
+ * team memberships, ownerships.
+ *
+ * @param user
+ * @return the effective list of permissions for the user
+ */
+ public List<RegistrantAccessPermission> getUserAccessPermissions(UserModel user) {
+ Set<RegistrantAccessPermission> set = new LinkedHashSet<RegistrantAccessPermission>();
+ set.addAll(user.getRepositoryPermissions());
+ // Flag missing repositories
+ for (RegistrantAccessPermission permission : set) {
+ if (permission.mutable && PermissionType.EXPLICIT.equals(permission.permissionType)) {
+ RepositoryModel rm = getRepository(permission.registrant);
+ if (rm == null) {
+ permission.permissionType = PermissionType.MISSING;
+ permission.mutable = false;
+ continue;
+ }
+ }
+ }
+
+ // TODO reconsider ownership as a user property
+ // manually specify personal repository ownerships
+ for (RepositoryModel rm : allRepositories) {
+ if (rm.isUsersPersonalRepository(user.username) || rm.isOwner(user.username)) {
+ RegistrantAccessPermission rp = new RegistrantAccessPermission(rm.name, AccessPermission.REWIND,
+ PermissionType.OWNER, RegistrantType.REPOSITORY, null, false);
+ // user may be owner of a repository to which they've inherited
+ // a team permission, replace any existing perm with owner perm
+ set.remove(rp);
+ set.add(rp);
+ }
+ }
+
+ List<RegistrantAccessPermission> list = new ArrayList<RegistrantAccessPermission>(set);
+ Collections.sort(list);
+ return list;
+ }
+
+ public List<RegistrantAccessPermission> getUserAccessPermissions(RepositoryModel repository) {
+ List<RegistrantAccessPermission> list = new ArrayList<RegistrantAccessPermission>();
+ if (AccessRestrictionType.NONE.equals(repository.accessRestriction)) {
+ // no permissions needed, REWIND for everyone!
+ return list;
+ }
+ if (AuthorizationControl.AUTHENTICATED.equals(repository.authorizationControl)) {
+ // no permissions needed, REWIND for authenticated!
+ return list;
+ }
+ // NAMED users and teams
+ for (UserModel user : allUsers) {
+ RegistrantAccessPermission ap = user.getRepositoryPermission(repository);
+ if (ap.permission.exceeds(AccessPermission.NONE)) {
+ list.add(ap);
+ }
+ }
+ return list;
+ }
+
+ public boolean setUserAccessPermissions(RepositoryModel repository, List<RegistrantAccessPermission> permissions) throws IOException {
+ return RpcUtils.setRepositoryMemberPermissions(repository, permissions, url, account, password);
+ }
+
+ public List<TeamModel> getTeams() {
+ return allTeams;
+ }
+
+ public List<String> getTeamnames() {
+ List<String> teamnames = new ArrayList<String>();
+ for (TeamModel team : this.allTeams) {
+ teamnames.add(team.name);
+ }
+ Collections.sort(teamnames);
+ return teamnames;
+ }
+
+ public List<String> getPermittedTeamnames(RepositoryModel repository) {
+ List<String> teamnames = new ArrayList<String>();
+ for (TeamModel team : this.allTeams) {
+ if (team.hasRepositoryPermission(repository.name)) {
+ teamnames.add(team.name);
+ }
+ }
+ return teamnames;
+ }
+
+ public List<RegistrantAccessPermission> getTeamAccessPermissions(RepositoryModel repository) {
+ List<RegistrantAccessPermission> list = new ArrayList<RegistrantAccessPermission>();
+ for (TeamModel team : allTeams) {
+ RegistrantAccessPermission ap = team.getRepositoryPermission(repository);
+ if (ap.permission.exceeds(AccessPermission.NONE)) {
+ list.add(ap);
+ }
+ }
+ Collections.sort(list);
+ return list;
+ }
+
+ public boolean setTeamAccessPermissions(RepositoryModel repository, List<RegistrantAccessPermission> permissions) throws IOException {
+ return RpcUtils.setRepositoryTeamPermissions(repository, permissions, url, account, password);
+ }
+
+ public TeamModel getTeamModel(String name) {
+ for (TeamModel team : allTeams) {
+ if (team.name.equalsIgnoreCase(name)) {
+ return team;
+ }
+ }
+ return null;
+ }
+
+ public List<String> getFederationSets() {
+ return settings.get(Keys.federation.sets).getStrings();
+ }
+
+ public List<RepositoryModel> getRepositories() {
+ return allRepositories;
+ }
+
+ public RepositoryModel getRepository(String name) {
+ for (RepositoryModel repository : allRepositories) {
+ if (repository.name.equalsIgnoreCase(name)) {
+ return repository;
+ }
+ }
+ return null;
+ }
+
+ public boolean createRepository(RepositoryModel repository, List<RegistrantAccessPermission> userPermissions)
+ throws IOException {
+ return createRepository(repository, userPermissions, null);
+ }
+
+ public boolean createRepository(RepositoryModel repository, List<RegistrantAccessPermission> userPermissions,
+ List<RegistrantAccessPermission> teamPermissions) throws IOException {
+ boolean success = true;
+ success &= RpcUtils.createRepository(repository, url, account, password);
+ if (userPermissions != null && userPermissions.size() > 0) {
+ // if new repository has named members, set them
+ success &= RpcUtils.setRepositoryMemberPermissions(repository, userPermissions, url, account,
+ password);
+ }
+ if (teamPermissions != null && teamPermissions.size() > 0) {
+ // if new repository has named teams, set them
+ success &= RpcUtils.setRepositoryTeamPermissions(repository, teamPermissions, url, account,
+ password);
+ }
+ return success;
+ }
+
+ public boolean updateRepository(String name, RepositoryModel repository,
+ List<RegistrantAccessPermission> userPermissions) throws IOException {
+ return updateRepository(name, repository, userPermissions, null);
+ }
+
+ public boolean updateRepository(String name, RepositoryModel repository,
+ List<RegistrantAccessPermission> userPermissions, List<RegistrantAccessPermission> teamPermissions) throws IOException {
+ boolean success = true;
+ success &= RpcUtils.updateRepository(name, repository, url, account, password);
+ // set the repository members
+ if (userPermissions != null) {
+ success &= RpcUtils.setRepositoryMemberPermissions(repository, userPermissions, url, account,
+ password);
+ }
+ if (teamPermissions != null) {
+ success &= RpcUtils.setRepositoryTeamPermissions(repository, teamPermissions, url, account,
+ password);
+ }
+ return success;
+ }
+
+ public boolean deleteRepository(RepositoryModel repository) throws IOException {
+ return RpcUtils.deleteRepository(repository, url, account, password);
+ }
+
+ public boolean clearRepositoryCache() throws IOException {
+ return RpcUtils.clearRepositoryCache(url, account, password);
+ }
+
+ public boolean createUser(UserModel user) throws IOException {
+ return RpcUtils.createUser(user, url, account, password);
+ }
+
+ public boolean updateUser(String name, UserModel user) throws IOException {
+ return RpcUtils.updateUser(name, user, url, account, password);
+ }
+
+ public boolean deleteUser(UserModel user) throws IOException {
+ return RpcUtils.deleteUser(user, url, account, password);
+ }
+
+ public boolean createTeam(TeamModel team) throws IOException {
+ return RpcUtils.createTeam(team, url, account, password);
+ }
+
+ public boolean updateTeam(String name, TeamModel team) throws IOException {
+ return RpcUtils.updateTeam(name, team, url, account, password);
+ }
+
+ public boolean deleteTeam(TeamModel team) throws IOException {
+ return RpcUtils.deleteTeam(team, url, account, password);
+ }
+
+ public boolean updateSettings(Map<String, String> newSettings) throws IOException {
+ return RpcUtils.updateSettings(newSettings, url, account, password);
+ }
+}
diff --git a/src/main/java/com/gitblit/client/GitblitManager.java b/src/main/java/com/gitblit/client/GitblitManager.java
new file mode 100644
index 00000000..d2fd7f7b
--- /dev/null
+++ b/src/main/java/com/gitblit/client/GitblitManager.java
@@ -0,0 +1,458 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.client;
+
+import java.awt.BorderLayout;
+import java.awt.Cursor;
+import java.awt.Dimension;
+import java.awt.EventQueue;
+import java.awt.Point;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.KeyEvent;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.net.ConnectException;
+import java.text.MessageFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.TimeZone;
+
+import javax.swing.ImageIcon;
+import javax.swing.JFrame;
+import javax.swing.JMenu;
+import javax.swing.JMenuBar;
+import javax.swing.JMenuItem;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JTabbedPane;
+import javax.swing.KeyStroke;
+import javax.swing.SwingWorker;
+import javax.swing.UIManager;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.StoredConfig;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+
+import com.gitblit.Constants;
+import com.gitblit.GitBlitException.ForbiddenException;
+import com.gitblit.models.FeedModel;
+import com.gitblit.utils.Base64;
+import com.gitblit.utils.StringUtils;
+
+/**
+ * Gitblit Manager issues JSON RPC requests to a Gitblit server.
+ *
+ * @author James Moger
+ *
+ */
+public class GitblitManager extends JFrame implements RegistrationsDialog.RegistrationListener {
+
+ private static final long serialVersionUID = 1L;
+ private static final String SERVER = "server";
+ private static final String FEED = "feed";
+ private final SimpleDateFormat dateFormat;
+ private JTabbedPane serverTabs;
+ private File configFile = new File(System.getProperty("user.home"), ".gitblit/config");
+
+ private Map<String, GitblitRegistration> registrations = new LinkedHashMap<String, GitblitRegistration>();
+ private JMenu recentMenu;
+ private int maxRecentCount = 5;
+
+ private GitblitManager() {
+ super();
+ dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US);
+ dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+ }
+
+ private void initialize() {
+ setContentPane(getCenterPanel());
+ setIconImage(new ImageIcon(getClass().getResource("/gitblt-favicon.png")).getImage());
+ setTitle("Gitblit Manager v" + Constants.getVersion() + " (" + Constants.getBuildDate() + ")");
+ setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+ addWindowListener(new WindowAdapter() {
+ @Override
+ public void windowClosing(WindowEvent event) {
+ saveSizeAndPosition();
+ }
+
+ @Override
+ public void windowOpened(WindowEvent event) {
+ manageRegistrations();
+ }
+ });
+
+ setSizeAndPosition();
+ loadRegistrations();
+ rebuildRecentMenu();
+ }
+
+ private void setSizeAndPosition() {
+ String sz = null;
+ String pos = null;
+ try {
+ StoredConfig config = getConfig();
+ sz = config.getString("ui", null, "size");
+ pos = config.getString("ui", null, "position");
+ } catch (Throwable t) {
+ t.printStackTrace();
+ }
+
+ // try to restore saved window size
+ if (StringUtils.isEmpty(sz)) {
+ setSize(850, 500);
+ } else {
+ String[] chunks = sz.split("x");
+ int width = Integer.parseInt(chunks[0]);
+ int height = Integer.parseInt(chunks[1]);
+ setSize(width, height);
+ }
+
+ // try to restore saved window position
+ if (StringUtils.isEmpty(pos)) {
+ setLocationRelativeTo(null);
+ } else {
+ String[] chunks = pos.split(",");
+ int x = Integer.parseInt(chunks[0]);
+ int y = Integer.parseInt(chunks[1]);
+ setLocation(x, y);
+ }
+ }
+
+ private void saveSizeAndPosition() {
+ try {
+ // save window size and position
+ StoredConfig config = getConfig();
+ Dimension sz = GitblitManager.this.getSize();
+ config.setString("ui", null, "size",
+ MessageFormat.format("{0,number,0}x{1,number,0}", sz.width, sz.height));
+ Point pos = GitblitManager.this.getLocationOnScreen();
+ config.setString("ui", null, "position",
+ MessageFormat.format("{0,number,0},{1,number,0}", pos.x, pos.y));
+ config.save();
+ } catch (Throwable t) {
+ Utils.showException(GitblitManager.this, t);
+ }
+ }
+
+ private JMenuBar setupMenu() {
+ JMenuBar menuBar = new JMenuBar();
+ JMenu serversMenu = new JMenu(Translation.get("gb.servers"));
+ menuBar.add(serversMenu);
+ recentMenu = new JMenu(Translation.get("gb.recent"));
+ serversMenu.add(recentMenu);
+
+ JMenuItem manage = new JMenuItem(Translation.get("gb.manage") + "...");
+ manage.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_M, KeyEvent.CTRL_DOWN_MASK, false));
+ manage.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent event) {
+ manageRegistrations();
+ }
+ });
+ serversMenu.add(manage);
+
+ return menuBar;
+ }
+
+ private JPanel getCenterPanel() {
+ serverTabs = new JTabbedPane(JTabbedPane.TOP);
+ JMenuBar menubar = setupMenu();
+ JPanel panel = new JPanel(new BorderLayout());
+ panel.add(menubar, BorderLayout.NORTH);
+ panel.add(serverTabs, BorderLayout.CENTER);
+ return panel;
+ }
+
+ private void manageRegistrations() {
+ RegistrationsDialog dialog = new RegistrationsDialog(new ArrayList<GitblitRegistration>(
+ registrations.values()), this);
+ dialog.setLocationRelativeTo(GitblitManager.this);
+ dialog.setVisible(true);
+ }
+
+ @Override
+ public void login(GitblitRegistration reg) {
+ if (!reg.savePassword && (reg.password == null || reg.password.length == 0)) {
+ // prompt for password
+ EditRegistrationDialog dialog = new EditRegistrationDialog(this, reg, true);
+ dialog.setLocationRelativeTo(GitblitManager.this);
+ dialog.setVisible(true);
+ GitblitRegistration newReg = dialog.getRegistration();
+ if (newReg == null) {
+ // user canceled
+ return;
+ }
+ // preserve feeds
+ newReg.feeds.addAll(reg.feeds);
+
+ // use new reg
+ reg = newReg;
+ }
+
+ // login
+ setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
+ final GitblitRegistration registration = reg;
+ final GitblitPanel panel = new GitblitPanel(registration, this);
+ SwingWorker<Boolean, Void> worker = new SwingWorker<Boolean, Void>() {
+
+ @Override
+ protected Boolean doInBackground() throws IOException {
+ panel.login();
+ return true;
+ }
+
+ @Override
+ protected void done() {
+ try {
+ boolean success = get();
+ serverTabs.addTab(registration.name, panel);
+ int idx = serverTabs.getTabCount() - 1;
+ serverTabs.setSelectedIndex(idx);
+ serverTabs.setTabComponentAt(idx, new ClosableTabComponent(registration.name,
+ null, serverTabs, panel));
+ registration.lastLogin = new Date();
+ saveRegistration(registration.name, registration);
+ registrations.put(registration.name, registration);
+ rebuildRecentMenu();
+ if (!registration.savePassword) {
+ // clear password
+ registration.password = null;
+ }
+ } catch (Throwable t) {
+ Throwable cause = t.getCause();
+ if (cause instanceof ConnectException) {
+ JOptionPane.showMessageDialog(GitblitManager.this, cause.getMessage(),
+ Translation.get("gb.error"), JOptionPane.ERROR_MESSAGE);
+ } else if (cause instanceof ForbiddenException) {
+ JOptionPane
+ .showMessageDialog(
+ GitblitManager.this,
+ "This Gitblit server does not allow RPC Management or Administration",
+ Translation.get("gb.error"), JOptionPane.ERROR_MESSAGE);
+ } else {
+ Utils.showException(GitblitManager.this, t);
+ }
+ } finally {
+ setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
+ }
+ }
+ };
+ worker.execute();
+ }
+
+ private void rebuildRecentMenu() {
+ recentMenu.removeAll();
+ ImageIcon icon = new ImageIcon(getClass().getResource("/gitblt-favicon.png"));
+ List<GitblitRegistration> list = new ArrayList<GitblitRegistration>(registrations.values());
+ Collections.sort(list, new Comparator<GitblitRegistration>() {
+ @Override
+ public int compare(GitblitRegistration o1, GitblitRegistration o2) {
+ return o2.lastLogin.compareTo(o1.lastLogin);
+ }
+ });
+ if (list.size() > maxRecentCount) {
+ list = list.subList(0, maxRecentCount);
+ }
+ for (int i = 0; i < list.size(); i++) {
+ final GitblitRegistration reg = list.get(i);
+ JMenuItem item = new JMenuItem(reg.name, icon);
+ item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_1 + i, KeyEvent.CTRL_DOWN_MASK,
+ false));
+ item.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ login(reg);
+ }
+ });
+ recentMenu.add(item);
+ }
+ }
+
+ private void loadRegistrations() {
+ try {
+ StoredConfig config = getConfig();
+ Set<String> servers = config.getSubsections(SERVER);
+ for (String server : servers) {
+ Date lastLogin = new Date(0);
+ String date = config.getString(SERVER, server, "lastLogin");
+ if (!StringUtils.isEmpty(date)) {
+ lastLogin = dateFormat.parse(date);
+ }
+ String url = config.getString(SERVER, server, "url");
+ String account = config.getString(SERVER, server, "account");
+ char[] password;
+ String pw = config.getString(SERVER, server, "password");
+ if (StringUtils.isEmpty(pw)) {
+ password = new char[0];
+ } else {
+ password = new String(Base64.decode(pw)).toCharArray();
+ }
+ GitblitRegistration reg = new GitblitRegistration(server, url, account, password) {
+ private static final long serialVersionUID = 1L;
+
+ protected void cacheFeeds() {
+ writeFeedCache(this);
+ }
+ };
+ String[] feeds = config.getStringList(SERVER, server, FEED);
+ if (feeds != null) {
+ // deserialize the field definitions
+ for (String definition : feeds) {
+ FeedModel feed = new FeedModel(definition);
+ reg.feeds.add(feed);
+ }
+ }
+ reg.lastLogin = lastLogin;
+ loadFeedCache(reg);
+ registrations.put(reg.name, reg);
+ }
+ } catch (Throwable t) {
+ Utils.showException(GitblitManager.this, t);
+ }
+ }
+
+ @Override
+ public boolean saveRegistration(String name, GitblitRegistration reg) {
+ try {
+ StoredConfig config = getConfig();
+ if (!StringUtils.isEmpty(name) && !name.equals(reg.name)) {
+ // delete old registration
+ registrations.remove(name);
+ config.unsetSection(SERVER, name);
+ }
+
+ // update registration
+ config.setString(SERVER, reg.name, "url", reg.url);
+ config.setString(SERVER, reg.name, "account", reg.account);
+ if (reg.savePassword) {
+ config.setString(SERVER, reg.name, "password",
+ Base64.encodeBytes(new String(reg.password).getBytes("UTF-8")));
+ } else {
+ config.setString(SERVER, reg.name, "password", "");
+ }
+ if (reg.lastLogin != null) {
+ config.setString(SERVER, reg.name, "lastLogin", dateFormat.format(reg.lastLogin));
+ }
+ // serialize the feed definitions
+ List<String> definitions = new ArrayList<String>();
+ for (FeedModel feed : reg.feeds) {
+ definitions.add(feed.toString());
+ }
+ if (definitions.size() > 0) {
+ config.setStringList(SERVER, reg.name, FEED, definitions);
+ }
+ config.save();
+ return true;
+ } catch (Throwable t) {
+ Utils.showException(GitblitManager.this, t);
+ }
+ return false;
+ }
+
+ @Override
+ public boolean deleteRegistrations(List<GitblitRegistration> list) {
+ boolean success = false;
+ try {
+ StoredConfig config = getConfig();
+ for (GitblitRegistration reg : list) {
+ config.unsetSection(SERVER, reg.name);
+ registrations.remove(reg.name);
+ }
+ config.save();
+ success = true;
+ } catch (Throwable t) {
+ Utils.showException(GitblitManager.this, t);
+ }
+ return success;
+ }
+
+ private StoredConfig getConfig() throws IOException, ConfigInvalidException {
+ FileBasedConfig config = new FileBasedConfig(configFile, FS.detect());
+ config.load();
+ return config;
+ }
+
+ private void loadFeedCache(GitblitRegistration reg) {
+ File feedCache = new File(configFile.getParentFile(), StringUtils.getSHA1(reg.toString())
+ + ".cache");
+ if (!feedCache.exists()) {
+ // no cache for this registration
+ return;
+ }
+ try {
+ BufferedReader reader = new BufferedReader(new FileReader(feedCache));
+ Map<String, Date> cache = new HashMap<String, Date>();
+ SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
+ String line = null;
+ while ((line = reader.readLine()) != null) {
+ String[] kvp = line.split("=");
+ cache.put(kvp[0], df.parse(kvp[1]));
+ }
+ reader.close();
+ for (FeedModel feed : reg.feeds) {
+ String name = feed.toString();
+ if (cache.containsKey(name)) {
+ feed.currentRefreshDate = cache.get(name);
+ }
+ }
+ } catch (Exception e) {
+ Utils.showException(GitblitManager.this, e);
+ }
+ }
+
+ private void writeFeedCache(GitblitRegistration reg) {
+ try {
+ File feedCache = new File(configFile.getParentFile(), StringUtils.getSHA1(reg
+ .toString()) + ".cache");
+ FileWriter writer = new FileWriter(feedCache);
+ for (FeedModel feed : reg.feeds) {
+ writer.append(MessageFormat.format("{0}={1,date,yyyy-MM-dd'T'HH:mm:ss}\n",
+ feed.toString(), feed.currentRefreshDate));
+ }
+ writer.close();
+ } catch (Exception e) {
+ Utils.showException(GitblitManager.this, e);
+ }
+ }
+
+ public static void main(String[] args) {
+ EventQueue.invokeLater(new Runnable() {
+ public void run() {
+ try {
+ UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
+ } catch (Exception e) {
+ }
+ GitblitManager frame = new GitblitManager();
+ frame.initialize();
+ frame.setVisible(true);
+ }
+ });
+ }
+}
diff --git a/src/main/java/com/gitblit/client/GitblitManagerLauncher.java b/src/main/java/com/gitblit/client/GitblitManagerLauncher.java
new file mode 100644
index 00000000..d0cc8393
--- /dev/null
+++ b/src/main/java/com/gitblit/client/GitblitManagerLauncher.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.client;
+
+import java.awt.Color;
+import java.awt.EventQueue;
+import java.awt.FontMetrics;
+import java.awt.Graphics2D;
+import java.awt.SplashScreen;
+import java.io.File;
+import java.io.FileFilter;
+import java.io.IOException;
+import java.lang.reflect.Method;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import com.gitblit.Constants;
+
+/**
+ * Downloads dependencies and launches Gitblit Manager.
+ *
+ * @author James Moger
+ *
+ */
+public class GitblitManagerLauncher {
+
+ public static final boolean DEBUG = false;
+
+ /**
+ * Parameters of the method to add an URL to the System classes.
+ */
+ private static final Class<?>[] PARAMETERS = new Class[] { URL.class };
+
+ public static void main(String[] args) {
+ final SplashScreen splash = SplashScreen.getSplashScreen();
+
+ File libFolder = new File("ext");
+ List<File> jars = findJars(libFolder.getAbsoluteFile());
+
+ // sort the jars by name and then reverse the order so the newer version
+ // of the library gets loaded in the event that this is an upgrade
+ Collections.sort(jars);
+ Collections.reverse(jars);
+ for (File jar : jars) {
+ try {
+ updateSplash(splash, Translation.get("gb.loading") + " " + jar.getName() + "...");
+ addJarFile(jar);
+ } catch (IOException e) {
+
+ }
+ }
+
+ updateSplash(splash, Translation.get("gb.starting") + " Gitblit Manager...");
+ GitblitManager.main(args);
+ }
+
+ private static void updateSplash(final SplashScreen splash, final String string) {
+ if (splash == null) {
+ return;
+ }
+ try {
+ EventQueue.invokeAndWait(new Runnable() {
+ public void run() {
+ Graphics2D g = splash.createGraphics();
+ if (g != null) {
+ // Splash is 320x120
+ FontMetrics fm = g.getFontMetrics();
+
+ // paint startup status
+ g.setColor(Color.darkGray);
+ int h = fm.getHeight() + fm.getMaxDescent();
+ int x = 5;
+ int y = 115;
+ int w = 320 - 2 * x;
+ g.fillRect(x, y - h, w, h);
+ g.setColor(Color.lightGray);
+ g.drawRect(x, y - h, w, h);
+ g.setColor(Color.WHITE);
+ int xw = fm.stringWidth(string);
+ g.drawString(string, x + ((w - xw) / 2), y - 5);
+
+ // paint version
+ String ver = "v" + Constants.getVersion();
+ int vw = g.getFontMetrics().stringWidth(ver);
+ g.drawString(ver, 320 - vw - 5, 34);
+ g.dispose();
+ splash.update();
+ }
+ }
+ });
+ } catch (Throwable t) {
+ t.printStackTrace();
+ }
+ }
+
+ public static List<File> findJars(File folder) {
+ List<File> jars = new ArrayList<File>();
+ if (folder.exists()) {
+ File[] libs = folder.listFiles(new FileFilter() {
+ @Override
+ public boolean accept(File file) {
+ return !file.isDirectory() && file.getName().toLowerCase().endsWith(".jar");
+ }
+ });
+ if (libs != null && libs.length > 0) {
+ jars.addAll(Arrays.asList(libs));
+ if (DEBUG) {
+ for (File jar : jars) {
+ System.out.println("found " + jar);
+ }
+ }
+ }
+ }
+
+ return jars;
+ }
+
+ /**
+ * Adds a file to the classpath
+ *
+ * @param f
+ * the file to be added
+ * @throws IOException
+ */
+ public static void addJarFile(File f) throws IOException {
+ if (f.getName().indexOf("-sources") > -1 || f.getName().indexOf("-javadoc") > -1) {
+ // don't add source or javadoc jars to runtime classpath
+ return;
+ }
+ URL u = f.toURI().toURL();
+ if (DEBUG) {
+ System.out.println("load=" + u.toExternalForm());
+ }
+ URLClassLoader sysloader = (URLClassLoader) ClassLoader.getSystemClassLoader();
+ Class<?> sysclass = URLClassLoader.class;
+ try {
+ Method method = sysclass.getDeclaredMethod("addURL", PARAMETERS);
+ method.setAccessible(true);
+ method.invoke(sysloader, new Object[] { u });
+ } catch (Throwable t) {
+ throw new IOException(MessageFormat.format(
+ "Error, could not add {0} to system classloader", f.getPath()), t);
+ }
+ }
+
+}
diff --git a/src/main/java/com/gitblit/client/GitblitPanel.java b/src/main/java/com/gitblit/client/GitblitPanel.java
new file mode 100644
index 00000000..f14ce790
--- /dev/null
+++ b/src/main/java/com/gitblit/client/GitblitPanel.java
@@ -0,0 +1,228 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.client;
+
+import java.awt.BorderLayout;
+import java.awt.Component;
+import java.awt.Insets;
+import java.io.IOException;
+import java.util.List;
+
+import javax.swing.JPanel;
+import javax.swing.JTabbedPane;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import com.gitblit.client.ClosableTabComponent.CloseTabListener;
+import com.gitblit.models.FeedModel;
+
+/**
+ * GitblitPanel is a container for the repository, users, settings, etc panels.
+ *
+ * @author James Moger
+ *
+ */
+public class GitblitPanel extends JPanel implements CloseTabListener {
+
+ private static final long serialVersionUID = 1L;
+
+ private final RegistrationsDialog.RegistrationListener listener;
+
+ private GitblitClient gitblit;
+
+ private JTabbedPane tabs;
+
+ private RepositoriesPanel repositoriesPanel;
+
+ private FeedsPanel feedsPanel;
+
+ private UsersPanel usersPanel;
+
+ private TeamsPanel teamsPanel;
+
+ private SettingsPanel settingsPanel;
+
+ private StatusPanel statusPanel;
+
+ public GitblitPanel(GitblitRegistration reg, RegistrationsDialog.RegistrationListener listener) {
+ this.gitblit = new GitblitClient(reg);
+ this.listener = listener;
+
+ tabs = new JTabbedPane(JTabbedPane.BOTTOM);
+ tabs.addTab(Translation.get("gb.repositories"), createRepositoriesPanel());
+ tabs.addTab(Translation.get("gb.activity"), createFeedsPanel());
+ tabs.addTab(Translation.get("gb.teams"), createTeamsPanel());
+ tabs.addTab(Translation.get("gb.users"), createUsersPanel());
+ tabs.addTab(Translation.get("gb.settings"), createSettingsPanel());
+ tabs.addTab(Translation.get("gb.status"), createStatusPanel());
+ tabs.addChangeListener(new ChangeListener() {
+ public void stateChanged(ChangeEvent e) {
+ tabs.getSelectedComponent().requestFocus();
+ }
+ });
+
+ setLayout(new BorderLayout());
+ add(tabs, BorderLayout.CENTER);
+ }
+
+ private JPanel createRepositoriesPanel() {
+ repositoriesPanel = new RepositoriesPanel(gitblit) {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ protected void subscribeFeeds(List<FeedModel> feeds) {
+ GitblitPanel.this.subscribeFeeds(feeds);
+ }
+
+ @Override
+ protected void updateUsersTable() {
+ usersPanel.updateTable(false);
+ }
+
+ @Override
+ protected void updateTeamsTable() {
+ teamsPanel.updateTable(false);
+ }
+
+ };
+ return repositoriesPanel;
+ }
+
+ private JPanel createFeedsPanel() {
+ feedsPanel = new FeedsPanel(gitblit) {
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ protected void subscribeFeeds(List<FeedModel> feeds) {
+ GitblitPanel.this.subscribeFeeds(feeds);
+ }
+ };
+ return feedsPanel;
+ }
+
+ private JPanel createUsersPanel() {
+ usersPanel = new UsersPanel(gitblit) {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ protected void updateTeamsTable() {
+ teamsPanel.updateTable(false);
+ }
+ };
+ return usersPanel;
+ }
+
+ private JPanel createTeamsPanel() {
+ teamsPanel = new TeamsPanel(gitblit) {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ protected void updateUsersTable() {
+ usersPanel.updateTable(false);
+ }
+ };
+ return teamsPanel;
+ }
+
+ private JPanel createSettingsPanel() {
+ settingsPanel = new SettingsPanel(gitblit);
+ return settingsPanel;
+ }
+
+ private JPanel createStatusPanel() {
+ statusPanel = new StatusPanel(gitblit);
+ return statusPanel;
+ }
+
+ public void login() throws IOException {
+ gitblit.login();
+
+ repositoriesPanel.updateTable(true);
+ feedsPanel.updateTable(true);
+
+ if (gitblit.allowManagement()) {
+ if (gitblit.getProtocolVersion() >= 2) {
+ // refresh teams panel
+ teamsPanel.updateTable(false);
+ } else {
+ // remove teams panel
+ String teams = Translation.get("gb.teams");
+ for (int i = 0; i < tabs.getTabCount(); i++) {
+ if (teams.equals(tabs.getTitleAt(i))) {
+ tabs.removeTabAt(i);
+ break;
+ }
+ }
+ }
+ usersPanel.updateTable(false);
+ } else {
+ // user does not have administrator privileges
+ // hide admin repository buttons
+ repositoriesPanel.disableManagement();
+
+ while (tabs.getTabCount() > 2) {
+ // remove all management/administration tabs
+ tabs.removeTabAt(2);
+ }
+ }
+
+ if (gitblit.allowAdministration()) {
+ settingsPanel.updateTable(true);
+ statusPanel.updateTable(false);
+ } else {
+ // remove the settings and status tab
+ String[] titles = { Translation.get("gb.settings"), Translation.get("gb.status") };
+ for (String title : titles) {
+ for (int i = 0; i < tabs.getTabCount(); i++) {
+ if (tabs.getTitleAt(i).equals(title)) {
+ tabs.removeTabAt(i);
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ public Insets getInsets() {
+ return Utils.INSETS;
+ }
+
+ @Override
+ public void closeTab(Component c) {
+ gitblit = null;
+ }
+
+ protected void subscribeFeeds(final List<FeedModel> feeds) {
+ SubscriptionsDialog dialog = new SubscriptionsDialog(feeds) {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public void save() {
+ gitblit.updateSubscribedFeeds(feeds);
+ listener.saveRegistration(gitblit.reg.name, gitblit.reg);
+ setVisible(false);
+ repositoriesPanel.updateTable(false);
+ }
+ };
+ dialog.setLocationRelativeTo(GitblitPanel.this);
+ dialog.setVisible(true);
+ }
+} \ No newline at end of file
diff --git a/src/main/java/com/gitblit/client/GitblitRegistration.java b/src/main/java/com/gitblit/client/GitblitRegistration.java
new file mode 100644
index 00000000..f9d07488
--- /dev/null
+++ b/src/main/java/com/gitblit/client/GitblitRegistration.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.client;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+import com.gitblit.models.FeedModel;
+import com.gitblit.utils.StringUtils;
+
+/**
+ * Simple class to encapsulate a Gitblit server registration.
+ *
+ * @author James Moger
+ *
+ */
+public class GitblitRegistration implements Serializable, Comparable<GitblitRegistration> {
+
+ private static final long serialVersionUID = 1L;
+
+ String name;
+ String url;
+ String account;
+ char[] password;
+ boolean savePassword;
+ Date lastLogin;
+ final List<FeedModel> feeds;
+
+ public GitblitRegistration(String name, String url, String account, char[] password) {
+ this.url = url;
+ this.account = account;
+ this.password = password;
+ this.savePassword = password != null && password.length > 0;
+ if (StringUtils.isEmpty(name)) {
+ this.name = url.substring(url.indexOf("//") + 2);
+ } else {
+ this.name = name;
+ }
+ feeds = new ArrayList<FeedModel>();
+ }
+
+ public void updateSubscribedFeeds(List<FeedModel> list) {
+ for (FeedModel feed : list) {
+ if (feeds.contains(feed)) {
+ // possibly unsubscribe/remove feed
+ int index = feeds.indexOf(feed);
+ FeedModel existingFeed = feeds.get(index);
+ existingFeed.subscribed = feed.subscribed;
+ if (!existingFeed.subscribed) {
+ feeds.remove(index);
+ }
+ } else if (feed.subscribed) {
+ // new subscription
+ feeds.add(feed);
+ }
+ }
+ }
+
+ protected void cacheFeeds() {
+ }
+
+ @Override
+ public int compareTo(GitblitRegistration o) {
+ return name.toLowerCase().compareTo(o.name.toLowerCase());
+ }
+
+ @Override
+ public String toString() {
+ return name + " (" + url + ")";
+ }
+}
diff --git a/src/main/java/com/gitblit/client/GitblitWorker.java b/src/main/java/com/gitblit/client/GitblitWorker.java
new file mode 100644
index 00000000..93c35d6b
--- /dev/null
+++ b/src/main/java/com/gitblit/client/GitblitWorker.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.client;
+
+import java.awt.Component;
+import java.awt.Cursor;
+import java.io.IOException;
+import java.text.MessageFormat;
+
+import javax.swing.JOptionPane;
+import javax.swing.SwingWorker;
+
+import com.gitblit.Constants.RpcRequest;
+import com.gitblit.GitBlitException.ForbiddenException;
+import com.gitblit.GitBlitException.NotAllowedException;
+import com.gitblit.GitBlitException.UnauthorizedException;
+import com.gitblit.GitBlitException.UnknownRequestException;
+
+public abstract class GitblitWorker extends SwingWorker<Boolean, Void> {
+
+ private final Component parent;
+
+ private final RpcRequest request;
+
+ public GitblitWorker(Component parent, RpcRequest request) {
+ this.parent = parent;
+ this.request = request;
+ parent.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
+ }
+
+ protected RpcRequest getRequestType() {
+ return request;
+ }
+
+ @Override
+ protected Boolean doInBackground() throws IOException {
+ return doRequest();
+ }
+
+ protected void done() {
+ parent.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
+ try {
+ Boolean success = get();
+ if (success) {
+ onSuccess();
+ } else {
+ onFailure();
+ }
+ } catch (Throwable t) {
+ if (t instanceof ForbiddenException) {
+ Utils.explainForbidden(parent, request);
+ } else if (t instanceof UnauthorizedException) {
+ Utils.explainUnauthorized(parent, request);
+ } else if (t instanceof NotAllowedException) {
+ Utils.explainNotAllowed(parent, request);
+ } else if (t instanceof UnknownRequestException) {
+ Utils.explainNotAllowed(parent, request);
+ } else {
+ Utils.showException(parent, t);
+ }
+ }
+ }
+
+ protected abstract Boolean doRequest() throws IOException;
+
+ protected abstract void onSuccess();
+
+ protected void onFailure() {
+ }
+
+ protected void showFailure(String message, Object... args) {
+ String msg = MessageFormat.format(message, args);
+ JOptionPane.showMessageDialog(parent, msg, Translation.get("gb.error"),
+ JOptionPane.ERROR_MESSAGE);
+ }
+}
diff --git a/src/main/java/com/gitblit/client/HeaderPanel.java b/src/main/java/com/gitblit/client/HeaderPanel.java
new file mode 100644
index 00000000..3cd89a70
--- /dev/null
+++ b/src/main/java/com/gitblit/client/HeaderPanel.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.client;
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.GradientPaint;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.GridLayout;
+import java.awt.Insets;
+import java.awt.Paint;
+import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+import javax.swing.ImageIcon;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+
+import com.gitblit.utils.StringUtils;
+
+public class HeaderPanel extends JPanel {
+
+ private static final long serialVersionUID = 1L;
+
+ private final Insets insets = new Insets(5, 5, 5, 5);
+
+ private Color lightColor = new Color(0, 0, 0x60);
+
+ private JLabel headerLabel;
+
+ private JLabel refreshLabel;
+
+ public HeaderPanel(String text, String icon) {
+ // super(new FlowLayout(FlowLayout.LEFT), true);
+ super(new GridLayout(1, 2, 5, 5), true);
+ setOpaque(true);
+ setBackground(new Color(0, 0, 0x20));
+
+ headerLabel = new JLabel(text);
+ if (!StringUtils.isEmpty(icon)) {
+ headerLabel.setIcon(new ImageIcon(getClass().getResource("/" + icon)));
+ }
+ headerLabel.setForeground(Color.white);
+ headerLabel.setFont(headerLabel.getFont().deriveFont(14f));
+ add(headerLabel);
+
+ refreshLabel = new JLabel("", JLabel.RIGHT);
+ refreshLabel.setForeground(Color.white);
+ add(refreshLabel);
+ }
+
+ public void setText(String text) {
+ headerLabel.setText(text);
+ SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+ refreshLabel.setText("refreshed " + df.format(new Date()));
+ }
+
+ @Override
+ public Insets getInsets() {
+ return insets;
+ }
+
+ @Override
+ public void paintComponent(Graphics oldG) {
+ Graphics2D g = (Graphics2D) oldG;
+ Point2D startPoint = new Point2D.Float(0, 0);
+ Point2D endPoint = new Point2D.Float(0, getHeight());
+ Paint gradientPaint = new GradientPaint(startPoint, lightColor, endPoint, getBackground(),
+ false);
+ g.setPaint(gradientPaint);
+ g.fill(new Rectangle2D.Double(0, 0, getWidth(), getHeight()));
+ g.setColor(new Color(0xff, 0x99, 0x00));
+ int stroke = 2;
+ g.setStroke(new BasicStroke(stroke));
+ g.drawLine(0, getHeight() - 1, getWidth(), getHeight() - 1);
+ }
+}
diff --git a/src/main/java/com/gitblit/client/IndicatorsRenderer.java b/src/main/java/com/gitblit/client/IndicatorsRenderer.java
new file mode 100644
index 00000000..44b39d01
--- /dev/null
+++ b/src/main/java/com/gitblit/client/IndicatorsRenderer.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.client;
+
+import java.awt.Component;
+import java.awt.FlowLayout;
+import java.io.Serializable;
+
+import javax.swing.ImageIcon;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JTable;
+import javax.swing.table.TableCellRenderer;
+
+import com.gitblit.models.RepositoryModel;
+
+/**
+ * Renders the type indicators (tickets, frozen, access restriction, etc) in a
+ * single cell.
+ *
+ * @author James Moger
+ *
+ */
+public class IndicatorsRenderer extends JPanel implements TableCellRenderer, Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ private final ImageIcon blankIcon;
+
+ private final ImageIcon pushIcon;
+
+ private final ImageIcon pullIcon;
+
+ private final ImageIcon viewIcon;
+
+ private final ImageIcon tixIcon;
+
+ private final ImageIcon doxIcon;
+
+ private final ImageIcon frozenIcon;
+
+ private final ImageIcon federatedIcon;
+
+ private final ImageIcon forkIcon;
+
+ private final ImageIcon sparkleshareIcon;
+
+ public IndicatorsRenderer() {
+ super(new FlowLayout(FlowLayout.RIGHT, 1, 0));
+ blankIcon = new ImageIcon(getClass().getResource("/blank.png"));
+ pushIcon = new ImageIcon(getClass().getResource("/lock_go_16x16.png"));
+ pullIcon = new ImageIcon(getClass().getResource("/lock_pull_16x16.png"));
+ viewIcon = new ImageIcon(getClass().getResource("/shield_16x16.png"));
+ tixIcon = new ImageIcon(getClass().getResource("/bug_16x16.png"));
+ doxIcon = new ImageIcon(getClass().getResource("/book_16x16.png"));
+ frozenIcon = new ImageIcon(getClass().getResource("/cold_16x16.png"));
+ federatedIcon = new ImageIcon(getClass().getResource("/federated_16x16.png"));
+ forkIcon = new ImageIcon(getClass().getResource("/commit_divide_16x16.png"));
+ sparkleshareIcon = new ImageIcon(getClass().getResource("/star_16x16.png"));
+ }
+
+ @Override
+ public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,
+ boolean hasFocus, int row, int column) {
+ if (isSelected)
+ setBackground(table.getSelectionBackground());
+ else
+ setBackground(table.getBackground());
+ removeAll();
+ if (value instanceof RepositoryModel) {
+ StringBuilder tooltip = new StringBuilder();
+ RepositoryModel model = (RepositoryModel) value;
+ if (model.isSparkleshared()) {
+ JLabel icon = new JLabel(sparkleshareIcon);
+ tooltip.append(Translation.get("gb.isSparkleshared")).append("<br/>");
+ add(icon);
+ }
+ if (model.isFork()) {
+ JLabel icon = new JLabel(forkIcon);
+ tooltip.append(Translation.get("gb.isFork")).append("<br/>");
+ add(icon);
+ }
+ if (model.useTickets) {
+ JLabel icon = new JLabel(tixIcon);
+ tooltip.append(Translation.get("gb.tickets")).append("<br/>");
+ add(icon);
+ }
+ if (model.useDocs) {
+ JLabel icon = new JLabel(doxIcon);
+ tooltip.append(Translation.get("gb.docs")).append("<br/>");
+ add(icon);
+ }
+ if (model.isFrozen) {
+ JLabel icon = new JLabel(frozenIcon);
+ tooltip.append(Translation.get("gb.isFrozen")).append("<br/>");
+ add(icon);
+ }
+ if (model.isFederated) {
+ JLabel icon = new JLabel(federatedIcon);
+ tooltip.append(Translation.get("gb.isFederated")).append("<br/>");
+ add(icon);
+ }
+
+ switch (model.accessRestriction) {
+ case NONE: {
+ add(new JLabel(blankIcon));
+ break;
+ }
+ case PUSH: {
+ JLabel icon = new JLabel(pushIcon);
+ tooltip.append(Translation.get("gb.pushRestricted")).append("<br/>");
+ add(icon);
+ break;
+ }
+ case CLONE: {
+ JLabel icon = new JLabel(pullIcon);
+ tooltip.append(Translation.get("gb.cloneRestricted")).append("<br/>");
+ add(icon);
+ break;
+ }
+ case VIEW: {
+ JLabel icon = new JLabel(viewIcon);
+ tooltip.append(Translation.get("gb.viewRestricted")).append("<br/>");
+ add(icon);
+ break;
+ }
+ default:
+ add(new JLabel(blankIcon));
+ }
+ if (tooltip.length() > 0) {
+ tooltip.insert(0, "<html><body>");
+ setToolTipText(tooltip.toString().trim());
+ }
+ }
+ return this;
+ }
+} \ No newline at end of file
diff --git a/src/main/java/com/gitblit/client/JPalette.java b/src/main/java/com/gitblit/client/JPalette.java
new file mode 100644
index 00000000..a0c2b258
--- /dev/null
+++ b/src/main/java/com/gitblit/client/JPalette.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.client;
+
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.Font;
+import java.awt.GridBagLayout;
+import java.awt.GridLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.JButton;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTable;
+import javax.swing.table.AbstractTableModel;
+
+public class JPalette<T> extends JPanel {
+
+ private static final long serialVersionUID = 1L;
+ private PaletteModel<T> availableModel;
+ private PaletteModel<T> selectedModel;
+ private JButton add;
+ private JButton subtract;
+ private JButton up;
+ private JButton down;
+
+ public JPalette() {
+ this(false);
+ }
+
+ public JPalette(boolean controlOrder) {
+ super(new BorderLayout(5, 5));
+
+ availableModel = new PaletteModel<T>();
+ selectedModel = new PaletteModel<T>();
+
+ final JTable available = new JTable(availableModel);
+ final JTable selected = new JTable(selectedModel);
+
+ add = new JButton("->");
+ add.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent event) {
+ List<T> move = new ArrayList<T>();
+ if (available.getSelectedRowCount() <= 0) {
+ return;
+ }
+ for (int row : available.getSelectedRows()) {
+ int modelIndex = available.convertRowIndexToModel(row);
+ T item = (T) availableModel.list.get(modelIndex);
+ move.add(item);
+ }
+ availableModel.list.removeAll(move);
+ selectedModel.list.addAll(move);
+ availableModel.fireTableDataChanged();
+ selectedModel.fireTableDataChanged();
+ }
+ });
+ subtract = new JButton("<-");
+ subtract.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent event) {
+ List<T> move = new ArrayList<T>();
+ if (selected.getSelectedRowCount() <= 0) {
+ return;
+ }
+ for (int row : selected.getSelectedRows()) {
+ int modelIndex = selected.convertRowIndexToModel(row);
+ T item = (T) selectedModel.list.get(modelIndex);
+ move.add(item);
+ }
+ selectedModel.list.removeAll(move);
+ availableModel.list.addAll(move);
+
+ selectedModel.fireTableDataChanged();
+ availableModel.fireTableDataChanged();
+ }
+ });
+
+ up = new JButton("\u2191");
+ up.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent event) {
+ int row = selected.getSelectedRow();
+ if (row > 0) {
+ T o = selectedModel.list.remove(row);
+ selectedModel.list.add(row - 1, o);
+ selectedModel.fireTableDataChanged();
+ }
+ }
+ });
+
+ down = new JButton("\u2193");
+ down.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent event) {
+ int row = selected.getSelectedRow();
+ if (row < selected.getRowCount() - 1) {
+ T o = selectedModel.list.remove(row);
+ selectedModel.list.add(row + 1, o);
+ selectedModel.fireTableDataChanged();
+ }
+ }
+ });
+
+ JPanel controls = new JPanel(new GridLayout(0, 1, 0, 5));
+ controls.add(add);
+ controls.add(subtract);
+ if (controlOrder) {
+ controls.add(up);
+ controls.add(down);
+ }
+
+ JPanel center = new JPanel(new GridBagLayout());
+ center.add(controls);
+
+ add(newListPanel(Translation.get("gb.available"), available), BorderLayout.WEST);
+ add(center, BorderLayout.CENTER);
+ add(newListPanel(Translation.get("gb.selected"), selected), BorderLayout.EAST);
+ }
+
+ private JPanel newListPanel(String label, JTable table) {
+ NameRenderer nameRenderer = new NameRenderer();
+ table.setCellSelectionEnabled(false);
+ table.setRowSelectionAllowed(true);
+ table.getTableHeader().setReorderingAllowed(false);
+ table.setGridColor(new Color(0xd9d9d9));
+ table.setBackground(Color.white);
+ table.getColumn(table.getColumnName(0)).setCellRenderer(nameRenderer);
+
+ JScrollPane jsp = new JScrollPane(table);
+ jsp.setPreferredSize(new Dimension(225, 160));
+ JPanel panel = new JPanel(new BorderLayout());
+ JLabel jlabel = new JLabel(label);
+ jlabel.setFont(jlabel.getFont().deriveFont(Font.BOLD));
+ panel.add(jlabel, BorderLayout.NORTH);
+ panel.add(jsp, BorderLayout.CENTER);
+ return panel;
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ super.setEnabled(enabled);
+ add.setEnabled(enabled);
+ subtract.setEnabled(enabled);
+ up.setEnabled(enabled);
+ down.setEnabled(enabled);
+ }
+
+ public void setObjects(List<T> all, List<T> selected) {
+ List<T> available = new ArrayList<T>(all);
+ if (selected != null) {
+ available.removeAll(selected);
+ }
+ availableModel.list.clear();
+ availableModel.list.addAll(available);
+ availableModel.fireTableDataChanged();
+
+ if (selected != null) {
+ selectedModel.list.clear();
+ selectedModel.list.addAll(selected);
+ selectedModel.fireTableDataChanged();
+ }
+ }
+
+ public List<T> getSelections() {
+ return new ArrayList<T>(selectedModel.list);
+ }
+
+ public class PaletteModel<K> extends AbstractTableModel {
+
+ private static final long serialVersionUID = 1L;
+
+ List<K> list;
+
+ public PaletteModel() {
+ this(new ArrayList<K>());
+ }
+
+ public PaletteModel(List<K> list) {
+ this.list = new ArrayList<K>(list);
+ }
+
+ @Override
+ public int getRowCount() {
+ return list.size();
+ }
+
+ @Override
+ public int getColumnCount() {
+ return 1;
+ }
+
+ @Override
+ public String getColumnName(int column) {
+ return Translation.get("gb.name");
+ }
+
+ public Class<?> getColumnClass(int columnIndex) {
+ return String.class;
+ }
+
+ @Override
+ public Object getValueAt(int rowIndex, int columnIndex) {
+ K o = list.get(rowIndex);
+ return o.toString();
+ }
+ }
+}
diff --git a/src/main/java/com/gitblit/client/MessageRenderer.java b/src/main/java/com/gitblit/client/MessageRenderer.java
new file mode 100644
index 00000000..2fe3415a
--- /dev/null
+++ b/src/main/java/com/gitblit/client/MessageRenderer.java
@@ -0,0 +1,205 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.client;
+
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.FlowLayout;
+import java.awt.Font;
+import java.io.Serializable;
+
+import javax.swing.ImageIcon;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JTable;
+import javax.swing.border.Border;
+import javax.swing.border.LineBorder;
+import javax.swing.table.TableCellRenderer;
+
+import org.eclipse.jgit.lib.Constants;
+
+import com.gitblit.models.FeedEntryModel;
+
+/**
+ * Message renderer displays the short log message and then any refs in a style
+ * like the site.
+ *
+ * @author James Moger
+ *
+ */
+public class MessageRenderer extends JPanel implements TableCellRenderer, Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ private final GitblitClient gitblit;
+
+ private final ImageIcon mergeIcon;
+
+ private final ImageIcon blankIcon;
+
+ private final JLabel messageLabel;
+
+ private final JLabel headLabel;
+
+ private final JLabel branchLabel;
+
+ private final JLabel remoteLabel;
+
+ private final JLabel tagLabel;
+
+ public MessageRenderer() {
+ this(null);
+ }
+
+ public MessageRenderer(GitblitClient gitblit) {
+ super(new FlowLayout(FlowLayout.LEFT, Utils.MARGIN, 1));
+ this.gitblit = gitblit;
+
+ mergeIcon = new ImageIcon(getClass().getResource("/commit_merge_16x16.png"));
+ blankIcon = new ImageIcon(getClass().getResource("/blank.png"));
+
+ messageLabel = new JLabel();
+
+ headLabel = newRefLabel();
+ branchLabel = newRefLabel();
+ remoteLabel = newRefLabel();
+ tagLabel = newRefLabel();
+
+ add(messageLabel);
+ add(headLabel);
+ add(branchLabel);
+ add(remoteLabel);
+ add(tagLabel);
+ }
+
+ private JLabel newRefLabel() {
+ JLabel label = new JLabel();
+ label.setOpaque(true);
+ Font font = label.getFont();
+ label.setFont(font.deriveFont(font.getSize2D() - 1f));
+ return label;
+ }
+
+ private void resetRef(JLabel label) {
+ label.setText("");
+ label.setBackground(messageLabel.getBackground());
+ label.setBorder(null);
+ label.setVisible(false);
+ }
+
+ private void showRef(String ref, JLabel label) {
+ String name = ref;
+ Color bg = getBackground();
+ Border border = null;
+ if (name.startsWith(Constants.R_HEADS)) {
+ // local branch
+ bg = Color.decode("#CCFFCC");
+ name = name.substring(Constants.R_HEADS.length());
+ border = new LineBorder(Color.decode("#00CC33"), 1);
+ } else if (name.startsWith(Constants.R_REMOTES)) {
+ // remote branch
+ bg = Color.decode("#CAC2F5");
+ name = name.substring(Constants.R_REMOTES.length());
+ border = new LineBorder(Color.decode("#6C6CBF"), 1);
+ } else if (name.startsWith(Constants.R_TAGS)) {
+ // tag
+ bg = Color.decode("#FFFFAA");
+ name = name.substring(Constants.R_TAGS.length());
+ border = new LineBorder(Color.decode("#FFCC00"), 1);
+ } else if (name.equals(Constants.HEAD)) {
+ // HEAD
+ bg = Color.decode("#FFAAFF");
+ border = new LineBorder(Color.decode("#FF00EE"), 1);
+ } else {
+ }
+ label.setText(name);
+ label.setBackground(bg);
+ label.setBorder(border);
+ label.setVisible(true);
+ }
+
+ public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,
+ boolean hasFocus, int row, int column) {
+ if (isSelected)
+ setBackground(table.getSelectionBackground());
+ else
+ setBackground(table.getBackground());
+ messageLabel.setForeground(isSelected ? table.getSelectionForeground() : table
+ .getForeground());
+ if (value == null) {
+ return this;
+ }
+ FeedEntryModel entry = (FeedEntryModel) value;
+
+ if (gitblit == null) {
+ // no gitblit client, just display message
+ messageLabel.setText(entry.title);
+ } else {
+ // show message in BOLD if its a new entry
+ if (entry.published.after(gitblit.getLastFeedRefresh(entry.repository, entry.branch))) {
+ messageLabel.setText("<html><body><b>" + entry.title);
+ } else {
+ messageLabel.setText(entry.title);
+ }
+ }
+
+ // reset ref label
+ resetRef(headLabel);
+ resetRef(branchLabel);
+ resetRef(remoteLabel);
+ resetRef(tagLabel);
+
+ int parentCount = 0;
+ if (entry.tags != null) {
+ for (String tag : entry.tags) {
+ if (tag.startsWith("ref:")) {
+ // strip ref:
+ tag = tag.substring("ref:".length());
+ } else {
+ // count parents
+ if (tag.startsWith("parent:")) {
+ parentCount++;
+ }
+ }
+ if (tag.equals(entry.branch)) {
+ // skip current branch label
+ continue;
+ }
+ if (tag.startsWith(Constants.R_HEADS)) {
+ // local branch
+ showRef(tag, branchLabel);
+ } else if (tag.startsWith(Constants.R_REMOTES)) {
+ // remote branch
+ showRef(tag, remoteLabel);
+ } else if (tag.startsWith(Constants.R_TAGS)) {
+ // tag
+ showRef(tag, tagLabel);
+ } else if (tag.equals(Constants.HEAD)) {
+ // HEAD
+ showRef(tag, headLabel);
+ }
+ }
+ }
+
+ if (parentCount > 1) {
+ // multiple parents, show merge icon
+ messageLabel.setIcon(mergeIcon);
+ } else {
+ messageLabel.setIcon(blankIcon);
+ }
+ return this;
+ }
+} \ No newline at end of file
diff --git a/src/main/java/com/gitblit/client/NameRenderer.java b/src/main/java/com/gitblit/client/NameRenderer.java
new file mode 100644
index 00000000..4cbb5906
--- /dev/null
+++ b/src/main/java/com/gitblit/client/NameRenderer.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.client;
+
+import java.awt.Color;
+import java.awt.Component;
+
+import javax.swing.JList;
+import javax.swing.JTable;
+import javax.swing.ListCellRenderer;
+import javax.swing.table.DefaultTableCellRenderer;
+
+/**
+ * Repository name cell renderer. This renderer shows the group name in a gray
+ * color and accentuates the repository name in a cornflower blue color.
+ *
+ * @author James Moger
+ *
+ */
+public class NameRenderer extends DefaultTableCellRenderer implements ListCellRenderer {
+
+ private static final long serialVersionUID = 1L;
+
+ private static final Color CORNFLOWER = new Color(0x00, 0x69, 0xD6);
+
+ private final String groupSpan;
+
+ public NameRenderer() {
+ this(Color.gray, CORNFLOWER);
+ }
+
+ private NameRenderer(Color group, Color repo) {
+ groupSpan = "<span style='color:" + getHexColor(group) + "'>";
+ setForeground(repo);
+ }
+
+ String getHexColor(Color c) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(Integer.toHexString((c.getRGB() & 0x00FFFFFF)));
+ while (sb.length() < 6)
+ sb.insert(0, '0');
+ sb.insert(0, '#');
+ return sb.toString();
+ }
+
+ public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,
+ boolean hasFocus, int row, int column) {
+ super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
+ setValue(value == null ? "" : value, isSelected);
+ return this;
+ }
+
+ @Override
+ public Component getListCellRendererComponent(JList list, Object value, int index,
+ boolean isSelected, boolean cellHasFocus) {
+ setValue(value == null ? "" : value, isSelected);
+ if (isSelected) {
+ setBackground(list.getSelectionBackground());
+ setForeground(list.getSelectionForeground());
+ } else {
+ setBackground(list.getBackground());
+ setForeground(CORNFLOWER);
+ }
+ return this;
+ }
+
+ private void setValue(Object value, boolean isSelected) {
+ String name = value.toString();
+ int lastSlash = name.lastIndexOf('/');
+ if (!isSelected && lastSlash > -1) {
+ String group = name.substring(0, lastSlash + 1);
+ String repo = name.substring(lastSlash + 1);
+ setText("<html><body>" + groupSpan + group + "</span>" + repo);
+ } else {
+ this.setText(name);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main/java/com/gitblit/client/PropertiesTableModel.java b/src/main/java/com/gitblit/client/PropertiesTableModel.java
new file mode 100644
index 00000000..0c803f47
--- /dev/null
+++ b/src/main/java/com/gitblit/client/PropertiesTableModel.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.client;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.swing.table.AbstractTableModel;
+
+/**
+ * Table model of a map of properties.
+ *
+ * @author James Moger
+ *
+ */
+public class PropertiesTableModel extends AbstractTableModel {
+
+ private static final long serialVersionUID = 1L;
+
+ List<String> keys;
+
+ Map<String, String> map;
+
+ enum Columns {
+ Name, Value;
+
+ @Override
+ public String toString() {
+ return name().replace('_', ' ');
+ }
+ }
+
+ public PropertiesTableModel() {
+ this(new HashMap<String, String>());
+ }
+
+ public PropertiesTableModel(Map<String, String> map) {
+ setProperties(map);
+ }
+
+ public void setProperties(Map<String, String> map) {
+ this.map = map;
+ keys = new ArrayList<String>(map.keySet());
+ Collections.sort(this.keys);
+ }
+
+ @Override
+ public int getRowCount() {
+ return keys.size();
+ }
+
+ @Override
+ public int getColumnCount() {
+ return Columns.values().length;
+ }
+
+ @Override
+ public String getColumnName(int column) {
+ Columns col = Columns.values()[column];
+ switch (col) {
+ case Name:
+ return Translation.get("gb.name");
+ }
+ return "";
+ }
+
+ /**
+ * Returns <code>Object.class</code> regardless of <code>columnIndex</code>.
+ *
+ * @param columnIndex
+ * the column being queried
+ * @return the Object.class
+ */
+ public Class<?> getColumnClass(int columnIndex) {
+ return String.class;
+ }
+
+ @Override
+ public Object getValueAt(int rowIndex, int columnIndex) {
+ String key = keys.get(rowIndex);
+ Columns col = Columns.values()[columnIndex];
+ switch (col) {
+ case Name:
+ return key;
+ case Value:
+ return map.get(key);
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/com/gitblit/client/RegistrantPermissionsPanel.java b/src/main/java/com/gitblit/client/RegistrantPermissionsPanel.java
new file mode 100644
index 00000000..98dbfb72
--- /dev/null
+++ b/src/main/java/com/gitblit/client/RegistrantPermissionsPanel.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright 2012 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.client;
+
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.Dimension;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import javax.swing.DefaultCellEditor;
+import javax.swing.DefaultComboBoxModel;
+import javax.swing.JButton;
+import javax.swing.JComboBox;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTable;
+import javax.swing.SwingConstants;
+import javax.swing.table.DefaultTableCellRenderer;
+
+import com.gitblit.Constants.AccessPermission;
+import com.gitblit.Constants.PermissionType;
+import com.gitblit.Constants.RegistrantType;
+import com.gitblit.client.Utils.RowRenderer;
+import com.gitblit.models.RegistrantAccessPermission;
+import com.gitblit.utils.StringUtils;
+
+public class RegistrantPermissionsPanel extends JPanel {
+
+ private static final long serialVersionUID = 1L;
+
+ private JTable permissionsTable;
+
+ private RegistrantPermissionsTableModel tableModel;
+
+ private DefaultComboBoxModel registrantModel;
+
+ private JComboBox registrantSelector;
+
+ private JComboBox permissionSelector;
+
+ private JButton addButton;
+
+ private JPanel addPanel;
+
+ public RegistrantPermissionsPanel(final RegistrantType registrantType) {
+ super(new BorderLayout(5, 5));
+ tableModel = new RegistrantPermissionsTableModel();
+ permissionsTable = Utils.newTable(tableModel, Utils.DATE_FORMAT, new RowRenderer() {
+ Color clear = new Color(0, 0, 0, 0);
+ Color iceGray = new Color(0xf0, 0xf0, 0xf0);
+
+ @Override
+ public void prepareRow(Component c, boolean isSelected, int row, int column) {
+ if (isSelected) {
+ c.setBackground(permissionsTable.getSelectionBackground());
+ } else {
+ if (tableModel.permissions.get(row).mutable) {
+ c.setBackground(clear);
+ } else {
+ c.setBackground(iceGray);
+ }
+ }
+ }
+ });
+ permissionsTable.setModel(tableModel);
+ permissionsTable.setPreferredScrollableViewportSize(new Dimension(400, 150));
+ JScrollPane jsp = new JScrollPane(permissionsTable);
+ add(jsp, BorderLayout.CENTER);
+
+ permissionsTable.getColumnModel().getColumn(RegistrantPermissionsTableModel.Columns.Registrant.ordinal())
+ .setCellRenderer(new NameRenderer());
+ permissionsTable.getColumnModel().getColumn(RegistrantPermissionsTableModel.Columns.Type.ordinal())
+ .setCellRenderer(new PermissionTypeRenderer());
+ permissionsTable.getColumnModel().getColumn(RegistrantPermissionsTableModel.Columns.Permission.ordinal())
+ .setCellEditor(new AccessPermissionEditor());
+
+ registrantModel = new DefaultComboBoxModel();
+ registrantSelector = new JComboBox(registrantModel);
+ permissionSelector = new JComboBox(AccessPermission.NEWPERMISSIONS);
+ addButton = new JButton(Translation.get("gb.add"));
+ addButton.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ if (registrantSelector.getSelectedIndex() < 0) {
+ return;
+ }
+ if (permissionSelector.getSelectedIndex() < 0) {
+ return;
+ }
+
+ RegistrantAccessPermission rp = new RegistrantAccessPermission(registrantType);
+ rp.registrant = registrantSelector.getSelectedItem().toString();
+ rp.permission = (AccessPermission) permissionSelector.getSelectedItem();
+ if (StringUtils.findInvalidCharacter(rp.registrant) != null) {
+ rp.permissionType = PermissionType.REGEX;
+ rp.source = rp.registrant;
+ } else {
+ rp.permissionType = PermissionType.EXPLICIT;
+ }
+
+ tableModel.permissions.add(rp);
+ // resort permissions after insert to convey idea of eval order
+ Collections.sort(tableModel.permissions);
+
+ registrantModel.removeElement(rp.registrant);
+ registrantSelector.setSelectedIndex(-1);
+ registrantSelector.invalidate();
+ addPanel.setVisible(registrantModel.getSize() > 0);
+
+ tableModel.fireTableDataChanged();
+ }
+ });
+
+ addPanel = new JPanel();
+ addPanel.add(registrantSelector);
+ addPanel.add(permissionSelector);
+ addPanel.add(addButton);
+ add(addPanel, BorderLayout.SOUTH);
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ super.setEnabled(enabled);
+ permissionsTable.setEnabled(enabled);
+ registrantSelector.setEnabled(enabled);
+ permissionSelector.setEnabled(enabled);
+ addButton.setEnabled(enabled);
+ }
+
+ public void setObjects(List<String> registrants, List<RegistrantAccessPermission> permissions) {
+ List<String> filtered;
+ if (registrants == null) {
+ filtered = new ArrayList<String>();
+ } else {
+ filtered = new ArrayList<String>(registrants);
+ }
+ if (permissions == null) {
+ permissions = new ArrayList<RegistrantAccessPermission>();
+ }
+ for (RegistrantAccessPermission rp : permissions) {
+ if (rp.mutable) {
+ // only remove editable duplicates
+ // this allows for specifying an explicit permission
+ filtered.remove(rp.registrant);
+ } else if (rp.isAdmin()) {
+ // administrators can not have their permission changed
+ filtered.remove(rp.registrant);
+ } else if (rp.isOwner()) {
+ // owners can not have their permission changed
+ filtered.remove(rp.registrant);
+ }
+ }
+ for (String registrant : filtered) {
+ registrantModel.addElement(registrant);
+ }
+ tableModel.setPermissions(permissions);
+
+ registrantSelector.setSelectedIndex(-1);
+ permissionSelector.setSelectedIndex(-1);
+ addPanel.setVisible(filtered.size() > 0);
+ }
+
+ public List<RegistrantAccessPermission> getPermissions() {
+ return tableModel.permissions;
+ }
+
+ private class AccessPermissionEditor extends DefaultCellEditor {
+
+ private static final long serialVersionUID = 1L;
+
+ public AccessPermissionEditor() {
+ super(new JComboBox(AccessPermission.values()));
+ }
+ }
+
+ private class PermissionTypeRenderer extends DefaultTableCellRenderer {
+
+ private static final long serialVersionUID = 1L;
+
+ public PermissionTypeRenderer() {
+ super();
+ setHorizontalAlignment(SwingConstants.CENTER);
+ }
+
+ @Override
+ protected void setValue(Object value) {
+ RegistrantAccessPermission ap = (RegistrantAccessPermission) value;
+ switch (ap.permissionType) {
+ case ADMINISTRATOR:
+ setText(ap.source == null ? Translation.get("gb.administrator") : ap.source);
+ setToolTipText(Translation.get("gb.administratorPermission"));
+ break;
+ case OWNER:
+ setText(Translation.get("gb.owner"));
+ setToolTipText(Translation.get("gb.ownerPermission"));
+ break;
+ case TEAM:
+ setText(ap.source == null ? Translation.get("gb.team") : ap.source);
+ setToolTipText(MessageFormat.format(Translation.get("gb.teamPermission"), ap.source));
+ break;
+ case REGEX:
+ setText("regex");
+ setToolTipText(MessageFormat.format(Translation.get("gb.regexPermission"), ap.source));
+ break;
+ default:
+ if (ap.isMissing()) {
+ setText(Translation.get("gb.missing"));
+ setToolTipText(Translation.get("gb.missingPermission"));
+ } else {
+ setText("");
+ setToolTipText(null);
+ }
+ break;
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/gitblit/client/RegistrantPermissionsTableModel.java b/src/main/java/com/gitblit/client/RegistrantPermissionsTableModel.java
new file mode 100644
index 00000000..28d25345
--- /dev/null
+++ b/src/main/java/com/gitblit/client/RegistrantPermissionsTableModel.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2012 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.client;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.table.AbstractTableModel;
+
+import com.gitblit.Constants.AccessPermission;
+import com.gitblit.models.RegistrantAccessPermission;
+
+/**
+ * Table model of a registrant permissions.
+ *
+ * @author James Moger
+ *
+ */
+public class RegistrantPermissionsTableModel extends AbstractTableModel {
+
+ private static final long serialVersionUID = 1L;
+
+ List<RegistrantAccessPermission> permissions;
+
+ enum Columns {
+ Registrant, Type, Permission;
+
+ @Override
+ public String toString() {
+ return name().replace('_', ' ');
+ }
+ }
+
+ public RegistrantPermissionsTableModel() {
+ this(new ArrayList<RegistrantAccessPermission>());
+ }
+
+ public RegistrantPermissionsTableModel(List<RegistrantAccessPermission> list) {
+ setPermissions(list);
+ }
+
+ public void setPermissions(List<RegistrantAccessPermission> list) {
+ this.permissions = list;
+ }
+
+ @Override
+ public int getRowCount() {
+ return permissions.size();
+ }
+
+ @Override
+ public int getColumnCount() {
+ return Columns.values().length;
+ }
+
+ @Override
+ public String getColumnName(int column) {
+ Columns col = Columns.values()[column];
+ switch (col) {
+ case Registrant:
+ return Translation.get("gb.name");
+ case Type:
+ return Translation.get("gb.type");
+ case Permission:
+ return Translation.get("gb.permission");
+ }
+ return "";
+ }
+
+ /**
+ * Returns <code>Object.class</code> regardless of <code>columnIndex</code>.
+ *
+ * @param columnIndex
+ * the column being queried
+ * @return the Object.class
+ */
+ public Class<?> getColumnClass(int columnIndex) {
+ if (columnIndex == Columns.Permission.ordinal()) {
+ return AccessPermission.class;
+ } else if (columnIndex == Columns.Type.ordinal()) {
+ return RegistrantAccessPermission.class;
+ }
+ return String.class;
+ }
+
+ @Override
+ public boolean isCellEditable(int rowIndex, int columnIndex) {
+ if (columnIndex == Columns.Permission.ordinal()) {
+ // in order for the permission to be editable it must be
+ // explicitly defined on the object. regex permissions are inherited
+ // and therefore can not be directly manipulated unless the current
+ // object is the source of the regex (i.e. a user or team with explicit
+ // regex definition)
+ return permissions.get(rowIndex).mutable;
+ }
+ return false;
+ }
+
+ @Override
+ public Object getValueAt(int rowIndex, int columnIndex) {
+ RegistrantAccessPermission rp = permissions.get(rowIndex);
+ Columns col = Columns.values()[columnIndex];
+ switch (col) {
+ case Registrant:
+ return rp.registrant;
+ case Type:
+ return rp;
+ case Permission:
+ return rp.permission;
+ }
+ return null;
+ }
+
+ @Override
+ public void setValueAt(Object o, int rowIndex, int columnIndex) {
+ RegistrantAccessPermission rp = permissions.get(rowIndex);
+ if (columnIndex == Columns.Permission.ordinal()) {
+ rp.permission = (AccessPermission) o;
+ }
+ }
+}
diff --git a/src/main/java/com/gitblit/client/RegistrationsDialog.java b/src/main/java/com/gitblit/client/RegistrationsDialog.java
new file mode 100644
index 00000000..9550e97a
--- /dev/null
+++ b/src/main/java/com/gitblit/client/RegistrationsDialog.java
@@ -0,0 +1,228 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.client;
+
+import java.awt.BorderLayout;
+import java.awt.FlowLayout;
+import java.awt.Insets;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.KeyEvent;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.ImageIcon;
+import javax.swing.JButton;
+import javax.swing.JComponent;
+import javax.swing.JDialog;
+import javax.swing.JPanel;
+import javax.swing.JRootPane;
+import javax.swing.JScrollPane;
+import javax.swing.JTable;
+import javax.swing.KeyStroke;
+import javax.swing.event.ListSelectionEvent;
+import javax.swing.event.ListSelectionListener;
+
+/**
+ * Displays a list of registrations and allows management of server
+ * registrations.
+ *
+ * @author James Moger
+ *
+ */
+public class RegistrationsDialog extends JDialog {
+
+ interface RegistrationListener {
+
+ void login(GitblitRegistration reg);
+
+ boolean saveRegistration(String name, GitblitRegistration reg);
+
+ boolean deleteRegistrations(List<GitblitRegistration> list);
+ }
+
+ private static final long serialVersionUID = 1L;
+
+ private final List<GitblitRegistration> registrations;
+
+ private final RegistrationListener listener;
+
+ private JTable registrationsTable;
+
+ private RegistrationsTableModel model;
+
+ public RegistrationsDialog(List<GitblitRegistration> registrations,
+ RegistrationListener listener) {
+ super();
+ this.registrations = registrations;
+ this.listener = listener;
+ setTitle(Translation.get("gb.manage"));
+ setIconImage(new ImageIcon(getClass().getResource("/gitblt-favicon.png")).getImage());
+ initialize();
+ setSize(600, 400);
+ }
+
+ @Override
+ protected JRootPane createRootPane() {
+ KeyStroke stroke = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);
+ JRootPane rootPane = new JRootPane();
+ rootPane.registerKeyboardAction(new ActionListener() {
+ public void actionPerformed(ActionEvent actionEvent) {
+ setVisible(false);
+ }
+ }, stroke, JComponent.WHEN_IN_FOCUSED_WINDOW);
+ return rootPane;
+ }
+
+ private void initialize() {
+ NameRenderer nameRenderer = new NameRenderer();
+ model = new RegistrationsTableModel(registrations);
+ registrationsTable = Utils.newTable(model, Utils.DATE_FORMAT);
+
+ String id = registrationsTable
+ .getColumnName(RegistrationsTableModel.Columns.Name.ordinal());
+ registrationsTable.getColumn(id).setCellRenderer(nameRenderer);
+ registrationsTable.addMouseListener(new MouseAdapter() {
+ public void mouseClicked(MouseEvent e) {
+ if (e.getClickCount() == 2) {
+ login();
+ }
+ }
+ });
+
+ final JButton create = new JButton(Translation.get("gb.create"));
+ create.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent event) {
+ create();
+ }
+ });
+
+ final JButton login = new JButton(Translation.get("gb.login"));
+ login.setEnabled(false);
+ login.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent event) {
+ login();
+ }
+ });
+
+ final JButton edit = new JButton(Translation.get("gb.edit"));
+ edit.setEnabled(false);
+ edit.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent event) {
+ edit();
+ }
+ });
+
+ final JButton delete = new JButton(Translation.get("gb.delete"));
+ delete.setEnabled(false);
+ delete.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent event) {
+ delete();
+ }
+ });
+
+ registrationsTable.getSelectionModel().addListSelectionListener(
+ new ListSelectionListener() {
+ @Override
+ public void valueChanged(ListSelectionEvent e) {
+ if (e.getValueIsAdjusting()) {
+ return;
+ }
+ boolean singleSelection = registrationsTable.getSelectedRowCount() == 1;
+ boolean selected = registrationsTable.getSelectedRow() > -1;
+ login.setEnabled(singleSelection);
+ edit.setEnabled(singleSelection);
+ delete.setEnabled(selected);
+ }
+ });
+
+ JPanel controls = new JPanel(new FlowLayout(FlowLayout.CENTER, 5, 0));
+ controls.add(create);
+ controls.add(login);
+ controls.add(edit);
+ controls.add(delete);
+
+ final Insets insets = new Insets(5, 5, 5, 5);
+ JPanel centerPanel = new JPanel(new BorderLayout(5, 5)) {
+
+ private static final long serialVersionUID = 1L;
+
+ public Insets getInsets() {
+ return insets;
+ }
+ };
+ centerPanel.add(new HeaderPanel(Translation.get("gb.servers"), null), BorderLayout.NORTH);
+ centerPanel.add(new JScrollPane(registrationsTable), BorderLayout.CENTER);
+ centerPanel.add(controls, BorderLayout.SOUTH);
+
+ getContentPane().setLayout(new BorderLayout(5, 5));
+ getContentPane().add(centerPanel, BorderLayout.CENTER);
+ }
+
+ private void login() {
+ int viewRow = registrationsTable.getSelectedRow();
+ int modelRow = registrationsTable.convertRowIndexToModel(viewRow);
+ GitblitRegistration reg = registrations.get(modelRow);
+ RegistrationsDialog.this.setVisible(false);
+ listener.login(reg);
+ }
+
+ private void create() {
+ EditRegistrationDialog dialog = new EditRegistrationDialog(getOwner());
+ dialog.setLocationRelativeTo(this);
+ dialog.setVisible(true);
+ GitblitRegistration reg = dialog.getRegistration();
+ if (reg == null) {
+ return;
+ }
+ if (listener.saveRegistration(reg.name, reg)) {
+ model.list.add(reg);
+ model.fireTableDataChanged();
+ }
+ }
+
+ private void edit() {
+ int viewRow = registrationsTable.getSelectedRow();
+ int modelRow = registrationsTable.convertRowIndexToModel(viewRow);
+ GitblitRegistration reg = registrations.get(modelRow);
+ String originalName = reg.name;
+ EditRegistrationDialog dialog = new EditRegistrationDialog(getOwner(), reg, false);
+ dialog.setLocationRelativeTo(this);
+ dialog.setVisible(true);
+ reg = dialog.getRegistration();
+ if (reg == null) {
+ return;
+ }
+ if (listener.saveRegistration(originalName, reg)) {
+ model.fireTableDataChanged();
+ }
+ }
+
+ private void delete() {
+ List<GitblitRegistration> list = new ArrayList<GitblitRegistration>();
+ for (int i : registrationsTable.getSelectedRows()) {
+ int model = registrationsTable.convertRowIndexToModel(i);
+ GitblitRegistration reg = registrations.get(model);
+ list.add(reg);
+ }
+ if (listener.deleteRegistrations(list)) {
+ registrations.removeAll(list);
+ model.fireTableDataChanged();
+ }
+ }
+}
diff --git a/src/main/java/com/gitblit/client/RegistrationsTableModel.java b/src/main/java/com/gitblit/client/RegistrationsTableModel.java
new file mode 100644
index 00000000..8c6b34ff
--- /dev/null
+++ b/src/main/java/com/gitblit/client/RegistrationsTableModel.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.client;
+
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+
+import javax.swing.table.AbstractTableModel;
+
+/**
+ * Table model of a list of Gitblit server registrations.
+ *
+ * @author James Moger
+ *
+ */
+public class RegistrationsTableModel extends AbstractTableModel {
+
+ private static final long serialVersionUID = 1L;
+
+ List<GitblitRegistration> list;
+
+ enum Columns {
+ Name, URL, Last_Login;
+
+ @Override
+ public String toString() {
+ return name().replace('_', ' ');
+ }
+ }
+
+ public RegistrationsTableModel(List<GitblitRegistration> list) {
+ this.list = list;
+ Collections.sort(this.list);
+ }
+
+ @Override
+ public int getRowCount() {
+ return list.size();
+ }
+
+ @Override
+ public int getColumnCount() {
+ return Columns.values().length;
+ }
+
+ @Override
+ public String getColumnName(int column) {
+ Columns col = Columns.values()[column];
+ switch (col) {
+ case Name:
+ return Translation.get("gb.name");
+ case URL:
+ return Translation.get("gb.url");
+ case Last_Login:
+ return Translation.get("gb.lastLogin");
+ }
+ return "";
+ }
+
+ /**
+ * Returns <code>Object.class</code> regardless of <code>columnIndex</code>.
+ *
+ * @param columnIndex
+ * the column being queried
+ * @return the Object.class
+ */
+ public Class<?> getColumnClass(int columnIndex) {
+ if (columnIndex == Columns.Last_Login.ordinal()) {
+ return Date.class;
+ }
+ return String.class;
+ }
+
+ @Override
+ public Object getValueAt(int rowIndex, int columnIndex) {
+ GitblitRegistration model = list.get(rowIndex);
+ Columns col = Columns.values()[columnIndex];
+ switch (col) {
+ case Name:
+ return model.name;
+ case URL:
+ return model.url;
+ case Last_Login:
+ return model.lastLogin;
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/com/gitblit/client/RepositoriesPanel.java b/src/main/java/com/gitblit/client/RepositoriesPanel.java
new file mode 100644
index 00000000..64bde9b8
--- /dev/null
+++ b/src/main/java/com/gitblit/client/RepositoriesPanel.java
@@ -0,0 +1,560 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.client;
+
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.FlowLayout;
+import java.awt.Insets;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.KeyAdapter;
+import java.awt.event.KeyEvent;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+import javax.swing.JButton;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTable;
+import javax.swing.JTextField;
+import javax.swing.RowFilter;
+import javax.swing.SwingConstants;
+import javax.swing.event.ListSelectionEvent;
+import javax.swing.event.ListSelectionListener;
+import javax.swing.table.DefaultTableCellRenderer;
+import javax.swing.table.TableCellRenderer;
+import javax.swing.table.TableRowSorter;
+
+import com.gitblit.Constants;
+import com.gitblit.Constants.RpcRequest;
+import com.gitblit.Keys;
+import com.gitblit.models.FeedModel;
+import com.gitblit.models.RegistrantAccessPermission;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.utils.StringUtils;
+
+/**
+ * RSS Feeds Panel displays recent entries and launches the browser to view the
+ * commit. commitdiff, or tree of a commit.
+ *
+ * @author James Moger
+ *
+ */
+public abstract class RepositoriesPanel extends JPanel {
+
+ private static final long serialVersionUID = 1L;
+
+ private final GitblitClient gitblit;
+
+ private HeaderPanel header;
+
+ private JTable table;
+
+ private RepositoriesTableModel tableModel;
+
+ private TableRowSorter<RepositoriesTableModel> defaultSorter;
+
+ private JButton createRepository;
+
+ private JButton editRepository;
+
+ private JButton delRepository;
+
+ private JTextField filterTextfield;
+
+ private JButton clearCache;
+
+ public RepositoriesPanel(GitblitClient gitblit) {
+ super();
+ this.gitblit = gitblit;
+ initialize();
+ }
+
+ private void initialize() {
+ final JButton browseRepository = new JButton(Translation.get("gb.browse"));
+ browseRepository.setEnabled(false);
+ browseRepository.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ RepositoryModel model = getSelectedRepositories().get(0);
+ String url = gitblit.getURL("summary", model.name, null);
+ Utils.browse(url);
+ }
+ });
+
+ JButton refreshRepositories = new JButton(Translation.get("gb.refresh"));
+ refreshRepositories.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ refreshRepositories();
+ }
+ });
+
+ clearCache = new JButton(Translation.get("gb.clearCache"));
+ clearCache.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ clearCache();
+ }
+ });
+
+ createRepository = new JButton(Translation.get("gb.create"));
+ createRepository.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ createRepository();
+ }
+ });
+
+ editRepository = new JButton(Translation.get("gb.edit"));
+ editRepository.setEnabled(false);
+ editRepository.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ editRepository(getSelectedRepositories().get(0));
+ }
+ });
+
+ delRepository = new JButton(Translation.get("gb.delete"));
+ delRepository.setEnabled(false);
+ delRepository.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ deleteRepositories(getSelectedRepositories());
+ }
+ });
+
+ final JButton subscribeRepository = new JButton(Translation.get("gb.subscribe") + "...");
+ subscribeRepository.setEnabled(false);
+ subscribeRepository.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ List<FeedModel> feeds = gitblit.getAvailableFeeds(getSelectedRepositories().get(0));
+ subscribeFeeds(feeds);
+ }
+ });
+
+ final JButton logRepository = new JButton(Translation.get("gb.log") + "...");
+ logRepository.setEnabled(false);
+ logRepository.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ RepositoryModel model = getSelectedRepositories().get(0);
+ showSearchDialog(false, model);
+ }
+ });
+
+ final JButton searchRepository = new JButton(Translation.get("gb.search") + "...");
+ searchRepository.setEnabled(false);
+ searchRepository.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ RepositoryModel model = getSelectedRepositories().get(0);
+ showSearchDialog(true, model);
+ }
+ });
+
+ SubscribedRepositoryRenderer nameRenderer = new SubscribedRepositoryRenderer(gitblit);
+ IndicatorsRenderer typeRenderer = new IndicatorsRenderer();
+
+ DefaultTableCellRenderer sizeRenderer = new DefaultTableCellRenderer();
+ sizeRenderer.setHorizontalAlignment(SwingConstants.RIGHT);
+ sizeRenderer.setForeground(new Color(0, 0x80, 0));
+
+ DefaultTableCellRenderer ownerRenderer = new DefaultTableCellRenderer();
+ ownerRenderer.setForeground(Color.gray);
+ ownerRenderer.setHorizontalAlignment(SwingConstants.CENTER);
+
+ tableModel = new RepositoriesTableModel();
+ defaultSorter = new TableRowSorter<RepositoriesTableModel>(tableModel);
+ table = Utils.newTable(tableModel, Utils.DATE_FORMAT);
+ table.setRowSorter(defaultSorter);
+ table.getRowSorter().toggleSortOrder(RepositoriesTableModel.Columns.Name.ordinal());
+
+ setRepositoryRenderer(RepositoriesTableModel.Columns.Name, nameRenderer, -1);
+ setRepositoryRenderer(RepositoriesTableModel.Columns.Indicators, typeRenderer, 100);
+ setRepositoryRenderer(RepositoriesTableModel.Columns.Owner, ownerRenderer, -1);
+ setRepositoryRenderer(RepositoriesTableModel.Columns.Size, sizeRenderer, 60);
+
+ table.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
+ @Override
+ public void valueChanged(ListSelectionEvent e) {
+ if (e.getValueIsAdjusting()) {
+ return;
+ }
+ boolean singleSelection = table.getSelectedRowCount() == 1;
+ boolean selected = table.getSelectedRow() > -1;
+ if (singleSelection) {
+ RepositoryModel repository = getSelectedRepositories().get(0);
+ browseRepository.setEnabled(repository.hasCommits);
+ logRepository.setEnabled(repository.hasCommits);
+ searchRepository.setEnabled(repository.hasCommits);
+ subscribeRepository.setEnabled(repository.hasCommits);
+ } else {
+ browseRepository.setEnabled(false);
+ logRepository.setEnabled(false);
+ searchRepository.setEnabled(false);
+ subscribeRepository.setEnabled(false);
+ }
+ delRepository.setEnabled(selected);
+ if (selected) {
+ int viewRow = table.getSelectedRow();
+ int modelRow = table.convertRowIndexToModel(viewRow);
+ RepositoryModel model = ((RepositoriesTableModel) table.getModel()).list
+ .get(modelRow);
+ editRepository.setEnabled(singleSelection
+ && (gitblit.allowManagement() || gitblit.isOwner(model)));
+ } else {
+ editRepository.setEnabled(false);
+ }
+ }
+ });
+
+ table.addMouseListener(new MouseAdapter() {
+ public void mouseClicked(MouseEvent e) {
+ if (e.getClickCount() == 2 && gitblit.allowManagement()) {
+ editRepository(getSelectedRepositories().get(0));
+ }
+ }
+ });
+
+ filterTextfield = new JTextField();
+ filterTextfield.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ filterRepositories(filterTextfield.getText());
+ }
+ });
+ filterTextfield.addKeyListener(new KeyAdapter() {
+ public void keyReleased(KeyEvent e) {
+ filterRepositories(filterTextfield.getText());
+ }
+ });
+
+ JPanel repositoryFilterPanel = new JPanel(new BorderLayout(Utils.MARGIN, Utils.MARGIN));
+ repositoryFilterPanel.add(new JLabel(Translation.get("gb.filter")), BorderLayout.WEST);
+ repositoryFilterPanel.add(filterTextfield, BorderLayout.CENTER);
+
+ JPanel repositoryTablePanel = new JPanel(new BorderLayout(Utils.MARGIN, Utils.MARGIN));
+ repositoryTablePanel.add(repositoryFilterPanel, BorderLayout.NORTH);
+ repositoryTablePanel.add(new JScrollPane(table), BorderLayout.CENTER);
+
+ JPanel repositoryControls = new JPanel(new FlowLayout(FlowLayout.CENTER, Utils.MARGIN, 0));
+ repositoryControls.add(clearCache);
+ repositoryControls.add(refreshRepositories);
+ repositoryControls.add(browseRepository);
+ repositoryControls.add(createRepository);
+ repositoryControls.add(editRepository);
+ repositoryControls.add(delRepository);
+ repositoryControls.add(subscribeRepository);
+ repositoryControls.add(logRepository);
+ repositoryControls.add(searchRepository);
+
+ setLayout(new BorderLayout(Utils.MARGIN, Utils.MARGIN));
+ header = new HeaderPanel(Translation.get("gb.repositories"), "git-orange-16x16.png");
+ add(header, BorderLayout.NORTH);
+ add(repositoryTablePanel, BorderLayout.CENTER);
+ add(repositoryControls, BorderLayout.SOUTH);
+ }
+
+ @Override
+ public void requestFocus() {
+ filterTextfield.requestFocus();
+ }
+
+ @Override
+ public Insets getInsets() {
+ return Utils.INSETS;
+ }
+
+ private void setRepositoryRenderer(RepositoriesTableModel.Columns col,
+ TableCellRenderer renderer, int maxWidth) {
+ String name = table.getColumnName(col.ordinal());
+ table.getColumn(name).setCellRenderer(renderer);
+ if (maxWidth > 0) {
+ table.getColumn(name).setMinWidth(maxWidth);
+ table.getColumn(name).setMaxWidth(maxWidth);
+ }
+ }
+
+ protected abstract void subscribeFeeds(List<FeedModel> feeds);
+
+ protected abstract void updateUsersTable();
+
+ protected abstract void updateTeamsTable();
+
+ protected void disableManagement() {
+ clearCache.setVisible(false);
+ createRepository.setVisible(false);
+ editRepository.setVisible(false);
+ delRepository.setVisible(false);
+ }
+
+ protected void updateTable(boolean pack) {
+ tableModel.list.clear();
+ tableModel.list.addAll(gitblit.getRepositories());
+ tableModel.fireTableDataChanged();
+ header.setText(Translation.get("gb.repositories") + " (" + gitblit.getRepositories().size()
+ + ")");
+ if (pack) {
+ Utils.packColumns(table, Utils.MARGIN);
+ }
+ }
+
+ private void filterRepositories(final String fragment) {
+ if (StringUtils.isEmpty(fragment)) {
+ table.setRowSorter(defaultSorter);
+ return;
+ }
+ RowFilter<RepositoriesTableModel, Object> containsFilter = new RowFilter<RepositoriesTableModel, Object>() {
+ public boolean include(Entry<? extends RepositoriesTableModel, ? extends Object> entry) {
+ for (int i = entry.getValueCount() - 1; i >= 0; i--) {
+ if (entry.getStringValue(i).toLowerCase().contains(fragment.toLowerCase())) {
+ return true;
+ }
+ }
+ return false;
+ }
+ };
+ TableRowSorter<RepositoriesTableModel> sorter = new TableRowSorter<RepositoriesTableModel>(
+ tableModel);
+ sorter.setRowFilter(containsFilter);
+ table.setRowSorter(sorter);
+ }
+
+ private List<RepositoryModel> getSelectedRepositories() {
+ List<RepositoryModel> repositories = new ArrayList<RepositoryModel>();
+ for (int viewRow : table.getSelectedRows()) {
+ int modelRow = table.convertRowIndexToModel(viewRow);
+ RepositoryModel model = tableModel.list.get(modelRow);
+ repositories.add(model);
+ }
+ return repositories;
+ }
+
+ protected void refreshRepositories() {
+ GitblitWorker worker = new GitblitWorker(RepositoriesPanel.this,
+ RpcRequest.LIST_REPOSITORIES) {
+ @Override
+ protected Boolean doRequest() throws IOException {
+ gitblit.refreshRepositories();
+ return true;
+ }
+
+ @Override
+ protected void onSuccess() {
+ updateTable(false);
+ }
+ };
+ worker.execute();
+ }
+
+ protected void clearCache() {
+ GitblitWorker worker = new GitblitWorker(RepositoriesPanel.this,
+ RpcRequest.CLEAR_REPOSITORY_CACHE) {
+ @Override
+ protected Boolean doRequest() throws IOException {
+ if (gitblit.clearRepositoryCache()) {
+ gitblit.refreshRepositories();
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ protected void onSuccess() {
+ updateTable(false);
+ }
+ };
+ worker.execute();
+ }
+
+ /**
+ * Displays the create repository dialog and fires a SwingWorker to update
+ * the server, if appropriate.
+ *
+ */
+ protected void createRepository() {
+ EditRepositoryDialog dialog = new EditRepositoryDialog(gitblit.getProtocolVersion());
+ dialog.setLocationRelativeTo(RepositoriesPanel.this);
+ dialog.setAccessRestriction(gitblit.getDefaultAccessRestriction());
+ dialog.setAuthorizationControl(gitblit.getDefaultAuthorizationControl());
+ dialog.setUsers(null, gitblit.getUsernames(), null);
+ dialog.setTeams(gitblit.getTeamnames(), null);
+ dialog.setRepositories(gitblit.getRepositories());
+ dialog.setFederationSets(gitblit.getFederationSets(), null);
+ dialog.setIndexedBranches(new ArrayList<String>(Arrays.asList(Constants.DEFAULT_BRANCH)), null);
+ dialog.setPreReceiveScripts(gitblit.getPreReceiveScriptsUnused(null),
+ gitblit.getPreReceiveScriptsInherited(null), null);
+ dialog.setPostReceiveScripts(gitblit.getPostReceiveScriptsUnused(null),
+ gitblit.getPostReceiveScriptsInherited(null), null);
+ dialog.setVisible(true);
+ final RepositoryModel newRepository = dialog.getRepository();
+ final List<RegistrantAccessPermission> permittedUsers = dialog.getUserAccessPermissions();
+ final List<RegistrantAccessPermission> permittedTeams = dialog.getTeamAccessPermissions();
+ if (newRepository == null) {
+ return;
+ }
+
+ GitblitWorker worker = new GitblitWorker(this, RpcRequest.CREATE_REPOSITORY) {
+
+ @Override
+ protected Boolean doRequest() throws IOException {
+ boolean success = gitblit.createRepository(newRepository, permittedUsers,
+ permittedTeams);
+ if (success) {
+ gitblit.refreshRepositories();
+ if (permittedUsers.size() > 0) {
+ gitblit.refreshUsers();
+ }
+ if (permittedTeams.size() > 0) {
+ gitblit.refreshTeams();
+ }
+ }
+ return success;
+ }
+
+ @Override
+ protected void onSuccess() {
+ updateTable(false);
+ updateUsersTable();
+ updateTeamsTable();
+ }
+
+ @Override
+ protected void onFailure() {
+ showFailure("Failed to execute request \"{0}\" for repository \"{1}\".",
+ getRequestType(), newRepository.name);
+ }
+ };
+ worker.execute();
+ }
+
+ /**
+ * Displays the edit repository dialog and fires a SwingWorker to update the
+ * server, if appropriate.
+ *
+ * @param repository
+ */
+ protected void editRepository(final RepositoryModel repository) {
+ EditRepositoryDialog dialog = new EditRepositoryDialog(gitblit.getProtocolVersion(),
+ repository);
+ dialog.setLocationRelativeTo(RepositoriesPanel.this);
+ List<String> usernames = gitblit.getUsernames();
+ List<RegistrantAccessPermission> members = gitblit.getUserAccessPermissions(repository);
+ dialog.setUsers(new ArrayList<String>(repository.owners), usernames, members);
+ dialog.setTeams(gitblit.getTeamnames(), gitblit.getTeamAccessPermissions(repository));
+ dialog.setRepositories(gitblit.getRepositories());
+ dialog.setFederationSets(gitblit.getFederationSets(), repository.federationSets);
+ List<String> allLocalBranches = new ArrayList<String>();
+ allLocalBranches.add(Constants.DEFAULT_BRANCH);
+ allLocalBranches.addAll(repository.getLocalBranches());
+ dialog.setIndexedBranches(allLocalBranches, repository.indexedBranches);
+ dialog.setPreReceiveScripts(gitblit.getPreReceiveScriptsUnused(repository),
+ gitblit.getPreReceiveScriptsInherited(repository), repository.preReceiveScripts);
+ dialog.setPostReceiveScripts(gitblit.getPostReceiveScriptsUnused(repository),
+ gitblit.getPostReceiveScriptsInherited(repository), repository.postReceiveScripts);
+ if (gitblit.getSettings().hasKey(Keys.groovy.customFields)) {
+ Map<String, String> map = gitblit.getSettings().get(Keys.groovy.customFields).getMap();
+ dialog.setCustomFields(repository, map);
+ }
+ dialog.setVisible(true);
+ final RepositoryModel revisedRepository = dialog.getRepository();
+ final List<RegistrantAccessPermission> permittedUsers = dialog.getUserAccessPermissions();
+ final List<RegistrantAccessPermission> permittedTeams = dialog.getTeamAccessPermissions();
+ if (revisedRepository == null) {
+ return;
+ }
+
+ GitblitWorker worker = new GitblitWorker(this, RpcRequest.EDIT_REPOSITORY) {
+
+ @Override
+ protected Boolean doRequest() throws IOException {
+ boolean success = gitblit.updateRepository(repository.name, revisedRepository,
+ permittedUsers, permittedTeams);
+ if (success) {
+ gitblit.refreshRepositories();
+ gitblit.refreshUsers();
+ gitblit.refreshTeams();
+ }
+ return success;
+ }
+
+ @Override
+ protected void onSuccess() {
+ updateTable(false);
+ updateUsersTable();
+ updateTeamsTable();
+ }
+
+ @Override
+ protected void onFailure() {
+ showFailure("Failed to execute request \"{0}\" for repository \"{1}\".",
+ getRequestType(), repository.name);
+ }
+ };
+ worker.execute();
+ }
+
+ protected void deleteRepositories(final List<RepositoryModel> repositories) {
+ if (repositories == null || repositories.size() == 0) {
+ return;
+ }
+ StringBuilder message = new StringBuilder("Delete the following repositories?\n\n");
+ for (RepositoryModel repository : repositories) {
+ message.append(repository.name).append("\n");
+ }
+ int result = JOptionPane.showConfirmDialog(RepositoriesPanel.this, message.toString(),
+ "Delete Repositories?", JOptionPane.YES_NO_OPTION);
+ if (result == JOptionPane.YES_OPTION) {
+ GitblitWorker worker = new GitblitWorker(this, RpcRequest.DELETE_REPOSITORY) {
+ @Override
+ protected Boolean doRequest() throws IOException {
+ boolean success = true;
+ for (RepositoryModel repository : repositories) {
+ success &= gitblit.deleteRepository(repository);
+ }
+ if (success) {
+ gitblit.refreshRepositories();
+ gitblit.refreshUsers();
+ gitblit.refreshTeams();
+ }
+ return success;
+ }
+
+ @Override
+ protected void onSuccess() {
+ updateTable(false);
+ updateUsersTable();
+ updateTeamsTable();
+ }
+
+ @Override
+ protected void onFailure() {
+ showFailure("Failed to delete specified repositories!");
+ }
+ };
+ worker.execute();
+ }
+ }
+
+ private void showSearchDialog(boolean isSearch, final RepositoryModel repository) {
+ final SearchDialog dialog = new SearchDialog(gitblit, isSearch);
+ if (repository != null) {
+ dialog.selectRepository(repository);
+ }
+ dialog.setLocationRelativeTo(this);
+ dialog.setVisible(true);
+ }
+}
diff --git a/src/main/java/com/gitblit/client/RepositoriesTableModel.java b/src/main/java/com/gitblit/client/RepositoriesTableModel.java
new file mode 100644
index 00000000..6b295a4b
--- /dev/null
+++ b/src/main/java/com/gitblit/client/RepositoriesTableModel.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.client;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+
+import javax.swing.table.AbstractTableModel;
+
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.utils.ArrayUtils;
+
+/**
+ * Table model of a list of repositories.
+ *
+ * @author James Moger
+ *
+ */
+public class RepositoriesTableModel extends AbstractTableModel {
+
+ private static final long serialVersionUID = 1L;
+
+ List<RepositoryModel> list;
+
+ enum Columns {
+ Name, Description, Owner, Indicators, Last_Change, Size;
+
+ @Override
+ public String toString() {
+ return name().replace('_', ' ');
+ }
+ }
+
+ public RepositoriesTableModel() {
+ this(new ArrayList<RepositoryModel>());
+ }
+
+ public RepositoriesTableModel(List<RepositoryModel> repositories) {
+ this.list = repositories;
+ Collections.sort(this.list);
+ }
+
+ @Override
+ public int getRowCount() {
+ return list.size();
+ }
+
+ @Override
+ public int getColumnCount() {
+ return Columns.values().length;
+ }
+
+ @Override
+ public String getColumnName(int column) {
+ Columns col = Columns.values()[column];
+ switch (col) {
+ case Name:
+ return Translation.get("gb.name");
+ case Description:
+ return Translation.get("gb.description");
+ case Owner:
+ return Translation.get("gb.owner");
+ case Last_Change:
+ return Translation.get("gb.lastChange");
+ case Size:
+ return Translation.get("gb.size");
+ }
+ return "";
+ }
+
+ /**
+ * Returns <code>Object.class</code> regardless of <code>columnIndex</code>.
+ *
+ * @param columnIndex
+ * the column being queried
+ * @return the Object.class
+ */
+ public Class<?> getColumnClass(int columnIndex) {
+ Columns col = Columns.values()[columnIndex];
+ switch (col) {
+ case Name:
+ case Indicators:
+ return RepositoryModel.class;
+ case Last_Change:
+ return Date.class;
+ }
+ return String.class;
+ }
+
+ @Override
+ public Object getValueAt(int rowIndex, int columnIndex) {
+ RepositoryModel model = list.get(rowIndex);
+ Columns col = Columns.values()[columnIndex];
+ switch (col) {
+ case Name:
+ return model;
+ case Description:
+ return model.description;
+ case Owner:
+ return ArrayUtils.toString(model.owners);
+ case Indicators:
+ return model;
+ case Last_Change:
+ return model.lastChange;
+ case Size:
+ if (model.hasCommits) {
+ return model.size;
+ }
+ return "(empty)";
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/com/gitblit/client/SearchDialog.java b/src/main/java/com/gitblit/client/SearchDialog.java
new file mode 100644
index 00000000..829bc52a
--- /dev/null
+++ b/src/main/java/com/gitblit/client/SearchDialog.java
@@ -0,0 +1,404 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.client;
+
+import java.awt.BorderLayout;
+import java.awt.Cursor;
+import java.awt.FlowLayout;
+import java.awt.Insets;
+import java.awt.Rectangle;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.io.IOException;
+import java.util.List;
+
+import javax.swing.DefaultComboBoxModel;
+import javax.swing.ImageIcon;
+import javax.swing.JButton;
+import javax.swing.JComboBox;
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTable;
+import javax.swing.JTextField;
+import javax.swing.SwingWorker;
+import javax.swing.event.ListSelectionEvent;
+import javax.swing.event.ListSelectionListener;
+
+import com.gitblit.Constants;
+import com.gitblit.models.FeedEntryModel;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.utils.StringUtils;
+
+/**
+ * The search dialog allows searching of a repository branch. This matches the
+ * search implementation of the site.
+ *
+ * @author James Moger
+ *
+ */
+public class SearchDialog extends JFrame {
+
+ private static final long serialVersionUID = 1L;
+
+ private final boolean isSearch;
+
+ private final GitblitClient gitblit;
+
+ private FeedEntryTableModel tableModel;
+
+ private HeaderPanel header;
+
+ private JTable table;
+
+ private JComboBox repositorySelector;
+
+ private DefaultComboBoxModel branchChoices;
+
+ private JComboBox branchSelector;
+
+ private JComboBox searchTypeSelector;
+
+ private JTextField searchFragment;
+
+ private JComboBox maxHitsSelector;
+
+ private int page;
+
+ private JButton prev;
+
+ private JButton next;
+
+ public SearchDialog(GitblitClient gitblit, boolean isSearch) {
+ super();
+ this.gitblit = gitblit;
+ this.isSearch = isSearch;
+ setTitle(Translation.get(isSearch ? "gb.search" : "gb.log"));
+ setIconImage(new ImageIcon(getClass().getResource("/gitblt-favicon.png")).getImage());
+ initialize();
+ setSize(900, 550);
+ }
+
+ private void initialize() {
+
+ prev = new JButton("<");
+ prev.setToolTipText(Translation.get("gb.pagePrevious"));
+ prev.setEnabled(false);
+ prev.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ search(--page);
+ }
+ });
+
+ next = new JButton(">");
+ next.setToolTipText(Translation.get("gb.pageNext"));
+ next.setEnabled(false);
+ next.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ search(++page);
+ }
+ });
+
+ final JButton search = new JButton(Translation.get(isSearch ? "gb.search" : "gb.refresh"));
+ search.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ search(0);
+ }
+ });
+
+ final JButton viewCommit = new JButton(Translation.get("gb.view"));
+ viewCommit.setEnabled(false);
+ viewCommit.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ viewCommit();
+ }
+ });
+
+ final JButton viewCommitDiff = new JButton(Translation.get("gb.commitdiff"));
+ viewCommitDiff.setEnabled(false);
+ viewCommitDiff.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ viewCommitDiff();
+ }
+ });
+
+ final JButton viewTree = new JButton(Translation.get("gb.tree"));
+ viewTree.setEnabled(false);
+ viewTree.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ viewTree();
+ }
+ });
+
+ JPanel controls = new JPanel(new FlowLayout(FlowLayout.CENTER, Utils.MARGIN, 0));
+ controls.add(viewCommit);
+ controls.add(viewCommitDiff);
+ controls.add(viewTree);
+
+ NameRenderer nameRenderer = new NameRenderer();
+ tableModel = new FeedEntryTableModel();
+ header = new HeaderPanel(Translation.get(isSearch ? "gb.search" : "gb.log"),
+ isSearch ? "search-icon.png" : "commit_changes_16x16.png");
+ table = Utils.newTable(tableModel, Utils.DATE_FORMAT);
+
+ String name = table.getColumnName(FeedEntryTableModel.Columns.Author.ordinal());
+ table.getColumn(name).setCellRenderer(nameRenderer);
+ name = table.getColumnName(FeedEntryTableModel.Columns.Repository.ordinal());
+ table.getColumn(name).setCellRenderer(nameRenderer);
+
+ name = table.getColumnName(FeedEntryTableModel.Columns.Branch.ordinal());
+ table.getColumn(name).setCellRenderer(new BranchRenderer());
+
+ name = table.getColumnName(FeedEntryTableModel.Columns.Message.ordinal());
+ table.getColumn(name).setCellRenderer(new MessageRenderer());
+
+ table.addMouseListener(new MouseAdapter() {
+ public void mouseClicked(MouseEvent e) {
+ if (e.getClickCount() == 2) {
+ if (e.isControlDown()) {
+ viewCommitDiff();
+ } else {
+ viewCommit();
+ }
+ }
+ }
+ });
+
+ table.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
+ @Override
+ public void valueChanged(ListSelectionEvent e) {
+ if (e.getValueIsAdjusting()) {
+ return;
+ }
+ boolean singleSelection = table.getSelectedRowCount() == 1;
+ viewCommit.setEnabled(singleSelection);
+ viewCommitDiff.setEnabled(singleSelection);
+ viewTree.setEnabled(singleSelection);
+ }
+ });
+
+ repositorySelector = new JComboBox(gitblit.getRepositories().toArray());
+ repositorySelector.setRenderer(nameRenderer);
+ repositorySelector.setForeground(nameRenderer.getForeground());
+ repositorySelector.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent event) {
+ // repopulate the branch list based on repository selection
+ // preserve branch selection, if possible
+ String selectedBranch = null;
+ if (branchSelector.getSelectedIndex() > -1) {
+ selectedBranch = branchSelector.getSelectedItem().toString();
+ }
+ updateBranches();
+ if (StringUtils.isEmpty(selectedBranch)) {
+ // do not select branch
+ branchSelector.setSelectedIndex(-1);
+ } else {
+ if (branchChoices.getIndexOf(selectedBranch) > -1) {
+ // select branch
+ branchChoices.setSelectedItem(selectedBranch);
+ } else {
+ // branch does not exist, do not select branch
+ branchSelector.setSelectedIndex(-1);
+ }
+ }
+ }
+ });
+
+ branchChoices = new DefaultComboBoxModel();
+ branchSelector = new JComboBox(branchChoices);
+ branchSelector.setRenderer(new BranchRenderer());
+
+ searchTypeSelector = new JComboBox(Constants.SearchType.values());
+ searchTypeSelector.setSelectedItem(Constants.SearchType.COMMIT);
+
+ maxHitsSelector = new JComboBox(new Integer[] { 25, 50, 75, 100 });
+ maxHitsSelector.setSelectedIndex(0);
+
+ searchFragment = new JTextField(25);
+ searchFragment.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent event) {
+ search(0);
+ }
+ });
+
+ JPanel queryPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, Utils.MARGIN, 0));
+ queryPanel.add(new JLabel(Translation.get("gb.repository")));
+ queryPanel.add(repositorySelector);
+ queryPanel.add(new JLabel(Translation.get("gb.branch")));
+ queryPanel.add(branchSelector);
+ if (isSearch) {
+ queryPanel.add(new JLabel(Translation.get("gb.type")));
+ queryPanel.add(searchTypeSelector);
+ }
+ queryPanel.add(new JLabel(Translation.get("gb.maxHits")));
+ queryPanel.add(maxHitsSelector);
+
+ JPanel actionsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, Utils.MARGIN, 0));
+ actionsPanel.add(search);
+ actionsPanel.add(prev);
+ actionsPanel.add(next);
+
+ JPanel northControls = new JPanel(new BorderLayout(Utils.MARGIN, Utils.MARGIN));
+ northControls.add(queryPanel, BorderLayout.WEST);
+ if (isSearch) {
+ northControls.add(searchFragment, BorderLayout.CENTER);
+ }
+ northControls.add(actionsPanel, BorderLayout.EAST);
+
+ JPanel northPanel = new JPanel(new BorderLayout(0, Utils.MARGIN));
+ northPanel.add(header, BorderLayout.NORTH);
+ northPanel.add(northControls, BorderLayout.CENTER);
+
+ JPanel contentPanel = new JPanel() {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public Insets getInsets() {
+ return Utils.INSETS;
+ }
+ };
+ contentPanel.setLayout(new BorderLayout(Utils.MARGIN, Utils.MARGIN));
+ contentPanel.add(northPanel, BorderLayout.NORTH);
+ contentPanel.add(new JScrollPane(table), BorderLayout.CENTER);
+ contentPanel.add(controls, BorderLayout.SOUTH);
+ setLayout(new BorderLayout());
+ add(contentPanel, BorderLayout.CENTER);
+ addWindowListener(new WindowAdapter() {
+ @Override
+ public void windowOpened(WindowEvent event) {
+ if (isSearch) {
+ searchFragment.requestFocus();
+ } else {
+ search(0);
+ }
+ }
+
+ @Override
+ public void windowActivated(WindowEvent event) {
+ if (isSearch) {
+ searchFragment.requestFocus();
+ }
+ }
+ });
+ }
+
+ public void selectRepository(RepositoryModel repository) {
+ repositorySelector.setSelectedItem(repository);
+ }
+
+ private void updateBranches() {
+ String repository = null;
+ if (repositorySelector.getSelectedIndex() > -1) {
+ repository = repositorySelector.getSelectedItem().toString();
+ }
+ List<String> branches = gitblit.getBranches(repository);
+ branchChoices.removeAllElements();
+ for (String branch : branches) {
+ branchChoices.addElement(branch);
+ }
+ }
+
+ protected void search(final int page) {
+ this.page = page;
+ final String repository = repositorySelector.getSelectedItem().toString();
+ final String branch = branchSelector.getSelectedIndex() > -1 ? branchSelector
+ .getSelectedItem().toString() : null;
+ final Constants.SearchType searchType = (Constants.SearchType) searchTypeSelector
+ .getSelectedItem();
+ final String fragment = isSearch ? searchFragment.getText() : null;
+ final int maxEntryCount = maxHitsSelector.getSelectedIndex() > -1 ? ((Integer) maxHitsSelector
+ .getSelectedItem()) : -1;
+
+ if (isSearch && StringUtils.isEmpty(fragment)) {
+ return;
+ }
+ setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
+ SwingWorker<List<FeedEntryModel>, Void> worker = new SwingWorker<List<FeedEntryModel>, Void>() {
+ @Override
+ protected List<FeedEntryModel> doInBackground() throws IOException {
+ if (isSearch) {
+ return gitblit.search(repository, branch, fragment, searchType, maxEntryCount,
+ page);
+ } else {
+ return gitblit.log(repository, branch, maxEntryCount, page);
+ }
+ }
+
+ @Override
+ protected void done() {
+ setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
+ try {
+ List<FeedEntryModel> results = get();
+ if (isSearch) {
+ updateTable(true, fragment, results);
+ } else {
+ updateTable(true, branch == null ? "" : branch, results);
+ }
+ } catch (Throwable t) {
+ Utils.showException(SearchDialog.this, t);
+ }
+ }
+ };
+ worker.execute();
+ }
+
+ protected void updateTable(boolean pack, String text, List<FeedEntryModel> entries) {
+ tableModel.entries.clear();
+ tableModel.entries.addAll(entries);
+ tableModel.fireTableDataChanged();
+ setTitle(Translation.get(isSearch ? "gb.search" : "gb.log")
+ + (StringUtils.isEmpty(text) ? "" : (": " + text)) + " (" + entries.size()
+ + (page > 0 ? (", pg " + (page + 1)) : "") + ")");
+ header.setText(getTitle());
+ if (pack) {
+ Utils.packColumns(table, Utils.MARGIN);
+ }
+ table.scrollRectToVisible(new Rectangle(table.getCellRect(0, 0, true)));
+
+ // update pagination buttons
+ int maxHits = (Integer) maxHitsSelector.getSelectedItem();
+ next.setEnabled(entries.size() == maxHits);
+ prev.setEnabled(page > 0);
+ }
+
+ protected FeedEntryModel getSelectedSyndicatedEntry() {
+ int viewRow = table.getSelectedRow();
+ int modelRow = table.convertRowIndexToModel(viewRow);
+ FeedEntryModel entry = tableModel.get(modelRow);
+ return entry;
+ }
+
+ protected void viewCommit() {
+ FeedEntryModel entry = getSelectedSyndicatedEntry();
+ Utils.browse(entry.link);
+ }
+
+ protected void viewCommitDiff() {
+ FeedEntryModel entry = getSelectedSyndicatedEntry();
+ Utils.browse(entry.link.replace("/commit/", "/commitdiff/"));
+ }
+
+ protected void viewTree() {
+ FeedEntryModel entry = getSelectedSyndicatedEntry();
+ Utils.browse(entry.link.replace("/commit/", "/tree/"));
+ }
+}
diff --git a/src/main/java/com/gitblit/client/SettingCellRenderer.java b/src/main/java/com/gitblit/client/SettingCellRenderer.java
new file mode 100644
index 00000000..d164fb1a
--- /dev/null
+++ b/src/main/java/com/gitblit/client/SettingCellRenderer.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.client;
+
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.Font;
+
+import javax.swing.JTable;
+import javax.swing.table.DefaultTableCellRenderer;
+
+import com.gitblit.models.SettingModel;
+
+/**
+ * SettingModel cell renderer that indicates if a setting is the default or
+ * modified.
+ *
+ * @author James Moger
+ *
+ */
+public class SettingCellRenderer extends DefaultTableCellRenderer {
+
+ private static final long serialVersionUID = 1L;
+
+ private final Font defaultFont;
+
+ private final Font modifiedFont;
+
+ public SettingCellRenderer() {
+ defaultFont = getFont();
+ modifiedFont = defaultFont.deriveFont(Font.BOLD);
+ }
+
+ public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,
+ boolean hasFocus, int row, int column) {
+ super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
+ if (value instanceof SettingModel) {
+ SettingModel setting = (SettingModel) value;
+ if (setting.isDefaultValue()) {
+ this.setFont(defaultFont);
+ if (!isSelected) {
+ this.setForeground(Color.BLACK);
+ }
+ } else {
+ this.setFont(modifiedFont);
+ if (!isSelected) {
+ this.setForeground(Color.BLUE);
+ }
+ }
+ this.setText(setting.getString(""));
+ }
+ return this;
+ }
+} \ No newline at end of file
diff --git a/src/main/java/com/gitblit/client/SettingPanel.java b/src/main/java/com/gitblit/client/SettingPanel.java
new file mode 100644
index 00000000..6da09e18
--- /dev/null
+++ b/src/main/java/com/gitblit/client/SettingPanel.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.client;
+
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.Font;
+import java.awt.GridLayout;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTextArea;
+import javax.swing.SwingConstants;
+
+import com.gitblit.models.SettingModel;
+import com.gitblit.utils.StringUtils;
+
+/**
+ * This panel displays the metadata for a particular setting.
+ *
+ * @author James Moger
+ */
+public class SettingPanel extends JPanel {
+
+ private static final long serialVersionUID = 1L;
+ private JTextArea descriptionArea;
+ private JLabel settingName;
+ private JLabel settingDefault;
+ private JLabel sinceVersion;
+ private JLabel directives;
+
+ public SettingPanel() {
+ super();
+ initialize();
+ }
+
+ public SettingPanel(SettingModel setting) {
+ this();
+ setSetting(setting);
+ }
+
+ private void initialize() {
+ descriptionArea = new JTextArea();
+ descriptionArea.setRows(6);
+ descriptionArea.setFont(new Font("monospaced", Font.PLAIN, 11));
+ descriptionArea.setEditable(false);
+
+ settingName = new JLabel(" ");
+ settingName.setFont(settingName.getFont().deriveFont(Font.BOLD));
+
+ settingDefault = new JLabel(" ");
+
+ sinceVersion = new JLabel(" ", SwingConstants.RIGHT);
+ sinceVersion.setForeground(new Color(0, 0x80, 0));
+
+ directives = new JLabel(" ", SwingConstants.RIGHT);
+ directives.setFont(directives.getFont().deriveFont(Font.ITALIC));
+
+ JPanel settingParameters = new JPanel(new GridLayout(2, 2, 0, 0));
+ settingParameters.add(settingName);
+ settingParameters.add(sinceVersion);
+ settingParameters.add(settingDefault, BorderLayout.CENTER);
+ settingParameters.add(directives);
+
+ JPanel settingPanel = new JPanel(new BorderLayout(5, 5));
+ settingPanel.add(settingParameters, BorderLayout.NORTH);
+ settingPanel.add(new JScrollPane(descriptionArea), BorderLayout.CENTER);
+ setLayout(new BorderLayout(0, 0));
+ add(settingPanel, BorderLayout.CENTER);
+ }
+
+ public void setSetting(SettingModel setting) {
+ settingName.setText(setting.name);
+ if (setting.since == null) {
+ sinceVersion.setText("custom");
+ } else {
+ sinceVersion.setText("since " + setting.since);
+ }
+ settingDefault.setText(Translation.get("gb.default") + ": " + setting.defaultValue);
+
+ List<String> values = new ArrayList<String>();
+ if (setting.caseSensitive) {
+ values.add("CASE-SENSITIVE");
+ }
+ if (setting.spaceDelimited) {
+ values.add("SPACE-DELIMITED");
+ }
+ if (setting.restartRequired) {
+ values.add("RESTART REQUIRED");
+ }
+ directives.setText(StringUtils.flattenStrings(values, ", "));
+
+ descriptionArea.setText(setting.description);
+ descriptionArea.setCaretPosition(0);
+ }
+
+ public void clear() {
+ settingName.setText(" ");
+ settingDefault.setText(" ");
+ sinceVersion.setText(" ");
+ directives.setText(" ");
+ descriptionArea.setText("");
+ }
+}
diff --git a/src/main/java/com/gitblit/client/SettingsPanel.java b/src/main/java/com/gitblit/client/SettingsPanel.java
new file mode 100644
index 00000000..b0adc0c7
--- /dev/null
+++ b/src/main/java/com/gitblit/client/SettingsPanel.java
@@ -0,0 +1,274 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.client;
+
+import java.awt.BorderLayout;
+import java.awt.Dimension;
+import java.awt.FlowLayout;
+import java.awt.GridLayout;
+import java.awt.Insets;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.KeyAdapter;
+import java.awt.event.KeyEvent;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.swing.ImageIcon;
+import javax.swing.JButton;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTable;
+import javax.swing.JTextField;
+import javax.swing.RowFilter;
+import javax.swing.event.ListSelectionEvent;
+import javax.swing.event.ListSelectionListener;
+import javax.swing.table.TableRowSorter;
+
+import com.gitblit.Constants.RpcRequest;
+import com.gitblit.models.SettingModel;
+import com.gitblit.utils.StringUtils;
+
+/**
+ * Settings panel displays a list of server settings and their associated
+ * metadata. This panel also allows editing of a setting.
+ *
+ * @author James Moger
+ *
+ */
+public class SettingsPanel extends JPanel {
+
+ private static final long serialVersionUID = 1L;
+
+ private final GitblitClient gitblit;
+
+ private HeaderPanel header;
+
+ private JTable table;
+
+ private SettingsTableModel tableModel;
+
+ private TableRowSorter<SettingsTableModel> defaultSorter;
+
+ private JTextField filterTextfield;
+
+ public SettingsPanel(GitblitClient gitblit) {
+ super();
+ this.gitblit = gitblit;
+ initialize();
+ }
+
+ private void initialize() {
+ JButton refreshSettings = new JButton(Translation.get("gb.refresh"));
+ refreshSettings.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ refreshSettings();
+ }
+ });
+
+ final JButton editSetting = new JButton(Translation.get("gb.edit"));
+ editSetting.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ int viewRow = table.getSelectedRow();
+ int modelRow = table.convertRowIndexToModel(viewRow);
+ String key = tableModel.keys.get(modelRow);
+ SettingModel setting = tableModel.settings.get(key);
+ editSetting(setting);
+ }
+ });
+
+ NameRenderer nameRenderer = new NameRenderer();
+ final SettingPanel settingPanel = new SettingPanel();
+ tableModel = new SettingsTableModel();
+ defaultSorter = new TableRowSorter<SettingsTableModel>(tableModel);
+ table = Utils.newTable(tableModel, Utils.DATE_FORMAT);
+ table.setDefaultRenderer(SettingModel.class, new SettingCellRenderer());
+ String name = table.getColumnName(UsersTableModel.Columns.Name.ordinal());
+ table.getColumn(name).setCellRenderer(nameRenderer);
+ table.setRowSorter(defaultSorter);
+ table.getRowSorter().toggleSortOrder(SettingsTableModel.Columns.Name.ordinal());
+ table.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
+
+ @Override
+ public void valueChanged(ListSelectionEvent e) {
+ if (e.getValueIsAdjusting()) {
+ return;
+ }
+ boolean singleSelection = table.getSelectedRows().length == 1;
+ editSetting.setEnabled(singleSelection);
+ if (singleSelection) {
+ int viewRow = table.getSelectedRow();
+ int modelRow = table.convertRowIndexToModel(viewRow);
+ SettingModel setting = tableModel.get(modelRow);
+ settingPanel.setSetting(setting);
+ } else {
+ settingPanel.clear();
+ }
+ }
+ });
+ table.addMouseListener(new MouseAdapter() {
+ public void mouseClicked(MouseEvent e) {
+ if (e.getClickCount() == 2) {
+ int viewRow = table.getSelectedRow();
+ int modelRow = table.convertRowIndexToModel(viewRow);
+ SettingModel setting = tableModel.get(modelRow);
+ editSetting(setting);
+ }
+ }
+ });
+
+ filterTextfield = new JTextField();
+ filterTextfield.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ filterSettings(filterTextfield.getText());
+ }
+ });
+ filterTextfield.addKeyListener(new KeyAdapter() {
+ public void keyReleased(KeyEvent e) {
+ filterSettings(filterTextfield.getText());
+ }
+ });
+
+ JPanel settingFilterPanel = new JPanel(new BorderLayout(Utils.MARGIN, Utils.MARGIN));
+ settingFilterPanel.add(new JLabel(Translation.get("gb.filter")), BorderLayout.WEST);
+ settingFilterPanel.add(filterTextfield, BorderLayout.CENTER);
+
+ JPanel settingsTablePanel = new JPanel(new BorderLayout(Utils.MARGIN, Utils.MARGIN));
+ settingsTablePanel.add(settingFilterPanel, BorderLayout.NORTH);
+ settingsTablePanel.add(new JScrollPane(table), BorderLayout.CENTER);
+ settingsTablePanel.add(settingPanel, BorderLayout.SOUTH);
+
+ JPanel settingsControls = new JPanel(new FlowLayout(FlowLayout.CENTER, 5, 0));
+ settingsControls.add(refreshSettings);
+ settingsControls.add(editSetting);
+
+ setLayout(new BorderLayout(Utils.MARGIN, Utils.MARGIN));
+ header = new HeaderPanel(Translation.get("gb.settings"), "settings_16x16.png");
+ add(header, BorderLayout.NORTH);
+ add(settingsTablePanel, BorderLayout.CENTER);
+ add(settingsControls, BorderLayout.SOUTH);
+ }
+
+ @Override
+ public void requestFocus() {
+ filterTextfield.requestFocus();
+ }
+
+ @Override
+ public Insets getInsets() {
+ return Utils.INSETS;
+ }
+
+ protected void updateTable(boolean pack) {
+ tableModel.setSettings(gitblit.getSettings());
+ tableModel.fireTableDataChanged();
+ header.setText(Translation.get("gb.settings"));
+ if (pack) {
+ Utils.packColumns(table, Utils.MARGIN);
+ }
+ }
+
+ private void filterSettings(final String fragment) {
+ if (StringUtils.isEmpty(fragment)) {
+ table.setRowSorter(defaultSorter);
+ return;
+ }
+ RowFilter<SettingsTableModel, Object> containsFilter = new RowFilter<SettingsTableModel, Object>() {
+ public boolean include(Entry<? extends SettingsTableModel, ? extends Object> entry) {
+ for (int i = entry.getValueCount() - 1; i >= 0; i--) {
+ if (entry.getStringValue(i).toLowerCase().contains(fragment.toLowerCase())) {
+ return true;
+ }
+ }
+ return false;
+ }
+ };
+ TableRowSorter<SettingsTableModel> sorter = new TableRowSorter<SettingsTableModel>(
+ tableModel);
+ sorter.setRowFilter(containsFilter);
+ table.setRowSorter(sorter);
+ }
+
+ protected void refreshSettings() {
+ GitblitWorker worker = new GitblitWorker(SettingsPanel.this, RpcRequest.LIST_SETTINGS) {
+ @Override
+ protected Boolean doRequest() throws IOException {
+ gitblit.refreshSettings();
+ return true;
+ }
+
+ @Override
+ protected void onSuccess() {
+ updateTable(false);
+ }
+ };
+ worker.execute();
+ }
+
+ protected void editSetting(final SettingModel settingModel) {
+ final JTextField textField = new JTextField(settingModel.currentValue);
+ JPanel editPanel = new JPanel(new GridLayout(0, 1));
+ editPanel.add(new JLabel("New Value"));
+ editPanel.add(textField);
+
+ JPanel settingPanel = new JPanel(new BorderLayout());
+ settingPanel.add(new SettingPanel(settingModel), BorderLayout.CENTER);
+ settingPanel.add(editPanel, BorderLayout.SOUTH);
+ settingPanel.setPreferredSize(new Dimension(800, 200));
+
+ String[] options;
+ if (settingModel.currentValue.equals(settingModel.defaultValue)) {
+ options = new String[] { Translation.get("gb.cancel"), Translation.get("gb.save") };
+ } else {
+ options = new String[] { Translation.get("gb.cancel"),
+ Translation.get("gb.setDefault"), Translation.get("gb.save") };
+ }
+ String defaultOption = options[0];
+ int selection = JOptionPane.showOptionDialog(SettingsPanel.this, settingPanel,
+ settingModel.name, JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE,
+ new ImageIcon(getClass().getResource("/settings_16x16.png")), options,
+ defaultOption);
+ if (selection <= 0) {
+ return;
+ }
+ if (options[selection].equals(Translation.get("gb.setDefault"))) {
+ textField.setText(settingModel.defaultValue);
+ }
+ final Map<String, String> newSettings = new HashMap<String, String>();
+ newSettings.put(settingModel.name, textField.getText().trim());
+ GitblitWorker worker = new GitblitWorker(SettingsPanel.this, RpcRequest.EDIT_SETTINGS) {
+ @Override
+ protected Boolean doRequest() throws IOException {
+ boolean success = gitblit.updateSettings(newSettings);
+ if (success) {
+ gitblit.refreshSettings();
+ }
+ return success;
+ }
+
+ @Override
+ protected void onSuccess() {
+ updateTable(false);
+ }
+ };
+ worker.execute();
+ }
+}
diff --git a/src/main/java/com/gitblit/client/SettingsTableModel.java b/src/main/java/com/gitblit/client/SettingsTableModel.java
new file mode 100644
index 00000000..f14eae4b
--- /dev/null
+++ b/src/main/java/com/gitblit/client/SettingsTableModel.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.client;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import javax.swing.table.AbstractTableModel;
+
+import com.gitblit.models.ServerSettings;
+import com.gitblit.models.SettingModel;
+
+/**
+ * Table model of Map<String, SettingModel>.
+ *
+ * @author James Moger
+ *
+ */
+public class SettingsTableModel extends AbstractTableModel {
+
+ private static final long serialVersionUID = 1L;
+
+ ServerSettings settings;
+
+ List<String> keys;
+
+ enum Columns {
+ Name, Value, Since;
+
+ @Override
+ public String toString() {
+ return name().replace('_', ' ');
+ }
+ }
+
+ public SettingsTableModel() {
+ this(null);
+ }
+
+ public SettingsTableModel(ServerSettings settings) {
+ setSettings(settings);
+ }
+
+ public void setSettings(ServerSettings settings) {
+ this.settings = settings;
+ if (settings == null) {
+ keys = new ArrayList<String>();
+ } else {
+ keys = new ArrayList<String>(settings.getKeys());
+ Collections.sort(keys);
+ }
+ }
+
+ @Override
+ public int getRowCount() {
+ return keys.size();
+ }
+
+ @Override
+ public int getColumnCount() {
+ return Columns.values().length;
+ }
+
+ @Override
+ public String getColumnName(int column) {
+ Columns col = Columns.values()[column];
+ switch (col) {
+ case Name:
+ return Translation.get("gb.name");
+ case Since:
+ return Translation.get("gb.since");
+ }
+ return "";
+ }
+
+ /**
+ * Returns <code>Object.class</code> regardless of <code>columnIndex</code>.
+ *
+ * @param columnIndex
+ * the column being queried
+ * @return the Object.class
+ */
+ public Class<?> getColumnClass(int columnIndex) {
+ if (Columns.Value.ordinal() == columnIndex) {
+ return SettingModel.class;
+ }
+ return String.class;
+ }
+
+ @Override
+ public Object getValueAt(int rowIndex, int columnIndex) {
+ String key = keys.get(rowIndex);
+ SettingModel setting = settings.get(key);
+ Columns col = Columns.values()[columnIndex];
+ switch (col) {
+ case Name:
+ return key;
+ case Value:
+ return setting;
+ case Since:
+ return setting.since;
+ }
+ return null;
+ }
+
+ public SettingModel get(int modelRow) {
+ String key = keys.get(modelRow);
+ return settings.get(key);
+ }
+}
diff --git a/src/main/java/com/gitblit/client/StatusPanel.java b/src/main/java/com/gitblit/client/StatusPanel.java
new file mode 100644
index 00000000..6d004f16
--- /dev/null
+++ b/src/main/java/com/gitblit/client/StatusPanel.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.client;
+
+import java.awt.BorderLayout;
+import java.awt.Dimension;
+import java.awt.FlowLayout;
+import java.awt.Font;
+import java.awt.GridLayout;
+import java.awt.Insets;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.io.IOException;
+
+import javax.swing.JButton;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTable;
+
+import com.gitblit.Constants;
+import com.gitblit.Constants.RpcRequest;
+import com.gitblit.models.ServerStatus;
+import com.gitblit.utils.ByteFormat;
+
+/**
+ * This panel displays the server status.
+ *
+ * @author James Moger
+ */
+public class StatusPanel extends JPanel {
+
+ private static final long serialVersionUID = 1L;
+ private final GitblitClient gitblit;
+ private JLabel bootDate;
+ private JLabel url;
+ private JLabel servletContainer;
+ private JLabel heapMaximum;
+ private JLabel heapAllocated;
+ private JLabel heapUsed;
+ private PropertiesTableModel tableModel;
+ private HeaderPanel header;
+ private JLabel version;
+ private JLabel releaseDate;
+
+ public StatusPanel(GitblitClient gitblit) {
+ super();
+ this.gitblit = gitblit;
+ initialize();
+ }
+
+ private void initialize() {
+ JButton refreshStatus = new JButton(Translation.get("gb.refresh"));
+ refreshStatus.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ refreshStatus();
+ }
+ });
+
+ version = new JLabel();
+ releaseDate = new JLabel();
+ bootDate = new JLabel();
+ url = new JLabel();
+ servletContainer = new JLabel();
+
+ heapMaximum = new JLabel();
+ heapAllocated = new JLabel();
+ heapUsed = new JLabel();
+
+ JPanel fieldsPanel = new JPanel(new GridLayout(0, 1, 0, Utils.MARGIN)) {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public Insets getInsets() {
+ return Utils.INSETS;
+ }
+ };
+ fieldsPanel.add(createFieldPanel("gb.version", version));
+ fieldsPanel.add(createFieldPanel("gb.releaseDate", releaseDate));
+ fieldsPanel.add(createFieldPanel("gb.bootDate", bootDate));
+ fieldsPanel.add(createFieldPanel("gb.url", url));
+ fieldsPanel.add(createFieldPanel("gb.servletContainer", servletContainer));
+ fieldsPanel.add(createFieldPanel("gb.heapUsed", heapUsed));
+ fieldsPanel.add(createFieldPanel("gb.heapAllocated", heapAllocated));
+ fieldsPanel.add(createFieldPanel("gb.heapMaximum", heapMaximum));
+
+ tableModel = new PropertiesTableModel();
+ JTable propertiesTable = Utils.newTable(tableModel, Utils.DATE_FORMAT);
+ String name = propertiesTable.getColumnName(PropertiesTableModel.Columns.Name.ordinal());
+ NameRenderer nameRenderer = new NameRenderer();
+ propertiesTable.getColumn(name).setCellRenderer(nameRenderer);
+
+ JPanel centerPanel = new JPanel(new BorderLayout(Utils.MARGIN, Utils.MARGIN));
+ centerPanel.add(fieldsPanel, BorderLayout.NORTH);
+ centerPanel.add(new JScrollPane(propertiesTable), BorderLayout.CENTER);
+
+ JPanel controls = new JPanel(new FlowLayout(FlowLayout.CENTER, Utils.MARGIN, 0));
+ controls.add(refreshStatus);
+
+ header = new HeaderPanel(Translation.get("gb.status"), "health_16x16.png");
+ setLayout(new BorderLayout(Utils.MARGIN, Utils.MARGIN));
+ add(header, BorderLayout.NORTH);
+ add(centerPanel, BorderLayout.CENTER);
+ add(controls, BorderLayout.SOUTH);
+ }
+
+ private JPanel createFieldPanel(String key, JLabel valueLabel) {
+ JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT, Utils.MARGIN, 0));
+ JLabel textLabel = new JLabel(Translation.get(key));
+ textLabel.setFont(textLabel.getFont().deriveFont(Font.BOLD));
+ textLabel.setPreferredSize(new Dimension(120, 10));
+ panel.add(textLabel);
+ panel.add(valueLabel);
+ return panel;
+ }
+
+ @Override
+ public Insets getInsets() {
+ return Utils.INSETS;
+ }
+
+ protected void refreshStatus() {
+ GitblitWorker worker = new GitblitWorker(StatusPanel.this, RpcRequest.LIST_STATUS) {
+ @Override
+ protected Boolean doRequest() throws IOException {
+ gitblit.refreshStatus();
+ return true;
+ }
+
+ @Override
+ protected void onSuccess() {
+ updateTable(false);
+ }
+ };
+ worker.execute();
+ }
+
+ protected void updateTable(boolean pack) {
+ ServerStatus status = gitblit.getStatus();
+ header.setText(Translation.get("gb.status"));
+ version.setText(Constants.NAME + (status.isGO ? " GO v" : " WAR v") + status.version);
+ releaseDate.setText(status.releaseDate);
+ bootDate.setText(status.bootDate.toString() + " (" + Translation.getTimeUtils().timeAgo(status.bootDate)
+ + ")");
+ url.setText(gitblit.url);
+ servletContainer.setText(status.servletContainer);
+ ByteFormat byteFormat = new ByteFormat();
+ heapMaximum.setText(byteFormat.format(status.heapMaximum));
+ heapAllocated.setText(byteFormat.format(status.heapAllocated));
+ heapUsed.setText(byteFormat.format(status.heapAllocated - status.heapFree) + " ("
+ + byteFormat.format(status.heapFree) + " " + Translation.get("gb.free") + ")");
+ tableModel.setProperties(status.systemProperties);
+ tableModel.fireTableDataChanged();
+ }
+}
diff --git a/src/main/java/com/gitblit/client/SubscribedRepositoryRenderer.java b/src/main/java/com/gitblit/client/SubscribedRepositoryRenderer.java
new file mode 100644
index 00000000..9943333f
--- /dev/null
+++ b/src/main/java/com/gitblit/client/SubscribedRepositoryRenderer.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.client;
+
+import java.awt.Component;
+
+import javax.swing.ImageIcon;
+import javax.swing.JTable;
+
+import com.gitblit.models.RepositoryModel;
+
+/**
+ * Displays a subscribed icon on the left of the repository name, if there is at
+ * least one subscribed branch.
+ *
+ * @author James Moger
+ *
+ */
+public class SubscribedRepositoryRenderer extends NameRenderer {
+
+ private static final long serialVersionUID = 1L;
+
+ private final GitblitClient gitblit;
+
+ private final ImageIcon blankIcon;
+
+ private final ImageIcon subscribedIcon;
+
+ public SubscribedRepositoryRenderer(GitblitClient gitblit) {
+ super();
+ this.gitblit = gitblit;
+ blankIcon = new ImageIcon(getClass().getResource("/blank.png"));
+ subscribedIcon = new ImageIcon(getClass().getResource("/bullet_feed.png"));
+ }
+
+ public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,
+ boolean hasFocus, int row, int column) {
+ super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
+ if (value instanceof RepositoryModel) {
+ RepositoryModel model = (RepositoryModel) value;
+ if (gitblit.isSubscribed(model)) {
+ setIcon(subscribedIcon);
+ } else {
+ setIcon(blankIcon);
+ }
+ }
+ return this;
+ }
+}
diff --git a/src/main/java/com/gitblit/client/SubscriptionsDialog.java b/src/main/java/com/gitblit/client/SubscriptionsDialog.java
new file mode 100644
index 00000000..5ae836a5
--- /dev/null
+++ b/src/main/java/com/gitblit/client/SubscriptionsDialog.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.client;
+
+import java.awt.BorderLayout;
+import java.awt.FlowLayout;
+import java.awt.Insets;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.KeyEvent;
+import java.util.List;
+
+import javax.swing.ImageIcon;
+import javax.swing.JButton;
+import javax.swing.JComponent;
+import javax.swing.JDialog;
+import javax.swing.JPanel;
+import javax.swing.JRootPane;
+import javax.swing.JScrollPane;
+import javax.swing.JTable;
+import javax.swing.KeyStroke;
+import javax.swing.event.ListSelectionEvent;
+import javax.swing.event.ListSelectionListener;
+
+import com.gitblit.models.FeedModel;
+
+/**
+ * Displays a list of repository branches and allows the user to check or
+ * uncheck branches.
+ *
+ * @author James Moger
+ *
+ */
+public abstract class SubscriptionsDialog extends JDialog {
+
+ private static final long serialVersionUID = 1L;
+
+ private final List<FeedModel> feeds;
+
+ private JTable feedsTable;
+
+ private FeedsTableModel model;
+
+ public SubscriptionsDialog(List<FeedModel> registrations) {
+ super();
+ this.feeds = registrations;
+ setTitle(Translation.get("gb.subscribe"));
+ setIconImage(new ImageIcon(getClass().getResource("/gitblt-favicon.png")).getImage());
+ initialize();
+ setSize(600, 400);
+ }
+
+ @Override
+ protected JRootPane createRootPane() {
+ KeyStroke stroke = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);
+ JRootPane rootPane = new JRootPane();
+ rootPane.registerKeyboardAction(new ActionListener() {
+ public void actionPerformed(ActionEvent actionEvent) {
+ setVisible(false);
+ }
+ }, stroke, JComponent.WHEN_IN_FOCUSED_WINDOW);
+ return rootPane;
+ }
+
+ private void initialize() {
+ NameRenderer nameRenderer = new NameRenderer();
+ model = new FeedsTableModel(feeds);
+ feedsTable = Utils.newTable(model, Utils.DATE_FORMAT);
+ feedsTable.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
+ @Override
+ public void valueChanged(ListSelectionEvent e) {
+ if (e.getValueIsAdjusting()) {
+ return;
+ }
+ int viewRow = feedsTable.getSelectedRow();
+ if (viewRow == -1) {
+ return;
+ }
+ int modelRow = feedsTable.convertRowIndexToModel(viewRow);
+ FeedModel feed = model.get(modelRow);
+ feed.subscribed = !feed.subscribed;
+ model.fireTableDataChanged();
+ }
+ });
+
+ String repository = feedsTable.getColumnName(FeedsTableModel.Columns.Repository.ordinal());
+ feedsTable.getColumn(repository).setCellRenderer(nameRenderer);
+
+ String branch = feedsTable.getColumnName(FeedsTableModel.Columns.Branch.ordinal());
+ feedsTable.getColumn(branch).setCellRenderer(new BranchRenderer());
+
+ String subscribed = feedsTable.getColumnName(FeedsTableModel.Columns.Subscribed.ordinal());
+ feedsTable.getColumn(subscribed).setCellRenderer(new BooleanCellRenderer());
+ feedsTable.getColumn(subscribed).setMinWidth(30);
+ feedsTable.getColumn(subscribed).setMaxWidth(30);
+
+ Utils.packColumns(feedsTable, 5);
+
+ final JButton cancel = new JButton(Translation.get("gb.cancel"));
+ cancel.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent event) {
+ setVisible(false);
+ }
+ });
+
+ final JButton save = new JButton(Translation.get("gb.save"));
+ save.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent event) {
+ save();
+ }
+ });
+
+ feedsTable.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
+ @Override
+ public void valueChanged(ListSelectionEvent e) {
+ if (e.getValueIsAdjusting()) {
+ return;
+ }
+ }
+ });
+
+ JPanel controls = new JPanel(new FlowLayout(FlowLayout.CENTER, 5, 0));
+ controls.add(cancel);
+ controls.add(save);
+
+ final Insets insets = new Insets(5, 5, 5, 5);
+ JPanel centerPanel = new JPanel(new BorderLayout(5, 5)) {
+
+ private static final long serialVersionUID = 1L;
+
+ public Insets getInsets() {
+ return insets;
+ }
+ };
+ centerPanel.add(new HeaderPanel(Translation.get("gb.subscribe") + "...", "feed_16x16.png"),
+ BorderLayout.NORTH);
+ centerPanel.add(new JScrollPane(feedsTable), BorderLayout.CENTER);
+ centerPanel.add(controls, BorderLayout.SOUTH);
+
+ getContentPane().setLayout(new BorderLayout(5, 5));
+ getContentPane().add(centerPanel, BorderLayout.CENTER);
+ }
+
+ public abstract void save();
+}
diff --git a/src/main/java/com/gitblit/client/TeamsPanel.java b/src/main/java/com/gitblit/client/TeamsPanel.java
new file mode 100644
index 00000000..92182222
--- /dev/null
+++ b/src/main/java/com/gitblit/client/TeamsPanel.java
@@ -0,0 +1,385 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.client;
+
+import java.awt.BorderLayout;
+import java.awt.FlowLayout;
+import java.awt.Insets;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.KeyAdapter;
+import java.awt.event.KeyEvent;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.JButton;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTable;
+import javax.swing.JTextField;
+import javax.swing.RowFilter;
+import javax.swing.event.ListSelectionEvent;
+import javax.swing.event.ListSelectionListener;
+import javax.swing.table.TableRowSorter;
+
+import com.gitblit.Constants.RpcRequest;
+import com.gitblit.models.TeamModel;
+import com.gitblit.utils.StringUtils;
+
+/**
+ * Users panel displays a list of user accounts and allows management of those
+ * accounts.
+ *
+ * @author James Moger
+ *
+ */
+public abstract class TeamsPanel extends JPanel {
+
+ private static final long serialVersionUID = 1L;
+
+ private final GitblitClient gitblit;
+
+ private HeaderPanel header;
+
+ private JTable table;
+
+ private TeamsTableModel tableModel;
+
+ private TableRowSorter<TeamsTableModel> defaultSorter;
+
+ private JTextField filterTextfield;
+
+ public TeamsPanel(GitblitClient gitblit) {
+ super();
+ this.gitblit = gitblit;
+ initialize();
+ }
+
+ private void initialize() {
+ JButton refreshTeams = new JButton(Translation.get("gb.refresh"));
+ refreshTeams.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ refreshTeams();
+ }
+ });
+
+ JButton createTeam = new JButton(Translation.get("gb.create"));
+ createTeam.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ createTeam();
+ }
+ });
+
+ final JButton editTeam = new JButton(Translation.get("gb.edit"));
+ editTeam.setEnabled(false);
+ editTeam.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ editTeam(getSelectedTeams().get(0));
+ }
+ });
+
+ final JButton delTeam = new JButton(Translation.get("gb.delete"));
+ delTeam.setEnabled(false);
+ delTeam.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ deleteTeams(getSelectedTeams());
+ }
+ });
+
+ NameRenderer nameRenderer = new NameRenderer();
+ tableModel = new TeamsTableModel();
+ defaultSorter = new TableRowSorter<TeamsTableModel>(tableModel);
+ table = Utils.newTable(tableModel, Utils.DATE_FORMAT);
+ String name = table.getColumnName(TeamsTableModel.Columns.Name.ordinal());
+ table.getColumn(name).setCellRenderer(nameRenderer);
+
+ int w = 125;
+ name = table.getColumnName(TeamsTableModel.Columns.Members.ordinal());
+ table.getColumn(name).setMinWidth(w);
+ table.getColumn(name).setMaxWidth(w);
+ name = table.getColumnName(TeamsTableModel.Columns.Repositories.ordinal());
+ table.getColumn(name).setMinWidth(w);
+ table.getColumn(name).setMaxWidth(w);
+
+ table.setRowSorter(defaultSorter);
+ table.getRowSorter().toggleSortOrder(TeamsTableModel.Columns.Name.ordinal());
+ table.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
+
+ @Override
+ public void valueChanged(ListSelectionEvent e) {
+ if (e.getValueIsAdjusting()) {
+ return;
+ }
+ boolean selected = table.getSelectedRow() > -1;
+ boolean singleSelection = table.getSelectedRows().length == 1;
+ editTeam.setEnabled(singleSelection && selected);
+ delTeam.setEnabled(selected);
+ }
+ });
+
+ table.addMouseListener(new MouseAdapter() {
+ public void mouseClicked(MouseEvent e) {
+ if (e.getClickCount() == 2) {
+ editTeam(getSelectedTeams().get(0));
+ }
+ }
+ });
+
+ filterTextfield = new JTextField();
+ filterTextfield.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ filterTeams(filterTextfield.getText());
+ }
+ });
+ filterTextfield.addKeyListener(new KeyAdapter() {
+ public void keyReleased(KeyEvent e) {
+ filterTeams(filterTextfield.getText());
+ }
+ });
+
+ JPanel teamFilterPanel = new JPanel(new BorderLayout(Utils.MARGIN, Utils.MARGIN));
+ teamFilterPanel.add(new JLabel(Translation.get("gb.filter")), BorderLayout.WEST);
+ teamFilterPanel.add(filterTextfield, BorderLayout.CENTER);
+
+ JPanel teamTablePanel = new JPanel(new BorderLayout(Utils.MARGIN, Utils.MARGIN));
+ teamTablePanel.add(teamFilterPanel, BorderLayout.NORTH);
+ teamTablePanel.add(new JScrollPane(table), BorderLayout.CENTER);
+
+ JPanel teamControls = new JPanel(new FlowLayout(FlowLayout.CENTER, 5, 0));
+ teamControls.add(refreshTeams);
+ teamControls.add(createTeam);
+ teamControls.add(editTeam);
+ teamControls.add(delTeam);
+
+ setLayout(new BorderLayout(Utils.MARGIN, Utils.MARGIN));
+ header = new HeaderPanel(Translation.get("gb.teams"), "users_16x16.png");
+ add(header, BorderLayout.NORTH);
+ add(teamTablePanel, BorderLayout.CENTER);
+ add(teamControls, BorderLayout.SOUTH);
+ }
+
+ @Override
+ public void requestFocus() {
+ filterTextfield.requestFocus();
+ }
+
+ @Override
+ public Insets getInsets() {
+ return Utils.INSETS;
+ }
+
+ protected abstract void updateUsersTable();
+
+ protected void updateTable(boolean pack) {
+ tableModel.list.clear();
+ tableModel.list.addAll(gitblit.getTeams());
+ tableModel.fireTableDataChanged();
+ header.setText(Translation.get("gb.teams") + " (" + gitblit.getTeams().size() + ")");
+ if (pack) {
+ Utils.packColumns(table, Utils.MARGIN);
+ }
+ }
+
+ private void filterTeams(final String fragment) {
+ if (StringUtils.isEmpty(fragment)) {
+ table.setRowSorter(defaultSorter);
+ return;
+ }
+ RowFilter<TeamsTableModel, Object> containsFilter = new RowFilter<TeamsTableModel, Object>() {
+ public boolean include(Entry<? extends TeamsTableModel, ? extends Object> entry) {
+ for (int i = entry.getValueCount() - 1; i >= 0; i--) {
+ if (entry.getStringValue(i).toLowerCase().contains(fragment.toLowerCase())) {
+ return true;
+ }
+ }
+ return false;
+ }
+ };
+ TableRowSorter<TeamsTableModel> sorter = new TableRowSorter<TeamsTableModel>(tableModel);
+ sorter.setRowFilter(containsFilter);
+ table.setRowSorter(sorter);
+ }
+
+ private List<TeamModel> getSelectedTeams() {
+ List<TeamModel> teams = new ArrayList<TeamModel>();
+ for (int viewRow : table.getSelectedRows()) {
+ int modelRow = table.convertRowIndexToModel(viewRow);
+ TeamModel model = tableModel.list.get(modelRow);
+ teams.add(model);
+ }
+ return teams;
+ }
+
+ protected void refreshTeams() {
+ GitblitWorker worker = new GitblitWorker(TeamsPanel.this, RpcRequest.LIST_TEAMS) {
+ @Override
+ protected Boolean doRequest() throws IOException {
+ gitblit.refreshTeams();
+ return true;
+ }
+
+ @Override
+ protected void onSuccess() {
+ updateTable(false);
+ }
+ };
+ worker.execute();
+ }
+
+ /**
+ * Displays the create team dialog and fires a SwingWorker to update the
+ * server, if appropriate.
+ *
+ */
+ protected void createTeam() {
+ EditTeamDialog dialog = new EditTeamDialog(gitblit.getProtocolVersion(),
+ gitblit.getSettings());
+ dialog.setLocationRelativeTo(TeamsPanel.this);
+ dialog.setTeams(gitblit.getTeams());
+ dialog.setRepositories(gitblit.getRepositories(), null);
+ dialog.setUsers(gitblit.getUsernames(), null);
+ dialog.setPreReceiveScripts(gitblit.getPreReceiveScriptsUnused(null),
+ gitblit.getPreReceiveScriptsInherited(null), null);
+ dialog.setPostReceiveScripts(gitblit.getPostReceiveScriptsUnused(null),
+ gitblit.getPostReceiveScriptsInherited(null), null);
+ dialog.setVisible(true);
+ final TeamModel newTeam = dialog.getTeam();
+ if (newTeam == null) {
+ return;
+ }
+
+ GitblitWorker worker = new GitblitWorker(this, RpcRequest.CREATE_TEAM) {
+
+ @Override
+ protected Boolean doRequest() throws IOException {
+ boolean success = gitblit.createTeam(newTeam);
+ if (success) {
+ gitblit.refreshTeams();
+ gitblit.refreshUsers();
+ }
+ return success;
+ }
+
+ @Override
+ protected void onSuccess() {
+ updateTable(false);
+ updateUsersTable();
+ }
+
+ @Override
+ protected void onFailure() {
+ showFailure("Failed to execute request \"{0}\" for team \"{1}\".",
+ getRequestType(), newTeam.name);
+ }
+ };
+ worker.execute();
+ }
+
+ /**
+ * Displays the edit team dialog and fires a SwingWorker to update the
+ * server, if appropriate.
+ *
+ * @param user
+ */
+ protected void editTeam(final TeamModel team) {
+ EditTeamDialog dialog = new EditTeamDialog(gitblit.getProtocolVersion(), team,
+ gitblit.getSettings());
+ dialog.setLocationRelativeTo(TeamsPanel.this);
+ dialog.setTeams(gitblit.getTeams());
+ dialog.setRepositories(gitblit.getRepositories(), team.getRepositoryPermissions());
+ dialog.setUsers(gitblit.getUsernames(), team.users == null ? null : new ArrayList<String>(
+ team.users));
+ dialog.setPreReceiveScripts(gitblit.getPreReceiveScriptsUnused(null),
+ gitblit.getPreReceiveScriptsInherited(null), team.preReceiveScripts);
+ dialog.setPostReceiveScripts(gitblit.getPostReceiveScriptsUnused(null),
+ gitblit.getPostReceiveScriptsInherited(null), team.postReceiveScripts);
+ dialog.setVisible(true);
+ final TeamModel revisedTeam = dialog.getTeam();
+ if (revisedTeam == null) {
+ return;
+ }
+
+ GitblitWorker worker = new GitblitWorker(this, RpcRequest.EDIT_TEAM) {
+ @Override
+ protected Boolean doRequest() throws IOException {
+ boolean success = gitblit.updateTeam(team.name, revisedTeam);
+ if (success) {
+ gitblit.refreshTeams();
+ gitblit.refreshUsers();
+ }
+ return success;
+ }
+
+ @Override
+ protected void onSuccess() {
+ updateTable(false);
+ updateUsersTable();
+ }
+
+ @Override
+ protected void onFailure() {
+ showFailure("Failed to execute request \"{0}\" for team \"{1}\".",
+ getRequestType(), team.name);
+ }
+ };
+ worker.execute();
+ }
+
+ protected void deleteTeams(final List<TeamModel> teams) {
+ if (teams == null || teams.size() == 0) {
+ return;
+ }
+ StringBuilder message = new StringBuilder("Delete the following teams?\n\n");
+ for (TeamModel team : teams) {
+ message.append(team.name).append("\n");
+ }
+ int result = JOptionPane.showConfirmDialog(TeamsPanel.this, message.toString(),
+ "Delete Teams?", JOptionPane.YES_NO_OPTION);
+ if (result == JOptionPane.YES_OPTION) {
+ GitblitWorker worker = new GitblitWorker(this, RpcRequest.DELETE_TEAM) {
+ @Override
+ protected Boolean doRequest() throws IOException {
+ boolean success = true;
+ for (TeamModel team : teams) {
+ success &= gitblit.deleteTeam(team);
+ }
+ if (success) {
+ gitblit.refreshTeams();
+ gitblit.refreshUsers();
+ }
+ return success;
+ }
+
+ @Override
+ protected void onSuccess() {
+ updateTable(false);
+ updateUsersTable();
+ }
+
+ @Override
+ protected void onFailure() {
+ showFailure("Failed to delete specified teams!");
+ }
+ };
+ worker.execute();
+ }
+ }
+}
diff --git a/src/main/java/com/gitblit/client/TeamsTableModel.java b/src/main/java/com/gitblit/client/TeamsTableModel.java
new file mode 100644
index 00000000..e6d8a945
--- /dev/null
+++ b/src/main/java/com/gitblit/client/TeamsTableModel.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.client;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import javax.swing.table.AbstractTableModel;
+
+import com.gitblit.models.TeamModel;
+
+/**
+ * Table model of a list of teams.
+ *
+ * @author James Moger
+ *
+ */
+public class TeamsTableModel extends AbstractTableModel {
+
+ private static final long serialVersionUID = 1L;
+
+ List<TeamModel> list;
+
+ enum Columns {
+ Name, Members, Repositories;
+
+ @Override
+ public String toString() {
+ return name().replace('_', ' ');
+ }
+ }
+
+ public TeamsTableModel() {
+ this(new ArrayList<TeamModel>());
+ }
+
+ public TeamsTableModel(List<TeamModel> teams) {
+ this.list = teams;
+ Collections.sort(this.list);
+ }
+
+ @Override
+ public int getRowCount() {
+ return list.size();
+ }
+
+ @Override
+ public int getColumnCount() {
+ return Columns.values().length;
+ }
+
+ @Override
+ public String getColumnName(int column) {
+ Columns col = Columns.values()[column];
+ switch (col) {
+ case Name:
+ return Translation.get("gb.name");
+ case Members:
+ return Translation.get("gb.teamMembers");
+ case Repositories:
+ return Translation.get("gb.repositories");
+ }
+ return "";
+ }
+
+ /**
+ * Returns <code>Object.class</code> regardless of <code>columnIndex</code>.
+ *
+ * @param columnIndex
+ * the column being queried
+ * @return the Object.class
+ */
+ public Class<?> getColumnClass(int columnIndex) {
+ return String.class;
+ }
+
+ @Override
+ public Object getValueAt(int rowIndex, int columnIndex) {
+ TeamModel model = list.get(rowIndex);
+ Columns col = Columns.values()[columnIndex];
+ switch (col) {
+ case Name:
+ return model.name;
+ case Members:
+ return model.users.size() == 0 ? "" : String.valueOf(model.users.size());
+ case Repositories:
+ return model.repositories.size() == 0 ? "" : String.valueOf(model.repositories.size());
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/com/gitblit/client/Translation.java b/src/main/java/com/gitblit/client/Translation.java
new file mode 100644
index 00000000..16ef20d4
--- /dev/null
+++ b/src/main/java/com/gitblit/client/Translation.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.client;
+
+import java.util.MissingResourceException;
+import java.util.ResourceBundle;
+
+import com.gitblit.utils.TimeUtils;
+
+/**
+ * Loads the Gitblit language resource file.
+ *
+ * @author James Moger
+ *
+ */
+public class Translation {
+
+ private final static ResourceBundle translation;
+
+ private final static TimeUtils timeUtils;
+
+ static {
+ ResourceBundle bundle;
+ try {
+ // development location
+ bundle = ResourceBundle.getBundle("com/gitblit/wicket/GitBlitWebApp");
+ } catch (MissingResourceException e) {
+ // runtime location
+ bundle = ResourceBundle.getBundle("GitBlitWebApp");
+ }
+ translation = bundle;
+
+ timeUtils = new TimeUtils(translation);
+ }
+
+ public static String get(String key) {
+ if (translation.containsKey(key)) {
+ return translation.getString(key).trim();
+ }
+ return key;
+ }
+
+ public static TimeUtils getTimeUtils() {
+ return timeUtils;
+ }
+}
diff --git a/src/main/java/com/gitblit/client/UsersPanel.java b/src/main/java/com/gitblit/client/UsersPanel.java
new file mode 100644
index 00000000..c53a5791
--- /dev/null
+++ b/src/main/java/com/gitblit/client/UsersPanel.java
@@ -0,0 +1,385 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.client;
+
+import java.awt.BorderLayout;
+import java.awt.FlowLayout;
+import java.awt.Insets;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.KeyAdapter;
+import java.awt.event.KeyEvent;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.JButton;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTable;
+import javax.swing.JTextField;
+import javax.swing.RowFilter;
+import javax.swing.event.ListSelectionEvent;
+import javax.swing.event.ListSelectionListener;
+import javax.swing.table.TableRowSorter;
+
+import com.gitblit.Constants.RpcRequest;
+import com.gitblit.models.TeamModel;
+import com.gitblit.models.UserModel;
+import com.gitblit.utils.StringUtils;
+
+/**
+ * Users panel displays a list of user accounts and allows management of those
+ * accounts.
+ *
+ * @author James Moger
+ *
+ */
+public abstract class UsersPanel extends JPanel {
+
+ private static final long serialVersionUID = 1L;
+
+ private final GitblitClient gitblit;
+
+ private HeaderPanel header;
+
+ private JTable table;
+
+ private UsersTableModel tableModel;
+
+ private TableRowSorter<UsersTableModel> defaultSorter;
+
+ private JTextField filterTextfield;
+
+ public UsersPanel(GitblitClient gitblit) {
+ super();
+ this.gitblit = gitblit;
+ initialize();
+ }
+
+ private void initialize() {
+ JButton refreshUsers = new JButton(Translation.get("gb.refresh"));
+ refreshUsers.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ refreshUsers();
+ }
+ });
+
+ JButton createUser = new JButton(Translation.get("gb.create"));
+ createUser.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ createUser();
+ }
+ });
+
+ final JButton editUser = new JButton(Translation.get("gb.edit"));
+ editUser.setEnabled(false);
+ editUser.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ editUser(getSelectedUsers().get(0));
+ }
+ });
+
+ final JButton delUser = new JButton(Translation.get("gb.delete"));
+ delUser.setEnabled(false);
+ delUser.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ deleteUsers(getSelectedUsers());
+ }
+ });
+
+ NameRenderer nameRenderer = new NameRenderer();
+ tableModel = new UsersTableModel();
+ defaultSorter = new TableRowSorter<UsersTableModel>(tableModel);
+ table = Utils.newTable(tableModel, Utils.DATE_FORMAT);
+ String name = table.getColumnName(UsersTableModel.Columns.Name.ordinal());
+ table.getColumn(name).setCellRenderer(nameRenderer);
+
+ int w = 130;
+ name = table.getColumnName(UsersTableModel.Columns.Type.ordinal());
+ table.getColumn(name).setMinWidth(w);
+ table.getColumn(name).setMaxWidth(w);
+ name = table.getColumnName(UsersTableModel.Columns.Teams.ordinal());
+ table.getColumn(name).setMinWidth(w);
+ table.getColumn(name).setMaxWidth(w);
+ name = table.getColumnName(UsersTableModel.Columns.Repositories.ordinal());
+ table.getColumn(name).setMinWidth(w);
+ table.getColumn(name).setMaxWidth(w);
+
+ table.setRowSorter(defaultSorter);
+ table.getRowSorter().toggleSortOrder(UsersTableModel.Columns.Name.ordinal());
+ table.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
+
+ @Override
+ public void valueChanged(ListSelectionEvent e) {
+ if (e.getValueIsAdjusting()) {
+ return;
+ }
+ boolean selected = table.getSelectedRow() > -1;
+ boolean singleSelection = table.getSelectedRows().length == 1;
+ editUser.setEnabled(singleSelection && selected);
+ delUser.setEnabled(selected);
+ }
+ });
+
+ table.addMouseListener(new MouseAdapter() {
+ public void mouseClicked(MouseEvent e) {
+ if (e.getClickCount() == 2) {
+ editUser(getSelectedUsers().get(0));
+ }
+ }
+ });
+
+ filterTextfield = new JTextField();
+ filterTextfield.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ filterUsers(filterTextfield.getText());
+ }
+ });
+ filterTextfield.addKeyListener(new KeyAdapter() {
+ public void keyReleased(KeyEvent e) {
+ filterUsers(filterTextfield.getText());
+ }
+ });
+
+ JPanel userFilterPanel = new JPanel(new BorderLayout(Utils.MARGIN, Utils.MARGIN));
+ userFilterPanel.add(new JLabel(Translation.get("gb.filter")), BorderLayout.WEST);
+ userFilterPanel.add(filterTextfield, BorderLayout.CENTER);
+
+ JPanel userTablePanel = new JPanel(new BorderLayout(Utils.MARGIN, Utils.MARGIN));
+ userTablePanel.add(userFilterPanel, BorderLayout.NORTH);
+ userTablePanel.add(new JScrollPane(table), BorderLayout.CENTER);
+
+ JPanel userControls = new JPanel(new FlowLayout(FlowLayout.CENTER, 5, 0));
+ userControls.add(refreshUsers);
+ userControls.add(createUser);
+ userControls.add(editUser);
+ userControls.add(delUser);
+
+ setLayout(new BorderLayout(Utils.MARGIN, Utils.MARGIN));
+ header = new HeaderPanel(Translation.get("gb.users"), "user_16x16.png");
+ add(header, BorderLayout.NORTH);
+ add(userTablePanel, BorderLayout.CENTER);
+ add(userControls, BorderLayout.SOUTH);
+ }
+
+ @Override
+ public void requestFocus() {
+ filterTextfield.requestFocus();
+ }
+
+ @Override
+ public Insets getInsets() {
+ return Utils.INSETS;
+ }
+
+ protected abstract void updateTeamsTable();
+
+ protected void updateTable(boolean pack) {
+ tableModel.list.clear();
+ tableModel.list.addAll(gitblit.getUsers());
+ tableModel.fireTableDataChanged();
+ header.setText(Translation.get("gb.users") + " (" + gitblit.getUsers().size() + ")");
+ if (pack) {
+ Utils.packColumns(table, Utils.MARGIN);
+ }
+ }
+
+ private void filterUsers(final String fragment) {
+ if (StringUtils.isEmpty(fragment)) {
+ table.setRowSorter(defaultSorter);
+ return;
+ }
+ RowFilter<UsersTableModel, Object> containsFilter = new RowFilter<UsersTableModel, Object>() {
+ public boolean include(Entry<? extends UsersTableModel, ? extends Object> entry) {
+ for (int i = entry.getValueCount() - 1; i >= 0; i--) {
+ if (entry.getStringValue(i).toLowerCase().contains(fragment.toLowerCase())) {
+ return true;
+ }
+ }
+ return false;
+ }
+ };
+ TableRowSorter<UsersTableModel> sorter = new TableRowSorter<UsersTableModel>(tableModel);
+ sorter.setRowFilter(containsFilter);
+ table.setRowSorter(sorter);
+ }
+
+ private List<UserModel> getSelectedUsers() {
+ List<UserModel> users = new ArrayList<UserModel>();
+ for (int viewRow : table.getSelectedRows()) {
+ int modelRow = table.convertRowIndexToModel(viewRow);
+ UserModel model = tableModel.list.get(modelRow);
+ users.add(model);
+ }
+ return users;
+ }
+
+ protected void refreshUsers() {
+ GitblitWorker worker = new GitblitWorker(UsersPanel.this, RpcRequest.LIST_USERS) {
+ @Override
+ protected Boolean doRequest() throws IOException {
+ gitblit.refreshUsers();
+ return true;
+ }
+
+ @Override
+ protected void onSuccess() {
+ updateTable(false);
+ }
+ };
+ worker.execute();
+ }
+
+ /**
+ * Displays the create user dialog and fires a SwingWorker to update the
+ * server, if appropriate.
+ *
+ */
+ protected void createUser() {
+ EditUserDialog dialog = new EditUserDialog(gitblit.getProtocolVersion(),
+ gitblit.getSettings());
+ dialog.setLocationRelativeTo(UsersPanel.this);
+ dialog.setUsers(gitblit.getUsers());
+ dialog.setRepositories(gitblit.getRepositories(), null);
+ dialog.setTeams(gitblit.getTeams(), null);
+ dialog.setVisible(true);
+ final UserModel newUser = dialog.getUser();
+ if (newUser == null) {
+ return;
+ }
+
+ GitblitWorker worker = new GitblitWorker(this, RpcRequest.CREATE_USER) {
+
+ @Override
+ protected Boolean doRequest() throws IOException {
+ boolean success = gitblit.createUser(newUser);
+ if (success) {
+ gitblit.refreshUsers();
+ if (newUser.teams.size() > 0) {
+ gitblit.refreshTeams();
+ }
+ }
+ return success;
+ }
+
+ @Override
+ protected void onSuccess() {
+ updateTable(false);
+ if (newUser.teams.size() > 0) {
+ updateTeamsTable();
+ }
+ }
+
+ @Override
+ protected void onFailure() {
+ showFailure("Failed to execute request \"{0}\" for user \"{1}\".",
+ getRequestType(), newUser.username);
+ }
+ };
+ worker.execute();
+ }
+
+ /**
+ * Displays the edit user dialog and fires a SwingWorker to update the
+ * server, if appropriate.
+ *
+ * @param user
+ */
+ protected void editUser(final UserModel user) {
+ EditUserDialog dialog = new EditUserDialog(gitblit.getProtocolVersion(), user,
+ gitblit.getSettings());
+ dialog.setLocationRelativeTo(UsersPanel.this);
+ dialog.setUsers(gitblit.getUsers());
+ dialog.setRepositories(gitblit.getRepositories(), gitblit.getUserAccessPermissions(user));
+ dialog.setTeams(gitblit.getTeams(), user.teams == null ? null : new ArrayList<TeamModel>(
+ user.teams));
+ dialog.setVisible(true);
+ final UserModel revisedUser = dialog.getUser();
+ if (revisedUser == null) {
+ return;
+ }
+
+ GitblitWorker worker = new GitblitWorker(this, RpcRequest.EDIT_USER) {
+ @Override
+ protected Boolean doRequest() throws IOException {
+ boolean success = gitblit.updateUser(user.username, revisedUser);
+ if (success) {
+ gitblit.refreshUsers();
+ gitblit.refreshTeams();
+ }
+ return success;
+ }
+
+ @Override
+ protected void onSuccess() {
+ updateTable(false);
+ updateTeamsTable();
+ }
+
+ @Override
+ protected void onFailure() {
+ showFailure("Failed to execute request \"{0}\" for user \"{1}\".",
+ getRequestType(), user.username);
+ }
+ };
+ worker.execute();
+ }
+
+ protected void deleteUsers(final List<UserModel> users) {
+ if (users == null || users.size() == 0) {
+ return;
+ }
+ StringBuilder message = new StringBuilder("Delete the following users?\n\n");
+ for (UserModel user : users) {
+ message.append(user.username).append("\n");
+ }
+ int result = JOptionPane.showConfirmDialog(UsersPanel.this, message.toString(),
+ "Delete Users?", JOptionPane.YES_NO_OPTION);
+ if (result == JOptionPane.YES_OPTION) {
+ GitblitWorker worker = new GitblitWorker(this, RpcRequest.DELETE_USER) {
+ @Override
+ protected Boolean doRequest() throws IOException {
+ boolean success = true;
+ for (UserModel user : users) {
+ success &= gitblit.deleteUser(user);
+ }
+ if (success) {
+ gitblit.refreshUsers();
+ gitblit.refreshTeams();
+ }
+ return success;
+ }
+
+ @Override
+ protected void onSuccess() {
+ updateTable(false);
+ updateTeamsTable();
+ }
+
+ @Override
+ protected void onFailure() {
+ showFailure("Failed to delete specified users!");
+ }
+ };
+ worker.execute();
+ }
+ }
+}
diff --git a/src/main/java/com/gitblit/client/UsersTableModel.java b/src/main/java/com/gitblit/client/UsersTableModel.java
new file mode 100644
index 00000000..439d5afb
--- /dev/null
+++ b/src/main/java/com/gitblit/client/UsersTableModel.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.client;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import javax.swing.table.AbstractTableModel;
+
+import com.gitblit.models.UserModel;
+
+/**
+ * Table model of a list of users.
+ *
+ * @author James Moger
+ *
+ */
+public class UsersTableModel extends AbstractTableModel {
+
+ private static final long serialVersionUID = 1L;
+
+ List<UserModel> list;
+
+ enum Columns {
+ Name, Display_Name, Type, Teams, Repositories;
+
+ @Override
+ public String toString() {
+ return name().replace('_', ' ');
+ }
+ }
+
+ public UsersTableModel() {
+ this(new ArrayList<UserModel>());
+ }
+
+ public UsersTableModel(List<UserModel> users) {
+ this.list = users;
+ Collections.sort(this.list);
+ }
+
+ @Override
+ public int getRowCount() {
+ return list.size();
+ }
+
+ @Override
+ public int getColumnCount() {
+ return Columns.values().length;
+ }
+
+ @Override
+ public String getColumnName(int column) {
+ Columns col = Columns.values()[column];
+ switch (col) {
+ case Name:
+ return Translation.get("gb.name");
+ case Display_Name:
+ return Translation.get("gb.displayName");
+ case Type:
+ return Translation.get("gb.type");
+ case Teams:
+ return Translation.get("gb.teamMemberships");
+ case Repositories:
+ return Translation.get("gb.repositories");
+ }
+ return "";
+ }
+
+ /**
+ * Returns <code>Object.class</code> regardless of <code>columnIndex</code>.
+ *
+ * @param columnIndex
+ * the column being queried
+ * @return the Object.class
+ */
+ public Class<?> getColumnClass(int columnIndex) {
+ return String.class;
+ }
+
+ @Override
+ public Object getValueAt(int rowIndex, int columnIndex) {
+ UserModel model = list.get(rowIndex);
+ Columns col = Columns.values()[columnIndex];
+ switch (col) {
+ case Name:
+ return model.username;
+ case Display_Name:
+ return model.displayName;
+ case Type:
+ StringBuilder sb = new StringBuilder();
+ if (model.accountType != null) {
+ sb.append(model.accountType.name());
+ }
+ if (model.canAdmin()) {
+ if (sb.length() > 0) {
+ sb.append(", ");
+ }
+ sb.append("admin");
+ }
+ return sb.toString();
+ case Teams:
+ return (model.teams == null || model.teams.size() == 0) ? "" : String
+ .valueOf(model.teams.size());
+ case Repositories:
+ return (model.permissions == null || model.permissions.size() == 0) ? "" : String
+ .valueOf(model.permissions.size());
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/com/gitblit/client/Utils.java b/src/main/java/com/gitblit/client/Utils.java
new file mode 100644
index 00000000..1e6ab2bf
--- /dev/null
+++ b/src/main/java/com/gitblit/client/Utils.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.client;
+
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.Desktop;
+import java.awt.Dimension;
+import java.awt.Font;
+import java.awt.Insets;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.net.URI;
+import java.text.MessageFormat;
+import java.util.Date;
+
+import javax.swing.JOptionPane;
+import javax.swing.JScrollPane;
+import javax.swing.JTable;
+import javax.swing.JTextArea;
+import javax.swing.table.DefaultTableColumnModel;
+import javax.swing.table.TableCellRenderer;
+import javax.swing.table.TableColumn;
+import javax.swing.table.TableModel;
+
+import com.gitblit.Constants.RpcRequest;
+
+public class Utils {
+
+ public final static int MARGIN = 5;
+
+ public final static Insets INSETS = new Insets(MARGIN, MARGIN, MARGIN, MARGIN);
+
+ public final static String TIMESTAMP_FORMAT = "yyyy-MM-dd HH:mm";
+
+ public final static String DATE_FORMAT = "yyyy-MM-dd";
+
+ public static JTable newTable(TableModel model, String datePattern) {
+ return newTable(model, datePattern, null);
+ }
+
+ public static JTable newTable(TableModel model, String datePattern, final RowRenderer rowRenderer) {
+ JTable table;
+ if (rowRenderer == null) {
+ table = new JTable(model);
+ } else {
+ table = new JTable(model) {
+
+ @Override
+ public Component prepareRenderer(TableCellRenderer renderer, int row, int column) {
+ Component c = super.prepareRenderer(renderer, row, column);
+ boolean isSelected = isCellSelected(row, column);
+ rowRenderer.prepareRow(c, isSelected, row, column);
+ return c;
+ }
+ };
+ }
+ table.setRowHeight(table.getFont().getSize() + 8);
+ table.setCellSelectionEnabled(false);
+ table.setRowSelectionAllowed(true);
+ table.getTableHeader().setReorderingAllowed(false);
+ table.setGridColor(new Color(0xd9d9d9));
+ table.setBackground(Color.white);
+ table.setDefaultRenderer(Date.class,
+ new DateCellRenderer(datePattern, Color.orange.darker()));
+ return table;
+ }
+
+ public static void explainNotAllowed(Component c, RpcRequest request) {
+ String msg = MessageFormat.format("The Gitblit server does not allow the request \"{0}\".",
+ request.name());
+ JOptionPane.showMessageDialog(c, msg, "Not Allowed", JOptionPane.ERROR_MESSAGE);
+ }
+
+ public static void explainForbidden(Component c, RpcRequest request) {
+ String msg = MessageFormat.format(
+ "The request \"{0}\" has been forbidden for the account by the Gitblit server.",
+ request.name());
+ JOptionPane.showMessageDialog(c, msg, "Forbidden", JOptionPane.ERROR_MESSAGE);
+ }
+
+ public static void explainUnauthorized(Component c, RpcRequest request) {
+ String msg = MessageFormat.format(
+ "This account is not authorized to execute the request \"{0}\".", request.name());
+ JOptionPane.showMessageDialog(c, msg, "Unauthorized", JOptionPane.ERROR_MESSAGE);
+ }
+
+ public static void explainUnknown(Component c, RpcRequest request) {
+ String msg = MessageFormat.format(
+ "The request \"{0}\" is not recognized by the Gitblit server.", request.name());
+ JOptionPane.showMessageDialog(c, msg, "Unknown Request", JOptionPane.ERROR_MESSAGE);
+ }
+
+ public static void showException(Component c, Throwable t) {
+ StringWriter writer = new StringWriter();
+ t.printStackTrace(new PrintWriter(writer));
+ String stacktrace = writer.toString();
+ try {
+ writer.close();
+ } catch (Throwable x) {
+ }
+ JTextArea textArea = new JTextArea(stacktrace);
+ textArea.setFont(new Font("monospaced", Font.PLAIN, 11));
+ JScrollPane jsp = new JScrollPane(textArea);
+ jsp.setPreferredSize(new Dimension(800, 400));
+ JOptionPane.showMessageDialog(c, jsp, Translation.get("gb.error"),
+ JOptionPane.ERROR_MESSAGE);
+ }
+
+ public static void packColumns(JTable table, int margin) {
+ for (int c = 0; c < table.getColumnCount(); c++) {
+ packColumn(table, c, 4);
+ }
+ }
+
+ // Sets the preferred width of the visible column specified by vColIndex.
+ // The column will be just wide enough to show the column head and the
+ // widest cell in the column. margin pixels are added to the left and right
+ // (resulting in an additional width of 2*margin pixels).
+ private static void packColumn(JTable table, int vColIndex, int margin) {
+ DefaultTableColumnModel colModel = (DefaultTableColumnModel) table.getColumnModel();
+ TableColumn col = colModel.getColumn(vColIndex);
+ int width = 0;
+
+ // Get width of column header
+ TableCellRenderer renderer = col.getHeaderRenderer();
+ if (renderer == null) {
+ renderer = table.getTableHeader().getDefaultRenderer();
+ }
+ Component comp = renderer.getTableCellRendererComponent(table, col.getHeaderValue(), false,
+ false, 0, 0);
+ width = comp.getPreferredSize().width;
+
+ // Get maximum width of column data
+ for (int r = 0; r < table.getRowCount(); r++) {
+ renderer = table.getCellRenderer(r, vColIndex);
+ comp = renderer.getTableCellRendererComponent(table, table.getValueAt(r, vColIndex),
+ false, false, r, vColIndex);
+ width = Math.max(width, comp.getPreferredSize().width);
+ }
+
+ // Add margin
+ width += 2 * margin;
+
+ // Set the width
+ col.setPreferredWidth(width);
+ }
+
+ public static void browse(String url) {
+ try {
+ Desktop.getDesktop().browse(new URI(url));
+ } catch (Exception x) {
+ showException(null, x);
+ }
+ }
+
+ public static abstract class RowRenderer {
+ public abstract void prepareRow(Component c, boolean isSelected, int row, int column);
+ }
+}
diff --git a/src/main/java/com/gitblit/client/splash.png b/src/main/java/com/gitblit/client/splash.png
new file mode 100644
index 00000000..d63932fb
--- /dev/null
+++ b/src/main/java/com/gitblit/client/splash.png
Binary files differ
diff --git a/src/main/java/com/gitblit/fanout/FanoutClient.java b/src/main/java/com/gitblit/fanout/FanoutClient.java
new file mode 100644
index 00000000..b9ace4be
--- /dev/null
+++ b/src/main/java/com/gitblit/fanout/FanoutClient.java
@@ -0,0 +1,413 @@
+/*
+ * Copyright 2013 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.fanout;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.nio.ByteBuffer;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.Selector;
+import java.nio.channels.SocketChannel;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetDecoder;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Fanout client class.
+ *
+ * @author James Moger
+ *
+ */
+public class FanoutClient implements Runnable {
+
+ private final static Logger logger = LoggerFactory.getLogger(FanoutClient.class);
+
+ private final int clientTimeout = 500;
+ private final int reconnectTimeout = 2000;
+ private final String host;
+ private final int port;
+ private final List<FanoutListener> listeners;
+
+ private String id;
+ private volatile Selector selector;
+ private volatile SocketChannel socketCh;
+ private Thread clientThread;
+
+ private final AtomicBoolean isConnected;
+ private final AtomicBoolean isRunning;
+ private final AtomicBoolean isAutomaticReconnect;
+ private final ByteBuffer writeBuffer;
+ private final ByteBuffer readBuffer;
+ private final CharsetDecoder decoder;
+
+ private final Set<String> subscriptions;
+ private boolean resubscribe;
+
+ public interface FanoutListener {
+ public void pong(Date timestamp);
+ public void announcement(String channel, String message);
+ }
+
+ public static class FanoutAdapter implements FanoutListener {
+ public void pong(Date timestamp) { }
+ public void announcement(String channel, String message) { }
+ }
+
+ public static void main(String args[]) throws Exception {
+ FanoutClient client = new FanoutClient("localhost", 2000);
+ client.addListener(new FanoutAdapter() {
+
+ @Override
+ public void pong(Date timestamp) {
+ System.out.println("Pong. " + timestamp);
+ }
+
+ @Override
+ public void announcement(String channel, String message) {
+ System.out.println(MessageFormat.format("Here ye, Here ye. {0} says {1}", channel, message));
+ }
+ });
+ client.start();
+
+ Thread.sleep(5000);
+ client.ping();
+ client.subscribe("james");
+ client.announce("james", "12345");
+ client.subscribe("c52f99d16eb5627877ae957df7ce1be102783bd5");
+
+ while (true) {
+ Thread.sleep(10000);
+ client.ping();
+ }
+ }
+
+ public FanoutClient(String host, int port) {
+ this.host = host;
+ this.port = port;
+ readBuffer = ByteBuffer.allocateDirect(FanoutConstants.BUFFER_LENGTH);
+ writeBuffer = ByteBuffer.allocateDirect(FanoutConstants.BUFFER_LENGTH);
+ decoder = Charset.forName(FanoutConstants.CHARSET).newDecoder();
+ listeners = Collections.synchronizedList(new ArrayList<FanoutListener>());
+ subscriptions = new LinkedHashSet<String>();
+ isRunning = new AtomicBoolean(false);
+ isConnected = new AtomicBoolean(false);
+ isAutomaticReconnect = new AtomicBoolean(true);
+ }
+
+ public void addListener(FanoutListener listener) {
+ listeners.add(listener);
+ }
+
+ public void removeListener(FanoutListener listener) {
+ listeners.remove(listener);
+ }
+
+ public boolean isAutomaticReconnect() {
+ return isAutomaticReconnect.get();
+ }
+
+ public void setAutomaticReconnect(boolean value) {
+ isAutomaticReconnect.set(value);
+ }
+
+ public void ping() {
+ confirmConnection();
+ write("ping");
+ }
+
+ public void status() {
+ confirmConnection();
+ write("status");
+ }
+
+ public void subscribe(String channel) {
+ confirmConnection();
+ if (subscriptions.add(channel)) {
+ write("subscribe " + channel);
+ }
+ }
+
+ public void unsubscribe(String channel) {
+ confirmConnection();
+ if (subscriptions.remove(channel)) {
+ write("unsubscribe " + channel);
+ }
+ }
+
+ public void announce(String channel, String message) {
+ confirmConnection();
+ write("announce " + channel + " " + message);
+ }
+
+ private void confirmConnection() {
+ if (!isConnected()) {
+ throw new RuntimeException("Fanout client is disconnected!");
+ }
+ }
+
+ public boolean isConnected() {
+ return isRunning.get() && socketCh != null && isConnected.get();
+ }
+
+ /**
+ * Start client connection and return immediately.
+ */
+ public void start() {
+ if (isRunning.get()) {
+ logger.warn("Fanout client is already running");
+ return;
+ }
+ clientThread = new Thread(this, "Fanout client");
+ clientThread.start();
+ }
+
+ /**
+ * Start client connection and wait until it has connected.
+ */
+ public void startSynchronously() {
+ start();
+ while (!isConnected()) {
+ try {
+ Thread.sleep(100);
+ } catch (Exception e) {
+ }
+ }
+ }
+
+ /**
+ * Stops client connection. This method returns when the connection has
+ * been completely shutdown.
+ */
+ public void stop() {
+ if (!isRunning.get()) {
+ logger.warn("Fanout client is not running");
+ return;
+ }
+ isRunning.set(false);
+ try {
+ if (clientThread != null) {
+ clientThread.join();
+ clientThread = null;
+ }
+ } catch (InterruptedException e1) {
+ }
+ }
+
+ @Override
+ public void run() {
+ resetState();
+
+ isRunning.set(true);
+ while (isRunning.get()) {
+ // (re)connect
+ if (socketCh == null) {
+ try {
+ InetAddress addr = InetAddress.getByName(host);
+ socketCh = SocketChannel.open(new InetSocketAddress(addr, port));
+ socketCh.configureBlocking(false);
+ selector = Selector.open();
+ id = FanoutConstants.getLocalSocketId(socketCh.socket());
+ socketCh.register(selector, SelectionKey.OP_READ);
+ } catch (Exception e) {
+ logger.error(MessageFormat.format("failed to open client connection to {0}:{1,number,0}", host, port), e);
+ try {
+ Thread.sleep(reconnectTimeout);
+ } catch (InterruptedException x) {
+ }
+ continue;
+ }
+ }
+
+ // read/write
+ try {
+ selector.select(clientTimeout);
+
+ Iterator<SelectionKey> i = selector.selectedKeys().iterator();
+ while (i.hasNext()) {
+ SelectionKey key = i.next();
+ i.remove();
+
+ if (key.isReadable()) {
+ // read message
+ String content = read();
+ String[] lines = content.split("\n");
+ for (String reply : lines) {
+ logger.trace(MessageFormat.format("fanout client {0} received: {1}", id, reply));
+ if (!processReply(reply)) {
+ logger.error(MessageFormat.format("fanout client {0} received unknown message", id));
+ }
+ }
+ } else if (key.isWritable()) {
+ // resubscribe
+ if (resubscribe) {
+ resubscribe = false;
+ logger.info(MessageFormat.format("fanout client {0} re-subscribing to {1} channels", id, subscriptions.size()));
+ for (String subscription : subscriptions) {
+ write("subscribe " + subscription);
+ }
+ }
+ socketCh.register(selector, SelectionKey.OP_READ);
+ }
+ }
+ } catch (IOException e) {
+ logger.error(MessageFormat.format("fanout client {0} error: {1}", id, e.getMessage()));
+ closeChannel();
+ if (!isAutomaticReconnect.get()) {
+ isRunning.set(false);
+ continue;
+ }
+ }
+ }
+
+ closeChannel();
+ resetState();
+ }
+
+ protected void resetState() {
+ readBuffer.clear();
+ writeBuffer.clear();
+ isRunning.set(false);
+ isConnected.set(false);
+ }
+
+ private void closeChannel() {
+ try {
+ if (socketCh != null) {
+ socketCh.close();
+ socketCh = null;
+ selector.close();
+ selector = null;
+ isConnected.set(false);
+ }
+ } catch (IOException x) {
+ }
+ }
+
+ protected boolean processReply(String reply) {
+ String[] fields = reply.split("!", 2);
+ if (fields.length == 1) {
+ try {
+ long time = Long.parseLong(fields[0]);
+ Date date = new Date(time);
+ firePong(date);
+ } catch (Exception e) {
+ }
+ return true;
+ } else if (fields.length == 2) {
+ String channel = fields[0];
+ String message = fields[1];
+ if (FanoutConstants.CH_DEBUG.equals(channel)) {
+ // debug messages are for internal use
+ if (FanoutConstants.MSG_CONNECTED.equals(message)) {
+ isConnected.set(true);
+ resubscribe = subscriptions.size() > 0;
+ if (resubscribe) {
+ try {
+ // register for async resubscribe
+ socketCh.register(selector, SelectionKey.OP_WRITE);
+ } catch (Exception e) {
+ logger.error("an error occurred", e);
+ }
+ }
+ }
+ logger.debug(MessageFormat.format("fanout client {0} < {1}", id, reply));
+ } else {
+ fireAnnouncement(channel, message);
+ }
+ return true;
+ } else {
+ // unknown message
+ return false;
+ }
+ }
+
+ protected void firePong(Date timestamp) {
+ logger.info(MessageFormat.format("fanout client {0} < pong {1,date,yyyy-MM-dd HH:mm:ss}", id, timestamp));
+ for (FanoutListener listener : listeners) {
+ try {
+ listener.pong(timestamp);
+ } catch (Throwable t) {
+ logger.error("FanoutListener threw an exception!", t);
+ }
+ }
+ }
+ protected void fireAnnouncement(String channel, String message) {
+ logger.info(MessageFormat.format("fanout client {0} < announcement {1} {2}", id, channel, message));
+ for (FanoutListener listener : listeners) {
+ try {
+ listener.announcement(channel, message);
+ } catch (Throwable t) {
+ logger.error("FanoutListener threw an exception!", t);
+ }
+ }
+ }
+
+ protected synchronized String read() throws IOException {
+ readBuffer.clear();
+ long len = socketCh.read(readBuffer);
+
+ if (len == -1) {
+ logger.error(MessageFormat.format("fanout client {0} lost connection to {1}:{2,number,0}, end of stream", id, host, port));
+ socketCh.close();
+ return null;
+ } else {
+ readBuffer.flip();
+ String content = decoder.decode(readBuffer).toString();
+ readBuffer.clear();
+ return content;
+ }
+ }
+
+ protected synchronized boolean write(String message) {
+ try {
+ logger.info(MessageFormat.format("fanout client {0} > {1}", id, message));
+ byte [] bytes = message.getBytes(FanoutConstants.CHARSET);
+ writeBuffer.clear();
+ writeBuffer.put(bytes);
+ if (bytes[bytes.length - 1] != 0xa) {
+ writeBuffer.put((byte) 0xa);
+ }
+ writeBuffer.flip();
+
+ // loop until write buffer has been completely sent
+ long written = 0;
+ long toWrite = writeBuffer.remaining();
+ while (written != toWrite) {
+ written += socketCh.write(writeBuffer);
+ try {
+ Thread.sleep(10);
+ } catch (Exception x) {
+ }
+ }
+ return true;
+ } catch (IOException e) {
+ logger.error("fanout client {0} error: {1}", id, e.getMessage());
+ }
+ return false;
+ }
+} \ No newline at end of file
diff --git a/src/main/java/com/gitblit/fanout/FanoutConstants.java b/src/main/java/com/gitblit/fanout/FanoutConstants.java
new file mode 100644
index 00000000..6e6964c9
--- /dev/null
+++ b/src/main/java/com/gitblit/fanout/FanoutConstants.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2013 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.fanout;
+
+import java.net.Socket;
+
+public class FanoutConstants {
+
+ public final static String CHARSET = "ISO-8859-1";
+ public final static int BUFFER_LENGTH = 512;
+ public final static String CH_ALL = "all";
+ public final static String CH_DEBUG = "debug";
+ public final static String MSG_CONNECTED = "connected...";
+ public final static String MSG_BUSY = "busy";
+
+ public static String getRemoteSocketId(Socket socket) {
+ return socket.getInetAddress().getHostAddress() + ":" + socket.getPort();
+ }
+
+ public static String getLocalSocketId(Socket socket) {
+ return socket.getInetAddress().getHostAddress() + ":" + socket.getLocalPort();
+ }
+}
diff --git a/src/main/java/com/gitblit/fanout/FanoutNioService.java b/src/main/java/com/gitblit/fanout/FanoutNioService.java
new file mode 100644
index 00000000..65d022ab
--- /dev/null
+++ b/src/main/java/com/gitblit/fanout/FanoutNioService.java
@@ -0,0 +1,332 @@
+/*
+ * Copyright 2013 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.fanout;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.Selector;
+import java.nio.channels.ServerSocketChannel;
+import java.nio.channels.SocketChannel;
+import java.nio.charset.CharacterCodingException;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetDecoder;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A single-thread NIO implementation of https://github.com/travisghansen/fanout
+ *
+ * This implementation uses channels and selectors, which are the Java analog of
+ * the Linux epoll mechanism used in the original fanout C code.
+ *
+ * @author James Moger
+ *
+ */
+public class FanoutNioService extends FanoutService {
+
+ private final static Logger logger = LoggerFactory.getLogger(FanoutNioService.class);
+
+ private volatile ServerSocketChannel serviceCh;
+ private volatile Selector selector;
+
+ public static void main(String[] args) throws Exception {
+ FanoutNioService pubsub = new FanoutNioService(null, DEFAULT_PORT);
+ pubsub.setStrictRequestTermination(false);
+ pubsub.setAllowAllChannelAnnouncements(false);
+ pubsub.start();
+ }
+
+ /**
+ * Create a single-threaded fanout service.
+ *
+ * @param host
+ * @param port
+ * the port for running the fanout PubSub service
+ * @throws IOException
+ */
+ public FanoutNioService(int port) {
+ this(null, port);
+ }
+
+ /**
+ * Create a single-threaded fanout service.
+ *
+ * @param bindInterface
+ * the ip address to bind for the service, may be null
+ * @param port
+ * the port for running the fanout PubSub service
+ * @throws IOException
+ */
+ public FanoutNioService(String bindInterface, int port) {
+ super(bindInterface, port, "Fanout nio service");
+ }
+
+ @Override
+ protected boolean isConnected() {
+ return serviceCh != null;
+ }
+
+ @Override
+ protected boolean connect() {
+ if (serviceCh == null) {
+ try {
+ serviceCh = ServerSocketChannel.open();
+ serviceCh.configureBlocking(false);
+ serviceCh.socket().setReuseAddress(true);
+ serviceCh.socket().bind(host == null ? new InetSocketAddress(port) : new InetSocketAddress(host, port));
+ selector = Selector.open();
+ serviceCh.register(selector, SelectionKey.OP_ACCEPT);
+ logger.info(MessageFormat.format("{0} is ready on {1}:{2,number,0}",
+ name, host == null ? "0.0.0.0" : host, port));
+ } catch (IOException e) {
+ logger.error(MessageFormat.format("failed to open {0} on {1}:{2,number,0}",
+ name, name, host == null ? "0.0.0.0" : host, port), e);
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ protected void disconnect() {
+ try {
+ if (serviceCh != null) {
+ // close all active client connections
+ Map<String, SocketChannel> clients = getCurrentClientSockets();
+ for (Map.Entry<String, SocketChannel> client : clients.entrySet()) {
+ closeClientSocket(client.getKey(), client.getValue());
+ }
+
+ // close service socket channel
+ logger.debug(MessageFormat.format("closing {0} socket channel", name));
+ serviceCh.socket().close();
+ serviceCh.close();
+ serviceCh = null;
+ selector.close();
+ selector = null;
+ }
+ } catch (IOException e) {
+ logger.error(MessageFormat.format("failed to disconnect {0}", name), e);
+ }
+ }
+
+ @Override
+ protected void listen() throws IOException {
+ while (selector.select(serviceTimeout) > 0) {
+ Set<SelectionKey> keys = selector.selectedKeys();
+ Iterator<SelectionKey> keyItr = keys.iterator();
+ while (keyItr.hasNext()) {
+ SelectionKey key = (SelectionKey) keyItr.next();
+ if (key.isAcceptable()) {
+ // new fanout client connection
+ ServerSocketChannel sch = (ServerSocketChannel) key.channel();
+ try {
+ SocketChannel ch = sch.accept();
+ ch.configureBlocking(false);
+ configureClientSocket(ch.socket());
+
+ FanoutNioConnection connection = new FanoutNioConnection(ch);
+ addConnection(connection);
+
+ // register to send the queued message
+ ch.register(selector, SelectionKey.OP_WRITE, connection);
+ } catch (IOException e) {
+ logger.error("error accepting fanout connection", e);
+ }
+ } else if (key.isReadable()) {
+ // read fanout client request
+ SocketChannel ch = (SocketChannel) key.channel();
+ FanoutNioConnection connection = (FanoutNioConnection) key.attachment();
+ try {
+ connection.read(ch, isStrictRequestTermination());
+ int replies = 0;
+ Iterator<String> reqItr = connection.requestQueue.iterator();
+ while (reqItr.hasNext()) {
+ String req = reqItr.next();
+ String reply = processRequest(connection, req);
+ reqItr.remove();
+ if (reply != null) {
+ replies++;
+ }
+ }
+
+ if (replies > 0) {
+ // register to send the replies to requests
+ ch.register(selector, SelectionKey.OP_WRITE, connection);
+ } else {
+ // re-register for next read
+ ch.register(selector, SelectionKey.OP_READ, connection);
+ }
+ } catch (IOException e) {
+ logger.error(MessageFormat.format("fanout connection {0} error: {1}", connection.id, e.getMessage()));
+ removeConnection(connection);
+ closeClientSocket(connection.id, ch);
+ }
+ } else if (key.isWritable()) {
+ // asynchronous reply to fanout client request
+ SocketChannel ch = (SocketChannel) key.channel();
+ FanoutNioConnection connection = (FanoutNioConnection) key.attachment();
+ try {
+ connection.write(ch);
+
+ if (hasConnection(connection)) {
+ // register for next read
+ ch.register(selector, SelectionKey.OP_READ, connection);
+ } else {
+ // Connection was rejected due to load or
+ // some other reason. Close it.
+ closeClientSocket(connection.id, ch);
+ }
+ } catch (IOException e) {
+ logger.error(MessageFormat.format("fanout connection {0}: {1}", connection.id, e.getMessage()));
+ removeConnection(connection);
+ closeClientSocket(connection.id, ch);
+ }
+ }
+ keyItr.remove();
+ }
+ }
+ }
+
+ protected void closeClientSocket(String id, SocketChannel ch) {
+ try {
+ ch.close();
+ } catch (IOException e) {
+ logger.error(MessageFormat.format("fanout connection {0}", id), e);
+ }
+ }
+
+ protected void broadcast(Collection<FanoutServiceConnection> connections, String channel, String message) {
+ super.broadcast(connections, channel, message);
+
+ // register queued write
+ Map<String, SocketChannel> sockets = getCurrentClientSockets();
+ for (FanoutServiceConnection connection : connections) {
+ SocketChannel ch = sockets.get(connection.id);
+ if (ch == null) {
+ logger.warn(MessageFormat.format("fanout connection {0} has been disconnected", connection.id));
+ removeConnection(connection);
+ continue;
+ }
+ try {
+ ch.register(selector, SelectionKey.OP_WRITE, connection);
+ } catch (IOException e) {
+ logger.error(MessageFormat.format("failed to register write op for fanout connection {0}", connection.id));
+ }
+ }
+ }
+
+ protected Map<String, SocketChannel> getCurrentClientSockets() {
+ Map<String, SocketChannel> sockets = new HashMap<String, SocketChannel>();
+ for (SelectionKey key : selector.keys()) {
+ if (key.channel() instanceof SocketChannel) {
+ SocketChannel ch = (SocketChannel) key.channel();
+ String id = FanoutConstants.getRemoteSocketId(ch.socket());
+ sockets.put(id, ch);
+ }
+ }
+ return sockets;
+ }
+
+ /**
+ * FanoutNioConnection handles reading/writing messages from a remote fanout
+ * connection.
+ *
+ * @author James Moger
+ *
+ */
+ static class FanoutNioConnection extends FanoutServiceConnection {
+ final ByteBuffer readBuffer;
+ final ByteBuffer writeBuffer;
+ final List<String> requestQueue;
+ final List<String> replyQueue;
+ final CharsetDecoder decoder;
+
+ FanoutNioConnection(SocketChannel ch) {
+ super(ch.socket());
+ readBuffer = ByteBuffer.allocate(FanoutConstants.BUFFER_LENGTH);
+ writeBuffer = ByteBuffer.allocate(FanoutConstants.BUFFER_LENGTH);
+ requestQueue = new ArrayList<String>();
+ replyQueue = new ArrayList<String>();
+ decoder = Charset.forName(FanoutConstants.CHARSET).newDecoder();
+ }
+
+ protected void read(SocketChannel ch, boolean strictRequestTermination) throws CharacterCodingException, IOException {
+ long bytesRead = 0;
+ readBuffer.clear();
+ bytesRead = ch.read(readBuffer);
+ readBuffer.flip();
+ if (bytesRead == -1) {
+ throw new IOException("lost client connection, end of stream");
+ }
+ if (readBuffer.limit() == 0) {
+ return;
+ }
+ CharBuffer cbuf = decoder.decode(readBuffer);
+ String req = cbuf.toString();
+ String [] lines = req.split(strictRequestTermination ? "\n" : "\n|\r");
+ requestQueue.addAll(Arrays.asList(lines));
+ }
+
+ protected void write(SocketChannel ch) throws IOException {
+ Iterator<String> itr = replyQueue.iterator();
+ while (itr.hasNext()) {
+ String reply = itr.next();
+ writeBuffer.clear();
+ logger.debug(MessageFormat.format("fanout reply to {0}: {1}", id, reply));
+ byte [] bytes = reply.getBytes(FanoutConstants.CHARSET);
+ writeBuffer.put(bytes);
+ if (bytes[bytes.length - 1] != 0xa) {
+ writeBuffer.put((byte) 0xa);
+ }
+ writeBuffer.flip();
+
+ // loop until write buffer has been completely sent
+ int written = 0;
+ int toWrite = writeBuffer.remaining();
+ while (written != toWrite) {
+ written += ch.write(writeBuffer);
+ try {
+ Thread.sleep(10);
+ } catch (Exception x) {
+ }
+ }
+ itr.remove();
+ }
+ writeBuffer.clear();
+ }
+
+ @Override
+ protected void reply(String content) throws IOException {
+ // queue the reply
+ // replies are transmitted asynchronously from the requests
+ replyQueue.add(content);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main/java/com/gitblit/fanout/FanoutService.java b/src/main/java/com/gitblit/fanout/FanoutService.java
new file mode 100644
index 00000000..cbfd8a24
--- /dev/null
+++ b/src/main/java/com/gitblit/fanout/FanoutService.java
@@ -0,0 +1,563 @@
+/*
+ * Copyright 2013 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.fanout;
+
+import java.io.IOException;
+import java.net.Socket;
+import java.net.SocketException;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentSkipListSet;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Base class for Fanout service implementations.
+ *
+ * Subclass implementations can be used as a Sparkleshare PubSub notification
+ * server. This allows Sparkleshare to be used in conjunction with Gitblit
+ * behind a corporate firewall that restricts or prohibits client internet access
+ * to the default Sparkleshare PubSub server: notifications.sparkleshare.org
+ *
+ * @author James Moger
+ *
+ */
+public abstract class FanoutService implements Runnable {
+
+ private final static Logger logger = LoggerFactory.getLogger(FanoutService.class);
+
+ public final static int DEFAULT_PORT = 17000;
+
+ protected final static int serviceTimeout = 5000;
+
+ protected final String host;
+ protected final int port;
+ protected final String name;
+
+ private Thread serviceThread;
+
+ private final Map<String, FanoutServiceConnection> connections;
+ private final Map<String, Set<FanoutServiceConnection>> subscriptions;
+
+ protected final AtomicBoolean isRunning;
+ private final AtomicBoolean strictRequestTermination;
+ private final AtomicBoolean allowAllChannelAnnouncements;
+ private final AtomicInteger concurrentConnectionLimit;
+
+ private final Date bootDate;
+ private final AtomicLong rejectedConnectionCount;
+ private final AtomicInteger peakConnectionCount;
+ private final AtomicLong totalConnections;
+ private final AtomicLong totalAnnouncements;
+ private final AtomicLong totalMessages;
+ private final AtomicLong totalSubscribes;
+ private final AtomicLong totalUnsubscribes;
+ private final AtomicLong totalPings;
+
+ protected FanoutService(String host, int port, String name) {
+ this.host = host;
+ this.port = port;
+ this.name = name;
+
+ connections = new ConcurrentHashMap<String, FanoutServiceConnection>();
+ subscriptions = new ConcurrentHashMap<String, Set<FanoutServiceConnection>>();
+ subscriptions.put(FanoutConstants.CH_ALL, new ConcurrentSkipListSet<FanoutServiceConnection>());
+
+ isRunning = new AtomicBoolean(false);
+ strictRequestTermination = new AtomicBoolean(false);
+ allowAllChannelAnnouncements = new AtomicBoolean(false);
+ concurrentConnectionLimit = new AtomicInteger(0);
+
+ bootDate = new Date();
+ rejectedConnectionCount = new AtomicLong(0);
+ peakConnectionCount = new AtomicInteger(0);
+ totalConnections = new AtomicLong(0);
+ totalAnnouncements = new AtomicLong(0);
+ totalMessages = new AtomicLong(0);
+ totalSubscribes = new AtomicLong(0);
+ totalUnsubscribes = new AtomicLong(0);
+ totalPings = new AtomicLong(0);
+ }
+
+ /*
+ * Abstract methods
+ */
+
+ protected abstract boolean isConnected();
+
+ protected abstract boolean connect();
+
+ protected abstract void listen() throws IOException;
+
+ protected abstract void disconnect();
+
+ /**
+ * Returns true if the service requires \n request termination.
+ *
+ * @return true if request requires \n termination
+ */
+ public boolean isStrictRequestTermination() {
+ return strictRequestTermination.get();
+ }
+
+ /**
+ * Control the termination of fanout requests. If true, fanout requests must
+ * be terminated with \n. If false, fanout requests may be terminated with
+ * \n, \r, \r\n, or \n\r. This is useful for debugging with a telnet client.
+ *
+ * @param isStrictTermination
+ */
+ public void setStrictRequestTermination(boolean isStrictTermination) {
+ strictRequestTermination.set(isStrictTermination);
+ }
+
+ /**
+ * Returns the maximum allowable concurrent fanout connections.
+ *
+ * @return the maximum allowable concurrent connection count
+ */
+ public int getConcurrentConnectionLimit() {
+ return concurrentConnectionLimit.get();
+ }
+
+ /**
+ * Sets the maximum allowable concurrent fanout connection count.
+ *
+ * @param value
+ */
+ public void setConcurrentConnectionLimit(int value) {
+ concurrentConnectionLimit.set(value);
+ }
+
+ /**
+ * Returns true if connections are allowed to announce on the all channel.
+ *
+ * @return true if connections are allowed to announce on the all channel
+ */
+ public boolean allowAllChannelAnnouncements() {
+ return allowAllChannelAnnouncements.get();
+ }
+
+ /**
+ * Allows/prohibits connections from announcing on the ALL channel.
+ *
+ * @param value
+ */
+ public void setAllowAllChannelAnnouncements(boolean value) {
+ allowAllChannelAnnouncements.set(value);
+ }
+
+ /**
+ * Returns the current connections
+ *
+ * @param channel
+ * @return map of current connections keyed by their id
+ */
+ public Map<String, FanoutServiceConnection> getCurrentConnections() {
+ return connections;
+ }
+
+ /**
+ * Returns all subscriptions
+ *
+ * @return map of current subscriptions keyed by channel name
+ */
+ public Map<String, Set<FanoutServiceConnection>> getCurrentSubscriptions() {
+ return subscriptions;
+ }
+
+ /**
+ * Returns the subscriptions for the specified channel
+ *
+ * @param channel
+ * @return set of subscribed connections for the specified channel
+ */
+ public Set<FanoutServiceConnection> getCurrentSubscriptions(String channel) {
+ return subscriptions.get(channel);
+ }
+
+ /**
+ * Returns the runtime statistics object for this service.
+ *
+ * @return stats
+ */
+ public FanoutStats getStatistics() {
+ FanoutStats stats = new FanoutStats();
+
+ // settings
+ stats.allowAllChannelAnnouncements = allowAllChannelAnnouncements();
+ stats.concurrentConnectionLimit = getConcurrentConnectionLimit();
+ stats.strictRequestTermination = isStrictRequestTermination();
+
+ // runtime stats
+ stats.bootDate = bootDate;
+ stats.rejectedConnectionCount = rejectedConnectionCount.get();
+ stats.peakConnectionCount = peakConnectionCount.get();
+ stats.totalConnections = totalConnections.get();
+ stats.totalAnnouncements = totalAnnouncements.get();
+ stats.totalMessages = totalMessages.get();
+ stats.totalSubscribes = totalSubscribes.get();
+ stats.totalUnsubscribes = totalUnsubscribes.get();
+ stats.totalPings = totalPings.get();
+ stats.currentConnections = connections.size();
+ stats.currentChannels = subscriptions.size();
+ stats.currentSubscriptions = subscriptions.size() * connections.size();
+ return stats;
+ }
+
+ /**
+ * Returns true if the service is ready.
+ *
+ * @return true, if the service is ready
+ */
+ public boolean isReady() {
+ if (isRunning.get()) {
+ return isConnected();
+ }
+ return false;
+ }
+
+ /**
+ * Start the Fanout service thread and immediatel return.
+ *
+ */
+ public void start() {
+ if (isRunning.get()) {
+ logger.warn(MessageFormat.format("{0} is already running", name));
+ return;
+ }
+ serviceThread = new Thread(this);
+ serviceThread.setName(MessageFormat.format("{0} {1}:{2,number,0}", name, host == null ? "all" : host, port));
+ serviceThread.start();
+ }
+
+ /**
+ * Start the Fanout service thread and wait until it is accepting connections.
+ *
+ */
+ public void startSynchronously() {
+ start();
+ while (!isReady()) {
+ try {
+ Thread.sleep(100);
+ } catch (Exception e) {
+ }
+ }
+ }
+
+ /**
+ * Stop the Fanout service. This method returns when the service has been
+ * completely shutdown.
+ */
+ public void stop() {
+ if (!isRunning.get()) {
+ logger.warn(MessageFormat.format("{0} is not running", name));
+ return;
+ }
+ logger.info(MessageFormat.format("stopping {0}...", name));
+ isRunning.set(false);
+ try {
+ if (serviceThread != null) {
+ serviceThread.join();
+ serviceThread = null;
+ }
+ } catch (InterruptedException e1) {
+ logger.error("", e1);
+ }
+ logger.info(MessageFormat.format("stopped {0}", name));
+ }
+
+ /**
+ * Main execution method of the service
+ */
+ @Override
+ public final void run() {
+ disconnect();
+ resetState();
+ isRunning.set(true);
+ while (isRunning.get()) {
+ if (connect()) {
+ try {
+ listen();
+ } catch (IOException e) {
+ logger.error(MessageFormat.format("error processing {0}", name), e);
+ isRunning.set(false);
+ }
+ } else {
+ try {
+ Thread.sleep(serviceTimeout);
+ } catch (InterruptedException x) {
+ }
+ }
+ }
+ disconnect();
+ resetState();
+ }
+
+ protected void resetState() {
+ // reset state data
+ connections.clear();
+ subscriptions.clear();
+ rejectedConnectionCount.set(0);
+ peakConnectionCount.set(0);
+ totalConnections.set(0);
+ totalAnnouncements.set(0);
+ totalMessages.set(0);
+ totalSubscribes.set(0);
+ totalUnsubscribes.set(0);
+ totalPings.set(0);
+ }
+
+ /**
+ * Configure the client connection socket.
+ *
+ * @param socket
+ * @throws SocketException
+ */
+ protected void configureClientSocket(Socket socket) throws SocketException {
+ socket.setKeepAlive(true);
+ socket.setSoLinger(true, 0); // immediately discard any remaining data
+ }
+
+ /**
+ * Add the connection to the connections map.
+ *
+ * @param connection
+ * @return false if the connection was rejected due to too many concurrent
+ * connections
+ */
+ protected boolean addConnection(FanoutServiceConnection connection) {
+ int limit = getConcurrentConnectionLimit();
+ if (limit > 0 && connections.size() > limit) {
+ logger.info(MessageFormat.format("hit {0,number,0} connection limit, rejecting fanout connection", concurrentConnectionLimit));
+ increment(rejectedConnectionCount);
+ connection.busy();
+ return false;
+ }
+
+ // add the connection to our map
+ connections.put(connection.id, connection);
+
+ // track peak number of concurrent connections
+ if (connections.size() > peakConnectionCount.get()) {
+ peakConnectionCount.set(connections.size());
+ }
+
+ logger.info("fanout new connection " + connection.id);
+ connection.connected();
+ return true;
+ }
+
+ /**
+ * Remove the connection from the connections list and from subscriptions.
+ *
+ * @param connection
+ */
+ protected void removeConnection(FanoutServiceConnection connection) {
+ connections.remove(connection.id);
+ Iterator<Map.Entry<String, Set<FanoutServiceConnection>>> itr = subscriptions.entrySet().iterator();
+ while (itr.hasNext()) {
+ Map.Entry<String, Set<FanoutServiceConnection>> entry = itr.next();
+ Set<FanoutServiceConnection> subscriptions = entry.getValue();
+ subscriptions.remove(connection);
+ if (!FanoutConstants.CH_ALL.equals(entry.getKey())) {
+ if (subscriptions.size() == 0) {
+ itr.remove();
+ logger.info(MessageFormat.format("fanout remove channel {0}, no subscribers", entry.getKey()));
+ }
+ }
+ }
+ logger.info(MessageFormat.format("fanout connection {0} removed", connection.id));
+ }
+
+ /**
+ * Tests to see if the connection is being monitored by the service.
+ *
+ * @param connection
+ * @return true if the service is monitoring the connection
+ */
+ protected boolean hasConnection(FanoutServiceConnection connection) {
+ return connections.containsKey(connection.id);
+ }
+
+ /**
+ * Reply to a connection on the specified channel.
+ *
+ * @param connection
+ * @param channel
+ * @param message
+ * @return the reply
+ */
+ protected String reply(FanoutServiceConnection connection, String channel, String message) {
+ if (channel != null && channel.length() > 0) {
+ increment(totalMessages);
+ }
+ return connection.reply(channel, message);
+ }
+
+ /**
+ * Service method to broadcast a message to all connections.
+ *
+ * @param message
+ */
+ public void broadcastAll(String message) {
+ broadcast(connections.values(), FanoutConstants.CH_ALL, message);
+ increment(totalAnnouncements);
+ }
+
+ /**
+ * Service method to broadcast a message to connections subscribed to the
+ * channel.
+ *
+ * @param message
+ */
+ public void broadcast(String channel, String message) {
+ List<FanoutServiceConnection> connections = new ArrayList<FanoutServiceConnection>(subscriptions.get(channel));
+ broadcast(connections, channel, message);
+ increment(totalAnnouncements);
+ }
+
+ /**
+ * Broadcast a message to connections subscribed to the specified channel.
+ *
+ * @param connections
+ * @param channel
+ * @param message
+ */
+ protected void broadcast(Collection<FanoutServiceConnection> connections, String channel, String message) {
+ for (FanoutServiceConnection connection : connections) {
+ reply(connection, channel, message);
+ }
+ }
+
+ /**
+ * Process an incoming Fanout request.
+ *
+ * @param connection
+ * @param req
+ * @return the reply to the request, may be null
+ */
+ protected String processRequest(FanoutServiceConnection connection, String req) {
+ logger.info(MessageFormat.format("fanout request from {0}: {1}", connection.id, req));
+ String[] fields = req.split(" ", 3);
+ String action = fields[0];
+ String channel = fields.length >= 2 ? fields[1] : null;
+ String message = fields.length >= 3 ? fields[2] : null;
+ try {
+ return processRequest(connection, action, channel, message);
+ } catch (IllegalArgumentException e) {
+ // invalid action
+ logger.error(MessageFormat.format("fanout connection {0} requested invalid action {1}", connection.id, action));
+ logger.error(asHexArray(req));
+ }
+ return null;
+ }
+
+ /**
+ * Process the Fanout request.
+ *
+ * @param connection
+ * @param action
+ * @param channel
+ * @param message
+ * @return the reply to the request, may be null
+ * @throws IllegalArgumentException
+ */
+ protected String processRequest(FanoutServiceConnection connection, String action, String channel, String message) throws IllegalArgumentException {
+ if ("ping".equals(action)) {
+ // ping
+ increment(totalPings);
+ return reply(connection, null, "" + System.currentTimeMillis());
+ } else if ("info".equals(action)) {
+ // info
+ String info = getStatistics().info();
+ return reply(connection, null, info);
+ } else if ("announce".equals(action)) {
+ // announcement
+ if (!allowAllChannelAnnouncements.get() && FanoutConstants.CH_ALL.equals(channel)) {
+ // prohibiting connection-sourced all announcements
+ logger.warn(MessageFormat.format("fanout connection {0} attempted to announce {1} on ALL channel", connection.id, message));
+ } else if ("debug".equals(channel)) {
+ // prohibiting connection-sourced debug announcements
+ logger.warn(MessageFormat.format("fanout connection {0} attempted to announce {1} on DEBUG channel", connection.id, message));
+ } else {
+ // acceptable announcement
+ List<FanoutServiceConnection> connections = new ArrayList<FanoutServiceConnection>(subscriptions.get(channel));
+ connections.remove(connection); // remove announcer
+ broadcast(connections, channel, message);
+ increment(totalAnnouncements);
+ }
+ } else if ("subscribe".equals(action)) {
+ // subscribe
+ if (!subscriptions.containsKey(channel)) {
+ logger.info(MessageFormat.format("fanout new channel {0}", channel));
+ subscriptions.put(channel, new ConcurrentSkipListSet<FanoutServiceConnection>());
+ }
+ subscriptions.get(channel).add(connection);
+ logger.debug(MessageFormat.format("fanout connection {0} subscribed to channel {1}", connection.id, channel));
+ increment(totalSubscribes);
+ } else if ("unsubscribe".equals(action)) {
+ // unsubscribe
+ if (subscriptions.containsKey(channel)) {
+ subscriptions.get(channel).remove(connection);
+ if (subscriptions.get(channel).size() == 0) {
+ subscriptions.remove(channel);
+ }
+ increment(totalUnsubscribes);
+ }
+ } else {
+ // invalid action
+ throw new IllegalArgumentException(action);
+ }
+ return null;
+ }
+
+ private String asHexArray(String req) {
+ StringBuilder sb = new StringBuilder();
+ for (char c : req.toCharArray()) {
+ sb.append(Integer.toHexString(c)).append(' ');
+ }
+ return "[ " + sb.toString().trim() + " ]";
+ }
+
+ /**
+ * Increment a long and prevent negative rollover.
+ *
+ * @param counter
+ */
+ private void increment(AtomicLong counter) {
+ long v = counter.incrementAndGet();
+ if (v < 0) {
+ counter.set(0);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+} \ No newline at end of file
diff --git a/src/main/java/com/gitblit/fanout/FanoutServiceConnection.java b/src/main/java/com/gitblit/fanout/FanoutServiceConnection.java
new file mode 100644
index 00000000..f7f2c959
--- /dev/null
+++ b/src/main/java/com/gitblit/fanout/FanoutServiceConnection.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2013 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.fanout;
+
+import java.io.IOException;
+import java.net.Socket;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * FanoutServiceConnection handles reading/writing messages from a remote fanout
+ * connection.
+ *
+ * @author James Moger
+ *
+ */
+public abstract class FanoutServiceConnection implements Comparable<FanoutServiceConnection> {
+
+ private static final Logger logger = LoggerFactory.getLogger(FanoutServiceConnection.class);
+
+ public final String id;
+
+ protected FanoutServiceConnection(Socket socket) {
+ this.id = FanoutConstants.getRemoteSocketId(socket);
+ }
+
+ protected abstract void reply(String content) throws IOException;
+
+ /**
+ * Send the connection a debug channel connected message.
+ *
+ * @param message
+ */
+ protected void connected() {
+ reply(FanoutConstants.CH_DEBUG, FanoutConstants.MSG_CONNECTED);
+ }
+
+ /**
+ * Send the connection a debug channel busy message.
+ *
+ * @param message
+ */
+ protected void busy() {
+ reply(FanoutConstants.CH_DEBUG, FanoutConstants.MSG_BUSY);
+ }
+
+ /**
+ * Send the connection a message for the specified channel.
+ *
+ * @param channel
+ * @param message
+ * @return the reply
+ */
+ protected String reply(String channel, String message) {
+ String content;
+ if (channel != null) {
+ content = channel + "!" + message;
+ } else {
+ content = message;
+ }
+ try {
+ reply(content);
+ } catch (Exception e) {
+ logger.error("failed to reply to fanout connection " + id, e);
+ }
+ return content;
+ }
+
+ @Override
+ public int compareTo(FanoutServiceConnection c) {
+ return id.compareTo(c.id);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof FanoutServiceConnection) {
+ return id.equals(((FanoutServiceConnection) o).id);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return id.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return id;
+ }
+} \ No newline at end of file
diff --git a/src/main/java/com/gitblit/fanout/FanoutSocketService.java b/src/main/java/com/gitblit/fanout/FanoutSocketService.java
new file mode 100644
index 00000000..07c18f90
--- /dev/null
+++ b/src/main/java/com/gitblit/fanout/FanoutSocketService.java
@@ -0,0 +1,234 @@
+/*
+ * Copyright 2013 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.fanout;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.SocketException;
+import java.net.SocketTimeoutException;
+import java.text.MessageFormat;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A multi-threaded socket implementation of https://github.com/travisghansen/fanout
+ *
+ * This implementation creates a master acceptor thread which accepts incoming
+ * fanout connections and then spawns a daemon thread for each accepted connection.
+ * If there are 100 concurrent fanout connections, there are 101 threads.
+ *
+ * @author James Moger
+ *
+ */
+public class FanoutSocketService extends FanoutService {
+
+ private final static Logger logger = LoggerFactory.getLogger(FanoutSocketService.class);
+
+ private volatile ServerSocket serviceSocket;
+
+ public static void main(String[] args) throws Exception {
+ FanoutSocketService pubsub = new FanoutSocketService(null, DEFAULT_PORT);
+ pubsub.setStrictRequestTermination(false);
+ pubsub.setAllowAllChannelAnnouncements(false);
+ pubsub.start();
+ }
+
+ /**
+ * Create a multi-threaded fanout service.
+ *
+ * @param port
+ * the port for running the fanout PubSub service
+ * @throws IOException
+ */
+ public FanoutSocketService(int port) {
+ this(null, port);
+ }
+
+ /**
+ * Create a multi-threaded fanout service.
+ *
+ * @param bindInterface
+ * the ip address to bind for the service, may be null
+ * @param port
+ * the port for running the fanout PubSub service
+ * @throws IOException
+ */
+ public FanoutSocketService(String bindInterface, int port) {
+ super(bindInterface, port, "Fanout socket service");
+ }
+
+ @Override
+ protected boolean isConnected() {
+ return serviceSocket != null;
+ }
+
+ @Override
+ protected boolean connect() {
+ if (serviceSocket == null) {
+ try {
+ serviceSocket = new ServerSocket();
+ serviceSocket.setReuseAddress(true);
+ serviceSocket.setSoTimeout(serviceTimeout);
+ serviceSocket.bind(host == null ? new InetSocketAddress(port) : new InetSocketAddress(host, port));
+ logger.info(MessageFormat.format("{0} is ready on {1}:{2,number,0}",
+ name, host == null ? "0.0.0.0" : host, serviceSocket.getLocalPort()));
+ } catch (IOException e) {
+ logger.error(MessageFormat.format("failed to open {0} on {1}:{2,number,0}",
+ name, host == null ? "0.0.0.0" : host, port), e);
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ protected void disconnect() {
+ try {
+ if (serviceSocket != null) {
+ logger.debug(MessageFormat.format("closing {0} server socket", name));
+ serviceSocket.close();
+ serviceSocket = null;
+ }
+ } catch (IOException e) {
+ logger.error(MessageFormat.format("failed to disconnect {0}", name), e);
+ }
+ }
+
+ /**
+ * This accepts incoming fanout connections and spawns connection threads.
+ */
+ @Override
+ protected void listen() throws IOException {
+ try {
+ Socket socket;
+ socket = serviceSocket.accept();
+ configureClientSocket(socket);
+
+ FanoutSocketConnection connection = new FanoutSocketConnection(socket);
+
+ if (addConnection(connection)) {
+ // spawn connection daemon thread
+ Thread connectionThread = new Thread(connection);
+ connectionThread.setDaemon(true);
+ connectionThread.setName("Fanout " + connection.id);
+ connectionThread.start();
+ } else {
+ // synchronously close the connection and remove it
+ removeConnection(connection);
+ connection.closeConnection();
+ connection = null;
+ }
+ } catch (SocketTimeoutException e) {
+ // ignore accept timeout exceptions
+ }
+ }
+
+ /**
+ * FanoutSocketConnection handles reading/writing messages from a remote fanout
+ * connection.
+ *
+ * @author James Moger
+ *
+ */
+ class FanoutSocketConnection extends FanoutServiceConnection implements Runnable {
+ Socket socket;
+
+ FanoutSocketConnection(Socket socket) {
+ super(socket);
+ this.socket = socket;
+ }
+
+ /**
+ * Connection thread read/write method.
+ */
+ @Override
+ public void run() {
+ try {
+ StringBuilder sb = new StringBuilder();
+ BufferedInputStream is = new BufferedInputStream(socket.getInputStream());
+ byte[] buffer = new byte[FanoutConstants.BUFFER_LENGTH];
+ int len = 0;
+ while (true) {
+ while (is.available() > 0) {
+ len = is.read(buffer);
+ for (int i = 0; i < len; i++) {
+ byte b = buffer[i];
+ if (b == 0xa || (!isStrictRequestTermination() && b == 0xd)) {
+ String req = sb.toString();
+ sb.setLength(0);
+ if (req.length() > 0) {
+ // ignore empty request strings
+ processRequest(this, req);
+ }
+ } else {
+ sb.append((char) b);
+ }
+ }
+ }
+
+ if (!isRunning.get()) {
+ // service has stopped, terminate client connection
+ break;
+ } else {
+ Thread.sleep(500);
+ }
+ }
+ } catch (Throwable t) {
+ if (t instanceof SocketException) {
+ logger.error(MessageFormat.format("fanout connection {0}: {1}", id, t.getMessage()));
+ } else if (t instanceof SocketTimeoutException) {
+ logger.error(MessageFormat.format("fanout connection {0}: {1}", id, t.getMessage()));
+ } else {
+ logger.error(MessageFormat.format("exception while handling fanout connection {0}", id), t);
+ }
+ } finally {
+ closeConnection();
+ }
+
+ logger.info(MessageFormat.format("thread for fanout connection {0} is finished", id));
+ }
+
+ @Override
+ protected void reply(String content) throws IOException {
+ // synchronously send reply
+ logger.debug(MessageFormat.format("fanout reply to {0}: {1}", id, content));
+ OutputStream os = socket.getOutputStream();
+ byte [] bytes = content.getBytes(FanoutConstants.CHARSET);
+ os.write(bytes);
+ if (bytes[bytes.length - 1] != 0xa) {
+ os.write(0xa);
+ }
+ os.flush();
+ }
+
+ protected void closeConnection() {
+ // close the connection socket
+ try {
+ socket.close();
+ } catch (IOException e) {
+ }
+ socket = null;
+
+ // remove this connection from the service
+ removeConnection(this);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main/java/com/gitblit/fanout/FanoutStats.java b/src/main/java/com/gitblit/fanout/FanoutStats.java
new file mode 100644
index 00000000..b06884d3
--- /dev/null
+++ b/src/main/java/com/gitblit/fanout/FanoutStats.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2013 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.fanout;
+
+import java.io.Serializable;
+import java.text.MessageFormat;
+import java.util.Date;
+
+/**
+ * Encapsulates the runtime stats of a fanout service.
+ *
+ * @author James Moger
+ *
+ */
+public class FanoutStats implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ public long concurrentConnectionLimit;
+ public boolean allowAllChannelAnnouncements;
+ public boolean strictRequestTermination;
+
+ public Date bootDate;
+ public long rejectedConnectionCount;
+ public int peakConnectionCount;
+ public long currentChannels;
+ public long currentSubscriptions;
+ public long currentConnections;
+ public long totalConnections;
+ public long totalAnnouncements;
+ public long totalMessages;
+ public long totalSubscribes;
+ public long totalUnsubscribes;
+ public long totalPings;
+
+ public String info() {
+ int i = 0;
+ StringBuilder sb = new StringBuilder();
+ sb.append(infoStr(i++, "boot date"));
+ sb.append(infoStr(i++, "strict request termination"));
+ sb.append(infoStr(i++, "allow connection \"all\" announcements"));
+ sb.append(infoInt(i++, "concurrent connection limit"));
+ sb.append(infoInt(i++, "concurrent limit rejected connections"));
+ sb.append(infoInt(i++, "peak connections"));
+ sb.append(infoInt(i++, "current connections"));
+ sb.append(infoInt(i++, "current channels"));
+ sb.append(infoInt(i++, "current subscriptions"));
+ sb.append(infoInt(i++, "user-requested subscriptions"));
+ sb.append(infoInt(i++, "total connections"));
+ sb.append(infoInt(i++, "total announcements"));
+ sb.append(infoInt(i++, "total messages"));
+ sb.append(infoInt(i++, "total subscribes"));
+ sb.append(infoInt(i++, "total unsubscribes"));
+ sb.append(infoInt(i++, "total pings"));
+ String template = sb.toString();
+
+ String info = MessageFormat.format(template,
+ bootDate.toString(),
+ Boolean.toString(strictRequestTermination),
+ Boolean.toString(allowAllChannelAnnouncements),
+ concurrentConnectionLimit,
+ rejectedConnectionCount,
+ peakConnectionCount,
+ currentConnections,
+ currentChannels,
+ currentSubscriptions,
+ currentSubscriptions == 0 ? 0 : (currentSubscriptions - currentConnections),
+ totalConnections,
+ totalAnnouncements,
+ totalMessages,
+ totalSubscribes,
+ totalUnsubscribes,
+ totalPings);
+ return info;
+ }
+
+ private String infoStr(int index, String label) {
+ return label + ": {" + index + "}\n";
+ }
+
+ private String infoInt(int index, String label) {
+ return label + ": {" + index + ",number,0}\n";
+ }
+
+}
diff --git a/src/main/java/com/gitblit/models/Activity.java b/src/main/java/com/gitblit/models/Activity.java
new file mode 100644
index 00000000..59405c7f
--- /dev/null
+++ b/src/main/java/com/gitblit/models/Activity.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.models;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.eclipse.jgit.revwalk.RevCommit;
+
+import com.gitblit.utils.StringUtils;
+import com.gitblit.utils.TimeUtils;
+
+/**
+ * Model class to represent the commit activity across many repositories. This
+ * class is used by the Activity page.
+ *
+ * @author James Moger
+ */
+public class Activity implements Serializable, Comparable<Activity> {
+
+ private static final long serialVersionUID = 1L;
+
+ public final Date startDate;
+
+ public final Date endDate;
+
+ private final Set<RepositoryCommit> commits;
+
+ private final Map<String, Metric> authorMetrics;
+
+ private final Map<String, Metric> repositoryMetrics;
+
+ /**
+ * Constructor for one day of activity.
+ *
+ * @param date
+ */
+ public Activity(Date date) {
+ this(date, TimeUtils.ONEDAY - 1);
+ }
+
+ /**
+ * Constructor for specified duration of activity from start date.
+ *
+ * @param date
+ * the start date of the activity
+ * @param duration
+ * the duration of the period in milliseconds
+ */
+ public Activity(Date date, long duration) {
+ startDate = date;
+ endDate = new Date(date.getTime() + duration);
+ commits = new LinkedHashSet<RepositoryCommit>();
+ authorMetrics = new HashMap<String, Metric>();
+ repositoryMetrics = new HashMap<String, Metric>();
+ }
+
+ /**
+ * Adds a commit to the activity object as long as the commit is not a
+ * duplicate.
+ *
+ * @param repository
+ * @param branch
+ * @param commit
+ * @return a RepositoryCommit, if one was added. Null if this is duplicate
+ * commit
+ */
+ public RepositoryCommit addCommit(String repository, String branch, RevCommit commit) {
+ RepositoryCommit commitModel = new RepositoryCommit(repository, branch, commit);
+ if (commits.add(commitModel)) {
+ if (!repositoryMetrics.containsKey(repository)) {
+ repositoryMetrics.put(repository, new Metric(repository));
+ }
+ repositoryMetrics.get(repository).count++;
+
+ String author = StringUtils.removeNewlines(commit.getAuthorIdent().getEmailAddress()).toLowerCase();
+ if (!authorMetrics.containsKey(author)) {
+ authorMetrics.put(author, new Metric(author));
+ }
+ authorMetrics.get(author).count++;
+ return commitModel;
+ }
+ return null;
+ }
+
+ public int getCommitCount() {
+ return commits.size();
+ }
+
+ public List<RepositoryCommit> getCommits() {
+ List<RepositoryCommit> list = new ArrayList<RepositoryCommit>(commits);
+ Collections.sort(list);
+ return list;
+ }
+
+ public Map<String, Metric> getAuthorMetrics() {
+ return authorMetrics;
+ }
+
+ public Map<String, Metric> getRepositoryMetrics() {
+ return repositoryMetrics;
+ }
+
+ @Override
+ public int compareTo(Activity o) {
+ // reverse chronological order
+ return o.startDate.compareTo(startDate);
+ }
+}
diff --git a/src/main/java/com/gitblit/models/AnnotatedLine.java b/src/main/java/com/gitblit/models/AnnotatedLine.java
new file mode 100644
index 00000000..69b55bcd
--- /dev/null
+++ b/src/main/java/com/gitblit/models/AnnotatedLine.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.models;
+
+import java.io.Serializable;
+import java.util.Date;
+
+import org.eclipse.jgit.revwalk.RevCommit;
+
+/**
+ * AnnotatedLine is a serializable model class that represents a the most recent
+ * author, date, and commit id of a line in a source file.
+ *
+ * @author James Moger
+ *
+ */
+public class AnnotatedLine implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ public final String commitId;
+ public final String author;
+ public final Date when;
+ public final int lineNumber;
+ public final String data;
+
+ public AnnotatedLine(RevCommit commit, int lineNumber, String data) {
+ this.commitId = commit.getName();
+ this.author = commit.getAuthorIdent().getName();
+ this.when = commit.getAuthorIdent().getWhen();
+ this.lineNumber = lineNumber;
+ this.data = data;
+ }
+} \ No newline at end of file
diff --git a/src/main/java/com/gitblit/models/FederationModel.java b/src/main/java/com/gitblit/models/FederationModel.java
new file mode 100644
index 00000000..1d211ce9
--- /dev/null
+++ b/src/main/java/com/gitblit/models/FederationModel.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.models;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import com.gitblit.Constants.FederationPullStatus;
+import com.gitblit.utils.StringUtils;
+
+/**
+ * Represents a federated server registration. Gitblit federation allows one
+ * Gitblit instance to pull the repositories and configuration from another
+ * Gitblit instance. This is a backup operation and can be considered something
+ * like svn-sync.
+ *
+ */
+public class FederationModel implements Serializable, Comparable<FederationModel> {
+
+ private static final long serialVersionUID = 1L;
+
+ public String name;
+
+ public String url;
+
+ public String token;
+
+ public String frequency;
+
+ public String folder;
+
+ public boolean bare;
+
+ public boolean mirror;
+
+ public boolean mergeAccounts;
+
+ public boolean sendStatus;
+
+ public boolean notifyOnError;
+
+ public List<String> exclusions = new ArrayList<String>();
+
+ public List<String> inclusions = new ArrayList<String>();
+
+ public Date lastPull;
+
+ public Date nextPull;
+
+ private Map<String, FederationPullStatus> results = new ConcurrentHashMap<String, FederationPullStatus>();
+
+ /**
+ * The constructor for a remote server configuration.
+ *
+ * @param serverName
+ */
+ public FederationModel(String serverName) {
+ this.name = serverName;
+ bare = true;
+ mirror = true;
+ this.lastPull = new Date(0);
+ this.nextPull = new Date(0);
+ }
+
+ public boolean isIncluded(RepositoryModel repository) {
+ // if exclusions has the all wildcard, then check for specific
+ // inclusions
+ if (exclusions.contains("*")) {
+ for (String name : inclusions) {
+ if (StringUtils.fuzzyMatch(repository.name, name)) {
+ results.put(repository.name, FederationPullStatus.PENDING);
+ return true;
+ }
+ }
+ results.put(repository.name, FederationPullStatus.EXCLUDED);
+ return false;
+ }
+
+ // named exclusions
+ for (String name : exclusions) {
+ if (StringUtils.fuzzyMatch(repository.name, name)) {
+ results.put(repository.name, FederationPullStatus.EXCLUDED);
+ return false;
+ }
+ }
+
+ // included by default
+ results.put(repository.name, FederationPullStatus.PENDING);
+ return true;
+ }
+
+ /**
+ * Updates the pull status of a particular repository in this federation
+ * registration.
+ *
+ * @param repository
+ * @param status
+ */
+ public void updateStatus(RepositoryModel repository, FederationPullStatus status) {
+ if (!results.containsKey(repository.name)) {
+ results.put(repository.name, FederationPullStatus.PENDING);
+ }
+ if (status != null) {
+ results.put(repository.name, status);
+ }
+ }
+
+ public List<RepositoryStatus> getStatusList() {
+ List<RepositoryStatus> list = new ArrayList<RepositoryStatus>();
+ for (Map.Entry<String, FederationPullStatus> entry : results.entrySet()) {
+ list.add(new RepositoryStatus(entry.getKey(), entry.getValue()));
+ }
+ return list;
+ }
+
+ /**
+ * Iterates over the current pull results and returns the lowest pull
+ * status.
+ *
+ * @return the lowest pull status of the registration
+ */
+ public FederationPullStatus getLowestStatus() {
+ if (results.size() == 0) {
+ return FederationPullStatus.PENDING;
+ }
+ FederationPullStatus status = FederationPullStatus.MIRRORED;
+ for (FederationPullStatus result : results.values()) {
+ if (result.ordinal() < status.ordinal()) {
+ status = result;
+ }
+ }
+ return status;
+ }
+
+ /**
+ * Returns true if this registration represents the result data sent by a
+ * pulling Gitblit instance.
+ *
+ * @return true, if this is result data
+ */
+ public boolean isResultData() {
+ return !url.toLowerCase().startsWith("http://") && !url.toLowerCase().startsWith("https://");
+ }
+
+ @Override
+ public String toString() {
+ return "Federated " + name + " (" + url + ")";
+ }
+
+ @Override
+ public int compareTo(FederationModel o) {
+ boolean r1 = isResultData();
+ boolean r2 = o.isResultData();
+ if ((r1 && r2) || (!r1 && !r2)) {
+ // sort registrations and results by name
+ return name.compareTo(o.name);
+ }
+ // sort registrations first
+ if (r1) {
+ return 1;
+ }
+ return -1;
+ }
+
+ /**
+ * Class that encapsulates a point-in-time pull result.
+ *
+ */
+ public static class RepositoryStatus implements Serializable, Comparable<RepositoryStatus> {
+
+ private static final long serialVersionUID = 1L;
+
+ public final String name;
+ public final FederationPullStatus status;
+
+ RepositoryStatus(String name, FederationPullStatus status) {
+ this.name = name;
+ this.status = status;
+ }
+
+ @Override
+ public int compareTo(RepositoryStatus o) {
+ if (status.equals(o.status)) {
+ return StringUtils.compareRepositoryNames(name, o.name);
+ }
+ return status.compareTo(o.status);
+ }
+ }
+}
diff --git a/src/main/java/com/gitblit/models/FederationProposal.java b/src/main/java/com/gitblit/models/FederationProposal.java
new file mode 100644
index 00000000..5cf9182c
--- /dev/null
+++ b/src/main/java/com/gitblit/models/FederationProposal.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.models;
+
+import java.io.Serializable;
+import java.util.Date;
+import java.util.Map;
+
+import com.gitblit.Constants.FederationToken;
+
+/**
+ * Represents a proposal from a Gitblit instance to pull its repositories.
+ */
+public class FederationProposal implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ public Date received;
+
+ public String name;
+
+ public String url;
+
+ public FederationToken tokenType;
+
+ public String token;
+
+ public String message;
+
+ public Map<String, RepositoryModel> repositories;
+
+ /**
+ * The constructor for a federation proposal.
+ *
+ * @param url
+ * the url of the source Gitblit instance
+ * @param tokenType
+ * the type of token from the source Gitblit instance
+ * @param token
+ * the federation token from the source Gitblit instance
+ * @param repositories
+ * the map of repositories to be pulled from the source Gitblit
+ * instance keyed by the repository clone url
+ */
+ public FederationProposal(String url, FederationToken tokenType, String token,
+ Map<String, RepositoryModel> repositories) {
+ this.received = new Date();
+ this.url = url;
+ this.tokenType = tokenType;
+ this.token = token;
+ this.message = "";
+ this.repositories = repositories;
+ try {
+ // determine server name and set that as the proposal name
+ name = url.substring(url.indexOf("//") + 2);
+ if (name.contains("/")) {
+ name = name.substring(0, name.indexOf('/'));
+ }
+ name = name.replace(".", "").replace(";", "").replace(":", "").replace("-", "");
+ } catch (Exception e) {
+ name = Long.toHexString(System.currentTimeMillis());
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "Federation Proposal (" + url + ")";
+ }
+}
diff --git a/src/main/java/com/gitblit/models/FederationSet.java b/src/main/java/com/gitblit/models/FederationSet.java
new file mode 100644
index 00000000..357689c9
--- /dev/null
+++ b/src/main/java/com/gitblit/models/FederationSet.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.models;
+
+import java.io.Serializable;
+import java.util.Map;
+
+import com.gitblit.Constants.FederationToken;
+
+/**
+ * Represents a group of repositories.
+ */
+public class FederationSet implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ public String name;
+
+ public String token;
+
+ public FederationToken tokenType;
+
+ public Map<String, RepositoryModel> repositories;
+
+ /**
+ * The constructor for a federation set.
+ *
+ * @param name
+ * the name of this federation set
+ * @param tokenType
+ * the type of token of this federation set
+ * @param token
+ * the federation token
+ */
+ public FederationSet(String name, FederationToken tokenType, String token) {
+ this.name = name;
+ this.tokenType = tokenType;
+ this.token = token;
+ }
+
+ @Override
+ public String toString() {
+ return "Federation Set (" + name + ")";
+ }
+}
diff --git a/src/main/java/com/gitblit/models/FeedEntryModel.java b/src/main/java/com/gitblit/models/FeedEntryModel.java
new file mode 100644
index 00000000..e1c00c38
--- /dev/null
+++ b/src/main/java/com/gitblit/models/FeedEntryModel.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.models;
+
+import java.io.Serializable;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * FeedEntryModel represents an entry in a syndication (RSS) feed.
+ *
+ * @author James Moger
+ */
+public class FeedEntryModel implements Serializable, Comparable<FeedEntryModel> {
+
+ public String repository;
+ public String branch;
+ public String title;
+ public String author;
+ public Date published;
+ public String link;
+ public String content;
+ public String contentType;
+ public List<String> tags;
+
+ private static final long serialVersionUID = 1L;
+
+ public FeedEntryModel() {
+ }
+
+ @Override
+ public int compareTo(FeedEntryModel o) {
+ return o.published.compareTo(published);
+ }
+
+ @Override
+ public int hashCode() {
+ return link.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof FeedEntryModel) {
+ return hashCode() == o.hashCode();
+ }
+ return false;
+ }
+}
diff --git a/src/main/java/com/gitblit/models/FeedModel.java b/src/main/java/com/gitblit/models/FeedModel.java
new file mode 100644
index 00000000..08f9e48e
--- /dev/null
+++ b/src/main/java/com/gitblit/models/FeedModel.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.models;
+
+import java.io.Serializable;
+import java.util.Date;
+
+import com.gitblit.utils.StringUtils;
+
+/**
+ * FeedModel represents a syndication (RSS) feed.
+ *
+ * @author James Moger
+ */
+public class FeedModel implements Serializable, Comparable<FeedModel> {
+
+ public String repository;
+ public String branch;
+ public Date lastRefreshDate;
+ public Date currentRefreshDate;
+
+ public boolean subscribed;
+
+ private static final long serialVersionUID = 1L;
+
+ public FeedModel() {
+ this("");
+ subscribed = false;
+ }
+
+ public FeedModel(String definition) {
+ subscribed = true;
+ lastRefreshDate = new Date(0);
+ currentRefreshDate = new Date(0);
+
+ String[] fields = definition.split(":");
+ repository = fields[0];
+ if (fields.length > 1) {
+ branch = fields[1];
+ }
+ }
+
+ @Override
+ public String toString() {
+ if (StringUtils.isEmpty(branch)) {
+ return repository;
+ }
+ return repository + ":" + branch;
+ }
+
+ @Override
+ public int compareTo(FeedModel o) {
+ int repositoryCompare = StringUtils.compareRepositoryNames(repository, o.repository);
+ if (repositoryCompare == 0) {
+ // same repository
+ if (StringUtils.isEmpty(branch)) {
+ return 1;
+ } else if (StringUtils.isEmpty(o.branch)) {
+ return -1;
+ }
+ return branch.compareTo(o.branch);
+ }
+ return repositoryCompare;
+ }
+
+ @Override
+ public int hashCode() {
+ return toString().toLowerCase().hashCode();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof FeedModel) {
+ return hashCode() == o.hashCode();
+ }
+ return false;
+ }
+}
diff --git a/src/main/java/com/gitblit/models/ForkModel.java b/src/main/java/com/gitblit/models/ForkModel.java
new file mode 100644
index 00000000..849986c1
--- /dev/null
+++ b/src/main/java/com/gitblit/models/ForkModel.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2012 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.models;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+import com.gitblit.utils.ArrayUtils;
+import com.gitblit.utils.StringUtils;
+
+/**
+ * A ForkModel represents a repository, its direct descendants, and its origin.
+ *
+ * @author James Moger
+ *
+ */
+public class ForkModel implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ public final RepositoryModel repository;
+
+ public final List<ForkModel> forks;
+
+ public ForkModel(RepositoryModel repository) {
+ this.repository = repository;
+ this.forks = new ArrayList<ForkModel>();
+ }
+
+ public boolean isRoot() {
+ return StringUtils.isEmpty(repository.originRepository);
+ }
+
+ public boolean isNode() {
+ return !ArrayUtils.isEmpty(forks);
+ }
+
+ public boolean isLeaf() {
+ return ArrayUtils.isEmpty(forks);
+ }
+
+ public boolean isPersonalRepository() {
+ return repository.isPersonalRepository();
+ }
+
+ @Override
+ public int hashCode() {
+ return repository.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof ForkModel) {
+ return repository.equals(((ForkModel) o).repository);
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return repository.toString();
+ }
+}
diff --git a/src/main/java/com/gitblit/models/GitNote.java b/src/main/java/com/gitblit/models/GitNote.java
new file mode 100644
index 00000000..c333a881
--- /dev/null
+++ b/src/main/java/com/gitblit/models/GitNote.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.models;
+
+import java.io.Serializable;
+
+/**
+ * GitNote is a serializable model class that represents a git note. This class
+ * retains an instance of the RefModel which contains the commit in which this
+ * git note was created.
+ *
+ * @author James Moger
+ *
+ */
+public class GitNote implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ public final String content;
+ public final RefModel notesRef;
+
+ public GitNote(RefModel notesRef, String text) {
+ this.notesRef = notesRef;
+ this.content = text;
+ }
+} \ No newline at end of file
diff --git a/src/main/java/com/gitblit/models/GravatarProfile.java b/src/main/java/com/gitblit/models/GravatarProfile.java
new file mode 100644
index 00000000..aa128ce0
--- /dev/null
+++ b/src/main/java/com/gitblit/models/GravatarProfile.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.models;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * Represents a Gravatar profile.
+ *
+ * @author James Moger
+ *
+ */
+public class GravatarProfile implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ public String id;
+ public String hash;
+ public String requestHash;
+ public String displayName;
+ public String preferredUsername;
+ public String currentLocation;
+ public String aboutMe;
+
+ public String profileUrl;
+ public String thumbnailUrl;
+ public List<ProfileObject> photos;
+// public Map<String, String> profileBackground;
+// public Map<String, String> name;
+
+ public List<ProfileObject> phoneNumbers;
+ public List<ProfileObject> emails;
+ public List<ProfileObject> ims;
+ public List<Account> accounts;
+ public List<ProfileObject> urls;
+
+ public static class ProfileObject implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ public String title;
+ public String type;
+ public String value;
+ public boolean primary;
+
+ @Override
+ public String toString() {
+ return value;
+ }
+ }
+
+ public static class Account implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ public String domain;
+ public String display;
+ public String url;
+ public String username;
+ public String userid;
+ public boolean verified;
+ public String shortname;
+
+ @Override
+ public String toString() {
+ return display;
+ }
+ }
+}
diff --git a/src/main/java/com/gitblit/models/IssueModel.java b/src/main/java/com/gitblit/models/IssueModel.java
new file mode 100644
index 00000000..c9038913
--- /dev/null
+++ b/src/main/java/com/gitblit/models/IssueModel.java
@@ -0,0 +1,532 @@
+/*
+ * Copyright 2012 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.models;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+import com.gitblit.utils.ArrayUtils;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.utils.TimeUtils;
+
+/**
+ * The Gitblit Issue model, its component classes, and enums.
+ *
+ * @author James Moger
+ *
+ */
+public class IssueModel implements Serializable, Comparable<IssueModel> {
+
+ private static final long serialVersionUID = 1L;
+
+ public String id;
+
+ public Type type;
+
+ public Status status;
+
+ public Priority priority;
+
+ public Date created;
+
+ public String summary;
+
+ public String description;
+
+ public String reporter;
+
+ public String owner;
+
+ public String milestone;
+
+ public List<Change> changes;
+
+ public IssueModel() {
+ // the first applied change set the date appropriately
+ created = new Date(0);
+
+ type = Type.Defect;
+ status = Status.New;
+ priority = Priority.Medium;
+
+ changes = new ArrayList<Change>();
+ }
+
+ public String getStatus() {
+ String s = status.toString();
+ if (!StringUtils.isEmpty(owner))
+ s += " (" + owner + ")";
+ return s;
+ }
+
+ public boolean hasLabel(String label) {
+ return getLabels().contains(label);
+ }
+
+ public List<String> getLabels() {
+ List<String> list = new ArrayList<String>();
+ String labels = null;
+ for (Change change : changes) {
+ if (change.hasField(Field.Labels)) {
+ labels = change.getString(Field.Labels);
+ }
+ }
+ if (!StringUtils.isEmpty(labels)) {
+ list.addAll(StringUtils.getStringsFromValue(labels, " "));
+ }
+ return list;
+ }
+
+ public Attachment getAttachment(String name) {
+ Attachment attachment = null;
+ for (Change change : changes) {
+ if (change.hasAttachments()) {
+ Attachment a = change.getAttachment(name);
+ if (a != null) {
+ attachment = a;
+ }
+ }
+ }
+ return attachment;
+ }
+
+ public List<Attachment> getAttachments() {
+ List<Attachment> list = new ArrayList<Attachment>();
+ for (Change change : changes) {
+ if (change.hasAttachments()) {
+ list.addAll(change.attachments);
+ }
+ }
+ return list;
+ }
+
+ public void applyChange(Change change) {
+ if (changes.size() == 0) {
+ // first change created the issue
+ created = change.created;
+ }
+ changes.add(change);
+
+ if (change.hasFieldChanges()) {
+ for (FieldChange fieldChange : change.fieldChanges) {
+ switch (fieldChange.field) {
+ case Id:
+ id = fieldChange.value.toString();
+ break;
+ case Type:
+ type = IssueModel.Type.fromObject(fieldChange.value);
+ break;
+ case Status:
+ status = IssueModel.Status.fromObject(fieldChange.value);
+ break;
+ case Priority:
+ priority = IssueModel.Priority.fromObject(fieldChange.value);
+ break;
+ case Summary:
+ summary = fieldChange.value.toString();
+ break;
+ case Description:
+ description = fieldChange.value.toString();
+ break;
+ case Reporter:
+ reporter = fieldChange.value.toString();
+ break;
+ case Owner:
+ owner = fieldChange.value.toString();
+ break;
+ case Milestone:
+ milestone = fieldChange.value.toString();
+ break;
+ }
+ }
+ }
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("issue ");
+ sb.append(id.substring(0, 8));
+ sb.append(" (" + summary + ")\n");
+ for (Change change : changes) {
+ sb.append(change);
+ sb.append('\n');
+ }
+ return sb.toString();
+ }
+
+ @Override
+ public int compareTo(IssueModel o) {
+ return o.created.compareTo(created);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof IssueModel)
+ return id.equals(((IssueModel) o).id);
+ return super.equals(o);
+ }
+
+ @Override
+ public int hashCode() {
+ return id.hashCode();
+ }
+
+ public static class Change implements Serializable, Comparable<Change> {
+
+ private static final long serialVersionUID = 1L;
+
+ public final Date created;
+
+ public final String author;
+
+ public String id;
+
+ public char code;
+
+ public Comment comment;
+
+ public Set<FieldChange> fieldChanges;
+
+ public Set<Attachment> attachments;
+
+ public Change(String author) {
+ this.created = new Date((System.currentTimeMillis() / 1000) * 1000);
+ this.author = author;
+ this.id = StringUtils.getSHA1(created.toString() + author);
+ }
+
+ public boolean hasComment() {
+ return comment != null && !comment.deleted;
+ }
+
+ public void comment(String text) {
+ comment = new Comment(text);
+ comment.id = StringUtils.getSHA1(created.toString() + author + text);
+ }
+
+ public boolean hasAttachments() {
+ return !ArrayUtils.isEmpty(attachments);
+ }
+
+ public void addAttachment(Attachment attachment) {
+ if (attachments == null) {
+ attachments = new LinkedHashSet<Attachment>();
+ }
+ attachments.add(attachment);
+ }
+
+ public Attachment getAttachment(String name) {
+ for (Attachment attachment : attachments) {
+ if (attachment.name.equalsIgnoreCase(name)) {
+ return attachment;
+ }
+ }
+ return null;
+ }
+
+ public boolean hasField(Field field) {
+ return !StringUtils.isEmpty(getString(field));
+ }
+
+ public boolean hasFieldChanges() {
+ return !ArrayUtils.isEmpty(fieldChanges);
+ }
+
+ public Object getField(Field field) {
+ if (fieldChanges != null) {
+ for (FieldChange fieldChange : fieldChanges) {
+ if (fieldChange.field == field) {
+ return fieldChange.value;
+ }
+ }
+ }
+ return null;
+ }
+
+ public void setField(Field field, Object value) {
+ FieldChange fieldChange = new FieldChange(field, value);
+ if (fieldChanges == null) {
+ fieldChanges = new LinkedHashSet<FieldChange>();
+ }
+ fieldChanges.add(fieldChange);
+ }
+
+ public String getString(Field field) {
+ Object value = getField(field);
+ if (value == null) {
+ return null;
+ }
+ return value.toString();
+ }
+
+ @Override
+ public int compareTo(Change c) {
+ return created.compareTo(c.created);
+ }
+
+ @Override
+ public int hashCode() {
+ return id.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof Change) {
+ return id.equals(((Change) o).id);
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append(new TimeUtils().timeAgo(created));
+ switch (code) {
+ case '+':
+ sb.append(" created by ");
+ break;
+ default:
+ if (hasComment()) {
+ sb.append(" commented on by ");
+ } else {
+ sb.append(" changed by ");
+ }
+ }
+ sb.append(author).append(" - ");
+ if (hasComment()) {
+ if (comment.deleted) {
+ sb.append("(deleted) ");
+ }
+ sb.append(comment.text).append(" ");
+ }
+ if (hasFieldChanges()) {
+ switch (code) {
+ case '+':
+ break;
+ default:
+ for (FieldChange fieldChange : fieldChanges) {
+ sb.append("\n ");
+ sb.append(fieldChange);
+ }
+ break;
+ }
+ }
+ return sb.toString();
+ }
+ }
+
+ public static class Comment implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ public String text;
+
+ public String id;
+
+ public boolean deleted;
+
+ Comment(String text) {
+ this.text = text;
+ }
+
+ @Override
+ public String toString() {
+ return text;
+ }
+ }
+
+ public static class FieldChange implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ public final Field field;
+
+ public final Object value;
+
+ FieldChange(Field field, Object value) {
+ this.field = field;
+ this.value = value;
+ }
+
+ @Override
+ public int hashCode() {
+ return field.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof FieldChange) {
+ return field.equals(((FieldChange) o).field);
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return field + ": " + value;
+ }
+ }
+
+ public static class Attachment implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ public final String name;
+ public String id;
+ public long size;
+ public byte[] content;
+ public boolean deleted;
+
+ public Attachment(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public int hashCode() {
+ return name.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof Attachment) {
+ return name.equalsIgnoreCase(((Attachment) o).name);
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+ }
+
+ public static enum Field {
+ Id, Summary, Description, Reporter, Owner, Type, Status, Priority, Milestone, Component, Labels;
+ }
+
+ public static enum Type {
+ Defect, Enhancement, Task, Review, Other;
+
+ public static Type fromObject(Object o) {
+ if (o instanceof Type) {
+ // cast and return
+ return (Type) o;
+ } else if (o instanceof String) {
+ // find by name
+ for (Type type : values()) {
+ String str = o.toString();
+ if (type.toString().equalsIgnoreCase(str)) {
+ return type;
+ }
+ }
+ } else if (o instanceof Number) {
+ // by ordinal
+ int id = ((Number) o).intValue();
+ if (id >= 0 && id < values().length) {
+ return values()[id];
+ }
+ }
+ return null;
+ }
+ }
+
+ public static enum Priority {
+ Low, Medium, High, Critical;
+
+ public static Priority fromObject(Object o) {
+ if (o instanceof Priority) {
+ // cast and return
+ return (Priority) o;
+ } else if (o instanceof String) {
+ // find by name
+ for (Priority priority : values()) {
+ String str = o.toString();
+ if (priority.toString().equalsIgnoreCase(str)) {
+ return priority;
+ }
+ }
+ } else if (o instanceof Number) {
+ // by ordinal
+ int id = ((Number) o).intValue();
+ if (id >= 0 && id < values().length) {
+ return values()[id];
+ }
+ }
+ return null;
+ }
+ }
+
+ public static enum Status {
+ New, Accepted, Started, Review, Queued, Testing, Done, Fixed, WontFix, Duplicate, Invalid;
+
+ public static Status fromObject(Object o) {
+ if (o instanceof Status) {
+ // cast and return
+ return (Status) o;
+ } else if (o instanceof String) {
+ // find by name
+ for (Status status : values()) {
+ String str = o.toString();
+ if (status.toString().equalsIgnoreCase(str)) {
+ return status;
+ }
+ }
+ } else if (o instanceof Number) {
+ // by ordinal
+ int id = ((Number) o).intValue();
+ if (id >= 0 && id < values().length) {
+ return values()[id];
+ }
+ }
+ return null;
+ }
+
+ public boolean atLeast(Status status) {
+ return ordinal() >= status.ordinal();
+ }
+
+ public boolean exceeds(Status status) {
+ return ordinal() > status.ordinal();
+ }
+
+ public boolean isClosed() {
+ return ordinal() >= Done.ordinal();
+ }
+
+ public Status next() {
+ switch (this) {
+ case New:
+ return Started;
+ case Accepted:
+ return Started;
+ case Started:
+ return Testing;
+ case Review:
+ return Testing;
+ case Queued:
+ return Testing;
+ case Testing:
+ return Done;
+ }
+ return Accepted;
+ }
+ }
+}
diff --git a/src/main/java/com/gitblit/models/Metric.java b/src/main/java/com/gitblit/models/Metric.java
new file mode 100644
index 00000000..2845c527
--- /dev/null
+++ b/src/main/java/com/gitblit/models/Metric.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.models;
+
+import java.io.Serializable;
+
+/**
+ * Metric is a serializable model class that encapsulates metrics for some given
+ * type.
+ *
+ * @author James Moger
+ *
+ */
+public class Metric implements Serializable, Comparable<Metric> {
+
+ private static final long serialVersionUID = 1L;
+
+ public final String name;
+ public double count;
+ public double tag;
+ public int duration;
+
+ public Metric(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public int compareTo(Metric o) {
+ if (count > o.count) {
+ return -1;
+ }
+ if (count < o.count) {
+ return 1;
+ }
+ return 0;
+ }
+} \ No newline at end of file
diff --git a/src/main/java/com/gitblit/models/PathModel.java b/src/main/java/com/gitblit/models/PathModel.java
new file mode 100644
index 00000000..84571cbb
--- /dev/null
+++ b/src/main/java/com/gitblit/models/PathModel.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.models;
+
+import java.io.Serializable;
+
+import org.eclipse.jgit.diff.DiffEntry.ChangeType;
+import org.eclipse.jgit.lib.FileMode;
+
+/**
+ * PathModel is a serializable model class that represents a file or a folder,
+ * including all its metadata and associated commit id.
+ *
+ * @author James Moger
+ *
+ */
+public class PathModel implements Serializable, Comparable<PathModel> {
+
+ private static final long serialVersionUID = 1L;
+
+ public final String name;
+ public final String path;
+ public final long size;
+ public final int mode;
+ public final String objectId;
+ public final String commitId;
+ public boolean isParentPath;
+
+ public PathModel(String name, String path, long size, int mode, String objectId, String commitId) {
+ this.name = name;
+ this.path = path;
+ this.size = size;
+ this.mode = mode;
+ this.objectId = objectId;
+ this.commitId = commitId;
+ }
+
+ public boolean isSymlink() {
+ return FileMode.SYMLINK.equals(mode);
+ }
+
+ public boolean isSubmodule() {
+ return FileMode.GITLINK.equals(mode);
+ }
+
+ public boolean isTree() {
+ return FileMode.TREE.equals(mode);
+ }
+
+ @Override
+ public int hashCode() {
+ return commitId.hashCode() + path.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof PathModel) {
+ PathModel other = (PathModel) o;
+ return this.path.equals(other.path);
+ }
+ return super.equals(o);
+ }
+
+ @Override
+ public int compareTo(PathModel o) {
+ boolean isTree = isTree();
+ boolean otherTree = o.isTree();
+ if (isTree && otherTree) {
+ return path.compareTo(o.path);
+ } else if (!isTree && !otherTree) {
+ if (isSubmodule() && o.isSubmodule()) {
+ return path.compareTo(o.path);
+ } else if (isSubmodule()) {
+ return -1;
+ } else if (o.isSubmodule()) {
+ return 1;
+ }
+ return path.compareTo(o.path);
+ } else if (isTree && !otherTree) {
+ return -1;
+ }
+ return 1;
+ }
+
+ /**
+ * PathChangeModel is a serializable class that represents a file changed in
+ * a commit.
+ *
+ * @author James Moger
+ *
+ */
+ public static class PathChangeModel extends PathModel {
+
+ private static final long serialVersionUID = 1L;
+
+ public final ChangeType changeType;
+
+ public PathChangeModel(String name, String path, long size, int mode, String objectId,
+ String commitId, ChangeType type) {
+ super(name, path, size, mode, objectId, commitId);
+ this.changeType = type;
+ }
+
+ @Override
+ public int hashCode() {
+ return super.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ return super.equals(o);
+ }
+ }
+}
diff --git a/src/main/java/com/gitblit/models/ProjectModel.java b/src/main/java/com/gitblit/models/ProjectModel.java
new file mode 100644
index 00000000..9e5d5233
--- /dev/null
+++ b/src/main/java/com/gitblit/models/ProjectModel.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2012 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.models;
+
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.Set;
+
+import com.gitblit.utils.StringUtils;
+
+/**
+ * ProjectModel is a serializable model class.
+ *
+ * @author James Moger
+ *
+ */
+public class ProjectModel implements Serializable, Comparable<ProjectModel> {
+
+ private static final long serialVersionUID = 1L;
+
+ // field names are reflectively mapped in EditProject page
+ public final String name;
+ public String title;
+ public String description;
+ public final Set<String> repositories = new HashSet<String>();
+
+ public String projectMarkdown;
+ public String repositoriesMarkdown;
+ public Date lastChange;
+ public final boolean isRoot;
+
+ public ProjectModel(String name) {
+ this(name, false);
+ }
+
+ public ProjectModel(String name, boolean isRoot) {
+ this.name = name;
+ this.isRoot = isRoot;
+ this.lastChange = new Date(0);
+ this.title = "";
+ this.description = "";
+ }
+
+ public boolean isUserProject() {
+ return name.charAt(0) == '~';
+ }
+
+ public boolean hasRepository(String name) {
+ return repositories.contains(name.toLowerCase());
+ }
+
+ public void addRepository(String name) {
+ repositories.add(name.toLowerCase());
+ }
+
+ public void addRepository(RepositoryModel model) {
+ repositories.add(model.name.toLowerCase());
+ if (lastChange.before(model.lastChange)) {
+ lastChange = model.lastChange;
+ }
+ }
+
+ public void addRepositories(Collection<String> names) {
+ for (String name:names) {
+ repositories.add(name.toLowerCase());
+ }
+ }
+
+ public void removeRepository(String name) {
+ repositories.remove(name.toLowerCase());
+ }
+
+ public String getDisplayName() {
+ return StringUtils.isEmpty(title) ? name : title;
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+
+ @Override
+ public int compareTo(ProjectModel o) {
+ return name.compareTo(o.name);
+ }
+}
diff --git a/src/main/java/com/gitblit/models/PushLogEntry.java b/src/main/java/com/gitblit/models/PushLogEntry.java
new file mode 100644
index 00000000..f625c2a3
--- /dev/null
+++ b/src/main/java/com/gitblit/models/PushLogEntry.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright 2013 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.models;
+
+import java.io.Serializable;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.transport.ReceiveCommand;
+
+/**
+ * Model class to represent a push into a repository.
+ *
+ * @author James Moger
+ */
+public class PushLogEntry implements Serializable, Comparable<PushLogEntry> {
+
+ private static final long serialVersionUID = 1L;
+
+ public final String repository;
+
+ public final Date date;
+
+ public final UserModel user;
+
+ private final Set<RepositoryCommit> commits;
+
+ private final Map<String, ReceiveCommand.Type> refUpdates;
+
+ /**
+ * Constructor for specified duration of push from start date.
+ *
+ * @param repository
+ * the repository that received the push
+ * @param date
+ * the date of the push
+ * @param user
+ * the user who pushed
+ */
+ public PushLogEntry(String repository, Date date, UserModel user) {
+ this.repository = repository;
+ this.date = date;
+ this.user = user;
+ this.commits = new LinkedHashSet<RepositoryCommit>();
+ this.refUpdates = new HashMap<String, ReceiveCommand.Type>();
+ }
+
+ /**
+ * Tracks the change type for the specified ref.
+ *
+ * @param ref
+ * @param type
+ */
+ public void updateRef(String ref, ReceiveCommand.Type type) {
+ if (!refUpdates.containsKey(ref)) {
+ refUpdates.put(ref, type);
+ }
+ }
+
+ /**
+ * Adds a commit to the push entry object as long as the commit is not a
+ * duplicate.
+ *
+ * @param branch
+ * @param commit
+ * @return a RepositoryCommit, if one was added. Null if this is duplicate
+ * commit
+ */
+ public RepositoryCommit addCommit(String branch, RevCommit commit) {
+ RepositoryCommit commitModel = new RepositoryCommit(repository, branch, commit);
+ if (commits.add(commitModel)) {
+ return commitModel;
+ }
+ return null;
+ }
+
+ /**
+ * Returns true if this push contains a non-fastforward ref update.
+ *
+ * @return true if this is a non-fastforward push
+ */
+ public boolean isNonFastForward() {
+ for (Map.Entry<String, ReceiveCommand.Type> entry : refUpdates.entrySet()) {
+ if (ReceiveCommand.Type.UPDATE_NONFASTFORWARD.equals(entry.getValue())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns the list of branches changed by the push.
+ *
+ * @return a list of branches
+ */
+ public List<String> getChangedBranches() {
+ return getChangedRefs(Constants.R_HEADS);
+ }
+
+ /**
+ * Returns the list of tags changed by the push.
+ *
+ * @return a list of tags
+ */
+ public List<String> getChangedTags() {
+ return getChangedRefs(Constants.R_TAGS);
+ }
+
+ /**
+ * Gets the changed refs in the push.
+ *
+ * @param baseRef
+ * @return the changed refs
+ */
+ protected List<String> getChangedRefs(String baseRef) {
+ Set<String> refs = new HashSet<String>();
+ for (String ref : refUpdates.keySet()) {
+ if (baseRef == null || ref.startsWith(baseRef)) {
+ refs.add(ref);
+ }
+ }
+ List<String> list = new ArrayList<String>(refs);
+ Collections.sort(list);
+ return list;
+ }
+
+ /**
+ * The total number of commits in the push.
+ *
+ * @return the number of commits in the push
+ */
+ public int getCommitCount() {
+ return commits.size();
+ }
+
+ /**
+ * Returns all commits in the push.
+ *
+ * @return a list of commits
+ */
+ public List<RepositoryCommit> getCommits() {
+ List<RepositoryCommit> list = new ArrayList<RepositoryCommit>(commits);
+ Collections.sort(list);
+ return list;
+ }
+
+ /**
+ * Returns all commits that belong to a particular ref
+ *
+ * @param ref
+ * @return a list of commits
+ */
+ public List<RepositoryCommit> getCommits(String ref) {
+ List<RepositoryCommit> list = new ArrayList<RepositoryCommit>();
+ for (RepositoryCommit commit : commits) {
+ if (commit.branch.equals(ref)) {
+ list.add(commit);
+ }
+ }
+ Collections.sort(list);
+ return list;
+ }
+
+ @Override
+ public int compareTo(PushLogEntry o) {
+ // reverse chronological order
+ return o.date.compareTo(date);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append(MessageFormat.format("{0,date,yyyy-MM-dd HH:mm}: {1} pushed {2,number,0} commit{3} to {4} ",
+ date, user.getDisplayName(), commits.size(), commits.size() == 1 ? "":"s", repository));
+ for (Map.Entry<String, ReceiveCommand.Type> entry : refUpdates.entrySet()) {
+ String ref = entry.getKey();
+ ReceiveCommand.Type type = entry.getValue();
+ sb.append("\n ").append(ref).append(' ').append(type.name()).append('\n');
+ for (RepositoryCommit commit : getCommits(ref)) {
+ sb.append(" ").append(commit.toString()).append('\n');
+ }
+ }
+ return sb.toString();
+ }
+}
diff --git a/src/main/java/com/gitblit/models/RefModel.java b/src/main/java/com/gitblit/models/RefModel.java
new file mode 100644
index 00000000..8489c817
--- /dev/null
+++ b/src/main/java/com/gitblit/models/RefModel.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.models;
+
+import java.io.Serializable;
+import java.util.Date;
+
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevObject;
+import org.eclipse.jgit.revwalk.RevTag;
+
+/**
+ * RefModel is a serializable model class that represents a tag or branch and
+ * includes the referenced object.
+ *
+ * @author James Moger
+ *
+ */
+public class RefModel implements Serializable, Comparable<RefModel> {
+
+ private static final long serialVersionUID = 1L;
+ public final String displayName;
+ public final RevObject referencedObject;
+ public transient Ref reference;
+
+ public RefModel(String displayName, Ref ref, RevObject refObject) {
+ this.displayName = displayName;
+ this.reference = ref;
+ this.referencedObject = refObject;
+ }
+
+ public Date getDate() {
+ Date date = new Date(0);
+ if (referencedObject != null) {
+ if (referencedObject instanceof RevTag) {
+ RevTag tag = (RevTag) referencedObject;
+ if (tag.getTaggerIdent() != null) {
+ date = tag.getTaggerIdent().getWhen();
+ }
+ } else if (referencedObject instanceof RevCommit) {
+ date = ((RevCommit) referencedObject).getCommitterIdent().getWhen();
+ }
+ }
+ return date;
+ }
+
+ public String getName() {
+ if (reference == null) {
+ return displayName;
+ }
+ return reference.getName();
+ }
+
+ public int getReferencedObjectType() {
+ int type = referencedObject.getType();
+ if (referencedObject instanceof RevTag) {
+ type = ((RevTag) referencedObject).getObject().getType();
+ }
+ return type;
+ }
+
+ public ObjectId getReferencedObjectId() {
+ if (referencedObject instanceof RevTag) {
+ return ((RevTag) referencedObject).getObject().getId();
+ }
+ return referencedObject.getId();
+ }
+
+ public String getShortMessage() {
+ String message = "";
+ if (referencedObject instanceof RevTag) {
+ message = ((RevTag) referencedObject).getShortMessage();
+ } else if (referencedObject instanceof RevCommit) {
+ message = ((RevCommit) referencedObject).getShortMessage();
+ }
+ return message;
+ }
+
+ public String getFullMessage() {
+ String message = "";
+ if (referencedObject instanceof RevTag) {
+ message = ((RevTag) referencedObject).getFullMessage();
+ } else if (referencedObject instanceof RevCommit) {
+ message = ((RevCommit) referencedObject).getFullMessage();
+ }
+ return message;
+ }
+
+ public PersonIdent getAuthorIdent() {
+ if (referencedObject instanceof RevTag) {
+ return ((RevTag) referencedObject).getTaggerIdent();
+ } else if (referencedObject instanceof RevCommit) {
+ return ((RevCommit) referencedObject).getAuthorIdent();
+ }
+ return null;
+ }
+
+ public ObjectId getObjectId() {
+ return reference.getObjectId();
+ }
+
+ public boolean isAnnotatedTag() {
+ if (referencedObject instanceof RevTag) {
+ return !getReferencedObjectId().equals(getObjectId());
+ }
+ return reference.getPeeledObjectId() != null;
+ }
+
+ @Override
+ public int hashCode() {
+ return getReferencedObjectId().hashCode() + getName().hashCode();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof RefModel) {
+ RefModel other = (RefModel) o;
+ return getName().equals(other.getName());
+ }
+ return super.equals(o);
+ }
+
+ @Override
+ public int compareTo(RefModel o) {
+ return getDate().compareTo(o.getDate());
+ }
+
+ @Override
+ public String toString() {
+ return displayName;
+ }
+} \ No newline at end of file
diff --git a/src/main/java/com/gitblit/models/RegistrantAccessPermission.java b/src/main/java/com/gitblit/models/RegistrantAccessPermission.java
new file mode 100644
index 00000000..8f4049a8
--- /dev/null
+++ b/src/main/java/com/gitblit/models/RegistrantAccessPermission.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright 2012 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.models;
+
+import java.io.Serializable;
+
+import com.gitblit.Constants.AccessPermission;
+import com.gitblit.Constants.PermissionType;
+import com.gitblit.Constants.RegistrantType;
+import com.gitblit.utils.StringUtils;
+
+/**
+ * Represents a Registrant-AccessPermission tuple.
+ *
+ * @author James Moger
+ */
+public class RegistrantAccessPermission implements Serializable, Comparable<RegistrantAccessPermission> {
+
+ private static final long serialVersionUID = 1L;
+
+ public String registrant;
+ public AccessPermission permission;
+ public RegistrantType registrantType;
+ public PermissionType permissionType;
+ public boolean mutable;
+ public String source;
+
+ public RegistrantAccessPermission() {
+ }
+
+ public RegistrantAccessPermission(RegistrantType registrantType) {
+ this.registrantType = registrantType;
+ this.permissionType = PermissionType.EXPLICIT;
+ this.mutable = true;
+ }
+
+ public RegistrantAccessPermission(String registrant, AccessPermission permission, PermissionType permissionType, RegistrantType registrantType, String source, boolean mutable) {
+ this.registrant = registrant;
+ this.permission = permission;
+ this.permissionType = permissionType;
+ this.registrantType = registrantType;
+ this.source = source;
+ this.mutable = mutable;
+ }
+
+ public boolean isAdmin() {
+ return PermissionType.ADMINISTRATOR.equals(permissionType);
+ }
+
+ public boolean isOwner() {
+ return PermissionType.OWNER.equals(permissionType);
+ }
+
+ public boolean isExplicit() {
+ return PermissionType.EXPLICIT.equals(permissionType);
+ }
+
+ public boolean isRegex() {
+ return PermissionType.REGEX.equals(permissionType);
+ }
+
+ public boolean isTeam() {
+ return PermissionType.TEAM.equals(permissionType);
+ }
+
+ public boolean isMissing() {
+ return PermissionType.MISSING.equals(permissionType);
+ }
+
+ public int getScore() {
+ switch (registrantType) {
+ case REPOSITORY:
+ if (isAdmin()) {
+ return 0;
+ }
+ if (isOwner()) {
+ return 1;
+ }
+ if (isExplicit()) {
+ return 2;
+ }
+ if (isRegex()) {
+ return 3;
+ }
+ if (isTeam()) {
+ return 4;
+ }
+ default:
+ return 0;
+ }
+ }
+
+ @Override
+ public int compareTo(RegistrantAccessPermission p) {
+ switch (registrantType) {
+ case REPOSITORY:
+ // repository permissions are sorted in score order
+ // to convey the order in which permissions are tested
+ int score1 = getScore();
+ int score2 = p.getScore();
+ if (score1 <= 2 && score2 <= 2) {
+ // group admin, owner, and explicit together
+ return StringUtils.compareRepositoryNames(registrant, p.registrant);
+ }
+ if (score1 < score2) {
+ return -1;
+ } else if (score2 < score1) {
+ return 1;
+ }
+ return StringUtils.compareRepositoryNames(registrant, p.registrant);
+ default:
+ // user and team permissions are string sorted
+ return registrant.toLowerCase().compareTo(p.registrant.toLowerCase());
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return registrant.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof RegistrantAccessPermission) {
+ RegistrantAccessPermission p = (RegistrantAccessPermission) o;
+ return registrant.equals(p.registrant);
+ }
+
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return permission.asRole(registrant);
+ }
+} \ No newline at end of file
diff --git a/src/main/java/com/gitblit/models/RepositoryCommit.java b/src/main/java/com/gitblit/models/RepositoryCommit.java
new file mode 100644
index 00000000..e68e8613
--- /dev/null
+++ b/src/main/java/com/gitblit/models/RepositoryCommit.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.models;
+
+import java.io.Serializable;
+import java.text.MessageFormat;
+import java.util.List;
+
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.revwalk.RevCommit;
+
+/**
+ * Model class to represent a RevCommit, it's source repository, and the branch.
+ * This class is used by the activity page.
+ *
+ * @author James Moger
+ */
+public class RepositoryCommit implements Serializable, Comparable<RepositoryCommit> {
+
+ private static final long serialVersionUID = 1L;
+
+ public final String repository;
+
+ public final String branch;
+
+ private final RevCommit commit;
+
+ private List<RefModel> refs;
+
+ public RepositoryCommit(String repository, String branch, RevCommit commit) {
+ this.repository = repository;
+ this.branch = branch;
+ this.commit = commit;
+ }
+
+ public void setRefs(List<RefModel> refs) {
+ this.refs = refs;
+ }
+
+ public List<RefModel> getRefs() {
+ return refs;
+ }
+
+ public String getName() {
+ return commit.getName();
+ }
+
+ public String getShortName() {
+ return commit.getName().substring(0, 8);
+ }
+
+ public String getShortMessage() {
+ return commit.getShortMessage();
+ }
+
+ public int getParentCount() {
+ return commit.getParentCount();
+ }
+
+ public PersonIdent getAuthorIdent() {
+ return commit.getAuthorIdent();
+ }
+
+ public PersonIdent getCommitterIdent() {
+ return commit.getCommitterIdent();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof RepositoryCommit) {
+ RepositoryCommit commit = (RepositoryCommit) o;
+ return repository.equals(commit.repository) && getName().equals(commit.getName());
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return (repository + commit).hashCode();
+ }
+
+ @Override
+ public int compareTo(RepositoryCommit o) {
+ // reverse-chronological order
+ if (commit.getCommitTime() > o.commit.getCommitTime()) {
+ return -1;
+ } else if (commit.getCommitTime() < o.commit.getCommitTime()) {
+ return 1;
+ }
+ return 0;
+ }
+
+ @Override
+ public String toString() {
+ return MessageFormat.format("{0} {1} {2,date,yyyy-MM-dd HH:mm} {3} {4}",
+ getShortName(), branch, getCommitterIdent().getWhen(), getAuthorIdent().getName(),
+ getShortMessage());
+ }
+} \ No newline at end of file
diff --git a/src/main/java/com/gitblit/models/RepositoryModel.java b/src/main/java/com/gitblit/models/RepositoryModel.java
new file mode 100644
index 00000000..a2dab3c5
--- /dev/null
+++ b/src/main/java/com/gitblit/models/RepositoryModel.java
@@ -0,0 +1,243 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.models;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+
+import com.gitblit.Constants.AccessRestrictionType;
+import com.gitblit.Constants.AuthorizationControl;
+import com.gitblit.Constants.FederationStrategy;
+import com.gitblit.utils.ArrayUtils;
+import com.gitblit.utils.StringUtils;
+
+/**
+ * RepositoryModel is a serializable model class that represents a Gitblit
+ * repository including its configuration settings and access restriction.
+ *
+ * @author James Moger
+ *
+ */
+public class RepositoryModel implements Serializable, Comparable<RepositoryModel> {
+
+ private static final long serialVersionUID = 1L;
+
+ // field names are reflectively mapped in EditRepository page
+ public String name;
+ public String description;
+ public List<String> owners;
+ public Date lastChange;
+ public boolean hasCommits;
+ public boolean showRemoteBranches;
+ public boolean useTickets;
+ public boolean useDocs;
+ public AccessRestrictionType accessRestriction;
+ public AuthorizationControl authorizationControl;
+ public boolean allowAuthenticated;
+ public boolean isFrozen;
+ public boolean showReadme;
+ public FederationStrategy federationStrategy;
+ public List<String> federationSets;
+ public boolean isFederated;
+ public boolean skipSizeCalculation;
+ public boolean skipSummaryMetrics;
+ public String frequency;
+ public boolean isBare;
+ public String origin;
+ public String HEAD;
+ public List<String> availableRefs;
+ public List<String> indexedBranches;
+ public String size;
+ public List<String> preReceiveScripts;
+ public List<String> postReceiveScripts;
+ public List<String> mailingLists;
+ public Map<String, String> customFields;
+ public String projectPath;
+ private String displayName;
+ public boolean allowForks;
+ public Set<String> forks;
+ public String originRepository;
+ public boolean verifyCommitter;
+ public String gcThreshold;
+ public int gcPeriod;
+ public int maxActivityCommits;
+
+ public transient boolean isCollectingGarbage;
+ public Date lastGC;
+ public String sparkleshareId;
+
+ public RepositoryModel() {
+ this("", "", "", new Date(0));
+ }
+
+ public RepositoryModel(String name, String description, String owner, Date lastchange) {
+ this.name = name;
+ this.description = description;
+ this.lastChange = lastchange;
+ this.accessRestriction = AccessRestrictionType.NONE;
+ this.authorizationControl = AuthorizationControl.NAMED;
+ this.federationSets = new ArrayList<String>();
+ this.federationStrategy = FederationStrategy.FEDERATE_THIS;
+ this.projectPath = StringUtils.getFirstPathElement(name);
+ this.owners = new ArrayList<String>();
+
+ addOwner(owner);
+ }
+
+ public List<String> getLocalBranches() {
+ if (ArrayUtils.isEmpty(availableRefs)) {
+ return new ArrayList<String>();
+ }
+ List<String> localBranches = new ArrayList<String>();
+ for (String ref : availableRefs) {
+ if (ref.startsWith("refs/heads")) {
+ localBranches.add(ref);
+ }
+ }
+ return localBranches;
+ }
+
+ public void addFork(String repository) {
+ if (forks == null) {
+ forks = new TreeSet<String>();
+ }
+ forks.add(repository);
+ }
+
+ public void removeFork(String repository) {
+ if (forks == null) {
+ return;
+ }
+ forks.remove(repository);
+ }
+
+ public void resetDisplayName() {
+ displayName = null;
+ }
+
+ @Override
+ public int hashCode() {
+ return name.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof RepositoryModel) {
+ return name.equals(((RepositoryModel) o).name);
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ if (displayName == null) {
+ displayName = StringUtils.stripDotGit(name);
+ }
+ return displayName;
+ }
+
+ @Override
+ public int compareTo(RepositoryModel o) {
+ return StringUtils.compareRepositoryNames(name, o.name);
+ }
+
+ public boolean isFork() {
+ return !StringUtils.isEmpty(originRepository);
+ }
+
+ public boolean isOwner(String username) {
+ if (StringUtils.isEmpty(username) || ArrayUtils.isEmpty(owners)) {
+ return false;
+ }
+ return owners.contains(username.toLowerCase());
+ }
+
+ public boolean isPersonalRepository() {
+ return !StringUtils.isEmpty(projectPath) && projectPath.charAt(0) == '~';
+ }
+
+ public boolean isUsersPersonalRepository(String username) {
+ return !StringUtils.isEmpty(projectPath) && projectPath.equalsIgnoreCase("~" + username);
+ }
+
+ public boolean allowAnonymousView() {
+ return !accessRestriction.atLeast(AccessRestrictionType.VIEW);
+ }
+
+ public boolean isSparkleshared() {
+ return !StringUtils.isEmpty(sparkleshareId);
+ }
+
+ public RepositoryModel cloneAs(String cloneName) {
+ RepositoryModel clone = new RepositoryModel();
+ clone.originRepository = name;
+ clone.name = cloneName;
+ clone.projectPath = StringUtils.getFirstPathElement(cloneName);
+ clone.isBare = true;
+ clone.description = description;
+ clone.accessRestriction = AccessRestrictionType.PUSH;
+ clone.authorizationControl = AuthorizationControl.NAMED;
+ clone.federationStrategy = federationStrategy;
+ clone.showReadme = showReadme;
+ clone.showRemoteBranches = false;
+ clone.allowForks = false;
+ clone.useDocs = useDocs;
+ clone.useTickets = useTickets;
+ clone.skipSizeCalculation = skipSizeCalculation;
+ clone.skipSummaryMetrics = skipSummaryMetrics;
+ clone.sparkleshareId = sparkleshareId;
+ return clone;
+ }
+
+ public void addOwner(String username) {
+ if (!StringUtils.isEmpty(username)) {
+ String name = username.toLowerCase();
+ // a set would be more efficient, but this complicates JSON
+ // deserialization so we enforce uniqueness with an arraylist
+ if (!owners.contains(name)) {
+ owners.add(name);
+ }
+ }
+ }
+
+ public void removeOwner(String username) {
+ if (!StringUtils.isEmpty(username)) {
+ owners.remove(username.toLowerCase());
+ }
+ }
+
+ public void addOwners(Collection<String> usernames) {
+ if (!ArrayUtils.isEmpty(usernames)) {
+ for (String username : usernames) {
+ addOwner(username);
+ }
+ }
+ }
+
+ public void removeOwners(Collection<String> usernames) {
+ if (!ArrayUtils.isEmpty(owners)) {
+ for (String username : usernames) {
+ removeOwner(username);
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/gitblit/models/SearchResult.java b/src/main/java/com/gitblit/models/SearchResult.java
new file mode 100644
index 00000000..efd1b075
--- /dev/null
+++ b/src/main/java/com/gitblit/models/SearchResult.java
@@ -0,0 +1,70 @@
+package com.gitblit.models;
+
+import java.io.Serializable;
+import java.util.Date;
+import java.util.List;
+
+import com.gitblit.Constants.SearchObjectType;
+
+/**
+ * Model class that represents a search result.
+ *
+ * @author James Moger
+ *
+ */
+public class SearchResult implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ public int hitId;
+
+ public int totalHits;
+
+ public float score;
+
+ public Date date;
+
+ public String author;
+
+ public String committer;
+
+ public String summary;
+
+ public String fragment;
+
+ public String repository;
+
+ public String branch;
+
+ public String commitId;
+
+ public String path;
+
+ public String issueId;
+
+ public List<String> tags;
+
+ public List<String> labels;
+
+ public SearchObjectType type;
+
+ public SearchResult() {
+ }
+
+ public String getId() {
+ switch (type) {
+ case blob:
+ return path;
+ case commit:
+ return commitId;
+ case issue:
+ return issueId;
+ }
+ return commitId;
+ }
+
+ @Override
+ public String toString() {
+ return score + " : " + type.name() + " : " + repository + " : " + getId() + " (" + branch + ")";
+ }
+} \ No newline at end of file
diff --git a/src/main/java/com/gitblit/models/ServerSettings.java b/src/main/java/com/gitblit/models/ServerSettings.java
new file mode 100644
index 00000000..27199b41
--- /dev/null
+++ b/src/main/java/com/gitblit/models/ServerSettings.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.models;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * Server settings represents the settings of the Gitblit server including all
+ * setting metadata such as name, current value, default value, description, and
+ * directives. It is a model class for serialization and presentation, but not
+ * for persistence.
+ *
+ * @author James Moger
+ */
+public class ServerSettings implements Serializable {
+
+ private final Map<String, SettingModel> settings;
+
+ private static final long serialVersionUID = 1L;
+
+ public List<String> pushScripts;
+
+ public boolean supportsCredentialChanges;
+
+ public boolean supportsDisplayNameChanges;
+
+ public boolean supportsEmailAddressChanges;
+
+ public boolean supportsTeamMembershipChanges;
+
+ public ServerSettings() {
+ settings = new TreeMap<String, SettingModel>();
+ }
+
+ public List<String> getKeys() {
+ return new ArrayList<String>(settings.keySet());
+ }
+
+ public void add(SettingModel setting) {
+ if (setting != null) {
+ settings.put(setting.name, setting);
+ }
+ }
+
+ public SettingModel get(String key) {
+ return settings.get(key);
+ }
+
+ public boolean hasKey(String key) {
+ return settings.containsKey(key);
+ }
+}
diff --git a/src/main/java/com/gitblit/models/ServerStatus.java b/src/main/java/com/gitblit/models/ServerStatus.java
new file mode 100644
index 00000000..3a1e0306
--- /dev/null
+++ b/src/main/java/com/gitblit/models/ServerStatus.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.models;
+
+import java.io.Serializable;
+import java.util.Date;
+import java.util.Map;
+import java.util.TreeMap;
+
+import com.gitblit.Constants;
+
+/**
+ * ServerStatus encapsulates runtime status information about the server
+ * including some information about the system environment.
+ *
+ * @author James Moger
+ *
+ */
+public class ServerStatus implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ public final Date bootDate;
+
+ public final String version;
+
+ public final String releaseDate;
+
+ public final boolean isGO;
+
+ public final Map<String, String> systemProperties;
+
+ public final long heapMaximum;
+
+ public volatile long heapAllocated;
+
+ public volatile long heapFree;
+
+ public String servletContainer;
+
+ public ServerStatus(boolean isGO) {
+ this.bootDate = new Date();
+ this.version = Constants.getVersion();
+ this.releaseDate = Constants.getBuildDate();
+ this.isGO = isGO;
+
+ this.heapMaximum = Runtime.getRuntime().maxMemory();
+
+ this.systemProperties = new TreeMap<String, String>();
+ put("file.encoding");
+ put("java.home");
+ put("java.awt.headless");
+ put("java.io.tmpdir");
+ put("java.runtime.name");
+ put("java.runtime.version");
+ put("java.vendor");
+ put("java.version");
+ put("java.vm.info");
+ put("java.vm.name");
+ put("java.vm.vendor");
+ put("java.vm.version");
+ put("os.arch");
+ put("os.name");
+ put("os.version");
+ }
+
+ private void put(String key) {
+ systemProperties.put(key, System.getProperty(key));
+ }
+}
diff --git a/src/main/java/com/gitblit/models/SettingModel.java b/src/main/java/com/gitblit/models/SettingModel.java
new file mode 100644
index 00000000..a04126e1
--- /dev/null
+++ b/src/main/java/com/gitblit/models/SettingModel.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.models;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.gitblit.utils.StringUtils;
+
+/**
+ * SettingModel represents a setting and all its metadata: name, current value,
+ * default value, description, and directives.
+ *
+ * @author James Moger
+ */
+public class SettingModel implements Serializable {
+
+ public static final String SPACE_DELIMITED = "SPACE-DELIMITED";
+
+ public static final String CASE_SENSITIVE = "CASE-SENSITIVE";
+
+ public static final String RESTART_REQUIRED = "RESTART REQUIRED";
+
+ public static final String SINCE = "SINCE";
+
+ public String name;
+ public volatile String currentValue;
+ public String defaultValue;
+ public String description;
+ public String since;
+ public boolean caseSensitive;
+ public boolean restartRequired;
+ public boolean spaceDelimited;
+
+ private static final long serialVersionUID = 1L;
+
+ public SettingModel() {
+ }
+
+ /**
+ * Returns true if the current value is the default value.
+ *
+ * @return true if current value is the default value
+ */
+ public boolean isDefaultValue() {
+ return (currentValue != null && currentValue.equals(defaultValue))
+ || currentValue.trim().length() == 0;
+ }
+
+ /**
+ * Returns the boolean value for the currentValue. If the currentValue can
+ * not be interpreted as a boolean, the defaultValue is returned.
+ *
+ * @param defaultValue
+ * @return key value or defaultValue
+ */
+ public boolean getBoolean(boolean defaultValue) {
+ if (!StringUtils.isEmpty(currentValue)) {
+ return Boolean.parseBoolean(currentValue.trim());
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Returns the integer value for the currentValue. If the currentValue can
+ * not be interpreted as an integer, the defaultValue is returned.
+ *
+ * @param defaultValue
+ * @return key value or defaultValue
+ */
+ public int getInteger(int defaultValue) {
+ try {
+ if (!StringUtils.isEmpty(currentValue)) {
+ return Integer.parseInt(currentValue.trim());
+ }
+ } catch (NumberFormatException e) {
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Returns the char value for currentValue. If the currentValue can not be
+ * interpreted as a char, the defaultValue is returned.
+ *
+ * @param defaultValue
+ * @return key value or defaultValue
+ */
+ public char getChar(char defaultValue) {
+ if (!StringUtils.isEmpty(currentValue)) {
+ return currentValue.trim().charAt(0);
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Returns the string value for currentValue. If the currentValue is null,
+ * the defaultValue is returned.
+ *
+ * @param defaultValue
+ * @return key value or defaultValue
+ */
+ public String getString(String defaultValue) {
+ if (currentValue != null) {
+ return currentValue.trim();
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Returns a list of space-separated strings from the specified key.
+ *
+ * @return list of strings
+ */
+ public List<String> getStrings() {
+ return getStrings(" ");
+ }
+
+ /**
+ * Returns a list of strings from the currentValue using the specified
+ * string separator.
+ *
+ * @param separator
+ * @return list of strings
+ */
+ public List<String> getStrings(String separator) {
+ List<String> strings = new ArrayList<String>();
+ strings = StringUtils.getStringsFromValue(currentValue, separator);
+ return strings;
+ }
+
+ /**
+ * Returns a map of strings from the current value.
+ *
+ * @return map of string, string
+ */
+ public Map<String, String> getMap() {
+ Map<String, String> map = new LinkedHashMap<String, String>();
+ for (String string : getStrings()) {
+ String[] kvp = string.split("=", 2);
+ String key = kvp[0];
+ String value = kvp[1];
+ map.put(key, value);
+ }
+ return map;
+ }
+}
diff --git a/src/main/java/com/gitblit/models/SubmoduleModel.java b/src/main/java/com/gitblit/models/SubmoduleModel.java
new file mode 100644
index 00000000..47f84b95
--- /dev/null
+++ b/src/main/java/com/gitblit/models/SubmoduleModel.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2012 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.models;
+
+import java.io.Serializable;
+
+/**
+ * SubmoduleModel is a serializable model class that represents a git submodule
+ * definition.
+ *
+ * @author James Moger
+ *
+ */
+public class SubmoduleModel implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ public final String name;
+ public final String path;
+ public final String url;
+
+ public boolean hasSubmodule;
+ public String gitblitPath;
+
+ public SubmoduleModel(String name, String path, String url) {
+ this.name = name;
+ this.path = path;
+ this.url = url;
+ }
+
+ public String toString() {
+ return path + "=" + url;
+ }
+} \ No newline at end of file
diff --git a/src/main/java/com/gitblit/models/TeamModel.java b/src/main/java/com/gitblit/models/TeamModel.java
new file mode 100644
index 00000000..9587ca7a
--- /dev/null
+++ b/src/main/java/com/gitblit/models/TeamModel.java
@@ -0,0 +1,310 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.models;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import com.gitblit.Constants.AccessPermission;
+import com.gitblit.Constants.AccessRestrictionType;
+import com.gitblit.Constants.PermissionType;
+import com.gitblit.Constants.RegistrantType;
+import com.gitblit.Constants.Unused;
+import com.gitblit.utils.StringUtils;
+
+/**
+ * TeamModel is a serializable model class that represents a group of users and
+ * a list of accessible repositories.
+ *
+ * @author James Moger
+ *
+ */
+public class TeamModel implements Serializable, Comparable<TeamModel> {
+
+ private static final long serialVersionUID = 1L;
+
+ // field names are reflectively mapped in EditTeam page
+ public String name;
+ public boolean canAdmin;
+ public boolean canFork;
+ public boolean canCreate;
+ public final Set<String> users = new HashSet<String>();
+ // retained for backwards-compatibility with RPC clients
+ @Deprecated
+ public final Set<String> repositories = new HashSet<String>();
+ public final Map<String, AccessPermission> permissions = new LinkedHashMap<String, AccessPermission>();
+ public final Set<String> mailingLists = new HashSet<String>();
+ public final List<String> preReceiveScripts = new ArrayList<String>();
+ public final List<String> postReceiveScripts = new ArrayList<String>();
+
+ public TeamModel(String name) {
+ this.name = name;
+ }
+
+ /**
+ * @use hasRepositoryPermission
+ * @param name
+ * @return
+ */
+ @Deprecated
+ @Unused
+ public boolean hasRepository(String name) {
+ return hasRepositoryPermission(name);
+ }
+
+ @Deprecated
+ @Unused
+ public void addRepository(String name) {
+ addRepositoryPermission(name);
+ }
+
+ @Deprecated
+ @Unused
+ public void addRepositories(Collection<String> names) {
+ addRepositoryPermissions(names);
+ }
+
+ @Deprecated
+ @Unused
+ public void removeRepository(String name) {
+ removeRepositoryPermission(name);
+ }
+
+
+ /**
+ * Returns a list of repository permissions for this team.
+ *
+ * @return the team's list of permissions
+ */
+ public List<RegistrantAccessPermission> getRepositoryPermissions() {
+ List<RegistrantAccessPermission> list = new ArrayList<RegistrantAccessPermission>();
+ if (canAdmin) {
+ // team has REWIND access to all repositories
+ return list;
+ }
+ for (Map.Entry<String, AccessPermission> entry : permissions.entrySet()) {
+ String registrant = entry.getKey();
+ String source = null;
+ boolean editable = true;
+ PermissionType pType = PermissionType.EXPLICIT;
+ if (StringUtils.findInvalidCharacter(registrant) != null) {
+ // a regex will have at least 1 invalid character
+ pType = PermissionType.REGEX;
+ source = registrant;
+ }
+ list.add(new RegistrantAccessPermission(registrant, entry.getValue(), pType, RegistrantType.REPOSITORY, source, editable));
+ }
+ Collections.sort(list);
+ return list;
+ }
+
+ /**
+ * Returns true if the team has any type of specified access permission for
+ * this repository.
+ *
+ * @param name
+ * @return true if team has a specified access permission for the repository
+ */
+ public boolean hasRepositoryPermission(String name) {
+ String repository = AccessPermission.repositoryFromRole(name).toLowerCase();
+ if (permissions.containsKey(repository)) {
+ // exact repository permission specified
+ return true;
+ } else {
+ // search for regex permission match
+ for (String key : permissions.keySet()) {
+ if (name.matches(key)) {
+ AccessPermission p = permissions.get(key);
+ if (p != null) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if the team has an explicitly specified access permission for
+ * this repository.
+ *
+ * @param name
+ * @return if the team has an explicitly specified access permission
+ */
+ public boolean hasExplicitRepositoryPermission(String name) {
+ String repository = AccessPermission.repositoryFromRole(name).toLowerCase();
+ return permissions.containsKey(repository);
+ }
+
+ /**
+ * Adds a repository permission to the team.
+ * <p>
+ * Role may be formatted as:
+ * <ul>
+ * <li> myrepo.git <i>(this is implicitly RW+)</i>
+ * <li> RW+:myrepo.git
+ * </ul>
+ * @param role
+ */
+ public void addRepositoryPermission(String role) {
+ AccessPermission permission = AccessPermission.permissionFromRole(role);
+ String repository = AccessPermission.repositoryFromRole(role).toLowerCase();
+ repositories.add(repository);
+ permissions.put(repository, permission);
+ }
+
+ public void addRepositoryPermissions(Collection<String> roles) {
+ for (String role:roles) {
+ addRepositoryPermission(role);
+ }
+ }
+
+ public AccessPermission removeRepositoryPermission(String name) {
+ String repository = AccessPermission.repositoryFromRole(name).toLowerCase();
+ repositories.remove(repository);
+ return permissions.remove(repository);
+ }
+
+ public void setRepositoryPermission(String repository, AccessPermission permission) {
+ permissions.put(repository.toLowerCase(), permission);
+ repositories.add(repository.toLowerCase());
+ }
+
+ public RegistrantAccessPermission getRepositoryPermission(RepositoryModel repository) {
+ RegistrantAccessPermission ap = new RegistrantAccessPermission();
+ ap.registrant = name;
+ ap.registrantType = RegistrantType.TEAM;
+ ap.permission = AccessPermission.NONE;
+ ap.mutable = false;
+
+ if (canAdmin) {
+ ap.permissionType = PermissionType.ADMINISTRATOR;
+ ap.permission = AccessPermission.REWIND;
+ return ap;
+ }
+
+ if (permissions.containsKey(repository.name.toLowerCase())) {
+ // exact repository permission specified
+ AccessPermission p = permissions.get(repository.name.toLowerCase());
+ if (p != null) {
+ ap.permissionType = PermissionType.EXPLICIT;
+ ap.permission = p;
+ ap.mutable = true;
+ return ap;
+ }
+ } else {
+ // search for case-insensitive regex permission match
+ for (String key : permissions.keySet()) {
+ if (StringUtils.matchesIgnoreCase(repository.name, key)) {
+ AccessPermission p = permissions.get(key);
+ if (p != null) {
+ // take first match
+ ap.permissionType = PermissionType.REGEX;
+ ap.permission = p;
+ ap.source = key;
+ return ap;
+ }
+ }
+ }
+ }
+ return ap;
+ }
+
+ protected boolean canAccess(RepositoryModel repository, AccessRestrictionType ifRestriction, AccessPermission requirePermission) {
+ if (repository.accessRestriction.atLeast(ifRestriction)) {
+ RegistrantAccessPermission ap = getRepositoryPermission(repository);
+ return ap.permission.atLeast(requirePermission);
+ }
+ return true;
+ }
+
+ public boolean canView(RepositoryModel repository) {
+ return canAccess(repository, AccessRestrictionType.VIEW, AccessPermission.VIEW);
+ }
+
+ public boolean canClone(RepositoryModel repository) {
+ return canAccess(repository, AccessRestrictionType.CLONE, AccessPermission.CLONE);
+ }
+
+ public boolean canPush(RepositoryModel repository) {
+ if (repository.isFrozen) {
+ return false;
+ }
+ return canAccess(repository, AccessRestrictionType.PUSH, AccessPermission.PUSH);
+ }
+
+ public boolean canCreateRef(RepositoryModel repository) {
+ if (repository.isFrozen) {
+ return false;
+ }
+ return canAccess(repository, AccessRestrictionType.PUSH, AccessPermission.CREATE);
+ }
+
+ public boolean canDeleteRef(RepositoryModel repository) {
+ if (repository.isFrozen) {
+ return false;
+ }
+ return canAccess(repository, AccessRestrictionType.PUSH, AccessPermission.DELETE);
+ }
+
+ public boolean canRewindRef(RepositoryModel repository) {
+ if (repository.isFrozen) {
+ return false;
+ }
+ return canAccess(repository, AccessRestrictionType.PUSH, AccessPermission.REWIND);
+ }
+
+ public boolean hasUser(String name) {
+ return users.contains(name.toLowerCase());
+ }
+
+ public void addUser(String name) {
+ users.add(name.toLowerCase());
+ }
+
+ public void addUsers(Collection<String> names) {
+ for (String name:names) {
+ users.add(name.toLowerCase());
+ }
+ }
+
+ public void removeUser(String name) {
+ users.remove(name.toLowerCase());
+ }
+
+ public void addMailingLists(Collection<String> addresses) {
+ for (String address:addresses) {
+ mailingLists.add(address.toLowerCase());
+ }
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+
+ @Override
+ public int compareTo(TeamModel o) {
+ return name.compareTo(o.name);
+ }
+}
diff --git a/src/main/java/com/gitblit/models/TicketModel.java b/src/main/java/com/gitblit/models/TicketModel.java
new file mode 100644
index 00000000..b8043c6c
--- /dev/null
+++ b/src/main/java/com/gitblit/models/TicketModel.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.models;
+
+import java.io.Serializable;
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * TicketModel is a serializable model class that represents a Ticgit ticket.
+ *
+ * @author James Moger
+ *
+ */
+public class TicketModel implements Serializable, Comparable<TicketModel> {
+
+ private static final long serialVersionUID = 1L;
+
+ public String id;
+ public String name;
+ public String title;
+ public String state;
+ public Date date;
+ public String handler;
+ public String milestone;
+ public String email;
+ public String author;
+ public List<Comment> comments;
+ public List<String> tags;
+
+ public TicketModel(String ticketName) throws ParseException {
+ state = "";
+ name = ticketName;
+ comments = new ArrayList<Comment>();
+ tags = new ArrayList<String>();
+
+ String[] chunks = name.split("_");
+ if (chunks.length == 3) {
+ date = new Date(Long.parseLong(chunks[0]) * 1000L);
+ title = chunks[1].replace('-', ' ');
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return id.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof TicketModel) {
+ TicketModel other = (TicketModel) o;
+ return id.equals(other.id);
+ }
+ return super.equals(o);
+ }
+
+ @Override
+ public int compareTo(TicketModel o) {
+ return date.compareTo(o.date);
+ }
+
+ /**
+ * Comment is a serializable model class that represents a Ticgit ticket
+ * comment.
+ *
+ * @author James Moger
+ *
+ */
+ public static class Comment implements Serializable, Comparable<Comment> {
+
+ private static final long serialVersionUID = 1L;
+
+ public String text;
+ public String author;
+ public Date date;
+
+ public Comment(String filename, String content) throws ParseException {
+ String[] chunks = filename.split("_", -1);
+ this.date = new Date(Long.parseLong(chunks[1]) * 1000L);
+ this.author = chunks[2];
+ this.text = content;
+ }
+
+ @Override
+ public int hashCode() {
+ return text.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof Comment) {
+ Comment other = (Comment) o;
+ return text.equals(other.text);
+ }
+ return super.equals(o);
+ }
+
+ @Override
+ public int compareTo(Comment o) {
+ return date.compareTo(o.date);
+ }
+ }
+}
diff --git a/src/main/java/com/gitblit/models/UserModel.java b/src/main/java/com/gitblit/models/UserModel.java
new file mode 100644
index 00000000..bec011d9
--- /dev/null
+++ b/src/main/java/com/gitblit/models/UserModel.java
@@ -0,0 +1,613 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.models;
+
+import java.io.Serializable;
+import java.security.Principal;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+
+import com.gitblit.Constants.AccessPermission;
+import com.gitblit.Constants.AccessRestrictionType;
+import com.gitblit.Constants.AccountType;
+import com.gitblit.Constants.AuthorizationControl;
+import com.gitblit.Constants.PermissionType;
+import com.gitblit.Constants.RegistrantType;
+import com.gitblit.Constants.Unused;
+import com.gitblit.utils.ArrayUtils;
+import com.gitblit.utils.StringUtils;
+
+/**
+ * UserModel is a serializable model class that represents a user and the user's
+ * restricted repository memberships. Instances of UserModels are also used as
+ * servlet user principals.
+ *
+ * @author James Moger
+ *
+ */
+public class UserModel implements Principal, Serializable, Comparable<UserModel> {
+
+ private static final long serialVersionUID = 1L;
+
+ public static final UserModel ANONYMOUS = new UserModel();
+
+ // field names are reflectively mapped in EditUser page
+ public String username;
+ public String password;
+ public String cookie;
+ public String displayName;
+ public String emailAddress;
+ public String organizationalUnit;
+ public String organization;
+ public String locality;
+ public String stateProvince;
+ public String countryCode;
+ public boolean canAdmin;
+ public boolean canFork;
+ public boolean canCreate;
+ public boolean excludeFromFederation;
+ // retained for backwards-compatibility with RPC clients
+ @Deprecated
+ public final Set<String> repositories = new HashSet<String>();
+ public final Map<String, AccessPermission> permissions = new LinkedHashMap<String, AccessPermission>();
+ public final Set<TeamModel> teams = new TreeSet<TeamModel>();
+
+ // non-persisted fields
+ public boolean isAuthenticated;
+ public AccountType accountType;
+
+ public UserModel(String username) {
+ this.username = username;
+ this.isAuthenticated = true;
+ this.accountType = AccountType.LOCAL;
+ }
+
+ private UserModel() {
+ this.username = "$anonymous";
+ this.isAuthenticated = false;
+ this.accountType = AccountType.LOCAL;
+ }
+
+ public boolean isLocalAccount() {
+ return accountType.isLocal();
+ }
+
+ /**
+ * This method does not take into consideration Ownership where the
+ * administrator has not explicitly granted access to the owner.
+ *
+ * @param repositoryName
+ * @return
+ */
+ @Deprecated
+ public boolean canAccessRepository(String repositoryName) {
+ return canAdmin() || repositories.contains(repositoryName.toLowerCase())
+ || hasTeamAccess(repositoryName);
+ }
+
+ @Deprecated
+ @Unused
+ public boolean canAccessRepository(RepositoryModel repository) {
+ boolean isOwner = repository.isOwner(username);
+ boolean allowAuthenticated = isAuthenticated && AuthorizationControl.AUTHENTICATED.equals(repository.authorizationControl);
+ return canAdmin() || isOwner || repositories.contains(repository.name.toLowerCase())
+ || hasTeamAccess(repository.name) || allowAuthenticated;
+ }
+
+ @Deprecated
+ @Unused
+ public boolean hasTeamAccess(String repositoryName) {
+ for (TeamModel team : teams) {
+ if (team.hasRepositoryPermission(repositoryName)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Deprecated
+ @Unused
+ public boolean hasRepository(String name) {
+ return hasRepositoryPermission(name);
+ }
+
+ @Deprecated
+ @Unused
+ public void addRepository(String name) {
+ addRepositoryPermission(name);
+ }
+
+ @Deprecated
+ @Unused
+ public void removeRepository(String name) {
+ removeRepositoryPermission(name);
+ }
+
+ /**
+ * Returns a list of repository permissions for this user exclusive of
+ * permissions inherited from team memberships.
+ *
+ * @return the user's list of permissions
+ */
+ public List<RegistrantAccessPermission> getRepositoryPermissions() {
+ List<RegistrantAccessPermission> list = new ArrayList<RegistrantAccessPermission>();
+ if (canAdmin()) {
+ // user has REWIND access to all repositories
+ return list;
+ }
+ for (Map.Entry<String, AccessPermission> entry : permissions.entrySet()) {
+ String registrant = entry.getKey();
+ AccessPermission ap = entry.getValue();
+ String source = null;
+ boolean mutable = true;
+ PermissionType pType = PermissionType.EXPLICIT;
+ if (isMyPersonalRepository(registrant)) {
+ pType = PermissionType.OWNER;
+ ap = AccessPermission.REWIND;
+ mutable = false;
+ } else if (StringUtils.findInvalidCharacter(registrant) != null) {
+ // a regex will have at least 1 invalid character
+ pType = PermissionType.REGEX;
+ source = registrant;
+ }
+ list.add(new RegistrantAccessPermission(registrant, ap, pType, RegistrantType.REPOSITORY, source, mutable));
+ }
+ Collections.sort(list);
+
+ // include immutable team permissions, being careful to preserve order
+ Set<RegistrantAccessPermission> set = new LinkedHashSet<RegistrantAccessPermission>(list);
+ for (TeamModel team : teams) {
+ for (RegistrantAccessPermission teamPermission : team.getRepositoryPermissions()) {
+ // we can not change an inherited team permission, though we can override
+ teamPermission.registrantType = RegistrantType.REPOSITORY;
+ teamPermission.permissionType = PermissionType.TEAM;
+ teamPermission.source = team.name;
+ teamPermission.mutable = false;
+ set.add(teamPermission);
+ }
+ }
+ return new ArrayList<RegistrantAccessPermission>(set);
+ }
+
+ /**
+ * Returns true if the user has any type of specified access permission for
+ * this repository.
+ *
+ * @param name
+ * @return true if user has a specified access permission for the repository
+ */
+ public boolean hasRepositoryPermission(String name) {
+ String repository = AccessPermission.repositoryFromRole(name).toLowerCase();
+ if (permissions.containsKey(repository)) {
+ // exact repository permission specified
+ return true;
+ } else {
+ // search for regex permission match
+ for (String key : permissions.keySet()) {
+ if (name.matches(key)) {
+ AccessPermission p = permissions.get(key);
+ if (p != null) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if the user has an explicitly specified access permission for
+ * this repository.
+ *
+ * @param name
+ * @return if the user has an explicitly specified access permission
+ */
+ public boolean hasExplicitRepositoryPermission(String name) {
+ String repository = AccessPermission.repositoryFromRole(name).toLowerCase();
+ return permissions.containsKey(repository);
+ }
+
+ /**
+ * Returns true if the user's team memberships specify an access permission for
+ * this repository.
+ *
+ * @param name
+ * @return if the user's team memberships specifi an access permission
+ */
+ public boolean hasTeamRepositoryPermission(String name) {
+ if (teams != null) {
+ for (TeamModel team : teams) {
+ if (team.hasRepositoryPermission(name)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Adds a repository permission to the team.
+ * <p>
+ * Role may be formatted as:
+ * <ul>
+ * <li> myrepo.git <i>(this is implicitly RW+)</i>
+ * <li> RW+:myrepo.git
+ * </ul>
+ * @param role
+ */
+ public void addRepositoryPermission(String role) {
+ AccessPermission permission = AccessPermission.permissionFromRole(role);
+ String repository = AccessPermission.repositoryFromRole(role).toLowerCase();
+ repositories.add(repository);
+ permissions.put(repository, permission);
+ }
+
+ public AccessPermission removeRepositoryPermission(String name) {
+ String repository = AccessPermission.repositoryFromRole(name).toLowerCase();
+ repositories.remove(repository);
+ return permissions.remove(repository);
+ }
+
+ public void setRepositoryPermission(String repository, AccessPermission permission) {
+ permissions.put(repository.toLowerCase(), permission);
+ }
+
+ public RegistrantAccessPermission getRepositoryPermission(RepositoryModel repository) {
+ RegistrantAccessPermission ap = new RegistrantAccessPermission();
+ ap.registrant = username;
+ ap.registrantType = RegistrantType.USER;
+ ap.permission = AccessPermission.NONE;
+ ap.mutable = false;
+
+ if (AccessRestrictionType.NONE.equals(repository.accessRestriction)) {
+ // anonymous rewind
+ ap.permissionType = PermissionType.ADMINISTRATOR;
+ ap.permission = AccessPermission.REWIND;
+ return ap;
+ }
+
+ // administrator
+ if (canAdmin()) {
+ ap.permissionType = PermissionType.ADMINISTRATOR;
+ ap.permission = AccessPermission.REWIND;
+ if (!canAdmin) {
+ // administator permission from team membership
+ for (TeamModel team : teams) {
+ if (team.canAdmin) {
+ ap.source = team.name;
+ break;
+ }
+ }
+ }
+ return ap;
+ }
+
+ // repository owner - either specified owner or personal repository
+ if (repository.isOwner(username) || repository.isUsersPersonalRepository(username)) {
+ ap.permissionType = PermissionType.OWNER;
+ ap.permission = AccessPermission.REWIND;
+ return ap;
+ }
+
+ if (AuthorizationControl.AUTHENTICATED.equals(repository.authorizationControl) && isAuthenticated) {
+ // AUTHENTICATED is a shortcut for authorizing all logged-in users RW+ access
+ ap.permission = AccessPermission.REWIND;
+ return ap;
+ }
+
+ // explicit user permission OR user regex match is used
+ // if that fails, then the best team permission is used
+ if (permissions.containsKey(repository.name.toLowerCase())) {
+ // exact repository permission specified, use it
+ AccessPermission p = permissions.get(repository.name.toLowerCase());
+ if (p != null) {
+ ap.permissionType = PermissionType.EXPLICIT;
+ ap.permission = p;
+ ap.mutable = true;
+ return ap;
+ }
+ } else {
+ // search for case-insensitive regex permission match
+ for (String key : permissions.keySet()) {
+ if (StringUtils.matchesIgnoreCase(repository.name, key)) {
+ AccessPermission p = permissions.get(key);
+ if (p != null) {
+ // take first match
+ ap.permissionType = PermissionType.REGEX;
+ ap.permission = p;
+ ap.source = key;
+ return ap;
+ }
+ }
+ }
+ }
+
+ // try to find a team match
+ for (TeamModel team : teams) {
+ RegistrantAccessPermission p = team.getRepositoryPermission(repository);
+ if (p.permission.exceeds(ap.permission)) {
+ // use highest team permission
+ ap.permission = p.permission;
+ ap.source = team.name;
+ ap.permissionType = PermissionType.TEAM;
+ }
+ }
+
+ return ap;
+ }
+
+ protected boolean canAccess(RepositoryModel repository, AccessRestrictionType ifRestriction, AccessPermission requirePermission) {
+ if (repository.accessRestriction.atLeast(ifRestriction)) {
+ RegistrantAccessPermission ap = getRepositoryPermission(repository);
+ return ap.permission.atLeast(requirePermission);
+ }
+ return true;
+ }
+
+ public boolean canView(RepositoryModel repository) {
+ return canAccess(repository, AccessRestrictionType.VIEW, AccessPermission.VIEW);
+ }
+
+ public boolean canView(RepositoryModel repository, String ref) {
+ // Default UserModel doesn't implement ref-level security.
+ // Other Realms (i.e. Gerrit) may override this method.
+ return canView(repository);
+ }
+
+ public boolean canClone(RepositoryModel repository) {
+ return canAccess(repository, AccessRestrictionType.CLONE, AccessPermission.CLONE);
+ }
+
+ public boolean canPush(RepositoryModel repository) {
+ if (repository.isFrozen) {
+ return false;
+ }
+ return canAccess(repository, AccessRestrictionType.PUSH, AccessPermission.PUSH);
+ }
+
+ public boolean canCreateRef(RepositoryModel repository) {
+ if (repository.isFrozen) {
+ return false;
+ }
+ return canAccess(repository, AccessRestrictionType.PUSH, AccessPermission.CREATE);
+ }
+
+ public boolean canDeleteRef(RepositoryModel repository) {
+ if (repository.isFrozen) {
+ return false;
+ }
+ return canAccess(repository, AccessRestrictionType.PUSH, AccessPermission.DELETE);
+ }
+
+ public boolean canRewindRef(RepositoryModel repository) {
+ if (repository.isFrozen) {
+ return false;
+ }
+ return canAccess(repository, AccessRestrictionType.PUSH, AccessPermission.REWIND);
+ }
+
+ public boolean canFork(RepositoryModel repository) {
+ if (repository.isUsersPersonalRepository(username)) {
+ // can not fork your own repository
+ return false;
+ }
+ if (canAdmin() || repository.isOwner(username)) {
+ return true;
+ }
+ if (!repository.allowForks) {
+ return false;
+ }
+ if (!isAuthenticated || !canFork()) {
+ return false;
+ }
+ return canClone(repository);
+ }
+
+ public boolean canDelete(RepositoryModel model) {
+ return canAdmin() || model.isUsersPersonalRepository(username);
+ }
+
+ public boolean canEdit(RepositoryModel model) {
+ return canAdmin() || model.isUsersPersonalRepository(username) || model.isOwner(username);
+ }
+
+ /**
+ * This returns true if the user has fork privileges or the user has fork
+ * privileges because of a team membership.
+ *
+ * @return true if the user can fork
+ */
+ public boolean canFork() {
+ if (canFork) {
+ return true;
+ }
+ if (!ArrayUtils.isEmpty(teams)) {
+ for (TeamModel team : teams) {
+ if (team.canFork) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * This returns true if the user has admin privileges or the user has admin
+ * privileges because of a team membership.
+ *
+ * @return true if the user can admin
+ */
+ public boolean canAdmin() {
+ if (canAdmin) {
+ return true;
+ }
+ if (!ArrayUtils.isEmpty(teams)) {
+ for (TeamModel team : teams) {
+ if (team.canAdmin) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * This returns true if the user has create privileges or the user has create
+ * privileges because of a team membership.
+ *
+ * @return true if the user can admin
+ */
+ public boolean canCreate() {
+ if (canCreate) {
+ return true;
+ }
+ if (!ArrayUtils.isEmpty(teams)) {
+ for (TeamModel team : teams) {
+ if (team.canCreate) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if the user is allowed to create the specified repository.
+ *
+ * @param repository
+ * @return true if the user can create the repository
+ */
+ public boolean canCreate(String repository) {
+ if (canAdmin()) {
+ // admins can create any repository
+ return true;
+ }
+ if (canCreate) {
+ String projectPath = StringUtils.getFirstPathElement(repository);
+ if (!StringUtils.isEmpty(projectPath) && projectPath.equalsIgnoreCase("~" + username)) {
+ // personal repository
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public boolean isTeamMember(String teamname) {
+ for (TeamModel team : teams) {
+ if (team.name.equalsIgnoreCase(teamname)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public TeamModel getTeam(String teamname) {
+ if (teams == null) {
+ return null;
+ }
+ for (TeamModel team : teams) {
+ if (team.name.equalsIgnoreCase(teamname)) {
+ return team;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public String getName() {
+ return username;
+ }
+
+ public String getDisplayName() {
+ if (StringUtils.isEmpty(displayName)) {
+ return username;
+ }
+ return displayName;
+ }
+
+ public String getPersonalPath() {
+ return "~" + username;
+ }
+
+ @Override
+ public int hashCode() {
+ return username.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof UserModel) {
+ return username.equals(((UserModel) o).username);
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return username;
+ }
+
+ @Override
+ public int compareTo(UserModel o) {
+ return username.compareTo(o.username);
+ }
+
+ /**
+ * Returns true if the name/email pair match this user account.
+ *
+ * @param name
+ * @param email
+ * @return true, if the name and email address match this account
+ */
+ public boolean is(String name, String email) {
+ // at a minimum a usename or display name must be supplied
+ if (StringUtils.isEmpty(name)) {
+ return false;
+ }
+ boolean nameVerified = name.equalsIgnoreCase(username) || name.equalsIgnoreCase(getDisplayName());
+ boolean emailVerified = false;
+ if (StringUtils.isEmpty(emailAddress)) {
+ // user account has not specified an email address
+ // rely on username/displayname verification
+ emailVerified = true;
+ } else {
+ // user account has specified an email address
+ // require email address verification
+ if (!StringUtils.isEmpty(email)) {
+ emailVerified = email.equalsIgnoreCase(emailAddress);
+ }
+ }
+ return nameVerified && emailVerified;
+ }
+
+ @Deprecated
+ public boolean hasBranchPermission(String repositoryName, String branch) {
+ // Default UserModel doesn't implement branch-level security. Other Realms (i.e. Gerrit) may override this method.
+ return hasRepositoryPermission(repositoryName) || hasTeamRepositoryPermission(repositoryName);
+ }
+
+ public boolean isMyPersonalRepository(String repository) {
+ String projectPath = StringUtils.getFirstPathElement(repository);
+ return !StringUtils.isEmpty(projectPath) && projectPath.equalsIgnoreCase("~" + username);
+ }
+}
diff --git a/src/main/java/com/gitblit/utils/ActivityUtils.java b/src/main/java/com/gitblit/utils/ActivityUtils.java
new file mode 100644
index 00000000..732fdeb1
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/ActivityUtils.java
@@ -0,0 +1,205 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.utils;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.lang.reflect.Type;
+import java.text.DateFormat;
+import java.text.MessageFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.TimeZone;
+
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+
+import com.gitblit.GitBlit;
+import com.gitblit.models.Activity;
+import com.gitblit.models.GravatarProfile;
+import com.gitblit.models.RefModel;
+import com.gitblit.models.RepositoryCommit;
+import com.gitblit.models.RepositoryModel;
+import com.google.gson.reflect.TypeToken;
+
+/**
+ * Utility class for building activity information from repositories.
+ *
+ * @author James Moger
+ *
+ */
+public class ActivityUtils {
+
+ /**
+ * Gets the recent activity from the repositories for the last daysBack days
+ * on the specified branch.
+ *
+ * @param models
+ * the list of repositories to query
+ * @param daysBack
+ * the number of days back from Now to collect
+ * @param objectId
+ * the branch to retrieve. If this value is null or empty all
+ * branches are queried.
+ * @param timezone
+ * the timezone for aggregating commits
+ * @return
+ */
+ public static List<Activity> getRecentActivity(List<RepositoryModel> models, int daysBack,
+ String objectId, TimeZone timezone) {
+
+ // Activity panel shows last daysBack of activity across all
+ // repositories.
+ Date thresholdDate = new Date(System.currentTimeMillis() - daysBack * TimeUtils.ONEDAY);
+
+ // Build a map of DailyActivity from the available repositories for the
+ // specified threshold date.
+ DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
+ df.setTimeZone(timezone);
+ Calendar cal = Calendar.getInstance();
+ cal.setTimeZone(timezone);
+
+ Map<String, Activity> activity = new HashMap<String, Activity>();
+ for (RepositoryModel model : models) {
+ if (model.maxActivityCommits == -1) {
+ // skip this repository
+ continue;
+ }
+ if (model.hasCommits && model.lastChange.after(thresholdDate)) {
+ if (model.isCollectingGarbage) {
+ continue;
+ }
+ Repository repository = GitBlit.self()
+ .getRepository(model.name);
+ List<String> branches = new ArrayList<String>();
+ if (StringUtils.isEmpty(objectId)) {
+ for (RefModel local : JGitUtils.getLocalBranches(
+ repository, true, -1)) {
+ branches.add(local.getName());
+ }
+ } else {
+ branches.add(objectId);
+ }
+ Map<ObjectId, List<RefModel>> allRefs = JGitUtils
+ .getAllRefs(repository, model.showRemoteBranches);
+
+ for (String branch : branches) {
+ String shortName = branch;
+ if (shortName.startsWith(Constants.R_HEADS)) {
+ shortName = shortName.substring(Constants.R_HEADS.length());
+ }
+ List<RevCommit> commits = JGitUtils.getRevLog(repository,
+ branch, thresholdDate);
+ if (model.maxActivityCommits > 0 && commits.size() > model.maxActivityCommits) {
+ // trim commits to maximum count
+ commits = commits.subList(0, model.maxActivityCommits);
+ }
+ for (RevCommit commit : commits) {
+ Date date = JGitUtils.getCommitDate(commit);
+ String dateStr = df.format(date);
+ if (!activity.containsKey(dateStr)) {
+ // Normalize the date to midnight
+ cal.setTime(date);
+ cal.set(Calendar.HOUR_OF_DAY, 0);
+ cal.set(Calendar.MINUTE, 0);
+ cal.set(Calendar.SECOND, 0);
+ cal.set(Calendar.MILLISECOND, 0);
+ activity.put(dateStr, new Activity(cal.getTime()));
+ }
+ RepositoryCommit commitModel = activity.get(dateStr)
+ .addCommit(model.name, shortName, commit);
+ if (commitModel != null) {
+ commitModel.setRefs(allRefs.get(commit.getId()));
+ }
+ }
+ }
+
+ // close the repository
+ repository.close();
+ }
+ }
+
+ List<Activity> recentActivity = new ArrayList<Activity>(activity.values());
+ return recentActivity;
+ }
+
+ /**
+ * Returns the Gravatar profile, if available, for the specified email
+ * address.
+ *
+ * @param emailaddress
+ * @return a Gravatar Profile
+ * @throws IOException
+ */
+ public static GravatarProfile getGravatarProfileFromAddress(String emailaddress)
+ throws IOException {
+ return getGravatarProfile(StringUtils.getMD5(emailaddress.toLowerCase()));
+ }
+
+ /**
+ * Creates a Gravatar thumbnail url from the specified email address.
+ *
+ * @param email
+ * address to query Gravatar
+ * @param width
+ * size of thumbnail. if width <= 0, the default of 50 is used.
+ * @return
+ */
+ public static String getGravatarThumbnailUrl(String email, int width) {
+ if (width <= 0) {
+ width = 50;
+ }
+ String emailHash = StringUtils.getMD5(email);
+ String url = MessageFormat.format(
+ "https://www.gravatar.com/avatar/{0}?s={1,number,0}&d=identicon", emailHash, width);
+ return url;
+ }
+
+ /**
+ * Returns the Gravatar profile, if available, for the specified hashcode.
+ * address.
+ *
+ * @param hash
+ * the hash of the email address
+ * @return a Gravatar Profile
+ * @throws IOException
+ */
+ public static GravatarProfile getGravatarProfile(String hash) throws IOException {
+ String url = MessageFormat.format("https://www.gravatar.com/{0}.json", hash);
+ // Gravatar has a complex json structure
+ Type profileType = new TypeToken<Map<String, List<GravatarProfile>>>() {
+ }.getType();
+ Map<String, List<GravatarProfile>> profiles = null;
+ try {
+ profiles = JsonUtils.retrieveJson(url, profileType);
+ } catch (FileNotFoundException e) {
+ }
+ if (profiles == null || profiles.size() == 0) {
+ return null;
+ }
+ // due to the complex json structure we need to pull out the profile
+ // from a list 2 levels deep
+ GravatarProfile profile = profiles.values().iterator().next().get(0);
+ return profile;
+ }
+}
diff --git a/src/main/java/com/gitblit/utils/ArrayUtils.java b/src/main/java/com/gitblit/utils/ArrayUtils.java
new file mode 100644
index 00000000..65834673
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/ArrayUtils.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2012 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.utils;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+
+/**
+ * Utility class for arrays and collections.
+ *
+ * @author James Moger
+ *
+ */
+public class ArrayUtils {
+
+ public static boolean isEmpty(byte [] array) {
+ return array == null || array.length == 0;
+ }
+
+ public static boolean isEmpty(char [] array) {
+ return array == null || array.length == 0;
+ }
+
+ public static boolean isEmpty(Object [] array) {
+ return array == null || array.length == 0;
+ }
+
+ public static boolean isEmpty(Collection<?> collection) {
+ return collection == null || collection.size() == 0;
+ }
+
+ public static String toString(Collection<?> collection) {
+ if (isEmpty(collection)) {
+ return "";
+ }
+ StringBuilder sb = new StringBuilder();
+ for (Object o : collection) {
+ sb.append(o.toString()).append(", ");
+ }
+ // trim trailing comma-space
+ sb.setLength(sb.length() - 2);
+ return sb.toString();
+ }
+
+ public static Collection<String> fromString(String value) {
+ if (StringUtils.isEmpty(value)) {
+ value = "";
+ }
+ List<String> list = new ArrayList<String>();
+ String [] values = value.split(",|;");
+ for (String v : values) {
+ String string = v.trim();
+ if (!StringUtils.isEmpty(string)) {
+ list.add(string);
+ }
+ }
+ return list;
+ }
+}
diff --git a/src/main/java/com/gitblit/utils/Base64.java b/src/main/java/com/gitblit/utils/Base64.java
new file mode 100644
index 00000000..6fd2daf1
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/Base64.java
@@ -0,0 +1,311 @@
+//
+// NOTE: The following source code is heavily derived from the
+// iHarder.net public domain Base64 library. See the original at
+// http://iharder.sourceforge.net/current/java/base64/
+//
+
+package com.gitblit.utils;
+
+import java.io.UnsupportedEncodingException;
+import java.text.MessageFormat;
+import java.util.Arrays;
+
+/**
+ * Encodes and decodes to and from Base64 notation.
+ * <p>
+ * I am placing this code in the Public Domain. Do with it as you will. This
+ * software comes with no guarantees or warranties but with plenty of
+ * well-wishing instead! Please visit <a
+ * href="http://iharder.net/base64">http://iharder.net/base64</a> periodically
+ * to check for updates or to contribute improvements.
+ * </p>
+ *
+ * @author Robert Harder
+ * @author rob@iharder.net
+ * @version 2.1, stripped to minimum feature set used by JGit.
+ */
+public class Base64 {
+ /** The equals sign (=) as a byte. */
+ private final static byte EQUALS_SIGN = (byte) '=';
+
+ /** Indicates equals sign in encoding. */
+ private final static byte EQUALS_SIGN_DEC = -1;
+
+ /** Indicates white space in encoding. */
+ private final static byte WHITE_SPACE_DEC = -2;
+
+ /** Indicates an invalid byte during decoding. */
+ private final static byte INVALID_DEC = -3;
+
+ /** Preferred encoding. */
+ private final static String UTF_8 = "UTF-8";
+
+ /** The 64 valid Base64 values. */
+ private final static byte[] ENC;
+
+ /**
+ * Translates a Base64 value to either its 6-bit reconstruction value or a
+ * negative number indicating some other meaning. The table is only 7 bits
+ * wide, as the 8th bit is discarded during decoding.
+ */
+ private final static byte[] DEC;
+
+ static {
+ try {
+ ENC = ("ABCDEFGHIJKLMNOPQRSTUVWXYZ" //
+ + "abcdefghijklmnopqrstuvwxyz" //
+ + "0123456789" //
+ + "+/" //
+ ).getBytes(UTF_8);
+ } catch (UnsupportedEncodingException uee) {
+ throw new RuntimeException(uee.getMessage(), uee);
+ }
+
+ DEC = new byte[128];
+ Arrays.fill(DEC, INVALID_DEC);
+
+ for (int i = 0; i < 64; i++)
+ DEC[ENC[i]] = (byte) i;
+ DEC[EQUALS_SIGN] = EQUALS_SIGN_DEC;
+
+ DEC['\t'] = WHITE_SPACE_DEC;
+ DEC['\n'] = WHITE_SPACE_DEC;
+ DEC['\r'] = WHITE_SPACE_DEC;
+ DEC[' '] = WHITE_SPACE_DEC;
+ }
+
+ /** Defeats instantiation. */
+ private Base64() {
+ // Suppress empty block warning.
+ }
+
+ /**
+ * Encodes up to three bytes of the array <var>source</var> and writes the
+ * resulting four Base64 bytes to <var>destination</var>. The source and
+ * destination arrays can be manipulated anywhere along their length by
+ * specifying <var>srcOffset</var> and <var>destOffset</var>. This method
+ * does not check to make sure your arrays are large enough to accommodate
+ * <var>srcOffset</var> + 3 for the <var>source</var> array or
+ * <var>destOffset</var> + 4 for the <var>destination</var> array. The
+ * actual number of significant bytes in your array is given by
+ * <var>numSigBytes</var>.
+ *
+ * @param source
+ * the array to convert
+ * @param srcOffset
+ * the index where conversion begins
+ * @param numSigBytes
+ * the number of significant bytes in your array
+ * @param destination
+ * the array to hold the conversion
+ * @param destOffset
+ * the index where output will be put
+ */
+ private static void encode3to4(byte[] source, int srcOffset, int numSigBytes,
+ byte[] destination, int destOffset) {
+ // We have to shift left 24 in order to flush out the 1's that appear
+ // when Java treats a value as negative that is cast from a byte.
+
+ int inBuff = 0;
+ switch (numSigBytes) {
+ case 3:
+ inBuff |= (source[srcOffset + 2] << 24) >>> 24;
+ //$FALL-THROUGH$
+
+ case 2:
+ inBuff |= (source[srcOffset + 1] << 24) >>> 16;
+ //$FALL-THROUGH$
+
+ case 1:
+ inBuff |= (source[srcOffset] << 24) >>> 8;
+ }
+
+ switch (numSigBytes) {
+ case 3:
+ destination[destOffset] = ENC[(inBuff >>> 18)];
+ destination[destOffset + 1] = ENC[(inBuff >>> 12) & 0x3f];
+ destination[destOffset + 2] = ENC[(inBuff >>> 6) & 0x3f];
+ destination[destOffset + 3] = ENC[(inBuff) & 0x3f];
+ break;
+
+ case 2:
+ destination[destOffset] = ENC[(inBuff >>> 18)];
+ destination[destOffset + 1] = ENC[(inBuff >>> 12) & 0x3f];
+ destination[destOffset + 2] = ENC[(inBuff >>> 6) & 0x3f];
+ destination[destOffset + 3] = EQUALS_SIGN;
+ break;
+
+ case 1:
+ destination[destOffset] = ENC[(inBuff >>> 18)];
+ destination[destOffset + 1] = ENC[(inBuff >>> 12) & 0x3f];
+ destination[destOffset + 2] = EQUALS_SIGN;
+ destination[destOffset + 3] = EQUALS_SIGN;
+ break;
+ }
+ }
+
+ /**
+ * Encodes a byte array into Base64 notation.
+ *
+ * @param source
+ * The data to convert
+ * @return encoded base64 representation of source.
+ */
+ public static String encodeBytes(byte[] source) {
+ return encodeBytes(source, 0, source.length);
+ }
+
+ /**
+ * Encodes a byte array into Base64 notation.
+ *
+ * @param source
+ * The data to convert
+ * @param off
+ * Offset in array where conversion should begin
+ * @param len
+ * Length of data to convert
+ * @return encoded base64 representation of source.
+ */
+ public static String encodeBytes(byte[] source, int off, int len) {
+ final int len43 = len * 4 / 3;
+
+ byte[] outBuff = new byte[len43 + ((len % 3) > 0 ? 4 : 0)];
+ int d = 0;
+ int e = 0;
+ int len2 = len - 2;
+
+ for (; d < len2; d += 3, e += 4)
+ encode3to4(source, d + off, 3, outBuff, e);
+
+ if (d < len) {
+ encode3to4(source, d + off, len - d, outBuff, e);
+ e += 4;
+ }
+
+ try {
+ return new String(outBuff, 0, e, UTF_8);
+ } catch (UnsupportedEncodingException uue) {
+ return new String(outBuff, 0, e);
+ }
+ }
+
+ /**
+ * Decodes four bytes from array <var>source</var> and writes the resulting
+ * bytes (up to three of them) to <var>destination</var>. The source and
+ * destination arrays can be manipulated anywhere along their length by
+ * specifying <var>srcOffset</var> and <var>destOffset</var>. This method
+ * does not check to make sure your arrays are large enough to accommodate
+ * <var>srcOffset</var> + 4 for the <var>source</var> array or
+ * <var>destOffset</var> + 3 for the <var>destination</var> array. This
+ * method returns the actual number of bytes that were converted from the
+ * Base64 encoding.
+ *
+ * @param source
+ * the array to convert
+ * @param srcOffset
+ * the index where conversion begins
+ * @param destination
+ * the array to hold the conversion
+ * @param destOffset
+ * the index where output will be put
+ * @return the number of decoded bytes converted
+ */
+ private static int decode4to3(byte[] source, int srcOffset, byte[] destination, int destOffset) {
+ // Example: Dk==
+ if (source[srcOffset + 2] == EQUALS_SIGN) {
+ int outBuff = ((DEC[source[srcOffset]] & 0xFF) << 18)
+ | ((DEC[source[srcOffset + 1]] & 0xFF) << 12);
+ destination[destOffset] = (byte) (outBuff >>> 16);
+ return 1;
+ }
+
+ // Example: DkL=
+ else if (source[srcOffset + 3] == EQUALS_SIGN) {
+ int outBuff = ((DEC[source[srcOffset]] & 0xFF) << 18)
+ | ((DEC[source[srcOffset + 1]] & 0xFF) << 12)
+ | ((DEC[source[srcOffset + 2]] & 0xFF) << 6);
+ destination[destOffset] = (byte) (outBuff >>> 16);
+ destination[destOffset + 1] = (byte) (outBuff >>> 8);
+ return 2;
+ }
+
+ // Example: DkLE
+ else {
+ int outBuff = ((DEC[source[srcOffset]] & 0xFF) << 18)
+ | ((DEC[source[srcOffset + 1]] & 0xFF) << 12)
+ | ((DEC[source[srcOffset + 2]] & 0xFF) << 6)
+ | ((DEC[source[srcOffset + 3]] & 0xFF));
+
+ destination[destOffset] = (byte) (outBuff >> 16);
+ destination[destOffset + 1] = (byte) (outBuff >> 8);
+ destination[destOffset + 2] = (byte) (outBuff);
+
+ return 3;
+ }
+ }
+
+ /**
+ * Low-level decoding ASCII characters from a byte array.
+ *
+ * @param source
+ * The Base64 encoded data
+ * @param off
+ * The offset of where to begin decoding
+ * @param len
+ * The length of characters to decode
+ * @return decoded data
+ * @throws IllegalArgumentException
+ * the input is not a valid Base64 sequence.
+ */
+ public static byte[] decode(byte[] source, int off, int len) {
+ byte[] outBuff = new byte[len * 3 / 4]; // Upper limit on size of output
+ int outBuffPosn = 0;
+
+ byte[] b4 = new byte[4];
+ int b4Posn = 0;
+
+ for (int i = off; i < off + len; i++) {
+ byte sbiCrop = (byte) (source[i] & 0x7f);
+ byte sbiDecode = DEC[sbiCrop];
+
+ if (EQUALS_SIGN_DEC <= sbiDecode) {
+ b4[b4Posn++] = sbiCrop;
+ if (b4Posn > 3) {
+ outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn);
+ b4Posn = 0;
+
+ // If that was the equals sign, break out of 'for' loop
+ if (sbiCrop == EQUALS_SIGN)
+ break;
+ }
+
+ } else if (sbiDecode != WHITE_SPACE_DEC)
+ throw new IllegalArgumentException(MessageFormat.format(
+ "bad base64 input character {1} at {0}", i, source[i] & 0xff));
+ }
+
+ if (outBuff.length == outBuffPosn)
+ return outBuff;
+
+ byte[] out = new byte[outBuffPosn];
+ System.arraycopy(outBuff, 0, out, 0, outBuffPosn);
+ return out;
+ }
+
+ /**
+ * Decodes data from Base64 notation.
+ *
+ * @param s
+ * the string to decode
+ * @return the decoded data
+ */
+ public static byte[] decode(String s) {
+ byte[] bytes;
+ try {
+ bytes = s.getBytes(UTF_8);
+ } catch (UnsupportedEncodingException uee) {
+ bytes = s.getBytes();
+ }
+ return decode(bytes, 0, bytes.length);
+ }
+}
diff --git a/src/main/java/com/gitblit/utils/ByteFormat.java b/src/main/java/com/gitblit/utils/ByteFormat.java
new file mode 100644
index 00000000..cb7da885
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/ByteFormat.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.utils;
+
+import java.text.DecimalFormat;
+import java.text.FieldPosition;
+import java.text.Format;
+import java.text.ParsePosition;
+
+/**
+ * ByteFormat is a formatter which takes numbers and returns filesizes in bytes,
+ * kilobytes, megabytes, or gigabytes.
+ *
+ * @author James Moger
+ *
+ */
+public class ByteFormat extends Format {
+
+ private static final long serialVersionUID = 1L;
+
+ public ByteFormat() {
+ }
+
+ public String format(long value) {
+ return format(Long.valueOf(value));
+ }
+
+ public StringBuffer format(Object obj, StringBuffer buf, FieldPosition pos) {
+ if (obj instanceof Number) {
+ long numBytes = ((Number) obj).longValue();
+ if (numBytes < 1024) {
+ DecimalFormat formatter = new DecimalFormat("#,##0");
+ buf.append(formatter.format((double) numBytes)).append(" b");
+ } else if (numBytes < 1024 * 1024) {
+ DecimalFormat formatter = new DecimalFormat("#,##0");
+ buf.append(formatter.format((double) numBytes / 1024.0)).append(" KB");
+ } else if (numBytes < 1024 * 1024 * 1024) {
+ DecimalFormat formatter = new DecimalFormat("#,##0.0");
+ buf.append(formatter.format((double) numBytes / (1024.0 * 1024.0))).append(" MB");
+ } else {
+ DecimalFormat formatter = new DecimalFormat("#,##0.0");
+ buf.append(formatter.format((double) numBytes / (1024.0 * 1024.0 * 1024.0)))
+ .append(" GB");
+ }
+ }
+ return buf;
+ }
+
+ public Object parseObject(String source, ParsePosition pos) {
+ return null;
+ }
+}
diff --git a/src/main/java/com/gitblit/utils/ClientLogger.java b/src/main/java/com/gitblit/utils/ClientLogger.java
new file mode 100644
index 00000000..7d18f3d6
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/ClientLogger.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2012 John Crygier
+ * Copyright 2012 gitblit.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.utils;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+import org.eclipse.jgit.transport.ReceivePack;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Class to log messages to the pushing Git client. Intended to be used by the
+ * Groovy Hooks.
+ *
+ * @author John Crygier
+ *
+ */
+public class ClientLogger {
+
+ static final Logger logger = LoggerFactory.getLogger(ClientLogger.class);
+ private ReceivePack rp;
+
+ public ClientLogger(ReceivePack rp) {
+ this.rp = rp;
+ }
+
+ /**
+ * Sends an info/warning message to the git client.
+ *
+ * @param message
+ */
+ public void info(String message) {
+ rp.sendMessage(message);
+ }
+
+ /**
+ * Sends an error message to the git client.
+ *
+ * @param message
+ */
+ public void error(String message) {
+ rp.sendError(message);
+ }
+
+ /**
+ * Sends an error message to the git client with an exception.
+ *
+ * @param message
+ * @param t
+ * an exception
+ */
+ public void error(String message, Throwable t) {
+ PrintWriter writer = new PrintWriter(new StringWriter());
+ if (!StringUtils.isEmpty(message)) {
+ writer.append(message);
+ writer.append('\n');
+ }
+ t.printStackTrace(writer);
+ rp.sendError(writer.toString());
+ }
+
+}
diff --git a/src/main/java/com/gitblit/utils/CompressionUtils.java b/src/main/java/com/gitblit/utils/CompressionUtils.java
new file mode 100644
index 00000000..a8dcdd8f
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/CompressionUtils.java
@@ -0,0 +1,299 @@
+/*
+ * Copyright 2012 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.utils;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
+import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
+import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
+import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
+import org.apache.commons.compress.compressors.CompressorException;
+import org.apache.commons.compress.compressors.CompressorStreamFactory;
+import org.apache.wicket.util.io.ByteArrayOutputStream;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.lib.MutableObjectId;
+import org.eclipse.jgit.lib.ObjectLoader;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.treewalk.filter.PathFilter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Collection of static methods for retrieving information from a repository.
+ *
+ * @author James Moger
+ *
+ */
+public class CompressionUtils {
+
+ static final Logger LOGGER = LoggerFactory.getLogger(CompressionUtils.class);
+
+ /**
+ * Log an error message and exception.
+ *
+ * @param t
+ * @param repository
+ * if repository is not null it MUST be the {0} parameter in the
+ * pattern.
+ * @param pattern
+ * @param objects
+ */
+ private static void error(Throwable t, Repository repository, String pattern, Object... objects) {
+ List<Object> parameters = new ArrayList<Object>();
+ if (objects != null && objects.length > 0) {
+ for (Object o : objects) {
+ parameters.add(o);
+ }
+ }
+ if (repository != null) {
+ parameters.add(0, repository.getDirectory().getAbsolutePath());
+ }
+ LOGGER.error(MessageFormat.format(pattern, parameters.toArray()), t);
+ }
+
+ /**
+ * Zips the contents of the tree at the (optionally) specified revision and
+ * the (optionally) specified basepath to the supplied outputstream.
+ *
+ * @param repository
+ * @param basePath
+ * if unspecified, entire repository is assumed.
+ * @param objectId
+ * if unspecified, HEAD is assumed.
+ * @param os
+ * @return true if repository was successfully zipped to supplied output
+ * stream
+ */
+ public static boolean zip(Repository repository, String basePath, String objectId,
+ OutputStream os) {
+ RevCommit commit = JGitUtils.getCommit(repository, objectId);
+ if (commit == null) {
+ return false;
+ }
+ boolean success = false;
+ RevWalk rw = new RevWalk(repository);
+ TreeWalk tw = new TreeWalk(repository);
+ try {
+ tw.reset();
+ tw.addTree(commit.getTree());
+ ZipArchiveOutputStream zos = new ZipArchiveOutputStream(os);
+ zos.setComment("Generated by Gitblit");
+ if (!StringUtils.isEmpty(basePath)) {
+ PathFilter f = PathFilter.create(basePath);
+ tw.setFilter(f);
+ }
+ tw.setRecursive(true);
+ MutableObjectId id = new MutableObjectId();
+ ObjectReader reader = tw.getObjectReader();
+ long modified = commit.getAuthorIdent().getWhen().getTime();
+ while (tw.next()) {
+ FileMode mode = tw.getFileMode(0);
+ if (mode == FileMode.GITLINK || mode == FileMode.TREE) {
+ continue;
+ }
+ tw.getObjectId(id, 0);
+
+ ZipArchiveEntry entry = new ZipArchiveEntry(tw.getPathString());
+ entry.setSize(reader.getObjectSize(id, Constants.OBJ_BLOB));
+ entry.setComment(commit.getName());
+ entry.setUnixMode(mode.getBits());
+ entry.setTime(modified);
+ zos.putArchiveEntry(entry);
+
+ ObjectLoader ldr = repository.open(id);
+ ldr.copyTo(zos);
+ zos.closeArchiveEntry();
+ }
+ zos.finish();
+ success = true;
+ } catch (IOException e) {
+ error(e, repository, "{0} failed to zip files from commit {1}", commit.getName());
+ } finally {
+ tw.release();
+ rw.dispose();
+ }
+ return success;
+ }
+
+ /**
+ * tar the contents of the tree at the (optionally) specified revision and
+ * the (optionally) specified basepath to the supplied outputstream.
+ *
+ * @param repository
+ * @param basePath
+ * if unspecified, entire repository is assumed.
+ * @param objectId
+ * if unspecified, HEAD is assumed.
+ * @param os
+ * @return true if repository was successfully zipped to supplied output
+ * stream
+ */
+ public static boolean tar(Repository repository, String basePath, String objectId,
+ OutputStream os) {
+ return tar(null, repository, basePath, objectId, os);
+ }
+
+ /**
+ * tar.gz the contents of the tree at the (optionally) specified revision and
+ * the (optionally) specified basepath to the supplied outputstream.
+ *
+ * @param repository
+ * @param basePath
+ * if unspecified, entire repository is assumed.
+ * @param objectId
+ * if unspecified, HEAD is assumed.
+ * @param os
+ * @return true if repository was successfully zipped to supplied output
+ * stream
+ */
+ public static boolean gz(Repository repository, String basePath, String objectId,
+ OutputStream os) {
+ return tar(CompressorStreamFactory.GZIP, repository, basePath, objectId, os);
+ }
+
+ /**
+ * tar.xz the contents of the tree at the (optionally) specified revision and
+ * the (optionally) specified basepath to the supplied outputstream.
+ *
+ * @param repository
+ * @param basePath
+ * if unspecified, entire repository is assumed.
+ * @param objectId
+ * if unspecified, HEAD is assumed.
+ * @param os
+ * @return true if repository was successfully zipped to supplied output
+ * stream
+ */
+ public static boolean xz(Repository repository, String basePath, String objectId,
+ OutputStream os) {
+ return tar(CompressorStreamFactory.XZ, repository, basePath, objectId, os);
+ }
+
+ /**
+ * tar.bzip2 the contents of the tree at the (optionally) specified revision and
+ * the (optionally) specified basepath to the supplied outputstream.
+ *
+ * @param repository
+ * @param basePath
+ * if unspecified, entire repository is assumed.
+ * @param objectId
+ * if unspecified, HEAD is assumed.
+ * @param os
+ * @return true if repository was successfully zipped to supplied output
+ * stream
+ */
+ public static boolean bzip2(Repository repository, String basePath, String objectId,
+ OutputStream os) {
+
+ return tar(CompressorStreamFactory.BZIP2, repository, basePath, objectId, os);
+ }
+
+ /**
+ * Compresses/archives the contents of the tree at the (optionally)
+ * specified revision and the (optionally) specified basepath to the
+ * supplied outputstream.
+ *
+ * @param algorithm
+ * compression algorithm for tar (optional)
+ * @param repository
+ * @param basePath
+ * if unspecified, entire repository is assumed.
+ * @param objectId
+ * if unspecified, HEAD is assumed.
+ * @param os
+ * @return true if repository was successfully zipped to supplied output
+ * stream
+ */
+ private static boolean tar(String algorithm, Repository repository, String basePath, String objectId,
+ OutputStream os) {
+ RevCommit commit = JGitUtils.getCommit(repository, objectId);
+ if (commit == null) {
+ return false;
+ }
+
+ OutputStream cos = os;
+ if (!StringUtils.isEmpty(algorithm)) {
+ try {
+ cos = new CompressorStreamFactory().createCompressorOutputStream(algorithm, os);
+ } catch (CompressorException e1) {
+ error(e1, repository, "{0} failed to open {1} stream", algorithm);
+ }
+ }
+ boolean success = false;
+ RevWalk rw = new RevWalk(repository);
+ TreeWalk tw = new TreeWalk(repository);
+ try {
+ tw.reset();
+ tw.addTree(commit.getTree());
+ TarArchiveOutputStream tos = new TarArchiveOutputStream(cos);
+ tos.setAddPaxHeadersForNonAsciiNames(true);
+ tos.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX);
+ if (!StringUtils.isEmpty(basePath)) {
+ PathFilter f = PathFilter.create(basePath);
+ tw.setFilter(f);
+ }
+ tw.setRecursive(true);
+ MutableObjectId id = new MutableObjectId();
+ long modified = commit.getAuthorIdent().getWhen().getTime();
+ while (tw.next()) {
+ FileMode mode = tw.getFileMode(0);
+ if (mode == FileMode.GITLINK || mode == FileMode.TREE) {
+ continue;
+ }
+ tw.getObjectId(id, 0);
+
+ ObjectLoader loader = repository.open(id);
+ if (FileMode.SYMLINK == mode) {
+ TarArchiveEntry entry = new TarArchiveEntry(tw.getPathString(),TarArchiveEntry.LF_SYMLINK);
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ loader.copyTo(bos);
+ entry.setLinkName(bos.toString());
+ entry.setModTime(modified);
+ tos.putArchiveEntry(entry);
+ tos.closeArchiveEntry();
+ } else {
+ TarArchiveEntry entry = new TarArchiveEntry(tw.getPathString());
+ entry.setMode(mode.getBits());
+ entry.setModTime(modified);
+ entry.setSize(loader.getSize());
+ tos.putArchiveEntry(entry);
+ loader.copyTo(tos);
+ tos.closeArchiveEntry();
+ }
+ }
+ tos.finish();
+ tos.close();
+ cos.close();
+ success = true;
+ } catch (IOException e) {
+ error(e, repository, "{0} failed to {1} stream files from commit {2}", algorithm, commit.getName());
+ } finally {
+ tw.release();
+ rw.dispose();
+ }
+ return success;
+ }
+}
diff --git a/src/main/java/com/gitblit/utils/ConnectionUtils.java b/src/main/java/com/gitblit/utils/ConnectionUtils.java
new file mode 100644
index 00000000..f0b41118
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/ConnectionUtils.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.utils;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.net.URL;
+import java.net.URLConnection;
+import java.net.UnknownHostException;
+import java.security.GeneralSecurityException;
+import java.security.SecureRandom;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+
+import javax.net.SocketFactory;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+
+
+/**
+ * Utility class for establishing HTTP/HTTPS connections.
+ *
+ * @author James Moger
+ *
+ */
+public class ConnectionUtils {
+
+ static final String CHARSET;
+
+ private static final SSLContext SSL_CONTEXT;
+
+ private static final DummyHostnameVerifier HOSTNAME_VERIFIER;
+
+ static {
+ SSLContext context = null;
+ try {
+ context = SSLContext.getInstance("SSL");
+ context.init(null, new TrustManager[] { new DummyTrustManager() }, new SecureRandom());
+ } catch (Throwable t) {
+ t.printStackTrace();
+ }
+ SSL_CONTEXT = context;
+ HOSTNAME_VERIFIER = new DummyHostnameVerifier();
+ CHARSET = "UTF-8";
+ }
+
+ public static void setAuthorization(URLConnection conn, String username, char[] password) {
+ if (!StringUtils.isEmpty(username) && (password != null && password.length > 0)) {
+ conn.setRequestProperty(
+ "Authorization",
+ "Basic "
+ + Base64.encodeBytes((username + ":" + new String(password)).getBytes()));
+ }
+ }
+
+ public static URLConnection openReadConnection(String url, String username, char[] password)
+ throws IOException {
+ URLConnection conn = openConnection(url, username, password);
+ conn.setRequestProperty("Accept-Charset", ConnectionUtils.CHARSET);
+ return conn;
+ }
+
+ public static URLConnection openConnection(String url, String username, char[] password)
+ throws IOException {
+ URL urlObject = new URL(url);
+ URLConnection conn = urlObject.openConnection();
+ setAuthorization(conn, username, password);
+ conn.setUseCaches(false);
+ conn.setDoOutput(true);
+ if (conn instanceof HttpsURLConnection) {
+ HttpsURLConnection secureConn = (HttpsURLConnection) conn;
+ secureConn.setSSLSocketFactory(SSL_CONTEXT.getSocketFactory());
+ secureConn.setHostnameVerifier(HOSTNAME_VERIFIER);
+ }
+ return conn;
+ }
+
+ // Copyright (C) 2009 The Android Open Source Project
+ //
+ // Licensed under the Apache License, Version 2.0 (the "License");
+ // you may not use this file except in compliance with the License.
+ // You may obtain a copy of the License at
+ //
+ // http://www.apache.org/licenses/LICENSE-2.0
+ //
+ // Unless required by applicable law or agreed to in writing, software
+ // distributed under the License is distributed on an "AS IS" BASIS,
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ // See the License for the specific language governing permissions and
+ // limitations under the License.
+ public static class BlindSSLSocketFactory extends SSLSocketFactory {
+ private static final BlindSSLSocketFactory INSTANCE;
+
+ static {
+ try {
+ final SSLContext context = SSLContext.getInstance("SSL");
+ final TrustManager[] trustManagers = { new DummyTrustManager() };
+ final SecureRandom rng = new SecureRandom();
+ context.init(null, trustManagers, rng);
+ INSTANCE = new BlindSSLSocketFactory(context.getSocketFactory());
+ } catch (GeneralSecurityException e) {
+ throw new RuntimeException("Cannot create BlindSslSocketFactory", e);
+ }
+ }
+
+ public static SocketFactory getDefault() {
+ return INSTANCE;
+ }
+
+ private final SSLSocketFactory sslFactory;
+
+ private BlindSSLSocketFactory(final SSLSocketFactory sslFactory) {
+ this.sslFactory = sslFactory;
+ }
+
+ @Override
+ public Socket createSocket(Socket s, String host, int port, boolean autoClose)
+ throws IOException {
+ return sslFactory.createSocket(s, host, port, autoClose);
+ }
+
+ @Override
+ public String[] getDefaultCipherSuites() {
+ return sslFactory.getDefaultCipherSuites();
+ }
+
+ @Override
+ public String[] getSupportedCipherSuites() {
+ return sslFactory.getSupportedCipherSuites();
+ }
+
+ @Override
+ public Socket createSocket() throws IOException {
+ return sslFactory.createSocket();
+ }
+
+ @Override
+ public Socket createSocket(String host, int port) throws IOException,
+ UnknownHostException {
+ return sslFactory.createSocket(host, port);
+ }
+
+ @Override
+ public Socket createSocket(InetAddress host, int port) throws IOException {
+ return sslFactory.createSocket(host, port);
+ }
+
+ @Override
+ public Socket createSocket(String host, int port, InetAddress localHost,
+ int localPort) throws IOException, UnknownHostException {
+ return sslFactory.createSocket(host, port, localHost, localPort);
+ }
+
+ @Override
+ public Socket createSocket(InetAddress address, int port,
+ InetAddress localAddress, int localPort) throws IOException {
+ return sslFactory.createSocket(address, port, localAddress, localPort);
+ }
+ }
+
+ /**
+ * DummyTrustManager trusts all certificates.
+ *
+ * @author James Moger
+ */
+ private static class DummyTrustManager implements X509TrustManager {
+
+ @Override
+ public void checkClientTrusted(X509Certificate[] certs, String authType)
+ throws CertificateException {
+ }
+
+ @Override
+ public void checkServerTrusted(X509Certificate[] certs, String authType)
+ throws CertificateException {
+ }
+
+ @Override
+ public X509Certificate[] getAcceptedIssuers() {
+ return null;
+ }
+ }
+
+ /**
+ * Trusts all hostnames from a certificate, including self-signed certs.
+ *
+ * @author James Moger
+ */
+ private static class DummyHostnameVerifier implements HostnameVerifier {
+ @Override
+ public boolean verify(String hostname, SSLSession session) {
+ return true;
+ }
+ }
+}
diff --git a/src/main/java/com/gitblit/utils/ContainerUtils.java b/src/main/java/com/gitblit/utils/ContainerUtils.java
new file mode 100644
index 00000000..919f99d6
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/ContainerUtils.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2012 PD Inc / gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.utils;
+
+import java.io.CharConversionException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.GitBlit;
+import com.gitblit.Keys;
+
+/**
+ * This is the support class for all container specific code.
+ *
+ * @author jpyeron
+ */
+public class ContainerUtils
+{
+ private static Logger LOGGER = LoggerFactory.getLogger(ContainerUtils.class);
+
+ /**
+ * The support class for managing and evaluating the environment with
+ * regards to CVE-2007-0405.
+ *
+ * @see http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2007-0450
+ * @author jpyeron
+ */
+ public static class CVE_2007_0450
+ {
+ /**
+ * This method will test for know issues in certain containers where %2F
+ * is blocked from use in URLs. It will emit a warning to the logger if
+ * the configuration of Tomcat causes the URL processing to fail on %2F.
+ */
+ public static void test()
+ {
+ if (GitBlit.getBoolean(Keys.web.mountParameters, true)
+ && ((GitBlit.getChar(Keys.web.forwardSlashCharacter, '/')) == '/' || (GitBlit.getChar(
+ Keys.web.forwardSlashCharacter, '/')) == '\\'))
+ {
+ try
+ {
+ if (GitBlit.isGO())
+ ;
+ else if (logCVE_2007_0450Tomcat())
+ ;
+ // else if (logCVE_2007_0450xxx());
+ else
+ {
+ LOGGER.info("Unknown container, cannot check for CVE-2007-0450 aplicability");
+ }
+ }
+ catch (Throwable t)
+ {
+ LOGGER.warn("Failure in checking for CVE-2007-0450 aplicability", t);
+ }
+ }
+
+ }
+
+ /**
+ * This method will test for know issues in certain versions of Tomcat,
+ * JBOSS, glassfish, and other embedded uses of Tomcat where %2F is
+ * blocked from use in certain URL s. It will emit a warning to the
+ * logger if the configuration of Tomcat causes the URL processing to
+ * fail on %2F.
+ *
+ * @return true if it recognizes Tomcat, false if it does not recognize
+ * Tomcat
+ */
+ private static boolean logCVE_2007_0450Tomcat()
+ {
+ try
+ {
+ byte[] test = "http://server.domain:8080/context/servlet/param%2fparam".getBytes();
+
+ // ByteChunk mb=new ByteChunk();
+ Class<?> cByteChunk = Class.forName("org.apache.tomcat.util.buf.ByteChunk");
+ Object mb = cByteChunk.newInstance();
+
+ // mb.setBytes(test, 0, test.length);
+ Method mByteChunck_setBytes = cByteChunk.getMethod("setBytes", byte[].class, int.class, int.class);
+ mByteChunck_setBytes.invoke(mb, test, (int) 0, test.length);
+
+ // UDecoder ud=new UDecoder();
+ Class<?> cUDecoder = Class.forName("org.apache.tomcat.util.buf.UDecoder");
+ Object ud = cUDecoder.newInstance();
+
+ // ud.convert(mb,false);
+ Method mUDecoder_convert = cUDecoder.getMethod("convert", cByteChunk, boolean.class);
+
+ try
+ {
+ mUDecoder_convert.invoke(ud, mb, false);
+ }
+ catch (InvocationTargetException e)
+ {
+ if (e.getTargetException() != null && e.getTargetException() instanceof CharConversionException)
+ {
+ LOGGER.warn("You are using a Tomcat-based server and your current settings will prevent grouped repositories, forks, personal repositories, and tree navigation from working properly. Please review the FAQ for details about running Gitblit on Tomcat. http://gitblit.com/faq.html and http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2007-0450");
+ return true;
+ }
+ throw e;
+ }
+ }
+ catch (Throwable t)
+ {
+ // The apache url decoder internals are different, this is not a
+ // Tomcat matching the failure pattern for CVE-2007-0450
+ if (t instanceof ClassNotFoundException || t instanceof NoSuchMethodException
+ || t instanceof IllegalArgumentException)
+ return false;
+ LOGGER.debug("This is a tomcat, but the test operation failed somehow", t);
+ }
+ return true;
+ }
+ }
+
+}
diff --git a/src/main/java/com/gitblit/utils/DeepCopier.java b/src/main/java/com/gitblit/utils/DeepCopier.java
new file mode 100644
index 00000000..5df30623
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/DeepCopier.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.utils;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+
+public class DeepCopier {
+
+ /**
+ * Produce a deep copy of the given object. Serializes the entire object to
+ * a byte array in memory. Recommended for relatively small objects.
+ */
+ @SuppressWarnings("unchecked")
+ public static <T> T copy(T original) {
+ T o = null;
+ try {
+ ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
+ ObjectOutputStream oos = new ObjectOutputStream(byteOut);
+ oos.writeObject(original);
+ ByteArrayInputStream byteIn = new ByteArrayInputStream(byteOut.toByteArray());
+ ObjectInputStream ois = new ObjectInputStream(byteIn);
+ try {
+ o = (T) ois.readObject();
+ } catch (ClassNotFoundException cex) {
+ // actually can not happen in this instance
+ }
+ } catch (IOException iox) {
+ // doesn't seem likely to happen as these streams are in memory
+ throw new RuntimeException(iox);
+ }
+ return o;
+ }
+
+ /**
+ * This conserves heap memory!!!!! Produce a deep copy of the given object.
+ * Serializes the object through a pipe between two threads. Recommended for
+ * very large objects. The current thread is used for serializing the
+ * original object in order to respect any synchronization the caller may
+ * have around it, and a new thread is used for deserializing the copy.
+ *
+ */
+ public static <T> T copyParallel(T original) {
+ try {
+ PipedOutputStream outputStream = new PipedOutputStream();
+ PipedInputStream inputStream = new PipedInputStream(outputStream);
+ ObjectOutputStream ois = new ObjectOutputStream(outputStream);
+ Receiver<T> receiver = new Receiver<T>(inputStream);
+ try {
+ ois.writeObject(original);
+ } finally {
+ ois.close();
+ }
+ return receiver.getResult();
+ } catch (IOException iox) {
+ // doesn't seem likely to happen as these streams are in memory
+ throw new RuntimeException(iox);
+ }
+ }
+
+ private static class Receiver<T> extends Thread {
+
+ private final InputStream inputStream;
+ private volatile T result;
+ private volatile Throwable throwable;
+
+ public Receiver(InputStream inputStream) {
+ this.inputStream = inputStream;
+ start();
+ }
+
+ @SuppressWarnings("unchecked")
+ public void run() {
+
+ try {
+ ObjectInputStream ois = new ObjectInputStream(inputStream);
+ try {
+ result = (T) ois.readObject();
+ try {
+ // Some serializers may write more than they actually
+ // need to deserialize the object, but if we don't
+ // read it all the PipedOutputStream will choke.
+ while (inputStream.read() != -1) {
+ }
+ } catch (IOException e) {
+ // The object has been successfully deserialized, so
+ // ignore problems at this point (for example, the
+ // serializer may have explicitly closed the inputStream
+ // itself, causing this read to fail).
+ }
+ } finally {
+ ois.close();
+ }
+ } catch (Throwable t) {
+ throwable = t;
+ }
+ }
+
+ public T getResult() throws IOException {
+ try {
+ join();
+ } catch (InterruptedException e) {
+ throw new RuntimeException("Unexpected InterruptedException", e);
+ }
+ // join() guarantees that all shared memory is synchronized between
+ // the two threads
+ if (throwable != null) {
+ if (throwable instanceof ClassNotFoundException) {
+ // actually can not happen in this instance
+ }
+ throw new RuntimeException(throwable);
+ }
+ return result;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main/java/com/gitblit/utils/DiffUtils.java b/src/main/java/com/gitblit/utils/DiffUtils.java
new file mode 100644
index 00000000..04b5b0b1
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/DiffUtils.java
@@ -0,0 +1,281 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.utils;
+
+import java.io.ByteArrayOutputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.jgit.api.BlameCommand;
+import org.eclipse.jgit.blame.BlameResult;
+import org.eclipse.jgit.diff.DiffEntry;
+import org.eclipse.jgit.diff.DiffFormatter;
+import org.eclipse.jgit.diff.RawText;
+import org.eclipse.jgit.diff.RawTextComparator;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevTree;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.models.AnnotatedLine;
+
+/**
+ * DiffUtils is a class of utility methods related to diff, patch, and blame.
+ *
+ * The diff methods support pluggable diff output types like Gitblit, Gitweb,
+ * and Plain.
+ *
+ * @author James Moger
+ *
+ */
+public class DiffUtils {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(DiffUtils.class);
+
+ /**
+ * Enumeration for the diff output types.
+ */
+ public static enum DiffOutputType {
+ PLAIN, GITWEB, GITBLIT;
+
+ public static DiffOutputType forName(String name) {
+ for (DiffOutputType type : values()) {
+ if (type.name().equalsIgnoreCase(name)) {
+ return type;
+ }
+ }
+ return null;
+ }
+ }
+
+ /**
+ * Returns the complete diff of the specified commit compared to its primary
+ * parent.
+ *
+ * @param repository
+ * @param commit
+ * @param outputType
+ * @return the diff as a string
+ */
+ public static String getCommitDiff(Repository repository, RevCommit commit,
+ DiffOutputType outputType) {
+ return getDiff(repository, null, commit, null, outputType);
+ }
+
+ /**
+ * Returns the diff for the specified file or folder from the specified
+ * commit compared to its primary parent.
+ *
+ * @param repository
+ * @param commit
+ * @param path
+ * @param outputType
+ * @return the diff as a string
+ */
+ public static String getDiff(Repository repository, RevCommit commit, String path,
+ DiffOutputType outputType) {
+ return getDiff(repository, null, commit, path, outputType);
+ }
+
+ /**
+ * Returns the complete diff between the two specified commits.
+ *
+ * @param repository
+ * @param baseCommit
+ * @param commit
+ * @param outputType
+ * @return the diff as a string
+ */
+ public static String getDiff(Repository repository, RevCommit baseCommit, RevCommit commit,
+ DiffOutputType outputType) {
+ return getDiff(repository, baseCommit, commit, null, outputType);
+ }
+
+ /**
+ * Returns the diff between two commits for the specified file.
+ *
+ * @param repository
+ * @param baseCommit
+ * if base commit is null the diff is to the primary parent of
+ * the commit.
+ * @param commit
+ * @param path
+ * if the path is specified, the diff is restricted to that file
+ * or folder. if unspecified, the diff is for the entire commit.
+ * @param outputType
+ * @return the diff as a string
+ */
+ public static String getDiff(Repository repository, RevCommit baseCommit, RevCommit commit,
+ String path, DiffOutputType outputType) {
+ String diff = null;
+ try {
+ final ByteArrayOutputStream os = new ByteArrayOutputStream();
+ RawTextComparator cmp = RawTextComparator.DEFAULT;
+ DiffFormatter df;
+ switch (outputType) {
+ case GITWEB:
+ df = new GitWebDiffFormatter(os);
+ break;
+ case GITBLIT:
+ df = new GitBlitDiffFormatter(os);
+ break;
+ case PLAIN:
+ default:
+ df = new DiffFormatter(os);
+ break;
+ }
+ df.setRepository(repository);
+ df.setDiffComparator(cmp);
+ df.setDetectRenames(true);
+
+ RevTree commitTree = commit.getTree();
+ RevTree baseTree;
+ if (baseCommit == null) {
+ if (commit.getParentCount() > 0) {
+ final RevWalk rw = new RevWalk(repository);
+ RevCommit parent = rw.parseCommit(commit.getParent(0).getId());
+ rw.dispose();
+ baseTree = parent.getTree();
+ } else {
+ // FIXME initial commit. no parent?!
+ baseTree = commitTree;
+ }
+ } else {
+ baseTree = baseCommit.getTree();
+ }
+
+ List<DiffEntry> diffEntries = df.scan(baseTree, commitTree);
+ if (path != null && path.length() > 0) {
+ for (DiffEntry diffEntry : diffEntries) {
+ if (diffEntry.getNewPath().equalsIgnoreCase(path)) {
+ df.format(diffEntry);
+ break;
+ }
+ }
+ } else {
+ df.format(diffEntries);
+ }
+ if (df instanceof GitWebDiffFormatter) {
+ // workaround for complex private methods in DiffFormatter
+ diff = ((GitWebDiffFormatter) df).getHtml();
+ } else {
+ diff = os.toString();
+ }
+ df.flush();
+ } catch (Throwable t) {
+ LOGGER.error("failed to generate commit diff!", t);
+ }
+ return diff;
+ }
+
+ /**
+ * Returns the diff between the two commits for the specified file or folder
+ * formatted as a patch.
+ *
+ * @param repository
+ * @param baseCommit
+ * if base commit is unspecified, the patch is generated against
+ * the primary parent of the specified commit.
+ * @param commit
+ * @param path
+ * if path is specified, the patch is generated only for the
+ * specified file or folder. if unspecified, the patch is
+ * generated for the entire diff between the two commits.
+ * @return patch as a string
+ */
+ public static String getCommitPatch(Repository repository, RevCommit baseCommit,
+ RevCommit commit, String path) {
+ String diff = null;
+ try {
+ final ByteArrayOutputStream os = new ByteArrayOutputStream();
+ RawTextComparator cmp = RawTextComparator.DEFAULT;
+ PatchFormatter df = new PatchFormatter(os);
+ df.setRepository(repository);
+ df.setDiffComparator(cmp);
+ df.setDetectRenames(true);
+
+ RevTree commitTree = commit.getTree();
+ RevTree baseTree;
+ if (baseCommit == null) {
+ if (commit.getParentCount() > 0) {
+ final RevWalk rw = new RevWalk(repository);
+ RevCommit parent = rw.parseCommit(commit.getParent(0).getId());
+ baseTree = parent.getTree();
+ } else {
+ // FIXME initial commit. no parent?!
+ baseTree = commitTree;
+ }
+ } else {
+ baseTree = baseCommit.getTree();
+ }
+
+ List<DiffEntry> diffEntries = df.scan(baseTree, commitTree);
+ if (path != null && path.length() > 0) {
+ for (DiffEntry diffEntry : diffEntries) {
+ if (diffEntry.getNewPath().equalsIgnoreCase(path)) {
+ df.format(diffEntry);
+ break;
+ }
+ }
+ } else {
+ df.format(diffEntries);
+ }
+ diff = df.getPatch(commit);
+ df.flush();
+ } catch (Throwable t) {
+ LOGGER.error("failed to generate commit diff!", t);
+ }
+ return diff;
+ }
+
+ /**
+ * Returns the list of lines in the specified source file annotated with the
+ * source commit metadata.
+ *
+ * @param repository
+ * @param blobPath
+ * @param objectId
+ * @return list of annotated lines
+ */
+ public static List<AnnotatedLine> blame(Repository repository, String blobPath, String objectId) {
+ List<AnnotatedLine> lines = new ArrayList<AnnotatedLine>();
+ try {
+ ObjectId object;
+ if (StringUtils.isEmpty(objectId)) {
+ object = JGitUtils.getDefaultBranch(repository);
+ } else {
+ object = repository.resolve(objectId);
+ }
+ BlameCommand blameCommand = new BlameCommand(repository);
+ blameCommand.setFilePath(blobPath);
+ blameCommand.setStartCommit(object);
+ BlameResult blameResult = blameCommand.call();
+ RawText rawText = blameResult.getResultContents();
+ int length = rawText.size();
+ for (int i = 0; i < length; i++) {
+ RevCommit commit = blameResult.getSourceCommit(i);
+ AnnotatedLine line = new AnnotatedLine(commit, i + 1, rawText.getString(i));
+ lines.add(line);
+ }
+ } catch (Throwable t) {
+ LOGGER.error("failed to generate blame!", t);
+ }
+ return lines;
+ }
+}
diff --git a/src/main/java/com/gitblit/utils/FederationUtils.java b/src/main/java/com/gitblit/utils/FederationUtils.java
new file mode 100644
index 00000000..4d6060dd
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/FederationUtils.java
@@ -0,0 +1,349 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.utils;
+
+import java.lang.reflect.Type;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletResponse;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.Constants;
+import com.gitblit.Constants.FederationProposalResult;
+import com.gitblit.Constants.FederationRequest;
+import com.gitblit.Constants.FederationToken;
+import com.gitblit.IStoredSettings;
+import com.gitblit.Keys;
+import com.gitblit.models.FederationModel;
+import com.gitblit.models.FederationProposal;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.TeamModel;
+import com.gitblit.models.UserModel;
+import com.google.gson.reflect.TypeToken;
+
+/**
+ * Utility methods for federation functions.
+ *
+ * @author James Moger
+ *
+ */
+public class FederationUtils {
+
+ private static final Type REPOSITORIES_TYPE = new TypeToken<Map<String, RepositoryModel>>() {
+ }.getType();
+
+ private static final Type SETTINGS_TYPE = new TypeToken<Map<String, String>>() {
+ }.getType();
+
+ private static final Type USERS_TYPE = new TypeToken<Collection<UserModel>>() {
+ }.getType();
+
+ private static final Type TEAMS_TYPE = new TypeToken<Collection<TeamModel>>() {
+ }.getType();
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(FederationUtils.class);
+
+ /**
+ * Returns an url to this servlet for the specified parameters.
+ *
+ * @param sourceURL
+ * the url of the source gitblit instance
+ * @param token
+ * the federation token of the source gitblit instance
+ * @param req
+ * the pull type request
+ */
+ public static String asLink(String sourceURL, String token, FederationRequest req) {
+ return asLink(sourceURL, null, token, req, null);
+ }
+
+ /**
+ *
+ * @param remoteURL
+ * the url of the remote gitblit instance
+ * @param tokenType
+ * the type of federation token of a gitblit instance
+ * @param token
+ * the federation token of a gitblit instance
+ * @param req
+ * the pull type request
+ * @param myURL
+ * the url of this gitblit instance
+ * @return
+ */
+ public static String asLink(String remoteURL, FederationToken tokenType, String token,
+ FederationRequest req, String myURL) {
+ if (remoteURL.length() > 0 && remoteURL.charAt(remoteURL.length() - 1) == '/') {
+ remoteURL = remoteURL.substring(0, remoteURL.length() - 1);
+ }
+ if (req == null) {
+ req = FederationRequest.PULL_REPOSITORIES;
+ }
+ return remoteURL + Constants.FEDERATION_PATH + "?req=" + req.name().toLowerCase()
+ + (token == null ? "" : ("&token=" + token))
+ + (tokenType == null ? "" : ("&tokenType=" + tokenType.name().toLowerCase()))
+ + (myURL == null ? "" : ("&url=" + StringUtils.encodeURL(myURL)));
+ }
+
+ /**
+ * Returns the list of federated gitblit instances that this instance will
+ * try to pull.
+ *
+ * @return list of registered gitblit instances
+ */
+ public static List<FederationModel> getFederationRegistrations(IStoredSettings settings) {
+ List<FederationModel> federationRegistrations = new ArrayList<FederationModel>();
+ List<String> keys = settings.getAllKeys(Keys.federation._ROOT);
+ keys.remove(Keys.federation.name);
+ keys.remove(Keys.federation.passphrase);
+ keys.remove(Keys.federation.allowProposals);
+ keys.remove(Keys.federation.proposalsFolder);
+ keys.remove(Keys.federation.defaultFrequency);
+ keys.remove(Keys.federation.sets);
+ Collections.sort(keys);
+ Map<String, FederationModel> federatedModels = new HashMap<String, FederationModel>();
+ for (String key : keys) {
+ String value = key.substring(Keys.federation._ROOT.length() + 1);
+ List<String> values = StringUtils.getStringsFromValue(value, "\\.");
+ String server = values.get(0);
+ if (!federatedModels.containsKey(server)) {
+ federatedModels.put(server, new FederationModel(server));
+ }
+ String setting = values.get(1);
+ if (setting.equals("url")) {
+ // url of the origin Gitblit instance
+ federatedModels.get(server).url = settings.getString(key, "");
+ } else if (setting.equals("token")) {
+ // token for the origin Gitblit instance
+ federatedModels.get(server).token = settings.getString(key, "");
+ } else if (setting.equals("frequency")) {
+ // frequency of the pull operation
+ federatedModels.get(server).frequency = settings.getString(key, "");
+ } else if (setting.equals("folder")) {
+ // destination folder of the pull operation
+ federatedModels.get(server).folder = settings.getString(key, "");
+ } else if (setting.equals("bare")) {
+ // whether pulled repositories should be bare
+ federatedModels.get(server).bare = settings.getBoolean(key, true);
+ } else if (setting.equals("mirror")) {
+ // are the repositories to be true mirrors of the origin
+ federatedModels.get(server).mirror = settings.getBoolean(key, true);
+ } else if (setting.equals("mergeAccounts")) {
+ // merge remote accounts into local accounts
+ federatedModels.get(server).mergeAccounts = settings.getBoolean(key, false);
+ } else if (setting.equals("sendStatus")) {
+ // send a status acknowledgment to source Gitblit instance
+ // at end of git pull
+ federatedModels.get(server).sendStatus = settings.getBoolean(key, false);
+ } else if (setting.equals("notifyOnError")) {
+ // notify administrators on federation pull failures
+ federatedModels.get(server).notifyOnError = settings.getBoolean(key, false);
+ } else if (setting.equals("exclude")) {
+ // excluded repositories
+ federatedModels.get(server).exclusions = settings.getStrings(key);
+ } else if (setting.equals("include")) {
+ // included repositories
+ federatedModels.get(server).inclusions = settings.getStrings(key);
+ }
+ }
+
+ // verify that registrations have a url and a token
+ for (FederationModel model : federatedModels.values()) {
+ if (StringUtils.isEmpty(model.url)) {
+ LOGGER.warn(MessageFormat.format(
+ "Dropping federation registration {0}. Missing url.", model.name));
+ continue;
+ }
+ if (StringUtils.isEmpty(model.token)) {
+ LOGGER.warn(MessageFormat.format(
+ "Dropping federation registration {0}. Missing token.", model.name));
+ continue;
+ }
+ // set default frequency if unspecified
+ if (StringUtils.isEmpty(model.frequency)) {
+ model.frequency = settings.getString(Keys.federation.defaultFrequency, "60 mins");
+ }
+ federationRegistrations.add(model);
+ }
+ return federationRegistrations;
+ }
+
+ /**
+ * Sends a federation poke to the Gitblit instance at remoteUrl. Pokes are
+ * sent by an pulling Gitblit instance to an origin Gitblit instance as part
+ * of the proposal process. This is to ensure that the pulling Gitblit
+ * instance has an IP route to the origin instance.
+ *
+ * @param remoteUrl
+ * the remote Gitblit instance to send a federation proposal to
+ * @param proposal
+ * a complete federation proposal
+ * @return true if there is a route to the remoteUrl
+ */
+ public static boolean poke(String remoteUrl) throws Exception {
+ String url = asLink(remoteUrl, null, FederationRequest.POKE);
+ String json = JsonUtils.toJsonString("POKE");
+ int status = JsonUtils.sendJsonString(url, json);
+ return status == HttpServletResponse.SC_OK;
+ }
+
+ /**
+ * Sends a federation proposal to the Gitblit instance at remoteUrl
+ *
+ * @param remoteUrl
+ * the remote Gitblit instance to send a federation proposal to
+ * @param proposal
+ * a complete federation proposal
+ * @return the federation proposal result code
+ */
+ public static FederationProposalResult propose(String remoteUrl, FederationProposal proposal)
+ throws Exception {
+ String url = asLink(remoteUrl, null, FederationRequest.PROPOSAL);
+ String json = JsonUtils.toJsonString(proposal);
+ int status = JsonUtils.sendJsonString(url, json);
+ switch (status) {
+ case HttpServletResponse.SC_FORBIDDEN:
+ // remote Gitblit Federation disabled
+ return FederationProposalResult.FEDERATION_DISABLED;
+ case HttpServletResponse.SC_BAD_REQUEST:
+ // remote Gitblit did not receive any JSON data
+ return FederationProposalResult.MISSING_DATA;
+ case HttpServletResponse.SC_METHOD_NOT_ALLOWED:
+ // remote Gitblit not accepting proposals
+ return FederationProposalResult.NO_PROPOSALS;
+ case HttpServletResponse.SC_NOT_ACCEPTABLE:
+ // remote Gitblit failed to poke this Gitblit instance
+ return FederationProposalResult.NO_POKE;
+ case HttpServletResponse.SC_OK:
+ // received
+ return FederationProposalResult.ACCEPTED;
+ default:
+ return FederationProposalResult.ERROR;
+ }
+ }
+
+ /**
+ * Retrieves a map of the repositories at the remote gitblit instance keyed
+ * by the repository clone url.
+ *
+ * @param registration
+ * @param checkExclusions
+ * should returned repositories remove registration exclusions
+ * @return a map of cloneable repositories
+ * @throws Exception
+ */
+ public static Map<String, RepositoryModel> getRepositories(FederationModel registration,
+ boolean checkExclusions) throws Exception {
+ String url = asLink(registration.url, registration.token,
+ FederationRequest.PULL_REPOSITORIES);
+ Map<String, RepositoryModel> models = JsonUtils.retrieveJson(url, REPOSITORIES_TYPE);
+ if (checkExclusions) {
+ Map<String, RepositoryModel> includedModels = new HashMap<String, RepositoryModel>();
+ for (Map.Entry<String, RepositoryModel> entry : models.entrySet()) {
+ if (registration.isIncluded(entry.getValue())) {
+ includedModels.put(entry.getKey(), entry.getValue());
+ }
+ }
+ return includedModels;
+ }
+ return models;
+ }
+
+ /**
+ * Tries to pull the gitblit user accounts from the remote gitblit instance.
+ *
+ * @param registration
+ * @return a collection of UserModel objects
+ * @throws Exception
+ */
+ public static List<UserModel> getUsers(FederationModel registration) throws Exception {
+ String url = asLink(registration.url, registration.token, FederationRequest.PULL_USERS);
+ Collection<UserModel> models = JsonUtils.retrieveJson(url, USERS_TYPE);
+ List<UserModel> list = new ArrayList<UserModel>(models);
+ return list;
+ }
+
+ /**
+ * Tries to pull the gitblit team definitions from the remote gitblit
+ * instance.
+ *
+ * @param registration
+ * @return a collection of TeamModel objects
+ * @throws Exception
+ */
+ public static List<TeamModel> getTeams(FederationModel registration) throws Exception {
+ String url = asLink(registration.url, registration.token, FederationRequest.PULL_TEAMS);
+ Collection<TeamModel> models = JsonUtils.retrieveJson(url, TEAMS_TYPE);
+ List<TeamModel> list = new ArrayList<TeamModel>(models);
+ return list;
+ }
+
+ /**
+ * Tries to pull the gitblit server settings from the remote gitblit
+ * instance.
+ *
+ * @param registration
+ * @return a map of the remote gitblit settings
+ * @throws Exception
+ */
+ public static Map<String, String> getSettings(FederationModel registration) throws Exception {
+ String url = asLink(registration.url, registration.token, FederationRequest.PULL_SETTINGS);
+ Map<String, String> settings = JsonUtils.retrieveJson(url, SETTINGS_TYPE);
+ return settings;
+ }
+
+ /**
+ * Tries to pull the referenced scripts from the remote gitblit instance.
+ *
+ * @param registration
+ * @return a map of the remote gitblit scripts by script name
+ * @throws Exception
+ */
+ public static Map<String, String> getScripts(FederationModel registration) throws Exception {
+ String url = asLink(registration.url, registration.token, FederationRequest.PULL_SCRIPTS);
+ Map<String, String> scripts = JsonUtils.retrieveJson(url, SETTINGS_TYPE);
+ return scripts;
+ }
+
+ /**
+ * Send an status acknowledgment to the remote Gitblit server.
+ *
+ * @param identification
+ * identification of this pulling instance
+ * @param registration
+ * the source Gitblit instance to receive an acknowledgment
+ * @param results
+ * the results of your pull operation
+ * @return true, if the remote Gitblit instance acknowledged your results
+ * @throws Exception
+ */
+ public static boolean acknowledgeStatus(String identification, FederationModel registration)
+ throws Exception {
+ String url = asLink(registration.url, null, registration.token, FederationRequest.STATUS,
+ identification);
+ String json = JsonUtils.toJsonString(registration);
+ int status = JsonUtils.sendJsonString(url, json);
+ return status == HttpServletResponse.SC_OK;
+ }
+}
diff --git a/src/main/java/com/gitblit/utils/FileUtils.java b/src/main/java/com/gitblit/utils/FileUtils.java
new file mode 100644
index 00000000..a21b5128
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/FileUtils.java
@@ -0,0 +1,292 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.utils;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.nio.charset.Charset;
+
+/**
+ * Common file utilities.
+ *
+ * @author James Moger
+ *
+ */
+public class FileUtils {
+
+ /** 1024 (number of bytes in one kilobyte) */
+ public static final int KB = 1024;
+
+ /** 1024 {@link #KB} (number of bytes in one megabyte) */
+ public static final int MB = 1024 * KB;
+
+ /** 1024 {@link #MB} (number of bytes in one gigabyte) */
+ public static final int GB = 1024 * MB;
+
+ /**
+ * Returns an int from a string representation of a file size.
+ * e.g. 50m = 50 megabytes
+ *
+ * @param aString
+ * @param defaultValue
+ * @return an int value or the defaultValue if aString can not be parsed
+ */
+ public static int convertSizeToInt(String aString, int defaultValue) {
+ return (int) convertSizeToLong(aString, defaultValue);
+ }
+
+ /**
+ * Returns a long from a string representation of a file size.
+ * e.g. 50m = 50 megabytes
+ *
+ * @param aString
+ * @param defaultValue
+ * @return a long value or the defaultValue if aString can not be parsed
+ */
+ public static long convertSizeToLong(String aString, long defaultValue) {
+ // trim string and remove all spaces
+ aString = aString.toLowerCase().trim();
+ StringBuilder sb = new StringBuilder();
+ for (String a : aString.split(" ")) {
+ sb.append(a);
+ }
+ aString = sb.toString();
+
+ // identify value and unit
+ int idx = 0;
+ int len = aString.length();
+ while (Character.isDigit(aString.charAt(idx))) {
+ idx++;
+ if (idx == len) {
+ break;
+ }
+ }
+ long value = 0;
+ String unit = null;
+ try {
+ value = Long.parseLong(aString.substring(0, idx));
+ unit = aString.substring(idx);
+ } catch (Exception e) {
+ return defaultValue;
+ }
+ if (unit.equals("g") || unit.equals("gb")) {
+ return value * GB;
+ } else if (unit.equals("m") || unit.equals("mb")) {
+ return value * MB;
+ } else if (unit.equals("k") || unit.equals("kb")) {
+ return value * KB;
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Returns the byte [] content of the specified file.
+ *
+ * @param file
+ * @return the byte content of the file
+ */
+ public static byte [] readContent(File file) {
+ byte [] buffer = new byte[(int) file.length()];
+ try {
+ BufferedInputStream is = new BufferedInputStream(new FileInputStream(file));
+ is.read(buffer, 0, buffer.length);
+ is.close();
+ } catch (Throwable t) {
+ System.err.println("Failed to read byte content of " + file.getAbsolutePath());
+ t.printStackTrace();
+ }
+ return buffer;
+ }
+
+ /**
+ * Returns the string content of the specified file.
+ *
+ * @param file
+ * @param lineEnding
+ * @return the string content of the file
+ */
+ public static String readContent(File file, String lineEnding) {
+ StringBuilder sb = new StringBuilder();
+ try {
+ InputStreamReader is = new InputStreamReader(new FileInputStream(file),
+ Charset.forName("UTF-8"));
+ BufferedReader reader = new BufferedReader(is);
+ String line = null;
+ while ((line = reader.readLine()) != null) {
+ sb.append(line);
+ if (lineEnding != null) {
+ sb.append(lineEnding);
+ }
+ }
+ reader.close();
+ } catch (Throwable t) {
+ System.err.println("Failed to read content of " + file.getAbsolutePath());
+ t.printStackTrace();
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Writes the string content to the file.
+ *
+ * @param file
+ * @param content
+ */
+ public static void writeContent(File file, String content) {
+ try {
+ OutputStreamWriter os = new OutputStreamWriter(new FileOutputStream(file),
+ Charset.forName("UTF-8"));
+ BufferedWriter writer = new BufferedWriter(os);
+ writer.append(content);
+ writer.close();
+ } catch (Throwable t) {
+ System.err.println("Failed to write content of " + file.getAbsolutePath());
+ t.printStackTrace();
+ }
+ }
+
+ /**
+ * Recursively traverses a folder and its subfolders to calculate the total
+ * size in bytes.
+ *
+ * @param directory
+ * @return folder size in bytes
+ */
+ public static long folderSize(File directory) {
+ if (directory == null || !directory.exists()) {
+ return -1;
+ }
+ if (directory.isDirectory()) {
+ long length = 0;
+ for (File file : directory.listFiles()) {
+ length += folderSize(file);
+ }
+ return length;
+ } else if (directory.isFile()) {
+ return directory.length();
+ }
+ return 0;
+ }
+
+ /**
+ * Copies a file or folder (recursively) to a destination folder.
+ *
+ * @param destinationFolder
+ * @param filesOrFolders
+ * @return
+ * @throws FileNotFoundException
+ * @throws IOException
+ */
+ public static void copy(File destinationFolder, File... filesOrFolders)
+ throws FileNotFoundException, IOException {
+ destinationFolder.mkdirs();
+ for (File file : filesOrFolders) {
+ if (file.isDirectory()) {
+ copy(new File(destinationFolder, file.getName()), file.listFiles());
+ } else {
+ File dFile = new File(destinationFolder, file.getName());
+ BufferedInputStream bufin = null;
+ FileOutputStream fos = null;
+ try {
+ bufin = new BufferedInputStream(new FileInputStream(file));
+ fos = new FileOutputStream(dFile);
+ int len = 8196;
+ byte[] buff = new byte[len];
+ int n = 0;
+ while ((n = bufin.read(buff, 0, len)) != -1) {
+ fos.write(buff, 0, n);
+ }
+ } finally {
+ try {
+ bufin.close();
+ } catch (Throwable t) {
+ }
+ try {
+ fos.close();
+ } catch (Throwable t) {
+ }
+ }
+ dFile.setLastModified(file.lastModified());
+ }
+ }
+ }
+
+ /**
+ * Determine the relative path between two files. Takes into account
+ * canonical paths, if possible.
+ *
+ * @param basePath
+ * @param path
+ * @return a relative path from basePath to path
+ */
+ public static String getRelativePath(File basePath, File path) {
+ File exactBase = getExactFile(basePath);
+ File exactPath = getExactFile(path);
+ if (path.getAbsolutePath().startsWith(basePath.getAbsolutePath())) {
+ // absolute base-path match
+ return StringUtils.getRelativePath(basePath.getAbsolutePath(), path.getAbsolutePath());
+ } else if (exactPath.getPath().startsWith(exactBase.getPath())) {
+ // canonical base-path match
+ return StringUtils.getRelativePath(exactBase.getPath(), exactPath.getPath());
+ } else if (exactPath.getPath().startsWith(basePath.getAbsolutePath())) {
+ // mixed path match
+ return StringUtils.getRelativePath(basePath.getAbsolutePath(), exactPath.getPath());
+ } else if (path.getAbsolutePath().startsWith(exactBase.getPath())) {
+ // mixed path match
+ return StringUtils.getRelativePath(exactBase.getPath(), path.getAbsolutePath());
+ }
+ // no relative relationship
+ return null;
+ }
+
+ /**
+ * Returns the exact path for a file. This path will be the canonical path
+ * unless an exception is thrown in which case it will be the absolute path.
+ *
+ * @param path
+ * @return the exact file
+ */
+ public static File getExactFile(File path) {
+ try {
+ return path.getCanonicalFile();
+ } catch (IOException e) {
+ return path.getAbsoluteFile();
+ }
+ }
+
+ public static File resolveParameter(String parameter, File aFolder, String path) {
+ if (aFolder == null) {
+ // strip any parameter reference
+ path = path.replace(parameter, "").trim();
+ if (path.length() > 0 && path.charAt(0) == '/') {
+ // strip leading /
+ path = path.substring(1);
+ }
+ } else if (path.contains(parameter)) {
+ // replace parameter with path
+ path = path.replace(parameter, aFolder.getAbsolutePath());
+ }
+ return new File(path);
+ }
+}
diff --git a/src/main/java/com/gitblit/utils/GitBlitDiffFormatter.java b/src/main/java/com/gitblit/utils/GitBlitDiffFormatter.java
new file mode 100644
index 00000000..2966aa8a
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/GitBlitDiffFormatter.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.utils;
+
+import static org.eclipse.jgit.lib.Constants.encode;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+import org.eclipse.jgit.diff.RawText;
+import org.eclipse.jgit.util.RawParseUtils;
+
+/**
+ * Generates an html snippet of a diff in Gitblit's style.
+ *
+ * @author James Moger
+ *
+ */
+public class GitBlitDiffFormatter extends GitWebDiffFormatter {
+
+ private final OutputStream os;
+
+ private int left, right;
+
+ public GitBlitDiffFormatter(OutputStream os) {
+ super(os);
+ this.os = os;
+ }
+
+ /**
+ * Output a hunk header
+ *
+ * @param aStartLine
+ * within first source
+ * @param aEndLine
+ * within first source
+ * @param bStartLine
+ * within second source
+ * @param bEndLine
+ * within second source
+ * @throws IOException
+ */
+ @Override
+ protected void writeHunkHeader(int aStartLine, int aEndLine, int bStartLine, int bEndLine)
+ throws IOException {
+ os.write("<tr><th>..</th><th>..</th><td class='hunk_header'>".getBytes());
+ os.write('@');
+ os.write('@');
+ writeRange('-', aStartLine + 1, aEndLine - aStartLine);
+ writeRange('+', bStartLine + 1, bEndLine - bStartLine);
+ os.write(' ');
+ os.write('@');
+ os.write('@');
+ os.write("</td></tr>\n".getBytes());
+ left = aStartLine + 1;
+ right = bStartLine + 1;
+ }
+
+ @Override
+ protected void writeLine(final char prefix, final RawText text, final int cur)
+ throws IOException {
+ os.write("<tr>".getBytes());
+ switch (prefix) {
+ case '+':
+ os.write(("<th></th><th>" + (right++) + "</th>").getBytes());
+ os.write("<td><div class=\"diff add2\">".getBytes());
+ break;
+ case '-':
+ os.write(("<th>" + (left++) + "</th><th></th>").getBytes());
+ os.write("<td><div class=\"diff remove2\">".getBytes());
+ break;
+ default:
+ os.write(("<th>" + (left++) + "</th><th>" + (right++) + "</th>").getBytes());
+ os.write("<td>".getBytes());
+ break;
+ }
+ os.write(prefix);
+ String line = text.getString(cur);
+ line = StringUtils.escapeForHtml(line, false);
+ os.write(encode(line));
+ switch (prefix) {
+ case '+':
+ case '-':
+ os.write("</div>".getBytes());
+ break;
+ default:
+ os.write("</td>".getBytes());
+ }
+ os.write("</tr>\n".getBytes());
+ }
+
+ /**
+ * Workaround function for complex private methods in DiffFormatter. This
+ * sets the html for the diff headers.
+ *
+ * @return
+ */
+ @Override
+ public String getHtml() {
+ ByteArrayOutputStream bos = (ByteArrayOutputStream) os;
+ String html = RawParseUtils.decode(bos.toByteArray());
+ String[] lines = html.split("\n");
+ StringBuilder sb = new StringBuilder();
+ boolean inFile = false;
+ String oldnull = "a/dev/null";
+ for (String line : lines) {
+ if (line.startsWith("index")) {
+ // skip index lines
+ } else if (line.startsWith("new file")) {
+ // skip new file lines
+ } else if (line.startsWith("\\ No newline")) {
+ // skip no new line
+ } else if (line.startsWith("---") || line.startsWith("+++")) {
+ // skip --- +++ lines
+ } else if (line.startsWith("diff")) {
+ line = StringUtils.convertOctal(line);
+ if (line.indexOf(oldnull) > -1) {
+ // a is null, use b
+ line = line.substring(("diff --git " + oldnull).length()).trim();
+ // trim b/
+ line = line.substring(2).trim();
+ } else {
+ // use a
+ line = line.substring("diff --git ".length()).trim();
+ line = line.substring(line.startsWith("\"a/") ? 3 : 2);
+ line = line.substring(0, line.indexOf(" b/") > -1 ? line.indexOf(" b/") : line.indexOf("\"b/")).trim();
+ }
+
+ if (line.charAt(0) == '"') {
+ line = line.substring(1);
+ }
+ if (line.charAt(line.length() - 1) == '"') {
+ line = line.substring(0, line.length() - 1);
+ }
+ if (inFile) {
+ sb.append("</tbody></table></div>\n");
+ inFile = false;
+ }
+ sb.append("<div class='header'>").append(line).append("</div>");
+ sb.append("<div class=\"diff\">");
+ sb.append("<table><tbody>");
+ inFile = true;
+ } else {
+ sb.append(line);
+ }
+ }
+ sb.append("</table></div>");
+ return sb.toString();
+ }
+}
diff --git a/src/main/java/com/gitblit/utils/GitWebDiffFormatter.java b/src/main/java/com/gitblit/utils/GitWebDiffFormatter.java
new file mode 100644
index 00000000..e657dc58
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/GitWebDiffFormatter.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.utils;
+
+import static org.eclipse.jgit.lib.Constants.encode;
+import static org.eclipse.jgit.lib.Constants.encodeASCII;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+import org.eclipse.jgit.diff.DiffFormatter;
+import org.eclipse.jgit.diff.RawText;
+import org.eclipse.jgit.util.RawParseUtils;
+
+/**
+ * Returns an html snippet of the diff in the standard Gitweb style.
+ *
+ * @author James Moger
+ *
+ */
+public class GitWebDiffFormatter extends DiffFormatter {
+
+ private final OutputStream os;
+
+ public GitWebDiffFormatter(OutputStream os) {
+ super(os);
+ this.os = os;
+ }
+
+ /**
+ * Output a hunk header
+ *
+ * @param aStartLine
+ * within first source
+ * @param aEndLine
+ * within first source
+ * @param bStartLine
+ * within second source
+ * @param bEndLine
+ * within second source
+ * @throws IOException
+ */
+ @Override
+ protected void writeHunkHeader(int aStartLine, int aEndLine, int bStartLine, int bEndLine)
+ throws IOException {
+ os.write("<div class=\"diff hunk_header\"><span class=\"diff hunk_info\">".getBytes());
+ os.write('@');
+ os.write('@');
+ writeRange('-', aStartLine + 1, aEndLine - aStartLine);
+ writeRange('+', bStartLine + 1, bEndLine - bStartLine);
+ os.write(' ');
+ os.write('@');
+ os.write('@');
+ os.write("</span></div>".getBytes());
+ }
+
+ protected void writeRange(final char prefix, final int begin, final int cnt) throws IOException {
+ os.write(' ');
+ os.write(prefix);
+ switch (cnt) {
+ case 0:
+ // If the range is empty, its beginning number must
+ // be the
+ // line just before the range, or 0 if the range is
+ // at the
+ // start of the file stream. Here, begin is always 1
+ // based,
+ // so an empty file would produce "0,0".
+ //
+ os.write(encodeASCII(begin - 1));
+ os.write(',');
+ os.write('0');
+ break;
+
+ case 1:
+ // If the range is exactly one line, produce only
+ // the number.
+ //
+ os.write(encodeASCII(begin));
+ break;
+
+ default:
+ os.write(encodeASCII(begin));
+ os.write(',');
+ os.write(encodeASCII(cnt));
+ break;
+ }
+ }
+
+ @Override
+ protected void writeLine(final char prefix, final RawText text, final int cur)
+ throws IOException {
+ switch (prefix) {
+ case '+':
+ os.write("<span style=\"color:#008000;\">".getBytes());
+ break;
+ case '-':
+ os.write("<span style=\"color:#800000;\">".getBytes());
+ break;
+ }
+ os.write(prefix);
+ String line = text.getString(cur);
+ line = StringUtils.escapeForHtml(line, false);
+ os.write(encode(line));
+ switch (prefix) {
+ case '+':
+ case '-':
+ os.write("</span>\n".getBytes());
+ break;
+ default:
+ os.write('\n');
+ }
+ }
+
+ /**
+ * Workaround function for complex private methods in DiffFormatter. This
+ * sets the html for the diff headers.
+ *
+ * @return
+ */
+ public String getHtml() {
+ ByteArrayOutputStream bos = (ByteArrayOutputStream) os;
+ String html = RawParseUtils.decode(bos.toByteArray());
+ String[] lines = html.split("\n");
+ StringBuilder sb = new StringBuilder();
+ sb.append("<div class=\"diff\">");
+ for (String line : lines) {
+ if (line.startsWith("diff")) {
+ sb.append("<div class=\"diff header\">").append(StringUtils.convertOctal(line)).append("</div>");
+ } else if (line.startsWith("---")) {
+ sb.append("<span style=\"color:#800000;\">").append(StringUtils.convertOctal(line)).append("</span><br/>");
+ } else if (line.startsWith("+++")) {
+ sb.append("<span style=\"color:#008000;\">").append(StringUtils.convertOctal(line)).append("</span><br/>");
+ } else {
+ sb.append(line).append('\n');
+ }
+ }
+ sb.append("</div>\n");
+ return sb.toString();
+ }
+}
diff --git a/src/main/java/com/gitblit/utils/HttpUtils.java b/src/main/java/com/gitblit/utils/HttpUtils.java
new file mode 100644
index 00000000..86f53cfe
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/HttpUtils.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.utils;
+
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.X509Certificate;
+import java.text.MessageFormat;
+import java.util.Date;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.models.UserModel;
+import com.gitblit.utils.X509Utils.X509Metadata;
+
+/**
+ * Collection of utility methods for http requests.
+ *
+ * @author James Moger
+ *
+ */
+public class HttpUtils {
+
+ /**
+ * Returns the Gitblit URL based on the request.
+ *
+ * @param request
+ * @return the host url
+ */
+ public static String getGitblitURL(HttpServletRequest request) {
+ // default to the request scheme and port
+ String scheme = request.getScheme();
+ int port = request.getServerPort();
+
+ // try to use reverse-proxy server's port
+ String forwardedPort = request.getHeader("X-Forwarded-Port");
+ if (StringUtils.isEmpty(forwardedPort)) {
+ forwardedPort = request.getHeader("X_Forwarded_Port");
+ }
+ if (!StringUtils.isEmpty(forwardedPort)) {
+ // reverse-proxy server has supplied the original port
+ try {
+ port = Integer.parseInt(forwardedPort);
+ } catch (Throwable t) {
+ }
+ }
+
+ // try to use reverse-proxy server's scheme
+ String forwardedScheme = request.getHeader("X-Forwarded-Proto");
+ if (StringUtils.isEmpty(forwardedScheme)) {
+ forwardedScheme = request.getHeader("X_Forwarded_Proto");
+ }
+ if (!StringUtils.isEmpty(forwardedScheme)) {
+ // reverse-proxy server has supplied the original scheme
+ scheme = forwardedScheme;
+
+ if ("https".equals(scheme) && port == 80) {
+ // proxy server is https, inside server is 80
+ // this is likely because the proxy server has not supplied
+ // x-forwarded-port. since 80 is almost definitely wrong,
+ // make an educated guess that 443 is correct.
+ port = 443;
+ }
+ }
+
+ String context = request.getContextPath();
+ String forwardedContext = request.getHeader("X-Forwarded-Context");
+ if (forwardedContext != null) {
+ forwardedContext = request.getHeader("X_Forwarded_Context");
+ }
+ if (!StringUtils.isEmpty(forwardedContext)) {
+ context = forwardedContext;
+ }
+
+ // trim any trailing slash
+ if (context.length() > 0 && context.charAt(context.length() - 1) == '/') {
+ context = context.substring(1);
+ }
+
+ StringBuilder sb = new StringBuilder();
+ sb.append(scheme);
+ sb.append("://");
+ sb.append(request.getServerName());
+ if (("http".equals(scheme) && port != 80)
+ || ("https".equals(scheme) && port != 443)) {
+ sb.append(":" + port);
+ }
+ sb.append(context);
+ return sb.toString();
+ }
+
+ /**
+ * Returns a user model object built from attributes in the SSL certificate.
+ * This model is not retrieved from the user service.
+ *
+ * @param httpRequest
+ * @param checkValidity ensure certificate can be used now
+ * @param usernameOIDs if unspecified, CN is used as the username
+ * @return a UserModel, if a valid certificate is in the request, null otherwise
+ */
+ public static UserModel getUserModelFromCertificate(HttpServletRequest httpRequest, boolean checkValidity, String... usernameOIDs) {
+ if (httpRequest.getAttribute("javax.servlet.request.X509Certificate") != null) {
+ X509Certificate[] certChain = (X509Certificate[]) httpRequest
+ .getAttribute("javax.servlet.request.X509Certificate");
+ if (certChain != null) {
+ X509Certificate cert = certChain[0];
+ // ensure certificate is valid
+ if (checkValidity) {
+ try {
+ cert.checkValidity(new Date());
+ } catch (CertificateNotYetValidException e) {
+ LoggerFactory.getLogger(HttpUtils.class).info(MessageFormat.format("X509 certificate {0} is not yet valid", cert.getSubjectDN().getName()));
+ return null;
+ } catch (CertificateExpiredException e) {
+ LoggerFactory.getLogger(HttpUtils.class).info(MessageFormat.format("X509 certificate {0} has expired", cert.getSubjectDN().getName()));
+ return null;
+ }
+ }
+ return getUserModelFromCertificate(cert, usernameOIDs);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Creates a UserModel from a certificate
+ * @param cert
+ * @param usernameOids if unspecified CN is used as the username
+ * @return
+ */
+ public static UserModel getUserModelFromCertificate(X509Certificate cert, String... usernameOIDs) {
+ X509Metadata metadata = X509Utils.getMetadata(cert);
+
+ UserModel user = new UserModel(metadata.commonName);
+ user.emailAddress = metadata.emailAddress;
+ user.isAuthenticated = false;
+
+ if (usernameOIDs == null || usernameOIDs.length == 0) {
+ // use default usename<->CN mapping
+ usernameOIDs = new String [] { "CN" };
+ }
+
+ // determine username from OID fingerprint
+ StringBuilder an = new StringBuilder();
+ for (String oid : usernameOIDs) {
+ String val = metadata.getOID(oid.toUpperCase(), null);
+ if (val != null) {
+ an.append(val).append(' ');
+ }
+ }
+ user.username = an.toString().trim();
+ return user;
+ }
+
+ public static X509Metadata getCertificateMetadata(HttpServletRequest httpRequest) {
+ if (httpRequest.getAttribute("javax.servlet.request.X509Certificate") != null) {
+ X509Certificate[] certChain = (X509Certificate[]) httpRequest
+ .getAttribute("javax.servlet.request.X509Certificate");
+ if (certChain != null) {
+ X509Certificate cert = certChain[0];
+ return X509Utils.getMetadata(cert);
+ }
+ }
+ return null;
+ }
+
+ public static boolean isIpAddress(String address) {
+ if (StringUtils.isEmpty(address)) {
+ return false;
+ }
+ String [] fields = address.split("\\.");
+ if (fields.length == 4) {
+ // IPV4
+ for (String field : fields) {
+ try {
+ int value = Integer.parseInt(field);
+ if (value < 0 || value > 255) {
+ return false;
+ }
+ } catch (Exception e) {
+ return false;
+ }
+ }
+ return true;
+ }
+ // TODO IPV6?
+ return false;
+ }
+}
diff --git a/src/main/java/com/gitblit/utils/IssueUtils.java b/src/main/java/com/gitblit/utils/IssueUtils.java
new file mode 100644
index 00000000..dd09235b
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/IssueUtils.java
@@ -0,0 +1,829 @@
+/*
+ * Copyright 2012 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.utils;
+
+import java.io.IOException;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+
+import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
+import org.eclipse.jgit.api.errors.JGitInternalException;
+import org.eclipse.jgit.dircache.DirCache;
+import org.eclipse.jgit.dircache.DirCacheBuilder;
+import org.eclipse.jgit.dircache.DirCacheEntry;
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.RefUpdate.Result;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevTree;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.treewalk.CanonicalTreeParser;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.treewalk.filter.AndTreeFilter;
+import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
+import org.eclipse.jgit.treewalk.filter.TreeFilter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.models.IssueModel;
+import com.gitblit.models.IssueModel.Attachment;
+import com.gitblit.models.IssueModel.Change;
+import com.gitblit.models.IssueModel.Field;
+import com.gitblit.models.IssueModel.Status;
+import com.gitblit.models.RefModel;
+import com.gitblit.utils.JsonUtils.ExcludeField;
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+
+/**
+ * Utility class for reading Gitblit issues.
+ *
+ * @author James Moger
+ *
+ */
+public class IssueUtils {
+
+ public static interface IssueFilter {
+ public abstract boolean accept(IssueModel issue);
+ }
+
+ public static final String GB_ISSUES = "refs/gitblit/issues";
+
+ static final Logger LOGGER = LoggerFactory.getLogger(IssueUtils.class);
+
+ /**
+ * Log an error message and exception.
+ *
+ * @param t
+ * @param repository
+ * if repository is not null it MUST be the {0} parameter in the
+ * pattern.
+ * @param pattern
+ * @param objects
+ */
+ private static void error(Throwable t, Repository repository, String pattern, Object... objects) {
+ List<Object> parameters = new ArrayList<Object>();
+ if (objects != null && objects.length > 0) {
+ for (Object o : objects) {
+ parameters.add(o);
+ }
+ }
+ if (repository != null) {
+ parameters.add(0, repository.getDirectory().getAbsolutePath());
+ }
+ LOGGER.error(MessageFormat.format(pattern, parameters.toArray()), t);
+ }
+
+ /**
+ * Returns a RefModel for the gb-issues branch in the repository. If the
+ * branch can not be found, null is returned.
+ *
+ * @param repository
+ * @return a refmodel for the gb-issues branch or null
+ */
+ public static RefModel getIssuesBranch(Repository repository) {
+ List<RefModel> refs = JGitUtils.getRefs(repository, com.gitblit.Constants.R_GITBLIT);
+ for (RefModel ref : refs) {
+ if (ref.reference.getName().equals(GB_ISSUES)) {
+ return ref;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns all the issues in the repository. Querying issues from the
+ * repository requires deserializing all changes for all issues. This is an
+ * expensive process and not recommended. Issues should be indexed by Lucene
+ * and queries should be executed against that index.
+ *
+ * @param repository
+ * @param filter
+ * optional issue filter to only return matching results
+ * @return a list of issues
+ */
+ public static List<IssueModel> getIssues(Repository repository, IssueFilter filter) {
+ List<IssueModel> list = new ArrayList<IssueModel>();
+ RefModel issuesBranch = getIssuesBranch(repository);
+ if (issuesBranch == null) {
+ return list;
+ }
+
+ // Collect the set of all issue paths
+ Set<String> issuePaths = new HashSet<String>();
+ final TreeWalk tw = new TreeWalk(repository);
+ try {
+ RevCommit head = JGitUtils.getCommit(repository, GB_ISSUES);
+ tw.addTree(head.getTree());
+ tw.setRecursive(false);
+ while (tw.next()) {
+ if (tw.getDepth() < 2 && tw.isSubtree()) {
+ tw.enterSubtree();
+ if (tw.getDepth() == 2) {
+ issuePaths.add(tw.getPathString());
+ }
+ }
+ }
+ } catch (IOException e) {
+ error(e, repository, "{0} failed to query issues");
+ } finally {
+ tw.release();
+ }
+
+ // Build each issue and optionally filter out unwanted issues
+
+ for (String issuePath : issuePaths) {
+ RevWalk rw = new RevWalk(repository);
+ try {
+ RevCommit start = rw.parseCommit(repository.resolve(GB_ISSUES));
+ rw.markStart(start);
+ } catch (Exception e) {
+ error(e, repository, "Failed to find {1} in {0}", GB_ISSUES);
+ }
+ TreeFilter treeFilter = AndTreeFilter.create(
+ PathFilterGroup.createFromStrings(issuePath), TreeFilter.ANY_DIFF);
+ rw.setTreeFilter(treeFilter);
+ Iterator<RevCommit> revlog = rw.iterator();
+
+ List<RevCommit> commits = new ArrayList<RevCommit>();
+ while (revlog.hasNext()) {
+ commits.add(revlog.next());
+ }
+
+ // release the revwalk
+ rw.release();
+
+ if (commits.size() == 0) {
+ LOGGER.warn("Failed to find changes for issue " + issuePath);
+ continue;
+ }
+
+ // sort by commit order, first commit first
+ Collections.reverse(commits);
+
+ StringBuilder sb = new StringBuilder("[");
+ boolean first = true;
+ for (RevCommit commit : commits) {
+ if (!first) {
+ sb.append(',');
+ }
+ String message = commit.getFullMessage();
+ // commit message is formatted: C ISSUEID\n\nJSON
+ // C is an single char commit code
+ // ISSUEID is an SHA-1 hash
+ String json = message.substring(43);
+ sb.append(json);
+ first = false;
+ }
+ sb.append(']');
+
+ // Deserialize the JSON array as a Collection<Change>, this seems
+ // slightly faster than deserializing each change by itself.
+ Collection<Change> changes = JsonUtils.fromJsonString(sb.toString(),
+ new TypeToken<Collection<Change>>() {
+ }.getType());
+
+ // create an issue object form the changes
+ IssueModel issue = buildIssue(changes, true);
+
+ // add the issue, conditionally, to the list
+ if (filter == null) {
+ list.add(issue);
+ } else {
+ if (filter.accept(issue)) {
+ list.add(issue);
+ }
+ }
+ }
+
+ // sort the issues by creation
+ Collections.sort(list);
+ return list;
+ }
+
+ /**
+ * Retrieves the specified issue from the repository with all changes
+ * applied to build the effective issue.
+ *
+ * @param repository
+ * @param issueId
+ * @return an issue, if it exists, otherwise null
+ */
+ public static IssueModel getIssue(Repository repository, String issueId) {
+ return getIssue(repository, issueId, true);
+ }
+
+ /**
+ * Retrieves the specified issue from the repository.
+ *
+ * @param repository
+ * @param issueId
+ * @param effective
+ * if true, the effective issue is built by processing comment
+ * changes, deletions, etc. if false, the raw issue is built
+ * without consideration for comment changes, deletions, etc.
+ * @return an issue, if it exists, otherwise null
+ */
+ public static IssueModel getIssue(Repository repository, String issueId, boolean effective) {
+ RefModel issuesBranch = getIssuesBranch(repository);
+ if (issuesBranch == null) {
+ return null;
+ }
+
+ if (StringUtils.isEmpty(issueId)) {
+ return null;
+ }
+
+ String issuePath = getIssuePath(issueId);
+
+ // Collect all changes as JSON array from commit messages
+ List<RevCommit> commits = JGitUtils.getRevLog(repository, GB_ISSUES, issuePath, 0, -1);
+
+ // sort by commit order, first commit first
+ Collections.reverse(commits);
+
+ StringBuilder sb = new StringBuilder("[");
+ boolean first = true;
+ for (RevCommit commit : commits) {
+ if (!first) {
+ sb.append(',');
+ }
+ String message = commit.getFullMessage();
+ // commit message is formatted: C ISSUEID\n\nJSON
+ // C is an single char commit code
+ // ISSUEID is an SHA-1 hash
+ String json = message.substring(43);
+ sb.append(json);
+ first = false;
+ }
+ sb.append(']');
+
+ // Deserialize the JSON array as a Collection<Change>, this seems
+ // slightly faster than deserializing each change by itself.
+ Collection<Change> changes = JsonUtils.fromJsonString(sb.toString(),
+ new TypeToken<Collection<Change>>() {
+ }.getType());
+
+ // create an issue object and apply the changes to it
+ IssueModel issue = buildIssue(changes, effective);
+ return issue;
+ }
+
+ /**
+ * Builds an issue from a set of changes.
+ *
+ * @param changes
+ * @param effective
+ * if true, the effective issue is built which accounts for
+ * comment changes, comment deletions, etc. if false, the raw
+ * issue is built.
+ * @return an issue
+ */
+ private static IssueModel buildIssue(Collection<Change> changes, boolean effective) {
+ IssueModel issue;
+ if (effective) {
+ List<Change> effectiveChanges = new ArrayList<Change>();
+ Map<String, Change> comments = new HashMap<String, Change>();
+ for (Change change : changes) {
+ if (change.comment != null) {
+ if (comments.containsKey(change.comment.id)) {
+ Change original = comments.get(change.comment.id);
+ Change clone = DeepCopier.copy(original);
+ clone.comment.text = change.comment.text;
+ clone.comment.deleted = change.comment.deleted;
+ int idx = effectiveChanges.indexOf(original);
+ effectiveChanges.remove(original);
+ effectiveChanges.add(idx, clone);
+ comments.put(clone.comment.id, clone);
+ } else {
+ effectiveChanges.add(change);
+ comments.put(change.comment.id, change);
+ }
+ } else {
+ effectiveChanges.add(change);
+ }
+ }
+
+ // effective issue
+ issue = new IssueModel();
+ for (Change change : effectiveChanges) {
+ issue.applyChange(change);
+ }
+ } else {
+ // raw issue
+ issue = new IssueModel();
+ for (Change change : changes) {
+ issue.applyChange(change);
+ }
+ }
+ return issue;
+ }
+
+ /**
+ * Retrieves the specified attachment from an issue.
+ *
+ * @param repository
+ * @param issueId
+ * @param filename
+ * @return an attachment, if found, null otherwise
+ */
+ public static Attachment getIssueAttachment(Repository repository, String issueId,
+ String filename) {
+ RefModel issuesBranch = getIssuesBranch(repository);
+ if (issuesBranch == null) {
+ return null;
+ }
+
+ if (StringUtils.isEmpty(issueId)) {
+ return null;
+ }
+
+ // deserialize the issue model so that we have the attachment metadata
+ IssueModel issue = getIssue(repository, issueId, true);
+ Attachment attachment = issue.getAttachment(filename);
+
+ // attachment not found
+ if (attachment == null) {
+ return null;
+ }
+
+ // retrieve the attachment content
+ String issuePath = getIssuePath(issueId);
+ RevTree tree = JGitUtils.getCommit(repository, GB_ISSUES).getTree();
+ byte[] content = JGitUtils
+ .getByteContent(repository, tree, issuePath + "/" + attachment.id, false);
+ attachment.content = content;
+ attachment.size = content.length;
+ return attachment;
+ }
+
+ /**
+ * Creates an issue in the gb-issues branch of the repository. The branch is
+ * automatically created if it does not already exist. Your change must
+ * include an author, summary, and description, at a minimum. If your change
+ * does not have those minimum requirements a RuntimeException will be
+ * thrown.
+ *
+ * @param repository
+ * @param change
+ * @return true if successful
+ */
+ public static IssueModel createIssue(Repository repository, Change change) {
+ RefModel issuesBranch = getIssuesBranch(repository);
+ if (issuesBranch == null) {
+ JGitUtils.createOrphanBranch(repository, GB_ISSUES, null);
+ }
+
+ if (StringUtils.isEmpty(change.author)) {
+ throw new RuntimeException("Must specify a change author!");
+ }
+ if (!change.hasField(Field.Summary)) {
+ throw new RuntimeException("Must specify a summary!");
+ }
+ if (!change.hasField(Field.Description)) {
+ throw new RuntimeException("Must specify a description!");
+ }
+
+ change.setField(Field.Reporter, change.author);
+
+ String issueId = StringUtils.getSHA1(change.created.toString() + change.author
+ + change.getString(Field.Summary) + change.getField(Field.Description));
+ change.setField(Field.Id, issueId);
+ change.code = '+';
+
+ boolean success = commit(repository, issueId, change);
+ if (success) {
+ return getIssue(repository, issueId, false);
+ }
+ return null;
+ }
+
+ /**
+ * Updates an issue in the gb-issues branch of the repository.
+ *
+ * @param repository
+ * @param issueId
+ * @param change
+ * @return true if successful
+ */
+ public static boolean updateIssue(Repository repository, String issueId, Change change) {
+ boolean success = false;
+ RefModel issuesBranch = getIssuesBranch(repository);
+
+ if (issuesBranch == null) {
+ throw new RuntimeException("gb-issues branch does not exist!");
+ }
+
+ if (change == null) {
+ throw new RuntimeException("change can not be null!");
+ }
+
+ if (StringUtils.isEmpty(change.author)) {
+ throw new RuntimeException("must specify a change author!");
+ }
+
+ // determine update code
+ // default update code is '=' for a general change
+ change.code = '=';
+ if (change.hasField(Field.Status)) {
+ Status status = Status.fromObject(change.getField(Field.Status));
+ if (status.isClosed()) {
+ // someone closed the issue
+ change.code = 'x';
+ }
+ }
+ success = commit(repository, issueId, change);
+ return success;
+ }
+
+ /**
+ * Deletes an issue from the repository.
+ *
+ * @param repository
+ * @param issueId
+ * @return true if successful
+ */
+ public static boolean deleteIssue(Repository repository, String issueId, String author) {
+ boolean success = false;
+ RefModel issuesBranch = getIssuesBranch(repository);
+
+ if (issuesBranch == null) {
+ throw new RuntimeException(GB_ISSUES + " does not exist!");
+ }
+
+ if (StringUtils.isEmpty(issueId)) {
+ throw new RuntimeException("must specify an issue id!");
+ }
+
+ String issuePath = getIssuePath(issueId);
+
+ String message = "- " + issueId;
+ try {
+ ObjectId headId = repository.resolve(GB_ISSUES + "^{commit}");
+ ObjectInserter odi = repository.newObjectInserter();
+ try {
+ // Create the in-memory index of the new/updated issue
+ DirCache index = DirCache.newInCore();
+ DirCacheBuilder dcBuilder = index.builder();
+ // Traverse HEAD to add all other paths
+ TreeWalk treeWalk = new TreeWalk(repository);
+ int hIdx = -1;
+ if (headId != null)
+ hIdx = treeWalk.addTree(new RevWalk(repository).parseTree(headId));
+ treeWalk.setRecursive(true);
+ while (treeWalk.next()) {
+ String path = treeWalk.getPathString();
+ CanonicalTreeParser hTree = null;
+ if (hIdx != -1)
+ hTree = treeWalk.getTree(hIdx, CanonicalTreeParser.class);
+ if (!path.startsWith(issuePath)) {
+ // add entries from HEAD for all other paths
+ if (hTree != null) {
+ // create a new DirCacheEntry with data retrieved
+ // from HEAD
+ final DirCacheEntry dcEntry = new DirCacheEntry(path);
+ dcEntry.setObjectId(hTree.getEntryObjectId());
+ dcEntry.setFileMode(hTree.getEntryFileMode());
+
+ // add to temporary in-core index
+ dcBuilder.add(dcEntry);
+ }
+ }
+ }
+
+ // release the treewalk
+ treeWalk.release();
+
+ // finish temporary in-core index used for this commit
+ dcBuilder.finish();
+
+ ObjectId indexTreeId = index.writeTree(odi);
+
+ // Create a commit object
+ PersonIdent ident = new PersonIdent(author, "gitblit@localhost");
+ CommitBuilder commit = new CommitBuilder();
+ commit.setAuthor(ident);
+ commit.setCommitter(ident);
+ commit.setEncoding(Constants.CHARACTER_ENCODING);
+ commit.setMessage(message);
+ commit.setParentId(headId);
+ commit.setTreeId(indexTreeId);
+
+ // Insert the commit into the repository
+ ObjectId commitId = odi.insert(commit);
+ odi.flush();
+
+ RevWalk revWalk = new RevWalk(repository);
+ try {
+ RevCommit revCommit = revWalk.parseCommit(commitId);
+ RefUpdate ru = repository.updateRef(GB_ISSUES);
+ ru.setNewObjectId(commitId);
+ ru.setExpectedOldObjectId(headId);
+ ru.setRefLogMessage("commit: " + revCommit.getShortMessage(), false);
+ Result rc = ru.forceUpdate();
+ switch (rc) {
+ case NEW:
+ case FORCED:
+ case FAST_FORWARD:
+ success = true;
+ break;
+ case REJECTED:
+ case LOCK_FAILURE:
+ throw new ConcurrentRefUpdateException(JGitText.get().couldNotLockHEAD,
+ ru.getRef(), rc);
+ default:
+ throw new JGitInternalException(MessageFormat.format(
+ JGitText.get().updatingRefFailed, GB_ISSUES, commitId.toString(),
+ rc));
+ }
+ } finally {
+ revWalk.release();
+ }
+ } finally {
+ odi.release();
+ }
+ } catch (Throwable t) {
+ error(t, repository, "Failed to delete issue {1} to {0}", issueId);
+ }
+ return success;
+ }
+
+ /**
+ * Changes the text of an issue comment.
+ *
+ * @param repository
+ * @param issue
+ * @param change
+ * the change with the comment to change
+ * @param author
+ * the author of the revision
+ * @param comment
+ * the revised comment
+ * @return true, if the change was successful
+ */
+ public static boolean changeComment(Repository repository, IssueModel issue, Change change,
+ String author, String comment) {
+ Change revision = new Change(author);
+ revision.comment(comment);
+ revision.comment.id = change.comment.id;
+ return updateIssue(repository, issue.id, revision);
+ }
+
+ /**
+ * Deletes a comment from an issue.
+ *
+ * @param repository
+ * @param issue
+ * @param change
+ * the change with the comment to delete
+ * @param author
+ * @return true, if the deletion was successful
+ */
+ public static boolean deleteComment(Repository repository, IssueModel issue, Change change,
+ String author) {
+ Change deletion = new Change(author);
+ deletion.comment(change.comment.text);
+ deletion.comment.id = change.comment.id;
+ deletion.comment.deleted = true;
+ return updateIssue(repository, issue.id, deletion);
+ }
+
+ /**
+ * Commit a change to the repository. Each issue is composed on changes.
+ * Issues are built from applying the changes in the order they were
+ * committed to the repository. The changes are actually specified in the
+ * commit messages and not in the RevTrees which allows for clean,
+ * distributed merging.
+ *
+ * @param repository
+ * @param issueId
+ * @param change
+ * @return true, if the change was committed
+ */
+ private static boolean commit(Repository repository, String issueId, Change change) {
+ boolean success = false;
+
+ try {
+ // assign ids to new attachments
+ // attachments are stored by an SHA1 id
+ if (change.hasAttachments()) {
+ for (Attachment attachment : change.attachments) {
+ if (!ArrayUtils.isEmpty(attachment.content)) {
+ byte[] prefix = (change.created.toString() + change.author).getBytes();
+ byte[] bytes = new byte[prefix.length + attachment.content.length];
+ System.arraycopy(prefix, 0, bytes, 0, prefix.length);
+ System.arraycopy(attachment.content, 0, bytes, prefix.length,
+ attachment.content.length);
+ attachment.id = "attachment-" + StringUtils.getSHA1(bytes);
+ }
+ }
+ }
+
+ // serialize the change as json
+ // exclude any attachment from json serialization
+ Gson gson = JsonUtils.gson(new ExcludeField(
+ "com.gitblit.models.IssueModel$Attachment.content"));
+ String json = gson.toJson(change);
+
+ // include the json change in the commit message
+ String issuePath = getIssuePath(issueId);
+ String message = change.code + " " + issueId + "\n\n" + json;
+
+ // Create a commit file. This is required for a proper commit and
+ // ensures we can retrieve the commit log of the issue path.
+ //
+ // This file is NOT serialized as part of the Change object.
+ switch (change.code) {
+ case '+': {
+ // New Issue.
+ Attachment placeholder = new Attachment("issue");
+ placeholder.id = placeholder.name;
+ placeholder.content = "DO NOT REMOVE".getBytes(Constants.CHARACTER_ENCODING);
+ change.addAttachment(placeholder);
+ break;
+ }
+ default: {
+ // Update Issue.
+ String changeId = StringUtils.getSHA1(json);
+ Attachment placeholder = new Attachment("change-" + changeId);
+ placeholder.id = placeholder.name;
+ placeholder.content = "REMOVABLE".getBytes(Constants.CHARACTER_ENCODING);
+ change.addAttachment(placeholder);
+ break;
+ }
+ }
+
+ ObjectId headId = repository.resolve(GB_ISSUES + "^{commit}");
+ ObjectInserter odi = repository.newObjectInserter();
+ try {
+ // Create the in-memory index of the new/updated issue
+ DirCache index = createIndex(repository, headId, issuePath, change);
+ ObjectId indexTreeId = index.writeTree(odi);
+
+ // Create a commit object
+ PersonIdent ident = new PersonIdent(change.author, "gitblit@localhost");
+ CommitBuilder commit = new CommitBuilder();
+ commit.setAuthor(ident);
+ commit.setCommitter(ident);
+ commit.setEncoding(Constants.CHARACTER_ENCODING);
+ commit.setMessage(message);
+ commit.setParentId(headId);
+ commit.setTreeId(indexTreeId);
+
+ // Insert the commit into the repository
+ ObjectId commitId = odi.insert(commit);
+ odi.flush();
+
+ RevWalk revWalk = new RevWalk(repository);
+ try {
+ RevCommit revCommit = revWalk.parseCommit(commitId);
+ RefUpdate ru = repository.updateRef(GB_ISSUES);
+ ru.setNewObjectId(commitId);
+ ru.setExpectedOldObjectId(headId);
+ ru.setRefLogMessage("commit: " + revCommit.getShortMessage(), false);
+ Result rc = ru.forceUpdate();
+ switch (rc) {
+ case NEW:
+ case FORCED:
+ case FAST_FORWARD:
+ success = true;
+ break;
+ case REJECTED:
+ case LOCK_FAILURE:
+ throw new ConcurrentRefUpdateException(JGitText.get().couldNotLockHEAD,
+ ru.getRef(), rc);
+ default:
+ throw new JGitInternalException(MessageFormat.format(
+ JGitText.get().updatingRefFailed, GB_ISSUES, commitId.toString(),
+ rc));
+ }
+ } finally {
+ revWalk.release();
+ }
+ } finally {
+ odi.release();
+ }
+ } catch (Throwable t) {
+ error(t, repository, "Failed to commit issue {1} to {0}", issueId);
+ }
+ return success;
+ }
+
+ /**
+ * Returns the issue path. This follows the same scheme as Git's object
+ * store path where the first two characters of the hash id are the root
+ * folder with the remaining characters as a subfolder within that folder.
+ *
+ * @param issueId
+ * @return the root path of the issue content on the gb-issues branch
+ */
+ static String getIssuePath(String issueId) {
+ return issueId.substring(0, 2) + "/" + issueId.substring(2);
+ }
+
+ /**
+ * Creates an in-memory index of the issue change.
+ *
+ * @param repo
+ * @param headId
+ * @param change
+ * @return an in-memory index
+ * @throws IOException
+ */
+ private static DirCache createIndex(Repository repo, ObjectId headId, String issuePath,
+ Change change) throws IOException {
+
+ DirCache inCoreIndex = DirCache.newInCore();
+ DirCacheBuilder dcBuilder = inCoreIndex.builder();
+ ObjectInserter inserter = repo.newObjectInserter();
+
+ Set<String> ignorePaths = new TreeSet<String>();
+ try {
+ // Add any attachments to the temporary index
+ if (change.hasAttachments()) {
+ for (Attachment attachment : change.attachments) {
+ // build a path name for the attachment and mark as ignored
+ String path = issuePath + "/" + attachment.id;
+ ignorePaths.add(path);
+
+ // create an index entry for this attachment
+ final DirCacheEntry dcEntry = new DirCacheEntry(path);
+ dcEntry.setLength(attachment.content.length);
+ dcEntry.setLastModified(change.created.getTime());
+ dcEntry.setFileMode(FileMode.REGULAR_FILE);
+
+ // insert object
+ dcEntry.setObjectId(inserter.insert(Constants.OBJ_BLOB, attachment.content));
+
+ // add to temporary in-core index
+ dcBuilder.add(dcEntry);
+ }
+ }
+
+ // Traverse HEAD to add all other paths
+ TreeWalk treeWalk = new TreeWalk(repo);
+ int hIdx = -1;
+ if (headId != null)
+ hIdx = treeWalk.addTree(new RevWalk(repo).parseTree(headId));
+ treeWalk.setRecursive(true);
+
+ while (treeWalk.next()) {
+ String path = treeWalk.getPathString();
+ CanonicalTreeParser hTree = null;
+ if (hIdx != -1)
+ hTree = treeWalk.getTree(hIdx, CanonicalTreeParser.class);
+ if (!ignorePaths.contains(path)) {
+ // add entries from HEAD for all other paths
+ if (hTree != null) {
+ // create a new DirCacheEntry with data retrieved from
+ // HEAD
+ final DirCacheEntry dcEntry = new DirCacheEntry(path);
+ dcEntry.setObjectId(hTree.getEntryObjectId());
+ dcEntry.setFileMode(hTree.getEntryFileMode());
+
+ // add to temporary in-core index
+ dcBuilder.add(dcEntry);
+ }
+ }
+ }
+
+ // release the treewalk
+ treeWalk.release();
+
+ // finish temporary in-core index used for this commit
+ dcBuilder.finish();
+ } finally {
+ inserter.release();
+ }
+ return inCoreIndex;
+ }
+} \ No newline at end of file
diff --git a/src/main/java/com/gitblit/utils/JGitUtils.java b/src/main/java/com/gitblit/utils/JGitUtils.java
new file mode 100644
index 00000000..1f2ae943
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/JGitUtils.java
@@ -0,0 +1,1775 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.utils;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.regex.Pattern;
+
+import org.eclipse.jgit.api.CloneCommand;
+import org.eclipse.jgit.api.FetchCommand;
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.diff.DiffEntry;
+import org.eclipse.jgit.diff.DiffEntry.ChangeType;
+import org.eclipse.jgit.diff.DiffFormatter;
+import org.eclipse.jgit.diff.RawTextComparator;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.errors.StopWalkException;
+import org.eclipse.jgit.lib.BlobBasedConfig;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.ObjectLoader;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.RefUpdate.Result;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.RepositoryCache.FileKey;
+import org.eclipse.jgit.lib.TreeFormatter;
+import org.eclipse.jgit.revwalk.RevBlob;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevObject;
+import org.eclipse.jgit.revwalk.RevSort;
+import org.eclipse.jgit.revwalk.RevTree;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.revwalk.filter.CommitTimeRevFilter;
+import org.eclipse.jgit.revwalk.filter.RevFilter;
+import org.eclipse.jgit.storage.file.FileRepository;
+import org.eclipse.jgit.transport.CredentialsProvider;
+import org.eclipse.jgit.transport.FetchResult;
+import org.eclipse.jgit.transport.RefSpec;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.treewalk.filter.AndTreeFilter;
+import org.eclipse.jgit.treewalk.filter.OrTreeFilter;
+import org.eclipse.jgit.treewalk.filter.PathFilter;
+import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
+import org.eclipse.jgit.treewalk.filter.PathSuffixFilter;
+import org.eclipse.jgit.treewalk.filter.TreeFilter;
+import org.eclipse.jgit.util.FS;
+import org.eclipse.jgit.util.io.DisabledOutputStream;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.models.GitNote;
+import com.gitblit.models.PathModel;
+import com.gitblit.models.PathModel.PathChangeModel;
+import com.gitblit.models.RefModel;
+import com.gitblit.models.SubmoduleModel;
+
+/**
+ * Collection of static methods for retrieving information from a repository.
+ *
+ * @author James Moger
+ *
+ */
+public class JGitUtils {
+
+ static final Logger LOGGER = LoggerFactory.getLogger(JGitUtils.class);
+
+ /**
+ * Log an error message and exception.
+ *
+ * @param t
+ * @param repository
+ * if repository is not null it MUST be the {0} parameter in the
+ * pattern.
+ * @param pattern
+ * @param objects
+ */
+ private static void error(Throwable t, Repository repository, String pattern, Object... objects) {
+ List<Object> parameters = new ArrayList<Object>();
+ if (objects != null && objects.length > 0) {
+ for (Object o : objects) {
+ parameters.add(o);
+ }
+ }
+ if (repository != null) {
+ parameters.add(0, repository.getDirectory().getAbsolutePath());
+ }
+ LOGGER.error(MessageFormat.format(pattern, parameters.toArray()), t);
+ }
+
+ /**
+ * Returns the displayable name of the person in the form "Real Name <email
+ * address>". If the email address is empty, just "Real Name" is returned.
+ *
+ * @param person
+ * @return "Real Name <email address>" or "Real Name"
+ */
+ public static String getDisplayName(PersonIdent person) {
+ if (StringUtils.isEmpty(person.getEmailAddress())) {
+ return person.getName();
+ }
+ final StringBuilder r = new StringBuilder();
+ r.append(person.getName());
+ r.append(" <");
+ r.append(person.getEmailAddress());
+ r.append('>');
+ return r.toString().trim();
+ }
+
+ /**
+ * Encapsulates the result of cloning or pulling from a repository.
+ */
+ public static class CloneResult {
+ public String name;
+ public FetchResult fetchResult;
+ public boolean createdRepository;
+ }
+
+ /**
+ * Clone or Fetch a repository. If the local repository does not exist,
+ * clone is called. If the repository does exist, fetch is called. By
+ * default the clone/fetch retrieves the remote heads, tags, and notes.
+ *
+ * @param repositoriesFolder
+ * @param name
+ * @param fromUrl
+ * @return CloneResult
+ * @throws Exception
+ */
+ public static CloneResult cloneRepository(File repositoriesFolder, String name, String fromUrl)
+ throws Exception {
+ return cloneRepository(repositoriesFolder, name, fromUrl, true, null);
+ }
+
+ /**
+ * Clone or Fetch a repository. If the local repository does not exist,
+ * clone is called. If the repository does exist, fetch is called. By
+ * default the clone/fetch retrieves the remote heads, tags, and notes.
+ *
+ * @param repositoriesFolder
+ * @param name
+ * @param fromUrl
+ * @param bare
+ * @param credentialsProvider
+ * @return CloneResult
+ * @throws Exception
+ */
+ public static CloneResult cloneRepository(File repositoriesFolder, String name, String fromUrl,
+ boolean bare, CredentialsProvider credentialsProvider) throws Exception {
+ CloneResult result = new CloneResult();
+ if (bare) {
+ // bare repository, ensure .git suffix
+ if (!name.toLowerCase().endsWith(Constants.DOT_GIT_EXT)) {
+ name += Constants.DOT_GIT_EXT;
+ }
+ } else {
+ // normal repository, strip .git suffix
+ if (name.toLowerCase().endsWith(Constants.DOT_GIT_EXT)) {
+ name = name.substring(0, name.indexOf(Constants.DOT_GIT_EXT));
+ }
+ }
+ result.name = name;
+
+ File folder = new File(repositoriesFolder, name);
+ if (folder.exists()) {
+ File gitDir = FileKey.resolve(new File(repositoriesFolder, name), FS.DETECTED);
+ FileRepository repository = new FileRepository(gitDir);
+ result.fetchResult = fetchRepository(credentialsProvider, repository);
+ repository.close();
+ } else {
+ CloneCommand clone = new CloneCommand();
+ clone.setBare(bare);
+ clone.setCloneAllBranches(true);
+ clone.setURI(fromUrl);
+ clone.setDirectory(folder);
+ if (credentialsProvider != null) {
+ clone.setCredentialsProvider(credentialsProvider);
+ }
+ Repository repository = clone.call().getRepository();
+
+ // Now we have to fetch because CloneCommand doesn't fetch
+ // refs/notes nor does it allow manual RefSpec.
+ result.createdRepository = true;
+ result.fetchResult = fetchRepository(credentialsProvider, repository);
+ repository.close();
+ }
+ return result;
+ }
+
+ /**
+ * Fetch updates from the remote repository. If refSpecs is unspecifed,
+ * remote heads, tags, and notes are retrieved.
+ *
+ * @param credentialsProvider
+ * @param repository
+ * @param refSpecs
+ * @return FetchResult
+ * @throws Exception
+ */
+ public static FetchResult fetchRepository(CredentialsProvider credentialsProvider,
+ Repository repository, RefSpec... refSpecs) throws Exception {
+ Git git = new Git(repository);
+ FetchCommand fetch = git.fetch();
+ List<RefSpec> specs = new ArrayList<RefSpec>();
+ if (refSpecs == null || refSpecs.length == 0) {
+ specs.add(new RefSpec("+refs/heads/*:refs/remotes/origin/*"));
+ specs.add(new RefSpec("+refs/tags/*:refs/tags/*"));
+ specs.add(new RefSpec("+refs/notes/*:refs/notes/*"));
+ } else {
+ specs.addAll(Arrays.asList(refSpecs));
+ }
+ if (credentialsProvider != null) {
+ fetch.setCredentialsProvider(credentialsProvider);
+ }
+ fetch.setRefSpecs(specs);
+ FetchResult fetchRes = fetch.call();
+ return fetchRes;
+ }
+
+ /**
+ * Creates a bare repository.
+ *
+ * @param repositoriesFolder
+ * @param name
+ * @return Repository
+ */
+ public static Repository createRepository(File repositoriesFolder, String name) {
+ try {
+ Git git = Git.init().setDirectory(new File(repositoriesFolder, name)).setBare(true).call();
+ return git.getRepository();
+ } catch (GitAPIException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Returns a list of repository names in the specified folder.
+ *
+ * @param repositoriesFolder
+ * @param onlyBare
+ * if true, only bare repositories repositories are listed. If
+ * false all repositories are included.
+ * @param searchSubfolders
+ * recurse into subfolders to find grouped repositories
+ * @param depth
+ * optional recursion depth, -1 = infinite recursion
+ * @param exclusions
+ * list of regex exclusions for matching to folder names
+ * @return list of repository names
+ */
+ public static List<String> getRepositoryList(File repositoriesFolder, boolean onlyBare,
+ boolean searchSubfolders, int depth, List<String> exclusions) {
+ List<String> list = new ArrayList<String>();
+ if (repositoriesFolder == null || !repositoriesFolder.exists()) {
+ return list;
+ }
+ List<Pattern> patterns = new ArrayList<Pattern>();
+ if (!ArrayUtils.isEmpty(exclusions)) {
+ for (String regex : exclusions) {
+ patterns.add(Pattern.compile(regex));
+ }
+ }
+ list.addAll(getRepositoryList(repositoriesFolder.getAbsolutePath(), repositoriesFolder,
+ onlyBare, searchSubfolders, depth, patterns));
+ StringUtils.sortRepositorynames(list);
+ return list;
+ }
+
+ /**
+ * Recursive function to find git repositories.
+ *
+ * @param basePath
+ * basePath is stripped from the repository name as repositories
+ * are relative to this path
+ * @param searchFolder
+ * @param onlyBare
+ * if true only bare repositories will be listed. if false all
+ * repositories are included.
+ * @param searchSubfolders
+ * recurse into subfolders to find grouped repositories
+ * @param depth
+ * recursion depth, -1 = infinite recursion
+ * @param patterns
+ * list of regex patterns for matching to folder names
+ * @return
+ */
+ private static List<String> getRepositoryList(String basePath, File searchFolder,
+ boolean onlyBare, boolean searchSubfolders, int depth, List<Pattern> patterns) {
+ File baseFile = new File(basePath);
+ List<String> list = new ArrayList<String>();
+ if (depth == 0) {
+ return list;
+ }
+
+ int nextDepth = (depth == -1) ? -1 : depth - 1;
+ for (File file : searchFolder.listFiles()) {
+ if (file.isDirectory()) {
+ boolean exclude = false;
+ for (Pattern pattern : patterns) {
+ String path = FileUtils.getRelativePath(baseFile, file).replace('\\', '/');
+ if (pattern.matcher(path).matches()) {
+ LOGGER.debug(MessageFormat.format("excluding {0} because of rule {1}", path, pattern.pattern()));
+ exclude = true;
+ break;
+ }
+ }
+ if (exclude) {
+ // skip to next file
+ continue;
+ }
+
+ File gitDir = FileKey.resolve(new File(searchFolder, file.getName()), FS.DETECTED);
+ if (gitDir != null) {
+ if (onlyBare && gitDir.getName().equals(".git")) {
+ continue;
+ }
+ if (gitDir.equals(file) || gitDir.getParentFile().equals(file)) {
+ // determine repository name relative to base path
+ String repository = FileUtils.getRelativePath(baseFile, file);
+ list.add(repository);
+ } else if (searchSubfolders && file.canRead()) {
+ // look for repositories in subfolders
+ list.addAll(getRepositoryList(basePath, file, onlyBare, searchSubfolders,
+ nextDepth, patterns));
+ }
+ } else if (searchSubfolders && file.canRead()) {
+ // look for repositories in subfolders
+ list.addAll(getRepositoryList(basePath, file, onlyBare, searchSubfolders,
+ nextDepth, patterns));
+ }
+ }
+ }
+ return list;
+ }
+
+ /**
+ * Returns the first commit on a branch. If the repository does not exist or
+ * is empty, null is returned.
+ *
+ * @param repository
+ * @param branch
+ * if unspecified, HEAD is assumed.
+ * @return RevCommit
+ */
+ public static RevCommit getFirstCommit(Repository repository, String branch) {
+ if (!hasCommits(repository)) {
+ return null;
+ }
+ RevCommit commit = null;
+ try {
+ // resolve branch
+ ObjectId branchObject;
+ if (StringUtils.isEmpty(branch)) {
+ branchObject = getDefaultBranch(repository);
+ } else {
+ branchObject = repository.resolve(branch);
+ }
+
+ RevWalk walk = new RevWalk(repository);
+ walk.sort(RevSort.REVERSE);
+ RevCommit head = walk.parseCommit(branchObject);
+ walk.markStart(head);
+ commit = walk.next();
+ walk.dispose();
+ } catch (Throwable t) {
+ error(t, repository, "{0} failed to determine first commit");
+ }
+ return commit;
+ }
+
+ /**
+ * Returns the date of the first commit on a branch. If the repository does
+ * not exist, Date(0) is returned. If the repository does exist bit is
+ * empty, the last modified date of the repository folder is returned.
+ *
+ * @param repository
+ * @param branch
+ * if unspecified, HEAD is assumed.
+ * @return Date of the first commit on a branch
+ */
+ public static Date getFirstChange(Repository repository, String branch) {
+ RevCommit commit = getFirstCommit(repository, branch);
+ if (commit == null) {
+ if (repository == null || !repository.getDirectory().exists()) {
+ return new Date(0);
+ }
+ // fresh repository
+ return new Date(repository.getDirectory().lastModified());
+ }
+ return getCommitDate(commit);
+ }
+
+ /**
+ * Determine if a repository has any commits. This is determined by checking
+ * the for loose and packed objects.
+ *
+ * @param repository
+ * @return true if the repository has commits
+ */
+ public static boolean hasCommits(Repository repository) {
+ if (repository != null && repository.getDirectory().exists()) {
+ return (new File(repository.getDirectory(), "objects").list().length > 2)
+ || (new File(repository.getDirectory(), "objects/pack").list().length > 0);
+ }
+ return false;
+ }
+
+ /**
+ * Returns the date of the most recent commit on a branch. If the repository
+ * does not exist Date(0) is returned. If it does exist but is empty, the
+ * last modified date of the repository folder is returned.
+ *
+ * @param repository
+ * @return
+ */
+ public static Date getLastChange(Repository repository) {
+ if (!hasCommits(repository)) {
+ // null repository
+ if (repository == null) {
+ return new Date(0);
+ }
+ // fresh repository
+ return new Date(repository.getDirectory().lastModified());
+ }
+
+ List<RefModel> branchModels = getLocalBranches(repository, true, -1);
+ if (branchModels.size() > 0) {
+ // find most recent branch update
+ Date lastChange = new Date(0);
+ for (RefModel branchModel : branchModels) {
+ if (branchModel.getDate().after(lastChange)) {
+ lastChange = branchModel.getDate();
+ }
+ }
+ return lastChange;
+ }
+
+ // default to the repository folder modification date
+ return new Date(repository.getDirectory().lastModified());
+ }
+
+ /**
+ * Retrieves a Java Date from a Git commit.
+ *
+ * @param commit
+ * @return date of the commit or Date(0) if the commit is null
+ */
+ public static Date getCommitDate(RevCommit commit) {
+ if (commit == null) {
+ return new Date(0);
+ }
+ return new Date(commit.getCommitTime() * 1000L);
+ }
+
+ /**
+ * Retrieves a Java Date from a Git commit.
+ *
+ * @param commit
+ * @return date of the commit or Date(0) if the commit is null
+ */
+ public static Date getAuthorDate(RevCommit commit) {
+ if (commit == null) {
+ return new Date(0);
+ }
+ return commit.getAuthorIdent().getWhen();
+ }
+
+ /**
+ * Returns the specified commit from the repository. If the repository does
+ * not exist or is empty, null is returned.
+ *
+ * @param repository
+ * @param objectId
+ * if unspecified, HEAD is assumed.
+ * @return RevCommit
+ */
+ public static RevCommit getCommit(Repository repository, String objectId) {
+ if (!hasCommits(repository)) {
+ return null;
+ }
+ RevCommit commit = null;
+ try {
+ // resolve object id
+ ObjectId branchObject;
+ if (StringUtils.isEmpty(objectId)) {
+ branchObject = getDefaultBranch(repository);
+ } else {
+ branchObject = repository.resolve(objectId);
+ }
+ RevWalk walk = new RevWalk(repository);
+ RevCommit rev = walk.parseCommit(branchObject);
+ commit = rev;
+ walk.dispose();
+ } catch (Throwable t) {
+ error(t, repository, "{0} failed to get commit {1}", objectId);
+ }
+ return commit;
+ }
+
+ /**
+ * Retrieves the raw byte content of a file in the specified tree.
+ *
+ * @param repository
+ * @param tree
+ * if null, the RevTree from HEAD is assumed.
+ * @param path
+ * @return content as a byte []
+ */
+ public static byte[] getByteContent(Repository repository, RevTree tree, final String path, boolean throwError) {
+ RevWalk rw = new RevWalk(repository);
+ TreeWalk tw = new TreeWalk(repository);
+ tw.setFilter(PathFilterGroup.createFromStrings(Collections.singleton(path)));
+ byte[] content = null;
+ try {
+ if (tree == null) {
+ ObjectId object = getDefaultBranch(repository);
+ RevCommit commit = rw.parseCommit(object);
+ tree = commit.getTree();
+ }
+ tw.reset(tree);
+ while (tw.next()) {
+ if (tw.isSubtree() && !path.equals(tw.getPathString())) {
+ tw.enterSubtree();
+ continue;
+ }
+ ObjectId entid = tw.getObjectId(0);
+ FileMode entmode = tw.getFileMode(0);
+ if (entmode != FileMode.GITLINK) {
+ RevObject ro = rw.lookupAny(entid, entmode.getObjectType());
+ rw.parseBody(ro);
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ ObjectLoader ldr = repository.open(ro.getId(), Constants.OBJ_BLOB);
+ byte[] tmp = new byte[4096];
+ InputStream in = ldr.openStream();
+ int n;
+ while ((n = in.read(tmp)) > 0) {
+ os.write(tmp, 0, n);
+ }
+ in.close();
+ content = os.toByteArray();
+ }
+ }
+ } catch (Throwable t) {
+ if (throwError) {
+ error(t, repository, "{0} can't find {1} in tree {2}", path, tree.name());
+ }
+ } finally {
+ rw.dispose();
+ tw.release();
+ }
+ return content;
+ }
+
+ /**
+ * Returns the UTF-8 string content of a file in the specified tree.
+ *
+ * @param repository
+ * @param tree
+ * if null, the RevTree from HEAD is assumed.
+ * @param blobPath
+ * @param charsets optional
+ * @return UTF-8 string content
+ */
+ public static String getStringContent(Repository repository, RevTree tree, String blobPath, String... charsets) {
+ byte[] content = getByteContent(repository, tree, blobPath, true);
+ if (content == null) {
+ return null;
+ }
+ return StringUtils.decodeString(content, charsets);
+ }
+
+ /**
+ * Gets the raw byte content of the specified blob object.
+ *
+ * @param repository
+ * @param objectId
+ * @return byte [] blob content
+ */
+ public static byte[] getByteContent(Repository repository, String objectId) {
+ RevWalk rw = new RevWalk(repository);
+ byte[] content = null;
+ try {
+ RevBlob blob = rw.lookupBlob(ObjectId.fromString(objectId));
+ rw.parseBody(blob);
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ ObjectLoader ldr = repository.open(blob.getId(), Constants.OBJ_BLOB);
+ byte[] tmp = new byte[4096];
+ InputStream in = ldr.openStream();
+ int n;
+ while ((n = in.read(tmp)) > 0) {
+ os.write(tmp, 0, n);
+ }
+ in.close();
+ content = os.toByteArray();
+ } catch (Throwable t) {
+ error(t, repository, "{0} can't find blob {1}", objectId);
+ } finally {
+ rw.dispose();
+ }
+ return content;
+ }
+
+ /**
+ * Gets the UTF-8 string content of the blob specified by objectId.
+ *
+ * @param repository
+ * @param objectId
+ * @param charsets optional
+ * @return UTF-8 string content
+ */
+ public static String getStringContent(Repository repository, String objectId, String... charsets) {
+ byte[] content = getByteContent(repository, objectId);
+ if (content == null) {
+ return null;
+ }
+ return StringUtils.decodeString(content, charsets);
+ }
+
+ /**
+ * Returns the list of files in the specified folder at the specified
+ * commit. If the repository does not exist or is empty, an empty list is
+ * returned.
+ *
+ * @param repository
+ * @param path
+ * if unspecified, root folder is assumed.
+ * @param commit
+ * if null, HEAD is assumed.
+ * @return list of files in specified path
+ */
+ public static List<PathModel> getFilesInPath(Repository repository, String path,
+ RevCommit commit) {
+ List<PathModel> list = new ArrayList<PathModel>();
+ if (!hasCommits(repository)) {
+ return list;
+ }
+ if (commit == null) {
+ commit = getCommit(repository, null);
+ }
+ final TreeWalk tw = new TreeWalk(repository);
+ try {
+ tw.addTree(commit.getTree());
+ if (!StringUtils.isEmpty(path)) {
+ PathFilter f = PathFilter.create(path);
+ tw.setFilter(f);
+ tw.setRecursive(false);
+ boolean foundFolder = false;
+ while (tw.next()) {
+ if (!foundFolder && tw.isSubtree()) {
+ tw.enterSubtree();
+ }
+ if (tw.getPathString().equals(path)) {
+ foundFolder = true;
+ continue;
+ }
+ if (foundFolder) {
+ list.add(getPathModel(tw, path, commit));
+ }
+ }
+ } else {
+ tw.setRecursive(false);
+ while (tw.next()) {
+ list.add(getPathModel(tw, null, commit));
+ }
+ }
+ } catch (IOException e) {
+ error(e, repository, "{0} failed to get files for commit {1}", commit.getName());
+ } finally {
+ tw.release();
+ }
+ Collections.sort(list);
+ return list;
+ }
+
+ /**
+ * Returns the list of files changed in a specified commit. If the
+ * repository does not exist or is empty, an empty list is returned.
+ *
+ * @param repository
+ * @param commit
+ * if null, HEAD is assumed.
+ * @return list of files changed in a commit
+ */
+ public static List<PathChangeModel> getFilesInCommit(Repository repository, RevCommit commit) {
+ List<PathChangeModel> list = new ArrayList<PathChangeModel>();
+ if (!hasCommits(repository)) {
+ return list;
+ }
+ RevWalk rw = new RevWalk(repository);
+ try {
+ if (commit == null) {
+ ObjectId object = getDefaultBranch(repository);
+ commit = rw.parseCommit(object);
+ }
+
+ if (commit.getParentCount() == 0) {
+ TreeWalk tw = new TreeWalk(repository);
+ tw.reset();
+ tw.setRecursive(true);
+ tw.addTree(commit.getTree());
+ while (tw.next()) {
+ list.add(new PathChangeModel(tw.getPathString(), tw.getPathString(), 0, tw
+ .getRawMode(0), tw.getObjectId(0).getName(), commit.getId().getName(),
+ ChangeType.ADD));
+ }
+ tw.release();
+ } else {
+ RevCommit parent = rw.parseCommit(commit.getParent(0).getId());
+ DiffFormatter df = new DiffFormatter(DisabledOutputStream.INSTANCE);
+ df.setRepository(repository);
+ df.setDiffComparator(RawTextComparator.DEFAULT);
+ df.setDetectRenames(true);
+ List<DiffEntry> diffs = df.scan(parent.getTree(), commit.getTree());
+ for (DiffEntry diff : diffs) {
+ String objectId = diff.getNewId().name();
+ if (diff.getChangeType().equals(ChangeType.DELETE)) {
+ list.add(new PathChangeModel(diff.getOldPath(), diff.getOldPath(), 0, diff
+ .getNewMode().getBits(), objectId, commit.getId().getName(), diff
+ .getChangeType()));
+ } else if (diff.getChangeType().equals(ChangeType.RENAME)) {
+ list.add(new PathChangeModel(diff.getOldPath(), diff.getNewPath(), 0, diff
+ .getNewMode().getBits(), objectId, commit.getId().getName(), diff
+ .getChangeType()));
+ } else {
+ list.add(new PathChangeModel(diff.getNewPath(), diff.getNewPath(), 0, diff
+ .getNewMode().getBits(), objectId, commit.getId().getName(), diff
+ .getChangeType()));
+ }
+ }
+ }
+ } catch (Throwable t) {
+ error(t, repository, "{0} failed to determine files in commit!");
+ } finally {
+ rw.dispose();
+ }
+ return list;
+ }
+
+ /**
+ * Returns the list of files in the repository on the default branch that
+ * match one of the specified extensions. This is a CASE-SENSITIVE search.
+ * If the repository does not exist or is empty, an empty list is returned.
+ *
+ * @param repository
+ * @param extensions
+ * @return list of files in repository with a matching extension
+ */
+ public static List<PathModel> getDocuments(Repository repository, List<String> extensions) {
+ return getDocuments(repository, extensions, null);
+ }
+
+ /**
+ * Returns the list of files in the repository in the specified commit that
+ * match one of the specified extensions. This is a CASE-SENSITIVE search.
+ * If the repository does not exist or is empty, an empty list is returned.
+ *
+ * @param repository
+ * @param extensions
+ * @param objectId
+ * @return list of files in repository with a matching extension
+ */
+ public static List<PathModel> getDocuments(Repository repository, List<String> extensions,
+ String objectId) {
+ List<PathModel> list = new ArrayList<PathModel>();
+ if (!hasCommits(repository)) {
+ return list;
+ }
+ RevCommit commit = getCommit(repository, objectId);
+ final TreeWalk tw = new TreeWalk(repository);
+ try {
+ tw.addTree(commit.getTree());
+ if (extensions != null && extensions.size() > 0) {
+ List<TreeFilter> suffixFilters = new ArrayList<TreeFilter>();
+ for (String extension : extensions) {
+ if (extension.charAt(0) == '.') {
+ suffixFilters.add(PathSuffixFilter.create("\\" + extension));
+ } else {
+ // escape the . since this is a regexp filter
+ suffixFilters.add(PathSuffixFilter.create("\\." + extension));
+ }
+ }
+ TreeFilter filter;
+ if (suffixFilters.size() == 1) {
+ filter = suffixFilters.get(0);
+ } else {
+ filter = OrTreeFilter.create(suffixFilters);
+ }
+ tw.setFilter(filter);
+ tw.setRecursive(true);
+ }
+ while (tw.next()) {
+ list.add(getPathModel(tw, null, commit));
+ }
+ } catch (IOException e) {
+ error(e, repository, "{0} failed to get documents for commit {1}", commit.getName());
+ } finally {
+ tw.release();
+ }
+ Collections.sort(list);
+ return list;
+ }
+
+ /**
+ * Returns a path model of the current file in the treewalk.
+ *
+ * @param tw
+ * @param basePath
+ * @param commit
+ * @return a path model of the current file in the treewalk
+ */
+ private static PathModel getPathModel(TreeWalk tw, String basePath, RevCommit commit) {
+ String name;
+ long size = 0;
+ if (StringUtils.isEmpty(basePath)) {
+ name = tw.getPathString();
+ } else {
+ name = tw.getPathString().substring(basePath.length() + 1);
+ }
+ ObjectId objectId = tw.getObjectId(0);
+ try {
+ if (!tw.isSubtree() && (tw.getFileMode(0) != FileMode.GITLINK)) {
+ size = tw.getObjectReader().getObjectSize(objectId, Constants.OBJ_BLOB);
+ }
+ } catch (Throwable t) {
+ error(t, null, "failed to retrieve blob size for " + tw.getPathString());
+ }
+ return new PathModel(name, tw.getPathString(), size, tw.getFileMode(0).getBits(),
+ objectId.getName(), commit.getName());
+ }
+
+ /**
+ * Returns a permissions representation of the mode bits.
+ *
+ * @param mode
+ * @return string representation of the mode bits
+ */
+ public static String getPermissionsFromMode(int mode) {
+ if (FileMode.TREE.equals(mode)) {
+ return "drwxr-xr-x";
+ } else if (FileMode.REGULAR_FILE.equals(mode)) {
+ return "-rw-r--r--";
+ } else if (FileMode.EXECUTABLE_FILE.equals(mode)) {
+ return "-rwxr-xr-x";
+ } else if (FileMode.SYMLINK.equals(mode)) {
+ return "symlink";
+ } else if (FileMode.GITLINK.equals(mode)) {
+ return "submodule";
+ }
+ return "missing";
+ }
+
+ /**
+ * Returns a list of commits since the minimum date starting from the
+ * specified object id.
+ *
+ * @param repository
+ * @param objectId
+ * if unspecified, HEAD is assumed.
+ * @param minimumDate
+ * @return list of commits
+ */
+ public static List<RevCommit> getRevLog(Repository repository, String objectId, Date minimumDate) {
+ List<RevCommit> list = new ArrayList<RevCommit>();
+ if (!hasCommits(repository)) {
+ return list;
+ }
+ try {
+ // resolve branch
+ ObjectId branchObject;
+ if (StringUtils.isEmpty(objectId)) {
+ branchObject = getDefaultBranch(repository);
+ } else {
+ branchObject = repository.resolve(objectId);
+ }
+
+ RevWalk rw = new RevWalk(repository);
+ rw.markStart(rw.parseCommit(branchObject));
+ rw.setRevFilter(CommitTimeRevFilter.after(minimumDate));
+ Iterable<RevCommit> revlog = rw;
+ for (RevCommit rev : revlog) {
+ list.add(rev);
+ }
+ rw.dispose();
+ } catch (Throwable t) {
+ error(t, repository, "{0} failed to get {1} revlog for minimum date {2}", objectId,
+ minimumDate);
+ }
+ return list;
+ }
+
+ /**
+ * Returns a list of commits starting from HEAD and working backwards.
+ *
+ * @param repository
+ * @param maxCount
+ * if < 0, all commits for the repository are returned.
+ * @return list of commits
+ */
+ public static List<RevCommit> getRevLog(Repository repository, int maxCount) {
+ return getRevLog(repository, null, 0, maxCount);
+ }
+
+ /**
+ * Returns a list of commits starting from the specified objectId using an
+ * offset and maxCount for paging. This is similar to LIMIT n OFFSET p in
+ * SQL. If the repository does not exist or is empty, an empty list is
+ * returned.
+ *
+ * @param repository
+ * @param objectId
+ * if unspecified, HEAD is assumed.
+ * @param offset
+ * @param maxCount
+ * if < 0, all commits are returned.
+ * @return a paged list of commits
+ */
+ public static List<RevCommit> getRevLog(Repository repository, String objectId, int offset,
+ int maxCount) {
+ return getRevLog(repository, objectId, null, offset, maxCount);
+ }
+
+ /**
+ * Returns a list of commits for the repository or a path within the
+ * repository. Caller may specify ending revision with objectId. Caller may
+ * specify offset and maxCount to achieve pagination of results. If the
+ * repository does not exist or is empty, an empty list is returned.
+ *
+ * @param repository
+ * @param objectId
+ * if unspecified, HEAD is assumed.
+ * @param path
+ * if unspecified, commits for repository are returned. If
+ * specified, commits for the path are returned.
+ * @param offset
+ * @param maxCount
+ * if < 0, all commits are returned.
+ * @return a paged list of commits
+ */
+ public static List<RevCommit> getRevLog(Repository repository, String objectId, String path,
+ int offset, int maxCount) {
+ List<RevCommit> list = new ArrayList<RevCommit>();
+ if (maxCount == 0) {
+ return list;
+ }
+ if (!hasCommits(repository)) {
+ return list;
+ }
+ try {
+ // resolve branch
+ ObjectId branchObject;
+ if (StringUtils.isEmpty(objectId)) {
+ branchObject = getDefaultBranch(repository);
+ } else {
+ branchObject = repository.resolve(objectId);
+ }
+ if (branchObject == null) {
+ return list;
+ }
+
+ RevWalk rw = new RevWalk(repository);
+ rw.markStart(rw.parseCommit(branchObject));
+ if (!StringUtils.isEmpty(path)) {
+ TreeFilter filter = AndTreeFilter.create(
+ PathFilterGroup.createFromStrings(Collections.singleton(path)),
+ TreeFilter.ANY_DIFF);
+ rw.setTreeFilter(filter);
+ }
+ Iterable<RevCommit> revlog = rw;
+ if (offset > 0) {
+ int count = 0;
+ for (RevCommit rev : revlog) {
+ count++;
+ if (count > offset) {
+ list.add(rev);
+ if (maxCount > 0 && list.size() == maxCount) {
+ break;
+ }
+ }
+ }
+ } else {
+ for (RevCommit rev : revlog) {
+ list.add(rev);
+ if (maxCount > 0 && list.size() == maxCount) {
+ break;
+ }
+ }
+ }
+ rw.dispose();
+ } catch (Throwable t) {
+ error(t, repository, "{0} failed to get {1} revlog for path {2}", objectId, path);
+ }
+ return list;
+ }
+
+ /**
+ * Returns a list of commits for the repository within the range specified
+ * by startRangeId and endRangeId. If the repository does not exist or is
+ * empty, an empty list is returned.
+ *
+ * @param repository
+ * @param startRangeId
+ * the first commit (not included in results)
+ * @param endRangeId
+ * the end commit (included in results)
+ * @return a list of commits
+ */
+ public static List<RevCommit> getRevLog(Repository repository, String startRangeId,
+ String endRangeId) {
+ List<RevCommit> list = new ArrayList<RevCommit>();
+ if (!hasCommits(repository)) {
+ return list;
+ }
+ try {
+ ObjectId endRange = repository.resolve(endRangeId);
+ ObjectId startRange = repository.resolve(startRangeId);
+
+ RevWalk rw = new RevWalk(repository);
+ rw.markStart(rw.parseCommit(endRange));
+ if (startRange.equals(ObjectId.zeroId())) {
+ // maybe this is a tag or an orphan branch
+ list.add(rw.parseCommit(endRange));
+ rw.dispose();
+ return list;
+ } else {
+ rw.markUninteresting(rw.parseCommit(startRange));
+ }
+
+ Iterable<RevCommit> revlog = rw;
+ for (RevCommit rev : revlog) {
+ list.add(rev);
+ }
+ rw.dispose();
+ } catch (Throwable t) {
+ error(t, repository, "{0} failed to get revlog for {1}..{2}", startRangeId, endRangeId);
+ }
+ return list;
+ }
+
+ /**
+ * Search the commit history for a case-insensitive match to the value.
+ * Search results require a specified SearchType of AUTHOR, COMMITTER, or
+ * COMMIT. Results may be paginated using offset and maxCount. If the
+ * repository does not exist or is empty, an empty list is returned.
+ *
+ * @param repository
+ * @param objectId
+ * if unspecified, HEAD is assumed.
+ * @param value
+ * @param type
+ * AUTHOR, COMMITTER, COMMIT
+ * @param offset
+ * @param maxCount
+ * if < 0, all matches are returned
+ * @return matching list of commits
+ */
+ public static List<RevCommit> searchRevlogs(Repository repository, String objectId,
+ String value, final com.gitblit.Constants.SearchType type, int offset, int maxCount) {
+ final String lcValue = value.toLowerCase();
+ List<RevCommit> list = new ArrayList<RevCommit>();
+ if (maxCount == 0) {
+ return list;
+ }
+ if (!hasCommits(repository)) {
+ return list;
+ }
+ try {
+ // resolve branch
+ ObjectId branchObject;
+ if (StringUtils.isEmpty(objectId)) {
+ branchObject = getDefaultBranch(repository);
+ } else {
+ branchObject = repository.resolve(objectId);
+ }
+
+ RevWalk rw = new RevWalk(repository);
+ rw.setRevFilter(new RevFilter() {
+
+ @Override
+ public RevFilter clone() {
+ // FindBugs complains about this method name.
+ // This is part of JGit design and unrelated to Cloneable.
+ return this;
+ }
+
+ @Override
+ public boolean include(RevWalk walker, RevCommit commit) throws StopWalkException,
+ MissingObjectException, IncorrectObjectTypeException, IOException {
+ boolean include = false;
+ switch (type) {
+ case AUTHOR:
+ include = (commit.getAuthorIdent().getName().toLowerCase().indexOf(lcValue) > -1)
+ || (commit.getAuthorIdent().getEmailAddress().toLowerCase()
+ .indexOf(lcValue) > -1);
+ break;
+ case COMMITTER:
+ include = (commit.getCommitterIdent().getName().toLowerCase()
+ .indexOf(lcValue) > -1)
+ || (commit.getCommitterIdent().getEmailAddress().toLowerCase()
+ .indexOf(lcValue) > -1);
+ break;
+ case COMMIT:
+ include = commit.getFullMessage().toLowerCase().indexOf(lcValue) > -1;
+ break;
+ }
+ return include;
+ }
+
+ });
+ rw.markStart(rw.parseCommit(branchObject));
+ Iterable<RevCommit> revlog = rw;
+ if (offset > 0) {
+ int count = 0;
+ for (RevCommit rev : revlog) {
+ count++;
+ if (count > offset) {
+ list.add(rev);
+ if (maxCount > 0 && list.size() == maxCount) {
+ break;
+ }
+ }
+ }
+ } else {
+ for (RevCommit rev : revlog) {
+ list.add(rev);
+ if (maxCount > 0 && list.size() == maxCount) {
+ break;
+ }
+ }
+ }
+ rw.dispose();
+ } catch (Throwable t) {
+ error(t, repository, "{0} failed to {1} search revlogs for {2}", type.name(), value);
+ }
+ return list;
+ }
+
+ /**
+ * Returns the default branch to use for a repository. Normally returns
+ * whatever branch HEAD points to, but if HEAD points to nothing it returns
+ * the most recently updated branch.
+ *
+ * @param repository
+ * @return the objectid of a branch
+ * @throws Exception
+ */
+ public static ObjectId getDefaultBranch(Repository repository) throws Exception {
+ ObjectId object = repository.resolve(Constants.HEAD);
+ if (object == null) {
+ // no HEAD
+ // perhaps non-standard repository, try local branches
+ List<RefModel> branchModels = getLocalBranches(repository, true, -1);
+ if (branchModels.size() > 0) {
+ // use most recently updated branch
+ RefModel branch = null;
+ Date lastDate = new Date(0);
+ for (RefModel branchModel : branchModels) {
+ if (branchModel.getDate().after(lastDate)) {
+ branch = branchModel;
+ lastDate = branch.getDate();
+ }
+ }
+ object = branch.getReferencedObjectId();
+ }
+ }
+ return object;
+ }
+
+ /**
+ * Returns the target of the symbolic HEAD reference for a repository.
+ * Normally returns a branch reference name, but when HEAD is detached,
+ * the commit is matched against the known tags. The most recent matching
+ * tag ref name will be returned if it references the HEAD commit. If
+ * no match is found, the SHA1 is returned.
+ *
+ * @param repository
+ * @return the ref name or the SHA1 for a detached HEAD
+ */
+ public static String getHEADRef(Repository repository) {
+ String target = null;
+ try {
+ target = repository.getFullBranch();
+ if (!target.startsWith(Constants.R_HEADS)) {
+ // refers to an actual commit, probably a tag
+ // find latest tag that matches the commit, if any
+ List<RefModel> tagModels = getTags(repository, true, -1);
+ if (tagModels.size() > 0) {
+ RefModel tag = null;
+ Date lastDate = new Date(0);
+ for (RefModel tagModel : tagModels) {
+ if (tagModel.getReferencedObjectId().getName().equals(target) &&
+ tagModel.getDate().after(lastDate)) {
+ tag = tagModel;
+ lastDate = tag.getDate();
+ }
+ }
+ target = tag.getName();
+ }
+ }
+ } catch (Throwable t) {
+ error(t, repository, "{0} failed to get symbolic HEAD target");
+ }
+ return target;
+ }
+
+ /**
+ * Sets the symbolic ref HEAD to the specified target ref. The
+ * HEAD will be detached if the target ref is not a branch.
+ *
+ * @param repository
+ * @param targetRef
+ * @return true if successful
+ */
+ public static boolean setHEADtoRef(Repository repository, String targetRef) {
+ try {
+ // detach HEAD if target ref is not a branch
+ boolean detach = !targetRef.startsWith(Constants.R_HEADS);
+ RefUpdate.Result result;
+ RefUpdate head = repository.updateRef(Constants.HEAD, detach);
+ if (detach) { // Tag
+ RevCommit commit = getCommit(repository, targetRef);
+ head.setNewObjectId(commit.getId());
+ result = head.forceUpdate();
+ } else {
+ result = head.link(targetRef);
+ }
+ switch (result) {
+ case NEW:
+ case FORCED:
+ case NO_CHANGE:
+ case FAST_FORWARD:
+ return true;
+ default:
+ LOGGER.error(MessageFormat.format("{0} HEAD update to {1} returned result {2}",
+ repository.getDirectory().getAbsolutePath(), targetRef, result));
+ }
+ } catch (Throwable t) {
+ error(t, repository, "{0} failed to set HEAD to {1}", targetRef);
+ }
+ return false;
+ }
+
+ /**
+ * Sets the local branch ref to point to the specified commit id.
+ *
+ * @param repository
+ * @param branch
+ * @param commitId
+ * @return true if successful
+ */
+ public static boolean setBranchRef(Repository repository, String branch, String commitId) {
+ String branchName = branch;
+ if (!branchName.startsWith(Constants.R_HEADS)) {
+ branchName = Constants.R_HEADS + branch;
+ }
+
+ try {
+ RefUpdate refUpdate = repository.updateRef(branchName, false);
+ refUpdate.setNewObjectId(ObjectId.fromString(commitId));
+ RefUpdate.Result result = refUpdate.forceUpdate();
+
+ switch (result) {
+ case NEW:
+ case FORCED:
+ case NO_CHANGE:
+ case FAST_FORWARD:
+ return true;
+ default:
+ LOGGER.error(MessageFormat.format("{0} {1} update to {2} returned result {3}",
+ repository.getDirectory().getAbsolutePath(), branchName, commitId, result));
+ }
+ } catch (Throwable t) {
+ error(t, repository, "{0} failed to set {1} to {2}", branchName, commitId);
+ }
+ return false;
+ }
+
+ /**
+ * Deletes the specified branch ref.
+ *
+ * @param repository
+ * @param branch
+ * @return true if successful
+ */
+ public static boolean deleteBranchRef(Repository repository, String branch) {
+ String branchName = branch;
+ if (!branchName.startsWith(Constants.R_HEADS)) {
+ branchName = Constants.R_HEADS + branch;
+ }
+
+ try {
+ RefUpdate refUpdate = repository.updateRef(branchName, false);
+ refUpdate.setForceUpdate(true);
+ RefUpdate.Result result = refUpdate.delete();
+ switch (result) {
+ case NEW:
+ case FORCED:
+ case NO_CHANGE:
+ case FAST_FORWARD:
+ return true;
+ default:
+ LOGGER.error(MessageFormat.format("{0} failed to delete to {1} returned result {2}",
+ repository.getDirectory().getAbsolutePath(), branchName, result));
+ }
+ } catch (Throwable t) {
+ error(t, repository, "{0} failed to delete {1}", branchName);
+ }
+ return false;
+ }
+
+ /**
+ * Get the full branch and tag ref names for any potential HEAD targets.
+ *
+ * @param repository
+ * @return a list of ref names
+ */
+ public static List<String> getAvailableHeadTargets(Repository repository) {
+ List<String> targets = new ArrayList<String>();
+ for (RefModel branchModel : JGitUtils.getLocalBranches(repository, true, -1)) {
+ targets.add(branchModel.getName());
+ }
+
+ for (RefModel tagModel : JGitUtils.getTags(repository, true, -1)) {
+ targets.add(tagModel.getName());
+ }
+ return targets;
+ }
+
+ /**
+ * Returns all refs grouped by their associated object id.
+ *
+ * @param repository
+ * @return all refs grouped by their referenced object id
+ */
+ public static Map<ObjectId, List<RefModel>> getAllRefs(Repository repository) {
+ return getAllRefs(repository, true);
+ }
+
+ /**
+ * Returns all refs grouped by their associated object id.
+ *
+ * @param repository
+ * @param includeRemoteRefs
+ * @return all refs grouped by their referenced object id
+ */
+ public static Map<ObjectId, List<RefModel>> getAllRefs(Repository repository, boolean includeRemoteRefs) {
+ List<RefModel> list = getRefs(repository, org.eclipse.jgit.lib.RefDatabase.ALL, true, -1);
+ Map<ObjectId, List<RefModel>> refs = new HashMap<ObjectId, List<RefModel>>();
+ for (RefModel ref : list) {
+ if (!includeRemoteRefs && ref.getName().startsWith(Constants.R_REMOTES)) {
+ continue;
+ }
+ ObjectId objectid = ref.getReferencedObjectId();
+ if (!refs.containsKey(objectid)) {
+ refs.put(objectid, new ArrayList<RefModel>());
+ }
+ refs.get(objectid).add(ref);
+ }
+ return refs;
+ }
+
+ /**
+ * Returns the list of tags in the repository. If repository does not exist
+ * or is empty, an empty list is returned.
+ *
+ * @param repository
+ * @param fullName
+ * if true, /refs/tags/yadayadayada is returned. If false,
+ * yadayadayada is returned.
+ * @param maxCount
+ * if < 0, all tags are returned
+ * @return list of tags
+ */
+ public static List<RefModel> getTags(Repository repository, boolean fullName, int maxCount) {
+ return getRefs(repository, Constants.R_TAGS, fullName, maxCount);
+ }
+
+ /**
+ * Returns the list of local branches in the repository. If repository does
+ * not exist or is empty, an empty list is returned.
+ *
+ * @param repository
+ * @param fullName
+ * if true, /refs/heads/yadayadayada is returned. If false,
+ * yadayadayada is returned.
+ * @param maxCount
+ * if < 0, all local branches are returned
+ * @return list of local branches
+ */
+ public static List<RefModel> getLocalBranches(Repository repository, boolean fullName,
+ int maxCount) {
+ return getRefs(repository, Constants.R_HEADS, fullName, maxCount);
+ }
+
+ /**
+ * Returns the list of remote branches in the repository. If repository does
+ * not exist or is empty, an empty list is returned.
+ *
+ * @param repository
+ * @param fullName
+ * if true, /refs/remotes/yadayadayada is returned. If false,
+ * yadayadayada is returned.
+ * @param maxCount
+ * if < 0, all remote branches are returned
+ * @return list of remote branches
+ */
+ public static List<RefModel> getRemoteBranches(Repository repository, boolean fullName,
+ int maxCount) {
+ return getRefs(repository, Constants.R_REMOTES, fullName, maxCount);
+ }
+
+ /**
+ * Returns the list of note branches. If repository does not exist or is
+ * empty, an empty list is returned.
+ *
+ * @param repository
+ * @param fullName
+ * if true, /refs/notes/yadayadayada is returned. If false,
+ * yadayadayada is returned.
+ * @param maxCount
+ * if < 0, all note branches are returned
+ * @return list of note branches
+ */
+ public static List<RefModel> getNoteBranches(Repository repository, boolean fullName,
+ int maxCount) {
+ return getRefs(repository, Constants.R_NOTES, fullName, maxCount);
+ }
+
+ /**
+ * Returns the list of refs in the specified base ref. If repository does
+ * not exist or is empty, an empty list is returned.
+ *
+ * @param repository
+ * @param fullName
+ * if true, /refs/yadayadayada is returned. If false,
+ * yadayadayada is returned.
+ * @return list of refs
+ */
+ public static List<RefModel> getRefs(Repository repository, String baseRef) {
+ return getRefs(repository, baseRef, true, -1);
+ }
+
+ /**
+ * Returns a list of references in the repository matching "refs". If the
+ * repository is null or empty, an empty list is returned.
+ *
+ * @param repository
+ * @param refs
+ * if unspecified, all refs are returned
+ * @param fullName
+ * if true, /refs/something/yadayadayada is returned. If false,
+ * yadayadayada is returned.
+ * @param maxCount
+ * if < 0, all references are returned
+ * @return list of references
+ */
+ private static List<RefModel> getRefs(Repository repository, String refs, boolean fullName,
+ int maxCount) {
+ List<RefModel> list = new ArrayList<RefModel>();
+ if (maxCount == 0) {
+ return list;
+ }
+ if (!hasCommits(repository)) {
+ return list;
+ }
+ try {
+ Map<String, Ref> map = repository.getRefDatabase().getRefs(refs);
+ RevWalk rw = new RevWalk(repository);
+ for (Entry<String, Ref> entry : map.entrySet()) {
+ Ref ref = entry.getValue();
+ RevObject object = rw.parseAny(ref.getObjectId());
+ String name = entry.getKey();
+ if (fullName && !StringUtils.isEmpty(refs)) {
+ name = refs + name;
+ }
+ list.add(new RefModel(name, ref, object));
+ }
+ rw.dispose();
+ Collections.sort(list);
+ Collections.reverse(list);
+ if (maxCount > 0 && list.size() > maxCount) {
+ list = new ArrayList<RefModel>(list.subList(0, maxCount));
+ }
+ } catch (IOException e) {
+ error(e, repository, "{0} failed to retrieve {1}", refs);
+ }
+ return list;
+ }
+
+ /**
+ * Returns a RefModel for the gh-pages branch in the repository. If the
+ * branch can not be found, null is returned.
+ *
+ * @param repository
+ * @return a refmodel for the gh-pages branch or null
+ */
+ public static RefModel getPagesBranch(Repository repository) {
+ return getBranch(repository, "gh-pages");
+ }
+
+ /**
+ * Returns a RefModel for a specific branch name in the repository. If the
+ * branch can not be found, null is returned.
+ *
+ * @param repository
+ * @return a refmodel for the branch or null
+ */
+ public static RefModel getBranch(Repository repository, String name) {
+ RefModel branch = null;
+ try {
+ // search for the branch in local heads
+ for (RefModel ref : JGitUtils.getLocalBranches(repository, false, -1)) {
+ if (ref.reference.getName().endsWith(name)) {
+ branch = ref;
+ break;
+ }
+ }
+
+ // search for the branch in remote heads
+ if (branch == null) {
+ for (RefModel ref : JGitUtils.getRemoteBranches(repository, false, -1)) {
+ if (ref.reference.getName().endsWith(name)) {
+ branch = ref;
+ break;
+ }
+ }
+ }
+ } catch (Throwable t) {
+ LOGGER.error(MessageFormat.format("Failed to find {0} branch!", name), t);
+ }
+ return branch;
+ }
+
+ /**
+ * Returns the list of submodules for this repository.
+ *
+ * @param repository
+ * @param commit
+ * @return list of submodules
+ */
+ public static List<SubmoduleModel> getSubmodules(Repository repository, String commitId) {
+ RevCommit commit = getCommit(repository, commitId);
+ return getSubmodules(repository, commit.getTree());
+ }
+
+ /**
+ * Returns the list of submodules for this repository.
+ *
+ * @param repository
+ * @param commit
+ * @return list of submodules
+ */
+ public static List<SubmoduleModel> getSubmodules(Repository repository, RevTree tree) {
+ List<SubmoduleModel> list = new ArrayList<SubmoduleModel>();
+ byte [] blob = getByteContent(repository, tree, ".gitmodules", false);
+ if (blob == null) {
+ return list;
+ }
+ try {
+ BlobBasedConfig config = new BlobBasedConfig(repository.getConfig(), blob);
+ for (String module : config.getSubsections("submodule")) {
+ String path = config.getString("submodule", module, "path");
+ String url = config.getString("submodule", module, "url");
+ list.add(new SubmoduleModel(module, path, url));
+ }
+ } catch (ConfigInvalidException e) {
+ LOGGER.error("Failed to load .gitmodules file for " + repository.getDirectory(), e);
+ }
+ return list;
+ }
+
+ /**
+ * Returns the submodule definition for the specified path at the specified
+ * commit. If no module is defined for the path, null is returned.
+ *
+ * @param repository
+ * @param commit
+ * @param path
+ * @return a submodule definition or null if there is no submodule
+ */
+ public static SubmoduleModel getSubmoduleModel(Repository repository, String commitId, String path) {
+ for (SubmoduleModel model : getSubmodules(repository, commitId)) {
+ if (model.path.equals(path)) {
+ return model;
+ }
+ }
+ return null;
+ }
+
+ public static String getSubmoduleCommitId(Repository repository, String path, RevCommit commit) {
+ String commitId = null;
+ RevWalk rw = new RevWalk(repository);
+ TreeWalk tw = new TreeWalk(repository);
+ tw.setFilter(PathFilterGroup.createFromStrings(Collections.singleton(path)));
+ try {
+ tw.reset(commit.getTree());
+ while (tw.next()) {
+ if (tw.isSubtree() && !path.equals(tw.getPathString())) {
+ tw.enterSubtree();
+ continue;
+ }
+ if (FileMode.GITLINK == tw.getFileMode(0)) {
+ commitId = tw.getObjectId(0).getName();
+ break;
+ }
+ }
+ } catch (Throwable t) {
+ error(t, repository, "{0} can't find {1} in commit {2}", path, commit.name());
+ } finally {
+ rw.dispose();
+ tw.release();
+ }
+ return commitId;
+ }
+
+ /**
+ * Returns the list of notes entered about the commit from the refs/notes
+ * namespace. If the repository does not exist or is empty, an empty list is
+ * returned.
+ *
+ * @param repository
+ * @param commit
+ * @return list of notes
+ */
+ public static List<GitNote> getNotesOnCommit(Repository repository, RevCommit commit) {
+ List<GitNote> list = new ArrayList<GitNote>();
+ if (!hasCommits(repository)) {
+ return list;
+ }
+ List<RefModel> noteBranches = getNoteBranches(repository, true, -1);
+ for (RefModel notesRef : noteBranches) {
+ RevTree notesTree = JGitUtils.getCommit(repository, notesRef.getName()).getTree();
+ // flat notes list
+ String notePath = commit.getName();
+ String text = getStringContent(repository, notesTree, notePath);
+ if (!StringUtils.isEmpty(text)) {
+ List<RevCommit> history = getRevLog(repository, notesRef.getName(), notePath, 0, -1);
+ RefModel noteRef = new RefModel(notesRef.displayName, null, history.get(history
+ .size() - 1));
+ GitNote gitNote = new GitNote(noteRef, text);
+ list.add(gitNote);
+ continue;
+ }
+
+ // folder structure
+ StringBuilder sb = new StringBuilder(commit.getName());
+ sb.insert(2, '/');
+ notePath = sb.toString();
+ text = getStringContent(repository, notesTree, notePath);
+ if (!StringUtils.isEmpty(text)) {
+ List<RevCommit> history = getRevLog(repository, notesRef.getName(), notePath, 0, -1);
+ RefModel noteRef = new RefModel(notesRef.displayName, null, history.get(history
+ .size() - 1));
+ GitNote gitNote = new GitNote(noteRef, text);
+ list.add(gitNote);
+ }
+ }
+ return list;
+ }
+
+ /**
+ * Create an orphaned branch in a repository.
+ *
+ * @param repository
+ * @param branchName
+ * @param author
+ * if unspecified, Gitblit will be the author of this new branch
+ * @return true if successful
+ */
+ public static boolean createOrphanBranch(Repository repository, String branchName,
+ PersonIdent author) {
+ boolean success = false;
+ String message = "Created branch " + branchName;
+ if (author == null) {
+ author = new PersonIdent("Gitblit", "gitblit@localhost");
+ }
+ try {
+ ObjectInserter odi = repository.newObjectInserter();
+ try {
+ // Create a blob object to insert into a tree
+ ObjectId blobId = odi.insert(Constants.OBJ_BLOB,
+ message.getBytes(Constants.CHARACTER_ENCODING));
+
+ // Create a tree object to reference from a commit
+ TreeFormatter tree = new TreeFormatter();
+ tree.append(".branch", FileMode.REGULAR_FILE, blobId);
+ ObjectId treeId = odi.insert(tree);
+
+ // Create a commit object
+ CommitBuilder commit = new CommitBuilder();
+ commit.setAuthor(author);
+ commit.setCommitter(author);
+ commit.setEncoding(Constants.CHARACTER_ENCODING);
+ commit.setMessage(message);
+ commit.setTreeId(treeId);
+
+ // Insert the commit into the repository
+ ObjectId commitId = odi.insert(commit);
+ odi.flush();
+
+ RevWalk revWalk = new RevWalk(repository);
+ try {
+ RevCommit revCommit = revWalk.parseCommit(commitId);
+ if (!branchName.startsWith("refs/")) {
+ branchName = "refs/heads/" + branchName;
+ }
+ RefUpdate ru = repository.updateRef(branchName);
+ ru.setNewObjectId(commitId);
+ ru.setRefLogMessage("commit: " + revCommit.getShortMessage(), false);
+ Result rc = ru.forceUpdate();
+ switch (rc) {
+ case NEW:
+ case FORCED:
+ case FAST_FORWARD:
+ success = true;
+ break;
+ default:
+ success = false;
+ }
+ } finally {
+ revWalk.release();
+ }
+ } finally {
+ odi.release();
+ }
+ } catch (Throwable t) {
+ error(t, repository, "Failed to create orphan branch {1} in repository {0}", branchName);
+ }
+ return success;
+ }
+
+ /**
+ * Reads the sparkleshare id, if present, from the repository.
+ *
+ * @param repository
+ * @return an id or null
+ */
+ public static String getSparkleshareId(Repository repository) {
+ byte[] content = getByteContent(repository, null, ".sparkleshare", false);
+ if (content == null) {
+ return null;
+ }
+ return StringUtils.decodeString(content);
+ }
+}
diff --git a/src/main/java/com/gitblit/utils/JsonUtils.java b/src/main/java/com/gitblit/utils/JsonUtils.java
new file mode 100644
index 00000000..24f4ecb8
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/JsonUtils.java
@@ -0,0 +1,346 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.utils;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.lang.reflect.Type;
+import java.net.HttpURLConnection;
+import java.net.URLConnection;
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Collection;
+import java.util.Date;
+import java.util.Locale;
+import java.util.Map;
+import java.util.TimeZone;
+
+import com.gitblit.Constants.AccessPermission;
+import com.gitblit.GitBlitException.ForbiddenException;
+import com.gitblit.GitBlitException.NotAllowedException;
+import com.gitblit.GitBlitException.UnauthorizedException;
+import com.gitblit.GitBlitException.UnknownRequestException;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.UserModel;
+import com.google.gson.ExclusionStrategy;
+import com.google.gson.FieldAttributes;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import com.google.gson.JsonSyntaxException;
+import com.google.gson.reflect.TypeToken;
+
+/**
+ * Utility methods for json calls to a Gitblit server.
+ *
+ * @author James Moger
+ *
+ */
+public class JsonUtils {
+
+ public static final Type REPOSITORIES_TYPE = new TypeToken<Map<String, RepositoryModel>>() {
+ }.getType();
+
+ public static final Type USERS_TYPE = new TypeToken<Collection<UserModel>>() {
+ }.getType();
+
+ /**
+ * Creates JSON from the specified object.
+ *
+ * @param o
+ * @return json
+ */
+ public static String toJsonString(Object o) {
+ String json = gson().toJson(o);
+ return json;
+ }
+
+ /**
+ * Convert a json string to an object of the specified type.
+ *
+ * @param json
+ * @param clazz
+ * @return an object
+ */
+ public static <X> X fromJsonString(String json, Class<X> clazz) {
+ return gson().fromJson(json, clazz);
+ }
+
+ /**
+ * Convert a json string to an object of the specified type.
+ *
+ * @param json
+ * @param clazz
+ * @return an object
+ */
+ public static <X> X fromJsonString(String json, Type type) {
+ return gson().fromJson(json, type);
+ }
+
+ /**
+ * Reads a gson object from the specified url.
+ *
+ * @param url
+ * @param type
+ * @return the deserialized object
+ * @throws {@link IOException}
+ */
+ public static <X> X retrieveJson(String url, Type type) throws IOException,
+ UnauthorizedException {
+ return retrieveJson(url, type, null, null);
+ }
+
+ /**
+ * Reads a gson object from the specified url.
+ *
+ * @param url
+ * @param type
+ * @return the deserialized object
+ * @throws {@link IOException}
+ */
+ public static <X> X retrieveJson(String url, Class<? extends X> clazz) throws IOException,
+ UnauthorizedException {
+ return retrieveJson(url, clazz, null, null);
+ }
+
+ /**
+ * Reads a gson object from the specified url.
+ *
+ * @param url
+ * @param type
+ * @param username
+ * @param password
+ * @return the deserialized object
+ * @throws {@link IOException}
+ */
+ public static <X> X retrieveJson(String url, Type type, String username, char[] password)
+ throws IOException {
+ String json = retrieveJsonString(url, username, password);
+ if (StringUtils.isEmpty(json)) {
+ return null;
+ }
+ return gson().fromJson(json, type);
+ }
+
+ /**
+ * Reads a gson object from the specified url.
+ *
+ * @param url
+ * @param clazz
+ * @param username
+ * @param password
+ * @return the deserialized object
+ * @throws {@link IOException}
+ */
+ public static <X> X retrieveJson(String url, Class<X> clazz, String username, char[] password)
+ throws IOException {
+ String json = retrieveJsonString(url, username, password);
+ if (StringUtils.isEmpty(json)) {
+ return null;
+ }
+ return gson().fromJson(json, clazz);
+ }
+
+ /**
+ * Retrieves a JSON message.
+ *
+ * @param url
+ * @return the JSON message as a string
+ * @throws {@link IOException}
+ */
+ public static String retrieveJsonString(String url, String username, char[] password)
+ throws IOException {
+ try {
+ URLConnection conn = ConnectionUtils.openReadConnection(url, username, password);
+ InputStream is = conn.getInputStream();
+ BufferedReader reader = new BufferedReader(new InputStreamReader(is,
+ ConnectionUtils.CHARSET));
+ StringBuilder json = new StringBuilder();
+ char[] buffer = new char[4096];
+ int len = 0;
+ while ((len = reader.read(buffer)) > -1) {
+ json.append(buffer, 0, len);
+ }
+ is.close();
+ return json.toString();
+ } catch (IOException e) {
+ if (e.getMessage().indexOf("401") > -1) {
+ // unauthorized
+ throw new UnauthorizedException(url);
+ } else if (e.getMessage().indexOf("403") > -1) {
+ // requested url is forbidden by the requesting user
+ throw new ForbiddenException(url);
+ } else if (e.getMessage().indexOf("405") > -1) {
+ // requested url is not allowed by the server
+ throw new NotAllowedException(url);
+ } else if (e.getMessage().indexOf("501") > -1) {
+ // requested url is not recognized by the server
+ throw new UnknownRequestException(url);
+ }
+ throw e;
+ }
+ }
+
+ /**
+ * Sends a JSON message.
+ *
+ * @param url
+ * the url to write to
+ * @param json
+ * the json message to send
+ * @return the http request result code
+ * @throws {@link IOException}
+ */
+ public static int sendJsonString(String url, String json) throws IOException {
+ return sendJsonString(url, json, null, null);
+ }
+
+ /**
+ * Sends a JSON message.
+ *
+ * @param url
+ * the url to write to
+ * @param json
+ * the json message to send
+ * @param username
+ * @param password
+ * @return the http request result code
+ * @throws {@link IOException}
+ */
+ public static int sendJsonString(String url, String json, String username, char[] password)
+ throws IOException {
+ try {
+ byte[] jsonBytes = json.getBytes(ConnectionUtils.CHARSET);
+ URLConnection conn = ConnectionUtils.openConnection(url, username, password);
+ conn.setRequestProperty("Content-Type", "text/plain;charset=" + ConnectionUtils.CHARSET);
+ conn.setRequestProperty("Content-Length", "" + jsonBytes.length);
+
+ // write json body
+ OutputStream os = conn.getOutputStream();
+ os.write(jsonBytes);
+ os.close();
+
+ int status = ((HttpURLConnection) conn).getResponseCode();
+ return status;
+ } catch (IOException e) {
+ if (e.getMessage().indexOf("401") > -1) {
+ // unauthorized
+ throw new UnauthorizedException(url);
+ } else if (e.getMessage().indexOf("403") > -1) {
+ // requested url is forbidden by the requesting user
+ throw new ForbiddenException(url);
+ } else if (e.getMessage().indexOf("405") > -1) {
+ // requested url is not allowed by the server
+ throw new NotAllowedException(url);
+ } else if (e.getMessage().indexOf("501") > -1) {
+ // requested url is not recognized by the server
+ throw new UnknownRequestException(url);
+ }
+ throw e;
+ }
+ }
+
+ // build custom gson instance with GMT date serializer/deserializer
+ // http://code.google.com/p/google-gson/issues/detail?id=281
+ public static Gson gson(ExclusionStrategy... strategies) {
+ GsonBuilder builder = new GsonBuilder();
+ builder.registerTypeAdapter(Date.class, new GmtDateTypeAdapter());
+ builder.registerTypeAdapter(AccessPermission.class, new AccessPermissionTypeAdapter());
+ builder.setPrettyPrinting();
+ if (!ArrayUtils.isEmpty(strategies)) {
+ builder.setExclusionStrategies(strategies);
+ }
+ return builder.create();
+ }
+
+ private static class GmtDateTypeAdapter implements JsonSerializer<Date>, JsonDeserializer<Date> {
+ private final DateFormat dateFormat;
+
+ private GmtDateTypeAdapter() {
+ dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US);
+ dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+ }
+
+ @Override
+ public synchronized JsonElement serialize(Date date, Type type,
+ JsonSerializationContext jsonSerializationContext) {
+ synchronized (dateFormat) {
+ String dateFormatAsString = dateFormat.format(date);
+ return new JsonPrimitive(dateFormatAsString);
+ }
+ }
+
+ @Override
+ public synchronized Date deserialize(JsonElement jsonElement, Type type,
+ JsonDeserializationContext jsonDeserializationContext) {
+ try {
+ synchronized (dateFormat) {
+ Date date = dateFormat.parse(jsonElement.getAsString());
+ return new Date((date.getTime() / 1000) * 1000);
+ }
+ } catch (ParseException e) {
+ throw new JsonSyntaxException(jsonElement.getAsString(), e);
+ }
+ }
+ }
+
+ private static class AccessPermissionTypeAdapter implements JsonSerializer<AccessPermission>, JsonDeserializer<AccessPermission> {
+
+ private AccessPermissionTypeAdapter() {
+ }
+
+ @Override
+ public synchronized JsonElement serialize(AccessPermission permission, Type type,
+ JsonSerializationContext jsonSerializationContext) {
+ return new JsonPrimitive(permission.code);
+ }
+
+ @Override
+ public synchronized AccessPermission deserialize(JsonElement jsonElement, Type type,
+ JsonDeserializationContext jsonDeserializationContext) {
+ return AccessPermission.fromCode(jsonElement.getAsString());
+ }
+ }
+
+ public static class ExcludeField implements ExclusionStrategy {
+
+ private Class<?> c;
+ private String fieldName;
+
+ public ExcludeField(String fqfn) throws SecurityException, NoSuchFieldException,
+ ClassNotFoundException {
+ this.c = Class.forName(fqfn.substring(0, fqfn.lastIndexOf(".")));
+ this.fieldName = fqfn.substring(fqfn.lastIndexOf(".") + 1);
+ }
+
+ public boolean shouldSkipClass(Class<?> arg0) {
+ return false;
+ }
+
+ public boolean shouldSkipField(FieldAttributes f) {
+ return (f.getDeclaringClass() == c && f.getName().equals(fieldName));
+ }
+ }
+}
diff --git a/src/main/java/com/gitblit/utils/MarkdownUtils.java b/src/main/java/com/gitblit/utils/MarkdownUtils.java
new file mode 100644
index 00000000..0b8c9c57
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/MarkdownUtils.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.utils;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+
+import org.slf4j.LoggerFactory;
+import org.tautua.markdownpapers.Markdown;
+import org.tautua.markdownpapers.parser.ParseException;
+
+/**
+ * Utility methods for transforming raw markdown text to html.
+ *
+ * @author James Moger
+ *
+ */
+public class MarkdownUtils {
+
+ /**
+ * Returns the html version of the markdown source text.
+ *
+ * @param markdown
+ * @return html version of markdown text
+ * @throws java.text.ParseException
+ */
+ public static String transformMarkdown(String markdown) throws java.text.ParseException {
+ try {
+ StringReader reader = new StringReader(markdown);
+ String html = transformMarkdown(reader);
+ reader.close();
+ return html;
+ } catch (IllegalArgumentException e) {
+ throw new java.text.ParseException(e.getMessage(), 0);
+ } catch (NullPointerException p) {
+ throw new java.text.ParseException("Markdown string is null!", 0);
+ }
+ }
+
+ /**
+ * Returns the html version of the markdown source reader. The reader is
+ * closed regardless of success or failure.
+ *
+ * @param markdownReader
+ * @return html version of the markdown text
+ * @throws java.text.ParseException
+ */
+ public static String transformMarkdown(Reader markdownReader) throws java.text.ParseException {
+ // Read raw markdown content and transform it to html
+ StringWriter writer = new StringWriter();
+ try {
+ Markdown md = new Markdown();
+ md.transform(markdownReader, writer);
+ return writer.toString().trim();
+ } catch (StringIndexOutOfBoundsException e) {
+ LoggerFactory.getLogger(MarkdownUtils.class).error("MarkdownPapers failed to parse Markdown!", e);
+ throw new java.text.ParseException(e.getMessage(), 0);
+ } catch (ParseException p) {
+ LoggerFactory.getLogger(MarkdownUtils.class).error("MarkdownPapers failed to parse Markdown!", p);
+ throw new java.text.ParseException(p.getMessage(), 0);
+ } catch (Exception e) {
+ LoggerFactory.getLogger(MarkdownUtils.class).error("MarkdownPapers failed to parse Markdown!", e);
+ throw new java.text.ParseException(e.getMessage(), 0);
+ } finally {
+ try {
+ writer.close();
+ } catch (IOException e) {
+ // IGNORE
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/gitblit/utils/MetricUtils.java b/src/main/java/com/gitblit/utils/MetricUtils.java
new file mode 100644
index 00000000..26e4581c
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/MetricUtils.java
@@ -0,0 +1,233 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.utils;
+
+import java.text.DateFormat;
+import java.text.MessageFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.TimeZone;
+
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.models.Metric;
+import com.gitblit.models.RefModel;
+
+/**
+ * Utility class for collecting metrics on a branch, tag, or other ref within
+ * the repository.
+ *
+ * @author James Moger
+ *
+ */
+public class MetricUtils {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(MetricUtils.class);
+
+ /**
+ * Log an error message and exception.
+ *
+ * @param t
+ * @param repository
+ * if repository is not null it MUST be the {0} parameter in the
+ * pattern.
+ * @param pattern
+ * @param objects
+ */
+ private static void error(Throwable t, Repository repository, String pattern, Object... objects) {
+ List<Object> parameters = new ArrayList<Object>();
+ if (objects != null && objects.length > 0) {
+ for (Object o : objects) {
+ parameters.add(o);
+ }
+ }
+ if (repository != null) {
+ parameters.add(0, repository.getDirectory().getAbsolutePath());
+ }
+ LOGGER.error(MessageFormat.format(pattern, parameters.toArray()), t);
+ }
+
+ /**
+ * Returns the list of metrics for the specified commit reference, branch,
+ * or tag within the repository. If includeTotal is true, the total of all
+ * the metrics will be included as the first element in the returned list.
+ *
+ * If the dateformat is unspecified an attempt is made to determine an
+ * appropriate date format by determining the time difference between the
+ * first commit on the branch and the most recent commit. This assumes that
+ * the commits are linear.
+ *
+ * @param repository
+ * @param objectId
+ * if null or empty, HEAD is assumed.
+ * @param includeTotal
+ * @param dateFormat
+ * @param timezone
+ * @return list of metrics
+ */
+ public static List<Metric> getDateMetrics(Repository repository, String objectId,
+ boolean includeTotal, String dateFormat, TimeZone timezone) {
+ Metric total = new Metric("TOTAL");
+ final Map<String, Metric> metricMap = new HashMap<String, Metric>();
+
+ if (JGitUtils.hasCommits(repository)) {
+ final List<RefModel> tags = JGitUtils.getTags(repository, true, -1);
+ final Map<ObjectId, RefModel> tagMap = new HashMap<ObjectId, RefModel>();
+ for (RefModel tag : tags) {
+ tagMap.put(tag.getReferencedObjectId(), tag);
+ }
+ RevWalk revWalk = null;
+ try {
+ // resolve branch
+ ObjectId branchObject;
+ if (StringUtils.isEmpty(objectId)) {
+ branchObject = JGitUtils.getDefaultBranch(repository);
+ } else {
+ branchObject = repository.resolve(objectId);
+ }
+
+ revWalk = new RevWalk(repository);
+ RevCommit lastCommit = revWalk.parseCommit(branchObject);
+ revWalk.markStart(lastCommit);
+
+ DateFormat df;
+ if (StringUtils.isEmpty(dateFormat)) {
+ // dynamically determine date format
+ RevCommit firstCommit = JGitUtils.getFirstCommit(repository,
+ branchObject.getName());
+ int diffDays = (lastCommit.getCommitTime() - firstCommit.getCommitTime())
+ / (60 * 60 * 24);
+ total.duration = diffDays;
+ if (diffDays <= 365) {
+ // Days
+ df = new SimpleDateFormat("yyyy-MM-dd");
+ } else {
+ // Months
+ df = new SimpleDateFormat("yyyy-MM");
+ }
+ } else {
+ // use specified date format
+ df = new SimpleDateFormat(dateFormat);
+ }
+ df.setTimeZone(timezone);
+
+ Iterable<RevCommit> revlog = revWalk;
+ for (RevCommit rev : revlog) {
+ Date d = JGitUtils.getCommitDate(rev);
+ String p = df.format(d);
+ if (!metricMap.containsKey(p)) {
+ metricMap.put(p, new Metric(p));
+ }
+ Metric m = metricMap.get(p);
+ m.count++;
+ total.count++;
+ if (tagMap.containsKey(rev.getId())) {
+ m.tag++;
+ total.tag++;
+ }
+ }
+ } catch (Throwable t) {
+ error(t, repository, "{0} failed to mine log history for date metrics of {1}",
+ objectId);
+ } finally {
+ if (revWalk != null) {
+ revWalk.dispose();
+ }
+ }
+ }
+ List<String> keys = new ArrayList<String>(metricMap.keySet());
+ Collections.sort(keys);
+ List<Metric> metrics = new ArrayList<Metric>();
+ for (String key : keys) {
+ metrics.add(metricMap.get(key));
+ }
+ if (includeTotal) {
+ metrics.add(0, total);
+ }
+ return metrics;
+ }
+
+ /**
+ * Returns a list of author metrics for the specified repository.
+ *
+ * @param repository
+ * @param objectId
+ * if null or empty, HEAD is assumed.
+ * @param byEmailAddress
+ * group metrics by author email address otherwise by author name
+ * @return list of metrics
+ */
+ public static List<Metric> getAuthorMetrics(Repository repository, String objectId,
+ boolean byEmailAddress) {
+ final Map<String, Metric> metricMap = new HashMap<String, Metric>();
+ if (JGitUtils.hasCommits(repository)) {
+ try {
+ RevWalk walk = new RevWalk(repository);
+ // resolve branch
+ ObjectId branchObject;
+ if (StringUtils.isEmpty(objectId)) {
+ branchObject = JGitUtils.getDefaultBranch(repository);
+ } else {
+ branchObject = repository.resolve(objectId);
+ }
+ RevCommit lastCommit = walk.parseCommit(branchObject);
+ walk.markStart(lastCommit);
+
+ Iterable<RevCommit> revlog = walk;
+ for (RevCommit rev : revlog) {
+ String p;
+ if (byEmailAddress) {
+ p = rev.getAuthorIdent().getEmailAddress().toLowerCase();
+ if (StringUtils.isEmpty(p)) {
+ p = rev.getAuthorIdent().getName().toLowerCase();
+ }
+ } else {
+ p = rev.getAuthorIdent().getName().toLowerCase();
+ if (StringUtils.isEmpty(p)) {
+ p = rev.getAuthorIdent().getEmailAddress().toLowerCase();
+ }
+ }
+ p = p.replace('\n',' ').replace('\r', ' ').trim();
+ if (!metricMap.containsKey(p)) {
+ metricMap.put(p, new Metric(p));
+ }
+ Metric m = metricMap.get(p);
+ m.count++;
+ }
+ } catch (Throwable t) {
+ error(t, repository, "{0} failed to mine log history for author metrics of {1}",
+ objectId);
+ }
+ }
+ List<String> keys = new ArrayList<String>(metricMap.keySet());
+ Collections.sort(keys);
+ List<Metric> metrics = new ArrayList<Metric>();
+ for (String key : keys) {
+ metrics.add(metricMap.get(key));
+ }
+ return metrics;
+ }
+}
diff --git a/src/main/java/com/gitblit/utils/ObjectCache.java b/src/main/java/com/gitblit/utils/ObjectCache.java
new file mode 100644
index 00000000..38f2e59a
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/ObjectCache.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.utils;
+
+import java.io.Serializable;
+import java.util.Date;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Reusable coarse date-based object cache. The date precision is in
+ * milliseconds and in fast, concurrent systems this cache is too simplistic.
+ * However, for the cases where its being used in Gitblit this cache technique
+ * is just fine.
+ *
+ * @author James Moger
+ *
+ */
+public class ObjectCache<X> implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ private final Map<String, CachedObject<X>> cache = new ConcurrentHashMap<String, CachedObject<X>>();
+
+ private class CachedObject<Y> {
+
+ public final String name;
+
+ private volatile Date date;
+
+ private volatile Y object;
+
+ CachedObject(String name) {
+ this.name = name;
+ date = new Date(0);
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + ": " + name;
+ }
+ }
+
+ public boolean hasCurrent(String name, Date date) {
+ return cache.containsKey(name) && cache.get(name).date.compareTo(date) == 0;
+ }
+
+ public Date getDate(String name) {
+ return cache.get(name).date;
+ }
+
+ public X getObject(String name) {
+ if (cache.containsKey(name)) {
+ return cache.get(name).object;
+ }
+ return null;
+ }
+
+ public void updateObject(String name, X object) {
+ this.updateObject(name, new Date(), object);
+ }
+
+ public void updateObject(String name, Date date, X object) {
+ CachedObject<X> obj;
+ if (cache.containsKey(name)) {
+ obj = cache.get(name);
+ } else {
+ obj = new CachedObject<X>(name);
+ cache.put(name, obj);
+ }
+ obj.date = date;
+ obj.object = object;
+ }
+
+ public Object remove(String name) {
+ if (cache.containsKey(name)) {
+ return cache.remove(name).object;
+ }
+ return null;
+ }
+
+ public int size() {
+ return cache.size();
+ }
+}
diff --git a/src/main/java/com/gitblit/utils/PatchFormatter.java b/src/main/java/com/gitblit/utils/PatchFormatter.java
new file mode 100644
index 00000000..90b3fb16
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/PatchFormatter.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.utils;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.text.MessageFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.jgit.diff.DiffEntry;
+import org.eclipse.jgit.diff.DiffFormatter;
+import org.eclipse.jgit.diff.RawText;
+import org.eclipse.jgit.revwalk.RevCommit;
+
+import com.gitblit.Constants;
+
+/**
+ * A diff formatter that outputs standard patch content.
+ *
+ * @author James Moger
+ *
+ */
+public class PatchFormatter extends DiffFormatter {
+
+ private final OutputStream os;
+
+ private Map<String, PatchTouple> changes = new HashMap<String, PatchTouple>();
+
+ private PatchTouple currentTouple;
+
+ public PatchFormatter(OutputStream os) {
+ super(os);
+ this.os = os;
+ }
+
+ public void format(DiffEntry entry) throws IOException {
+ currentTouple = new PatchTouple();
+ changes.put(entry.getNewPath(), currentTouple);
+ super.format(entry);
+ }
+
+ @Override
+ protected void writeLine(final char prefix, final RawText text, final int cur)
+ throws IOException {
+ switch (prefix) {
+ case '+':
+ currentTouple.insertions++;
+ break;
+ case '-':
+ currentTouple.deletions++;
+ break;
+ }
+ super.writeLine(prefix, text, cur);
+ }
+
+ public String getPatch(RevCommit commit) {
+ StringBuilder patch = new StringBuilder();
+ // hard-code the mon sep 17 2001 date string.
+ // I have no idea why that is there. it seems to be a constant.
+ patch.append("From " + commit.getName() + " Mon Sep 17 00:00:00 2001" + "\n");
+ patch.append("From: " + JGitUtils.getDisplayName(commit.getAuthorIdent()) + "\n");
+ patch.append("Date: "
+ + (new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z").format(new Date(commit
+ .getCommitTime() * 1000L))) + "\n");
+ patch.append("Subject: [PATCH] " + commit.getShortMessage() + "\n");
+ patch.append('\n');
+ patch.append("---");
+ int maxPathLen = 0;
+ int files = 0;
+ int insertions = 0;
+ int deletions = 0;
+ for (String path : changes.keySet()) {
+ if (path.length() > maxPathLen) {
+ maxPathLen = path.length();
+ }
+ PatchTouple touple = changes.get(path);
+ files++;
+ insertions += touple.insertions;
+ deletions += touple.deletions;
+ }
+ int columns = 60;
+ int total = insertions + deletions;
+ int unit = total / columns + (total % columns > 0 ? 1 : 0);
+ if (unit == 0) {
+ unit = 1;
+ }
+ for (String path : changes.keySet()) {
+ PatchTouple touple = changes.get(path);
+ patch.append("\n " + StringUtils.rightPad(path, maxPathLen, ' ') + " | "
+ + StringUtils.leftPad("" + touple.total(), 4, ' ') + " "
+ + touple.relativeScale(unit));
+ }
+ patch.append(MessageFormat.format(
+ "\n {0} files changed, {1} insertions(+), {2} deletions(-)\n\n", files, insertions,
+ deletions));
+ patch.append(os.toString());
+ patch.append("\n--\n");
+ patch.append(Constants.getGitBlitVersion());
+ return patch.toString();
+ }
+
+ /**
+ * Class that represents the number of insertions and deletions from a
+ * chunk.
+ */
+ private static class PatchTouple {
+ int insertions;
+ int deletions;
+
+ int total() {
+ return insertions + deletions;
+ }
+
+ String relativeScale(int unit) {
+ int plus = insertions / unit;
+ int minus = deletions / unit;
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < plus; i++) {
+ sb.append('+');
+ }
+ for (int i = 0; i < minus; i++) {
+ sb.append('-');
+ }
+ return sb.toString();
+ }
+ }
+}
diff --git a/src/main/java/com/gitblit/utils/PushLogUtils.java b/src/main/java/com/gitblit/utils/PushLogUtils.java
new file mode 100644
index 00000000..665533b3
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/PushLogUtils.java
@@ -0,0 +1,344 @@
+/*
+ * Copyright 2013 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.utils;
+
+import java.io.IOException;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
+
+import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
+import org.eclipse.jgit.api.errors.JGitInternalException;
+import org.eclipse.jgit.dircache.DirCache;
+import org.eclipse.jgit.dircache.DirCacheBuilder;
+import org.eclipse.jgit.dircache.DirCacheEntry;
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.RefUpdate.Result;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
+import org.eclipse.jgit.treewalk.CanonicalTreeParser;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.models.PathModel.PathChangeModel;
+import com.gitblit.models.PushLogEntry;
+import com.gitblit.models.RefModel;
+import com.gitblit.models.UserModel;
+
+/**
+ * Utility class for maintaining a pushlog within a git repository on an
+ * orphan branch.
+ *
+ * @author James Moger
+ *
+ */
+public class PushLogUtils {
+
+ public static final String GB_PUSHES = "refs/gitblit/pushes";
+
+ static final Logger LOGGER = LoggerFactory.getLogger(PushLogUtils.class);
+
+ /**
+ * Log an error message and exception.
+ *
+ * @param t
+ * @param repository
+ * if repository is not null it MUST be the {0} parameter in the
+ * pattern.
+ * @param pattern
+ * @param objects
+ */
+ private static void error(Throwable t, Repository repository, String pattern, Object... objects) {
+ List<Object> parameters = new ArrayList<Object>();
+ if (objects != null && objects.length > 0) {
+ for (Object o : objects) {
+ parameters.add(o);
+ }
+ }
+ if (repository != null) {
+ parameters.add(0, repository.getDirectory().getAbsolutePath());
+ }
+ LOGGER.error(MessageFormat.format(pattern, parameters.toArray()), t);
+ }
+
+ /**
+ * Returns a RefModel for the gb-pushes branch in the repository. If the
+ * branch can not be found, null is returned.
+ *
+ * @param repository
+ * @return a refmodel for the gb-pushes branch or null
+ */
+ public static RefModel getPushLogBranch(Repository repository) {
+ List<RefModel> refs = JGitUtils.getRefs(repository, com.gitblit.Constants.R_GITBLIT);
+ for (RefModel ref : refs) {
+ if (ref.reference.getName().equals(GB_PUSHES)) {
+ return ref;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Updates a push log.
+ *
+ * @param user
+ * @param repository
+ * @param commands
+ * @return true, if the update was successful
+ */
+ public static boolean updatePushLog(UserModel user, Repository repository,
+ Collection<ReceiveCommand> commands) {
+ RefModel pushlogBranch = getPushLogBranch(repository);
+ if (pushlogBranch == null) {
+ JGitUtils.createOrphanBranch(repository, GB_PUSHES, null);
+ }
+
+ boolean success = false;
+ String message = "push";
+
+ try {
+ ObjectId headId = repository.resolve(GB_PUSHES + "^{commit}");
+ ObjectInserter odi = repository.newObjectInserter();
+ try {
+ // Create the in-memory index of the push log entry
+ DirCache index = createIndex(repository, headId, commands);
+ ObjectId indexTreeId = index.writeTree(odi);
+
+ PersonIdent ident = new PersonIdent(user.getDisplayName(),
+ user.emailAddress == null ? user.username:user.emailAddress);
+
+ // Create a commit object
+ CommitBuilder commit = new CommitBuilder();
+ commit.setAuthor(ident);
+ commit.setCommitter(ident);
+ commit.setEncoding(Constants.CHARACTER_ENCODING);
+ commit.setMessage(message);
+ commit.setParentId(headId);
+ commit.setTreeId(indexTreeId);
+
+ // Insert the commit into the repository
+ ObjectId commitId = odi.insert(commit);
+ odi.flush();
+
+ RevWalk revWalk = new RevWalk(repository);
+ try {
+ RevCommit revCommit = revWalk.parseCommit(commitId);
+ RefUpdate ru = repository.updateRef(GB_PUSHES);
+ ru.setNewObjectId(commitId);
+ ru.setExpectedOldObjectId(headId);
+ ru.setRefLogMessage("commit: " + revCommit.getShortMessage(), false);
+ Result rc = ru.forceUpdate();
+ switch (rc) {
+ case NEW:
+ case FORCED:
+ case FAST_FORWARD:
+ success = true;
+ break;
+ case REJECTED:
+ case LOCK_FAILURE:
+ throw new ConcurrentRefUpdateException(JGitText.get().couldNotLockHEAD,
+ ru.getRef(), rc);
+ default:
+ throw new JGitInternalException(MessageFormat.format(
+ JGitText.get().updatingRefFailed, GB_PUSHES, commitId.toString(),
+ rc));
+ }
+ } finally {
+ revWalk.release();
+ }
+ } finally {
+ odi.release();
+ }
+ } catch (Throwable t) {
+ error(t, repository, "Failed to commit pushlog entry to {0}");
+ }
+ return success;
+ }
+
+ /**
+ * Creates an in-memory index of the push log entry.
+ *
+ * @param repo
+ * @param headId
+ * @param commands
+ * @return an in-memory index
+ * @throws IOException
+ */
+ private static DirCache createIndex(Repository repo, ObjectId headId,
+ Collection<ReceiveCommand> commands) throws IOException {
+
+ DirCache inCoreIndex = DirCache.newInCore();
+ DirCacheBuilder dcBuilder = inCoreIndex.builder();
+ ObjectInserter inserter = repo.newObjectInserter();
+
+ long now = System.currentTimeMillis();
+ Set<String> ignorePaths = new TreeSet<String>();
+ try {
+ // add receive commands to the temporary index
+ for (ReceiveCommand command : commands) {
+ // use the ref names as the path names
+ String path = command.getRefName();
+ ignorePaths.add(path);
+
+ StringBuilder change = new StringBuilder();
+ change.append(command.getType().name()).append(' ');
+ switch (command.getType()) {
+ case CREATE:
+ change.append(ObjectId.zeroId().getName());
+ change.append(' ');
+ change.append(command.getNewId().getName());
+ break;
+ case UPDATE:
+ case UPDATE_NONFASTFORWARD:
+ change.append(command.getOldId().getName());
+ change.append(' ');
+ change.append(command.getNewId().getName());
+ break;
+ case DELETE:
+ change = null;
+ break;
+ }
+ if (change == null) {
+ // ref deleted
+ continue;
+ }
+ String content = change.toString();
+
+ // create an index entry for this attachment
+ final DirCacheEntry dcEntry = new DirCacheEntry(path);
+ dcEntry.setLength(content.length());
+ dcEntry.setLastModified(now);
+ dcEntry.setFileMode(FileMode.REGULAR_FILE);
+
+ // insert object
+ dcEntry.setObjectId(inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8")));
+
+ // add to temporary in-core index
+ dcBuilder.add(dcEntry);
+ }
+
+ // Traverse HEAD to add all other paths
+ TreeWalk treeWalk = new TreeWalk(repo);
+ int hIdx = -1;
+ if (headId != null)
+ hIdx = treeWalk.addTree(new RevWalk(repo).parseTree(headId));
+ treeWalk.setRecursive(true);
+
+ while (treeWalk.next()) {
+ String path = treeWalk.getPathString();
+ CanonicalTreeParser hTree = null;
+ if (hIdx != -1)
+ hTree = treeWalk.getTree(hIdx, CanonicalTreeParser.class);
+ if (!ignorePaths.contains(path)) {
+ // add entries from HEAD for all other paths
+ if (hTree != null) {
+ // create a new DirCacheEntry with data retrieved from
+ // HEAD
+ final DirCacheEntry dcEntry = new DirCacheEntry(path);
+ dcEntry.setObjectId(hTree.getEntryObjectId());
+ dcEntry.setFileMode(hTree.getEntryFileMode());
+
+ // add to temporary in-core index
+ dcBuilder.add(dcEntry);
+ }
+ }
+ }
+
+ // release the treewalk
+ treeWalk.release();
+
+ // finish temporary in-core index used for this commit
+ dcBuilder.finish();
+ } finally {
+ inserter.release();
+ }
+ return inCoreIndex;
+ }
+
+ public static List<PushLogEntry> getPushLog(String repositoryName, Repository repository) {
+ return getPushLog(repositoryName, repository, null, -1);
+ }
+
+ public static List<PushLogEntry> getPushLog(String repositoryName, Repository repository, int maxCount) {
+ return getPushLog(repositoryName, repository, null, maxCount);
+ }
+
+ public static List<PushLogEntry> getPushLog(String repositoryName, Repository repository, Date minimumDate) {
+ return getPushLog(repositoryName, repository, minimumDate, -1);
+ }
+
+ public static List<PushLogEntry> getPushLog(String repositoryName, Repository repository, Date minimumDate, int maxCount) {
+ List<PushLogEntry> list = new ArrayList<PushLogEntry>();
+ RefModel ref = getPushLogBranch(repository);
+ if (ref == null) {
+ return list;
+ }
+ List<RevCommit> pushes;
+ if (minimumDate == null) {
+ pushes = JGitUtils.getRevLog(repository, GB_PUSHES, 0, maxCount);
+ } else {
+ pushes = JGitUtils.getRevLog(repository, GB_PUSHES, minimumDate);
+ }
+ for (RevCommit push : pushes) {
+ if (push.getAuthorIdent().getName().equalsIgnoreCase("gitblit")) {
+ // skip gitblit/internal commits
+ continue;
+ }
+ Date date = push.getAuthorIdent().getWhen();
+ UserModel user = new UserModel(push.getAuthorIdent().getEmailAddress());
+ user.displayName = push.getAuthorIdent().getName();
+ PushLogEntry log = new PushLogEntry(repositoryName, date, user);
+ list.add(log);
+ List<PathChangeModel> changedRefs = JGitUtils.getFilesInCommit(repository, push);
+ for (PathChangeModel change : changedRefs) {
+ switch (change.changeType) {
+ case DELETE:
+ log.updateRef(change.path, ReceiveCommand.Type.DELETE);
+ break;
+ case ADD:
+ log.updateRef(change.path, ReceiveCommand.Type.CREATE);
+ default:
+ String content = JGitUtils.getStringContent(repository, push.getTree(), change.path);
+ String [] fields = content.split(" ");
+ log.updateRef(change.path, ReceiveCommand.Type.valueOf(fields[0]));
+ String oldId = fields[1];
+ String newId = fields[2];
+ List<RevCommit> pushedCommits = JGitUtils.getRevLog(repository, oldId, newId);
+ for (RevCommit pushedCommit : pushedCommits) {
+ log.addCommit(change.path, pushedCommit);
+ }
+ }
+ }
+ }
+ Collections.sort(list);
+ return list;
+ }
+}
diff --git a/src/main/java/com/gitblit/utils/RpcUtils.java b/src/main/java/com/gitblit/utils/RpcUtils.java
new file mode 100644
index 00000000..ed23dab6
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/RpcUtils.java
@@ -0,0 +1,637 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.utils;
+
+import java.io.IOException;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import com.gitblit.Constants;
+import com.gitblit.Constants.RpcRequest;
+import com.gitblit.GitBlitException.UnknownRequestException;
+import com.gitblit.models.RegistrantAccessPermission;
+import com.gitblit.models.FederationModel;
+import com.gitblit.models.FederationProposal;
+import com.gitblit.models.FederationSet;
+import com.gitblit.models.FeedModel;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.ServerSettings;
+import com.gitblit.models.ServerStatus;
+import com.gitblit.models.TeamModel;
+import com.gitblit.models.UserModel;
+import com.google.gson.reflect.TypeToken;
+
+/**
+ * Utility methods for rpc calls.
+ *
+ * @author James Moger
+ *
+ */
+public class RpcUtils {
+
+ public static final Type NAMES_TYPE = new TypeToken<Collection<String>>() {
+ }.getType();
+
+ public static final Type SETTINGS_TYPE = new TypeToken<Map<String, String>>() {
+ }.getType();
+
+ private static final Type REPOSITORIES_TYPE = new TypeToken<Map<String, RepositoryModel>>() {
+ }.getType();
+
+ private static final Type USERS_TYPE = new TypeToken<Collection<UserModel>>() {
+ }.getType();
+
+ private static final Type TEAMS_TYPE = new TypeToken<Collection<TeamModel>>() {
+ }.getType();
+
+ private static final Type REGISTRATIONS_TYPE = new TypeToken<Collection<FederationModel>>() {
+ }.getType();
+
+ private static final Type PROPOSALS_TYPE = new TypeToken<Collection<FederationProposal>>() {
+ }.getType();
+
+ private static final Type SETS_TYPE = new TypeToken<Collection<FederationSet>>() {
+ }.getType();
+
+ private static final Type BRANCHES_TYPE = new TypeToken<Map<String, Collection<String>>>() {
+ }.getType();
+
+ public static final Type REGISTRANT_PERMISSIONS_TYPE = new TypeToken<Collection<RegistrantAccessPermission>>() {
+ }.getType();
+
+ /**
+ *
+ * @param remoteURL
+ * the url of the remote gitblit instance
+ * @param req
+ * the rpc request type
+ * @return
+ */
+ public static String asLink(String remoteURL, RpcRequest req) {
+ return asLink(remoteURL, req, null);
+ }
+
+ /**
+ *
+ * @param remoteURL
+ * the url of the remote gitblit instance
+ * @param req
+ * the rpc request type
+ * @param name
+ * the name of the actionable object
+ * @return
+ */
+ public static String asLink(String remoteURL, RpcRequest req, String name) {
+ if (remoteURL.length() > 0 && remoteURL.charAt(remoteURL.length() - 1) == '/') {
+ remoteURL = remoteURL.substring(0, remoteURL.length() - 1);
+ }
+ if (req == null) {
+ req = RpcRequest.LIST_REPOSITORIES;
+ }
+ return remoteURL + Constants.RPC_PATH + "?req=" + req.name().toLowerCase()
+ + (name == null ? "" : ("&name=" + StringUtils.encodeURL(name)));
+ }
+
+ /**
+ * Returns the version of the RPC protocol on the server.
+ *
+ * @param serverUrl
+ * @param account
+ * @param password
+ * @return the protocol version
+ * @throws IOException
+ */
+ public static int getProtocolVersion(String serverUrl, String account, char[] password)
+ throws IOException {
+ String url = asLink(serverUrl, RpcRequest.GET_PROTOCOL);
+ int protocol = 1;
+ try {
+ protocol = JsonUtils.retrieveJson(url, Integer.class, account, password);
+ } catch (UnknownRequestException e) {
+ // v0.7.0 (protocol 1) did not have this request type
+ }
+ return protocol;
+ }
+
+ /**
+ * Retrieves a map of the repositories at the remote gitblit instance keyed
+ * by the repository clone url.
+ *
+ * @param serverUrl
+ * @param account
+ * @param password
+ * @return a map of cloneable repositories
+ * @throws IOException
+ */
+ public static Map<String, RepositoryModel> getRepositories(String serverUrl, String account,
+ char[] password) throws IOException {
+ String url = asLink(serverUrl, RpcRequest.LIST_REPOSITORIES);
+ Map<String, RepositoryModel> models = JsonUtils.retrieveJson(url, REPOSITORIES_TYPE,
+ account, password);
+ return models;
+ }
+
+ /**
+ * Tries to pull the gitblit user accounts from the remote gitblit instance.
+ *
+ * @param serverUrl
+ * @param account
+ * @param password
+ * @return a collection of UserModel objects
+ * @throws IOException
+ */
+ public static List<UserModel> getUsers(String serverUrl, String account, char[] password)
+ throws IOException {
+ String url = asLink(serverUrl, RpcRequest.LIST_USERS);
+ Collection<UserModel> models = JsonUtils.retrieveJson(url, USERS_TYPE, account, password);
+ List<UserModel> list = new ArrayList<UserModel>(models);
+ return list;
+ }
+
+ /**
+ * Tries to pull the gitblit team definitions from the remote gitblit
+ * instance.
+ *
+ * @param serverUrl
+ * @param account
+ * @param password
+ * @return a collection of UserModel objects
+ * @throws IOException
+ */
+ public static List<TeamModel> getTeams(String serverUrl, String account, char[] password)
+ throws IOException {
+ String url = asLink(serverUrl, RpcRequest.LIST_TEAMS);
+ Collection<TeamModel> models = JsonUtils.retrieveJson(url, TEAMS_TYPE, account, password);
+ List<TeamModel> list = new ArrayList<TeamModel>(models);
+ return list;
+ }
+
+ /**
+ * Create a repository on the Gitblit server.
+ *
+ * @param repository
+ * @param serverUrl
+ * @param account
+ * @param password
+ * @return true if the action succeeded
+ * @throws IOException
+ */
+ public static boolean createRepository(RepositoryModel repository, String serverUrl,
+ String account, char[] password) throws IOException {
+ // ensure repository name ends with .git
+ if (!repository.name.endsWith(".git")) {
+ repository.name += ".git";
+ }
+ return doAction(RpcRequest.CREATE_REPOSITORY, null, repository, serverUrl, account,
+ password);
+
+ }
+
+ /**
+ * Send a revised version of the repository model to the Gitblit server.
+ *
+ * @param repository
+ * @param serverUrl
+ * @param account
+ * @param password
+ * @return true if the action succeeded
+ * @throws IOException
+ */
+ public static boolean updateRepository(String repositoryName, RepositoryModel repository,
+ String serverUrl, String account, char[] password) throws IOException {
+ return doAction(RpcRequest.EDIT_REPOSITORY, repositoryName, repository, serverUrl, account,
+ password);
+ }
+
+ /**
+ * Delete a repository from the Gitblit server.
+ *
+ * @param repository
+ * @param serverUrl
+ * @param account
+ * @param password
+ * @return true if the action succeeded
+ * @throws IOException
+ */
+ public static boolean deleteRepository(RepositoryModel repository, String serverUrl,
+ String account, char[] password) throws IOException {
+ return doAction(RpcRequest.DELETE_REPOSITORY, null, repository, serverUrl, account,
+ password);
+
+ }
+
+ /**
+ * Clears the repository cache on the Gitblit server.
+ *
+ * @param serverUrl
+ * @param account
+ * @param password
+ * @return true if the action succeeded
+ * @throws IOException
+ */
+ public static boolean clearRepositoryCache(String serverUrl, String account,
+ char[] password) throws IOException {
+ return doAction(RpcRequest.CLEAR_REPOSITORY_CACHE, null, null, serverUrl, account,
+ password);
+ }
+
+ /**
+ * Create a user on the Gitblit server.
+ *
+ * @param user
+ * @param serverUrl
+ * @param account
+ * @param password
+ * @return true if the action succeeded
+ * @throws IOException
+ */
+ public static boolean createUser(UserModel user, String serverUrl, String account,
+ char[] password) throws IOException {
+ return doAction(RpcRequest.CREATE_USER, null, user, serverUrl, account, password);
+
+ }
+
+ /**
+ * Send a revised version of the user model to the Gitblit server.
+ *
+ * @param user
+ * @param serverUrl
+ * @param account
+ * @param password
+ * @return true if the action succeeded
+ * @throws IOException
+ */
+ public static boolean updateUser(String username, UserModel user, String serverUrl,
+ String account, char[] password) throws IOException {
+ return doAction(RpcRequest.EDIT_USER, username, user, serverUrl, account, password);
+
+ }
+
+ /**
+ * Deletes a user from the Gitblit server.
+ *
+ * @param user
+ * @param serverUrl
+ * @param account
+ * @param password
+ * @return true if the action succeeded
+ * @throws IOException
+ */
+ public static boolean deleteUser(UserModel user, String serverUrl, String account,
+ char[] password) throws IOException {
+ return doAction(RpcRequest.DELETE_USER, null, user, serverUrl, account, password);
+ }
+
+ /**
+ * Create a team on the Gitblit server.
+ *
+ * @param team
+ * @param serverUrl
+ * @param account
+ * @param password
+ * @return true if the action succeeded
+ * @throws IOException
+ */
+ public static boolean createTeam(TeamModel team, String serverUrl, String account,
+ char[] password) throws IOException {
+ return doAction(RpcRequest.CREATE_TEAM, null, team, serverUrl, account, password);
+
+ }
+
+ /**
+ * Send a revised version of the team model to the Gitblit server.
+ *
+ * @param team
+ * @param serverUrl
+ * @param account
+ * @param password
+ * @return true if the action succeeded
+ * @throws IOException
+ */
+ public static boolean updateTeam(String teamname, TeamModel team, String serverUrl,
+ String account, char[] password) throws IOException {
+ return doAction(RpcRequest.EDIT_TEAM, teamname, team, serverUrl, account, password);
+
+ }
+
+ /**
+ * Deletes a team from the Gitblit server.
+ *
+ * @param team
+ * @param serverUrl
+ * @param account
+ * @param password
+ * @return true if the action succeeded
+ * @throws IOException
+ */
+ public static boolean deleteTeam(TeamModel team, String serverUrl, String account,
+ char[] password) throws IOException {
+ return doAction(RpcRequest.DELETE_TEAM, null, team, serverUrl, account, password);
+ }
+
+ /**
+ * Retrieves the list of users that can access the specified repository.
+ *
+ * @param repository
+ * @param serverUrl
+ * @param account
+ * @param password
+ * @return list of members
+ * @throws IOException
+ */
+ public static List<String> getRepositoryMembers(RepositoryModel repository, String serverUrl,
+ String account, char[] password) throws IOException {
+ String url = asLink(serverUrl, RpcRequest.LIST_REPOSITORY_MEMBERS, repository.name);
+ Collection<String> list = JsonUtils.retrieveJson(url, NAMES_TYPE, account, password);
+ return new ArrayList<String>(list);
+ }
+
+ /**
+ * Retrieves the list of user access permissions for the specified repository.
+ *
+ * @param repository
+ * @param serverUrl
+ * @param account
+ * @param password
+ * @return list of User-AccessPermission tuples
+ * @throws IOException
+ */
+ public static List<RegistrantAccessPermission> getRepositoryMemberPermissions(RepositoryModel repository,
+ String serverUrl, String account, char [] password) throws IOException {
+ String url = asLink(serverUrl, RpcRequest.LIST_REPOSITORY_MEMBER_PERMISSIONS, repository.name);
+ Collection<RegistrantAccessPermission> list = JsonUtils.retrieveJson(url, REGISTRANT_PERMISSIONS_TYPE, account, password);
+ return new ArrayList<RegistrantAccessPermission>(list);
+ }
+
+ /**
+ * Sets the repository user access permissions
+ *
+ * @param repository
+ * @param permissions
+ * @param serverUrl
+ * @param account
+ * @param password
+ * @return true if the action succeeded
+ * @throws IOException
+ */
+ public static boolean setRepositoryMemberPermissions(RepositoryModel repository,
+ List<RegistrantAccessPermission> permissions, String serverUrl, String account, char[] password)
+ throws IOException {
+ return doAction(RpcRequest.SET_REPOSITORY_MEMBER_PERMISSIONS, repository.name, permissions, serverUrl,
+ account, password);
+ }
+
+ /**
+ * Retrieves the list of teams that can access the specified repository.
+ *
+ * @param repository
+ * @param serverUrl
+ * @param account
+ * @param password
+ * @return list of teams
+ * @throws IOException
+ */
+ public static List<String> getRepositoryTeams(RepositoryModel repository, String serverUrl,
+ String account, char[] password) throws IOException {
+ String url = asLink(serverUrl, RpcRequest.LIST_REPOSITORY_TEAMS, repository.name);
+ Collection<String> list = JsonUtils.retrieveJson(url, NAMES_TYPE, account, password);
+ return new ArrayList<String>(list);
+ }
+
+ /**
+ * Retrieves the list of team access permissions for the specified repository.
+ *
+ * @param repository
+ * @param serverUrl
+ * @param account
+ * @param password
+ * @return list of Team-AccessPermission tuples
+ * @throws IOException
+ */
+ public static List<RegistrantAccessPermission> getRepositoryTeamPermissions(RepositoryModel repository,
+ String serverUrl, String account, char [] password) throws IOException {
+ String url = asLink(serverUrl, RpcRequest.LIST_REPOSITORY_TEAM_PERMISSIONS, repository.name);
+ Collection<RegistrantAccessPermission> list = JsonUtils.retrieveJson(url, REGISTRANT_PERMISSIONS_TYPE, account, password);
+ return new ArrayList<RegistrantAccessPermission>(list);
+ }
+
+ /**
+ * Sets the repository team access permissions
+ *
+ * @param repository
+ * @param permissions
+ * @param serverUrl
+ * @param account
+ * @param password
+ * @return true if the action succeeded
+ * @throws IOException
+ */
+ public static boolean setRepositoryTeamPermissions(RepositoryModel repository,
+ List<RegistrantAccessPermission> permissions, String serverUrl, String account, char[] password)
+ throws IOException {
+ return doAction(RpcRequest.SET_REPOSITORY_TEAM_PERMISSIONS, repository.name, permissions, serverUrl,
+ account, password);
+ }
+
+ /**
+ * Retrieves the list of federation registrations. These are the list of
+ * registrations that this Gitblit instance is pulling from.
+ *
+ * @param serverUrl
+ * @param account
+ * @param password
+ * @return a collection of FederationRegistration objects
+ * @throws IOException
+ */
+ public static List<FederationModel> getFederationRegistrations(String serverUrl,
+ String account, char[] password) throws IOException {
+ String url = asLink(serverUrl, RpcRequest.LIST_FEDERATION_REGISTRATIONS);
+ Collection<FederationModel> registrations = JsonUtils.retrieveJson(url, REGISTRATIONS_TYPE,
+ account, password);
+ List<FederationModel> list = new ArrayList<FederationModel>(registrations);
+ return list;
+ }
+
+ /**
+ * Retrieves the list of federation result registrations. These are the
+ * results reported back to this Gitblit instance from a federation client.
+ *
+ * @param serverUrl
+ * @param account
+ * @param password
+ * @return a collection of FederationRegistration objects
+ * @throws IOException
+ */
+ public static List<FederationModel> getFederationResultRegistrations(String serverUrl,
+ String account, char[] password) throws IOException {
+ String url = asLink(serverUrl, RpcRequest.LIST_FEDERATION_RESULTS);
+ Collection<FederationModel> registrations = JsonUtils.retrieveJson(url, REGISTRATIONS_TYPE,
+ account, password);
+ List<FederationModel> list = new ArrayList<FederationModel>(registrations);
+ return list;
+ }
+
+ /**
+ * Retrieves the list of federation proposals.
+ *
+ * @param serverUrl
+ * @param account
+ * @param password
+ * @return a collection of FederationProposal objects
+ * @throws IOException
+ */
+ public static List<FederationProposal> getFederationProposals(String serverUrl, String account,
+ char[] password) throws IOException {
+ String url = asLink(serverUrl, RpcRequest.LIST_FEDERATION_PROPOSALS);
+ Collection<FederationProposal> proposals = JsonUtils.retrieveJson(url, PROPOSALS_TYPE,
+ account, password);
+ List<FederationProposal> list = new ArrayList<FederationProposal>(proposals);
+ return list;
+ }
+
+ /**
+ * Retrieves the list of federation repository sets.
+ *
+ * @param serverUrl
+ * @param account
+ * @param password
+ * @return a collection of FederationSet objects
+ * @throws IOException
+ */
+ public static List<FederationSet> getFederationSets(String serverUrl, String account,
+ char[] password) throws IOException {
+ String url = asLink(serverUrl, RpcRequest.LIST_FEDERATION_SETS);
+ Collection<FederationSet> sets = JsonUtils.retrieveJson(url, SETS_TYPE, account, password);
+ List<FederationSet> list = new ArrayList<FederationSet>(sets);
+ return list;
+ }
+
+ /**
+ * Retrieves the settings of the Gitblit server.
+ *
+ * @param serverUrl
+ * @param account
+ * @param password
+ * @return an Settings object
+ * @throws IOException
+ */
+ public static ServerSettings getSettings(String serverUrl, String account, char[] password)
+ throws IOException {
+ String url = asLink(serverUrl, RpcRequest.LIST_SETTINGS);
+ ServerSettings settings = JsonUtils.retrieveJson(url, ServerSettings.class, account,
+ password);
+ return settings;
+ }
+
+ /**
+ * Update the settings on the Gitblit server.
+ *
+ * @param settings
+ * the settings to update
+ * @param serverUrl
+ * @param account
+ * @param password
+ * @return true if the action succeeded
+ * @throws IOException
+ */
+ public static boolean updateSettings(Map<String, String> settings, String serverUrl,
+ String account, char[] password) throws IOException {
+ return doAction(RpcRequest.EDIT_SETTINGS, null, settings, serverUrl, account, password);
+
+ }
+
+ /**
+ * Retrieves the server status object.
+ *
+ * @param serverUrl
+ * @param account
+ * @param password
+ * @return an ServerStatus object
+ * @throws IOException
+ */
+ public static ServerStatus getStatus(String serverUrl, String account, char[] password)
+ throws IOException {
+ String url = asLink(serverUrl, RpcRequest.LIST_STATUS);
+ ServerStatus status = JsonUtils.retrieveJson(url, ServerStatus.class, account, password);
+ return status;
+ }
+
+ /**
+ * Retrieves a map of local branches in the Gitblit server keyed by
+ * repository.
+ *
+ * @param serverUrl
+ * @param account
+ * @param password
+ * @return
+ * @throws IOException
+ */
+ public static Map<String, Collection<String>> getBranches(String serverUrl, String account,
+ char[] password) throws IOException {
+ String url = asLink(serverUrl, RpcRequest.LIST_BRANCHES);
+ Map<String, Collection<String>> branches = JsonUtils.retrieveJson(url, BRANCHES_TYPE,
+ account, password);
+ return branches;
+ }
+
+ /**
+ * Retrieves a list of available branch feeds in the Gitblit server.
+ *
+ * @param serverUrl
+ * @param account
+ * @param password
+ * @return
+ * @throws IOException
+ */
+ public static List<FeedModel> getBranchFeeds(String serverUrl, String account, char[] password)
+ throws IOException {
+ List<FeedModel> feeds = new ArrayList<FeedModel>();
+ Map<String, Collection<String>> allBranches = getBranches(serverUrl, account, password);
+ for (Map.Entry<String, Collection<String>> entry : allBranches.entrySet()) {
+ for (String branch : entry.getValue()) {
+ FeedModel feed = new FeedModel();
+ feed.repository = entry.getKey();
+ feed.branch = branch;
+ feeds.add(feed);
+ }
+ }
+ return feeds;
+ }
+
+ /**
+ * Do the specified administrative action on the Gitblit server.
+ *
+ * @param request
+ * @param name
+ * the name of the object (may be null)
+ * @param object
+ * @param serverUrl
+ * @param account
+ * @param password
+ * @return true if the action succeeded
+ * @throws IOException
+ */
+ protected static boolean doAction(RpcRequest request, String name, Object object,
+ String serverUrl, String account, char[] password) throws IOException {
+ String url = asLink(serverUrl, request, name);
+ String json = JsonUtils.toJsonString(object);
+ int resultCode = JsonUtils.sendJsonString(url, json, account, password);
+ return resultCode == 200;
+ }
+}
diff --git a/src/main/java/com/gitblit/utils/StringUtils.java b/src/main/java/com/gitblit/utils/StringUtils.java
new file mode 100644
index 00000000..86823db5
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/StringUtils.java
@@ -0,0 +1,736 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.utils;
+
+import java.io.ByteArrayOutputStream;
+import java.io.UnsupportedEncodingException;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.CharacterCodingException;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetDecoder;
+import java.nio.charset.IllegalCharsetNameException;
+import java.nio.charset.UnsupportedCharsetException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+
+/**
+ * Utility class of string functions.
+ *
+ * @author James Moger
+ *
+ */
+public class StringUtils {
+
+ public static final String MD5_TYPE = "MD5:";
+
+ public static final String COMBINED_MD5_TYPE = "CMD5:";
+
+ /**
+ * Returns true if the string is null or empty.
+ *
+ * @param value
+ * @return true if string is null or empty
+ */
+ public static boolean isEmpty(String value) {
+ return value == null || value.trim().length() == 0;
+ }
+
+ /**
+ * Replaces carriage returns and line feeds with html line breaks.
+ *
+ * @param string
+ * @return plain text with html line breaks
+ */
+ public static String breakLinesForHtml(String string) {
+ return string.replace("\r\n", "<br/>").replace("\r", "<br/>").replace("\n", "<br/>");
+ }
+
+ /**
+ * Prepare text for html presentation. Replace sensitive characters with
+ * html entities.
+ *
+ * @param inStr
+ * @param changeSpace
+ * @return plain text escaped for html
+ */
+ public static String escapeForHtml(String inStr, boolean changeSpace) {
+ StringBuilder retStr = new StringBuilder();
+ int i = 0;
+ while (i < inStr.length()) {
+ if (inStr.charAt(i) == '&') {
+ retStr.append("&amp;");
+ } else if (inStr.charAt(i) == '<') {
+ retStr.append("&lt;");
+ } else if (inStr.charAt(i) == '>') {
+ retStr.append("&gt;");
+ } else if (inStr.charAt(i) == '\"') {
+ retStr.append("&quot;");
+ } else if (changeSpace && inStr.charAt(i) == ' ') {
+ retStr.append("&nbsp;");
+ } else if (changeSpace && inStr.charAt(i) == '\t') {
+ retStr.append(" &nbsp; &nbsp;");
+ } else {
+ retStr.append(inStr.charAt(i));
+ }
+ i++;
+ }
+ return retStr.toString();
+ }
+
+ /**
+ * Decode html entities back into plain text characters.
+ *
+ * @param inStr
+ * @return returns plain text from html
+ */
+ public static String decodeFromHtml(String inStr) {
+ return inStr.replace("&amp;", "&").replace("&lt;", "<").replace("&gt;", ">")
+ .replace("&quot;", "\"").replace("&nbsp;", " ");
+ }
+
+ /**
+ * Encodes a url parameter by escaping troublesome characters.
+ *
+ * @param inStr
+ * @return properly escaped url
+ */
+ public static String encodeURL(String inStr) {
+ StringBuilder retStr = new StringBuilder();
+ int i = 0;
+ while (i < inStr.length()) {
+ if (inStr.charAt(i) == '/') {
+ retStr.append("%2F");
+ } else if (inStr.charAt(i) == ' ') {
+ retStr.append("%20");
+ } else {
+ retStr.append(inStr.charAt(i));
+ }
+ i++;
+ }
+ return retStr.toString();
+ }
+
+ /**
+ * Flatten the list of strings into a single string with a space separator.
+ *
+ * @param values
+ * @return flattened list
+ */
+ public static String flattenStrings(Collection<String> values) {
+ return flattenStrings(values, " ");
+ }
+
+ /**
+ * Flatten the list of strings into a single string with the specified
+ * separator.
+ *
+ * @param values
+ * @param separator
+ * @return flattened list
+ */
+ public static String flattenStrings(Collection<String> values, String separator) {
+ StringBuilder sb = new StringBuilder();
+ for (String value : values) {
+ sb.append(value).append(separator);
+ }
+ if (sb.length() > 0) {
+ // truncate trailing separator
+ sb.setLength(sb.length() - separator.length());
+ }
+ return sb.toString().trim();
+ }
+
+ /**
+ * Returns a string trimmed to a maximum length with trailing ellipses. If
+ * the string length is shorter than the max, the original string is
+ * returned.
+ *
+ * @param value
+ * @param max
+ * @return trimmed string
+ */
+ public static String trimString(String value, int max) {
+ if (value.length() <= max) {
+ return value;
+ }
+ return value.substring(0, max - 3) + "...";
+ }
+
+ /**
+ * Left pad a string with the specified character, if the string length is
+ * less than the specified length.
+ *
+ * @param input
+ * @param length
+ * @param pad
+ * @return left-padded string
+ */
+ public static String leftPad(String input, int length, char pad) {
+ if (input.length() < length) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0, len = length - input.length(); i < len; i++) {
+ sb.append(pad);
+ }
+ sb.append(input);
+ return sb.toString();
+ }
+ return input;
+ }
+
+ /**
+ * Right pad a string with the specified character, if the string length is
+ * less then the specified length.
+ *
+ * @param input
+ * @param length
+ * @param pad
+ * @return right-padded string
+ */
+ public static String rightPad(String input, int length, char pad) {
+ if (input.length() < length) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(input);
+ for (int i = 0, len = length - input.length(); i < len; i++) {
+ sb.append(pad);
+ }
+ return sb.toString();
+ }
+ return input;
+ }
+
+ /**
+ * Calculates the SHA1 of the string.
+ *
+ * @param text
+ * @return sha1 of the string
+ */
+ public static String getSHA1(String text) {
+ try {
+ byte[] bytes = text.getBytes("iso-8859-1");
+ return getSHA1(bytes);
+ } catch (UnsupportedEncodingException u) {
+ throw new RuntimeException(u);
+ }
+ }
+
+ /**
+ * Calculates the SHA1 of the byte array.
+ *
+ * @param bytes
+ * @return sha1 of the byte array
+ */
+ public static String getSHA1(byte[] bytes) {
+ try {
+ MessageDigest md = MessageDigest.getInstance("SHA-1");
+ md.update(bytes, 0, bytes.length);
+ byte[] digest = md.digest();
+ return toHex(digest);
+ } catch (NoSuchAlgorithmException t) {
+ throw new RuntimeException(t);
+ }
+ }
+
+ /**
+ * Calculates the MD5 of the string.
+ *
+ * @param string
+ * @return md5 of the string
+ */
+ public static String getMD5(String string) {
+ try {
+ return getMD5(string.getBytes("iso-8859-1"));
+ } catch (UnsupportedEncodingException u) {
+ throw new RuntimeException(u);
+ }
+ }
+
+ /**
+ * Calculates the MD5 of the string.
+ *
+ * @param string
+ * @return md5 of the string
+ */
+ public static String getMD5(byte [] bytes) {
+ try {
+ MessageDigest md = MessageDigest.getInstance("MD5");
+ md.reset();
+ md.update(bytes);
+ byte[] digest = md.digest();
+ return toHex(digest);
+ } catch (NoSuchAlgorithmException t) {
+ throw new RuntimeException(t);
+ }
+ }
+
+ /**
+ * Returns the hex representation of the byte array.
+ *
+ * @param bytes
+ * @return byte array as hex string
+ */
+ private static String toHex(byte[] bytes) {
+ StringBuilder sb = new StringBuilder(bytes.length * 2);
+ for (int i = 0; i < bytes.length; i++) {
+ if (((int) bytes[i] & 0xff) < 0x10) {
+ sb.append('0');
+ }
+ sb.append(Long.toString((int) bytes[i] & 0xff, 16));
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Returns the root path of the specified path. Returns a blank string if
+ * there is no root path.
+ *
+ * @param path
+ * @return root path or blank
+ */
+ public static String getRootPath(String path) {
+ if (path.indexOf('/') > -1) {
+ return path.substring(0, path.lastIndexOf('/'));
+ }
+ return "";
+ }
+
+ /**
+ * Returns the path remainder after subtracting the basePath from the
+ * fullPath.
+ *
+ * @param basePath
+ * @param fullPath
+ * @return the relative path
+ */
+ public static String getRelativePath(String basePath, String fullPath) {
+ String bp = basePath.replace('\\', '/').toLowerCase();
+ String fp = fullPath.replace('\\', '/').toLowerCase();
+ if (fp.startsWith(bp)) {
+ String relativePath = fullPath.substring(basePath.length()).replace('\\', '/');
+ if (relativePath.charAt(0) == '/') {
+ relativePath = relativePath.substring(1);
+ }
+ return relativePath;
+ }
+ return fullPath;
+ }
+
+ /**
+ * Splits the space-separated string into a list of strings.
+ *
+ * @param value
+ * @return list of strings
+ */
+ public static List<String> getStringsFromValue(String value) {
+ return getStringsFromValue(value, " ");
+ }
+
+ /**
+ * Splits the string into a list of string by the specified separator.
+ *
+ * @param value
+ * @param separator
+ * @return list of strings
+ */
+ public static List<String> getStringsFromValue(String value, String separator) {
+ List<String> strings = new ArrayList<String>();
+ try {
+ String[] chunks = value.split(separator + "(?=([^\"]*\"[^\"]*\")*[^\"]*$)");
+ for (String chunk : chunks) {
+ chunk = chunk.trim();
+ if (chunk.length() > 0) {
+ if (chunk.charAt(0) == '"' && chunk.charAt(chunk.length() - 1) == '"') {
+ // strip double quotes
+ chunk = chunk.substring(1, chunk.length() - 1).trim();
+ }
+ strings.add(chunk);
+ }
+ }
+ } catch (PatternSyntaxException e) {
+ throw new RuntimeException(e);
+ }
+ return strings;
+ }
+
+ /**
+ * Validates that a name is composed of letters, digits, or limited other
+ * characters.
+ *
+ * @param name
+ * @return the first invalid character found or null if string is acceptable
+ */
+ public static Character findInvalidCharacter(String name) {
+ char[] validChars = { '/', '.', '_', '-', '~' };
+ for (char c : name.toCharArray()) {
+ if (!Character.isLetterOrDigit(c)) {
+ boolean ok = false;
+ for (char vc : validChars) {
+ ok |= c == vc;
+ }
+ if (!ok) {
+ return c;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Simple fuzzy string comparison. This is a case-insensitive check. A
+ * single wildcard * value is supported.
+ *
+ * @param value
+ * @param pattern
+ * @return true if the value matches the pattern
+ */
+ public static boolean fuzzyMatch(String value, String pattern) {
+ if (value.equalsIgnoreCase(pattern)) {
+ return true;
+ }
+ if (pattern.contains("*")) {
+ boolean prefixMatches = false;
+ boolean suffixMatches = false;
+
+ int wildcard = pattern.indexOf('*');
+ String prefix = pattern.substring(0, wildcard).toLowerCase();
+ prefixMatches = value.toLowerCase().startsWith(prefix);
+
+ if (pattern.length() > (wildcard + 1)) {
+ String suffix = pattern.substring(wildcard + 1).toLowerCase();
+ suffixMatches = value.toLowerCase().endsWith(suffix);
+ return prefixMatches && suffixMatches;
+ }
+ return prefixMatches || suffixMatches;
+ }
+ return false;
+ }
+
+ /**
+ * Compare two repository names for proper group sorting.
+ *
+ * @param r1
+ * @param r2
+ * @return
+ */
+ public static int compareRepositoryNames(String r1, String r2) {
+ // sort root repositories first, alphabetically
+ // then sort grouped repositories, alphabetically
+ r1 = r1.toLowerCase();
+ r2 = r2.toLowerCase();
+ int s1 = r1.indexOf('/');
+ int s2 = r2.indexOf('/');
+ if (s1 == -1 && s2 == -1) {
+ // neither grouped
+ return r1.compareTo(r2);
+ } else if (s1 > -1 && s2 > -1) {
+ // both grouped
+ return r1.compareTo(r2);
+ } else if (s1 == -1) {
+ return -1;
+ } else if (s2 == -1) {
+ return 1;
+ }
+ return 0;
+ }
+
+ /**
+ * Sort grouped repository names.
+ *
+ * @param list
+ */
+ public static void sortRepositorynames(List<String> list) {
+ Collections.sort(list, new Comparator<String>() {
+ @Override
+ public int compare(String o1, String o2) {
+ return compareRepositoryNames(o1, o2);
+ }
+ });
+ }
+
+ public static String getColor(String value) {
+ int cs = 0;
+ for (char c : getMD5(value.toLowerCase()).toCharArray()) {
+ cs += c;
+ }
+ int n = (cs % 360);
+ float hue = ((float) n) / 360;
+ return hsvToRgb(hue, 0.90f, 0.65f);
+ }
+
+ public static String hsvToRgb(float hue, float saturation, float value) {
+ int h = (int) (hue * 6);
+ float f = hue * 6 - h;
+ float p = value * (1 - saturation);
+ float q = value * (1 - f * saturation);
+ float t = value * (1 - (1 - f) * saturation);
+
+ switch (h) {
+ case 0:
+ return rgbToString(value, t, p);
+ case 1:
+ return rgbToString(q, value, p);
+ case 2:
+ return rgbToString(p, value, t);
+ case 3:
+ return rgbToString(p, q, value);
+ case 4:
+ return rgbToString(t, p, value);
+ case 5:
+ return rgbToString(value, p, q);
+ default:
+ throw new RuntimeException(
+ "Something went wrong when converting from HSV to RGB. Input was " + hue + ", "
+ + saturation + ", " + value);
+ }
+ }
+
+ public static String rgbToString(float r, float g, float b) {
+ String rs = Integer.toHexString((int) (r * 256));
+ String gs = Integer.toHexString((int) (g * 256));
+ String bs = Integer.toHexString((int) (b * 256));
+ return "#" + rs + gs + bs;
+ }
+
+ /**
+ * Strips a trailing ".git" from the value.
+ *
+ * @param value
+ * @return a stripped value or the original value if .git is not found
+ */
+ public static String stripDotGit(String value) {
+ if (value.toLowerCase().endsWith(".git")) {
+ return value.substring(0, value.length() - 4);
+ }
+ return value;
+ }
+
+ /**
+ * Count the number of lines in a string.
+ *
+ * @param value
+ * @return the line count
+ */
+ public static int countLines(String value) {
+ if (isEmpty(value)) {
+ return 0;
+ }
+ return value.split("\n").length;
+ }
+
+ /**
+ * Returns the file extension of a path.
+ *
+ * @param path
+ * @return a blank string or a file extension
+ */
+ public static String getFileExtension(String path) {
+ int lastDot = path.lastIndexOf('.');
+ if (lastDot > -1) {
+ return path.substring(lastDot + 1);
+ }
+ return "";
+ }
+
+ /**
+ * Replace all occurences of a substring within a string with
+ * another string.
+ *
+ * From Spring StringUtils.
+ *
+ * @param inString String to examine
+ * @param oldPattern String to replace
+ * @param newPattern String to insert
+ * @return a String with the replacements
+ */
+ public static String replace(String inString, String oldPattern, String newPattern) {
+ StringBuilder sb = new StringBuilder();
+ int pos = 0; // our position in the old string
+ int index = inString.indexOf(oldPattern);
+ // the index of an occurrence we've found, or -1
+ int patLen = oldPattern.length();
+ while (index >= 0) {
+ sb.append(inString.substring(pos, index));
+ sb.append(newPattern);
+ pos = index + patLen;
+ index = inString.indexOf(oldPattern, pos);
+ }
+ sb.append(inString.substring(pos));
+ // remember to append any characters to the right of a match
+ return sb.toString();
+ }
+
+ /**
+ * Decodes a string by trying several charsets until one does not throw a
+ * coding exception. Last resort is to interpret as UTF-8 with illegal
+ * character substitution.
+ *
+ * @param content
+ * @param charsets optional
+ * @return a string
+ */
+ public static String decodeString(byte [] content, String... charsets) {
+ Set<String> sets = new LinkedHashSet<String>();
+ if (!ArrayUtils.isEmpty(charsets)) {
+ sets.addAll(Arrays.asList(charsets));
+ }
+ String value = null;
+ sets.addAll(Arrays.asList("UTF-8", "ISO-8859-1", Charset.defaultCharset().name()));
+ for (String charset : sets) {
+ try {
+ Charset cs = Charset.forName(charset);
+ CharsetDecoder decoder = cs.newDecoder();
+ CharBuffer buffer = decoder.decode(ByteBuffer.wrap(content));
+ value = buffer.toString();
+ break;
+ } catch (CharacterCodingException e) {
+ // ignore and advance to the next charset
+ } catch (IllegalCharsetNameException e) {
+ // ignore illegal charset names
+ } catch (UnsupportedCharsetException e) {
+ // ignore unsupported charsets
+ }
+ }
+ if (value.startsWith("\uFEFF")) {
+ // strip UTF-8 BOM
+ return value.substring(1);
+ }
+ return value;
+ }
+
+ /**
+ * Attempt to extract a repository name from a given url using regular
+ * expressions. If no match is made, then return whatever trails after
+ * the final / character.
+ *
+ * @param regexUrls
+ * @return a repository path
+ */
+ public static String extractRepositoryPath(String url, String... urlpatterns) {
+ for (String urlPattern : urlpatterns) {
+ Pattern p = Pattern.compile(urlPattern);
+ Matcher m = p.matcher(url);
+ while (m.find()) {
+ String repositoryPath = m.group(1);
+ return repositoryPath;
+ }
+ }
+ // last resort
+ if (url.lastIndexOf('/') > -1) {
+ return url.substring(url.lastIndexOf('/') + 1);
+ }
+ return url;
+ }
+
+ /**
+ * Converts a string with \nnn sequences into a UTF-8 encoded string.
+ * @param input
+ * @return
+ */
+ public static String convertOctal(String input) {
+ try {
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ Pattern p = Pattern.compile("(\\\\\\d{3})");
+ Matcher m = p.matcher(input);
+ int i = 0;
+ while (m.find()) {
+ bytes.write(input.substring(i, m.start()).getBytes("UTF-8"));
+ // replace octal encoded value
+ // strip leading \ character
+ String oct = m.group().substring(1);
+ bytes.write(Integer.parseInt(oct, 8));
+ i = m.end();
+ }
+ if (bytes.size() == 0) {
+ // no octal matches
+ return input;
+ } else {
+ if (i < input.length()) {
+ // add remainder of string
+ bytes.write(input.substring(i).getBytes("UTF-8"));
+ }
+ }
+ return bytes.toString("UTF-8");
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return input;
+ }
+
+ /**
+ * Returns the first path element of a path string. If no path separator is
+ * found in the path, an empty string is returned.
+ *
+ * @param path
+ * @return the first element in the path
+ */
+ public static String getFirstPathElement(String path) {
+ if (path.indexOf('/') > -1) {
+ return path.substring(0, path.indexOf('/')).trim();
+ }
+ return "";
+ }
+
+ /**
+ * Returns the last path element of a path string
+ *
+ * @param path
+ * @return the last element in the path
+ */
+ public static String getLastPathElement(String path) {
+ if (path.indexOf('/') > -1) {
+ return path.substring(path.lastIndexOf('/') + 1);
+ }
+ return path;
+ }
+
+ /**
+ * Variation of String.matches() which disregards case issues.
+ *
+ * @param regex
+ * @param input
+ * @return true if the pattern matches
+ */
+ public static boolean matchesIgnoreCase(String input, String regex) {
+ Pattern p = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
+ Matcher m = p.matcher(input);
+ return m.matches();
+ }
+
+ /**
+ * Removes new line and carriage return chars from a string.
+ * If input value is null an empty string is returned.
+ *
+ * @param input
+ * @return a sanitized or empty string
+ */
+ public static String removeNewlines(String input) {
+ if (input == null) {
+ return "";
+ }
+ return input.replace('\n',' ').replace('\r', ' ').trim();
+ }
+} \ No newline at end of file
diff --git a/src/main/java/com/gitblit/utils/SyndicationUtils.java b/src/main/java/com/gitblit/utils/SyndicationUtils.java
new file mode 100644
index 00000000..d01d4691
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/SyndicationUtils.java
@@ -0,0 +1,262 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.utils;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.net.URLConnection;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.List;
+
+import com.gitblit.Constants;
+import com.gitblit.GitBlitException;
+import com.gitblit.models.FeedEntryModel;
+import com.sun.syndication.feed.synd.SyndCategory;
+import com.sun.syndication.feed.synd.SyndCategoryImpl;
+import com.sun.syndication.feed.synd.SyndContent;
+import com.sun.syndication.feed.synd.SyndContentImpl;
+import com.sun.syndication.feed.synd.SyndEntry;
+import com.sun.syndication.feed.synd.SyndEntryImpl;
+import com.sun.syndication.feed.synd.SyndFeed;
+import com.sun.syndication.feed.synd.SyndFeedImpl;
+import com.sun.syndication.feed.synd.SyndImageImpl;
+import com.sun.syndication.io.FeedException;
+import com.sun.syndication.io.SyndFeedInput;
+import com.sun.syndication.io.SyndFeedOutput;
+import com.sun.syndication.io.XmlReader;
+
+/**
+ * Utility class for RSS feeds.
+ *
+ * @author James Moger
+ *
+ */
+public class SyndicationUtils {
+
+ /**
+ * Outputs an RSS feed of the list of entries to the outputstream.
+ *
+ * @param hostUrl
+ * @param feedLink
+ * @param title
+ * @param description
+ * @param entryModels
+ * @param os
+ * @throws IOException
+ * @throws FeedException
+ */
+ public static void toRSS(String hostUrl, String feedLink, String title, String description,
+ List<FeedEntryModel> entryModels, OutputStream os)
+ throws IOException, FeedException {
+
+ SyndFeed feed = new SyndFeedImpl();
+ feed.setFeedType("rss_2.0");
+ feed.setEncoding("UTF-8");
+ feed.setTitle(title);
+ feed.setLink(feedLink);
+ feed.setDescription(description);
+ SyndImageImpl image = new SyndImageImpl();
+ image.setTitle(Constants.NAME);
+ image.setUrl(hostUrl + "/gitblt_25.png");
+ image.setLink(hostUrl);
+ feed.setImage(image);
+
+ List<SyndEntry> entries = new ArrayList<SyndEntry>();
+ for (FeedEntryModel entryModel : entryModels) {
+ SyndEntry entry = new SyndEntryImpl();
+ entry.setTitle(entryModel.title);
+ entry.setAuthor(entryModel.author);
+ entry.setLink(entryModel.link);
+ entry.setPublishedDate(entryModel.published);
+
+ if (entryModel.tags != null && entryModel.tags.size() > 0) {
+ List<SyndCategory> tags = new ArrayList<SyndCategory>();
+ for (String tag : entryModel.tags) {
+ SyndCategoryImpl cat = new SyndCategoryImpl();
+ cat.setName(tag);
+ tags.add(cat);
+ }
+ entry.setCategories(tags);
+ }
+
+ SyndContent content = new SyndContentImpl();
+ if (StringUtils.isEmpty(entryModel.contentType)
+ || entryModel.contentType.equalsIgnoreCase("text/plain")) {
+ content.setType("text/html");
+ content.setValue(StringUtils.breakLinesForHtml(entryModel.content));
+ } else {
+ content.setType(entryModel.contentType);
+ content.setValue(entryModel.content);
+ }
+ entry.setDescription(content);
+
+ entries.add(entry);
+ }
+ feed.setEntries(entries);
+
+ OutputStreamWriter writer = new OutputStreamWriter(os, "UTF-8");
+ SyndFeedOutput output = new SyndFeedOutput();
+ output.output(feed, writer);
+ writer.close();
+ }
+
+ /**
+ * Reads a Gitblit RSS feed.
+ *
+ * @param url
+ * the url of the Gitblit server
+ * @param repository
+ * the repository name
+ * @param branch
+ * the branch name (optional)
+ * @param numberOfEntries
+ * the number of entries to retrieve. if <= 0 the server default
+ * is used.
+ * @param page
+ * 0-indexed. used to paginate the results.
+ * @param username
+ * @param password
+ * @return a list of SyndicationModel entries
+ * @throws {@link IOException}
+ */
+ public static List<FeedEntryModel> readFeed(String url, String repository, String branch,
+ int numberOfEntries, int page, String username, char[] password) throws IOException {
+ // build feed url
+ List<String> parameters = new ArrayList<String>();
+ if (numberOfEntries > 0) {
+ parameters.add("l=" + numberOfEntries);
+ }
+ if (page > 0) {
+ parameters.add("pg=" + page);
+ }
+ if (!StringUtils.isEmpty(branch)) {
+ parameters.add("h=" + branch);
+ }
+ return readFeed(url, parameters, repository, branch, username, password);
+ }
+
+ /**
+ * Reads a Gitblit RSS search feed.
+ *
+ * @param url
+ * the url of the Gitblit server
+ * @param repository
+ * the repository name
+ * @param fragment
+ * the search fragment
+ * @param searchType
+ * the search type (optional, defaults to COMMIT)
+ * @param numberOfEntries
+ * the number of entries to retrieve. if <= 0 the server default
+ * is used.
+ * @param page
+ * 0-indexed. used to paginate the results.
+ * @param username
+ * @param password
+ * @return a list of SyndicationModel entries
+ * @throws {@link IOException}
+ */
+ public static List<FeedEntryModel> readSearchFeed(String url, String repository, String branch,
+ String fragment, Constants.SearchType searchType, int numberOfEntries, int page,
+ String username, char[] password) throws IOException {
+ // determine parameters
+ List<String> parameters = new ArrayList<String>();
+ parameters.add("s=" + StringUtils.encodeURL(fragment));
+ if (numberOfEntries > 0) {
+ parameters.add("l=" + numberOfEntries);
+ }
+ if (page > 0) {
+ parameters.add("pg=" + page);
+ }
+ if (!StringUtils.isEmpty(branch)) {
+ parameters.add("h=" + branch);
+ }
+ if (searchType != null) {
+ parameters.add("st=" + searchType.name());
+ }
+ return readFeed(url, parameters, repository, branch, username, password);
+ }
+
+ /**
+ * Reads a Gitblit RSS feed.
+ *
+ * @param url
+ * the url of the Gitblit server
+ * @param parameters
+ * the list of RSS parameters
+ * @param repository
+ * the repository name
+ * @param username
+ * @param password
+ * @return a list of SyndicationModel entries
+ * @throws {@link IOException}
+ */
+ private static List<FeedEntryModel> readFeed(String url, List<String> parameters,
+ String repository, String branch, String username, char[] password) throws IOException {
+ // build url
+ StringBuilder sb = new StringBuilder();
+ sb.append(MessageFormat.format("{0}" + Constants.SYNDICATION_PATH + "{1}", url, repository));
+ if (parameters.size() > 0) {
+ boolean first = true;
+ for (String parameter : parameters) {
+ if (first) {
+ sb.append('?');
+ first = false;
+ } else {
+ sb.append('&');
+ }
+ sb.append(parameter);
+ }
+ }
+ String feedUrl = sb.toString();
+ URLConnection conn = ConnectionUtils.openReadConnection(feedUrl, username, password);
+ InputStream is = conn.getInputStream();
+ SyndFeedInput input = new SyndFeedInput();
+ SyndFeed feed = null;
+ try {
+ feed = input.build(new XmlReader(is));
+ } catch (FeedException f) {
+ throw new GitBlitException(f);
+ }
+ is.close();
+ List<FeedEntryModel> entries = new ArrayList<FeedEntryModel>();
+ for (Object o : feed.getEntries()) {
+ SyndEntryImpl entry = (SyndEntryImpl) o;
+ FeedEntryModel model = new FeedEntryModel();
+ model.repository = repository;
+ model.branch = branch;
+ model.title = entry.getTitle();
+ model.author = entry.getAuthor();
+ model.published = entry.getPublishedDate();
+ model.link = entry.getLink();
+ model.content = entry.getDescription().getValue();
+ model.contentType = entry.getDescription().getType();
+ if (entry.getCategories() != null && entry.getCategories().size() > 0) {
+ List<String> tags = new ArrayList<String>();
+ for (Object p : entry.getCategories()) {
+ SyndCategory cat = (SyndCategory) p;
+ tags.add(cat.getName());
+ }
+ model.tags = tags;
+ }
+ entries.add(model);
+ }
+ return entries;
+ }
+}
diff --git a/src/main/java/com/gitblit/utils/TicgitUtils.java b/src/main/java/com/gitblit/utils/TicgitUtils.java
new file mode 100644
index 00000000..aab5a3e1
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/TicgitUtils.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.utils;
+
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.models.PathModel;
+import com.gitblit.models.RefModel;
+import com.gitblit.models.TicketModel;
+import com.gitblit.models.TicketModel.Comment;
+
+/**
+ * Utility class for reading Ticgit issues.
+ *
+ * @author James Moger
+ *
+ */
+public class TicgitUtils {
+
+ static final Logger LOGGER = LoggerFactory.getLogger(TicgitUtils.class);
+
+ /**
+ * Returns a RefModel for the Ticgit branch in the repository. If the branch
+ * can not be found, null is returned.
+ *
+ * @param repository
+ * @return a refmodel for the ticgit branch or null
+ */
+ public static RefModel getTicketsBranch(Repository repository) {
+ return JGitUtils.getBranch(repository, "ticgit");
+ }
+
+ /**
+ * Returns a list of all tickets in the ticgit branch of the repository.
+ *
+ * @param repository
+ * @return list of tickets
+ */
+ public static List<TicketModel> getTickets(Repository repository) {
+ RefModel ticgitBranch = getTicketsBranch(repository);
+ if (ticgitBranch == null) {
+ return null;
+ }
+ RevCommit commit = (RevCommit) ticgitBranch.referencedObject;
+ List<PathModel> paths = JGitUtils.getFilesInPath(repository, null, commit);
+ List<TicketModel> tickets = new ArrayList<TicketModel>();
+ for (PathModel ticketFolder : paths) {
+ if (ticketFolder.isTree()) {
+ try {
+ TicketModel t = new TicketModel(ticketFolder.name);
+ loadTicketContents(repository, ticgitBranch, t);
+ tickets.add(t);
+ } catch (Throwable t) {
+ LOGGER.error("Failed to get a ticket!", t);
+ }
+ }
+ }
+ Collections.sort(tickets);
+ Collections.reverse(tickets);
+ return tickets;
+ }
+
+ /**
+ * Returns a TicketModel for the specified ticgit ticket. Returns null if
+ * the ticket does not exist or some other error occurs.
+ *
+ * @param repository
+ * @param ticketFolder
+ * @return a ticket
+ */
+ public static TicketModel getTicket(Repository repository, String ticketFolder) {
+ RefModel ticketsBranch = getTicketsBranch(repository);
+ if (ticketsBranch != null) {
+ try {
+ TicketModel ticket = new TicketModel(ticketFolder);
+ loadTicketContents(repository, ticketsBranch, ticket);
+ return ticket;
+ } catch (Throwable t) {
+ LOGGER.error("Failed to get ticket " + ticketFolder, t);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Loads the contents of the ticket.
+ *
+ * @param repository
+ * @param ticketsBranch
+ * @param ticket
+ */
+ private static void loadTicketContents(Repository repository, RefModel ticketsBranch,
+ TicketModel ticket) {
+ RevCommit commit = (RevCommit) ticketsBranch.referencedObject;
+ List<PathModel> ticketFiles = JGitUtils.getFilesInPath(repository, ticket.name, commit);
+ for (PathModel file : ticketFiles) {
+ String content = JGitUtils.getStringContent(repository, commit.getTree(), file.path)
+ .trim();
+ if (file.name.equals("TICKET_ID")) {
+ ticket.id = content;
+ } else if (file.name.equals("TITLE")) {
+ ticket.title = content;
+ } else {
+ String[] chunks = file.name.split("_");
+ if (chunks[0].equals("ASSIGNED")) {
+ ticket.handler = content;
+ } else if (chunks[0].equals("COMMENT")) {
+ try {
+ Comment c = new Comment(file.name, content);
+ ticket.comments.add(c);
+ } catch (ParseException e) {
+ LOGGER.error("Failed to parse ticket comment", e);
+ }
+ } else if (chunks[0].equals("TAG")) {
+ if (content.startsWith("TAG_")) {
+ ticket.tags.add(content.substring(4));
+ } else {
+ ticket.tags.add(content);
+ }
+ } else if (chunks[0].equals("STATE")) {
+ ticket.state = content;
+ }
+ }
+ }
+ Collections.sort(ticket.comments);
+ }
+}
diff --git a/src/main/java/com/gitblit/utils/TimeUtils.java b/src/main/java/com/gitblit/utils/TimeUtils.java
new file mode 100644
index 00000000..ec8871c6
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/TimeUtils.java
@@ -0,0 +1,341 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.utils;
+
+import java.text.MessageFormat;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.ResourceBundle;
+
+/**
+ * Utility class of time functions.
+ *
+ * @author James Moger
+ *
+ */
+public class TimeUtils {
+ public static final long MIN = 1000 * 60L;
+
+ public static final long HALFHOUR = MIN * 30L;
+
+ public static final long ONEHOUR = HALFHOUR * 2;
+
+ public static final long ONEDAY = ONEHOUR * 24L;
+
+ public static final long ONEYEAR = ONEDAY * 365L;
+
+ private final ResourceBundle translation;
+
+ public TimeUtils() {
+ this(null);
+ }
+
+ public TimeUtils(ResourceBundle translation) {
+ this.translation = translation;
+ }
+
+ /**
+ * Returns true if date is today.
+ *
+ * @param date
+ * @return true if date is today
+ */
+ public static boolean isToday(Date date) {
+ return (System.currentTimeMillis() - date.getTime()) < ONEDAY;
+ }
+
+ /**
+ * Returns true if date is yesterday.
+ *
+ * @param date
+ * @return true if date is yesterday
+ */
+ public static boolean isYesterday(Date date) {
+ Calendar cal = Calendar.getInstance();
+ cal.setTime(new Date());
+ cal.add(Calendar.DATE, -1);
+ SimpleDateFormat df = new SimpleDateFormat("yyyyMMdd");
+ return df.format(cal.getTime()).equals(df.format(date));
+ }
+
+ /**
+ * Returns the string representation of the duration as days, months and/or
+ * years.
+ *
+ * @param days
+ * @return duration as string in days, months, and/or years
+ */
+ public String duration(int days) {
+ if (days <= 60) {
+ return (days > 1 ? translate(days, "gb.duration.days", "{0} days") : translate("gb.duration.oneDay", "1 day"));
+ } else if (days < 365) {
+ int rem = days % 30;
+ return translate(((days / 30) + (rem >= 15 ? 1 : 0)), "gb.duration.months", "{0} months");
+ } else {
+ int years = days / 365;
+ int rem = days % 365;
+ String yearsString = (years > 1 ? translate(years, "gb.duration.years", "{0} years") : translate("gb.duration.oneYear", "1 year"));
+ if (rem < 30) {
+ if (rem == 0) {
+ return yearsString;
+ } else {
+ return yearsString + (rem >= 15 ? (", " + translate("gb.duration.oneMonth", "1 month")): "");
+ }
+ } else {
+ int months = rem / 30;
+ int remDays = rem % 30;
+ if (remDays >= 15) {
+ months++;
+ }
+ String monthsString = yearsString + ", "
+ + (months > 1 ? translate(months, "gb.duration.months", "{0} months") : translate("gb.duration.oneMonth", "1 month"));
+ return monthsString;
+ }
+ }
+ }
+
+ /**
+ * Returns the number of minutes ago between the start time and the end
+ * time.
+ *
+ * @param date
+ * @param endTime
+ * @param roundup
+ * @return difference in minutes
+ */
+ public static int minutesAgo(Date date, long endTime, boolean roundup) {
+ long diff = endTime - date.getTime();
+ int mins = (int) (diff / MIN);
+ if (roundup && (diff % MIN) >= 30) {
+ mins++;
+ }
+ return mins;
+ }
+
+ /**
+ * Return the difference in minutes between now and the date.
+ *
+ * @param date
+ * @param roundup
+ * @return minutes ago
+ */
+ public static int minutesAgo(Date date, boolean roundup) {
+ return minutesAgo(date, System.currentTimeMillis(), roundup);
+ }
+
+ /**
+ * Return the difference in hours between now and the date.
+ *
+ * @param date
+ * @param roundup
+ * @return hours ago
+ */
+ public static int hoursAgo(Date date, boolean roundup) {
+ long diff = System.currentTimeMillis() - date.getTime();
+ int hours = (int) (diff / ONEHOUR);
+ if (roundup && (diff % ONEHOUR) >= HALFHOUR) {
+ hours++;
+ }
+ return hours;
+ }
+
+ /**
+ * Return the difference in days between now and the date.
+ *
+ * @param date
+ * @return days ago
+ */
+ public static int daysAgo(Date date) {
+ long today = ONEDAY * (System.currentTimeMillis()/ONEDAY);
+ long day = ONEDAY * (date.getTime()/ONEDAY);
+ long diff = today - day;
+ int days = (int) (diff / ONEDAY);
+ return days;
+ }
+
+ public String today() {
+ return translate("gb.time.today", "today");
+ }
+
+ public String yesterday() {
+ return translate("gb.time.yesterday", "yesterday");
+ }
+
+ /**
+ * Returns the string representation of the duration between now and the
+ * date.
+ *
+ * @param date
+ * @return duration as a string
+ */
+ public String timeAgo(Date date) {
+ return timeAgo(date, false);
+ }
+
+ /**
+ * Returns the CSS class for the date based on its age from Now.
+ *
+ * @param date
+ * @return the css class
+ */
+ public String timeAgoCss(Date date) {
+ return timeAgo(date, true);
+ }
+
+ /**
+ * Returns the string representation of the duration OR the css class for
+ * the duration.
+ *
+ * @param date
+ * @param css
+ * @return the string representation of the duration OR the css class
+ */
+ private String timeAgo(Date date, boolean css) {
+ if (isToday(date) || isYesterday(date)) {
+ int mins = minutesAgo(date, true);
+ if (mins >= 120) {
+ if (css) {
+ return "age1";
+ }
+ int hours = hoursAgo(date, true);
+ if (hours > 23) {
+ return yesterday();
+ } else {
+ return translate(hours, "gb.time.hoursAgo", "{0} hours ago");
+ }
+ }
+ if (css) {
+ return "age0";
+ }
+ if (mins > 2) {
+ return translate(mins, "gb.time.minsAgo", "{0} mins ago");
+ }
+ return translate("gb.time.justNow", "just now");
+ } else {
+ int days = daysAgo(date);
+ if (css) {
+ if (days <= 7) {
+ return "age2";
+ } if (days <= 30) {
+ return "age3";
+ } else {
+ return "age4";
+ }
+ }
+ if (days < 365) {
+ if (days <= 30) {
+ return translate(days, "gb.time.daysAgo", "{0} days ago");
+ } else if (days <= 90) {
+ int weeks = days / 7;
+ if (weeks == 12) {
+ return translate(3, "gb.time.monthsAgo", "{0} months ago");
+ } else {
+ return translate(weeks, "gb.time.weeksAgo", "{0} weeks ago");
+ }
+ }
+ int months = days / 30;
+ int weeks = (days % 30) / 7;
+ if (weeks >= 2) {
+ months++;
+ }
+ return translate(months, "gb.time.monthsAgo", "{0} months ago");
+ } else if (days == 365) {
+ return translate("gb.time.oneYearAgo", "1 year ago");
+ } else {
+ int yr = days / 365;
+ days = days % 365;
+ int months = (yr * 12) + (days / 30);
+ if (months > 23) {
+ return translate(yr, "gb.time.yearsAgo", "{0} years ago");
+ } else {
+ return translate(months, "gb.time.monthsAgo", "{0} months ago");
+ }
+ }
+ }
+ }
+
+ public String inFuture(Date date) {
+ long diff = date.getTime() - System.currentTimeMillis();
+ if (diff > ONEDAY) {
+ double days = ((double) diff)/ONEDAY;
+ return translate((int) Math.round(days), "gb.time.inDays", "in {0} days");
+ } else {
+ double hours = ((double) diff)/ONEHOUR;
+ if (hours > 2) {
+ return translate((int) Math.round(hours), "gb.time.inHours", "in {0} hours");
+ } else {
+ int mins = (int) (diff/MIN);
+ return translate(mins, "gb.time.inMinutes", "in {0} minutes");
+ }
+ }
+ }
+
+ private String translate(String key, String defaultValue) {
+ String value = defaultValue;
+ if (translation != null && translation.containsKey(key)) {
+ String aValue = translation.getString(key);
+ if (!StringUtils.isEmpty(aValue)) {
+ value = aValue;
+ }
+ }
+ return value;
+ }
+
+ private String translate(int val, String key, String defaultPattern) {
+ String pattern = defaultPattern;
+ if (translation != null && translation.containsKey(key)) {
+ String aValue = translation.getString(key);
+ if (!StringUtils.isEmpty(aValue)) {
+ pattern = aValue;
+ }
+ }
+ return MessageFormat.format(pattern, val);
+ }
+
+ /**
+ * Convert a frequency string into minutes.
+ *
+ * @param frequency
+ * @return minutes
+ */
+ public static int convertFrequencyToMinutes(String frequency) {
+ // parse the frequency
+ frequency = frequency.toLowerCase();
+ int mins = 60;
+ if (!StringUtils.isEmpty(frequency)) {
+ try {
+ String str = frequency.trim();
+ if (frequency.indexOf(' ') > -1) {
+ str = str.substring(0, str.indexOf(' ')).trim();
+ }
+ mins = (int) Float.parseFloat(str);
+ } catch (NumberFormatException e) {
+ }
+ if (mins < 5) {
+ mins = 5;
+ }
+ }
+ if (frequency.indexOf("day") > -1) {
+ // convert to minutes
+ mins *= 1440;
+ } else if (frequency.indexOf("hour") > -1) {
+ // convert to minutes
+ mins *= 60;
+ }
+ return mins;
+ }
+}
diff --git a/src/main/java/com/gitblit/utils/X509Utils.java b/src/main/java/com/gitblit/utils/X509Utils.java
new file mode 100644
index 00000000..237c8dad
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/X509Utils.java
@@ -0,0 +1,1136 @@
+/*
+ * Copyright 2012 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.utils;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Field;
+import java.math.BigInteger;
+import java.security.InvalidKeyException;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.KeyStore;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.SecureRandom;
+import java.security.Security;
+import java.security.SignatureException;
+import java.security.cert.CertPathBuilder;
+import java.security.cert.CertPathBuilderException;
+import java.security.cert.CertStore;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateFactory;
+import java.security.cert.CollectionCertStoreParameters;
+import java.security.cert.PKIXBuilderParameters;
+import java.security.cert.PKIXCertPathBuilderResult;
+import java.security.cert.TrustAnchor;
+import java.security.cert.X509CRL;
+import java.security.cert.X509CertSelector;
+import java.security.cert.X509Certificate;
+import java.text.MessageFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TimeZone;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+import javax.crypto.Cipher;
+
+import org.bouncycastle.asn1.ASN1ObjectIdentifier;
+import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.asn1.x500.X500NameBuilder;
+import org.bouncycastle.asn1.x500.style.BCStyle;
+import org.bouncycastle.asn1.x509.BasicConstraints;
+import org.bouncycastle.asn1.x509.GeneralName;
+import org.bouncycastle.asn1.x509.GeneralNames;
+import org.bouncycastle.asn1.x509.KeyUsage;
+import org.bouncycastle.asn1.x509.X509Extension;
+import org.bouncycastle.cert.X509CRLHolder;
+import org.bouncycastle.cert.X509v2CRLBuilder;
+import org.bouncycastle.cert.X509v3CertificateBuilder;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
+import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
+import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
+import org.bouncycastle.jce.PrincipalUtil;
+import org.bouncycastle.jce.interfaces.PKCS12BagAttributeCarrier;
+import org.bouncycastle.openssl.PEMWriter;
+import org.bouncycastle.operator.ContentSigner;
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.Constants;
+
+/**
+ * Utility class to generate X509 certificates, keystores, and truststores.
+ *
+ * @author James Moger
+ *
+ */
+public class X509Utils {
+
+ public static final String SERVER_KEY_STORE = "serverKeyStore.jks";
+
+ public static final String SERVER_TRUST_STORE = "serverTrustStore.jks";
+
+ public static final String CERTS = "certs";
+
+ public static final String CA_KEY_STORE = "certs/caKeyStore.p12";
+
+ public static final String CA_REVOCATION_LIST = "certs/caRevocationList.crl";
+
+ public static final String CA_CONFIG = "certs/authority.conf";
+
+ public static final String CA_CN = "Gitblit Certificate Authority";
+
+ public static final String CA_ALIAS = CA_CN;
+
+ private static final String BC = org.bouncycastle.jce.provider.BouncyCastleProvider.PROVIDER_NAME;
+
+ private static final int KEY_LENGTH = 2048;
+
+ private static final String KEY_ALGORITHM = "RSA";
+
+ private static final String SIGNING_ALGORITHM = "SHA512withRSA";
+
+ public static final boolean unlimitedStrength;
+
+ private static final Logger logger = LoggerFactory.getLogger(X509Utils.class);
+
+ static {
+ Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
+
+ // check for JCE Unlimited Strength
+ int maxKeyLen = 0;
+ try {
+ maxKeyLen = Cipher.getMaxAllowedKeyLength("AES");
+ } catch (NoSuchAlgorithmException e) {
+ }
+
+ unlimitedStrength = maxKeyLen > 128;
+ if (unlimitedStrength) {
+ logger.info("Using JCE Unlimited Strength Jurisdiction Policy files");
+ } else {
+ logger.info("Using JCE Standard Encryption Policy files, encryption key lengths will be limited");
+ }
+ }
+
+ public static enum RevocationReason {
+ // https://en.wikipedia.org/wiki/Revocation_list
+ unspecified, keyCompromise, caCompromise, affiliationChanged, superseded,
+ cessationOfOperation, certificateHold, unused, removeFromCRL, privilegeWithdrawn,
+ ACompromise;
+
+ public static RevocationReason [] reasons = {
+ unspecified, keyCompromise, caCompromise,
+ affiliationChanged, superseded, cessationOfOperation,
+ privilegeWithdrawn };
+
+ @Override
+ public String toString() {
+ return name() + " (" + ordinal() + ")";
+ }
+ }
+
+ public interface X509Log {
+ void log(String message);
+ }
+
+ public static class X509Metadata {
+
+ // map for distinguished name OIDs
+ public final Map<String, String> oids;
+
+ // CN in distingiushed name
+ public final String commonName;
+
+ // password for store
+ public final String password;
+
+ // password hint for README in bundle
+ public String passwordHint;
+
+ // E or EMAILADDRESS in distinguished name
+ public String emailAddress;
+
+ // start date of generated certificate
+ public Date notBefore;
+
+ // expiraiton date of generated certificate
+ public Date notAfter;
+
+ // hostname of server for which certificate is generated
+ public String serverHostname;
+
+ // displayname of user for README in bundle
+ public String userDisplayname;
+
+ // serialnumber of generated or read certificate
+ public String serialNumber;
+
+ public X509Metadata(String cn, String pwd) {
+ if (StringUtils.isEmpty(cn)) {
+ throw new RuntimeException("Common name required!");
+ }
+ if (StringUtils.isEmpty(pwd)) {
+ throw new RuntimeException("Password required!");
+ }
+
+ commonName = cn;
+ password = pwd;
+ Calendar c = Calendar.getInstance(TimeZone.getDefault());
+ c.set(Calendar.SECOND, 0);
+ c.set(Calendar.MILLISECOND, 0);
+ notBefore = c.getTime();
+ c.add(Calendar.YEAR, 1);
+ c.add(Calendar.DATE, 1);
+ notAfter = c.getTime();
+ oids = new HashMap<String, String>();
+ }
+
+ public X509Metadata clone(String commonName, String password) {
+ X509Metadata clone = new X509Metadata(commonName, password);
+ clone.emailAddress = emailAddress;
+ clone.notBefore = notBefore;
+ clone.notAfter = notAfter;
+ clone.oids.putAll(oids);
+ clone.passwordHint = passwordHint;
+ clone.serverHostname = serverHostname;
+ clone.userDisplayname = userDisplayname;
+ return clone;
+ }
+
+ public String getOID(String oid, String defaultValue) {
+ if (oids.containsKey(oid)) {
+ return oids.get(oid);
+ }
+ return defaultValue;
+ }
+
+ public void setOID(String oid, String value) {
+ if (StringUtils.isEmpty(value)) {
+ oids.remove(oid);
+ } else {
+ oids.put(oid, value);
+ }
+ }
+ }
+
+ /**
+ * Prepare all the certificates and stores necessary for a Gitblit GO server.
+ *
+ * @param metadata
+ * @param folder
+ * @param x509log
+ */
+ public static void prepareX509Infrastructure(X509Metadata metadata, File folder, X509Log x509log) {
+ // make the specified folder, if necessary
+ folder.mkdirs();
+
+ // Gitblit CA certificate
+ File caKeyStore = new File(folder, CA_KEY_STORE);
+ if (!caKeyStore.exists()) {
+ logger.info(MessageFormat.format("Generating {0} ({1})", CA_CN, caKeyStore.getAbsolutePath()));
+ X509Certificate caCert = newCertificateAuthority(metadata, caKeyStore, x509log);
+ saveCertificate(caCert, new File(caKeyStore.getParentFile(), "ca.cer"));
+ }
+
+ // Gitblit CRL
+ File caRevocationList = new File(folder, CA_REVOCATION_LIST);
+ if (!caRevocationList.exists()) {
+ logger.info(MessageFormat.format("Generating {0} CRL ({1})", CA_CN, caRevocationList.getAbsolutePath()));
+ newCertificateRevocationList(caRevocationList, caKeyStore, metadata.password);
+ x509log.log("new certificate revocation list created");
+ }
+
+ // rename the old keystore to the new name
+ File oldKeyStore = new File(folder, "keystore");
+ if (oldKeyStore.exists()) {
+ oldKeyStore.renameTo(new File(folder, SERVER_KEY_STORE));
+ logger.info(MessageFormat.format("Renaming {0} to {1}", oldKeyStore.getName(), SERVER_KEY_STORE));
+ }
+
+ // create web SSL certificate signed by CA
+ File serverKeyStore = new File(folder, SERVER_KEY_STORE);
+ if (!serverKeyStore.exists()) {
+ logger.info(MessageFormat.format("Generating SSL certificate for {0} signed by {1} ({2})", metadata.commonName, CA_CN, serverKeyStore.getAbsolutePath()));
+ PrivateKey caPrivateKey = getPrivateKey(CA_ALIAS, caKeyStore, metadata.password);
+ X509Certificate caCert = getCertificate(CA_ALIAS, caKeyStore, metadata.password);
+ newSSLCertificate(metadata, caPrivateKey, caCert, serverKeyStore, x509log);
+ }
+
+ // server certificate trust store holds trusted public certificates
+ File serverTrustStore = new File(folder, X509Utils.SERVER_TRUST_STORE);
+ if (!serverTrustStore.exists()) {
+ logger.info(MessageFormat.format("Importing {0} into trust store ({1})", CA_ALIAS, serverTrustStore.getAbsolutePath()));
+ X509Certificate caCert = getCertificate(CA_ALIAS, caKeyStore, metadata.password);
+ addTrustedCertificate(CA_ALIAS, caCert, serverTrustStore, metadata.password);
+ }
+ }
+
+ /**
+ * Open a keystore. Store type is determined by file extension of name. If
+ * undetermined, JKS is assumed. The keystore does not need to exist.
+ *
+ * @param storeFile
+ * @param storePassword
+ * @return a KeyStore
+ */
+ public static KeyStore openKeyStore(File storeFile, String storePassword) {
+ String lc = storeFile.getName().toLowerCase();
+ String type = "JKS";
+ String provider = null;
+ if (lc.endsWith(".p12") || lc.endsWith(".pfx")) {
+ type = "PKCS12";
+ provider = BC;
+ }
+
+ try {
+ KeyStore store;
+ if (provider == null) {
+ store = KeyStore.getInstance(type);
+ } else {
+ store = KeyStore.getInstance(type, provider);
+ }
+ if (storeFile.exists()) {
+ FileInputStream fis = null;
+ try {
+ fis = new FileInputStream(storeFile);
+ store.load(fis, storePassword.toCharArray());
+ } finally {
+ if (fis != null) {
+ fis.close();
+ }
+ }
+ } else {
+ store.load(null);
+ }
+ return store;
+ } catch (Exception e) {
+ throw new RuntimeException("Could not open keystore " + storeFile, e);
+ }
+ }
+
+ /**
+ * Saves the keystore to the specified file.
+ *
+ * @param targetStoreFile
+ * @param store
+ * @param password
+ */
+ public static void saveKeyStore(File targetStoreFile, KeyStore store, String password) {
+ File folder = targetStoreFile.getAbsoluteFile().getParentFile();
+ if (!folder.exists()) {
+ folder.mkdirs();
+ }
+ File tmpFile = new File(folder, Long.toHexString(System.currentTimeMillis()) + ".tmp");
+ FileOutputStream fos = null;
+ try {
+ fos = new FileOutputStream(tmpFile);
+ store.store(fos, password.toCharArray());
+ fos.flush();
+ fos.close();
+ if (targetStoreFile.exists()) {
+ targetStoreFile.delete();
+ }
+ tmpFile.renameTo(targetStoreFile);
+ } catch (IOException e) {
+ String message = e.getMessage().toLowerCase();
+ if (message.contains("illegal key size")) {
+ throw new RuntimeException("Illegal Key Size! You might consider installing the JCE Unlimited Strength Jurisdiction Policy files for your JVM.");
+ } else {
+ throw new RuntimeException("Could not save keystore " + targetStoreFile, e);
+ }
+ } catch (Exception e) {
+ throw new RuntimeException("Could not save keystore " + targetStoreFile, e);
+ } finally {
+ if (fos != null) {
+ try {
+ fos.close();
+ } catch (IOException e) {
+ }
+ }
+
+ if (tmpFile.exists()) {
+ tmpFile.delete();
+ }
+ }
+ }
+
+ /**
+ * Retrieves the X509 certificate with the specified alias from the certificate
+ * store.
+ *
+ * @param alias
+ * @param storeFile
+ * @param storePassword
+ * @return the certificate
+ */
+ public static X509Certificate getCertificate(String alias, File storeFile, String storePassword) {
+ try {
+ KeyStore store = openKeyStore(storeFile, storePassword);
+ X509Certificate caCert = (X509Certificate) store.getCertificate(alias);
+ return caCert;
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Retrieves the private key for the specified alias from the certificate
+ * store.
+ *
+ * @param alias
+ * @param storeFile
+ * @param storePassword
+ * @return the private key
+ */
+ public static PrivateKey getPrivateKey(String alias, File storeFile, String storePassword) {
+ try {
+ KeyStore store = openKeyStore(storeFile, storePassword);
+ PrivateKey key = (PrivateKey) store.getKey(alias, storePassword.toCharArray());
+ return key;
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Saves the certificate to the file system. If the destination filename
+ * ends with the pem extension, the certificate is written in the PEM format,
+ * otherwise the certificate is written in the DER format.
+ *
+ * @param cert
+ * @param targetFile
+ */
+ public static void saveCertificate(X509Certificate cert, File targetFile) {
+ File folder = targetFile.getAbsoluteFile().getParentFile();
+ if (!folder.exists()) {
+ folder.mkdirs();
+ }
+ File tmpFile = new File(folder, Long.toHexString(System.currentTimeMillis()) + ".tmp");
+ try {
+ boolean asPem = targetFile.getName().toLowerCase().endsWith(".pem");
+ if (asPem) {
+ // PEM encoded X509
+ PEMWriter pemWriter = null;
+ try {
+ pemWriter = new PEMWriter(new FileWriter(tmpFile));
+ pemWriter.writeObject(cert);
+ pemWriter.flush();
+ } finally {
+ if (pemWriter != null) {
+ pemWriter.close();
+ }
+ }
+ } else {
+ // DER encoded X509
+ FileOutputStream fos = null;
+ try {
+ fos = new FileOutputStream(tmpFile);
+ fos.write(cert.getEncoded());
+ fos.flush();
+ } finally {
+ if (fos != null) {
+ fos.close();
+ }
+ }
+ }
+
+ // rename tmp file to target
+ if (targetFile.exists()) {
+ targetFile.delete();
+ }
+ tmpFile.renameTo(targetFile);
+ } catch (Exception e) {
+ if (tmpFile.exists()) {
+ tmpFile.delete();
+ }
+ throw new RuntimeException("Failed to save certificate " + cert.getSubjectX500Principal().getName(), e);
+ }
+ }
+
+ /**
+ * Generate a new keypair.
+ *
+ * @return a keypair
+ * @throws Exception
+ */
+ private static KeyPair newKeyPair() throws Exception {
+ KeyPairGenerator kpGen = KeyPairGenerator.getInstance(KEY_ALGORITHM, BC);
+ kpGen.initialize(KEY_LENGTH, new SecureRandom());
+ return kpGen.generateKeyPair();
+ }
+
+ /**
+ * Builds a distinguished name from the X509Metadata.
+ *
+ * @return a DN
+ */
+ private static X500Name buildDistinguishedName(X509Metadata metadata) {
+ X500NameBuilder dnBuilder = new X500NameBuilder(BCStyle.INSTANCE);
+ setOID(dnBuilder, metadata, "C", null);
+ setOID(dnBuilder, metadata, "ST", null);
+ setOID(dnBuilder, metadata, "L", null);
+ setOID(dnBuilder, metadata, "O", Constants.NAME);
+ setOID(dnBuilder, metadata, "OU", Constants.NAME);
+ setOID(dnBuilder, metadata, "E", metadata.emailAddress);
+ setOID(dnBuilder, metadata, "CN", metadata.commonName);
+ X500Name dn = dnBuilder.build();
+ return dn;
+ }
+
+ private static void setOID(X500NameBuilder dnBuilder, X509Metadata metadata,
+ String oid, String defaultValue) {
+
+ String value = null;
+ if (metadata.oids != null && metadata.oids.containsKey(oid)) {
+ value = metadata.oids.get(oid);
+ }
+ if (StringUtils.isEmpty(value)) {
+ value = defaultValue;
+ }
+
+ if (!StringUtils.isEmpty(value)) {
+ try {
+ Field field = BCStyle.class.getField(oid);
+ ASN1ObjectIdentifier objectId = (ASN1ObjectIdentifier) field.get(null);
+ dnBuilder.addRDN(objectId, value);
+ } catch (Exception e) {
+ logger.error(MessageFormat.format("Failed to set OID \"{0}\"!", oid) ,e);
+ }
+ }
+ }
+
+ /**
+ * Creates a new SSL certificate signed by the CA private key and stored in
+ * keyStore.
+ *
+ * @param sslMetadata
+ * @param caPrivateKey
+ * @param caCert
+ * @param targetStoreFile
+ * @param x509log
+ */
+ public static X509Certificate newSSLCertificate(X509Metadata sslMetadata, PrivateKey caPrivateKey, X509Certificate caCert, File targetStoreFile, X509Log x509log) {
+ try {
+ KeyPair pair = newKeyPair();
+
+ X500Name webDN = buildDistinguishedName(sslMetadata);
+ X500Name issuerDN = new X500Name(PrincipalUtil.getIssuerX509Principal(caCert).getName());
+
+ X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(
+ issuerDN,
+ BigInteger.valueOf(System.currentTimeMillis()),
+ sslMetadata.notBefore,
+ sslMetadata.notAfter,
+ webDN,
+ pair.getPublic());
+
+ JcaX509ExtensionUtils extUtils = new JcaX509ExtensionUtils();
+ certBuilder.addExtension(X509Extension.subjectKeyIdentifier, false, extUtils.createSubjectKeyIdentifier(pair.getPublic()));
+ certBuilder.addExtension(X509Extension.basicConstraints, false, new BasicConstraints(false));
+ certBuilder.addExtension(X509Extension.authorityKeyIdentifier, false, extUtils.createAuthorityKeyIdentifier(caCert.getPublicKey()));
+
+ // support alternateSubjectNames for SSL certificates
+ List<GeneralName> altNames = new ArrayList<GeneralName>();
+ if (HttpUtils.isIpAddress(sslMetadata.commonName)) {
+ altNames.add(new GeneralName(GeneralName.iPAddress, sslMetadata.commonName));
+ }
+ if (altNames.size() > 0) {
+ GeneralNames subjectAltName = new GeneralNames(altNames.toArray(new GeneralName [altNames.size()]));
+ certBuilder.addExtension(X509Extension.subjectAlternativeName, false, subjectAltName);
+ }
+
+ ContentSigner caSigner = new JcaContentSignerBuilder(SIGNING_ALGORITHM)
+ .setProvider(BC).build(caPrivateKey);
+ X509Certificate cert = new JcaX509CertificateConverter().setProvider(BC)
+ .getCertificate(certBuilder.build(caSigner));
+
+ cert.checkValidity(new Date());
+ cert.verify(caCert.getPublicKey());
+
+ // Save to keystore
+ KeyStore serverStore = openKeyStore(targetStoreFile, sslMetadata.password);
+ serverStore.setKeyEntry(sslMetadata.commonName, pair.getPrivate(), sslMetadata.password.toCharArray(),
+ new Certificate[] { cert, caCert });
+ saveKeyStore(targetStoreFile, serverStore, sslMetadata.password);
+
+ x509log.log(MessageFormat.format("New SSL certificate {0,number,0} [{1}]", cert.getSerialNumber(), cert.getSubjectDN().getName()));
+
+ // update serial number in metadata object
+ sslMetadata.serialNumber = cert.getSerialNumber().toString();
+
+ return cert;
+ } catch (Throwable t) {
+ throw new RuntimeException("Failed to generate SSL certificate!", t);
+ }
+ }
+
+ /**
+ * Creates a new certificate authority PKCS#12 store. This function will
+ * destroy any existing CA store.
+ *
+ * @param metadata
+ * @param storeFile
+ * @param keystorePassword
+ * @param x509log
+ * @return
+ */
+ public static X509Certificate newCertificateAuthority(X509Metadata metadata, File storeFile, X509Log x509log) {
+ try {
+ KeyPair caPair = newKeyPair();
+
+ ContentSigner caSigner = new JcaContentSignerBuilder(SIGNING_ALGORITHM).setProvider(BC).build(caPair.getPrivate());
+
+ // clone metadata
+ X509Metadata caMetadata = metadata.clone(CA_CN, metadata.password);
+ X500Name issuerDN = buildDistinguishedName(caMetadata);
+
+ // Generate self-signed certificate
+ X509v3CertificateBuilder caBuilder = new JcaX509v3CertificateBuilder(
+ issuerDN,
+ BigInteger.valueOf(System.currentTimeMillis()),
+ caMetadata.notBefore,
+ caMetadata.notAfter,
+ issuerDN,
+ caPair.getPublic());
+
+ JcaX509ExtensionUtils extUtils = new JcaX509ExtensionUtils();
+ caBuilder.addExtension(X509Extension.subjectKeyIdentifier, false, extUtils.createSubjectKeyIdentifier(caPair.getPublic()));
+ caBuilder.addExtension(X509Extension.authorityKeyIdentifier, false, extUtils.createAuthorityKeyIdentifier(caPair.getPublic()));
+ caBuilder.addExtension(X509Extension.basicConstraints, false, new BasicConstraints(true));
+ caBuilder.addExtension(X509Extension.keyUsage, true, new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyCertSign | KeyUsage.cRLSign));
+
+ JcaX509CertificateConverter converter = new JcaX509CertificateConverter().setProvider(BC);
+ X509Certificate cert = converter.getCertificate(caBuilder.build(caSigner));
+
+ // confirm the validity of the CA certificate
+ cert.checkValidity(new Date());
+ cert.verify(cert.getPublicKey());
+
+ // Delete existing keystore
+ if (storeFile.exists()) {
+ storeFile.delete();
+ }
+
+ // Save private key and certificate to new keystore
+ KeyStore store = openKeyStore(storeFile, caMetadata.password);
+ store.setKeyEntry(CA_ALIAS, caPair.getPrivate(), caMetadata.password.toCharArray(),
+ new Certificate[] { cert });
+ saveKeyStore(storeFile, store, caMetadata.password);
+
+ x509log.log(MessageFormat.format("New CA certificate {0,number,0} [{1}]", cert.getSerialNumber(), cert.getIssuerDN().getName()));
+
+ // update serial number in metadata object
+ caMetadata.serialNumber = cert.getSerialNumber().toString();
+
+ return cert;
+ } catch (Throwable t) {
+ throw new RuntimeException("Failed to generate Gitblit CA certificate!", t);
+ }
+ }
+
+ /**
+ * Creates a new certificate revocation list (CRL). This function will
+ * destroy any existing CRL file.
+ *
+ * @param caRevocationList
+ * @param storeFile
+ * @param keystorePassword
+ * @return
+ */
+ public static void newCertificateRevocationList(File caRevocationList, File caKeystoreFile, String caKeystorePassword) {
+ try {
+ // read the Gitblit CA key and certificate
+ KeyStore store = openKeyStore(caKeystoreFile, caKeystorePassword);
+ PrivateKey caPrivateKey = (PrivateKey) store.getKey(CA_ALIAS, caKeystorePassword.toCharArray());
+ X509Certificate caCert = (X509Certificate) store.getCertificate(CA_ALIAS);
+
+ X500Name issuerDN = new X500Name(PrincipalUtil.getIssuerX509Principal(caCert).getName());
+ X509v2CRLBuilder crlBuilder = new X509v2CRLBuilder(issuerDN, new Date());
+
+ // build and sign CRL with CA private key
+ ContentSigner signer = new JcaContentSignerBuilder(SIGNING_ALGORITHM).setProvider(BC).build(caPrivateKey);
+ X509CRLHolder crl = crlBuilder.build(signer);
+
+ File tmpFile = new File(caRevocationList.getParentFile(), Long.toHexString(System.currentTimeMillis()) + ".tmp");
+ FileOutputStream fos = null;
+ try {
+ fos = new FileOutputStream(tmpFile);
+ fos.write(crl.getEncoded());
+ fos.flush();
+ fos.close();
+ if (caRevocationList.exists()) {
+ caRevocationList.delete();
+ }
+ tmpFile.renameTo(caRevocationList);
+ } finally {
+ if (fos != null) {
+ fos.close();
+ }
+ if (tmpFile.exists()) {
+ tmpFile.delete();
+ }
+ }
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to create new certificate revocation list " + caRevocationList, e);
+ }
+ }
+
+ /**
+ * Imports a certificate into the trust store.
+ *
+ * @param alias
+ * @param cert
+ * @param storeFile
+ * @param storePassword
+ */
+ public static void addTrustedCertificate(String alias, X509Certificate cert, File storeFile, String storePassword) {
+ try {
+ KeyStore store = openKeyStore(storeFile, storePassword);
+ store.setCertificateEntry(alias, cert);
+ saveKeyStore(storeFile, store, storePassword);
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to import certificate into trust store " + storeFile, e);
+ }
+ }
+
+ /**
+ * Creates a new client certificate PKCS#12 and PEM store. Any existing
+ * stores are destroyed. After generation, the certificates are bundled
+ * into a zip file with a personalized README file.
+ *
+ * The zip file reference is returned.
+ *
+ * @param clientMetadata a container for dynamic parameters needed for generation
+ * @param caKeystoreFile
+ * @param caKeystorePassword
+ * @param x509log
+ * @return a zip file containing the P12, PEM, and personalized README
+ */
+ public static File newClientBundle(X509Metadata clientMetadata, File caKeystoreFile,
+ String caKeystorePassword, X509Log x509log) {
+ try {
+ // read the Gitblit CA key and certificate
+ KeyStore store = openKeyStore(caKeystoreFile, caKeystorePassword);
+ PrivateKey caPrivateKey = (PrivateKey) store.getKey(CA_ALIAS, caKeystorePassword.toCharArray());
+ X509Certificate caCert = (X509Certificate) store.getCertificate(CA_ALIAS);
+
+ // generate the P12 and PEM files
+ File targetFolder = new File(caKeystoreFile.getParentFile(), clientMetadata.commonName);
+ X509Certificate cert = newClientCertificate(clientMetadata, caPrivateKey, caCert, targetFolder);
+ x509log.log(MessageFormat.format("New client certificate {0,number,0} [{1}]", cert.getSerialNumber(), cert.getSubjectDN().getName()));
+
+ // process template message
+ String readme = processTemplate(new File(caKeystoreFile.getParentFile(), "instructions.tmpl"), clientMetadata);
+
+ // Create a zip bundle with the p12, pem, and a personalized readme
+ File zipFile = new File(targetFolder, clientMetadata.commonName + ".zip");
+ if (zipFile.exists()) {
+ zipFile.delete();
+ }
+ ZipOutputStream zos = null;
+ try {
+ zos = new ZipOutputStream(new FileOutputStream(zipFile));
+ File p12File = new File(targetFolder, clientMetadata.commonName + ".p12");
+ if (p12File.exists()) {
+ zos.putNextEntry(new ZipEntry(p12File.getName()));
+ zos.write(FileUtils.readContent(p12File));
+ zos.closeEntry();
+ }
+ File pemFile = new File(targetFolder, clientMetadata.commonName + ".pem");
+ if (pemFile.exists()) {
+ zos.putNextEntry(new ZipEntry(pemFile.getName()));
+ zos.write(FileUtils.readContent(pemFile));
+ zos.closeEntry();
+ }
+
+ // include user's public certificate
+ zos.putNextEntry(new ZipEntry(clientMetadata.commonName + ".cer"));
+ zos.write(cert.getEncoded());
+ zos.closeEntry();
+
+ // include CA public certificate
+ zos.putNextEntry(new ZipEntry("ca.cer"));
+ zos.write(caCert.getEncoded());
+ zos.closeEntry();
+
+ if (readme != null) {
+ zos.putNextEntry(new ZipEntry("README.TXT"));
+ zos.write(readme.getBytes("UTF-8"));
+ zos.closeEntry();
+ }
+ zos.flush();
+ } finally {
+ if (zos != null) {
+ zos.close();
+ }
+ }
+
+ return zipFile;
+ } catch (Throwable t) {
+ throw new RuntimeException("Failed to generate client bundle!", t);
+ }
+ }
+
+ /**
+ * Creates a new client certificate PKCS#12 and PEM store. Any existing
+ * stores are destroyed.
+ *
+ * @param clientMetadata a container for dynamic parameters needed for generation
+ * @param caKeystoreFile
+ * @param caKeystorePassword
+ * @param targetFolder
+ * @return
+ */
+ public static X509Certificate newClientCertificate(X509Metadata clientMetadata,
+ PrivateKey caPrivateKey, X509Certificate caCert, File targetFolder) {
+ try {
+ KeyPair pair = newKeyPair();
+
+ X500Name userDN = buildDistinguishedName(clientMetadata);
+ X500Name issuerDN = new X500Name(PrincipalUtil.getIssuerX509Principal(caCert).getName());
+
+ // create a new certificate signed by the Gitblit CA certificate
+ X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(
+ issuerDN,
+ BigInteger.valueOf(System.currentTimeMillis()),
+ clientMetadata.notBefore,
+ clientMetadata.notAfter,
+ userDN,
+ pair.getPublic());
+
+ JcaX509ExtensionUtils extUtils = new JcaX509ExtensionUtils();
+ certBuilder.addExtension(X509Extension.subjectKeyIdentifier, false, extUtils.createSubjectKeyIdentifier(pair.getPublic()));
+ certBuilder.addExtension(X509Extension.basicConstraints, false, new BasicConstraints(false));
+ certBuilder.addExtension(X509Extension.authorityKeyIdentifier, false, extUtils.createAuthorityKeyIdentifier(caCert.getPublicKey()));
+ certBuilder.addExtension(X509Extension.keyUsage, true, new KeyUsage(KeyUsage.keyEncipherment | KeyUsage.digitalSignature));
+ if (!StringUtils.isEmpty(clientMetadata.emailAddress)) {
+ GeneralNames subjectAltName = new GeneralNames(
+ new GeneralName(GeneralName.rfc822Name, clientMetadata.emailAddress));
+ certBuilder.addExtension(X509Extension.subjectAlternativeName, false, subjectAltName);
+ }
+
+ ContentSigner signer = new JcaContentSignerBuilder(SIGNING_ALGORITHM).setProvider(BC).build(caPrivateKey);
+
+ X509Certificate userCert = new JcaX509CertificateConverter().setProvider(BC).getCertificate(certBuilder.build(signer));
+ PKCS12BagAttributeCarrier bagAttr = (PKCS12BagAttributeCarrier)pair.getPrivate();
+ bagAttr.setBagAttribute(PKCSObjectIdentifiers.pkcs_9_at_localKeyId,
+ extUtils.createSubjectKeyIdentifier(pair.getPublic()));
+
+ // confirm the validity of the user certificate
+ userCert.checkValidity();
+ userCert.verify(caCert.getPublicKey());
+ userCert.getIssuerDN().equals(caCert.getSubjectDN());
+
+ // verify user certificate chain
+ verifyChain(userCert, caCert);
+
+ targetFolder.mkdirs();
+
+ // save certificate, stamped with unique name
+ String date = new SimpleDateFormat("yyyyMMdd").format(new Date());
+ String id = date;
+ File certFile = new File(targetFolder, id + ".cer");
+ int count = 0;
+ while (certFile.exists()) {
+ id = date + "_" + Character.toString((char) (0x61 + count));
+ certFile = new File(targetFolder, id + ".cer");
+ count++;
+ }
+
+ // save user private key, user certificate and CA certificate to a PKCS#12 store
+ File p12File = new File(targetFolder, clientMetadata.commonName + ".p12");
+ if (p12File.exists()) {
+ p12File.delete();
+ }
+ KeyStore userStore = openKeyStore(p12File, clientMetadata.password);
+ userStore.setKeyEntry(MessageFormat.format("Gitblit ({0}) {1} {2}", clientMetadata.serverHostname, clientMetadata.userDisplayname, id), pair.getPrivate(), null, new Certificate [] { userCert });
+ userStore.setCertificateEntry(MessageFormat.format("Gitblit ({0}) Certificate Authority", clientMetadata.serverHostname), caCert);
+ saveKeyStore(p12File, userStore, clientMetadata.password);
+
+ // save user private key, user certificate, and CA certificate to a PEM store
+ File pemFile = new File(targetFolder, clientMetadata.commonName + ".pem");
+ if (pemFile.exists()) {
+ pemFile.delete();
+ }
+ PEMWriter pemWriter = new PEMWriter(new FileWriter(pemFile));
+ pemWriter.writeObject(pair.getPrivate(), "DES-EDE3-CBC", clientMetadata.password.toCharArray(), new SecureRandom());
+ pemWriter.writeObject(userCert);
+ pemWriter.writeObject(caCert);
+ pemWriter.flush();
+ pemWriter.close();
+
+ // save certificate after successfully creating the key stores
+ saveCertificate(userCert, certFile);
+
+ // update serial number in metadata object
+ clientMetadata.serialNumber = userCert.getSerialNumber().toString();
+
+ return userCert;
+ } catch (Throwable t) {
+ throw new RuntimeException("Failed to generate client certificate!", t);
+ }
+ }
+
+ /**
+ * Verifies a certificate's chain to ensure that it will function properly.
+ *
+ * @param testCert
+ * @param additionalCerts
+ * @return
+ */
+ public static PKIXCertPathBuilderResult verifyChain(X509Certificate testCert, X509Certificate... additionalCerts) {
+ try {
+ // Check for self-signed certificate
+ if (isSelfSigned(testCert)) {
+ throw new RuntimeException("The certificate is self-signed. Nothing to verify.");
+ }
+
+ // Prepare a set of all certificates
+ // chain builder must have all certs, including cert to validate
+ // http://stackoverflow.com/a/10788392
+ Set<X509Certificate> certs = new HashSet<X509Certificate>();
+ certs.add(testCert);
+ certs.addAll(Arrays.asList(additionalCerts));
+
+ // Attempt to build the certification chain and verify it
+ // Create the selector that specifies the starting certificate
+ X509CertSelector selector = new X509CertSelector();
+ selector.setCertificate(testCert);
+
+ // Create the trust anchors (set of root CA certificates)
+ Set<TrustAnchor> trustAnchors = new HashSet<TrustAnchor>();
+ for (X509Certificate cert : additionalCerts) {
+ if (isSelfSigned(cert)) {
+ trustAnchors.add(new TrustAnchor(cert, null));
+ }
+ }
+
+ // Configure the PKIX certificate builder
+ PKIXBuilderParameters pkixParams = new PKIXBuilderParameters(trustAnchors, selector);
+ pkixParams.setRevocationEnabled(false);
+ pkixParams.addCertStore(CertStore.getInstance("Collection", new CollectionCertStoreParameters(certs), BC));
+
+ // Build and verify the certification chain
+ CertPathBuilder builder = CertPathBuilder.getInstance("PKIX", BC);
+ PKIXCertPathBuilderResult verifiedCertChain = (PKIXCertPathBuilderResult) builder.build(pkixParams);
+
+ // The chain is built and verified
+ return verifiedCertChain;
+ } catch (CertPathBuilderException e) {
+ throw new RuntimeException("Error building certification path: " + testCert.getSubjectX500Principal(), e);
+ } catch (Exception e) {
+ throw new RuntimeException("Error verifying the certificate: " + testCert.getSubjectX500Principal(), e);
+ }
+ }
+
+ /**
+ * Checks whether given X.509 certificate is self-signed.
+ *
+ * @param cert
+ * @return true if the certificate is self-signed
+ */
+ public static boolean isSelfSigned(X509Certificate cert) {
+ try {
+ cert.verify(cert.getPublicKey());
+ return true;
+ } catch (SignatureException e) {
+ return false;
+ } catch (InvalidKeyException e) {
+ return false;
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static String processTemplate(File template, X509Metadata metadata) {
+ String content = null;
+ if (template.exists()) {
+ String message = FileUtils.readContent(template, "\n");
+ if (!StringUtils.isEmpty(message)) {
+ content = message;
+ if (!StringUtils.isEmpty(metadata.serverHostname)) {
+ content = content.replace("$serverHostname", metadata.serverHostname);
+ }
+ if (!StringUtils.isEmpty(metadata.commonName)) {
+ content = content.replace("$username", metadata.commonName);
+ }
+ if (!StringUtils.isEmpty(metadata.userDisplayname)) {
+ content = content.replace("$userDisplayname", metadata.userDisplayname);
+ }
+ if (!StringUtils.isEmpty(metadata.passwordHint)) {
+ content = content.replace("$storePasswordHint", metadata.passwordHint);
+ }
+ }
+ }
+ return content;
+ }
+
+ /**
+ * Revoke a certificate.
+ *
+ * @param cert
+ * @param reason
+ * @param caRevocationList
+ * @param caKeystoreFile
+ * @param caKeystorePassword
+ * @param x509log
+ * @return true if the certificate has been revoked
+ */
+ public static boolean revoke(X509Certificate cert, RevocationReason reason,
+ File caRevocationList, File caKeystoreFile, String caKeystorePassword,
+ X509Log x509log) {
+ try {
+ // read the Gitblit CA key and certificate
+ KeyStore store = openKeyStore(caKeystoreFile, caKeystorePassword);
+ PrivateKey caPrivateKey = (PrivateKey) store.getKey(CA_ALIAS, caKeystorePassword.toCharArray());
+ return revoke(cert, reason, caRevocationList, caPrivateKey, x509log);
+ } catch (Exception e) {
+ logger.error(MessageFormat.format("Failed to revoke certificate {0,number,0} [{1}] in {2}",
+ cert.getSerialNumber(), cert.getSubjectDN().getName(), caRevocationList));
+ }
+ return false;
+ }
+
+ /**
+ * Revoke a certificate.
+ *
+ * @param cert
+ * @param reason
+ * @param caRevocationList
+ * @param caPrivateKey
+ * @param x509log
+ * @return true if the certificate has been revoked
+ */
+ public static boolean revoke(X509Certificate cert, RevocationReason reason,
+ File caRevocationList, PrivateKey caPrivateKey, X509Log x509log) {
+ try {
+ X500Name issuerDN = new X500Name(PrincipalUtil.getIssuerX509Principal(cert).getName());
+ X509v2CRLBuilder crlBuilder = new X509v2CRLBuilder(issuerDN, new Date());
+ if (caRevocationList.exists()) {
+ byte [] data = FileUtils.readContent(caRevocationList);
+ X509CRLHolder crl = new X509CRLHolder(data);
+ crlBuilder.addCRL(crl);
+ }
+ crlBuilder.addCRLEntry(cert.getSerialNumber(), new Date(), reason.ordinal());
+
+ // build and sign CRL with CA private key
+ ContentSigner signer = new JcaContentSignerBuilder("SHA1WithRSA").setProvider(BC).build(caPrivateKey);
+ X509CRLHolder crl = crlBuilder.build(signer);
+
+ File tmpFile = new File(caRevocationList.getParentFile(), Long.toHexString(System.currentTimeMillis()) + ".tmp");
+ FileOutputStream fos = null;
+ try {
+ fos = new FileOutputStream(tmpFile);
+ fos.write(crl.getEncoded());
+ fos.flush();
+ fos.close();
+ if (caRevocationList.exists()) {
+ caRevocationList.delete();
+ }
+ tmpFile.renameTo(caRevocationList);
+
+ } finally {
+ if (fos != null) {
+ fos.close();
+ }
+ if (tmpFile.exists()) {
+ tmpFile.delete();
+ }
+ }
+
+ x509log.log(MessageFormat.format("Revoked certificate {0,number,0} reason: {1} [{2}]",
+ cert.getSerialNumber(), reason.toString(), cert.getSubjectDN().getName()));
+ return true;
+ } catch (Exception e) {
+ logger.error(MessageFormat.format("Failed to revoke certificate {0,number,0} [{1}] in {2}",
+ cert.getSerialNumber(), cert.getSubjectDN().getName(), caRevocationList));
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if the certificate has been revoked.
+ *
+ * @param cert
+ * @param caRevocationList
+ * @return true if the certificate is revoked
+ */
+ public static boolean isRevoked(X509Certificate cert, File caRevocationList) {
+ if (!caRevocationList.exists()) {
+ return false;
+ }
+ InputStream inStream = null;
+ try {
+ inStream = new FileInputStream(caRevocationList);
+ CertificateFactory cf = CertificateFactory.getInstance("X.509");
+ X509CRL crl = (X509CRL)cf.generateCRL(inStream);
+ return crl.isRevoked(cert);
+ } catch (Exception e) {
+ logger.error(MessageFormat.format("Failed to check revocation status for certificate {0,number,0} [{1}] in {2}",
+ cert.getSerialNumber(), cert.getSubjectDN().getName(), caRevocationList));
+ } finally {
+ if (inStream != null) {
+ try {
+ inStream.close();
+ } catch (Exception e) {
+ }
+ }
+ }
+ return false;
+ }
+
+ public static X509Metadata getMetadata(X509Certificate cert) {
+ // manually split DN into OID components
+ // this is instead of parsing with LdapName which:
+ // (1) I don't trust the order of values
+ // (2) it filters out values like EMAILADDRESS
+ String dn = cert.getSubjectDN().getName();
+ Map<String, String> oids = new HashMap<String, String>();
+ for (String kvp : dn.split(",")) {
+ String [] val = kvp.trim().split("=");
+ String oid = val[0].toUpperCase().trim();
+ String data = val[1].trim();
+ oids.put(oid, data);
+ }
+
+ X509Metadata metadata = new X509Metadata(oids.get("CN"), "whocares");
+ metadata.oids.putAll(oids);
+ metadata.serialNumber = cert.getSerialNumber().toString();
+ metadata.notAfter = cert.getNotAfter();
+ metadata.notBefore = cert.getNotBefore();
+ metadata.emailAddress = metadata.getOID("E", null);
+ if (metadata.emailAddress == null) {
+ metadata.emailAddress = metadata.getOID("EMAILADDRESS", null);
+ }
+ return metadata;
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/AuthorizationStrategy.java b/src/main/java/com/gitblit/wicket/AuthorizationStrategy.java
new file mode 100644
index 00000000..765d8608
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/AuthorizationStrategy.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket;
+
+import org.apache.wicket.Component;
+import org.apache.wicket.RestartResponseException;
+import org.apache.wicket.authorization.IUnauthorizedComponentInstantiationListener;
+import org.apache.wicket.authorization.strategies.page.AbstractPageAuthorizationStrategy;
+
+import com.gitblit.GitBlit;
+import com.gitblit.Keys;
+import com.gitblit.models.UserModel;
+import com.gitblit.wicket.pages.BasePage;
+import com.gitblit.wicket.pages.RepositoriesPage;
+
+public class AuthorizationStrategy extends AbstractPageAuthorizationStrategy implements
+ IUnauthorizedComponentInstantiationListener {
+
+ public AuthorizationStrategy() {
+ }
+
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ @Override
+ protected boolean isPageAuthorized(Class pageClass) {
+ if (RepositoriesPage.class.equals(pageClass)) {
+ // allow all requests to get to the RepositoriesPage with its inline
+ // authentication form
+ return true;
+ }
+
+ if (BasePage.class.isAssignableFrom(pageClass)) {
+ boolean authenticateView = GitBlit.getBoolean(Keys.web.authenticateViewPages, true);
+ boolean authenticateAdmin = GitBlit.getBoolean(Keys.web.authenticateAdminPages, true);
+ boolean allowAdmin = GitBlit.getBoolean(Keys.web.allowAdministration, true);
+
+ GitBlitWebSession session = GitBlitWebSession.get();
+ if (authenticateView && !session.isLoggedIn()) {
+ // authentication required
+ session.cacheRequest(pageClass);
+ return false;
+ }
+
+ UserModel user = session.getUser();
+ if (pageClass.isAnnotationPresent(RequiresAdminRole.class)) {
+ // admin page
+ if (allowAdmin) {
+ if (authenticateAdmin) {
+ // authenticate admin
+ if (user != null) {
+ return user.canAdmin();
+ }
+ return false;
+ } else {
+ // no admin authentication required
+ return true;
+ }
+ } else {
+ // admin prohibited
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public void onUnauthorizedInstantiation(Component component) {
+
+ if (component instanceof BasePage) {
+ throw new RestartResponseException(RepositoriesPage.class);
+ }
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/ExternalImage.java b/src/main/java/com/gitblit/wicket/ExternalImage.java
new file mode 100644
index 00000000..33257740
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/ExternalImage.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2012 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket;
+
+import org.apache.wicket.markup.ComponentTag;
+import org.apache.wicket.markup.html.WebComponent;
+import org.apache.wicket.model.Model;
+
+public class ExternalImage extends WebComponent {
+
+ private static final long serialVersionUID = 1L;
+
+ public ExternalImage(String id, String url) {
+ super(id, new Model<String>(url));
+ }
+
+ protected void onComponentTag(ComponentTag tag) {
+ super.onComponentTag(tag);
+ checkComponentTag(tag, "img");
+ tag.put("src", getDefaultModelObjectAsString());
+ }
+} \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/GitBlitWebApp.java b/src/main/java/com/gitblit/wicket/GitBlitWebApp.java
new file mode 100644
index 00000000..2300d0ff
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/GitBlitWebApp.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket;
+
+import org.apache.wicket.Application;
+import org.apache.wicket.Page;
+import org.apache.wicket.Request;
+import org.apache.wicket.Response;
+import org.apache.wicket.Session;
+import org.apache.wicket.markup.html.WebPage;
+import org.apache.wicket.protocol.http.WebApplication;
+
+import com.gitblit.GitBlit;
+import com.gitblit.Keys;
+import com.gitblit.wicket.pages.ActivityPage;
+import com.gitblit.wicket.pages.BlamePage;
+import com.gitblit.wicket.pages.BlobDiffPage;
+import com.gitblit.wicket.pages.BlobPage;
+import com.gitblit.wicket.pages.BranchesPage;
+import com.gitblit.wicket.pages.CommitDiffPage;
+import com.gitblit.wicket.pages.CommitPage;
+import com.gitblit.wicket.pages.DocsPage;
+import com.gitblit.wicket.pages.FederationRegistrationPage;
+import com.gitblit.wicket.pages.ForkPage;
+import com.gitblit.wicket.pages.ForksPage;
+import com.gitblit.wicket.pages.GitSearchPage;
+import com.gitblit.wicket.pages.GravatarProfilePage;
+import com.gitblit.wicket.pages.HistoryPage;
+import com.gitblit.wicket.pages.LogPage;
+import com.gitblit.wicket.pages.LogoutPage;
+import com.gitblit.wicket.pages.LuceneSearchPage;
+import com.gitblit.wicket.pages.MarkdownPage;
+import com.gitblit.wicket.pages.MetricsPage;
+import com.gitblit.wicket.pages.PatchPage;
+import com.gitblit.wicket.pages.ProjectPage;
+import com.gitblit.wicket.pages.ProjectsPage;
+import com.gitblit.wicket.pages.RawPage;
+import com.gitblit.wicket.pages.RepositoriesPage;
+import com.gitblit.wicket.pages.ReviewProposalPage;
+import com.gitblit.wicket.pages.SummaryPage;
+import com.gitblit.wicket.pages.TagPage;
+import com.gitblit.wicket.pages.TagsPage;
+import com.gitblit.wicket.pages.TicketPage;
+import com.gitblit.wicket.pages.TicketsPage;
+import com.gitblit.wicket.pages.TreePage;
+import com.gitblit.wicket.pages.UserPage;
+import com.gitblit.wicket.pages.UsersPage;
+
+public class GitBlitWebApp extends WebApplication {
+
+ @Override
+ public void init() {
+ super.init();
+
+ // Setup page authorization mechanism
+ boolean useAuthentication = GitBlit.getBoolean(Keys.web.authenticateViewPages, false)
+ || GitBlit.getBoolean(Keys.web.authenticateAdminPages, false);
+ if (useAuthentication) {
+ AuthorizationStrategy authStrategy = new AuthorizationStrategy();
+ getSecuritySettings().setAuthorizationStrategy(authStrategy);
+ getSecuritySettings().setUnauthorizedComponentInstantiationListener(authStrategy);
+ }
+
+ // Grab Browser info (like timezone, etc)
+ if (GitBlit.getBoolean(Keys.web.useClientTimezone, false)) {
+ getRequestCycleSettings().setGatherExtendedBrowserInfo(true);
+ }
+
+ // configure the resource cache duration to 90 days for deployment
+ if (!GitBlit.isDebugMode()) {
+ getResourceSettings().setDefaultCacheDuration(90 * 86400);
+ }
+
+ // setup the standard gitweb-ish urls
+ mount("/summary", SummaryPage.class, "r");
+ mount("/log", LogPage.class, "r", "h");
+ mount("/tags", TagsPage.class, "r");
+ mount("/branches", BranchesPage.class, "r");
+ mount("/commit", CommitPage.class, "r", "h");
+ mount("/tag", TagPage.class, "r", "h");
+ mount("/tree", TreePage.class, "r", "h", "f");
+ mount("/blob", BlobPage.class, "r", "h", "f");
+ mount("/raw", RawPage.class, "r", "h", "f");
+ mount("/blobdiff", BlobDiffPage.class, "r", "h", "f");
+ mount("/commitdiff", CommitDiffPage.class, "r", "h");
+ mount("/patch", PatchPage.class, "r", "h", "f");
+ mount("/history", HistoryPage.class, "r", "h", "f");
+ mount("/search", GitSearchPage.class);
+ mount("/metrics", MetricsPage.class, "r");
+ mount("/blame", BlamePage.class, "r", "h", "f");
+ mount("/users", UsersPage.class);
+ mount("/logout", LogoutPage.class);
+
+ // setup ticket urls
+ mount("/tickets", TicketsPage.class, "r");
+ mount("/ticket", TicketPage.class, "r", "f");
+
+ // setup the markdown urls
+ mount("/docs", DocsPage.class, "r");
+ mount("/markdown", MarkdownPage.class, "r", "h", "f");
+
+ // federation urls
+ mount("/proposal", ReviewProposalPage.class, "t");
+ mount("/registration", FederationRegistrationPage.class, "u", "n");
+
+ mount("/activity", ActivityPage.class, "r", "h");
+ mount("/gravatar", GravatarProfilePage.class, "h");
+ mount("/lucene", LuceneSearchPage.class);
+ mount("/project", ProjectPage.class, "p");
+ mount("/projects", ProjectsPage.class);
+ mount("/user", UserPage.class, "user");
+ mount("/forks", ForksPage.class, "r");
+ mount("/fork", ForkPage.class, "r");
+ }
+
+ private void mount(String location, Class<? extends WebPage> clazz, String... parameters) {
+ if (parameters == null) {
+ parameters = new String[] {};
+ }
+ if (!GitBlit.getBoolean(Keys.web.mountParameters, true)) {
+ parameters = new String[] {};
+ }
+ mount(new GitblitParamUrlCodingStrategy(location, clazz, parameters));
+ }
+
+ @Override
+ public Class<? extends Page> getHomePage() {
+ return RepositoriesPage.class;
+ }
+
+ @Override
+ public final Session newSession(Request request, Response response) {
+ return new GitBlitWebSession(request);
+ }
+
+ @Override
+ public final String getConfigurationType() {
+ if (GitBlit.isDebugMode()) {
+ return Application.DEVELOPMENT;
+ }
+ return Application.DEPLOYMENT;
+ }
+
+ public static GitBlitWebApp get() {
+ return (GitBlitWebApp) WebApplication.get();
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties b/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties
new file mode 100644
index 00000000..7a2b1bb3
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties
@@ -0,0 +1,447 @@
+gb.repository = repository
+gb.owner = owner
+gb.description = description
+gb.lastChange = last change
+gb.refs = refs
+gb.tag = tag
+gb.tags = tags
+gb.author = author
+gb.committer = committer
+gb.commit = commit
+gb.tree = tree
+gb.parent = parent
+gb.url = URL
+gb.history = history
+gb.raw = raw
+gb.object = object
+gb.ticketId = ticket id
+gb.ticketAssigned = assigned
+gb.ticketOpenDate = open date
+gb.ticketState = state
+gb.ticketComments = comments
+gb.view = view
+gb.local = local
+gb.remote = remote
+gb.branches = branches
+gb.patch = patch
+gb.diff = diff
+gb.log = log
+gb.moreLogs = more commits...
+gb.allTags = all tags...
+gb.allBranches = all branches...
+gb.summary = summary
+gb.ticket = ticket
+gb.newRepository = new repository
+gb.newUser = new user
+gb.commitdiff = commitdiff
+gb.tickets = tickets
+gb.pageFirst = first
+gb.pagePrevious prev
+gb.pageNext = next
+gb.head = HEAD
+gb.blame = blame
+gb.login = login
+gb.logout = logout
+gb.username = username
+gb.password = password
+gb.tagger = tagger
+gb.moreHistory = more history...
+gb.difftocurrent = diff to current
+gb.search = search
+gb.searchForAuthor = Search for commits authored by
+gb.searchForCommitter = Search for commits committed by
+gb.addition = addition
+gb.modification = modification
+gb.deletion = deletion
+gb.rename = rename
+gb.metrics = metrics
+gb.stats = stats
+gb.markdown = markdown
+gb.changedFiles = changed files
+gb.filesAdded = {0} files added
+gb.filesModified = {0} files modified
+gb.filesDeleted = {0} files deleted
+gb.filesCopied = {0} files copied
+gb.filesRenamed = {0} files renamed
+gb.missingUsername = Missing Username
+gb.edit = edit
+gb.searchTypeTooltip = Select Search Type
+gb.searchTooltip = Search {0}
+gb.delete = delete
+gb.docs = docs
+gb.accessRestriction = access restriction
+gb.name = name
+gb.enableTickets = enable tickets
+gb.enableDocs = enable docs
+gb.save = save
+gb.showRemoteBranches = show remote branches
+gb.editUsers = edit users
+gb.confirmPassword = confirm password
+gb.restrictedRepositories = restricted repositories
+gb.canAdmin = can admin
+gb.notRestricted = anonymous view, clone, & push
+gb.pushRestricted = authenticated push
+gb.cloneRestricted = authenticated clone & push
+gb.viewRestricted = authenticated view, clone, & push
+gb.useTicketsDescription = readonly, distributed Ticgit issues
+gb.useDocsDescription = enumerates Markdown documentation in repository
+gb.showRemoteBranchesDescription = show remote branches
+gb.canAdminDescription = can administer Gitblit server
+gb.permittedUsers = permitted users
+gb.isFrozen = is frozen
+gb.isFrozenDescription = deny push operations
+gb.zip = zip
+gb.showReadme = show readme
+gb.showReadmeDescription = show a \"readme\" Markdown file on the summary page
+gb.nameDescription = use '/' to group repositories. e.g. libraries/mycoollib.git
+gb.ownerDescription = the owner may edit repository settings
+gb.blob = blob
+gb.commitActivityTrend = commit activity trend
+gb.commitActivityDOW = commit activity by day of week
+gb.commitActivityAuthors = primary authors by commit activity
+gb.feed = feed
+gb.cancel = cancel
+gb.changePassword = change password
+gb.isFederated = is federated
+gb.federateThis = federate this repository
+gb.federateOrigin = federate the origin
+gb.excludeFromFederation = exclude from federation
+gb.excludeFromFederationDescription = block federated Gitblit instances from pulling this account
+gb.tokens = federation tokens
+gb.tokenAllDescription = all repositories, users, & settings
+gb.tokenUnrDescription = all repositories & users
+gb.tokenJurDescription = all repositories
+gb.federatedRepositoryDefinitions = repository definitions
+gb.federatedUserDefinitions = user definitions
+gb.federatedSettingDefinitions = setting definitions
+gb.proposals = federation proposals
+gb.received = received
+gb.type = type
+gb.token = token
+gb.repositories = repositories
+gb.proposal = proposal
+gb.frequency = frequency
+gb.folder = folder
+gb.lastPull = last pull
+gb.nextPull = next pull
+gb.inclusions = inclusions
+gb.exclusions = exclusions
+gb.registration = registration
+gb.registrations = federation registrations
+gb.sendProposal = propose
+gb.status = status
+gb.origin = origin
+gb.headRef = default branch (HEAD)
+gb.headRefDescription = change the ref that HEAD links to. e.g. refs/heads/master
+gb.federationStrategy = federation strategy
+gb.federationRegistration = federation registration
+gb.federationResults = federation pull results
+gb.federationSets = federation sets
+gb.message = message
+gb.myUrlDescription = the publicly accessible url for your Gitblit instance
+gb.destinationUrl = send to
+gb.destinationUrlDescription = the url of the Gitblit instance to send your proposal
+gb.users = users
+gb.federation = federation
+gb.error = error
+gb.refresh = refresh
+gb.browse = browse
+gb.clone = clone
+gb.filter = filter
+gb.create = create
+gb.servers = servers
+gb.recent = recent
+gb.available = available
+gb.selected = selected
+gb.size = size
+gb.downloading = downloading
+gb.loading = loading
+gb.starting = starting
+gb.general = general
+gb.settings = settings
+gb.manage = manage
+gb.lastLogin = last login
+gb.skipSizeCalculation = skip size calculation
+gb.skipSizeCalculationDescription = do not calculate the repository size (reduces page load time)
+gb.skipSummaryMetrics = skip summary metrics
+gb.skipSummaryMetricsDescription = do not calculate metrics on the summary page (reduces page load time)
+gb.accessLevel = access level
+gb.default = default
+gb.setDefault = set default
+gb.since = since
+gb.status = status
+gb.bootDate = boot date
+gb.servletContainer = servlet container
+gb.heapMaximum = maximum heap
+gb.heapAllocated = allocated heap
+gb.heapUsed = used heap
+gb.free = free
+gb.version = version
+gb.releaseDate = release date
+gb.date = date
+gb.activity = activity
+gb.subscribe = subscribe
+gb.branch = branch
+gb.maxHits = max hits
+gb.recentActivity = recent activity
+gb.recentActivityStats = last {0} days / {1} commits by {2} authors
+gb.recentActivityNone = last {0} days / none
+gb.dailyActivity = daily activity
+gb.activeRepositories = active repositories
+gb.activeAuthors = active authors
+gb.commits = commits
+gb.teams = teams
+gb.teamName = team name
+gb.teamMembers = team members
+gb.teamMemberships = team memberships
+gb.newTeam = new team
+gb.permittedTeams = permitted teams
+gb.emptyRepository = empty repository
+gb.repositoryUrl = repository url
+gb.mailingLists = mailing lists
+gb.preReceiveScripts = pre-receive scripts
+gb.postReceiveScripts = post-receive scripts
+gb.hookScripts = hook scripts
+gb.customFields = custom fields
+gb.customFieldsDescription = custom fields available to Groovy hooks
+gb.accessPermissions = access permissions
+gb.filters = filters
+gb.generalDescription = common settings
+gb.accessPermissionsDescription = restrict access by users and teams
+gb.accessPermissionsForUserDescription = set team memberships or grant access to specific restricted repositories
+gb.accessPermissionsForTeamDescription = set team members and grant access to specific restricted repositories
+gb.federationRepositoryDescription = share this repository with other Gitblit servers
+gb.hookScriptsDescription = run Groovy scripts on pushes to this Gitblit server
+gb.reset = reset
+gb.pages = pages
+gb.workingCopy = working copy
+gb.workingCopyWarning = this repository has a working copy and can not receive pushes
+gb.query = query
+gb.queryHelp = Standard query syntax is supported.<p/><p/>Please see <a target="_new" href="http://lucene.apache.org/core/old_versioned_docs/versions/3_5_0/queryparsersyntax.html">Lucene Query Parser Syntax</a> for details.
+gb.queryResults = results {0} - {1} ({2} hits)
+gb.noHits = no hits
+gb.authored = authored
+gb.committed = committed
+gb.indexedBranches = indexed branches
+gb.indexedBranchesDescription = select the branches to include in your Lucene index
+gb.noIndexedRepositoriesWarning = none of your repositories are configured for Lucene indexing
+gb.undefinedQueryWarning = query is undefined!
+gb.noSelectedRepositoriesWarning = please select one or more repositories!
+gb.luceneDisabled = Lucene indexing is disabled
+gb.failedtoRead = Failed to read
+gb.isNotValidFile = is not a valid file
+gb.failedToReadMessage = Failed to read default message from {0}!
+gb.passwordsDoNotMatch = Passwords do not match!
+gb.passwordTooShort = Password is too short. Minimum length is {0} characters.
+gb.passwordChanged = Password successfully changed.
+gb.passwordChangeAborted = Password change aborted.
+gb.pleaseSetRepositoryName = Please set repository name!
+gb.illegalLeadingSlash = Leading root folder references (/) are prohibited.
+gb.illegalRelativeSlash = Relative folder references (../) are prohibited.
+gb.illegalCharacterRepositoryName = Illegal character ''{0}'' in repository name!
+gb.selectAccessRestriction = Please select access restriction!
+gb.selectFederationStrategy = Please select federation strategy!
+gb.pleaseSetTeamName = Please enter a teamname!
+gb.teamNameUnavailable = Team name ''{0}'' is unavailable.
+gb.teamMustSpecifyRepository = A team must specify at least one repository.
+gb.teamCreated = New team ''{0}'' successfully created.
+gb.pleaseSetUsername = Please enter a username!
+gb.usernameUnavailable = Username ''{0}'' is unavailable.
+gb.combinedMd5Rename = Gitblit is configured for combined-md5 password hashing. You must enter a new password on account rename.
+gb.userCreated = New user ''{0}'' successfully created.
+gb.couldNotFindFederationRegistration = Could not find federation registration!
+gb.failedToFindGravatarProfile = Failed to find Gravatar profile for {0}
+gb.branchStats = {0} commits and {1} tags in {2}
+gb.repositoryNotSpecified = Repository not specified!
+gb.repositoryNotSpecifiedFor = Repository not specified for {0}!
+gb.canNotLoadRepository = Can not load repository
+gb.commitIsNull = Commit is null
+gb.unauthorizedAccessForRepository = Unauthorized access for repository
+gb.failedToFindCommit = Failed to find commit \"{0}\" in {1} for {2} page!
+gb.couldNotFindFederationProposal = Could not find federation proposal!
+gb.invalidUsernameOrPassword = Invalid username or password!
+gb.OneProposalToReview = There is 1 federation proposal awaiting review.
+gb.nFederationProposalsToReview = There are {0} federation proposals awaiting review.
+gb.couldNotFindTag = Could not find tag {0}
+gb.couldNotCreateFederationProposal = Could not create federation proposal!
+gb.pleaseSetGitblitUrl = Please enter your Gitblit url!
+gb.pleaseSetDestinationUrl = Please enter a destination url for your proposal!
+gb.proposalReceived = Proposal successfully received by {0}.
+gb.noGitblitFound = Sorry, {0} could not find a Gitblit instance at {1}.
+gb.noProposals = Sorry, {0} is not accepting proposals at this time.
+gb.noFederation = Sorry, {0} is not configured to federate with any Gitblit instances.
+gb.proposalFailed = Sorry, {0} did not receive any proposal data!
+gb.proposalError = Sorry, {0} reports that an unexpected error occurred!
+gb.failedToSendProposal = Failed to send proposal!
+gb.userServiceDoesNotPermitAddUser = {0} does not permit adding a user account!
+gb.userServiceDoesNotPermitPasswordChanges = {0} does not permit password changes!
+gb.displayName = display name
+gb.emailAddress = email address
+gb.errorAdminLoginRequired = Administration requires a login
+gb.errorOnlyAdminMayCreateRepository = Only an administrator may create a repository
+gb.errorOnlyAdminOrOwnerMayEditRepository = Only an administrator or the owner may edit a repository
+gb.errorAdministrationDisabled = Administration is disabled
+gb.lastNDays = last {0} days
+gb.completeGravatarProfile = Complete profile on Gravatar.com
+gb.none = none
+gb.line = line
+gb.content = content
+gb.empty = empty
+gb.inherited = inherited
+gb.deleteRepository = Delete repository \"{0}\"?
+gb.repositoryDeleted = Repository ''{0}'' deleted.
+gb.repositoryDeleteFailed = Failed to delete repository ''{0}''!
+gb.deleteUser = Delete user \"{0}\"?
+gb.userDeleted = User ''{0}'' deleted.
+gb.userDeleteFailed = Failed to delete user ''{0}''!
+gb.time.justNow = just now
+gb.time.today = today
+gb.time.yesterday = yesterday
+gb.time.minsAgo = {0} mins ago
+gb.time.hoursAgo = {0} hours ago
+gb.time.daysAgo = {0} days ago
+gb.time.weeksAgo = {0} weeks ago
+gb.time.monthsAgo = {0} months ago
+gb.time.oneYearAgo = 1 year ago
+gb.time.yearsAgo = {0} years ago
+gb.duration.oneDay = 1 day
+gb.duration.days = {0} days
+gb.duration.oneMonth = 1 month
+gb.duration.months = {0} months
+gb.duration.oneYear = 1 year
+gb.duration.years = {0} years
+gb.authorizationControl = authorization control
+gb.allowAuthenticatedDescription = grant RW+ permission to all authenticated users
+gb.allowNamedDescription = grant fine-grained permissions to named users or teams
+gb.markdownFailure = Failed to parse Markdown content!
+gb.clearCache = clear cache
+gb.projects = projects
+gb.project = project
+gb.allProjects = all projects
+gb.copyToClipboard = copy to clipboard
+gb.fork = fork
+gb.forks = forks
+gb.forkRepository = fork {0}?
+gb.repositoryForked = {0} has been forked
+gb.repositoryForkFailed= fork has failed
+gb.personalRepositories = personal repositories
+gb.allowForks = allow forks
+gb.allowForksDescription = allow authorized users to fork this repository
+gb.forkedFrom = forked from
+gb.canFork = can fork
+gb.canForkDescription = can fork authorized repositories to personal repositories
+gb.myFork = view my fork
+gb.forksProhibited = forks prohibited
+gb.forksProhibitedWarning = this repository forbids forks
+gb.noForks = {0} has no forks
+gb.forkNotAuthorized = sorry, you are not authorized to fork {0}
+gb.forkInProgress = fork in progress
+gb.preparingFork = preparing your fork...
+gb.isFork = is fork
+gb.canCreate = can create
+gb.canCreateDescription = can create personal repositories
+gb.illegalPersonalRepositoryLocation = your personal repository must be located at \"{0}\"
+gb.verifyCommitter = verify committer
+gb.verifyCommitterDescription = require committer identity to match pushing Gitblt user account
+gb.verifyCommitterNote = all merges require "--no-ff" to enforce committer identity
+gb.repositoryPermissions = repository permissions
+gb.userPermissions = user permissions
+gb.teamPermissions = team permissions
+gb.add = add
+gb.noPermission = DELETE THIS PERMISSION
+gb.excludePermission = {0} (exclude)
+gb.viewPermission = {0} (view)
+gb.clonePermission = {0} (clone)
+gb.pushPermission = {0} (push)
+gb.createPermission = {0} (push, ref creation)
+gb.deletePermission = {0} (push, ref creation+deletion)
+gb.rewindPermission = {0} (push, ref creation+deletion+rewind)
+gb.permission = permission
+gb.regexPermission = this permission is set from regular expression \"{0}\"
+gb.accessDenied = access denied
+gb.busyCollectingGarbage = sorry, Gitblit is busy collecting garbage in {0}
+gb.gcPeriod = GC period
+gb.gcPeriodDescription = duration between garbage collections
+gb.gcThreshold = GC threshold
+gb.gcThresholdDescription = minimum total size of loose objects to trigger early garbage collection
+gb.ownerPermission = repository owner
+gb.administrator = admin
+gb.administratorPermission = Gitblit administrator
+gb.team = team
+gb.teamPermission = permission set by \"{0}\" team membership
+gb.missing = missing!
+gb.missingPermission = the repository for this permission is missing!
+gb.mutable = mutable
+gb.specified = specified
+gb.effective = effective
+gb.organizationalUnit = organizational unit
+gb.organization = organization
+gb.locality = locality
+gb.stateProvince = state or province
+gb.countryCode = country code
+gb.properties = properties
+gb.issued = issued
+gb.expires = expires
+gb.expired = expired
+gb.expiring = expiring
+gb.revoked = revoked
+gb.serialNumber = serial number
+gb.certificates = certificates
+gb.newCertificate = new certificate
+gb.revokeCertificate = revoke certificate
+gb.sendEmail = send email
+gb.passwordHint = password hint
+gb.ok = ok
+gb.invalidExpirationDate = invalid expiration date!
+gb.passwordHintRequired = password hint required!
+gb.viewCertificate = view certificate
+gb.subject = subject
+gb.issuer = issuer
+gb.validFrom = valid from
+gb.validUntil = valid until
+gb.publicKey = public key
+gb.signatureAlgorithm = signature algorithm
+gb.sha1FingerPrint = SHA-1 Fingerprint
+gb.md5FingerPrint = MD5 Fingerprint
+gb.reason = reason
+gb.revokeCertificateReason = Please select a reason for certificate revocation
+gb.unspecified = unspecified
+gb.keyCompromise = key compromise
+gb.caCompromise = CA compromise
+gb.affiliationChanged = affiliation changed
+gb.superseded = superseded
+gb.cessationOfOperation = cessation of operation
+gb.privilegeWithdrawn = privilege withdrawn
+gb.time.inMinutes = in {0} mins
+gb.time.inHours = in {0} hours
+gb.time.inDays = in {0} days
+gb.hostname = hostname
+gb.hostnameRequired = Please enter a hostname
+gb.newSSLCertificate = new server SSL certificate
+gb.newCertificateDefaults = new certificate defaults
+gb.duration = duration
+gb.certificateRevoked = Certificate {0,number,0} has been revoked
+gb.clientCertificateGenerated = Successfully generated new client certificate for {0}
+gb.sslCertificateGenerated = Successfully generated new server SSL certificate for {0}
+gb.newClientCertificateMessage = NOTE:\nThe 'password' is not the user's password, it is the password to protect the user's keystore. This password is not saved so you must also enter a 'hint' which will be included in the user's README instructions.
+gb.certificate = certificate
+gb.emailCertificateBundle = email client certificate bundle
+gb.pleaseGenerateClientCertificate = Please generate a client certificate for {0}
+gb.clientCertificateBundleSent = Client certificate bundle for {0} sent
+gb.enterKeystorePassword = Please enter the Gitblit keystore password
+gb.warning = warning
+gb.jceWarning = Your Java Runtime Environment does not have the \"JCE Unlimited Strength Jurisdiction Policy\" files.\nThis will limit the length of passwords you may use to encrypt your keystores to 7 characters.\nThese policy files are an optional download from Oracle.\n\nWould you like to continue and generate the certificate infrastructure anyway?\n\nAnswering No will direct your browser to Oracle's download page so that you may download the policy files.
+gb.maxActivityCommits = max activity commits
+gb.maxActivityCommitsDescription = maximum number of commits to contribute to the Activity page
+gb.noMaximum = no maximum
+gb.attributes = attributes
+gb.serveCertificate = serve https with this certificate
+gb.sslCertificateGeneratedRestart = Successfully generated new server SSL certificate for {0}.\nYou must restart Gitblit to use the new certificate.\n\nIf you are launching with the '--alias' parameter you will have to set that to ''--alias {0}''.
+gb.validity = validity
+gb.siteName = site name
+gb.siteNameDescription = short, descriptive name of your server
+gb.excludeFromActivity = exclude from activity page
+gb.isSparkleshared = repository is Sparkleshared
+gb.owners = owners
+gb.sessionEnded = Session has been closed
+gb.closeBrowser = Please close the browser to properly end the session. \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/GitBlitWebApp_es.properties b/src/main/java/com/gitblit/wicket/GitBlitWebApp_es.properties
new file mode 100644
index 00000000..210a75f9
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/GitBlitWebApp_es.properties
@@ -0,0 +1,445 @@
+gb.repository = Repositorio
+gb.owner = Propietario
+gb.description = Descripci\u00F3n
+gb.lastChange = Actualizado
+gb.refs = Refs
+gb.tag = Etiqueta
+gb.tags = Etiquetas
+gb.author = Autor
+gb.committer = Consignador
+gb.commit = Consigna
+gb.tree = \u00C1rbol
+gb.parent = Antecesor
+gb.url = URL
+gb.history = Hist\u00F3rico
+gb.raw = Bruto
+gb.object = Objeto
+gb.ticketId = Id Ticket
+gb.ticketAssigned = Asignado
+gb.ticketOpenDate = Fecha de apertura
+gb.ticketState = Estado
+gb.ticketComments = Comentarios
+gb.view = Ver
+gb.local = Local
+gb.remote = Remoto
+gb.branches = Ramas
+gb.patch = Parche
+gb.diff = Dif
+gb.log = Reg.
+gb.moreLogs = M\u00E1s Consignas...
+gb.allTags = Todas las Etiquetas...
+gb.allBranches = Todas las Ramas...
+gb.summary = Resumen
+gb.ticket = Ticket
+gb.newRepository = Nuevo Repositorio
+gb.newUser = Nuevo usuario
+gb.commitdiff = Dif Consigna
+gb.tickets = Tickets
+gb.pageFirst = Primera
+gb.pagePrevious = Anterior
+gb.pageNext = Siguiente
+gb.head = HEAD
+gb.blame = Acuse
+gb.login = Idenfiticarse
+gb.logout = Salir
+gb.username = Usuario
+gb.password = Contrase\u00F1a
+gb.tagger = Etiquetador
+gb.moreHistory = M\u00E1s hist\u00F3ricos...
+gb.difftocurrent = Dif con actual
+gb.search = Buscar
+gb.searchForAuthor = Buscar consignas de autor por
+gb.searchForCommitter = Buscar consignas enviadas por
+gb.addition = Adici\u00F3n
+gb.modification = Modificaci\u00F3n
+gb.deletion = Eliminado
+gb.rename = Renombrar
+gb.metrics = Movimientos
+gb.stats = Estad&iacute;sticas
+gb.markdown = Markdown
+gb.changedFiles = Archivos cambiados
+gb.filesAdded = {0} Archivos a\u00F1adidos
+gb.filesModified = {0} Archivos modificados
+gb.filesDeleted = {0} Archivos eliminados
+gb.filesCopied = {0} Archivos copiados
+gb.filesRenamed = {0} Archivos renombrados
+gb.missingUsername = Falta usuario
+gb.edit = Editar
+gb.searchTypeTooltip = Seleccionar tipo de b\u00FAsqueda
+gb.searchTooltip = Buscar {0}
+gb.delete = Eliminar
+gb.docs = Docs
+gb.accessRestriction = Restricci\u00F3n de acceso
+gb.name = Nombre
+gb.enableTickets = Habilitar tickets
+gb.enableDocs = Habilitar Docs
+gb.save = Guardar
+gb.showRemoteBranches = Mostrar ramas remotas
+gb.editUsers = Editar usuarios
+gb.confirmPassword = Confirmar contrase\u00F1a
+gb.restrictedRepositories = Repositorios restringidos
+gb.canAdmin = Puede Administrar
+gb.notRestricted = An\u00F3nimos pueden Ver, clonar y empujar
+gb.pushRestricted = Autentificados pueden empujar
+gb.cloneRestricted = Autentificados pueden clonar y empujar
+gb.viewRestricted = Autentificados pueden Ver, clonar y empujar
+gb.useTicketsDescription = Distribuir temas mediante Ticgit (s\u00F3lo lecura)
+gb.useDocsDescription = Enumerar documentaci\u00F3n Markdown en el Repositorio.
+gb.showRemoteBranchesDescription = Mostrar Ramas Remotas
+gb.canAdminDescription = Puede administrar el servidor de GitBlit
+gb.permittedUsers = Usuarios permitidos
+gb.isFrozen = Est\u00E1 congelado
+gb.isFrozenDescription = No se le puede empujar
+gb.zip = Zip
+gb.showReadme = Ver l\u00E9eme
+gb.showReadmeDescription = Mostrar el archivo \"l\u00E9eme\" de Markdown en la p\u00E1gina resumen
+gb.nameDescription = Usa '/' para agrupar repositorios. ej. librerias/mylibreria.git
+gb.ownerDescription = El propietario puede editar la configuraci\u00F3n del repositorio
+gb.blob = Objeto
+gb.commitActivityTrend = Tendencia de actividad del repositorio
+gb.commitActivityDOW = Actividad de consignas por d\u00EDa de la semana
+gb.commitActivityAuthors = Principales autores por actividad de consignas
+gb.feed = Sindicaci\u00F3n
+gb.cancel = Cancelar
+gb.changePassword = Cambiar contrase\u00F1a
+gb.isFederated = Est\u00E1 federado
+gb.federateThis = Federar este repositorio
+gb.federateOrigin = Federar desde el origen
+gb.excludeFromFederation = Excluir de la federaci\u00F3n
+gb.excludeFromFederationDescription = Bloquear a esta cuenta el recibir de instancias federadas de GitBlit
+gb.tokens = Tarjetas de federaci\u00F3n
+gb.tokenAllDescription = Todos los repositorios, usuarios y configuraciones
+gb.tokenUnrDescription = Todos los repositorios y usuarios
+gb.tokenJurDescription = Todos los repositorios
+gb.federatedRepositoryDefinitions = Definiciones del repositorio
+gb.federatedUserDefinitions = Definiciones del usuario
+gb.federatedSettingDefinitions = Definiciones de configuraci\u00F3n
+gb.proposals = Propuestas de federaci\u00F3n
+gb.received = Recibida
+gb.type = Tipo
+gb.token = Tarjeta
+gb.repositories = Repositorios
+gb.proposal = Propuesta
+gb.frequency = Frecuencia
+gb.folder = Carpeta
+gb.lastPull = \u00DAltimo recibo
+gb.nextPull = Siguiente recibo
+gb.inclusions = Inclusiones
+gb.exclusions = Exclusiones
+gb.registration = Registro
+gb.registrations = Registros de federaci\u00F3n
+gb.sendProposal = Proponer
+gb.status = Estado
+gb.origin = Origen
+gb.headRef = Rama por defecto (HEAD)
+gb.headRefDescription = Cambiar la Ref. a la que apunta HEAD ej. refs/heads/master
+gb.federationStrategy = Estrategia de federaci\u00F3n
+gb.federationRegistration = Registro de federaci\u00F3n
+gb.federationResults = Resultados de recibos federados
+gb.federationSets = Grupos de federaci\u00F3n
+gb.message = Mensaje
+gb.myUrlDescription = La URL p\u00FAblica y accesible de tu instancia de GitBlit
+gb.destinationUrl = Enviar a
+gb.destinationUrlDescription = La URL de la instancia de GitBlit a la que env\u00EDas tu propuesta
+gb.users = Usuarios
+gb.federation = Federaci\u00F3n
+gb.error = Error
+gb.refresh = Actualizar
+gb.browse = Buscar
+gb.clone = Clonar
+gb.filter = Filtrar
+gb.create = Crear
+gb.servers = Servidores
+gb.recent = Recientes
+gb.available = Disponible
+gb.selected = Seleccionados
+gb.size = Tama\u00F1o
+gb.downloading = Descargando
+gb.loading = Cargando
+gb.starting = Iniciando
+gb.general = General
+gb.settings = Configuraci\u00F3n
+gb.manage = Administrar
+gb.lastLogin = \u00DAltimo acceso
+gb.skipSizeCalculation = Saltar comprobaciones de tama\u00F1o
+gb.skipSizeCalculationDescription = No calcular el tama\u00F1o del repositorio (Reduce tiempo de carga de la p\u00E1gina)
+gb.skipSummaryMetrics = Saltar resumen de estad\u00EDsticas
+gb.skipSummaryMetricsDescription = No calcular estad\u00EDsticas (Reduce tiempo de carga de la p\u00E1gina)
+gb.accessLevel = Nivel de acceso
+gb.default = Predeterminado
+gb.setDefault = Ajustar por defecto
+gb.since = Desde
+gb.status = Estado
+gb.bootDate = Fecha de inicio
+gb.servletContainer = Contenedor ServLet
+gb.heapMaximum = Pila m\u00E1xima
+gb.heapAllocated = Pila asignada
+gb.heapUsed = Pila usada
+gb.free = Libre
+gb.version = Versi\u00F3n
+gb.releaseDate = Fecha de lanzamiento
+gb.date = Fecha
+gb.activity = Actividad
+gb.subscribe = Suscribir
+gb.branch = Rama
+gb.maxHits = Coincidencias m\u00E1ximas
+gb.recentActivity = Actividad reciente
+gb.recentActivityStats = \u00DAltimo(s) {0} d\u00EDa(s) / {1} Consigna(s) de {2} Autor(es)
+gb.recentActivityNone = \u00DAltimo(s) {0} d\u00EDa(s) / Ninguna
+gb.dailyActivity = Actividad diaria
+gb.activeRepositories = Repositorios activos
+gb.activeAuthors = Autores activos
+gb.commits = Consignas
+gb.teams = Equipos
+gb.teamName = Nombre del Equipo
+gb.teamMembers = Suscriptores
+gb.teamMemberships = Inscripciones
+gb.newTeam = Nuevo Equipo
+gb.permittedTeams = Equipos permitidos
+gb.emptyRepository = Repositorio vac\u00EDo
+gb.repositoryUrl = URL del Repositorio
+gb.mailingLists = Listas de correo
+gb.preReceiveScripts = Scripts para pre-recibo
+gb.postReceiveScripts = Scripts para post-recibo
+gb.hookScripts = Scripts enganchados
+gb.customFields = Campos Propios
+gb.customFieldsDescription = Campos Propios disponibles para los engachados Groovy
+gb.accessPermissions = Permisos de acceso
+gb.filters = Filtros
+gb.generalDescription = Configuraciones comunes
+gb.accessPermissionsDescription = Restringir acceso a usuarios y Equipos
+gb.accessPermissionsForUserDescription = Modificar inscripciones y especificar acceso a repositorios o restringirlos
+gb.accessPermissionsForTeamDescription = A\u00F1ada miembros al Equipo y conceda o restrinja acceso a repositorios.
+gb.federationRepositoryDescription = Compartir este repositorio con otros servidores GitBlit
+gb.hookScriptsDescription = Scripts Groovy ha ejecutar cuando empujen a este servidor.
+gb.reset = Reinicializa
+gb.pages = P\u00E1ginas
+gb.workingCopy = Copia de trabajo
+gb.workingCopyWarning = Este repositorio tiene una copia de trabajo y no se le puede empujar
+gb.query = Consulta
+gb.queryHelp = Se admite la sintaxis de consulta est\u00E1ndar.<p/>Por favor, lee el: <a target="_new" href="http://lucene.apache.org/core/old_versioned_docs/versions/3_5_0/queryparsersyntax.html">Analizador sint\u00E1ctico de consultas de Lucene</a> para m\u00E1s detalles.
+gb.queryResults = Resultados {0} - {1} ({2} coincidencias)
+gb.noHits = Sin coincidencias
+gb.authored = Autor
+gb.committed = Consignado
+gb.indexedBranches = Ramas indexadas
+gb.indexedBranchesDescription = Selecciona las Ramas a incluir en tu \u00EDndice de Lucene
+gb.noIndexedRepositoriesWarning = Ninguno de tus repositorios est\u00E1 configurado para el indexado de Lucene
+gb.undefinedQueryWarning = \u00A1Consulta indefinida!
+gb.noSelectedRepositoriesWarning = \u00A1Por favor selecciona uno o m\u00E1s repositorios!
+gb.luceneDisabled = Indexado de Lucene deshabilitado
+gb.failedtoRead = Fallo de lectura
+gb.isNotValidFile = No es un archivo v\u00E1lido
+gb.failedToReadMessage = \u00A1Fallo al leer el mensaje por defecto de {0}!
+gb.passwordsDoNotMatch = \u00A1Las contrase\u00F1as no coinciden!
+gb.passwordTooShort = La contrase\u00F1a es muy corta. La longitud m\u00EDnima es de {0} caracteres.
+gb.passwordChanged = Contrase\u00F1a cambiada satisfactoriamente.
+gb.passwordChangeAborted = Cambio de contrase\u00F1a abortado.
+gb.pleaseSetRepositoryName = \u00A1Por favor introduce un nombre para el repositorio!
+gb.illegalLeadingSlash = Referencias a la carpeta ra\i00EDz (/) estu00E1n prohibidas.
+gb.illegalRelativeSlash = Referencias relativas a la carpeta (../) est\u00E1n prohibidas.
+gb.illegalCharacterRepositoryName = \u00A1Caracter ilegal ''{0}'' en el nombre del repositorio!
+gb.selectAccessRestriction = \u00A1Por favor selecciona la restricci\u00F3n de acceso!
+gb.selectFederationStrategy = \u00A1Por favor, selecciona la estrategia de federaci\u00F3n!
+gb.pleaseSetTeamName = \u00A1Por favor, introduce un nombre para el Equipo!
+gb.teamNameUnavailable = El nombre de Equipo ''{0}'' no est\u00E1 disponible.
+gb.teamMustSpecifyRepository = Debe especificar al menos un repositorio para el Equipo.
+gb.teamCreated = Nuevo Equipo ''{0}'' creado satisfactoriamente.
+gb.pleaseSetUsername = \u00A1Por favor, introduce un usuario!
+gb.usernameUnavailable = El usuario ''{0}'' no est\u00E1 disponible.
+gb.combinedMd5Rename = GitBlit est\u00E1 configurado para Hashes combinados md5. Debes introducir una nueva contrase\u00F1a para renombrar la cuenta.
+gb.userCreated = Nuevo usuario ''{0}'' creado satisfactoriamente.
+gb.couldNotFindFederationRegistration = \u00A1No se pudo encontrar el registro de federaci\u00F3n!
+gb.failedToFindGravatarProfile = Fallo al buscar el perfil Gravatar de {0}
+gb.branchStats = {0} consigna(s) y {1} etiqueta(s) en {2}
+gb.repositoryNotSpecified = /u00A1Repositorio no especificado!
+gb.repositoryNotSpecifiedFor = /u00A1Repositorio no especificado para {0}!
+gb.canNotLoadRepository = No se puede cargar el repositorio
+gb.commitIsNull = La consigna es nula
+gb.unauthorizedAccessForRepository = Acceso no autorizado al repositorio
+gb.failedToFindCommit = \u00A1Fallo al buscar la consigna \"{0}\" en {1} de {2} p\u00E1ginas!
+gb.couldNotFindFederationProposal = \u00A1No se puede encontrar una propuesta de federaci\u00F3n!
+gb.invalidUsernameOrPassword = \u00A1Usuario o contrase\u00F1a inv\u00E1lidos!
+gb.OneProposalToReview = Hay 1 petici\u00F3n de federaci\u00F3n esperando revisi\u00F3n.
+gb.nFederationProposalsToReview = Hay {0} peticiones de federaci\u00F3n esperando revisi\u00F3n.
+gb.couldNotFindTag = No se puede encontrar la etiqueta {0}
+gb.couldNotCreateFederationProposal = \u00A1No se puede crear una propuesta de federaci\u00F3n!
+gb.pleaseSetGitblitUrl = \u00A1Por favor, introduce la URL de tu GitBlit!
+gb.pleaseSetDestinationUrl = \u00A1Por favor, introduce la URL de destino para tu propuesta!
+gb.proposalReceived = Propuesta recibida satisfactoriamente por {0}.
+gb.noGitblitFound = Lo siento, {0} no puede encontrar una instancia de GitBlit en {1}.
+gb.noProposals = Lo siento, {0} no acepta propuestas en este momento.
+gb.noFederation = Lo siento, {0} no est\u00E1 configurado para Federar con ninguna instancia de GitBlit.
+gb.proposalFailed = /u00A1Lo siento, {0} no ha recibido ning\u00FAn dato de propuesta!
+gb.proposalError = /u00A1Lo siento, {0} informa de que ha ocurrido un error inesperado!
+gb.failedToSendProposal = /u00A1Fallo al enviar la propuesta!
+gb.userServiceDoesNotPermitAddUser = \u00A1{0} no permite a\u00F1adir una cuenta de usuario!
+gb.userServiceDoesNotPermitPasswordChanges = \u00A1{0} no permite cambio de contrase\u00F1a!
+gb.displayName = Nombre
+gb.emailAddress = Direcci\u00F3n de correo
+gb.errorAdminLoginRequired = La administraci&oacute;n requiere identificarse
+gb.errorOnlyAdminMayCreateRepository = S&oacute;lo un administrador puede crear un repositorio
+gb.errorOnlyAdminOrOwnerMayEditRepository = S&oacute;lo un administrador o el propietario puede editar un repositorio
+gb.errorAdministrationDisabled = La administraci&oacute;n est&aacute; desactivada
+gb.lastNDays = \u00FAltimos {0} d\u00EDas
+gb.completeGravatarProfile = Perfil completo en Gravatar.com
+gb.none = nadie
+gb.line = L\u00EDenea
+gb.content = Contenido
+gb.empty = vac\u00EDo
+gb.inherited = heredado
+gb.deleteRepository = \u00BFBorrar el repositorio \"{0}\"?
+gb.repositoryDeleted = Repositorio ''{0}'' borrado.
+gb.repositoryDeleteFailed = \u00A1Fallo al borrar el repositorio ''{0}''!
+gb.deleteUser = \u00BFEliminar usuario\"{0}\"?
+gb.userDeleted = Usuario ''{0}'' eliminado.
+gb.userDeleteFailed = \u00A1Fallo al eliminar usuario ''{0}''!
+gb.time.justNow = hace poco
+gb.time.today = hoy
+gb.time.yesterday = ayer
+gb.time.minsAgo = hace {0} min
+gb.time.hoursAgo = hace {0} horas
+gb.time.daysAgo = hace {0} d\u00EDas
+gb.time.weeksAgo = hace {0} semanas
+gb.time.monthsAgo = hace {0} meses
+gb.time.oneYearAgo = hace 1 a\u00F1o
+gb.time.yearsAgo = hace {0} a\u00F1os
+gb.duration.oneDay = 1 d\u00EDa
+gb.duration.days = {0} d\u00EDas
+gb.duration.oneMonth = 1 mes
+gb.duration.months = {0} meses
+gb.duration.oneYear = 1 a\u00F1o
+gb.duration.years = {0} a\u00F1os
+gb.authorizationControl = Control de autorizaciones
+gb.allowAuthenticatedDescription = Permitir acceso a todos los usuarios registrados
+gb.allowNamedDescription = Permitir acceso a usuarios inscritos \u00F3 equipos
+gb.markdownFailure = \u00A1Fallo al analizar el contenido Markdown!
+gb.clearCache = Limpiar cache
+gb.projects = Proyectos
+gb.project = Proyecto
+gb.allProjects = Todos los proyectos
+gb.copyToClipboard = Copiar al portapapeles
+gb.fork = Bifurcar
+gb.forks = Bifurcados
+gb.forkRepository = \u00BFBifurcar {0}?
+gb.repositoryForked = {0} se ha bifurcado
+gb.repositoryForkFailed= Bifurcaci\u00F3n fallida
+gb.personalRepositories = Repositorios personales
+gb.allowForks = Permitir bifurcados
+gb.allowForksDescription = Permitir a usuarios autorizados bifurcar este repositorio
+gb.forkedFrom = Bifurcado de
+gb.canFork = Puede bifurcar
+gb.canForkDescription = Puede bifurcar repositorios permitidos ha sus repositorios personales
+gb.myFork = Ver mi bifurcado
+gb.forksProhibited = Prohibido bifurcar
+gb.forksProhibitedWarning = Este repositorio proh\u00EDbe bifurcarse
+gb.noForks = {0} no tiene bifurcados
+gb.forkNotAuthorized = Perdona, no est\u00E1s autorizado a bifurcar {0}
+gb.forkInProgress = Bifurcar en curso
+gb.preparingFork = Preparando tu bifurcaci\u00F3n...
+gb.isFork = Es bifurcado
+gb.canCreate = Puede crear
+gb.canCreateDescription = Puede crear repositorios personales
+gb.illegalPersonalRepositoryLocation = Tu repositorio personal debe estar ubicado en \"{0}\"
+gb.verifyCommitter = Consignador acreditado
+gb.verifyCommitterDescription = Require que la acreditaci\u00F3n del consignador coincida con la de la cuenta del usuario en Gitblt
+gb.verifyCommitterNote = es obligatorio "--no-ff" al empujar para que el consignador se acredite
+gb.repositoryPermissions = Permisos del repositorio
+gb.userPermissions = Permisos de usuarios
+gb.teamPermissions = Permisos de equipos
+gb.add = A\u00F1adir
+gb.noPermission = BORRAR ESTE PERMISO
+gb.excludePermission = {0} (excluir)
+gb.viewPermission = {0} (ver)
+gb.clonePermission = {0} (clonar)
+gb.pushPermission = {0} (empujar)
+gb.createPermission = {0} (empujar, ref creaci\u00F3n)
+gb.deletePermission = {0} (empujar, ref creaci\u00F3n+borrado)
+gb.rewindPermission = {0} (empujar, ref creaci\u00F3n+borrado+supresi\u00F3n)
+gb.permission = Permisos
+gb.regexPermission = Estos permisos se ajustan desde la expresi\u00F3n regulare \"{0}\"
+gb.accessDenied = Acceso denegado
+gb.busyCollectingGarbage = Perd\u00F3n, Gitblit est\u00E1 ocupado quitando basura de {0}
+gb.gcPeriod = Periodo para GC
+gb.gcPeriodDescription = Duraci\u00F3n entre periodos de limpieza
+gb.gcThreshold = L\u00EDmites para GC
+gb.gcThresholdDescription = Tama\u00F1o m\u00EDnimo total de objetos sueltos para activar la recolecci\u00F3n inmediata de basura
+gb.ownerPermission = Propietario del repositorio
+gb.administrator = Admin
+gb.administratorPermission = Administrador de Gitblit
+gb.team = Equipo
+gb.teamPermission = Permisos ajustados para \"{0}\" mienbros de equipo
+gb.missing = \u00A1Omitido!
+gb.missingPermission = \u00A1Falta el repositorio de este permiso!
+gb.mutable = Alterables
+gb.specified = Espec\u00EDficos
+gb.effective = Efectivos
+gb.organizationalUnit = Unidad de organizaci\u00F3n
+gb.organization = Organizaci\u00F3n
+gb.locality = Localidad
+gb.stateProvince = Estado o provincia
+gb.countryCode = C\u00F3digo postal
+gb.properties = Propiedades
+gb.issued = Publicado
+gb.expires = Expira
+gb.expired = Expirado
+gb.expiring = Concluido
+gb.revoked = Revocado
+gb.serialNumber = N\u00FAmero de serie
+gb.certificates = Certificados
+gb.newCertificate = Nuevo certificado
+gb.revokeCertificate = Revocar certificado
+gb.sendEmail = Enviar correo
+gb.passwordHint = Recordatorio de contrase\u00F1a
+gb.ok = ok
+gb.invalidExpirationDate = \u00A1La fecha de expiraci\u00F3n no es v\u00E1lida!
+gb.passwordHintRequired = \u00A1Se requiere una pista para la contrase\u00F1a!
+gb.viewCertificate = Ver certificado
+gb.subject = Asunto
+gb.issuer = Emisor
+gb.validFrom = V\u00E1lido desde
+gb.validUntil = V\u00E1lido hasta
+gb.publicKey = Clave p\u00FAblica
+gb.signatureAlgorithm = Algoritmo de firma
+gb.sha1FingerPrint = Huella digital SHA-1
+gb.md5FingerPrint = Huella digital MD5
+gb.reason = Motivo
+gb.revokeCertificateReason = Por favor, selecciona un motivo por el que revocas el certificado
+gb.unspecified = Sin especificar
+gb.keyCompromise = Clave de compromiso
+gb.caCompromise = Compromiso CA
+gb.affiliationChanged = Afiliaci\u00F3n cambiada
+gb.superseded = Sustituida
+gb.cessationOfOperation = Cese de operaci\u00F3n
+gb.privilegeWithdrawn = Privilegios retirados
+gb.time.inMinutes = en {0} mints
+gb.time.inHours = en {0} horas
+gb.time.inDays = en {0} d\u00EDas
+gb.hostname = Nombre de host
+gb.hostnameRequired = Por favor, introduzca un nombre de host
+gb.newSSLCertificate = Nuevo certificado SSL del servidor
+gb.newCertificateDefaults = Nuevo certificado predeterminado
+gb.duration = Duraci\u00F3n
+gb.certificateRevoked = El cretificado {0,n\u00FAmero,0} ha sido revocado
+gb.clientCertificateGenerated = Nuevo certificado de cliente generado correctamente para {0}
+gb.sslCertificateGenerated = Nuevo certificado de SSL generado correctamente para {0}
+gb.newClientCertificateMessage = AVISO:\nLa 'contrase\u00F1a' no es la contrase\u00F1a del usuario, es la contrase\u00F1a para proteger el almac\u00E9n de claves del usuario. Esta contrase\u00F1a no se guarda por lo que tambi\u00E9n debe introducirse una "pista" que ser\u00E1 incluida en las instrucciones LEEME del usuario.
+gb.certificate = Certificado
+gb.emailCertificateBundle = Correo del cliente para el paquete del certificado
+gb.pleaseGenerateClientCertificate = Por favor, genera un certificado de cliente para {0}
+gb.clientCertificateBundleSent = Paquete de certificado de cliente {0} enviado
+gb.enterKeystorePassword = Por favor, introduzca la contrase\u00F1a del almac\u00E9n de claves de Gitblit
+gb.warning = Advertencia
+gb.jceWarning = Tu entorno de trabajo JAVA no contiene los archivos \"JCE Unlimited Strength Jurisdiction Policy\".\nEsto limita la longitud de la contrase\u00F1a que puedes usuar para cifrar el almac\u00E9n de claves a 7 caracteres.\nEstos archivos opcionales puedes descargarlos desde Oracle.\n\n\u00BFQuieres continuar y generar la infraestructura de certificados de todos modos?\n\nSi respondes No tu navegador te dirigir\u00E1 a la p\u00E1gina de descarga de Oracle para que pueda descargar dichos archivos.
+gb.maxActivityCommits = Actividad m\u00E1xima de consignas
+gb.maxActivityCommitsDescription = N\u00FAmero m\u00E1ximo de consignas a incluir en la p\u00E1gina de actividad
+gb.noMaximum = Sin m\u00E1ximos
+gb.attributes = Atributos
+gb.serveCertificate = Servidor https con este certificado
+gb.sslCertificateGeneratedRestart = Certificado SSL generado correctamente para {0}.\nDebes reiniciar Gitblit para usar el nuevo certificado.\n\nSi lo has iniciado con la opci\u00F3n '--alias' deber\u00E1s ajustar dicha opci\u00F3n a ''--alias {0}''.
+gb.validity = Vigencia
+gb.siteName = Nombre del sitio
+gb.siteNameDescription = Nombre corto y descriptivo de tu servidor
+gb.excludeFromActivity = Excluir de la p\u00E1gina de actividad
+gb.sessionEnded = La sesi\u00F3n ha sido cerrada
+gb.closeBrowser = Porfavor cierre el navegador para terminar correctamente la sesi\u00F3n. \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/GitBlitWebApp_ja.properties b/src/main/java/com/gitblit/wicket/GitBlitWebApp_ja.properties
new file mode 100644
index 00000000..d0234f87
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/GitBlitWebApp_ja.properties
@@ -0,0 +1,318 @@
+gb.repository = \u30ea\u30dd\u30b8\u30c8\u30ea
+gb.owner = \u6240\u6709\u8005
+gb.description = \u8aac\u660e
+gb.lastChange = \u6700\u5f8c\u306e\u5909\u66f4
+gb.refs = refs
+gb.tag = \u30bf\u30b0
+gb.tags = \u30bf\u30b0
+gb.author = \u4f5c\u8005
+gb.committer = \u30b3\u30df\u30c3\u30bf\u30fc
+gb.commit = \u30b3\u30df\u30c3\u30c8
+gb.tree = tree
+gb.parent = \u89aa
+gb.url = URL
+gb.history = \u5c65\u6b74
+gb.raw = raw
+gb.object = object
+gb.ticketId = \u30c1\u30b1\u30c3\u30c8ID
+gb.ticketAssigned = \u5272\u308a\u5f53\u3066\u6e08\u307f
+gb.ticketOpenDate = \u30aa\u30fc\u30d7\u30f3\u65e5
+gb.ticketState = \u72b6\u614b
+gb.ticketComments = \u30b3\u30e1\u30f3\u30c8
+gb.view = \u898b\u308b
+gb.local = \u30ed\u30fc\u30ab\u30eb
+gb.remote = \u30ea\u30e2\u30fc\u30c8
+gb.branches = \u30d6\u30e9\u30f3\u30c1
+gb.patch = \u30d1\u30c3\u30c1
+gb.diff = diff
+gb.log = \u30ed\u30b0
+gb.moreLogs = more commits...
+gb.allTags = all tags...
+gb.allBranches = all branches...
+gb.summary = \u6982\u8981
+gb.ticket = \u30c1\u30b1\u30c3\u30c8
+gb.newRepository = \u30ea\u30dd\u30b8\u30c8\u30ea\u4f5c\u6210
+gb.newUser = \u30e6\u30fc\u30b6\u30fc\u4f5c\u6210
+gb.commitdiff = commitdiff
+gb.tickets = \u30c1\u30b1\u30c3\u30c8
+gb.pageFirst = first
+gb.pagePrevious = prev
+gb.pageNext = next
+gb.head = HEAD
+gb.blame = \u6ce8\u91c8\u5c65\u6b74
+gb.login = \u30ed\u30b0\u30a4\u30f3
+gb.logout = \u30ed\u30b0\u30a2\u30a6\u30c8
+gb.username = \u30e6\u30fc\u30b6\u30fc\u540d
+gb.password = \u30d1\u30b9\u30ef\u30fc\u30c9
+gb.tagger = tagger
+gb.moreHistory = more history...
+gb.difftocurrent = diff to current
+gb.search = \u691c\u7d22
+gb.searchForAuthor = Search for commits authored by
+gb.searchForCommitter = Search for commits committed by
+gb.addition = \u8ffd\u52a0
+gb.modification = \u4fee\u6b63
+gb.deletion = \u524a\u9664
+gb.rename = \u30ea\u30cd\u30fc\u30e0
+gb.metrics = \u6307\u6a19
+gb.stats = \u7d71\u8a08
+gb.markdown = markdown
+gb.changedFiles = \u5909\u66f4\u3055\u308c\u305f\u30d5\u30a1\u30a4\u30eb
+gb.filesAdded = {0} \u30d5\u30a1\u30a4\u30eb\u304c\u8ffd\u52a0
+gb.filesModified = {0} \u30d5\u30a1\u30a4\u30eb\u304c\u5909\u66f4
+gb.filesDeleted = {0} \u30d5\u30a1\u30a4\u30eb\u304c\u524a\u9664
+gb.filesCopied = {0} \u30d5\u30a1\u30a4\u30eb\u304c\u30b3\u30d4\u30fc
+gb.filesRenamed = {0} \u30d5\u30a1\u30a4\u30eb\u304c\u30ea\u30cd\u30fc\u30e0
+gb.missingUsername = Missing Username
+gb.edit = \u7de8\u96c6
+gb.searchTypeTooltip = Select Search Type
+gb.searchTooltip = Search {0}
+gb.delete = \u524a\u9664
+gb.docs = docs
+gb.accessRestriction = \u30a2\u30af\u30bb\u30b9\u5236\u9650
+gb.name = \u540d\u524d
+gb.enableTickets = \u30c1\u30b1\u30c3\u30c8\u3092\u6709\u52b9\u5316
+gb.enableDocs = \u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u3092\u6709\u52b9\u5316
+gb.save = \u4fdd\u5b58
+gb.showRemoteBranches = \u30ea\u30e2\u30fc\u30c8\u30d6\u30e9\u30f3\u30c1\u3092\u8868\u793a
+gb.editUsers = \u30e6\u30fc\u30b6\u30fc\u7de8\u96c6
+gb.confirmPassword = \u30d1\u30b9\u30ef\u30fc\u30c9(\u78ba\u8a8d)
+gb.restrictedRepositories = \u5236\u9650\u30ea\u30dd\u30b8\u30c8\u30ea
+gb.canAdmin = \u7ba1\u7406\u8005
+gb.notRestricted = \u533f\u540d view, clone, & push
+gb.pushRestricted = \u8a8d\u8a3c push
+gb.cloneRestricted = \u8a8d\u8a3c clone & push
+gb.viewRestricted = \u8a8d\u8a3c view, clone, & push
+gb.useTicketsDescription = \u5206\u6563\u30a4\u30b7\u30e5\u30fc\u7ba1\u7406\u30b7\u30b9\u30c6\u30e0 Ticgit \u3092\u5229\u7528\u3059\u308b
+gb.useDocsDescription = \u30ea\u30dd\u30b8\u30c8\u30ea\u5185\u306e Markdown \u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u3092\u5217\u6319\u3059\u308b
+gb.showRemoteBranchesDescription = \u30ea\u30e2\u30fc\u30c8\u30d6\u30e9\u30f3\u30c1\u3092\u8868\u793a\u3059\u308b
+gb.canAdminDescription = Gitblit\u30b5\u30fc\u30d0\u30fc\u306e\u7ba1\u7406\u8005
+gb.permittedUsers = \u8a31\u53ef\u3055\u308c\u305f\u30e6\u30fc\u30b6\u30fc
+gb.isFrozen = \u51cd\u7d50
+gb.isFrozenDescription = push\u64cd\u4f5c\u3092\u62d2\u5426\u3059\u308b
+gb.zip = zip
+gb.showReadme = readme\u8868\u793a
+gb.showReadmeDescription = \"readme\" Markdown\u30d5\u30a1\u30a4\u30eb\u3092\u6982\u8981\u30da\u30fc\u30b8\u306b\u8868\u793a\u3059\u308b
+gb.nameDescription = \u30ea\u30dd\u30b8\u30c8\u30ea\u3092\u30b0\u30eb\u30fc\u30d7\u5316\u3059\u308b\u306b\u306f '/' \u3092\u4f7f\u3046\u3002 e.g. libraries/mycoollib.git
+gb.ownerDescription = \u6240\u6709\u8005\u306f\u30ea\u30dd\u30b8\u30c8\u30ea\u306e\u8a2d\u5b9a\u3092\u5909\u66f4\u3067\u304d\u308b
+gb.blob = blob
+gb.commitActivityTrend = commit activity trend
+gb.commitActivityDOW = commit activity by day of week
+gb.commitActivityAuthors = primary authors by commit activity
+gb.feed = \u30d5\u30a3\u30fc\u30c9
+gb.cancel = \u30ad\u30e3\u30f3\u30bb\u30eb
+gb.changePassword = \u30d1\u30b9\u30ef\u30fc\u30c9\u5909\u66f4
+gb.isFederated = is federated
+gb.federateThis = federate this repository
+gb.federateOrigin = federate the origin
+gb.excludeFromFederation = \u30d5\u30a7\u30c7\u30ec\u30fc\u30b7\u30e7\u30f3\u304b\u3089\u9664\u5916\u3059\u308b
+gb.excludeFromFederationDescription = block federated Gitblit instances from pulling this account
+gb.tokens = federation tokens
+gb.tokenAllDescription = all repositories, users, & settings
+gb.tokenUnrDescription = all repositories & users
+gb.tokenJurDescription = all repositories
+gb.federatedRepositoryDefinitions = repository definitions
+gb.federatedUserDefinitions = user definitions
+gb.federatedSettingDefinitions = setting definitions
+gb.proposals = federation proposals
+gb.received = received
+gb.type = type
+gb.token = token
+gb.repositories = \u30ea\u30dd\u30b8\u30c8\u30ea
+gb.proposal = proposal
+gb.frequency = frequency
+gb.folder = \u30d5\u30a9\u30eb\u30c0\u30fc
+gb.lastPull = last pull
+gb.nextPull = next pull
+gb.inclusions = inclusions
+gb.exclusions = exclusions
+gb.registration = registration
+gb.registrations = federation registrations
+gb.sendProposal = propose
+gb.status = status
+gb.origin = origin
+gb.headRef = \u30c7\u30d5\u30a9\u30eb\u30c8\u30d6\u30e9\u30f3\u30c1 (HEAD)
+gb.headRefDescription = HEAD \u306e\u30ea\u30f3\u30af\u5148 ref \u3092\u5909\u66f4\u3059\u308b e.g. refs/heads/master
+gb.federationStrategy = \u30d5\u30a7\u30c7\u30ec\u30fc\u30b7\u30e7\u30f3\u6226\u7565
+gb.federationRegistration = federation registration
+gb.federationResults = federation pull results
+gb.federationSets = federation sets
+gb.message = \u30e1\u30c3\u30bb\u30fc\u30b8
+gb.myUrlDescription = the publicly accessible url for your Gitblit instance
+gb.destinationUrl = send to
+gb.destinationUrlDescription = the url of the Gitblit instance to send your proposal
+gb.users = \u30e6\u30fc\u30b6\u30fc
+gb.federation = \u30d5\u30a7\u30c7\u30ec\u30fc\u30b7\u30e7\u30f3
+gb.error = \u30a8\u30e9\u30fc
+gb.refresh = \u66f4\u65b0
+gb.browse = \u95b2\u89a7
+gb.clone = clone
+gb.filter = \u30d5\u30a3\u30eb\u30bf\u30fc
+gb.create = \u4f5c\u6210
+gb.servers = \u30b5\u30fc\u30d0\u30fc
+gb.recent = recent
+gb.available = available
+gb.selected = selected
+gb.size = \u30b5\u30a4\u30ba
+gb.downloading = downloading
+gb.loading = loading
+gb.starting = starting
+gb.general = \u4e00\u822c
+gb.settings = \u8a2d\u5b9a
+gb.manage = \u7ba1\u7406
+gb.lastLogin = last login
+gb.skipSizeCalculation = \u30b5\u30a4\u30ba\u8a08\u7b97\u3092\u30b9\u30ad\u30c3\u30d7
+gb.skipSizeCalculationDescription = \u30ea\u30dd\u30b8\u30c8\u30ea\u306e\u30b5\u30a4\u30ba\u3092\u8a08\u7b97\u3057\u306a\u3044 (\u30da\u30fc\u30b8\u306e\u30ed\u30fc\u30c9\u6642\u9593\u3092\u524a\u6e1b)
+gb.skipSummaryMetrics = \u6982\u8981\u3067\u306e\u6307\u6a19\u3092\u30b9\u30ad\u30c3\u30d7
+gb.skipSummaryMetricsDescription = \u6982\u8981\u30da\u30fc\u30b8\u3067\u6307\u6a19\u3092\u8a08\u7b97\u3057\u306a\u3044 (\u30da\u30fc\u30b8\u306e\u30ed\u30fc\u30c9\u6642\u9593\u3092\u524a\u6e1b)
+gb.accessLevel = \u30a2\u30af\u30bb\u30b9\u30ec\u30d9\u30eb
+gb.default = \u30c7\u30d5\u30a9\u30eb\u30c8
+gb.setDefault = \u30c7\u30d5\u30a9\u30eb\u30c8\u306b\u8a2d\u5b9a
+gb.since = since
+gb.status = status
+gb.bootDate = boot date
+gb.servletContainer = \u30b5\u30fc\u30d6\u30ec\u30c3\u30c8\u30b3\u30f3\u30c6\u30ca
+gb.heapMaximum = \u6700\u5927\u30d2\u30fc\u30d7
+gb.heapAllocated = \u78ba\u4fdd\u6e08\u307f\u30d2\u30fc\u30d7
+gb.heapUsed = \u4f7f\u7528\u30d2\u30fc\u30d7
+gb.free = free
+gb.version = \u30d0\u30fc\u30b8\u30e7\u30f3
+gb.releaseDate = \u30ea\u30ea\u30fc\u30b9\u65e5
+gb.date = date
+gb.activity = \u6d3b\u52d5
+gb.subscribe = \u8cfc\u8aad
+gb.branch = \u30d6\u30e9\u30f3\u30c1
+gb.maxHits = \u6700\u5927\u30d2\u30c3\u30c8\u6570
+gb.recentActivity = \u6700\u8fd1\u306e\u6d3b\u52d5
+gb.recentActivityStats = \u3053\u3053{0}\u65e5\u9593 / {2}\u4eba\u306e\u4f5c\u8005\u304b\u3089 {1}\u30b3\u30df\u30c3\u30c8
+gb.recentActivityNone = \u3053\u3053{0}\u65e5\u9593 / \u306a\u3057
+gb.dailyActivity = \u6bce\u65e5\u306e\u6d3b\u52d5
+gb.activeRepositories = \u6d3b\u767a\u306a\u30ea\u30dd\u30b8\u30c8\u30ea
+gb.activeAuthors = \u6d3b\u767a\u306a\u4f5c\u8005
+gb.commits = \u30b3\u30df\u30c3\u30c8
+gb.teams = \u30c1\u30fc\u30e0
+gb.teamName = \u30c1\u30fc\u30e0\u540d
+gb.teamMembers = \u30c1\u30fc\u30e0\u30e1\u30f3\u30d0\u30fc
+gb.teamMemberships = \u30c1\u30fc\u30e0
+gb.newTeam = \u30c1\u30fc\u30e0\u4f5c\u6210
+gb.permittedTeams = \u8a31\u53ef\u3055\u308c\u305f\u30c1\u30fc\u30e0
+gb.emptyRepository = \u7a7a\u306e\u30ea\u30dd\u30b8\u30c8\u30ea
+gb.repositoryUrl = \u30ea\u30dd\u30b8\u30c8\u30ea\u306eURL
+gb.mailingLists = \u30e1\u30fc\u30ea\u30f3\u30b0\u30ea\u30b9\u30c8
+gb.preReceiveScripts = pre-receive \u30b9\u30af\u30ea\u30d7\u30c8
+gb.postReceiveScripts = post-receive \u30b9\u30af\u30ea\u30d7\u30c8
+gb.hookScripts = \u30d5\u30c3\u30af\u30b9\u30af\u30ea\u30d7\u30c8
+gb.customFields = custom fields
+gb.customFieldsDescription = custom fields available to Groovy hooks
+gb.accessPermissions = \u30a2\u30af\u30bb\u30b9\u6a29\u9650
+gb.filters = \u30d5\u30a3\u30eb\u30bf\u30fc
+gb.generalDescription = \u4e00\u822c\u7684\u306a\u8a2d\u5b9a
+gb.accessPermissionsDescription = \u30e6\u30fc\u30b6\u30fc\u3068\u30c1\u30fc\u30e0\u3067\u30a2\u30af\u30bb\u30b9\u3092\u5236\u9650\u3059\u308b
+gb.accessPermissionsForUserDescription = \u30c1\u30fc\u30e0\u3092\u8a2d\u5b9a\u3059\u308b\u3001\u7279\u5b9a\u306e\u5236\u9650\u30ea\u30dd\u30b8\u30c8\u30ea\u3078\u306e\u30a2\u30af\u30bb\u30b9\u3092\u8a31\u53ef\u3059\u308b
+gb.accessPermissionsForTeamDescription = \u30c1\u30fc\u30e0\u30e1\u30f3\u30d0\u30fc\u3092\u8a2d\u5b9a\u3059\u308b\u3001\u7279\u5b9a\u306e\u5236\u9650\u30ea\u30dd\u30b8\u30c8\u30ea\u3078\u306e\u30a2\u30af\u30bb\u30b9\u3092\u8a31\u53ef\u3059\u308b
+gb.federationRepositoryDescription = \u3053\u306e\u30ea\u30dd\u30b8\u30c8\u30ea\u3092\u4ed6\u306e Gitblit \u30b5\u30fc\u30d0\u30fc\u3068\u5171\u6709\u3059\u308b
+gb.hookScriptsDescription = \u3053\u306e Gitblit \u30b5\u30fc\u30d0\u30fc\u306b push \u3055\u308c\u305f\u6642\u306b Groovy \u30b9\u30af\u30ea\u30d7\u30c8\u3092\u5b9f\u884c\u3059\u308b
+gb.reset = \u30ea\u30bb\u30c3\u30c8
+gb.pages = \u30da\u30fc\u30b8
+gb.workingCopy = \u4f5c\u696d\u30b3\u30d4\u30fc
+gb.workingCopyWarning = \u3053\u306e\u30ea\u30dd\u30b8\u30c8\u30ea\u306b\u306f\u4f5c\u696d\u30b3\u30d4\u30fc\u304c\u3042\u308b\u305f\u3081 push \u3067\u304d\u307e\u305b\u3093
+gb.query = \u30af\u30a8\u30ea\u30fc
+gb.queryHelp = \u6a19\u6e96\u7684\u306a\u30af\u30a8\u30ea\u30fc\u66f8\u5f0f\u3092\u30b5\u30dd\u30fc\u30c8\u3057\u3066\u3044\u307e\u3059\u3002<p/><p/>\u8a73\u7d30\u306f <a target="_new" href="http://lucene.apache.org/core/old_versioned_docs/versions/3_5_0/queryparsersyntax.html">Lucene Query Parser Syntax</a> \u3092\u53c2\u7167\u3057\u3066\u4e0b\u3055\u3044\u3002
+gb.queryResults = results {0} - {1} ({2} hits)
+gb.noHits = no hits
+gb.authored = authored
+gb.committed = committed
+gb.indexedBranches = \u30a4\u30f3\u30c7\u30c3\u30af\u30b9\u3059\u308b\u30d6\u30e9\u30f3\u30c1
+gb.indexedBranchesDescription = Lucene \u3067\u30a4\u30f3\u30c7\u30c3\u30af\u30b9\u3059\u308b\u30d6\u30e9\u30f3\u30c1\u3092\u9078\u629e
+gb.noIndexedRepositoriesWarning = Lucene \u3067\u30a4\u30f3\u30c7\u30c3\u30af\u30b9\u3059\u308b\u3088\u3046\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u308b\u30ea\u30dd\u30b8\u30c8\u30ea\u304c\u3042\u308a\u307e\u305b\u3093
+gb.undefinedQueryWarning = \u30af\u30a8\u30ea\u30fc\u304c\u672a\u5b9a\u7fa9\u3067\u3059!
+gb.noSelectedRepositoriesWarning = \u3072\u3068\u3064\u4ee5\u4e0a\u306e\u30ea\u30dd\u30b8\u30c8\u30ea\u3092\u9078\u629e\u3057\u3066\u4e0b\u3055\u3044!
+gb.luceneDisabled = Lucene \u30a4\u30f3\u30c7\u30c3\u30af\u30b9\u306f\u7121\u52b9\u5316\u3055\u308c\u3066\u3044\u307e\u3059
+gb.failedtoRead = \u8aad\u307f\u8fbc\u307f\u5931\u6557
+gb.isNotValidFile = is not a valid file
+gb.failedToReadMessage = {0}\u304b\u3089\u306e\u30c7\u30d5\u30a9\u30eb\u30c8\u30e1\u30c3\u30bb\u30fc\u30b8\u306e\u8aad\u307f\u8fbc\u307f\u306b\u5931\u6557\u3057\u307e\u3057\u305f!
+gb.passwordsDoNotMatch = \u30d1\u30b9\u30ef\u30fc\u30c9\u304c\u4e00\u81f4\u3057\u307e\u305b\u3093!
+gb.passwordTooShort = \u30d1\u30b9\u30ef\u30fc\u30c9\u304c\u77ed\u3059\u304e\u307e\u3059\u3002\u6700\u4f4e\u3067{0}\u6587\u5b57\u5fc5\u8981\u3067\u3059\u3002
+gb.passwordChanged = \u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5909\u66f4\u3057\u307e\u3057\u305f\u3002
+gb.passwordChangeAborted = \u30d1\u30b9\u30ef\u30fc\u30c9\u306e\u5909\u66f4\u3092\u4e2d\u6b62\u3057\u307e\u3057\u305f\u3002
+gb.pleaseSetRepositoryName = \u30ea\u30dd\u30b8\u30c8\u30ea\u540d\u3092\u8a2d\u5b9a\u3057\u3066\u4e0b\u3055\u3044\u3002
+gb.illegalLeadingSlash = \u5148\u982d\u306e\u30eb\u30fc\u30c8\u30d5\u30a9\u30eb\u30c0\u53c2\u7167(/)\u306f\u7981\u6b62\u3067\u3059\u3002
+gb.illegalRelativeSlash = \u76f8\u5bfe\u30d5\u30a9\u30eb\u30c0\u53c2\u7167(../)\u306f\u7981\u6b62\u3067\u3059\u3002
+gb.illegalCharacterRepositoryName = \u30ea\u30dd\u30b8\u30c8\u30ea\u540d\u306b''{0}''\u4e0d\u6b63\u306a\u6587\u5b57\u304c\u542b\u307e\u308c\u3066\u3044\u307e\u3059!
+gb.selectAccessRestriction = \u30a2\u30af\u30bb\u30b9\u5236\u9650\u3092\u9078\u629e\u3057\u3066\u4e0b\u3055\u3044!
+gb.selectFederationStrategy = \u30d5\u30a7\u30c7\u30ec\u30fc\u30b7\u30e7\u30f3\u6226\u7565\u3092\u9078\u629e\u3057\u3066\u4e0b\u3055\u3044!
+gb.pleaseSetTeamName = \u30c1\u30fc\u30e0\u540d\u3092\u5165\u529b\u3057\u3066\u4e0b\u3055\u3044!
+gb.teamNameUnavailable = \u30c1\u30fc\u30e0\u540d''{0}''\u306f\u5229\u7528\u3067\u304d\u307e\u305b\u3093\u3002.
+gb.teamMustSpecifyRepository = \u6700\u4f4e\u3067\u3082\u30ea\u30dd\u30b8\u30c8\u30ea\u3092\u30c1\u30fc\u30e0\u306b\u4e00\u3064\u6307\u5b9a\u3057\u3066\u4e0b\u3055\u3044\u3002
+gb.teamCreated = \u65b0\u3057\u3044\u30c1\u30fc\u30e0''{0}''\u3092\u4f5c\u6210\u3057\u307e\u3057\u305f\u3002
+gb.pleaseSetUsername = \u30e6\u30fc\u30b6\u30fc\u540d\u3092\u5165\u529b\u3057\u3066\u4e0b\u3055\u3044!
+gb.usernameUnavailable = \u30e6\u30fc\u30b6\u30fc\u540d''{0}''\u306f\u5229\u7528\u3067\u304d\u307e\u305b\u3093\u3002
+gb.combinedMd5Rename = Gitblit\u306fcombined-md5\u30d1\u30b9\u30ef\u30fc\u30c9\u30cf\u30c3\u30b7\u30e5\u304c\u6709\u52b9\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u30a2\u30ab\u30a6\u30f3\u30c8\u540d\u306e\u5909\u66f4\u3067\u306f\u65b0\u3057\u3044\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u4e0b\u3055\u3044\u3002
+gb.userCreated = \u65b0\u3057\u3044\u30e6\u30fc\u30b6\u30fc''{0}''\u3092\u4f5c\u6210\u3057\u307e\u3057\u305f\u3002
+gb.couldNotFindFederationRegistration = \u30d5\u30a7\u30c7\u30ec\u30fc\u30b7\u30e7\u30f3\u767b\u9332\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f!
+gb.failedToFindGravatarProfile = Failed to find Gravatar profile for {0}
+gb.branchStats = {0} commits and {1} tags in {2}
+gb.repositoryNotSpecified = Repository not specified!
+gb.repositoryNotSpecifiedFor = Repository not specified for {0}!
+gb.canNotLoadRepository = \u30ea\u30dd\u30b8\u30c8\u30ea\u3092\u30ed\u30fc\u30c9\u3067\u304d\u307e\u305b\u3093
+gb.commitIsNull = \u30b3\u30df\u30c3\u30c8\u304c\u7a7a\u3067\u3059
+gb.unauthorizedAccessForRepository = \u30ea\u30dd\u30b8\u30c8\u30ea\u3078\u306e\u30a2\u30af\u30bb\u30b9\u6a29\u304c\u3042\u308a\u307e\u305b\u3093
+gb.failedToFindCommit = Failed to find commit \"{0}\" in {1} for {2} page!
+gb.couldNotFindFederationProposal = Could not find federation proposal!
+gb.invalidUsernameOrPassword = \u30e6\u30fc\u30b6\u30fc\u540d\u307e\u305f\u306f\u30d1\u30b9\u30ef\u30fc\u30c9\u304c\u7121\u52b9\u3067\u3059!
+gb.OneProposalToReview = There is 1 federation proposal awaiting review.
+gb.nFederationProposalsToReview = There are {0} federation proposals awaiting review.
+gb.couldNotFindTag = {0} \u30bf\u30b0\u3092\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f
+gb.couldNotCreateFederationProposal = Could not create federation proposal!
+gb.pleaseSetGitblitUrl = Please enter your Gitblit url!
+gb.pleaseSetDestinationUrl = Please enter a destination url for your proposal!
+gb.proposalReceived = Proposal successfully received by {0}.
+gb.noGitblitFound = Sorry, {0} could not find a Gitblit instance at {1}.
+gb.noProposals = Sorry, {0} is not accepting proposals at this time.
+gb.noFederation = Sorry, {0} is not configured to federate with any Gitblit instances.
+gb.proposalFailed = Sorry, {0} did not receive any proposal data!
+gb.proposalError = Sorry, {0} reports that an unexpected error occurred!
+gb.failedToSendProposal = Failed to send proposal!
+gb.userServiceDoesNotPermitAddUser = {0} does not permit adding a user account!
+gb.userServiceDoesNotPermitPasswordChanges = {0} does not permit password changes!
+gb.displayName = display name
+gb.emailAddress = email address
+gb.errorAdminLoginRequired = Administration requires a login
+gb.errorOnlyAdminMayCreateRepository = Only an administrator may create a repository
+gb.errorOnlyAdminOrOwnerMayEditRepository = Only an administrator or the owner may edit a repository
+gb.errorAdministrationDisabled = Administration is disabled
+gb.lastNDays = last {0} days
+gb.completeGravatarProfile = Complete profile on Gravatar.com
+gb.none = none
+gb.line = line
+gb.content = content
+gb.empty = empty
+gb.inherited = inherited
+gb.deleteRepository = Delete repository \"{0}\"?
+gb.repositoryDeleted = Repository ''{0}'' deleted.
+gb.repositoryDeleteFailed = Failed to delete repository ''{0}''!
+gb.deleteUser = Delete user \"{0}\"?
+gb.userDeleted = User ''{0}'' deleted.
+gb.userDeleteFailed = Failed to delete user ''{0}''!
+gb.time.justNow = \u305f\u3063\u305f\u4eca
+gb.time.today = \u4eca\u65e5
+gb.time.yesterday = \u6628\u65e5
+gb.time.minsAgo = {0}\u5206\u524d
+gb.time.hoursAgo = {0}\u6642\u9593\u524d
+gb.time.daysAgo = {0}\u65e5\u524d
+gb.time.weeksAgo = {0}\u9031\u524d
+gb.time.monthsAgo = {0}\u6708\u524d
+gb.time.oneYearAgo = 1\u5e74\u524d
+gb.time.yearsAgo = {0}\u5e74\u524d
+gb.duration.oneDay = 1\u65e5
+gb.duration.days = {0}\u65e5
+gb.duration.oneMonth = 1\u30f6\u6708
+gb.duration.months = {0}\u30f6\u6708
+gb.duration.oneYear = 1\u5e74
+gb.duration.years = {0}\u5e74
+gb.authorizationControl = \u6a29\u9650\u5236\u5fa1
+gb.allowAuthenticatedDescription = \u5168\u3066\u306e\u8a8d\u8a3c\u6e08\u307f\u30e6\u30fc\u30b6\u30fc\u3078\u30a2\u30af\u30bb\u30b9\u3092\u8a31\u53ef\u3059\u308b
+gb.allowNamedDescription = \u6307\u5b9a\u3057\u305f\u540d\u524d\u306e\u30e6\u30fc\u30b6\u30fc/\u30c1\u30fc\u30e0\u3078\u30a2\u30af\u30bb\u30b9\u3092\u8a31\u53ef\u3059\u308b
+gb.markdownFailure = Markdown \u306e\u30d1\u30fc\u30b9\u306b\u5931\u6557\u3057\u307e\u3057\u305f!
+gb.sessionEnded = Session has been closed
+gb.closeBrowser = Please close the browser to properly end the session.
diff --git a/src/main/java/com/gitblit/wicket/GitBlitWebApp_ko.properties b/src/main/java/com/gitblit/wicket/GitBlitWebApp_ko.properties
new file mode 100644
index 00000000..42915df8
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/GitBlitWebApp_ko.properties
@@ -0,0 +1,445 @@
+gb.repository = \uC800\uC7A5\uC18C
+gb.owner = \uC18C\uC720\uC790
+gb.description = \uC124\uBA85
+gb.lastChange = \uCD5C\uADFC \uBCC0\uACBD
+gb.refs = refs
+gb.tag = \uD0DC\uADF8
+gb.tags = \uD0DC\uADF8
+gb.author = \uC791\uC131\uC790
+gb.committer = \uCEE4\uBBF8\uD130
+gb.commit = \uCEE4\uBC0B
+gb.tree = \uD2B8\uB9AC
+gb.parent = \uBD80\uBAA8
+gb.url = URL
+gb.history = \uD788\uC2A4\uD1A0\uB9AC
+gb.raw = raw
+gb.object = object
+gb.ticketId = \uD2F0\uCF13 id
+gb.ticketAssigned = \uD560\uB2F9
+gb.ticketOpenDate = \uC5F4\uB9B0 \uB0A0\uC790
+gb.ticketState = \uC0C1\uD0DC
+gb.ticketComments = \uCF54\uBA58\uD2B8
+gb.view = \uBCF4\uAE30
+gb.local = \uB85C\uCEEC
+gb.remote = \uB9AC\uBAA8\uD2B8
+gb.branches = \uBE0C\uB79C\uCE58
+gb.patch = \uD328\uCE58
+gb.diff = \uCC28\uC774
+gb.log = \uB85C\uADF8
+gb.moreLogs = \uCEE4\uBC0B \uB354 \uBCF4\uAE30...
+gb.allTags = \uBAA8\uB4E0 \uD0DC\uADF8...
+gb.allBranches = \uBAA8\uB4E0 \uBE0C\uB79C\uCE58...
+gb.summary = \uC694\uC57D
+gb.ticket = \uD2F0\uCF13
+gb.newRepository = \uC0C8 \uC800\uC7A5\uC18C
+gb.newUser = \uB0B4 \uC0AC\uC6A9\uC790
+gb.commitdiff = \uCEE4\uBC0B\uBE44\uAD50
+gb.tickets = \uD2F0\uCF13
+gb.pageFirst = \uCCAB
+gb.pagePrevious = \uC774\uC804
+gb.pageNext = \uB2E4\uC74C
+gb.head = HEAD
+gb.blame = blame
+gb.login = \uB85C\uADF8\uC778
+gb.logout = \uB85C\uADF8\uC544\uC6C3
+gb.username = \uC720\uC800\uB124\uC784
+gb.password = \uD328\uC2A4\uC6CC\uB4DC
+gb.tagger = \uD0DC\uAC70
+gb.moreHistory = \uD788\uC2A4\uD1A0\uB9AC \uB354 \uBCF4\uAE30...
+gb.difftocurrent = \uD604\uC7AC\uC640 \uBE44\uAD50
+gb.search = \uAC80\uC0C9
+gb.searchForAuthor = \uCEE4\uBC0B\uC744 \uC791\uC131\uC790\uB85C \uAC80\uC0C9
+gb.searchForCommitter = \uCEE4\uBC0B\uC744 \uCEE4\uBC0B\uD130\uB85C \uAC80\uC0C9
+gb.addition = \uCD94\uAC00
+gb.modification = \uBCC0\uACBD
+gb.deletion = \uC0AD\uC81C
+gb.rename = \uC774\uB984\uBCC0\uACBD
+gb.metrics = \uBA54\uD2B8\uB9AD
+gb.stats = \uC0C1\uD0DC
+gb.markdown = \uB9C8\uD06C\uB2E4\uC6B4
+gb.changedFiles = \uD30C\uC77C \uBCC0\uACBD\uB428
+gb.filesAdded = {0} \uD30C\uC77C \uCD94\uAC00\uB428
+gb.filesModified = {0} \uD30C\uC77C \uBCC0\uACBD\uB428
+gb.filesDeleted = {0} \uD30C\uC77C \uC0AD\uC81C\uB428
+gb.filesCopied = {0} \uD30C\uC77C \uBCF5\uC0AC\uB428
+gb.filesRenamed = {0} \uD30C\uC77C \uC774\uB984 \uBCC0\uACBD\uB428
+gb.missingUsername = \uC720\uC800\uB124\uC784 \uB204\uB77D
+gb.edit = \uC218\uC815
+gb.searchTypeTooltip = \uAC80\uC0C9 \uD0C0\uC785 \uC120\uD0DD
+gb.searchTooltip = {0} \uAC80\uC0C9
+gb.delete = \uC0AD\uC81C
+gb.docs = \uBB38\uC11C
+gb.accessRestriction = \uC811\uC18D \uC81C\uD55C
+gb.name = \uC774\uB984
+gb.enableTickets = \uD2F0\uCF13 \uC0AC\uC6A9
+gb.enableDocs = \uBB38\uC11C \uC0AC\uC6A9
+gb.save = \uC800\uC7A5
+gb.showRemoteBranches = \uB9AC\uBAA8\uD2B8 \uBE0C\uB79C\uCE58 \uBCF4\uAE30
+gb.editUsers = \uC720\uC800 \uC218\uC815
+gb.confirmPassword = \uD328\uC2A4\uC6CC\uB4DC \uD655\uC778
+gb.restrictedRepositories = \uC81C\uD55C\uB41C \uC800\uC7A5\uC18C
+gb.canAdmin = \uAD00\uB9AC \uAC00\uB2A5
+gb.notRestricted = \uC775\uBA85 view, clone, & push
+gb.pushRestricted = \uD5C8\uC6A9\uB41C \uC720\uC800\uB9CC push
+gb.cloneRestricted = \uD5C8\uC6A9\uB41C \uC720\uC800\uB9CC clone & push
+gb.viewRestricted = \uD5C8\uC6A9\uB41C \uC720\uC800\uB9CC view, clone, & push
+gb.useTicketsDescription = Ticgit(\uBD84\uC0B0 \uD2F0\uCF13 \uC2DC\uC2A4\uD15C) \uC774\uC288 \uC0AC\uC6A9
+gb.useDocsDescription = \uC800\uC7A5\uC18C \uC788\uB294 \uB9C8\uD06C\uB2E4\uC6B4 \uBB38\uC11C \uC0AC\uC6A9
+gb.showRemoteBranchesDescription = \uB9AC\uBAA8\uD2B8 \uBE0C\uB79C\uCE58 \uBCF4\uAE30
+gb.canAdminDescription = Gitblit \uAD00\uB9AC \uAD8C\uD55C \uBD80\uC5EC
+gb.permittedUsers = \uD5C8\uC6A9\uB41C \uC0AC\uC6A9\uC790
+gb.isFrozen = \uD504\uB9AC\uC9D5\uB428
+gb.isFrozenDescription = \uD478\uC2DC \uCC28\uB2E8
+gb.zip = zip
+gb.showReadme = \uB9AC\uB4DC\uBBF8(readme) \uBCF4\uAE30
+gb.showReadmeDescription = \uC694\uC57D\uD398\uC774\uC9C0\uC5D0\uC11C \"readme\" \uB9C8\uD06C\uB2E4\uC6B4 \uD30C\uC77C \uBCF4\uAE30
+gb.nameDescription = \uC800\uC7A5\uC18C\uB97C \uADF8\uB8F9\uC73C\uB85C \uBB36\uC73C\uB824\uBA74 '/' \uB97C \uC0AC\uC6A9. \uC608) libraries/reponame.git
+gb.ownerDescription = \uC18C\uC720\uC790\uB294 \uC800\uC7A5\uC18C \uC124\uC815\uC744 \uBCC0\uACBD\uD560 \uC218 \uC788\uC74C
+gb.blob = blob
+gb.commitActivityTrend = \uCEE4\uBC0B \uD65C\uB3D9 \uD2B8\uB79C\uB4DC
+gb.commitActivityDOW = 1\uC8FC\uC77C\uC758 \uC77C\uB2E8\uC704 \uCEE4\uBC0B \uD65C\uB3D9
+gb.commitActivityAuthors = \uCEE4\uBC0B \uD65C\uB3D9\uC758 \uC8FC \uC791\uC131\uC790
+gb.feed = \uD53C\uB4DC
+gb.cancel = \uCDE8\uC18C
+gb.changePassword = \uD328\uC2A4\uC6CC\uB4DC \uBCC0\uACBD
+gb.isFederated = \uD398\uB354\uB808\uC774\uC158\uB428
+gb.federateThis = \uC774 \uC800\uC7A5\uC18C\uB97C \uD398\uB354\uB808\uC774\uC158\uD568
+gb.federateOrigin = origin \uC5D0 \uD398\uB354\uB808\uC774\uC158
+gb.excludeFromFederation = \uD398\uB354\uB808\uC774\uC158 \uC81C\uC678
+gb.excludeFromFederationDescription = \uC774 \uACC4\uC815\uC73C\uB85C \uD480\uB9C1\uB418\uB294 \uD398\uB7EC\uB808\uC774\uC158 \uB41C Gitblit \uC778\uC2A4\uD134\uC2A4 \uCC28\uB2E8
+gb.tokens = \uD398\uB354\uB808\uC774\uC158 \uD1A0\uD070
+gb.tokenAllDescription = \uBAA8\uB4E0 \uC800\uC7A5\uC18C, \uD1A0\uD070, \uC0AC\uC6A9\uC790 & \uC124\uC815
+gb.tokenUnrDescription = \uBAA8\uB4E0 \uC800\uC7A5\uC18C & \uC0AC\uC6A9\uC790
+gb.tokenJurDescription = \uBAA8\uB4E0 \uC800\uC7A5\uC18C
+gb.federatedRepositoryDefinitions = \uC800\uC7A5\uC18C \uC815\uC758
+gb.federatedUserDefinitions = \uC0AC\uC6A9\uC790 \uC815\uC758
+gb.federatedSettingDefinitions = \uC124\uC815 \uC815\uC758
+gb.proposals = \uD398\uB354\uB808\uC774\uC158 \uC81C\uC548
+gb.received = \uC218\uC2E0\uD568
+gb.type = \uD0C0\uC785
+gb.token = \uD1A0\uD070
+gb.repositories = \uC800\uC7A5\uC18C
+gb.proposal = \uC81C\uC548
+gb.frequency = \uBE48\uB3C4
+gb.folder = \uD3F4\uB354
+gb.lastPull = \uB9C8\uC9C0\uB9C9 \uD480
+gb.nextPull = \uB2E4\uC74C \uD480
+gb.inclusions = \uD3EC\uD568
+gb.exclusions = \uC81C\uC678
+gb.registration = \uB4F1\uB85D
+gb.registrations = \uD398\uB354\uB808\uC774\uC158 \uB4F1\uB85D
+gb.sendProposal = \uC81C\uC548\uD558\uAE30
+gb.status = \uC0C1\uD0DC
+gb.origin = origin
+gb.headRef = \uB514\uD3F4\uD2B8 \uBE0C\uB79C\uCE58(HEAD)
+gb.headRefDescription = \uB514\uD3F4\uD2B8 \uBE0C\uB79C\uCE58\uB97C \uC785\uB825. \uC608) refs/heads/master
+gb.federationStrategy = \uD398\uB354\uB808\uC774\uC158 \uC815\uCC45
+gb.federationRegistration = \uD398\uB354\uB808\uC774\uC158 \uB4F1\uB85D
+gb.federationResults = \uD398\uB354\uB808\uC774\uC158 \uD480 \uACB0\uACFC
+gb.federationSets = \uD398\uB354\uB808\uC774\uC158 \uC14B
+gb.message = \uBA54\uC2DC\uC9C0
+gb.myUrlDescription = \uACF5\uAC1C\uB418\uC5B4 \uC811\uC18D\uD560 \uC218 \uC788\uB294 Gitblit \uC778\uC2A4\uD134\uC2A4 url
+gb.destinationUrl = \uB85C \uBCF4\uB0C4
+gb.destinationUrlDescription = \uC81C\uC548\uC744 \uC804\uC1A1\uD560 \uB300\uC0C1 Gitblit \uC778\uC2A4\uD134\uC2A4\uC758 url
+gb.users = \uC720\uC800
+gb.federation = \uD398\uB354\uB808\uC774\uC158
+gb.error = \uC5D0\uB7EC
+gb.refresh = \uC0C8\uB85C\uACE0\uCE68
+gb.browse = \uBE0C\uB77C\uC6B0\uC988
+gb.clone = clone
+gb.filter = \uD544\uD130
+gb.create = \uC0DD\uC131
+gb.servers = \uC11C\uBC84
+gb.recent = recent
+gb.available = \uAC00\uB2A5\uD55C
+gb.selected = \uC120\uD0DD\uB41C
+gb.size = \uD06C\uAE30
+gb.downloading = \uB2E4\uC6B4\uB85C\uB4DC\uC911
+gb.loading = \uB85C\uB529\uC911
+gb.starting = \uC2DC\uC791\uC911
+gb.general = \uC77C\uBC18
+gb.settings = \uC138\uD305
+gb.manage = \uAD00\uB9AC
+gb.lastLogin = \uB9C8\uC9C0\uB9C9 \uB85C\uADF8\uC778
+gb.skipSizeCalculation = \uD06C\uAE30 \uACC4\uC0B0 \uBB34\uC2DC
+gb.skipSizeCalculationDescription = \uC800\uC7A5\uC18C \uD06C\uAE30 \uACC4\uC0B0\uD558\uC9C0 \uC54A\uC74C (\uD398\uC774\uC9C0 \uB85C\uB529 \uC2DC\uAC04 \uB2E8\uCD95\uB428)
+gb.skipSummaryMetrics = \uBA54\uD2B8\uB9AD \uC694\uC57D \uBB34\uC2DC
+gb.skipSummaryMetricsDescription = \uC694\uC57D \uD398\uC9C0\uC774\uC5D0\uC11C \uBA54\uD2B8\uB9AD \uACC4\uC0B0\uD558\uC9C0 \uC54A\uC74C (\uD398\uC774\uC9C0 \uB85C\uB529 \uC2DC\uAC04 \uB2E8\uCD95\uB428)
+gb.accessLevel = \uC811\uC18D \uB808\uBCA8
+gb.default = \uB514\uD3F4\uD2B8
+gb.setDefault = \uB514\uD3F4\uD2B8 \uC124\uC815
+gb.since = since
+gb.status = \uC0C1\uD0DC
+gb.bootDate = \uBD80\uD305 \uC77C\uC790
+gb.servletContainer = \uC11C\uBE14\uB9BF \uCEE8\uD14C\uC774\uB108
+gb.heapMaximum = \uB9E5\uC2DC\uBA48 \uD799
+gb.heapAllocated = \uD560\uB2F9\uB41C \uD799
+gb.heapUsed = \uC0AC\uC6A9\uB41C \uD799
+gb.free = \uD504\uB9AC
+gb.version = \uBC84\uC804
+gb.releaseDate = \uB9B4\uB9AC\uC988 \uB0A0\uC9DC
+gb.date = date
+gb.activity = \uC561\uD2F0\uBE44\uD2F0
+gb.subscribe = \uAD6C\uB3C5
+gb.branch = \uBE0C\uB79C\uCE58
+gb.maxHits = \uB9E5\uC2A4\uD788\uD2B8
+gb.recentActivity = \uCD5C\uADFC \uC561\uD2F0\uBE44\uD2F0
+gb.recentActivityStats = \uC9C0\uB09C {0} \uC77C / {2} \uC5D0 \uC758\uD574 {1} \uAC1C \uCEE4\uBC0B \uB428
+gb.recentActivityNone = \uC9C0\uB09C {0} \uC77C / \uC5C6\uC74C
+gb.dailyActivity = \uC77C\uC77C \uC561\uD2F0\uBE44\uD2F0
+gb.activeRepositories = \uC0AC\uC6A9\uC911\uC778 \uC800\uC7A5\uC18C
+gb.activeAuthors = \uC0AC\uC6A9\uC911\uC778 \uC791\uC131\uC790
+gb.commits = \uCEE4\uBC0B
+gb.teams = \uD300
+gb.teamName = \uD300 \uC774\uB984
+gb.teamMembers = \uD300 \uBA64\uBC84
+gb.teamMemberships = \uD300 \uB9F4\uBC84\uC27D
+gb.newTeam = \uC0C8\uB85C\uC6B4 \uD300
+gb.permittedTeams = \uD5C8\uC6A9\uB41C \uD300
+gb.emptyRepository = \uBE48 \uC800\uC7A5\uC18C
+gb.repositoryUrl = \uC800\uC7A5\uC18C url
+gb.mailingLists = \uBA54\uC77C\uB9C1 \uB9AC\uC2A4\uD2B8
+gb.preReceiveScripts = pre-receive \uC2A4\uD06C\uB9BD\uD2B8
+gb.postReceiveScripts = post-receive \uC2A4\uD06C\uB9BD\uD2B8
+gb.hookScripts = \uD6C4\uD06C \uC2A4\uD06C\uB9BD\uD2B8
+gb.customFields = \uC0AC\uC6A9\uC790 \uD544\uB4DC
+gb.customFieldsDescription = \uADF8\uB8E8\uBE44 \uD6C5\uC5D0 \uC0AC\uC6A9\uC790 \uD544\uB4DC \uC0AC\uC6A9 \uAC00\uB2A5
+gb.accessPermissions = \uC811\uC18D \uAD8C\uD55C
+gb.filters = \uD544\uD130
+gb.generalDescription = \uC77C\uBC18 \uC124\uC815
+gb.accessPermissionsDescription = \uC720\uC800\uC640 \uD300\uC73C\uB85C \uC811\uC18D\uAD8C\uD55C \uBD80\uC5EC
+gb.accessPermissionsForUserDescription = \uD300\uC744 \uC9C0\uC815\uD558\uAC70\uB098 \uC811\uC18D \uAD8C\uD55C\uC744 \uC9C0\uC815\uD560 \uC800\uC7A5\uC18C \uC120\uD0DD
+gb.accessPermissionsForTeamDescription = \uD300 \uB9F4\uBC84\uB97C \uC120\uD0DD\uD558\uACE0, \uC811\uC18D \uAD8C\uD55C\uC744 \uC9C0\uC815\uD560 \uC800\uC7A5\uC18C \uC120\uD0DD
+gb.federationRepositoryDescription = \uC774 \uC800\uC7A5\uC18C\uB97C \uB2E4\uB978 Gitblit \uC11C\uBC84\uC640 \uACF5\uC720
+gb.hookScriptsDescription = \uC774 Gitblit \uC11C\uBC84\uC5D0 \uD478\uC2DC\uB418\uBA74 \uADF8\uB8E8\uBE44(Groovy) \uC2A4\uD06C\uB9BD\uD2B8\uB97C \uC2E4\uD589
+gb.reset = \uB9AC\uC14B
+gb.pages = \uD398\uC774\uC9C0
+gb.workingCopy = \uC6CC\uD0B9 \uCE74\uD53C
+gb.workingCopyWarning = \uC774 \uC800\uC7A5\uC18C\uB294 \uC6CC\uD0B9\uCE74\uD53C\uB97C \uAC00\uC9C0\uACE0 \uC788\uACE0 \uD478\uC2DC\uB97C \uBC1B\uC744 \uC218 \uC5C6\uC74C
+gb.query = \uCFFC\uB9AC
+gb.queryHelp = \uD45C\uC900 \uCFFC\uB9AC \uBB38\uBC95\uC744 \uC9C0\uC6D0.<p/><p/>\uC790\uC138\uD55C \uAC83\uC744 \uC6D0\uD55C\uB2E4\uBA74 <a target="_new" href="http://lucene.apache.org/core/old_versioned_docs/versions/3_5_0/queryparsersyntax.html">Lucene Query Parser Syntax</a> \uC744 \uBC29\uBB38\uD574 \uC8FC\uC138\uC694.
+gb.queryResults = \uAC80\uC0C9\uACB0\uACFC {0} - {1} ({2}\uAC1C \uAC80\uC0C9\uB428)
+gb.noHits = \uAC80\uC0C9 \uACB0\uACFC \uC5C6\uC74C
+gb.authored = \uAC00 \uC791\uC131\uD568.
+gb.committed = \uCEE4\uBC0B\uB428
+gb.indexedBranches = \uC778\uB371\uC2F1\uD560 \uBE0C\uB79C\uCE58
+gb.indexedBranchesDescription = \uB8E8\uC2E0 \uC778\uB371\uC2A4\uC5D0 \uD3EC\uD568\uD560 \uBE0C\uB79C\uCE58 \uC120\uD0DD
+gb.noIndexedRepositoriesWarning = \uC800\uC7A5\uC18C\uAC00 \uB8E8\uC2E0 \uC778\uB371\uC2F1\uC5D0 \uC124\uC815\uB418\uC9C0 \uC54A\uC74C
+gb.undefinedQueryWarning = \uCFFC\uB9AC \uC9C0\uC815\uB418\uC9C0 \uC54A\uC74C!
+gb.noSelectedRepositoriesWarning = \uD558\uB098 \uB610\uB294 \uADF8 \uC774\uC0C1\uC758 \uC800\uC7A5\uC18C\uB97C \uC120\uD0DD\uD558\uC138\uC694!
+gb.luceneDisabled = \uB8E8\uC2E0 \uC778\uB371\uC2F1 \uC911\uC9C0\uB428
+gb.failedtoRead = \uC77C\uAE30 \uC2E4\uD328
+gb.isNotValidFile = \uC720\uD6A8\uD55C \uD30C\uC77C\uC774 \uC544\uB2D8
+gb.failedToReadMessage = {0}\uC5D0\uC11C \uB514\uD3F4\uD2B8 \uBA54\uC2DC\uC9C0 \uC77C\uAE30 \uC2E4\uD328!
+gb.passwordsDoNotMatch = \uD328\uC2A4\uC6CC\uB4DC\uAC00 \uC77C\uCE58\uD558\uC9C0 \uC54A\uC544\uC694!
+gb.passwordTooShort = \uD328\uC2A4\uC6CC\uB4DC\uAC00 \uB108\uBB34 \uC9E7\uC544\uC694. \uC801\uC5B4\uB3C4 {0} \uAC1C \uBB38\uC790\uC5EC\uC57C \uD569\uB2C8\uB2E4.
+gb.passwordChanged = \uD328\uC2A4\uC6CC\uB4DC\uAC00 \uBCC0\uACBD \uC131\uACF5.
+gb.passwordChangeAborted = \uD328\uC2A4\uC6CC\uB4DC \uBCC0\uACBD \uCDE8\uC18C\uB428.
+gb.pleaseSetRepositoryName = \uC800\uC7A5\uC18C \uC774\uB984\uC744 \uC785\uB825\uD558\uC138\uC694!
+gb.illegalLeadingSlash = \uC800\uC7A5\uC18C \uC774\uB984 \uB610\uB294 \uD3F4\uB354\uB294 (/) \uB85C \uC2DC\uC791\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.
+gb.illegalRelativeSlash = \uC0C1\uB300 \uACBD\uB85C \uC9C0\uC815 (../) \uC740 \uC0AC\uC6A9\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4..
+gb.illegalCharacterRepositoryName = \uBB38\uC790 ''{0}'' \uC800\uC7A5\uC18C \uC774\uB984\uC5D0 \uC0AC\uC6A9\uD560 \uC218 \uC5C6\uC5B4\uC694!
+gb.selectAccessRestriction = \uC811\uC18D \uAD8C\uD55C\uC744 \uC120\uD0DD\uD558\uC138\uC694!
+gb.selectFederationStrategy = \uD398\uB354\uB808\uC774\uC158 \uC815\uCC45\uC744 \uC120\uD0DD\uD558\uC138\uC694!
+gb.pleaseSetTeamName = \uD300\uC774\uB984\uC744 \uC785\uB825\uD558\uC138\uC694!
+gb.teamNameUnavailable = ''{0}'' \uD300\uC740 \uC0AC\uC6A9\uD560 \uC218 \uC5C6\uC5B4\uC694.
+gb.teamMustSpecifyRepository = \uD300\uC740 \uC801\uC5B4\uB3C4 \uD558\uB098\uC758 \uC800\uC7A5\uC18C\uB97C \uC9C0\uC815\uD574\uC57C \uD569\uB2C8\uB2E4.
+gb.teamCreated = \uC0C8\uB85C\uC6B4 \uD300 ''{0}'' \uC0DD\uC131 \uC644\uB8CC.
+gb.pleaseSetUsername = \uC720\uC800\uB124\uC784\uC744 \uC785\uB825\uD558\uC138\uC694!
+gb.usernameUnavailable = ''{0}'' \uC720\uC800\uB124\uC784\uC740 \uC0AC\uC6A9\uD560 \uC218 \uC5C6\uC5B4\uC694.
+gb.combinedMd5Rename = Gitblit \uC740 combined-md5 \uD574\uC2F1 \uD328\uC2A4\uC6CC\uB4DC\uB85C \uC124\uC815\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uACC4\uC815 \uC774\uB984 \uBCC0\uACBD \uC2DC \uC0C8 \uD328\uC2A4\uC6CC\uB4DC\uB97C \uC785\uB825\uD574\uC57C \uD569\uB2C8\uB2E4.
+gb.userCreated = \uC0C8\uB85C\uC6B4 \uC720\uC800 ''{0}'' \uC0DD\uC131 \uC644\uB8CC.
+gb.couldNotFindFederationRegistration = \uD398\uB354\uB808\uC774\uC158 \uB4F1\uB85D\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4!
+gb.failedToFindGravatarProfile = {0} \uC758 Gravatar \uD504\uB85C\uD30C\uC77C \uCC3E\uAE30 \uC2E4\uD328
+gb.branchStats = {2} \uC548\uC5D0 {0} \uCEE4\uBC0B {1} \uD0DC\uADF8
+gb.repositoryNotSpecified = \uC800\uC7A5\uC18C\uAC00 \uC9C0\uC815\uB418\uC9C0 \uC54A\uC74C!
+gb.repositoryNotSpecifiedFor = {0} \uB97C \uC704\uD55C \uC800\uC7A5\uC18C\uAC00 \uC9C0\uC815\uB418\uC9C0 \uC54A\uC74C!
+gb.canNotLoadRepository = \uC800\uC7A5\uC18C\uB97C \uBD88\uB7EC\uC62C \uC218 \uC5C6\uC74C
+gb.commitIsNull = \uB110 \uCEE4\uBC0B
+gb.unauthorizedAccessForRepository = \uC800\uC7A5\uC18C\uC5D0 \uC811\uADFC \uD5C8\uC6A9\uB418\uC9C0 \uC54A\uC74C
+gb.failedToFindCommit = \uCEE4\uBC0B\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC74C \"{0}\" in {1} for {2} \uD398\uC774\uC9C0!
+gb.couldNotFindFederationProposal = \uD398\uB354\uB808\uC774\uC158 \uC81C\uC548\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4!
+gb.invalidUsernameOrPassword = \uC798\uBABB\uB41C \uC720\uC800\uB124\uC784 \uB610\uB294 \uD328\uC2A4\uC6CC\uB4DC!
+gb.OneProposalToReview = \uB9AC\uBDF0\uB97C \uAE30\uB2E4\uB9AC\uACE0 \uC788\uB294 1\uAC1C\uC758 \uD398\uB354\uB808\uC774\uC158 \uC81C\uC548\uC774 \uC788\uC2B5\uB2C8\uB2E4.
+gb.nFederationProposalsToReview = \uB9AC\uBDF0\uB97C \uAE30\uB2E4\uB9AC\uACE0 \uC788\uB294 {0} \uAC1C\uC758 \uD398\uB354\uB808\uC774\uC158 \uC81C\uC548\uC774 \uC788\uC2B5\uB2C8\uB2E4.
+gb.couldNotFindTag = \uD0DC\uADF8 {0} \uB97C(\uC744) \uCC3E\uC744 \uC218 \uC5C6\uC74C
+gb.couldNotCreateFederationProposal = \uD398\uB354\uB808\uC774\uC158 \uC81C\uC548 \uC0DD\uC131 \uC2E4\uD328!
+gb.pleaseSetGitblitUrl = Gitblit url \uC744 \uC785\uB825\uD558\uC138\uC694!
+gb.pleaseSetDestinationUrl = \uB2F9\uC2E0\uC758 \uC81C\uC548\uC5D0 \uB300\uD55C \uB300\uC0C1 url \uC744 \uC785\uB825\uD558\uC138\uC694!
+gb.proposalReceived = {0} \uC758 \uC81C\uC548 \uC131\uACF5\uC801 \uC218\uC2E0
+gb.noGitblitFound = \uC8C4\uC1A1\uD569\uB2C8\uB2E4, Gitblit \uC778\uC2A4\uD134\uC2A4 {1} \uC5D0\uC11C {0} \uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.
+gb.noProposals = \uC8C4\uC1A1\uD569\uB2C8\uB2E4, \uC774\uBC88\uC5D0\uB294 {0} \uC758 \uC81C\uC548\uC744 \uC218\uC6A9\uD558\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.
+gb.noFederation = \uC8C4\uC1A1\uD569\uB2C8\uB2E4, {0} \uC5D0\uB294 \uD398\uB354\uB808\uC774\uC158 \uC124\uC815\uB41C Gitblit \uC778\uC2A4\uD134\uC2A4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.
+gb.proposalFailed = \uC8C4\uC1A1\uD569\uB2C8\uB2E4, {0} \uC5D0\uB294 \uC81C\uC548 \uB370\uC774\uD130\uB97C \uBC1B\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.
+gb.proposalError = \uC8C4\uC1A1\uD569\uB2C8\uB2E4, {0} \uC5D0 \uB300\uD55C \uC624\uB958 \uBC1C\uC0DD \uBCF4\uACE0
+gb.failedToSendProposal = \uC81C\uC548 \uBCF4\uB0B4\uAE30 \uC2E4\uD328!
+gb.userServiceDoesNotPermitAddUser = {0} \uC0C8\uB85C\uC6B4 \uC720\uC800\uB97C \uCD94\uAC00\uD560 \uC218 \uC5C6\uC74C!
+gb.userServiceDoesNotPermitPasswordChanges = {0} \uD328\uC2A4\uC6CC\uB4DC\uB97C \uBCC0\uACBD\uD560 \uC218 \uC5C6\uC74C!
+gb.displayName = \uD45C\uC2DC\uB418\uB294 \uC774\uB984
+gb.emailAddress = \uC774\uBA54\uC77C \uC8FC\uC18C
+gb.errorAdminLoginRequired = \uAD00\uB9AC\uB97C \uC704\uD574\uC11C\uB294 \uB85C\uADF8\uC778\uC774 \uD544\uC694
+gb.errorOnlyAdminMayCreateRepository = \uAD00\uB9AC\uC790\uB9CC \uC800\uC7A5\uC18C\uB97C \uB9CC\uB4E4\uC218 \uC788\uC74C
+gb.errorOnlyAdminOrOwnerMayEditRepository = \uAD00\uB9AC\uC790\uC640 \uC18C\uC720\uC790\uB9CC \uC800\uC7A5\uC18C\uB97C \uC218\uC815\uD560 \uC218 \uC788\uC74C
+gb.errorAdministrationDisabled = \uAD00\uB9AC\uAE30\uB2A5 \uBE44\uD65C\uC131\uD654\uB428
+gb.lastNDays = {0} \uC77C\uC804
+gb.completeGravatarProfile = Gravatar.com \uC5D0 \uD504\uB85C\uD30C\uC77C \uC0DD\uC131\uB428
+gb.none = none
+gb.line = \uB77C\uC778
+gb.content = \uB0B4\uC6A9
+gb.empty = empty
+gb.inherited = \uC0C1\uC18D
+gb.deleteRepository = \"{0}\" \uC800\uC7A5\uC18C\uB97C \uC0AD\uC81C\uD560\uAE4C\uC694?
+gb.repositoryDeleted = ''{0}'' \uC800\uC7A5\uC18C \uC0AD\uC81C\uB428.
+gb.repositoryDeleteFailed = ''{0}'' \uC800\uC7A5\uC18C \uC0AD\uC81C \uC2E4\uD328!
+gb.deleteUser = \"{0}\" \uC0AC\uC6A9\uC790 \uC0AD\uC81C?
+gb.userDeleted = ''{0}'' \uC0AC\uC6A9\uC790 \uC0AD\uC81C\uB428.
+gb.userDeleteFailed = ''{0}'' \uC0AC\uC6A9\uC790 \uC0AD\uC81C \uC2E4\uD328!
+gb.time.justNow = \uC9C0\uAE08
+gb.time.today = \uC624\uB298
+gb.time.yesterday = \uC5B4\uC81C
+gb.time.minsAgo = {0} \uBD84 \uC804
+gb.time.hoursAgo = {0} \uC2DC\uAC04 \uC804
+gb.time.daysAgo = {0} \uC77C \uC804
+gb.time.weeksAgo = {0} \uC8FC \uC804
+gb.time.monthsAgo = {0} \uB2EC \uC804
+gb.time.oneYearAgo = 1 \uB144 \uC804
+gb.time.yearsAgo = {0} \uB144 \uC804
+gb.duration.oneDay = 1 \uC77C
+gb.duration.days = {0} \uC77C
+gb.duration.oneMonth = 1 \uAC1C\uC6D4
+gb.duration.months = {0} \uAC1C\uC6D4
+gb.duration.oneYear = 1 \uB144
+gb.duration.years = {0} \uB144
+gb.authorizationControl = \uC778\uC99D \uC81C\uC5B4
+gb.allowAuthenticatedDescription = \uBAA8\uB4E0 \uC778\uC99D\uB41C \uC720\uC800\uC5D0\uAC8C \uAD8C\uD55C \uBD80\uC5EC
+gb.allowNamedDescription = \uC774\uB984\uC73C\uB85C \uC720\uC800\uB098 \uD300\uC5D0\uAC8C \uAD8C\uD55C \uBD80\uC5EC
+gb.markdownFailure = \uB9C8\uD06C\uB2E4\uC6B4 \uCEE8\uD150\uD2B8 \uD30C\uC2F1 \uC624\uB958!
+gb.clearCache = \uCE90\uC2DC \uC9C0\uC6B0\uAE30
+gb.projects = \uD504\uB85C\uC81D\uD2B8\uB4E4
+gb.project = \uD504\uB85C\uC81D\uD2B8
+gb.allProjects = \uBAA8\uB4E0 \uD504\uB85C\uC81D\uD2B8
+gb.copyToClipboard = \uD074\uB9BD\uBCF4\uB4DC\uC5D0 \uBCF5\uC0AC
+gb.fork = \uD3EC\uD06C
+gb.forks = \uD3EC\uD06C
+gb.forkRepository = fork {0}?
+gb.repositoryForked = {0} \uD3EC\uD06C\uB428
+gb.repositoryForkFailed= \uD3EC\uD06C\uC2E4\uD328
+gb.personalRepositories = \uAC1C\uC778 \uC800\uC7A5\uC18C
+gb.allowForks = \uD3EC\uD06C \uD5C8\uC6A9
+gb.allowForksDescription = \uC774 \uC800\uC7A5\uC18C\uB97C \uC778\uC99D\uB41C \uC720\uC800\uC5D0\uAC70 \uD3EC\uD06C \uD5C8\uC6A9
+gb.forkedFrom = forked from
+gb.canFork = \uD3EC\uD06C \uAC00\uB2A5
+gb.canForkDescription = \uD5C8\uC6A9\uB41C \uC800\uC7A5\uC18C\uB97C \uAC1C\uC778 \uC800\uC7A5\uC18C\uC5D0 \uD3EC\uD06C\uD560 \uC218 \uC788\uC74C
+gb.myFork = \uB0B4 \uD3EC\uD06C \uBCF4\uAE30
+gb.forksProhibited = \uD3EC\uD06C \uCC28\uB2E8\uB428
+gb.forksProhibitedWarning = \uC774 \uC800\uC7A5\uC18C\uB294 \uD3EC\uD06C \uCC28\uB2E8\uB418\uC5B4 \uC788\uC74C
+gb.noForks = {0} \uB294 \uD3EC\uD06C \uC5C6\uC74C
+gb.forkNotAuthorized = \uC8C4\uC1A1\uD569\uB2C8\uB2E4. {0} \uD3EC\uD06C\uC5D0 \uC811\uC18D \uC778\uC99D\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.
+gb.forkInProgress = \uD504\uD06C \uC9C4\uD589 \uC911
+gb.preparingFork = \uD3EC\uD06C \uC900\uBE44 \uC911...
+gb.isFork = \uD3EC\uD06C\uD55C
+gb.canCreate = \uC0DD\uC131 \uAC00\uB2A5
+gb.canCreateDescription = \uAC1C\uC778 \uC800\uC7A5\uC18C\uB97C \uB9CC\uB4E4 \uC218 \uC788\uC74C
+gb.illegalPersonalRepositoryLocation = \uAC1C\uC778 \uC800\uC7A5\uC18C\uB294 \uBC18\uB4DC\uC2DC \"{0}\" \uC5D0 \uC704\uCE58\uD574\uC57C \uD569\uB2C8\uB2E4.
+gb.verifyCommitter = \uCEE4\uBBF8\uD130 \uD655\uC778
+gb.verifyCommitterDescription = \uCEE4\uBBF8\uD130 ID \uB294 Gitblit ID \uC640 \uB9E4\uCE58\uB418\uC5B4\uC57C \uD568
+gb.verifyCommitterNote = \uBAA8\uB4E0 \uBA38\uC9C0\uB294 \uCEE4\uBBF8\uD130 ID \uB97C \uC801\uC6A9\uD558\uAE30 \uC704\uD574 "--no-ff" \uC635\uC158 \uD544\uC694
+gb.repositoryPermissions = \uC800\uC7A5\uC18C \uAD8C\uD55C
+gb.userPermissions = \uC720\uC800 \uAD8C\uD55C
+gb.teamPermissions = \uD300 \uAD8C\uD55C
+gb.add = \uCD94\uAC00
+gb.noPermission = \uC774 \uAD8C\uD55C \uC0AD\uC81C
+gb.excludePermission = {0} (\uC81C\uC678)
+gb.viewPermission = {0} (\uBCF4\uAE30)
+gb.clonePermission = {0} (\uD074\uB860)
+gb.pushPermission = {0} (\uD478\uC2DC)
+gb.createPermission = {0} (\uD478\uC2DC, ref \uC0DD\uC131)
+gb.deletePermission = {0} (\uD478\uC2DC, ref \uC0DD\uC131+\uC0AD\uC81C)
+gb.rewindPermission = {0} (\uD478\uC2DC, ref \uC0DD\uC131+\uC0AD\uC81C+\uB418\uB3CC\uB9AC\uAE30)
+gb.permission = \uAD8C\uD55C
+gb.regexPermission = \uC774 \uAD8C\uD55C\uC740 \uC815\uADDC\uC2DD \"{0}\" \uB85C\uBD80\uD130 \uC124\uC815\uB428
+gb.accessDenied = \uC811\uC18D \uAC70\uBD80
+gb.busyCollectingGarbage = \uC8C4\uC1A1\uD569\uB2C8\uB2E4. Gitblit \uC740 \uAC00\uBE44\uC9C0 \uCEEC\uB809\uC158 \uC911\uC785\uB2C8\uB2E4. {0}
+gb.gcPeriod = GC \uC8FC\uAE30
+gb.gcPeriodDescription = \uAC00\uBE44\uC9C0 \uD074\uB809\uC158\uAC04\uC758 \uC2DC\uAC04 \uAC04\uACA9
+gb.gcThreshold = GC \uAE30\uC900\uC810
+gb.gcThresholdDescription = \uC870\uAE30 \uAC00\uBE44\uC9C0 \uCEEC\uB809\uC158\uC744 \uBC1C\uC0DD\uC2DC\uD0A4\uAE30 \uC704\uD55C \uC624\uBE0C\uC81D\uD2B8\uB4E4\uC758 \uCD5C\uC18C \uC804\uCCB4 \uD06C\uAE30
+gb.ownerPermission = \uC800\uC7A5\uC18C \uC624\uB108
+gb.administrator = \uAD00\uB9AC\uC790
+gb.administratorPermission = Gitblit \uAD00\uB9AC\uC790
+gb.team = \uD300
+gb.teamPermission = \"{0}\" \uD300 \uBA64\uBC84\uC5D0 \uAD8C\uD55C \uC124\uC815\uB428
+gb.missing = \uB204\uB77D!
+gb.missingPermission = \uC774 \uAD8C\uD55C\uC744 \uC704\uD55C \uC800\uC7A5\uC18C \uB204\uB77D!
+gb.mutable = \uAC00\uBCC0
+gb.specified = \uC9C0\uC815\uB41C
+gb.effective = \uD6A8\uACFC\uC801
+gb.organizationalUnit = \uC870\uC9C1
+gb.organization = \uAE30\uAD00
+gb.locality = \uC704\uCE58
+gb.stateProvince = \uB3C4 \uB610\uB294 \uC8FC
+gb.countryCode = \uAD6D\uAC00\uCF54\uB4DC
+gb.properties = \uC18D\uC131
+gb.issued = \uBC1C\uAE09\uB428
+gb.expires = \uB9CC\uB8CC
+gb.expired = \uB9CC\uB8CC\uB428
+gb.expiring = \uB9CC\uB8CC\uC911
+gb.revoked = \uD3D0\uAE30\uB428
+gb.serialNumber = \uC77C\uB828\uBC88\uD638
+gb.certificates = \uC778\uC99D\uC11C
+gb.newCertificate = \uC0C8 \uC778\uC99D\uC11C
+gb.revokeCertificate = \uC778\uC99D\uC11C \uD3D0\uAE30
+gb.sendEmail = \uBA54\uC77C \uBCF4\uB0B4\uAE30
+gb.passwordHint = \uD328\uC2A4\uC6CC\uB4DC \uD78C\uD2B8
+gb.ok = ok
+gb.invalidExpirationDate = \uB9D0\uB8CC\uC77C\uC790 \uC624\uB958!
+gb.passwordHintRequired = \uD328\uC2A4\uC6CC\uB4DC \uD78C\uD2B8 \uD544\uC218!
+gb.viewCertificate = \uC778\uC99D\uC11C \uBCF4\uAE30
+gb.subject = \uC774\uB984
+gb.issuer = \uBC1C\uAE09\uC790
+gb.validFrom = \uC720\uD6A8\uAE30\uAC04 (\uC2DC\uC791)
+gb.validUntil = \uC720\uD6A8\uAE30\uAC04 (\uB05D)
+gb.publicKey = \uACF5\uAC1C\uD0A4
+gb.signatureAlgorithm = \uC11C\uBA85 \uC54C\uACE0\uB9AC\uC998
+gb.sha1FingerPrint = SHA-1 \uC9C0\uBB38 \uC54C\uACE0\uB9AC\uC998
+gb.md5FingerPrint = MD5 \uC9C0\uBB38 \uC54C\uACE0\uB9AC\uC998
+gb.reason = \uC774\uC720
+gb.revokeCertificateReason = \uC778\uC99D\uC11C \uD574\uC9C0\uC774\uC720\uB97C \uC120\uD0DD\uD558\uC138\uC694
+gb.unspecified = \uD45C\uC2DC\uD558\uC9C0 \uC54A\uC74C
+gb.keyCompromise = \uD0A4 \uC190\uC0C1
+gb.caCompromise = CA \uC190\uC0C1
+gb.affiliationChanged = \uAD00\uACC4 \uBCC0\uACBD\uB428
+gb.superseded = \uB300\uCCB4\uB428
+gb.cessationOfOperation = \uC6B4\uC601 \uC911\uC9C0
+gb.privilegeWithdrawn = \uAD8C\uD55C \uCCA0\uD68C\uB428
+gb.time.inMinutes = {0} \uBD84
+gb.time.inHours = {0} \uC2DC\uAC04
+gb.time.inDays = {0} \uC77C
+gb.hostname = \uD638\uC2A4\uD2B8\uBA85
+gb.hostnameRequired = \uD638\uC2A4\uD2B8\uBA85\uC744 \uC785\uB825\uD558\uC138\uC694
+gb.newSSLCertificate = \uC0C8 \uC11C\uBC84 SSL \uC778\uC99D\uC11C
+gb.newCertificateDefaults = \uC0C8 \uC778\uC99D\uC11C \uAE30\uBCF8
+gb.duration = \uAE30\uAC04
+gb.certificateRevoked = \uC778\uC99D\uC11C {0,number,0} \uB294 \uD3D0\uAE30\uB418\uC5C8\uC2B5\uB2C8\uB2E4
+gb.clientCertificateGenerated = {0} \uC744(\uB97C) \uC704\uD55C \uC0C8\uB85C\uC6B4 \uD074\uB77C\uC774\uC5B8\uD2B8 \uC778\uC99D\uC11C \uC0DD\uC131 \uC131\uACF5
+gb.sslCertificateGenerated = {0} \uC744(\uB97C) \uC704\uD55C \uC0C8\uB85C\uC6B4 \uC11C\uBC84 SSL \uC778\uC99D\uC11C \uC0DD\uC131 \uC131\uACF5
+gb.newClientCertificateMessage = \uB178\uD2B8:\n'\uD328\uC2A4\uC6CC\uB4DC' \uB294 \uC720\uC800\uC758 \uD328\uC2A4\uC6CC\uB4DC\uAC00 \uC544\uB2C8\uB77C \uC720\uC800\uC758 \uD0A4\uC2A4\uD1A0\uC5B4 \uB97C \uBCF4\uD638\uD558\uAE30 \uC704\uD55C \uAC83\uC785\uB2C8\uB2E4. \uC774 \uD328\uC2A4\uC6CC\uB4DC\uB294 \uC800\uC7A5\uB418\uC9C0 \uC54A\uC73C\uBBC0\uB85C \uC0AC\uC6A9\uC790 README \uC9C0\uCE68\uC5D0 \uD3EC\uD568\uB420 '\uD78C\uD2B8' \uB97C \uBC18\uB4DC\uC2DC \uC785\uB825\uD574\uC57C \uD569\uB2C8\uB2E4.
+gb.certificate = \uC778\uC99D\uC11C
+gb.emailCertificateBundle = \uC774\uBA54\uC77C \uD074\uB77C\uC774\uC5B8\uD2B8 \uC778\uC99D\uC11C \uBC88\uB4E4
+gb.pleaseGenerateClientCertificate = {0} \uC744(\uB97C) \uC704\uD55C \uD074\uB77C\uC774\uC5B8\uD2B8 \uC778\uC99D\uC11C\uB97C \uC0DD\uC131\uD558\uC138\uC694
+gb.clientCertificateBundleSent = {0} \uC744(\uB97C) \uC704\uD55C \uD074\uB77C\uC774\uC5B8\uD2B8 \uC778\uC99D\uC11C \uBC88\uB4E4 \uBC1C\uC1A1\uB428
+gb.enterKeystorePassword = Gitblit \uD0A4\uC2A4\uD1A0\uC5B4 \uD328\uC2A4\uC6CC\uB4DC\uB97C \uC785\uB825\uD558\uC138\uC694
+gb.warning = \uACBD\uACE0
+gb.jceWarning = \uC790\uBC14 \uC2E4\uD589\uD658\uACBD\uC5D0 \"JCE Unlimited Strength Jurisdiction Policy\" \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.\n\uC774\uAC83\uC740 \uD0A4\uC800\uC7A5\uC18C \uC554\uD638\uD654\uC5D0 \uC0AC\uC6A9\uB418\uB294 \uD328\uC2A4\uC6CC\uB4DC\uC758 \uAE38\uC774\uB294 7\uC790\uB85C \uC81C\uD55C\uD569\uB2C8\uB2E4.\n\uC774 \uC815\uCC45 \uD30C\uC77C\uC740 Oracle \uC5D0\uC11C \uC120\uD0DD\uC801\uC73C\uB85C \uB2E4\uC6B4\uB85C\uB4DC\uD574\uC57C \uD569\uB2C8\uB2E4.\n\n\uBB34\uC2DC\uD558\uACE0 \uC778\uC99D\uC11C \uC778\uD504\uB77C\uB97C \uC0DD\uC131\uD558\uC2DC\uACA0\uC2B5\uB2C8\uAE4C?\n\n\uC544\uB2C8\uC624(No) \uB77C\uACE0 \uB2F5\uD558\uBA74 \uC815\uCC45\uD30C\uC77C\uC744 \uB2E4\uC6B4\uBC1B\uC744 \uC218 \uC788\uB294 Oracle \uB2E4\uC6B4\uB85C\uB4DC \uD398\uC774\uC9C0\uB97C \uBE0C\uB77C\uC6B0\uC800\uB85C \uC548\uB0B4\uD560 \uAC83\uC785\uB2C8\uB2E4.
+gb.maxActivityCommits = \uCD5C\uB300 \uC561\uD2F0\uBE44\uD2F0 \uCEE4\uBC0B
+gb.maxActivityCommitsDescription = \uC561\uD2F0\uBE44\uD2F0 \uD398\uC774\uC9C0\uC5D0 \uD45C\uC2DC\uD560 \uCD5C\uB300 \uCEE4\uBC0B \uC218
+gb.noMaximum = \uBB34\uC81C\uD55C
+gb.attributes = \uC18D\uC131
+gb.serveCertificate = \uC774 \uC778\uC99D\uC11C\uB85C https \uC81C\uACF5
+gb.sslCertificateGeneratedRestart = {0} \uC744(\uB97C) \uC704\uD55C \uC0C8 \uC11C\uBC84 SSL \uC778\uC99D\uC11C\uB97C \uC131\uACF5\uC801\uC73C\uB85C \uC0DD\uC131\uD558\uC600\uC2B5\uB2C8\uB2E4. \n\uC0C8 \uC778\uC99D\uC11C\uB97C \uC0AC\uC6A9\uD558\uB824\uBA74 Gitblit \uC744 \uC7AC\uC2DC\uC791 \uD574\uC57C \uD569\uB2C8\uB2E4.\n\n'--alias' \uD30C\uB77C\uBBF8\uD130\uB85C \uC2E4\uD589\uD55C\uB2E4\uBA74 ''--alias {0}'' \uB85C \uC124\uC815\uD574\uC57C \uD569\uB2C8\uB2E4.
+gb.validity = \uC720\uD6A8\uC131
+gb.siteName = \uC0AC\uC774\uD2B8 \uC774\uB984
+gb.siteNameDescription = \uC11C\uBC84\uC758 \uC9E6\uC740 \uC124\uBA85\uC774 \uD3EC\uD568\uB41C \uC774\uB984
+gb.excludeFromActivity = \uC561\uD2F0\uBE44\uD2F0 \uD398\uC774\uC9C0\uC5D0\uC11C \uC81C\uC678
+gb.sessionEnded = Session has been closed
+gb.closeBrowser = Please close the browser to properly end the session.
diff --git a/src/main/java/com/gitblit/wicket/GitBlitWebApp_nl.properties b/src/main/java/com/gitblit/wicket/GitBlitWebApp_nl.properties
new file mode 100644
index 00000000..f1281e14
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/GitBlitWebApp_nl.properties
@@ -0,0 +1,445 @@
+gb.repository = repositorie
+gb.owner = eigenaar
+gb.description = omschrijving
+gb.lastChange = laatste wijziging
+gb.refs = refs
+gb.tag = tag
+gb.tags = tags
+gb.author = auteur
+gb.committer = committer
+gb.commit = commit
+gb.tree = tree
+gb.parent = parent
+gb.url = URL
+gb.history = historie
+gb.raw = raw
+gb.object = object
+gb.ticketId = ticket id
+gb.ticketAssigned = toegewezen
+gb.ticketOpenDate = open datum
+gb.ticketState = status
+gb.ticketComments = commentaar
+gb.view = view
+gb.local = local
+gb.remote = remote
+gb.branches = branches
+gb.patch = patch
+gb.diff = diff
+gb.log = log
+gb.moreLogs = meer commits...
+gb.allTags = alle tags...
+gb.allBranches = alle branches...
+gb.summary = samenvatting
+gb.ticket = ticket
+gb.newRepository = nieuwe repositorie
+gb.newUser = nieuwe gebruiker
+gb.commitdiff = commitdiff
+gb.tickets = tickets
+gb.pageFirst = eerste
+gb.pagePrevious = vorige
+gb.pageNext = volgende
+gb.head = HEAD
+gb.blame = blame
+gb.login = aanmelden
+gb.logout = afmelden
+gb.username = gebruikersnaam
+gb.password = wachtwoord
+gb.tagger = tagger
+gb.moreHistory = meer historie...
+gb.difftocurrent = diff naar current
+gb.search = zoeken
+gb.searchForAuthor = Zoeken naar commits authored door
+gb.searchForCommitter = Zoeken naar commits committed door
+gb.addition = additie
+gb.modification = wijziging
+gb.deletion = verwijdering
+gb.rename = hernoem
+gb.metrics = metrieken
+gb.stats = stats
+gb.markdown = markdown
+gb.changedFiles = gewijzigde bestanden
+gb.filesAdded = {0} bestanden toegevoegd
+gb.filesModified = {0} bestanden gewijzigd
+gb.filesDeleted = {0} bestanden verwijderd
+gb.filesCopied = {0} bestanden gekopieerd
+gb.filesRenamed = {0} bestanden hernoemd
+gb.missingUsername = Ontbrekende Gebruikersnaam
+gb.edit = edit
+gb.searchTypeTooltip = Selecteer Zoek Type
+gb.searchTooltip = Zoek {0}
+gb.delete = verwijder
+gb.docs = docs
+gb.accessRestriction = toegangsbeperking
+gb.name = naam
+gb.enableTickets = enable tickets
+gb.enableDocs = enable docs
+gb.save = opslaan
+gb.showRemoteBranches = toon remote branches
+gb.editUsers = wijzig gebruikers
+gb.confirmPassword = bevestig wachtwoord
+gb.restrictedRepositories = restricted repositories
+gb.canAdmin = kan beheren
+gb.notRestricted = anoniem view, clone, & push
+gb.pushRestricted = geauthenticeerde push
+gb.cloneRestricted = geauthenticeerde clone & push
+gb.viewRestricted = geauthenticeerde view, clone, & push
+gb.useTicketsDescription = readonly, gedistribueerde Ticgit issues
+gb.useDocsDescription = enumereer Markdown documentatie in repositorie
+gb.showRemoteBranchesDescription = toon remote branches
+gb.canAdminDescription = kan Gitblit server beheren
+gb.permittedUsers = toegestane gebruikers
+gb.isFrozen = is bevroren
+gb.isFrozenDescription = weiger push operaties
+gb.zip = zip
+gb.showReadme = toon readme
+gb.showReadmeDescription = toon een \"readme\" Markdown bestand in de samenvattingspagina
+gb.nameDescription = gebruik '/' voor het groeperen van repositories. bijv. libraries/mycoollib.git
+gb.ownerDescription = de eigenaar mag repository instellingen wijzigen
+gb.blob = blob
+gb.commitActivityTrend = commit activiteit trend
+gb.commitActivityDOW = commit activiteit per dag van de week
+gb.commitActivityAuthors = primaire auteurs op basis van commit activiteit
+gb.feed = feed
+gb.cancel = afbreken
+gb.changePassword = wijzig wachtwoord
+gb.isFederated = is gefedereerd
+gb.federateThis = federeer deze repositorie
+gb.federateOrigin = federeer deze origin
+gb.excludeFromFederation = uitsluiten van federatie
+gb.excludeFromFederationDescription = sluit gefedereerde Gitblit instances uit van het pullen van dit account
+gb.tokens = federatie tokens
+gb.tokenAllDescription = alle repositories, gebruikers, & instellingen
+gb.tokenUnrDescription = alle repositories & gebruikers
+gb.tokenJurDescription = alle repositories
+gb.federatedRepositoryDefinitions = repositorie definities
+gb.federatedUserDefinitions = gebruikersdefinities
+gb.federatedSettingDefinitions = instellingendefinities
+gb.proposals = federatie voorstellen
+gb.received = ontvangen
+gb.type = type
+gb.token = token
+gb.repositories = repositories
+gb.proposal = voorstel
+gb.frequency = frequentie
+gb.folder = map
+gb.lastPull = laatste pull
+gb.nextPull = volgende pull
+gb.inclusions = inclusies
+gb.exclusions = exclusies
+gb.registration = registratie
+gb.registrations = federatie registraties
+gb.sendProposal = voorstel
+gb.status = status
+gb.origin = origin
+gb.headRef = default branch (HEAD)
+gb.headRefDescription = wijzig de ref waar HEAD naar linkt naar bijv. refs/heads/master
+gb.federationStrategy = federatie strategie
+gb.federationRegistration = federatie registratie
+gb.federationResults = federatie pull resultaten
+gb.federationSets = federatie sets
+gb.message = melding
+gb.myUrlDescription = de publiek toegankelijke url voor uw Gitblit instantie
+gb.destinationUrl = zend naar
+gb.destinationUrlDescription = de url van de Gitblit instantie voor het verzenden van uw voorstel
+gb.users = gebruikers
+gb.federation = federatie
+gb.error = fout
+gb.refresh = ververs
+gb.browse = blader
+gb.clone = clone
+gb.filter = filter
+gb.create = maak
+gb.servers = servers
+gb.recent = recent
+gb.available = beschikbaar
+gb.selected = geselecteerd
+gb.size = grootte
+gb.downloading = downloading
+gb.loading = laden
+gb.starting = starten
+gb.general = algemeen
+gb.settings = instellingen
+gb.manage = beheer
+gb.lastLogin = laatste login
+gb.skipSizeCalculation = geen berekening van de omvang
+gb.skipSizeCalculationDescription = geen berekening van de repositoriegrootte (beperkt laadtijd pagina)
+gb.skipSummaryMetrics = geen metrieken samenvatting
+gb.skipSummaryMetricsDescription = geen berekening van metrieken op de samenvattingspagina (beperkt laadtijd pagina)
+gb.accessLevel = toegangsniveau
+gb.default = standaard
+gb.setDefault = instellen als standaard
+gb.since = sinds
+gb.status = status
+gb.bootDate = boot datum
+gb.servletContainer = servlet container
+gb.heapMaximum = maximum heap
+gb.heapAllocated = toegewezen heap
+gb.heapUsed = gebruikte heap
+gb.free = beschikbaar
+gb.version = versie
+gb.releaseDate = release datum
+gb.date = datum
+gb.activity = activiteit
+gb.subscribe = aboneer
+gb.branch = branch
+gb.maxHits = max hits
+gb.recentActivity = recente activiteit
+gb.recentActivityStats = laatste {0} dagen / {1} commits door {2} auteurs
+gb.recentActivityNone = laatste {0} dagen / geen
+gb.dailyActivity = dagelijkse activiteit
+gb.activeRepositories = actieve repositories
+gb.activeAuthors = actieve auteurs
+gb.commits = commits
+gb.teams = teams
+gb.teamName = teamnaam
+gb.teamMembers = teamleden
+gb.teamMemberships = teamlidmaatschappen
+gb.newTeam = nieuw team
+gb.permittedTeams = toegestane teams
+gb.emptyRepository = lege repositorie
+gb.repositoryUrl = repositorie url
+gb.mailingLists = mailing lijsten
+gb.preReceiveScripts = pre-receive scripts
+gb.postReceiveScripts = post-receive scripts
+gb.hookScripts = hook scripts
+gb.customFields = custom velden
+gb.customFieldsDescription = custom velden beschikbaar voor Groovy hooks
+gb.accessPermissions = toegangsrechten
+gb.filters = filters
+gb.generalDescription = algemene instellingen
+gb.accessPermissionsDescription = beperk toegang voor gebruikers en teams
+gb.accessPermissionsForUserDescription = stel teamlidmaatschappen in of geef toegang tot specifieke besloten repositories
+gb.accessPermissionsForTeamDescription = stel teamlidmaatschappen in en geef toegang tot specifieke besloten repositories
+gb.federationRepositoryDescription = deel deze repositorie met andere Gitblit servers
+gb.hookScriptsDescription = run Groovy scripts bij pushes naar deze Gitblit server
+gb.reset = reset
+gb.pages = paginas
+gb.workingCopy = werkkopie
+gb.workingCopyWarning = deze repositorie heeft een werkkopie en kan geen pushes ontvangen
+gb.query = query
+gb.queryHelp = Standaard query syntax wordt ondersteund.<p/><p/>Zie aub <a target="_new" href="http://lucene.apache.org/core/old_versioned_docs/versions/3_5_0/queryparsersyntax.html">Lucene Query Parser Syntax</a> voor informatie.
+gb.queryResults = resultaten {0} - {1} ({2} hits)
+gb.noHits = geen hits
+gb.authored = authored
+gb.committed = committed
+gb.indexedBranches = geïndexeerde branches
+gb.indexedBranchesDescription = kies de branches voor opname in uw Lucene index
+gb.noIndexedRepositoriesWarning = geen van uw repositories is geconfigureerd voor Lucene indexering
+gb.undefinedQueryWarning = query is niet gedefinieerd!
+gb.noSelectedRepositoriesWarning = kies aub één of meerdere repositories!
+gb.luceneDisabled = Lucene indexering staat uit
+gb.failedtoRead = Lezen is mislukt
+gb.isNotValidFile = is geen valide bestand
+gb.failedToReadMessage = Het lezen van de standaard boodschap van {0} is mislukt!
+gb.passwordsDoNotMatch = Wachtwoorden komen niet overeen!
+gb.passwordTooShort = Wachtwoord is te kort. Minimum lengte is {0} karakters.
+gb.passwordChanged = Wachtwoord succesvol gewijzigd.
+gb.passwordChangeAborted = Wijziging wachtwoord afgebroken.
+gb.pleaseSetRepositoryName = Vul aub een repositorie naam in!
+gb.illegalLeadingSlash = Leidende root folder referenties (/) zijn niet toegestaan.
+gb.illegalRelativeSlash = Relatieve folder referenties (../) zijn niet toegestaan.
+gb.illegalCharacterRepositoryName = Illegaal karakter ''{0}'' in repositorie naam!
+gb.selectAccessRestriction = Stel aub een toegangsbeperking in!
+gb.selectFederationStrategy = Selecteer aub een federatie strategie!
+gb.pleaseSetTeamName = Vul aub een teamnaam in!
+gb.teamNameUnavailable = Teamnaam ''{0}'' is niet beschikbaar.
+gb.teamMustSpecifyRepository = Een team moet minimaal één repositorie specificeren.
+gb.teamCreated = Nieuw team ''{0}'' successvol aangemaakt.
+gb.pleaseSetUsername = Vul aub een gebruikersnaam in!
+gb.usernameUnavailable = Gebruikersnaam ''{0}'' is niet beschikbaar.
+gb.combinedMd5Rename = Gitblit is geconfigureerd voor combined-md5 wachtwoord hashing. U moet een nieuw wachtwoord opgeven bij het hernoemen van een account.
+gb.userCreated = Nieuwe gebruiker ''{0}'' succesvol aangemaakt.
+gb.couldNotFindFederationRegistration = Kon de federatie registratie niet vinden!
+gb.failedToFindGravatarProfile = Kon het Gravatar profiel voor {0} niet vinden
+gb.branchStats = {0} commits en {1} tags in {2}
+gb.repositoryNotSpecified = Repositorie niet gespecificeerd!
+gb.repositoryNotSpecifiedFor = Repositorie niet gespecificeerd voor {0}!
+gb.canNotLoadRepository = Kan repositorie niet laden
+gb.commitIsNull = Commit is null
+gb.unauthorizedAccessForRepository = Niet toegestane toegang tot repositorie
+gb.failedToFindCommit = Het vinden van commit \"{0}\" in {1} voor {2} pagina is mislukt!
+gb.couldNotFindFederationProposal = Kon federatievoorstel niet vinden!
+gb.invalidUsernameOrPassword = Onjuiste gebruikersnaam of wachtwoord!
+gb.OneProposalToReview = Er is 1 federatie voorstel dat wacht op review.
+gb.nFederationProposalsToReview = Er zijn {0} federatie verzoeken die wachten op review.
+gb.couldNotFindTag = Kon tag {0} niet vinden
+gb.couldNotCreateFederationProposal = Kon geen federatie voorstel maken!
+gb.pleaseSetGitblitUrl = Vul aub uw Gitblit url in!
+gb.pleaseSetDestinationUrl = Vul aub een bestemmings-url in voor uw voorstel!
+gb.proposalReceived = Voorstel correct ontvangen door {0}.
+gb.noGitblitFound = Sorry, {0} kon geen Gitblit instance vinden op {1}.
+gb.noProposals = Sorry, {0} accepteert geen voorstellen op dit moment.
+gb.noFederation = Sorry, {0} is niet geconfigureerd voor het federeren met een Gitblit instance.
+gb.proposalFailed = Sorry, {0} ontving geen voorstelgegevens!
+gb.proposalError = Sorry, {0} rapporteert dat een onverwachte fout is opgetreden!
+gb.failedToSendProposal = Voorstel verzenden is niet gelukt!
+gb.userServiceDoesNotPermitAddUser = {0} staat het toevoegen van een gebruikersaccount niet toe!
+gb.userServiceDoesNotPermitPasswordChanges = {0} staat wachtwoord wijzigingen niet toe!
+gb.displayName = display naam
+gb.emailAddress = emailadres
+gb.errorAdminLoginRequired = Aanmelden vereist voor beheerwerk
+gb.errorOnlyAdminMayCreateRepository = Alleen een beheerder kan een repositorie maken
+gb.errorOnlyAdminOrOwnerMayEditRepository = Alleen een beheerder of de eigenaar kan een repositorie wijzigen
+gb.errorAdministrationDisabled = Beheer is uitgeschakeld
+gb.lastNDays = laatste {0} dagen
+gb.completeGravatarProfile = Completeer profiel op Gravatar.com
+gb.none = geen
+gb.line = regel
+gb.content = inhoud
+gb.empty = leeg
+gb.inherited = geërfd
+gb.deleteRepository = Verwijder repositorie \"{0}\"?
+gb.repositoryDeleted = Repositorie ''{0}'' verwijderd.
+gb.repositoryDeleteFailed = Verwijdering van repositorie ''{0}'' mislukt!
+gb.deleteUser = Verwijder gebruiker \"{0}\"?
+gb.userDeleted = Gebruiker ''{0}'' verwijderd.
+gb.userDeleteFailed = Verwijdering van gebruiker ''{0}'' mislukt!
+gb.time.justNow = net
+gb.time.today = vandaag
+gb.time.yesterday = gisteren
+gb.time.minsAgo = {0} minuten geleden
+gb.time.hoursAgo = {0} uren geleden
+gb.time.daysAgo = {0} dagen geleden
+gb.time.weeksAgo = {0} weken geleden
+gb.time.monthsAgo = {0} maanden geleden
+gb.time.oneYearAgo = 1 jaar geleden
+gb.time.yearsAgo = {0} jaren geleden
+gb.duration.oneDay = 1 dag
+gb.duration.days = {0} dagen
+gb.duration.oneMonth = 1 maand
+gb.duration.months = {0} maanden
+gb.duration.oneYear = 1 jaar
+gb.duration.years = {0} jaren
+gb.authorizationControl = authorisatiebeheer
+gb.allowAuthenticatedDescription = ken RW+ rechten toe aan alle geautoriseerde gebruikers
+gb.allowNamedDescription = ken verfijnde rechten toe aan genoemde gebruikers of teams
+gb.markdownFailure = Het parsen van Markdown content is mislukt!
+gb.clearCache = maak cache leeg
+gb.projects = projecten
+gb.project = project
+gb.allProjects = alle projecten
+gb.copyToClipboard = kopieer naar clipboard
+gb.fork = fork
+gb.forks = forks
+gb.forkRepository = fork {0}?
+gb.repositoryForked = {0} is geforked
+gb.repositoryForkFailed= fork is mislukt
+gb.personalRepositories = personlijke repositories
+gb.allowForks = sta forks toe
+gb.allowForksDescription = sta geauthoriseerde gebruikers toe om deze repositorie te forken
+gb.forkedFrom = geforked vanaf
+gb.canFork = kan geforked worden
+gb.canForkDescription = kan geauthoriseerde repositories forken naar persoonlijke repositories
+gb.myFork = toon mijn fork
+gb.forksProhibited = forks niet toegestaan
+gb.forksProhibitedWarning = deze repositorie staat forken niet toe
+gb.noForks = {0} heeft geen forks
+gb.forkNotAuthorized = sorry, u bent niet geautoriseerd voor het forken van {0}
+gb.forkInProgress = bezig met forken
+gb.preparingFork = bezig met het maken van uw fork...
+gb.isFork = is een fork
+gb.canCreate = mag maken
+gb.canCreateDescription = mag persoonlijke repositories maken
+gb.illegalPersonalRepositoryLocation = uw persoonlijke repositorie moet te vinden zijn op \"{0}\"
+gb.verifyCommitter = controleer committer
+gb.verifyCommitterDescription = vereis dat committer identiteit overeen komt met pushing Gitblt gebruikersaccount
+gb.verifyCommitterNote = alle merges vereisen "--no-ff" om committer identiteit af te dwingen
+gb.repositoryPermissions = repository rechten
+gb.userPermissions = gebruikersrechten
+gb.teamPermissions = teamrechten
+gb.add = toevoegen
+gb.noPermission = VERWIJDER DIT RECHT
+gb.excludePermission = {0} (exclude)
+gb.viewPermission = {0} (view)
+gb.clonePermission = {0} (clone)
+gb.pushPermission = {0} (push)
+gb.createPermission = {0} (push, ref creëer)
+gb.deletePermission = {0} (push, ref creëer+verwijdering)
+gb.rewindPermission = {0} (push, ref creëer+verwijdering+rewind)
+gb.permission = recht
+gb.regexPermission = dit recht is gezet vanaf de reguliere expressie \"{0}\"
+gb.accessDenied = toegang geweigerd
+gb.busyCollectingGarbage = sorry, Gitblit is bezig met opruimen in {0}
+gb.gcPeriod = opruim periode
+gb.gcPeriodDescription = tijdsduur tussen opruimacties
+gb.gcThreshold = opruim drempel
+gb.gcThresholdDescription = minimum totaalomvang van losse objecten voor het starten van opruimactie
+gb.ownerPermission = repositorie eigenaar
+gb.administrator = beheer
+gb.administratorPermission = Gitblit beheerder
+gb.team = team
+gb.teamPermission = permissie ingesteld via \"{0}\" teamlidmaatschap
+gb.missing = ontbrekend!
+gb.missingPermission = de repositorie voor deze permissie ontbreekt!
+gb.mutable = te wijzigen
+gb.specified = gespecificeerd
+gb.effective = geldig
+gb.organizationalUnit = organisatie eenheid
+gb.organization = organisatie
+gb.locality = localiteit
+gb.stateProvince = staat of provincie
+gb.countryCode = landcode
+gb.properties = eigenschappen
+gb.issued = uitgegeven
+gb.expires = verloopt op
+gb.expired = verlopen
+gb.expiring = verloopt
+gb.revoked = ingetrokken
+gb.serialNumber = serie nummer
+gb.certificates = certificaten
+gb.newCertificate = nieuwe certificaten
+gb.revokeCertificate = trek certificaat in
+gb.sendEmail = zend email
+gb.passwordHint = wachtwoord hint
+gb.ok = ok
+gb.invalidExpirationDate = ongeldige verloopdatum!
+gb.passwordHintRequired = wachtwoord hint vereist!
+gb.viewCertificate = toon certificaat
+gb.subject = onderwerp
+gb.issuer = issuer
+gb.validFrom = geldig vanaf
+gb.validUntil = geldig tot
+gb.publicKey = publieke sleutel
+gb.signatureAlgorithm = signature algoritme
+gb.sha1FingerPrint = SHA-1 Fingerprint
+gb.md5FingerPrint = MD5 Fingerprint
+gb.reason = reden
+gb.revokeCertificateReason = Kies aub een reden voor het intrekken van het certificaat
+gb.unspecified = niet gespecificeerd
+gb.keyCompromise = sleutel gecompromitteerd
+gb.caCompromise = CA gecompromitteerd
+gb.affiliationChanged = affiliatie gewijzigd
+gb.superseded = opgevolgd
+gb.cessationOfOperation = gestaakt
+gb.privilegeWithdrawn = privilege ingetrokken
+gb.time.inMinutes = in {0} minuten
+gb.time.inHours = in {0} uren
+gb.time.inDays = in {0} dagen
+gb.hostname = hostnaam
+gb.hostnameRequired = Vul aub een hostnaam in
+gb.newSSLCertificate = nieuw server SSL certificaat
+gb.newCertificateDefaults = nieuw certificaat defaults
+gb.duration = duur
+gb.certificateRevoked = Certificaat {0,number,0} is ingetrokken
+gb.clientCertificateGenerated = Nieuw client certificaat voor {0} succesvol gegenereerd
+gb.sslCertificateGenerated = Nieuw server SSL certificaat voor {0} succesvol gegenereerd
+gb.newClientCertificateMessage = MERK OP:\nHet 'wachtwoord' is niet het wachtwoord van de gebruiker. Het is het wachtwoord voor het afschermen van de sleutelring van de gebruiker. Dit wachtwoord wordt niet opgeslagen dus moet u ook een 'hint' invullen die zal worden opgenomen in de README instructies van de gebruiker.
+gb.certificate = certificaat
+gb.emailCertificateBundle = email client certificaat bundel
+gb.pleaseGenerateClientCertificate = Genereer aub een client certificaat voor {0}
+gb.clientCertificateBundleSent = Client certificaat bundel voor {0} verzonden
+gb.enterKeystorePassword = Vul aub het Gitblit keystore wachtwoord in
+gb.warning = waarschuwing
+gb.jceWarning = Uw Java Runtime Environment heeft geen \"JCE Unlimited Strength Jurisdiction Policy\" bestanden.\nDit zal de lengte van wachtwoorden voor het eventueel versleutelen van uw keystores beperken tot 7 karakters.\nDeze policy bestanden zijn een optionele download van Oracle.\n\nWilt u toch doorgaan en de certificaat infrastructuur genereren?\n\nNee antwoorden zal uw browser doorsturen naar de downloadpagina van Oracle zodat u de policybestanden kunt downloaden.
+gb.maxActivityCommits = maximum activiteit commits
+gb.maxActivityCommitsDescription = maximum aantal commits om bij te dragen aan de Activiteitspagina
+gb.noMaximum = geen maximum
+gb.attributes = attributen
+gb.serveCertificate = gebruik deze certificaten voor https
+gb.sslCertificateGeneratedRestart = Nieuwe SSL certificaten voor {0} succesvol gegenereerd.\nU dient Gitblit te herstarten om de nieuwe certificaten te gebruiken.\n\nAls u opstart met de '--alias' parameter moet u die wijzigen naar ''--alias {0}''.
+gb.validity = geldigheid
+gb.siteName = site naam
+gb.siteNameDescription = korte, verduidelijkende naam van deze server
+gb.excludeFromActivity = sluit uit van activiteitspagina
+gb.sessionEnded = Sessie is afgesloten
+gb.closeBrowser = Sluit de browser af om de sessie helemaal te beeindigen. \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/GitBlitWebApp_pl.properties b/src/main/java/com/gitblit/wicket/GitBlitWebApp_pl.properties
new file mode 100644
index 00000000..b75e8f81
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/GitBlitWebApp_pl.properties
@@ -0,0 +1,323 @@
+gb.repository = Repozytorium
+gb.owner = W\u0142a\u015Bciciel
+gb.description = Opis
+gb.lastChange = Ostatnia zmiana
+gb.refs = Refs
+gb.tag = Tag
+gb.tags = Tagi
+gb.author = Autor
+gb.committer = Wgrywaj\u0105cy
+gb.commit = Commit
+gb.tree = Drzewo
+gb.parent = Rodzic
+gb.url = URL
+gb.history = Historia
+gb.raw = Raw
+gb.object = Obiekt
+gb.ticketId = Id ticketu
+gb.ticketAssigned = Przydzielony
+gb.ticketOpenDate = Data otwarcia
+gb.ticketState = Status
+gb.ticketComments = Komentarze
+gb.view = Widok
+gb.local = Lokalne
+gb.remote = Zdalne
+gb.branches = Rozga\u0142\u0119zienia
+gb.patch = \u0141atki
+gb.diff = R\u00F3\u017Cnice
+gb.log = Log
+gb.moreLogs = Wi\u0119cej log\u00F3w...
+gb.allTags = Wszystkie tagi...
+gb.allBranches = Wszystkie rozga\u0142\u0119zienia...
+gb.summary = Podsumowanie
+gb.ticket = Ticket
+gb.newRepository = Nowe repozytorium
+gb.newUser = Nowy u\u017Cytkownik
+gb.commitdiff = Diff zmiany
+gb.tickets = Tickets
+gb.pageFirst = Pierwsza strona
+gb.pagePrevious = Poprzednia strona
+gb.pageNext = Nast\u0119pna strona
+gb.head = HEAD
+gb.blame = Blame
+gb.login = Zaloguj
+gb.logout = Wyloguj
+gb.username = U\u017Cytkownik
+gb.password = Has\u0142o
+gb.tagger = Taguj\u0105cy
+gb.moreHistory = Wi\u0119cej historii...
+gb.difftocurrent = Por\u00F3wnaj z obecnym
+gb.search = Szukaj
+gb.searchForAuthor = Szukaj po autorze
+gb.searchForCommitter = Szukaj po wgrywaj\u0105cym
+gb.addition = Dodane
+gb.modification = Zmodyfikowane
+gb.deletion = Usuni\u0119te
+gb.rename = Zmiana nazwy
+gb.metrics = Metryki
+gb.stats = Statystyki
+gb.markdown = Markdown
+gb.changedFiles = Zmienione pliki
+gb.filesAdded = {0} plik\u00F3w dodano
+gb.filesModified = {0} plik\u00F3w zmieniono
+gb.filesDeleted = {0} plik\u00F3w usuni\u0119to
+gb.filesCopied = {0} plik\u00F3w skopiowano
+gb.filesRenamed = {0} plik\u00F3w przemianowano
+gb.missingUsername = Brak nazwy u\u017Cytkownika
+gb.edit = Edytuj
+gb.searchTypeTooltip = Szukaj typu
+gb.searchTooltip = Szukaj {0}
+gb.delete = Usu\u0144
+gb.docs = Dokumentacja
+gb.accessRestriction = Uprawnienia dost\u0119pu
+gb.name = Nazwa
+gb.enableTickets = Uaktywnij tickety
+gb.enableDocs = Uaktywnij dokumentacj\u0119
+gb.save = Zapisz
+gb.showRemoteBranches = Poka\u017C zdalne rozga\u0142\u0119zienia
+gb.editUsers = Edytuj u\u017Cytkownika
+gb.confirmPassword = Potwierd\u017A has\u0142o
+gb.restrictedRepositories = Chronione repozytoria
+gb.canAdmin = Mo\u017Ce administrowa\u0107
+gb.notRestricted = Anonimowy podgl\u0105d, klonowanie i zapis
+gb.pushRestricted = Uwierzytelniony zapis
+gb.cloneRestricted = Uwierzytelnione klonowanie i zapis
+gb.viewRestricted = Uwierzytelniony podgl\u0105d, klonowanie i zapis
+gb.useTicketsDescription = Rozproszone zg\u0142oszenia Ticgit
+gb.useDocsDescription = Parsuj znaczniki Markdown w repozytorium
+gb.showRemoteBranchesDescription = Poka\u017C zdalne rozga\u0142\u0119zienia
+gb.canAdminDescription = Mo\u017Ce administrowa\u0107 serwerem Gitblit
+gb.permittedUsers = Uprawnieni u\u017Cytkownicy
+gb.isFrozen = jest zamro\u017Cony
+gb.isFrozenDescription = Odrzucaj operacje zapisu
+gb.zip = zip
+gb.showReadme = Poka\u017C readme
+gb.showReadmeDescription = Poka\u017C sparsowany \"readme\" na stronie podsumowania
+gb.nameDescription = u\u017Cyj '/' do grupowania repozytori\u00F3w, np. libraries/server-lib.git
+gb.ownerDescription = W\u0142a\u015Bciciel mo\u017Ce edytowa\u0107 ustawienia repozytorium
+gb.blob = blob
+gb.commitActivityTrend = Aktywno\u015B\u0107 zmian
+gb.commitActivityDOW = Aktywno\u015B\u0107 zmian wed\u0142ug dnia tygodnia
+gb.commitActivityAuthors = G\u0142\u00F3wni aktywni autorzy
+gb.feed = feed
+gb.cancel = Anuluj
+gb.changePassword = Zmie\u0144 has\u0142o
+gb.isFederated = Jest sfederowany
+gb.federateThis = Federuj to repozytorium
+gb.federateOrigin = Federuj origin
+gb.excludeFromFederation = Wy\u0142\u0105cz z federacji
+gb.excludeFromFederationDescription = Blokuj sfederowane instancje Gitblit od pobierania tego konta
+gb.tokens = Tokeny federacji
+gb.tokenAllDescription = Wszystkie repozytoria, u\u017Cytkownicy i ustawienia
+gb.tokenUnrDescription = Wszystkie repozytoria i u\u017Cytkownicy
+gb.tokenJurDescription = Wszystkie repozytoria
+gb.federatedRepositoryDefinitions = Definicje repozytori\u00F3w
+gb.federatedUserDefinitions = Definicje u\u017Cytkownik\u00F3w
+gb.federatedSettingDefinitions = Definicje ustawie\u0144
+gb.proposals = Propozycje federacji
+gb.received = Otrzymane
+gb.type = Typ
+gb.token = Token
+gb.repositories = Repozytoria
+gb.proposal = Propozycja
+gb.frequency = Cz\u0119stotliwo\u015B\u0107
+gb.folder = Folder
+gb.lastPull = Ostatnie pobranie
+gb.nextPull = Nast\u0119pne pobranie
+gb.inclusions = Do\u0142\u0105czenia
+gb.exclusions = Wy\u0142\u0105czenia
+gb.registration = Rejestracja
+gb.registrations = Sfederowane rejestracje
+gb.sendProposal = Zaproponuj
+gb.status = Status
+gb.origin = origin
+gb.headRef = Domy\u015Blne rozga\u0142\u0119zienie (HEAD)
+gb.headRefDescription = Zmie\u0144 ref aby wskazywa\u0142o na to co HEAD np. refs/heads/master
+gb.federationStrategy = Strategia federowania
+gb.federationRegistration = Rejestracja federowania
+gb.federationResults = Wyniki sfederowanego pobierania
+gb.federationSets = Zbiory federacji
+gb.message = Wiadomo\u015B\u0107
+gb.myUrlDescription = Zewn\u0119trznie widoczy url do tej instancji Gitblit
+gb.destinationUrl = Wy\u015Blij do
+gb.destinationUrlDescription = Url instacji Gitblit, gdzie chcesz wys\u0142a\u0107 swoj\u0105 propozycj\u0119
+gb.users = U\u017Cytkownicy
+gb.federation = Federacja
+gb.error = B\u0142\u0105d
+gb.refresh = Od\u015Bwie\u017C
+gb.browse = Przegl\u0105daj
+gb.clone = Klonuj
+gb.filter = Filtr
+gb.create = Stw\u00F3rz
+gb.servers = Serwery
+gb.recent = Ostatnie
+gb.available = Dost\u0119pne
+gb.selected = Wybrane
+gb.size = Rozmiar
+gb.downloading = Pobieranie
+gb.loading = \u0141adowanie
+gb.starting = Startowanie
+gb.general = Og\u00F3lne
+gb.settings = Ustawienia
+gb.manage = Zarz\u0105dzaj
+gb.lastLogin = Ostatni login
+gb.skipSizeCalculation = Pomi\u0144 obliczanie rozmiaru
+gb.skipSizeCalculationDescription = Nie obliczaj rozmiaru repozytorium (przyspiesza \u0142adowanie strony)
+gb.skipSummaryMetrics = Pomi\u0144 obliczanie metryk
+gb.skipSummaryMetricsDescription = Nie obliczaj metryk na stronie podsumowania (przyspiesza \u0142adowanie strony)
+gb.accessLevel = Poziom dost\u0119pu
+gb.default = Domy\u015Blny
+gb.setDefault = Ustaw domy\u015Blny
+gb.since = Od
+gb.status = Status
+gb.bootDate = Data uruchomienia
+gb.servletContainer = Kontener serwlet\u00F3w
+gb.heapMaximum = Maksymalny stos
+gb.heapAllocated = Przydzielony stos
+gb.heapUsed = U\u017Cywany stos
+gb.free = Wolne
+gb.version = Wersja
+gb.releaseDate = Data wydania
+gb.date = Data
+gb.activity = Aktywno\u015B\u0107
+gb.subscribe = Subskrybuj
+gb.branch = Rozga\u0142\u0119zienie
+gb.maxHits = Maks. ilo\u015B\u0107 dost\u0119pu
+gb.recentActivity = Ostatnia aktywno\u015B\u0107
+gb.recentActivityStats = Ostatnich {0} dni / {1} zmian przez {2} autor\u00F3w
+gb.recentActivityNone = Ostatnich {0} dni / brak
+gb.dailyActivity = Dzienna aktywno\u015B\u0107
+gb.activeRepositories = Aktywne repozytoria
+gb.activeAuthors = Aktywni u\u017Cytkownicy
+gb.commits = Zmiany
+gb.teams = Zespo\u0142y
+gb.teamName = Nazwa zespo\u0142u
+gb.teamMembers = Cz\u0142onkowie zespo\u0142u
+gb.teamMemberships = Cz\u0142onkostwo zespo\u0142u
+gb.newTeam = Nowy zesp\u00F3\u0142
+gb.permittedTeams = Uprawnione zespo\u0142y
+gb.emptyRepository = Puste repozytorium
+gb.repositoryUrl = Url repozytorium
+gb.mailingLists = Lista mailingowa
+gb.preReceiveScripts = Skrypty przed odbiorem zmian
+gb.postReceiveScripts = Skrypty po odbiorze zmian
+gb.hookScripts = Wpi\u0119te haki
+gb.customFields = Niestandardowe pola
+gb.customFieldsDescription = Niestandardowe pola dost\u0119pne dla hak\u00F3w Groovy
+gb.accessPermissions = Uprawnienia dot\u0119pu
+gb.filters = Filtry
+gb.generalDescription = Wsp\u00F3lne ustawienia
+gb.accessPermissionsDescription = Ogranicz dost\u0119p u\u017Cytkownikom i zespo\u0142om
+gb.accessPermissionsForUserDescription = Ustal cz\u0142onkostwo w zespo\u0142ach lub udziel dost\u0119p do specyficzynch repozytori\u00F3w
+gb.accessPermissionsForTeamDescription = Ustal cz\u0142onk\u00F3w zespo\u0142u i udziel dost\u0119p do specyficzynch repozytori\u00F3w
+gb.federationRepositoryDescription = Udost\u0119pnij to repozytorium innym serwerom Gitblit
+gb.hookScriptsDescription = Uruchamiaj skrypty Groovy w momencie wgrania zmian na ten serwer
+gb.reset = Resetuj
+gb.pages = Strony
+gb.workingCopy = Kopia robocza
+gb.workingCopyWarning = To repozytorium ma kopi\u0119 robocz\u0105 i nie mo\u017Ce otrzymywa\u0107 zmian
+gb.query = Szukaj
+gb.queryHelp = Standardowa sk\u0142adnia wyszukiwa\u0144 jest wspierana.<p/><p/>Na stronie <a target="_new" href="http://lucene.apache.org/core/old_versioned_docs/versions/3_5_0/queryparsersyntax.html">Lucene Query Parser Syntax</a> dost\u0119pne s\u0105 dalsze szczeg\u00F3\u0142y.
+gb.queryResults = Wyniki {0} - {1} ({2} wynik\u00F3w)
+gb.noHits = Brak wynik\u00F3w
+gb.authored = utworzy\u0142
+gb.committed = wgra\u0142
+gb.indexedBranches = Indeksowane rozga\u0142\u0119zienia
+gb.indexedBranchesDescription = Wybierz rozga\u0142\u0119zienia do w\u0142\u0105czenia do indeksu Lucene
+gb.noIndexedRepositoriesWarning = \u017Badne z rozga\u0142\u0119zie\u0144 w repozytorium nie jest dost\u0119pne dla Lucene
+gb.undefinedQueryWarning = Wyszukanie nie zdefiniowane!
+gb.noSelectedRepositoriesWarning = Wska\u017C jedno lub wi\u0119cej repozytori\u00F3w!
+gb.luceneDisabled = Indeksowanie Lucene jest wy\u0142\u0105czone.
+gb.failedtoRead = B\u0142\u0105d podczas odczytu
+gb.isNotValidFile = nie jest poprawnym plikiem.
+gb.failedToReadMessage = B\u0142\u0105d podczas pdczytu domy\u015Blnego komunikatu z {0}!
+gb.passwordsDoNotMatch = Brak zgodno\u015Bci has\u0142a!
+gb.passwordTooShort = Has\u0142o za kr\u00F3tkie. Minimalna d\u0142ugo\u015B\u0107 to {0} znak\u00F3w.
+gb.passwordChanged = Pomy\u015Blnie zmieniono has\u0142o.
+gb.passwordChangeAborted = Zmiania has\u0142a porzucona.
+gb.pleaseSetRepositoryName = Wska\u017C nazw\u0119 repozytorium!
+gb.illegalLeadingSlash = Poprzedzaj\u0105cy g\u0142\u00F3wny folder znaki (/) s\u0105 niedozwolone.
+gb.illegalRelativeSlash = Wzgl\u0119dne odwo\u0142ania do folder\u00F3w (../) s\u0105 niedozwolone.
+gb.illegalCharacterRepositoryName = Zabronionu znak ''{0}'' w nazwie repozytorium!
+gb.selectAccessRestriction = Wska\u017C ograniczenia dost\u0119pu!
+gb.selectFederationStrategy = Wska\u017C strategi\u0119 federacji!
+gb.pleaseSetTeamName = Wpisz nazw\u0119 zespo\u0142u!
+gb.teamNameUnavailable = Nazwa zespo\u0142u ''{0}'' jest niedost\u0119pna.
+gb.teamMustSpecifyRepository = Zesp\u00F3\u0142 musi posiada\u0107 conajmniej jedno repozytorium.
+gb.teamCreated = Zesp\u00F3\u0142 ''{0}'' zosta\u0142 utworzony.
+gb.pleaseSetUsername = Wpisz nazw\u0119 u\u017Cytkownika!
+gb.usernameUnavailable = Nazwa u\u017Cytkownika''{0}'' jest niedost\u0119pna.
+gb.combinedMd5Rename = Gitblit jest skonfigurowany na po\u0142\u0105czone haszowanie hase\u0142 md5. Musisz wpisa\u0107 nowe has\u0142o przy zmianie nazwy konta.
+gb.userCreated = U\u017Cytkownik ''{0}'' zosta\u0142 utworzony.
+gb.couldNotFindFederationRegistration = Nie mo\u017Cna znale\u017A\u0107 rejestracji federacji!
+gb.failedToFindGravatarProfile = B\u0142\u0105d podczas dopasowania profilu Gravatar dla {0}
+gb.branchStats = {0} zmian oraz {1} tag\u00F3w w {2}
+gb.repositoryNotSpecified = Repozytorium nie wskazane!
+gb.repositoryNotSpecifiedFor = Repozytorium nie wskazane dla {0}!
+gb.canNotLoadRepository = Nie mo\u017Cna za\u0142adowa\u0107 repozytorium
+gb.commitIsNull = Zmiana jest nullem
+gb.unauthorizedAccessForRepository = Nieuwierzytelniony dost\u0119p do repozytorium
+gb.failedToFindCommit = B\u0142\u0105d podczas wyszukania zmiany \"{0}\" w {1} dla {2} strony!
+gb.couldNotFindFederationProposal = Nie mo\u017Cna odnale\u017A\u0107 propozycji federacji!
+gb.invalidUsernameOrPassword = B\u0142\u0119dny u\u017Cytkownik lub has\u0142o!
+gb.OneProposalToReview = Jest 1 oczekuj\u0105ca propozycja federacji.
+gb.nFederationProposalsToReview = Jest {0} oczekuj\u0105cych propozycji federacji.
+gb.couldNotFindTag = Nie mo\u017Cna odnale\u017A\u0107 taga {0}
+gb.couldNotCreateFederationProposal = Nie mo\u017Cna utworzy\u0107 propozycji federacji!
+gb.pleaseSetGitblitUrl = Wpisz url serwera Gitblit!
+gb.pleaseSetDestinationUrl = Wpisz url, gdzie ma trafi\u0107 propozycja!
+gb.proposalReceived = Odebrano propozycj\u0119 od {0}.
+gb.noGitblitFound = Przepraszamy, {0} nie mo\u017Cna odnale\u017A\u0107 instancji Gitblit pod {1}.
+gb.noProposals = Przepraszamy, {0} nie akceptuje obecnie propozycji.
+gb.noFederation = Przepraszamy, {0} nie jest skonfigurowany do federacji z jakimikolwiek instancjami Gitblit.
+gb.proposalFailed = Przepraszamy, {0} nie otrzyma\u0142 \u017Cadnych danych propozycji!
+gb.proposalError = Przepraszamy, {0} informuje o nieoczekiwanym b\u0142\u0119dzie!
+gb.failedToSendProposal = B\u0142\u0105d podczas wysy\u0142ania propozycji!
+gb.userServiceDoesNotPermitAddUser = {0} nie zezwala na utworzenie nowego u\u017Cytkownika!
+gb.userServiceDoesNotPermitPasswordChanges = {0} nie zezwala na zmian\u0119 has\u0142a!
+gb.displayName = Wy\u015Bwietlana nazwa
+gb.emailAddress = Adres email
+gb.errorAdminLoginRequired = Administracja wymaga zalogowania
+gb.errorOnlyAdminMayCreateRepository = Tylko administrator mo\u017Ce utworzy\u0107 repozytorium
+gb.errorOnlyAdminOrOwnerMayEditRepository = Tylko administrator lub w\u0142a\u015Bciciel mo\u017Ce edytowa\u0107 repozytorium.
+gb.errorAdministrationDisabled = Administracja jest wy\u0142\u0105czona
+gb.lastNDays = Ostatnich {0} dni
+gb.completeGravatarProfile = Pe\u0142ny profil na Gravatar.com
+gb.none = Brak
+gb.line = Linia
+gb.content = Zawarto\u015B\u0107
+gb.empty = Pusty
+gb.inherited = Odziedziczony
+gb.deleteRepository = Usun\u0105\u0107 repozytorium \"{0}\"?
+gb.repositoryDeleted = Repozytorium ''{0}'' usuni\u0119te.
+gb.repositoryDeleteFailed = B\u0142\u0105d podczas usuwania repozytorium ''{0}''!
+gb.deleteUser = Usun\u0105\u0107 u\u017Cytkownika \"{0}\"?
+gb.userDeleted = U\u017Cytkownik ''{0}'' usuni\u0119ty.
+gb.userDeleteFailed = B\u0142\u0105d podczas usuwania u\u017Cytkownika ''{0}''!
+gb.time.justNow = Przed chwil\u0105
+gb.time.today = Dzisiaj
+gb.time.yesterday = Wczoraj
+gb.time.minsAgo = {0} minut temu
+gb.time.hoursAgo = {0} godzin temu
+gb.time.daysAgo = {0} dni temu
+gb.time.weeksAgo = {0} tygodni temu
+gb.time.monthsAgo = {0} miesi\u0119cy temu
+gb.time.oneYearAgo = Rok temu
+gb.time.yearsAgo = {0} lat temu
+gb.duration.oneDay = Dzie\u0144 temu
+gb.duration.days = {0} dni
+gb.duration.oneMonth = Miesi\u0105c
+gb.duration.months = {0} miesi\u0119cy
+gb.duration.oneYear = rok
+gb.duration.years = {0} lat
+gb.authorizationControl = kontrola autoryzacji
+gb.allowAuthenticatedDescription = udziel ograniczonego dost\u0119pu wszystkim uwierzytelnionym u\u017Cytkownikom
+gb.allowNamedDescription = udziel ograniczonego dost\u0119pu nazwanym u\u017Cytkownikom lub zespo\u0142om
+gb.markdownFailure = Nieudane parsowanie znacznik\u00F3w Markdown!
+gb.clearCache = Wyczy\u015B\u0107 cache
+gb.projects = Projekty
+gb.project = Projekt
+gb.allProjects = Wszystkie projekty
+gb.copyToClipboard = Kopiuj do schowka
+gb.sessionEnded = Session has been closed
+gb.closeBrowser = Please close the browser to properly end the session. \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/GitBlitWebApp_pt_BR.properties b/src/main/java/com/gitblit/wicket/GitBlitWebApp_pt_BR.properties
new file mode 100644
index 00000000..a02d2ffa
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/GitBlitWebApp_pt_BR.properties
@@ -0,0 +1,447 @@
+gb.repository = repositório
+gb.owner = proprietário
+gb.description = descrição
+gb.lastChange = última alteração
+gb.refs = refs
+gb.tag = tag
+gb.tags = tags
+gb.author = autor
+gb.committer = committer
+gb.commit = commit
+gb.tree = árvore
+gb.parent = parent
+gb.url = URL
+gb.history = histórico
+gb.raw = raw
+gb.object = object
+gb.ticketId = ticket id
+gb.ticketAssigned = atribuído
+gb.ticketOpenDate = data da abertura
+gb.ticketState = estado
+gb.ticketComments = comentários
+gb.view = visualizar
+gb.local = local
+gb.remote = remote
+gb.branches = branches
+gb.patch = patch
+gb.diff = diff
+gb.log = log
+gb.moreLogs = mais commits...
+gb.allTags = todas as tags...
+gb.allBranches = todos os branches...
+gb.summary = resumo
+gb.ticket = ticket
+gb.newRepository = novo repositório
+gb.newUser = novo usuário
+gb.commitdiff = commitdiff
+gb.tickets = tickets
+gb.pageFirst = primeira
+gb.pagePrevious anterior
+gb.pageNext = próxima
+gb.head = HEAD
+gb.blame = blame
+gb.login = login
+gb.logout = logout
+gb.username = username
+gb.password = password
+gb.tagger = tagger
+gb.moreHistory = mais histórico...
+gb.difftocurrent = diff para a atual
+gb.search = pesquisar
+gb.searchForAuthor = Procurar por commits cujo autor é
+gb.searchForCommitter = Procurar por commits commitados por
+gb.addition = adicionados
+gb.modification = modificados
+gb.deletion = apagados
+gb.rename = renomear
+gb.metrics = métricas
+gb.stats = estatísticas
+gb.markdown = markdown
+gb.changedFiles = arquivos alterados
+gb.filesAdded = {0} arquivos adicionados
+gb.filesModified = {0} arquivos modificados
+gb.filesDeleted = {0} arquivos deletados
+gb.filesCopied = {0} arquivos copiados
+gb.filesRenamed = {0} arquivos renomeados
+gb.missingUsername = Faltando username
+gb.edit = editar
+gb.searchTypeTooltip = Selecione o Tipo de Pesquisa
+gb.searchTooltip = Pesquisar {0}
+gb.delete = deletar
+gb.docs = docs
+gb.accessRestriction = restrição de acesso
+gb.name = nome
+gb.enableTickets = ativar tickets
+gb.enableDocs = ativar documentação
+gb.save = salvar
+gb.showRemoteBranches = mostrar branches remotos
+gb.editUsers = editar usuários
+gb.confirmPassword = confirmar password
+gb.restrictedRepositories = repositórios restritos
+gb.canAdmin = pode administrar
+gb.notRestricted = visualização anônima, clone, & push
+gb.pushRestricted = push autênticado
+gb.cloneRestricted = clone & push autênticados
+gb.viewRestricted = view, clone, & push autênticados
+gb.useTicketsDescription = somente leitura, issues do Ticgit distribuídos
+gb.useDocsDescription = enumerar documentação Markdown no repositório
+gb.showRemoteBranchesDescription = mostrar branches remotos
+gb.canAdminDescription = pode administrar o server Gitblit
+gb.permittedUsers = usuários autorizados
+gb.isFrozen = está congelado
+gb.isFrozenDescription = proibir fazer push
+gb.zip = zip
+gb.showReadme = mostrar readme
+gb.showReadmeDescription = mostrar um arquivo \"leia-me\" na página de resumo
+gb.nameDescription = usar '/' para agrupar repositórios. e.g. libraries/mycoollib.git
+gb.ownerDescription = o proprietário pode editar configurações do repositório
+gb.blob = blob
+gb.commitActivityTrend = tendência dos commits
+gb.commitActivityDOW = commits por dia da semana
+gb.commitActivityAuthors = principais committers
+gb.feed = feed
+gb.cancel = cancelar
+gb.changePassword = alterar password
+gb.isFederated = está federado
+gb.federateThis = federar este repositório
+gb.federateOrigin = federar o origin
+gb.excludeFromFederation = excluir da federação
+gb.excludeFromFederationDescription = bloquear instâncias federadas do GitBlit de fazer pull desta conta
+gb.tokens = tokens de federação
+gb.tokenAllDescription = todos repositórios, usuários & configurações
+gb.tokenUnrDescription = todos repositórios & usuários
+gb.tokenJurDescription = todos repositórios
+gb.federatedRepositoryDefinitions = definições de repositório
+gb.federatedUserDefinitions = definições de usuários
+gb.federatedSettingDefinitions = definições de configurações
+gb.proposals = propostas de federações
+gb.received = recebidos
+gb.type = tipo
+gb.token = token
+gb.repositories = repositórios
+gb.proposal = propostas
+gb.frequency = frequência
+gb.folder = pasta
+gb.lastPull = último pull
+gb.nextPull = próximo pull
+gb.inclusions = inclusões
+gb.exclusions = excluões
+gb.registration = cadastro
+gb.registrations = cadastro de federações
+gb.sendProposal = enviar proposta
+gb.status = status
+gb.origin = origin
+gb.headRef = default branch (HEAD)
+gb.headRefDescription = alterar a ref o qual a HEAD aponta. e.g. refs/heads/master
+gb.federationStrategy = estratégia de federação
+gb.federationRegistration = cadastro de federações
+gb.federationResults = resultados dos pulls de federações
+gb.federationSets = ajustes de federações
+gb.message = mensagem
+gb.myUrlDescription = a url de acesso público para a instância Gitblit
+gb.destinationUrl = enviar para
+gb.destinationUrlDescription = a url da intância do Gitblit para enviar sua proposta
+gb.users = usuários
+gb.federation = federação
+gb.error = erro
+gb.refresh = atualizar
+gb.browse = navegar
+gb.clone = clonar
+gb.filter = filtrar
+gb.create = criar
+gb.servers = servidores
+gb.recent = recente
+gb.available = disponível
+gb.selected = selecionado
+gb.size = tamanho
+gb.downloading = downloading
+gb.loading = loading
+gb.starting = inciando
+gb.general = geral
+gb.settings = configurações
+gb.manage = administrar
+gb.lastLogin = último login
+gb.skipSizeCalculation = ignorar cálculo do tamanho
+gb.skipSizeCalculationDescription = não calcular o tamanho do repositório (reduz o tempo de load da página)
+gb.skipSummaryMetrics = ignorar resumo das métricas
+gb.skipSummaryMetricsDescription = não calcular métricas na página de resumo
+gb.accessLevel = acesso
+gb.default = default
+gb.setDefault = tornar default
+gb.since = desde
+gb.status = status
+gb.bootDate = data do boot
+gb.servletContainer = servlet container
+gb.heapMaximum = heap máximo
+gb.heapAllocated = alocar heap
+gb.heapUsed = usar heap
+gb.free = free
+gb.version = versão
+gb.releaseDate = data de release
+gb.date = data
+gb.activity = atividade
+gb.subscribe = inscrever
+gb.branch = branch
+gb.maxHits = hits máximos
+gb.recentActivity = atividade recente
+gb.recentActivityStats = últimos {0} dias / {1} commits por {2} autores
+gb.recentActivityNone = últimos {0} dias / nenhum
+gb.dailyActivity = atividade diária
+gb.activeRepositories = repositórios ativos
+gb.activeAuthors = autores ativos
+gb.commits = commits
+gb.teams = equipes
+gb.teamName = nome da equipe
+gb.teamMembers = membros
+gb.teamMemberships = filiações em equipes
+gb.newTeam = nova equipe
+gb.permittedTeams = equipes permitidas
+gb.emptyRepository = repositório vazio
+gb.repositoryUrl = url do repositório
+gb.mailingLists = listas de e-mails
+gb.preReceiveScripts = pre-receive scripts
+gb.postReceiveScripts = post-receive scripts
+gb.hookScripts = hook scripts
+gb.customFields = campos customizados
+gb.customFieldsDescription = campos customizados disponíveis para Groovy hooks
+gb.accessPermissions = permissões de acesso
+gb.filters = filtros
+gb.generalDescription = configurações comuns
+gb.accessPermissionsDescription = restringir acesso por usuários e equipes
+gb.accessPermissionsForUserDescription = ajustar filiações em equipes ou garantir acesso a repositórios específicos
+gb.accessPermissionsForTeamDescription = ajustar membros da equipe e garantir acesso a repositórios específicos
+gb.federationRepositoryDescription = compartilhar este repositório com outros servidores Gitblit
+gb.hookScriptsDescription = rodar scripts Groovy nos pushes para este servidor Gitblit
+gb.reset = reset
+gb.pages = páginas
+gb.workingCopy = working copy
+gb.workingCopyWarning = este repositório tem uma working copy e não pode receber pushes
+gb.query = query
+gb.queryHelp = Standard query syntax é suportada.<p/><p/>Por favor veja <a target="_new" href="http://lucene.apache.org/core/old_versioned_docs/versions/3_5_0/queryparsersyntax.html">Lucene Query Parser Syntax</a> para mais detalhes.
+gb.queryResults = resultados {0} - {1} ({2} hits)
+gb.noHits = sem hits
+gb.authored = foi autor de
+gb.committed = committed
+gb.indexedBranches = branches indexados
+gb.indexedBranchesDescription = selecione os branches para incluir nos seus índices Lucene
+gb.noIndexedRepositoriesWarning = nenhum dos seus repositórios foram configurados para indexação do Lucene
+gb.undefinedQueryWarning = a query não foi definida!
+gb.noSelectedRepositoriesWarning = por favor selecione um ou mais repositórios!
+gb.luceneDisabled = indexação do Lucene está desabilitada
+gb.failedtoRead = leitura falhou
+gb.isNotValidFile = não é um arquivo válido
+gb.failedToReadMessage = Falhou em ler mensagens default de {0}!
+gb.passwordsDoNotMatch = Passwords não conferem!
+gb.passwordTooShort = Password é muito curto. Tamanho mínimo são {0} caracteres.
+gb.passwordChanged = Password alterado com sucesso.
+gb.passwordChangeAborted = alteração do password foi abortada.
+gb.pleaseSetRepositoryName = Por favor ajuste o nome do repositório!
+gb.illegalLeadingSlash = Referências a diretórios raiz começando com (/) são proibidas.
+gb.illegalRelativeSlash = Referências a diretórios relativos (../) são proibidas.
+gb.illegalCharacterRepositoryName = Caractere ilegal ''{0}'' no nome do repositório!
+gb.selectAccessRestriction = Por favor selecione a restrição de acesso!
+gb.selectFederationStrategy = Por favor selecione a estratégia de federação!
+gb.pleaseSetTeamName = Por favor insira um nome de equipe!
+gb.teamNameUnavailable = O nome de equipe ''{0}'' está indisponível.
+gb.teamMustSpecifyRepository = Uma equipe deve especificar pelo menos um repositório.
+gb.teamCreated = Nova equipe ''{0}'' criada com sucesso.
+gb.pleaseSetUsername = Por favor entre com um username!
+gb.usernameUnavailable = Username ''{0}'' está indisponível.
+gb.combinedMd5Rename = Gitblit está configurado para usar um hash combinado-md5. Você deve inserir um novo password ao renamear a conta.
+gb.userCreated = Novo usuário ''{0}'' criado com sucesso.
+gb.couldNotFindFederationRegistration = Não foi possível localizar o registro da federação!
+gb.failedToFindGravatarProfile = Falhou em localizar um perfil Gravatar para {0}
+gb.branchStats = {0} commits e {1} tags em {2}
+gb.repositoryNotSpecified = Repositório não específicado!
+gb.repositoryNotSpecifiedFor = Repositório não específicado para {0}!
+gb.canNotLoadRepository = Não foi possível carregar o repositório
+gb.commitIsNull = Commit está nulo
+gb.unauthorizedAccessForRepository = Acesso não autorizado para o repositório
+gb.failedToFindCommit = Não foi possível achar o commit \"{0}\" em {1} para {2} página!
+gb.couldNotFindFederationProposal = Não foi possível localizar propostas de federação!
+gb.invalidUsernameOrPassword = username ou password inválido!
+gb.OneProposalToReview = Existe uma proposta de federação aguardando revisão.
+gb.nFederationProposalsToReview = Existem {0} propostas de federação aguardando revisão.
+gb.couldNotFindTag = Não foi possível localizar a tag {0}
+gb.couldNotCreateFederationProposal = Não foi possível criar uma proposta de federation!
+gb.pleaseSetGitblitUrl = Por favor insira sua url do Gitblit!
+gb.pleaseSetDestinationUrl = Por favor insira a url de destino para sua proposta!
+gb.proposalReceived = Proposta recebida com sucesso por {0}.
+gb.noGitblitFound = Desculpe, {0} não localizou uma instância do Gitblit em {1}.
+gb.noProposals = Desculpe, {0} não está aceitando propostas agora.
+gb.noFederation = Desculpe, {0} não está configurado com nenhuma intância do Gitblit.
+gb.proposalFailed = Desculpe, {0} não recebeu nenhum dado de proposta!
+gb.proposalError = Desculpe, {0} reportou que um erro inesperado ocorreu!
+gb.failedToSendProposal = Não foi possível enviar a proposta!
+gb.userServiceDoesNotPermitAddUser = {0} não permite adicionar uma conta de usuário!
+gb.userServiceDoesNotPermitPasswordChanges = {0} não permite alterações no password!
+gb.displayName = nome
+gb.emailAddress = e-mail
+gb.errorAdminLoginRequired = Administração requer um login
+gb.errorOnlyAdminMayCreateRepository = Somente umadministrador pode criar um repositório
+gb.errorOnlyAdminOrOwnerMayEditRepository = Somente umadministrador pode editar um repositório
+gb.errorAdministrationDisabled = Administração está desabilitada
+gb.lastNDays = últimos {0} dias
+gb.completeGravatarProfile = Profile completo em Gravatar.com
+gb.none = nenhum
+gb.line = linha
+gb.content = conteúdo
+gb.empty = vazio
+gb.inherited = herdado
+gb.deleteRepository = Deletar repositório \"{0}\"?
+gb.repositoryDeleted = Repositório ''{0}'' deletado.
+gb.repositoryDeleteFailed = Não foi possível apagar o repositório ''{0}''!
+gb.deleteUser = Deletar usuário \"{0}\"?
+gb.userDeleted = Usuário ''{0}'' deletado.
+gb.userDeleteFailed = Não foi possível apagar o usuário ''{0}''!
+gb.time.justNow = agora mesmo
+gb.time.today = hoje
+gb.time.yesterday = ontem
+gb.time.minsAgo = há {0} minutos
+gb.time.hoursAgo = há {0} horas
+gb.time.daysAgo = há {0} dias
+gb.time.weeksAgo = há {0} semanas
+gb.time.monthsAgo = há {0} meses
+gb.time.oneYearAgo = há 1 ano
+gb.time.yearsAgo = há {0} anos
+gb.duration.oneDay = 1 dia
+gb.duration.days = {0} dias
+gb.duration.oneMonth = 1 mês
+gb.duration.months = {0} meses
+gb.duration.oneYear = 1 ano
+gb.duration.years = {0} anos
+gb.authorizationControl = controle de autorização
+gb.allowAuthenticatedDescription = conceder permissão RW+ para todos os usuários autênticados
+gb.allowNamedDescription = conceder permissões refinadas para usuários escolhidos ou equipes
+gb.markdownFailure = Não foi possível converter conteúdo Markdown!
+gb.clearCache = limpar o cache
+gb.projects = projetos
+gb.project = projeto
+gb.allProjects = todos projetos
+gb.copyToClipboard = copiar para o clipboard
+gb.fork = fork
+gb.forks = forks
+gb.forkRepository = fork {0}?
+gb.repositoryForked = fork feito em {0}
+gb.repositoryForkFailed= não foi possível fazer fork
+gb.personalRepositories = repositórios pessoais
+gb.allowForks = permitir forks
+gb.allowForksDescription = permitir usuários autorizados a fazer fork deste repositório
+gb.forkedFrom = forked de
+gb.canFork = pode fazer fork
+gb.canForkDescription = pode fazer fork de repositórios autorizados para repositórios pessoais
+gb.myFork = visualizar meu fork
+gb.forksProhibited = forks proibidos
+gb.forksProhibitedWarning = este repositório proíbe forks
+gb.noForks = {0} não possui forks
+gb.forkNotAuthorized = desculpe, você não está autorizado a fazer fork de {0}
+gb.forkInProgress = fork em progresso
+gb.preparingFork = preparando seu fork...
+gb.isFork = é fork
+gb.canCreate = pode criar
+gb.canCreateDescription = pode criar repositórios pessoais
+gb.illegalPersonalRepositoryLocation = seu repositório pessoal deve estar localizado em \"{0}\"
+gb.verifyCommitter = verificar committer
+gb.verifyCommitterDescription = requer a identidade do committer para combinar com uma conta do Gitblt
+gb.verifyCommitterNote = todos os merges requerem "--no-ff" para impor a identidade do committer
+gb.repositoryPermissions = permissões de repositório
+gb.userPermissions = permissões de usuário
+gb.teamPermissions = permissões de equipe
+gb.add = add
+gb.noPermission = APAGAR ESTA PERMISSÃO
+gb.excludePermission = {0} (excluir)
+gb.viewPermission = {0} (visualizar)
+gb.clonePermission = {0} (clonar)
+gb.pushPermission = {0} (push)
+gb.createPermission = {0} (push, ref creation)
+gb.deletePermission = {0} (push, ref creation+deletion)
+gb.rewindPermission = {0} (push, ref creation+deletion+rewind)
+gb.permission = permissão
+gb.regexPermission = esta permissão foi configurada através da expressão regular \"{0}\"
+gb.accessDenied = acesso negado
+gb.busyCollectingGarbage = desculpe, o Gitblit está ocupado coletando lixo em {0}
+gb.gcPeriod = período do GC
+gb.gcPeriodDescription = duração entre as coletas de lixo
+gb.gcThreshold = limite do GC
+gb.gcThresholdDescription = tamanho total mínimo de objetos \"soltos\" que ativam a coleta de lixo
+gb.ownerPermission = proprietário do repositório
+gb.administrator = administrador
+gb.administratorPermission = administrador do Gitblit
+gb.team = equipe
+gb.teamPermission = permissão concedida pela filiação a equipe \"{0}\"
+gb.missing = faltando!
+gb.missingPermission = o repositório para esta permissão está faltando!
+gb.mutable = mutável
+gb.specified = específico
+gb.effective = efetivo
+gb.organizationalUnit = unidade organizacional
+gb.organization = organização
+gb.locality = localidade
+gb.stateProvince = estado ou província
+gb.countryCode = código do país
+gb.properties = propriedades
+gb.issued = emitido
+gb.expires = expira
+gb.expired = expirado
+gb.expiring = expirando
+gb.revoked = revogado
+gb.serialNumber = número serial
+gb.certificates = certificados
+gb.newCertificate = novo certificado
+gb.revokeCertificate = revogar certificado
+gb.sendEmail = enviar email
+gb.passwordHint = dica de password
+gb.ok = ok
+gb.invalidExpirationDate = data de expiração inválida!
+gb.passwordHintRequired = dica de password requerida!
+gb.viewCertificate = visualizar certificado
+gb.subject = assunto
+gb.issuer = emissor
+gb.validFrom = válido a partir de
+gb.validUntil = válido até
+gb.publicKey = chave pública
+gb.signatureAlgorithm = algoritmo de assinatura
+gb.sha1FingerPrint = digital SHA-1
+gb.md5FingerPrint = digital MD5
+gb.reason = razão
+gb.revokeCertificateReason = Por selecione a razão da revogação do certificado
+gb.unspecified = não específico
+gb.keyCompromise = comprometimento de chave
+gb.caCompromise = compromisso CA
+gb.affiliationChanged = afiliação foi alterada
+gb.superseded = substituídas
+gb.cessationOfOperation = cessação de funcionamento
+gb.privilegeWithdrawn = privilégio retirado
+gb.time.inMinutes = em {0} minutos
+gb.time.inHours = em {0} horas
+gb.time.inDays = em {0} dias
+gb.hostname = hostname
+gb.hostnameRequired = Por favor insira um hostname
+gb.newSSLCertificate = novo servidor de certificado SSL
+gb.newCertificateDefaults = novos padrões de certificação
+gb.duration = duração
+gb.certificateRevoked = Certificado {0, número, 0} foi revogado
+gb.clientCertificateGenerated = Novo certificado cliente para {0} foi gerado com sucesso
+gb.sslCertificateGenerated = Novo servidor de certificado SSL gerado com sucesso para {0}
+gb.newClientCertificateMessage = OBSERVAÇÃO:\nO 'password' não é o password do usuário mas sim o password usado para proteger a keystore. Este password não será salvo então você também inserir uma dica que será incluída nas instruções de LEIA-ME do usuário.
+gb.certificate = certificado
+gb.emailCertificateBundle = pacote certificado de cliente de email
+gb.pleaseGenerateClientCertificate = Por favor gere um certificado cliente para {0}
+gb.clientCertificateBundleSent = Pacote de certificado de cliente para {0} enviada
+gb.enterKeystorePassword = Por favor insira uma chave para keystore do Gitblit
+gb.warning = warning
+gb.jceWarning = Seu Java Runtime Environment não tem os arquivos \"JCE Unlimited Strength Jurisdiction Policy\".\nIsto irá limitar o tamanho dos passwords que você usará para encriptar suas keystores para 7 caracteres.\nEstes arquivos de políticas são um download opcional da Oracle.\n\nVocê gostaria de continuar e gerar os certificados de infraestrutura de qualquer forma?\n\nRespondendo "Não" irá redirecionar o seu browser para a página de downloads da Oracle, de onde você poderá fazer download desses arquivos.
+gb.maxActivityCommits = limitar exibição de commits
+gb.maxActivityCommitsDescription = quantidade máxima de commits para contribuir para a página de atividade
+gb.noMaximum = ilimitado
+gb.attributes = atributos
+gb.serveCertificate = servir https com este certificado
+gb.sslCertificateGeneratedRestart = Novo certificado SSL de servidor gerado com sucesso para {0}.\nVocê deve reiniciar o Gitblit para usar o novo certificado.\n\nSe você estiver executando com o parâmetro '--alias', você precisará alterá-lo para ''--alias {0}''.
+gb.validity = validade
+gb.siteName = nome do site
+gb.siteNameDescription = breve, mas ainda assim um nome descritivo para seu servidor
+gb.excludeFromActivity = excluir da página de atividades
+gb.isSparkleshared = repositório é Sparkleshared
+gb.owners = proprietários
+gb.sessionEnded = Session has been closed
+gb.closeBrowser = Please close the browser to properly end the session. \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/GitBlitWebApp_zh_CN.properties b/src/main/java/com/gitblit/wicket/GitBlitWebApp_zh_CN.properties
new file mode 100644
index 00000000..96e2067b
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/GitBlitWebApp_zh_CN.properties
@@ -0,0 +1,446 @@
+gb.repository = \u7248\u672c\u5e93
+gb.owner = \u7ba1\u7406\u5458
+gb.description = \u63cf\u8ff0
+gb.lastChange = \u6700\u8fd1\u4fee\u6539
+gb.refs = refs
+gb.tag = \u6807\u7b7e
+gb.tags = \u6807\u7b7e
+gb.author = \u7528\u6237
+gb.committer = \u63d0\u4ea4\u8005
+gb.commit = \u63d0\u4ea4
+gb.tree = \u76ee\u5f55
+gb.parent = parent
+gb.url = URL
+gb.history = \u5386\u53f2\u4fe1\u606f
+gb.raw = raw
+gb.object = object
+gb.ticketId = ticket id
+gb.ticketAssigned = assigned
+gb.ticketOpenDate = \u5f00\u542f\u65e5\u671f
+gb.ticketState = \u72b6\u6001
+gb.ticketComments = \u8bc4\u8bba
+gb.view = \u67e5\u770b
+gb.local = \u672c\u5730
+gb.remote = \u8fdc\u7a0b
+gb.branches = \u5206\u652f
+gb.patch = patch
+gb.diff = \u5bf9\u6bd4
+gb.log = \u65e5\u5fd7
+gb.moreLogs = \u66f4\u591a\u63d0\u4ea4...
+gb.allTags = \u6240\u6709\u6807\u7b7e...
+gb.allBranches = \u6240\u6709\u5206\u652f...
+gb.summary = \u6982\u51b5
+gb.ticket = ticket
+gb.newRepository = \u521b\u5efa\u7248\u672c\u5e93
+gb.newUser = \u6dfb\u52a0\u7528\u6237
+gb.commitdiff = \u5bf9\u6bd4\u63d0\u4ea4\u7684\u5185\u5bb9
+gb.tickets = tickets
+gb.pageFirst = \u9996\u9875
+gb.pagePrevious = \u524d\u4e00\u9875
+gb.pageNext = \u4e0b\u4e00\u9875
+gb.head = HEAD
+gb.blame = blame
+gb.login = \u767b\u5f55
+gb.logout = \u6ce8\u9500
+gb.username = \u7528\u6237\u540d
+gb.password = \u5bc6\u7801
+gb.tagger = \u6807\u8bb0\u8005
+gb.moreHistory = \u66f4\u591a\u7684\u5386\u53f2\u4fe1\u606f...
+gb.difftocurrent = \u5bf9\u6bd4\u5f53\u524d
+gb.search = \u641c\u7d22
+gb.searchForAuthor = \u6309\u4f5c\u8005\u641c\u7d22 commits
+gb.searchForCommitter = \u6309\u63d0\u4ea4\u8005\u641c\u7d22 commits
+gb.addition = \u6dfb\u52a0
+gb.modification = \u4fee\u6539
+gb.deletion = \u5220\u9664
+gb.rename = \u91cd\u547d\u540d
+gb.metrics = metrics
+gb.stats = \u7edf\u8ba1
+gb.markdown = markdown
+gb.changedFiles = \u5df2\u4fee\u6539\u6587\u4ef6
+gb.filesAdded = {0}\u4e2a\u6587\u4ef6\u5df2\u6dfb\u52a0
+gb.filesModified = {0}\u4e2a\u6587\u4ef6\u5df2\u4fee\u6539
+gb.filesDeleted = {0}\u4e2a\u6587\u4ef6\u5df2\u5220\u9664
+gb.filesCopied = {0} \u6587\u4ef6\u5df2\u590d\u5236
+gb.filesRenamed = {0} \u6587\u4ef6\u5df2\u91cd\u547d\u540d
+gb.missingUsername = \u7528\u6237\u540d\u4e0d\u5b58\u5728
+gb.edit = \u7f16\u8f91
+gb.searchTypeTooltip = \u9009\u62e9\u641c\u7d22\u7c7b\u578b
+gb.searchTooltip = \u641c\u7d22 {0}
+gb.delete = \u5220\u9664
+gb.docs = \u6587\u6863
+gb.accessRestriction = \u8bbf\u95ee\u9650\u5236
+gb.name = \u540d\u79f0
+gb.enableTickets = \u5141\u8bb8 tickets
+gb.enableDocs = \u5141\u8bb8\u6587\u6863
+gb.save = \u4fdd\u5b58
+gb.showRemoteBranches = \u663e\u793a\u8fdc\u7a0b\u5206\u652f
+gb.editUsers = \u7f16\u8f91\u7528\u6237
+gb.confirmPassword = \u786e\u8ba4\u5bc6\u7801
+gb.restrictedRepositories = \u7248\u672c\u5e93\u8bbe\u7f6e
+gb.canAdmin = \u7ba1\u7406\u6743\u9650
+gb.notRestricted = anonymous view, clone, & push
+gb.pushRestricted = authenticated push
+gb.cloneRestricted = authenticated clone & push
+gb.viewRestricted = authenticated view, clone, & push
+gb.useTicketsDescription = distributed Ticgit issues
+gb.useDocsDescription = \u5217\u51fa\u7248\u672c\u5e93\u5185\u6240\u6709 Markdown \u6587\u6863
+gb.showRemoteBranchesDescription = \u663e\u793a\u8fdc\u7a0b\u5206\u652f
+gb.canAdminDescription = Gitblit \u670d\u52a1\u5668\u7ba1\u7406\u5458
+gb.permittedUsers = \u5141\u8bb8\u7528\u6237
+gb.isFrozen = \u88ab\u51bb\u7ed3
+gb.isFrozenDescription = \u7981\u6b62\u63a8\u9001\u64cd\u4f5c
+gb.zip = zip
+gb.showReadme = \u663e\u793areadme
+gb.showReadmeDescription = \u5728\u6982\u51b5\u9875\u9762\u663e\u793a \\"readme\\" Markdown \u6587\u4ef6
+gb.nameDescription = \u4f7f\u7528 '/' \u5bf9\u7248\u672c\u5e93\u8fdb\u884c\u5206\u7ec4 \u4f8b\u5982. libraries/mycoollib.git
+gb.ownerDescription = \u521b\u5efa\u8005\u53ef\u4ee5\u7f16\u8f91\u7248\u672c\u5e93\u5c5e\u6027
+gb.blob = blob
+gb.commitActivityTrend = commit \u6d3b\u52a8\u8d8b\u52bf
+gb.commitActivityDOW = \u6bcf\u5468 commit \u6d3b\u52a8
+gb.commitActivityAuthors = commit \u6d3b\u52a8\u4e3b\u8981\u7528\u6237
+gb.feed = feed
+gb.cancel = \u53d6\u6d88
+gb.changePassword = \u4fee\u6539\u5bc6\u7801
+gb.isFederated = is federated
+gb.federateThis = federate this repository
+gb.federateOrigin = federate the origin
+gb.excludeFromFederation = exclude from federation
+gb.excludeFromFederationDescription = \u7981\u6b62\u5df2 federated \u7684 Gitblit \u5b9e\u4f8b\u4ece\u672c\u8d26\u6237\u62c9\u53d6
+gb.tokens = federation tokens
+gb.tokenAllDescription = all repositories, users, & settings
+gb.tokenUnrDescription = all repositories & users
+gb.tokenJurDescription = all repositories
+gb.federatedRepositoryDefinitions = \u7248\u672c\u5e93\u5b9a\u4e49
+gb.federatedUserDefinitions = \u7528\u6237\u5b9a\u4e49
+gb.federatedSettingDefinitions = \u8bbe\u7f6e\u5b9a\u4e49
+gb.proposals = federation proposals
+gb.received = \u5df2\u63a5\u53d7
+gb.type = type
+gb.token = token
+gb.repositories = \u7248\u672c\u5e93
+gb.proposal = proposal
+gb.frequency = \u9891\u7387
+gb.folder = \u6587\u4ef6\u5939
+gb.lastPull = \u4e0a\u4e00\u6b21\u62c9\u53d6
+gb.nextPull = \u4e0b\u4e00\u6b21\u62c9\u53d6
+gb.inclusions = \u5305\u542b\u5185\u5bb9
+gb.exclusions = \u4f8b\u5916
+gb.registration = \u6ce8\u518c
+gb.registrations = federation \u6ce8\u518c
+gb.sendProposal = propose
+gb.status = \u72b6\u6001
+gb.origin = origin
+gb.headRef = \u9ed8\u8ba4\u5206\u652f (HEAD)
+gb.headRefDescription = \u4fee\u6539 HEAD \u6240\u6307\u5411\u7684 ref\u3002 \u4f8b\u5982: refs/heads/master
+gb.federationStrategy = federation \u7b56\u7565
+gb.federationRegistration = federation \u6ce8\u518c
+gb.federationResults = federation \u62c9\u53d6\u7ed3\u679c
+gb.federationSets = federation \u96c6
+gb.message = \u6d88\u606f
+gb.myUrlDescription = \u60a8\u7684 Gitblit \u5b9e\u4f8b\u7684\u516c\u5171\u8bbf\u95ee\u7f51\u5740
+gb.destinationUrl = \u53d1\u9001\u81f3
+gb.destinationUrlDescription = \u4f60\u6240\u8981\u53d1\u9001proposal\u7684 Gitblit \u5b9e\u4f8b\u7f51\u5740
+gb.users = \u7528\u6237
+gb.federation = federation
+gb.error = \u9519\u8bef
+gb.refresh = \u5237\u65b0
+gb.browse = \u6d4f\u89c8
+gb.clone = \u514b\u9686
+gb.filter = \u8fc7\u6ee4
+gb.create = \u521b\u5efa
+gb.servers = \u670d\u52a1\u5668
+gb.recent = \u6700\u8fd1
+gb.available = \u53ef\u7528
+gb.selected = \u5df2\u9009\u4e2d
+gb.size = \u5927\u5c0f
+gb.downloading = \u4e0b\u8f7d\u4e2d
+gb.loading = \u8f7d\u5165\u4e2d
+gb.starting = \u542f\u52a8\u4e2d
+gb.general = \u5e38\u89c4
+gb.settings = \u8bbe\u7f6e
+gb.manage = \u7ba1\u7406
+gb.lastLogin = \u4e0a\u6b21\u767b\u5f55
+gb.skipSizeCalculation = \u5ffd\u7565\u5927\u5c0f\u4f30\u8ba1
+gb.skipSizeCalculationDescription = \u4e0d\u8ba1\u7b97\u7248\u672c\u5e93\u5927\u5c0f\uff08\u8282\u7701\u9875\u9762\u8f7d\u5165\u65f6\u95f4\uff09
+gb.skipSummaryMetrics = \u5ffd\u7565\u6982\u51b5\u5904 metrics
+gb.skipSummaryMetricsDescription = \u6982\u51b5\u9875\u9762\u4e0d\u8ba1\u7b97metrics\uff08\u8282\u7701\u9875\u9762\u8f7d\u5165\u65f6\u95f4\uff09
+gb.accessLevel = \u8bbf\u95ee\u7ea7\u522b
+gb.default = \u9ed8\u8ba4
+gb.setDefault = \u9ed8\u8ba4\u8bbe\u7f6e
+gb.since = \u81ea\u4ece
+gb.status = \u72b6\u6001
+gb.bootDate = \u542f\u52a8\u65e5\u671f
+gb.servletContainer = servlet container
+gb.heapMaximum = \u6700\u5927\u5806
+gb.heapAllocated = \u5df2\u5206\u914d\u5806
+gb.heapUsed = \u5df2\u4f7f\u7528\u5806
+gb.free = \u7a7a\u95f2
+gb.version = \u7248\u672c
+gb.releaseDate = \u53d1\u884c\u65e5\u671f
+gb.date = \u65e5\u671f
+gb.activity = \u6d3b\u52a8
+gb.subscribe = \u8ba2\u9605
+gb.branch = \u5206\u652f
+gb.maxHits = \u6700\u5927\u547d\u4e2d\u6570
+gb.recentActivity = \u6700\u8fd1\u6d3b\u52a8
+gb.recentActivityStats = \u6700\u8fd1{0}\u5929 / {2}\u4f4d\u7528\u6237\u505a\u4e86{1}\u6b21\u63d0\u4ea4
+gb.recentActivityNone = \u6700\u8fd1{0}\u5929 / \u6ca1\u6709\u6d3b\u52a8
+gb.dailyActivity = \u65e5\u5e38\u6d3b\u52a8
+gb.activeRepositories = \u6d3b\u8dc3\u7684\u7248\u672c\u5e93
+gb.activeAuthors = \u6d3b\u8dc3\u7528\u6237
+gb.commits = \u63d0\u4ea4\u6b21\u6570
+gb.teams = \u56e2\u961f
+gb.teamName = \u56e2\u961f\u540d\u79f0
+gb.teamMembers = \u56e2\u961f\u6210\u5458
+gb.teamMemberships = \u56e2\u961f\u6210\u5458
+gb.newTeam = \u6dfb\u52a0\u56e2\u961f
+gb.permittedTeams = \u5141\u8bb8\u56e2\u961f
+gb.emptyRepository = \u7a7a\u7248\u672c\u5e93
+gb.repositoryUrl = \u7248\u672c\u5e93\u5730\u5740
+gb.mailingLists = \u90ae\u4ef6\u5217\u8868
+gb.preReceiveScripts = pre-receive \u811a\u672c
+gb.postReceiveScripts = post-receive \u811a\u672c
+gb.hookScripts = hook \u811a\u672c
+gb.customFields = \u81ea\u5b9a\u4e49\u57df
+gb.customFieldsDescription = Groovy\u811a\u672c\u652f\u6301\u7684\u81ea\u5b9a\u4e49\u57df
+gb.accessPermissions = \u8bbf\u95ee\u6743\u9650
+gb.filters = \u8fc7\u6ee4
+gb.generalDescription = \u4e00\u822c\u8bbe\u7f6e
+gb.accessPermissionsDescription = \u6309\u7167\u7528\u6237\u548c\u56e2\u961f\u9650\u5236\u8bbf\u95ee
+gb.accessPermissionsForUserDescription = \u8bbe\u7f6e\u56e2\u961f\u6210\u5458\u6216\u8005\u6388\u4e88\u6307\u5b9a\u7248\u672c\u5e93\u6743\u9650
+gb.accessPermissionsForTeamDescription = \u8bbe\u7f6e\u56e2\u961f\u6210\u5458\u5e76\u6388\u4e88\u6307\u5b9a\u7248\u672c\u5e93\u6743\u9650
+gb.federationRepositoryDescription = \u4e0e\u5176\u4ed6Gitblit\u670d\u52a1\u5668\u5206\u4eab\u7248\u672c\u5e93
+gb.hookScriptsDescription = \u5728\u670d\u52a1\u5668\u4e0a\u8fd0\u884cGroovy\u811a\u672c
+gb.reset = \u91cd\u7f6e
+gb.pages = \u9875\u9762
+gb.workingCopy = \u5de5\u4f5c\u526f\u672c
+gb.workingCopyWarning = \u6b64\u7248\u672c\u5e93\u5b58\u5728\u4e00\u4efd\u5de5\u4f5c\u526f\u672c\uff0c\u65e0\u6cd5\u8fdb\u884c\u63a8\u9001
+gb.query = \u67e5\u8be2
+gb.queryHelp = \u652f\u6301\u6807\u51c6\u67e5\u8be2\u683c\u5f0f.<p/><p/>\u8bf7\u67e5\u770b <a target="_new" href="http://lucene.apache.org/core/old_versioned_docs/versions/3_5_0/queryparsersyntax.html">Lucene \u67e5\u8be2\u5904\u7406\u5668\u683c\u5f0f</a> \u4ee5\u83b7\u53d6\u8be6\u7ec6\u5185\u5bb9\u3002
+gb.queryResults = \u7ed3\u679c {0} - {1} ({2} \u6b21\u547d\u4e2d)
+gb.noHits = \u672a\u547d\u4e2d
+gb.authored = authored
+gb.committed = committed
+gb.indexedBranches = \u5df2\u7d22\u5f15\u5206\u652f
+gb.indexedBranchesDescription = \u9009\u62e9\u8981\u653e\u5165\u4f60\u7684 Lucene \u7d22\u5f15\u7684\u5206\u652f
+gb.noIndexedRepositoriesWarning = \u60a8\u7684\u6240\u6709\u7248\u672c\u5e93\u90fd\u6ca1\u6709\u7ecf\u8fc7Lucene\u7d22\u5f15
+gb.undefinedQueryWarning = \u67e5\u8be2\u672a\u5b9a\u4e49!
+gb.noSelectedRepositoriesWarning = \u8bf7\u81f3\u5c11\u9009\u62e9\u4e00\u4e2a\u7248\u672c\u5e93!
+gb.luceneDisabled = Lucene\u7d22\u5f15\u5df2\u88ab\u7981\u6b62
+gb.failedtoRead = \u8bfb\u53d6\u5931\u8d25
+gb.isNotValidFile = \u4e0d\u662f\u5408\u6cd5\u6587\u4ef6
+gb.failedToReadMessage = \u5728 {0} \u4e2d\u8bfb\u53d6\u9ed8\u8ba4\u6d88\u606f\u5931\u8d25!
+gb.passwordsDoNotMatch = \u5bc6\u7801\u4e0d\u5339\u914d!
+gb.passwordTooShort = \u5bc6\u7801\u957f\u5ea6\u592a\u77ed\u3002\u6700\u77ed\u957f\u5ea6 {0} \u4e2a\u5b57\u7b26\u3002
+gb.passwordChanged = \u5bc6\u7801\u4fee\u6539\u6210\u529f\u3002
+gb.passwordChangeAborted = \u5bc6\u7801\u4fee\u6539\u7ec8\u6b62
+gb.pleaseSetRepositoryName = \u8bf7\u8bbe\u7f6e\u4e00\u4e2a\u7248\u672c\u5e93\u540d\u79f0!
+gb.illegalLeadingSlash = \u7981\u6b62\u4f7f\u7528\u6839\u76ee\u5f55\u5f15\u7528 (/) \u3002
+gb.illegalRelativeSlash = \u76f8\u5bf9\u6587\u4ef6\u5939\u8def\u5f84(../)\u7981\u6b62\u4f7f\u7528
+gb.illegalCharacterRepositoryName = \u7248\u672c\u5e93\u4e2d\u542b\u6709\u4e0d\u5408\u6cd5\u5b57\u7b26 ''{0}'' !
+gb.selectAccessRestriction = \u8bf7\u9009\u62e9\u8bbf\u95ee\u6743\u9650\uff01
+gb.selectFederationStrategy = \u8bf7\u9009\u62e9federation\u7b56\u7565!
+gb.pleaseSetTeamName = \u8bf7\u8f93\u5165\u4e00\u4e2a\u56e2\u961f\u540d\u79f0\uff01
+gb.teamNameUnavailable = \u56e2\u961f\u540d ''{0}'' \u4e0d\u5408\u6cd5.
+gb.teamMustSpecifyRepository = \u56e2\u961f\u5fc5\u987b\u62e5\u6709\u81f3\u5c11\u4e00\u4e2a\u7248\u672c\u5e93\u3002
+gb.teamCreated = \u6210\u529f\u521b\u5efa\u65b0\u56e2\u961f ''{0}'' .
+gb.pleaseSetUsername = \u8bf7\u8f93\u5165\u7528\u6237\u540d\uff01
+gb.usernameUnavailable = \u7528\u6237\u540d ''{0}'' \u4e0d\u53ef\u7528..
+gb.combinedMd5Rename = Gitblit\u91c7\u7528\u6df7\u5408md5\u5bc6\u7801\u54c8\u5e0c\u3002\u56e0\u6b64\u5fc5\u987b\u5728\u4fee\u6539\u7528\u6237\u540d\u540e\u4fee\u6539\u5bc6\u7801\u3002
+gb.userCreated = \u6210\u529f\u521b\u5efa\u65b0\u7528\u6237 \\"{0}\\"\u3002
+gb.couldNotFindFederationRegistration = \u65e0\u6cd5\u627e\u5230federation registration!
+gb.failedToFindGravatarProfile = \u52a0\u8f7d {0} \u7684Gravatar\u4fe1\u606f\u5931\u8d25
+gb.branchStats = {0} \u4e2a\u63d0\u4ea4\u548c {1} \u4e2a\u6807\u7b7e\u5728 {2} \u5185
+gb.repositoryNotSpecified = \u672a\u6307\u5b9a\u7248\u672c\u5e93!
+gb.repositoryNotSpecifiedFor = \u6ca1\u6709\u4e3a {0} \u8bbe\u7f6e\u7248\u672c\u5e93!
+gb.canNotLoadRepository = \u65e0\u6cd5\u8f7d\u5165\u7248\u672c\u5e93
+gb.commitIsNull = \u63d0\u4ea4\u5185\u5bb9\u4e3a\u7a7a
+gb.unauthorizedAccessForRepository = \u672a\u6388\u6743\u8bbf\u95ee\u7248\u672c\u5e93
+gb.failedToFindCommit = \u5728 {1} \u4e2d {2} \u4e2a\u9875\u9762\u5185\u67e5\u627e\u63d0\u4ea4 \\"{0}\\"\u5931\u8d25!
+gb.couldNotFindFederationProposal = \u65e0\u6cd5\u627e\u5230federation proposal!
+gb.invalidUsernameOrPassword = \u7528\u6237\u540d\u6216\u8005\u5bc6\u7801\u9519\u8bef\uff01
+gb.OneProposalToReview = 1\u4e2afederation proposals\u7b49\u5f85\u68c0\u67e5\u3002
+gb.nFederationProposalsToReview = {0} \u4e2afederation proposals\u7b49\u5f85\u68c0\u67e5
+gb.couldNotFindTag = \u65e0\u6cd5\u627e\u5230\u6807\u7b7e {0}
+gb.couldNotCreateFederationProposal = \u65e0\u6cd5\u521b\u5efafederation proposal!
+gb.pleaseSetGitblitUrl = \u8bf7\u8f93\u5165\u4f60\u7684Gitblit\u7f51\u5740!
+gb.pleaseSetDestinationUrl = \u8bf7\u4e3a\u4f60\u7684proposal\u8f93\u5165\u4e00\u4e2a\u76ee\u6807\u5730\u5740!
+gb.proposalReceived = \u6210\u529f\u4ece {0} \u63a5\u6536Proposal.
+gb.noGitblitFound = \u62b1\u6b49, {0} \u65e0\u6cd5\u5728{1} \u4e2d\u627e\u5230Gitblit\u5b9e\u4f8b\u3002
+gb.noProposals = \u62b1\u6b49, {0} \u5f53\u524d\u4e0d\u63a5\u53d7proposals\u3002
+gb.noFederation = \u62b1\u6b49, {0} \u6ca1\u6709\u4e0e\u4efb\u4f55Gitblit\u5b9e\u4f8b\u8bbe\u7f6efederate\u3002.
+gb.proposalFailed = \u62b1\u6b49, {0} \u65e0\u6cd5\u63a5\u53d7\u4efb\u4f55proposal\u6570\u636e!
+gb.proposalError = \u62b1\u6b49\uff0c{0} \u62a5\u544a\u4e2d\u53d1\u73b0\u672a\u9884\u671f\u7684\u9519\u8bef\uff01
+gb.failedToSendProposal = \u53d1\u9001proposal\u5931\u8d25!
+gb.userServiceDoesNotPermitAddUser = {0} \u4e0d\u5141\u8bb8\u6dfb\u52a0\u7528\u6237!
+gb.userServiceDoesNotPermitPasswordChanges = {0} \u4e0d\u5141\u8bb8\u8fdb\u884c\u5bc6\u7801\u4fee\u6539!
+gb.displayName = \u663e\u793a\u540d\u79f0
+gb.emailAddress = \u90ae\u7bb1
+gb.errorAdminLoginRequired = \u9700\u8981\u7ba1\u7406\u5458\u767b\u9646
+gb.errorOnlyAdminMayCreateRepository = \u53ea\u6709\u7ba1\u7406\u5458\u624d\u53ef\u4ee5\u521b\u5efa\u7248\u672c\u5e93
+gb.errorOnlyAdminOrOwnerMayEditRepository = \u53ea\u6709\u7ba1\u7406\u5458\u6216\u8005\u6240\u6709\u8005\u624d\u53ef\u4ee5\u7f16\u8f91\u4ee3\u7801\u5e93
+gb.errorAdministrationDisabled = \u7ba1\u7406\u6743\u9650\u88ab\u7981\u6b62\u3002
+gb.lastNDays = \u6700\u8fd1 {0} \u5929
+gb.completeGravatarProfile = \u5728Gravatar.com\u4e0a\u5b8c\u6210\u4e2a\u4eba\u8bbe\u5b9a
+gb.none = \u65e0
+gb.line = \u884c
+gb.content = \u5185\u5bb9
+gb.empty = \u7a7a\u767d\u7248\u672c\u5e93
+gb.inherited = \u7ee7\u627f
+gb.deleteRepository = \u5220\u9664\u7248\u672c\u5e93 \\"{0}\\" \uff1f
+gb.repositoryDeleted = \u7248\u672c\u5e93 ''{0}'' \u5df2\u5220\u9664\u3002
+gb.repositoryDeleteFailed = \u5220\u9664\u7248\u672c\u5e93 \\"{0}\\" \u5931\u8d25\uff01
+gb.deleteUser = \u5220\u9664\u7528\u6237 \\"{0}\\" \uff1f
+gb.userDeleted = \u7528\u6237 ''{0}'' \u5df2\u5220\u9664\uff01
+gb.userDeleteFailed = \u5220\u9664\u7528\u6237''{0}''\u5931\u8d25\uff01
+gb.time.justNow = \u521a\u521a
+gb.time.today = \u4eca\u5929
+gb.time.yesterday = \u6628\u5929
+gb.time.minsAgo = {0} \u5206\u949f\u4ee5\u524d
+gb.time.hoursAgo = {0} \u5c0f\u65f6\u4ee5\u524d
+gb.time.daysAgo = {0} \u5929\u4ee5\u524d
+gb.time.weeksAgo = {0} \u5468\u4ee5\u524d
+gb.time.monthsAgo = {0} \u4e2a\u6708\u4ee5\u524d
+gb.time.oneYearAgo = 1 \u5e74\u4ee5\u524d
+gb.time.yearsAgo = {0} \u5e74\u4ee5\u524d
+gb.duration.oneDay = 1 \u5929
+gb.duration.days = {0} \u5929
+gb.duration.oneMonth = 1 \u6708
+gb.duration.months = {0} \u6708
+gb.duration.oneYear = 1 \u5e74
+gb.duration.years = {0} \u5e74
+gb.authorizationControl = \u6388\u6743\u63a7\u5236
+gb.allowAuthenticatedDescription = \u6388\u4e88\u6240\u6709\u8ba4\u8bc1\u7528\u6237\u53d7\u9650\u5236\u7684\u8bbf\u95ee\u6743\u9650
+gb.allowNamedDescription = \u6388\u4e88\u6307\u5b9a\u540d\u79f0\u7684\u7528\u6237\u6216\u56e2\u961f\u53d7\u9650\u5236\u7684\u8bbf\u95ee\u6743\u9650
+gb.markdownFailure = \u8bfb\u53d6 Markdown \u5185\u5bb9\u5931\u8d25\uff01
+gb.clearCache = \u6e05\u9664\u7f13\u5b58
+gb.projects = \u9879\u76ee
+gb.project = \u9879\u76ee
+gb.allProjects = \u6240\u6709\u9879\u76ee
+gb.copyToClipboard = \u590d\u5236\u5230\u526a\u8d34\u677f
+gb.fork = \u6d3e\u751f
+gb.forks = \u6d3e\u751f
+gb.forkRepository = \u6d3e\u751f {0} ?
+gb.repositoryForked = {0} \u5df2\u88ab\u6d3e\u751f
+gb.repositoryForkFailed = \u6d3e\u751f\u5931\u8d25
+gb.personalRepositories = \u79c1\u4eba\u7248\u672c\u5e93
+gb.allowForks = \u5141\u8bb8\u6d3e\u751f
+gb.allowForksDescription = \u5141\u8bb8\u8ba4\u8bc1\u7528\u6237\u6d3e\u751f\u6b64\u7248\u672c\u5e93
+gb.forkedFrom = \u6d3e\u751f\u81ea
+gb.canFork = \u5141\u8bb8\u6d3e\u751f
+gb.canForkDescription = \u5141\u8bb8\u6d3e\u751f\u8ba4\u8bc1\u7248\u672c\u5e93\u5230\u79c1\u4eba\u7248\u672c\u5e93
+gb.myFork = \u67e5\u770b\u6211\u7684\u6d3e\u751f
+gb.forksProhibited = \u7981\u6b62\u6d3e\u751f
+gb.forksProhibitedWarning = \u5f53\u524d\u7248\u672c\u5e93\u7981\u6b62\u6d3e\u751f
+gb.noForks = {0} \u6ca1\u6709\u6d3e\u751f
+gb.forkNotAuthorized = \u62b1\u6b49\uff0c\u4f60\u65e0\u6743\u6d3e\u751f {0}
+gb.forkInProgress = \u6b63\u5728\u6d3e\u751f
+gb.preparingFork = \u6b63\u5728\u4e3a\u60a8\u51c6\u5907\u6d3e\u751f...
+gb.isFork = \u5df2\u6d3e\u751f
+gb.canCreate = \u5141\u8bb8\u521b\u5efa
+gb.canCreateDescription = \u5141\u8bb8\u521b\u5efa\u79c1\u4eba\u7248\u672c\u5e93
+gb.illegalPersonalRepositoryLocation = \u60a8\u7684\u79c1\u4eba\u7248\u672c\u5e93\u5fc5\u987b\u4f4d\u4e8e \\"{0}\\"
+gb.verifyCommitter = \u9a8c\u8bc1\u63d0\u4ea4\u8005
+gb.verifyCommitterDescription = \u9700\u8981\u63d0\u4ea4\u8005\u7684\u8eab\u4efd\u4e0e Gitblit \u7528\u6237\u8eab\u4efd\u76f8\u7b26
+gb.verifyCommitterNote = \u6240\u6709\u5408\u5e76\u9009\u9879\u9700\u8981\u4f7f\u7528 \\"--no-ff\\" \u6765\u6267\u884c\u63d0\u4ea4\u8005\u9a8c\u8bc1
+gb.repositoryPermissions = \u7248\u672c\u5e93\u6743\u9650
+gb.userPermissions = \u7528\u6237\u6743\u9650
+gb.teamPermissions = \u56e2\u961f\u6743\u9650
+gb.add = \u6dfb\u52a0
+gb.noPermission = \u5220\u9664\u6b64\u6743\u9650
+gb.excludePermission = {0} (exclude)
+gb.viewPermission = {0} (view)
+gb.clonePermission = {0} (clone)
+gb.pushPermission = {0} (push)
+gb.createPermission = {0} (push, ref creation)
+gb.deletePermission = {0} (push, ref creation+deletion)
+gb.rewindPermission = {0} (push, ref creation+deletion+rewind)
+gb.permission = \u6743\u9650
+gb.regexPermission = \u6b64\u6743\u9650\u662f\u901a\u8fc7\u6b63\u5219\u8868\u8fbe\u5f0f \\"{0}\\" \u8bbe\u7f6e
+gb.accessDenied = \u8bbf\u95ee\u88ab\u62d2\u7edd
+gb.busyCollectingGarbage = \u62b1\u6b49\uff0cGitblit\u6b63\u5728 {0} \u5185\u6e05\u7406\u5783\u573e
+gb.gcPeriod = GC \u65f6\u95f4
+gb.gcPeriodDescription = \u5783\u573e\u6e05\u7406\u7684\u6301\u7eed\u65f6\u95f4
+gb.gcThreshold = GC \u9600\u503c
+gb.gcThresholdDescription = \u6fc0\u53d1\u5783\u573e\u6e05\u7406\u7684\u6700\u5c0f objects \u5927\u5c0f
+gb.ownerPermission = \u7248\u672c\u5e93\u521b\u5efa\u8005
+gb.administrator = \u7ba1\u7406\u5458
+gb.administratorPermission = Gitblit \u7ba1\u7406\u5458
+gb.team = \u56e2\u961f
+gb.teamPermission = \u901a\u8fc7 \\"{0}\\" \u56e2\u961f\u6210\u5458\u8bbe\u7f6e\u6743\u9650
+gb.missing = \u4e0d\u5b58\u5728!
+gb.missingPermission = \u6b64\u6743\u9650\u7684\u7248\u672c\u5e93\u4e0d\u5b58\u5728!
+gb.mutable = mutable
+gb.specified = specified
+gb.effective = effective
+gb.organizationalUnit = \u7ec4\u7ec7\u90e8\u5206
+gb.organization = \u7ec4\u7ec7
+gb.locality = \u5730\u533a
+gb.stateProvince = \u5dde\u6216\u7701
+gb.countryCode = \u56fd\u5bb6\u4ee3\u7801
+gb.properties = \u5c5e\u6027
+gb.issued = issued
+gb.expires = \u5230\u671f
+gb.expired = \u5df2\u5230\u671f
+gb.expiring = \u5373\u5c06\u8fc7\u671f
+gb.revoked = \u5df2\u64a4\u9500
+gb.serialNumber = \u5e8f\u5217\u53f7
+gb.certificates = \u8bc1\u4e66
+gb.newCertificate = \u521b\u5efa\u8bc1\u4e66
+gb.revokeCertificate = \u64a4\u9500\u8bc1\u4e66
+gb.sendEmail = \u53d1\u9001\u90ae\u4ef6
+gb.passwordHint = \u5bc6\u7801\u63d0\u793a
+gb.ok = \u786e\u5b9a
+gb.invalidExpirationDate = \u65e0\u6548\u7684\u8fc7\u671f\u65f6\u95f4!
+gb.passwordHintRequired = \u9700\u8981\u586b\u5199\u5bc6\u7801\u63d0\u793a!
+gb.viewCertificate = \u67e5\u770b\u8bc1\u4e66
+gb.subject = \u4e3b\u9898
+gb.issuer = \u63d0\u4ea4\u8005
+gb.validFrom = \u6709\u6548\u671f\u5f00\u59cb\u81ea
+gb.validUntil = \u6709\u6548\u671f\u622a\u6b62\u4e8e
+gb.publicKey = \u516c\u94a5
+gb.signatureAlgorithm = \u7b7e\u540d\u7b97\u6cd5
+gb.sha1FingerPrint = SHA-1 \u6307\u7eb9\u7b97\u6cd5
+gb.md5FingerPrint = MD5 \u6307\u7eb9\u7b97\u6cd5
+gb.reason = \u7406\u7531
+gb.revokeCertificateReason = \u8bf7\u9009\u62e9\u64a4\u9500\u8bc1\u4e66\u7684\u7406\u7531
+gb.unspecified = \u672a\u6307\u5b9a
+gb.keyCompromise = key compromise
+gb.caCompromise = CA compromise
+gb.affiliationChanged = \u96b6\u5c5e\u5173\u7cfb\u5df2\u4fee\u6539
+gb.superseded = \u5df2\u53d6\u4ee3
+gb.cessationOfOperation = \u505c\u6b62\u64cd\u4f5c
+gb.privilegeWithdrawn = \u7279\u6743\u5df2\u64a4\u56de
+gb.time.inMinutes = {0} \u5206\u949f\u4e4b\u5185
+gb.time.inHours = {0} \u5c0f\u65f6\u4e4b\u5185
+gb.time.inDays = {0} \u5929\u4e4b\u5185
+gb.hostname = hostname
+gb.hostnameRequired = \u8bf7\u8f93\u5165 hostname
+gb.newSSLCertificate = \u521b\u5efa\u670d\u52a1\u5668 SSL \u8bc1\u4e66
+gb.newCertificateDefaults = \u521b\u5efa\u8bc1\u4e66\u9ed8\u8ba4\u8bbe\u7f6e
+gb.duration = \u6301\u7eed\u65f6\u95f4
+gb.certificateRevoked = \u8bc1\u4e66 {0,number,0} \u5df2\u88ab\u64a4\u9500
+gb.clientCertificateGenerated = \u6210\u529f\u4e3a {0} \u751f\u6210\u65b0\u7684\u5ba2\u6237\u7aef\u8bc1\u4e66
+gb.sslCertificateGenerated = \u6210\u529f\u4e3a {0} \u751f\u6210\u65b0\u7684\u670d\u52a1\u5668 SSL \u8bc1\u4e66
+gb.newClientCertificateMessage = \u6ce8\u610f:\\n\u6b64\u5bc6\u7801\u5e76\u975e\u7528\u6237\u5bc6\u7801, \u8fd9\u662f\u4fdd\u5b58\u7528\u6237 keystore \u7684\u5bc6\u7801\u3002 \u7531\u4e8e\u672c\u5bc6\u7801\u672a\u5b58\u50a8\uff0c\u56e0\u6b64\u4f60\u5fc5\u987b\u4e00\u4e2a\u5bc6\u7801\u63d0\u793a\uff0c\u8fd9\u4e2a\u63d0\u793a\u4f1a\u8bb0\u5f55\u5728\u7528\u6237\u7684 README \u6587\u6863\u5185\u3002
+gb.certificate = \u8bc1\u4e66
+gb.emailCertificateBundle = \u53d1\u9001\u5ba2\u6237\u7aef\u8bc1\u4e66
+gb.pleaseGenerateClientCertificate = \u8bf7\u4e3a {0} \u751f\u6210\u4e00\u4e2a\u5ba2\u6237\u7aef\u8bc1\u4e66
+gb.clientCertificateBundleSent = {0} \u7684\u5ba2\u6237\u7aef\u8bc1\u4e66\u5df2\u53d1\u9001
+gb.enterKeystorePassword = \u8bf7\u8f93\u5165 Gitblit keystore \u5bc6\u7801
+gb.warning = \u8b66\u544a
+gb.jceWarning = \u60a8\u7684 JAVA \u8fd0\u884c\u73af\u5883\u4e0d\u5305\u542b \\"JCE Unlimited Strength Jurisdiction Policy\\" \u6587\u4ef6\u3002\\n\u8fd9\u5c06\u5bfc\u81f4\u60a8\u6700\u591a\u53ea\u80fd\u75287\u4e2a\u5b57\u7b26\u7684\u5bc6\u7801\u4fdd\u62a4\u60a8\u7684 keystore\u3002 \\n\u8fd9\u4e9b\u662f\u4e00\u4e9b\u53ef\u9009\u4e0b\u8f7d\u7684\u653f\u7b56\u6587\u4ef6\u3002\\n\\n\u4f60\u662f\u5426\u8981\u7ee7\u7eed\u751f\u6210\u8bc1\u4e66\uff1f\\n\\n\u9009\u62e9\u5426\u7684\u8bdd\uff0c\u5c06\u4f1a\u6253\u5f00\u4e00\u4e2a\u6d4f\u89c8\u5668\u754c\u9762\u4f9b\u60a8\u4e0b\u8f7d\u76f8\u5173\u6587\u4ef6\u3002
+gb.maxActivityCommits = \u6700\u5927\u6d3b\u52a8\u63d0\u4ea4\u6570
+gb.maxActivityCommitsDescription = \u6d3b\u52a8\u9875\u9762\u663e\u793a\u7684\u6700\u5927\u63d0\u4ea4\u6570
+gb.noMaximum = \u65e0\u4e0a\u9650
+gb.attributes = \u5c5e\u6027
+gb.serveCertificate = \u4f7f\u7528\u6b64\u8bc1\u4e66\u63d0\u4f9b https \u652f\u6301
+gb.sslCertificateGeneratedRestart = \u6210\u529f\u4e3a {0} \u751f\u6210\u65b0\u7684 SSL \u8bc1\u4e66.\\n\u4f60\u5fc5\u987b\u91cd\u65b0\u542f\u52a8 Gitblit \u4ee5\u4f7f\u7528\u6b64\u8bc1\u4e66\u3002\\n\\n\u5982\u679c\u60a8\u4f7f\u7528 '--alias' \u53c2\u6570\u542f\u52a8\uff0c\u4f60\u5fc5\u987b\u4e5f\u8981\u8bbe\u7f6e ''--alias {0}''\u3002
+gb.validity = \u5408\u6cd5\u6027
+gb.siteName = \u7f51\u7ad9\u540d\u79f0
+gb.siteNameDescription = \u60a8\u7684\u670d\u52a1\u5668\u7684\u7b80\u8981\u63cf\u8ff0
+gb.excludeFromActivity = \u4ece\u6d3b\u52a8\u9875\u9762\u6392\u9664
+gb.isSparkleshared = repository is Sparkleshared
+gb.sessionEnded = Session has been closed
+gb.closeBrowser = Please close the browser to properly end the session. \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/GitBlitWebSession.java b/src/main/java/com/gitblit/wicket/GitBlitWebSession.java
new file mode 100644
index 00000000..5195a1fd
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/GitBlitWebSession.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket;
+
+import java.util.Map;
+import java.util.TimeZone;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.apache.wicket.Page;
+import org.apache.wicket.PageParameters;
+import org.apache.wicket.RedirectToUrlException;
+import org.apache.wicket.Request;
+import org.apache.wicket.Session;
+import org.apache.wicket.protocol.http.RequestUtils;
+import org.apache.wicket.protocol.http.WebRequestCycle;
+import org.apache.wicket.protocol.http.WebSession;
+import org.apache.wicket.protocol.http.request.WebClientInfo;
+
+import com.gitblit.Constants.AuthenticationType;
+import com.gitblit.models.UserModel;
+
+public final class GitBlitWebSession extends WebSession {
+
+ private static final long serialVersionUID = 1L;
+
+ protected TimeZone timezone;
+
+ private UserModel user;
+
+ private String errorMessage;
+
+ private String requestUrl;
+
+ private AtomicBoolean isForking;
+
+ public AuthenticationType authenticationType;
+
+ public GitBlitWebSession(Request request) {
+ super(request);
+ isForking = new AtomicBoolean();
+ authenticationType = AuthenticationType.CREDENTIALS;
+ }
+
+ public void invalidate() {
+ super.invalidate();
+ user = null;
+ }
+
+ /**
+ * Cache the requested protected resource pending successful authentication.
+ *
+ * @param pageClass
+ */
+ public void cacheRequest(Class<? extends Page> pageClass) {
+ // build absolute url with correctly encoded parameters?!
+ Request req = WebRequestCycle.get().getRequest();
+ Map<String, ?> params = req.getRequestParameters().getParameters();
+ PageParameters pageParams = new PageParameters(params);
+ String relativeUrl = WebRequestCycle.get().urlFor(pageClass, pageParams).toString();
+ requestUrl = RequestUtils.toAbsolutePath(relativeUrl);
+ if (isTemporary())
+ {
+ // we must bind the temporary session into the session store
+ // so that we can re-use this session for reporting an error message
+ // on the redirected page and continuing the request after
+ // authentication.
+ bind();
+ }
+ }
+
+ /**
+ * Continue any cached request. This is used when a request for a protected
+ * resource is aborted/redirected pending proper authentication. Gitblit
+ * no longer uses Wicket's built-in mechanism for this because of Wicket's
+ * failure to properly handle parameters with forward-slashes. This is a
+ * constant source of headaches with Wicket.
+ *
+ * @return false if there is no cached request to process
+ */
+ public boolean continueRequest() {
+ if (requestUrl != null) {
+ String url = requestUrl;
+ requestUrl = null;
+ throw new RedirectToUrlException(url);
+ }
+ return false;
+ }
+
+ public boolean isLoggedIn() {
+ return user != null;
+ }
+
+ public boolean canAdmin() {
+ if (user == null) {
+ return false;
+ }
+ return user.canAdmin();
+ }
+
+ public String getUsername() {
+ return user == null ? "anonymous" : user.username;
+ }
+
+ public UserModel getUser() {
+ return user;
+ }
+
+ public void setUser(UserModel user) {
+ this.user = user;
+ }
+
+ public TimeZone getTimezone() {
+ if (timezone == null) {
+ timezone = ((WebClientInfo) getClientInfo()).getProperties().getTimeZone();
+ }
+ // use server timezone if we can't determine the client timezone
+ if (timezone == null) {
+ timezone = TimeZone.getDefault();
+ }
+ return timezone;
+ }
+
+ public void cacheErrorMessage(String message) {
+ this.errorMessage = message;
+ }
+
+ public String clearErrorMessage() {
+ String msg = errorMessage;
+ errorMessage = null;
+ return msg;
+ }
+
+ public boolean isForking() {
+ return isForking.get();
+ }
+
+ public void isForking(boolean val) {
+ isForking.set(val);
+ }
+
+ public static GitBlitWebSession get() {
+ return (GitBlitWebSession) Session.get();
+ }
+} \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/GitblitParamUrlCodingStrategy.java b/src/main/java/com/gitblit/wicket/GitblitParamUrlCodingStrategy.java
new file mode 100644
index 00000000..fb86fb0e
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/GitblitParamUrlCodingStrategy.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket;
+
+import java.text.MessageFormat;
+
+import org.apache.wicket.IRequestTarget;
+import org.apache.wicket.Page;
+import org.apache.wicket.PageParameters;
+import org.apache.wicket.request.RequestParameters;
+import org.apache.wicket.request.target.coding.MixedParamUrlCodingStrategy;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.GitBlit;
+import com.gitblit.Keys;
+
+/**
+ * Simple subclass of mixed parameter url coding strategy that works around the
+ * encoded forward-slash issue that is present in some servlet containers.
+ *
+ * https://issues.apache.org/jira/browse/WICKET-1303
+ * http://tomcat.apache.org/security-6.html
+ *
+ * @author James Moger
+ *
+ */
+public class GitblitParamUrlCodingStrategy extends MixedParamUrlCodingStrategy {
+
+ private Logger logger = LoggerFactory.getLogger(GitblitParamUrlCodingStrategy.class);
+
+ /**
+ * Construct.
+ *
+ * @param <C>
+ * @param mountPath
+ * mount path (not empty)
+ * @param bookmarkablePageClass
+ * class of mounted page (not null)
+ * @param parameterNames
+ * the parameter names (not null)
+ */
+ public <C extends Page> GitblitParamUrlCodingStrategy(String mountPath,
+ Class<C> bookmarkablePageClass, String[] parameterNames) {
+ super(mountPath, bookmarkablePageClass, parameterNames);
+ }
+
+ /**
+ * Url encodes a string that is mean for a URL path (e.g., between slashes)
+ *
+ * @param string
+ * string to be encoded
+ * @return encoded string
+ */
+ protected String urlEncodePathComponent(String string) {
+ char altChar = GitBlit.getChar(Keys.web.forwardSlashCharacter, '/');
+ if (altChar != '/') {
+ string = string.replace('/', altChar);
+ }
+ return super.urlEncodePathComponent(string);
+ }
+
+ /**
+ * Returns a decoded value of the given value (taken from a URL path
+ * section)
+ *
+ * @param value
+ * @return Decodes the value
+ */
+ protected String urlDecodePathComponent(String value) {
+ char altChar = GitBlit.getChar(Keys.web.forwardSlashCharacter, '/');
+ if (altChar != '/') {
+ value = value.replace(altChar, '/');
+ }
+ return super.urlDecodePathComponent(value);
+ }
+
+ /**
+ * Gets the decoded request target.
+ *
+ * @param requestParameters
+ * the request parameters
+ * @return the decoded request target
+ */
+ @Override
+ public IRequestTarget decode(RequestParameters requestParameters) {
+ final String parametersFragment = requestParameters.getPath().substring(
+ getMountPath().length());
+ logger.debug(MessageFormat
+ .format("REQ: {0} PARAMS {1}", getMountPath(), parametersFragment));
+
+ final PageParameters parameters = new PageParameters(decodeParameters(parametersFragment,
+ requestParameters.getParameters()));
+ return super.decode(requestParameters);
+ }
+} \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/GitblitRedirectException.java b/src/main/java/com/gitblit/wicket/GitblitRedirectException.java
new file mode 100644
index 00000000..c3df1ac1
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/GitblitRedirectException.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2012 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket;
+
+import org.apache.wicket.AbstractRestartResponseException;
+import org.apache.wicket.Page;
+import org.apache.wicket.PageParameters;
+import org.apache.wicket.RequestCycle;
+import org.apache.wicket.protocol.http.RequestUtils;
+import org.apache.wicket.request.target.basic.RedirectRequestTarget;
+
+/**
+ * This exception bypasses the servlet container rewriting relative redirect
+ * urls. The container can and does decode the carefully crafted %2F path
+ * separators on a redirect. :( Bad, bad servlet container.
+ *
+ * org.eclipse.jetty.server.Response#L447: String path=uri.getDecodedPath();
+ *
+ * @author James Moger
+ */
+public class GitblitRedirectException extends AbstractRestartResponseException {
+
+ private static final long serialVersionUID = 1L;
+
+ public <C extends Page> GitblitRedirectException(Class<C> pageClass) {
+ this(pageClass, null);
+ }
+
+ public <C extends Page> GitblitRedirectException(Class<C> pageClass, PageParameters params) {
+ RequestCycle cycle = RequestCycle.get();
+ String relativeUrl = cycle.urlFor(pageClass, params).toString();
+ String absoluteUrl = RequestUtils.toAbsolutePath(relativeUrl);
+ cycle.setRequestTarget(new RedirectRequestTarget(absoluteUrl));
+ cycle.setRedirect(true);
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/PageRegistration.java b/src/main/java/com/gitblit/wicket/PageRegistration.java
new file mode 100644
index 00000000..e8eeabae
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/PageRegistration.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.wicket.PageParameters;
+import org.apache.wicket.markup.html.WebPage;
+
+import com.gitblit.utils.StringUtils;
+
+/**
+ * Represents a page link registration for the topbar.
+ *
+ * @author James Moger
+ *
+ */
+public class PageRegistration implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ public final String translationKey;
+ public final Class<? extends WebPage> pageClass;
+ public final PageParameters params;
+
+ public PageRegistration(String translationKey, Class<? extends WebPage> pageClass) {
+ this(translationKey, pageClass, null);
+ }
+
+ public PageRegistration(String translationKey, Class<? extends WebPage> pageClass,
+ PageParameters params) {
+ this.translationKey = translationKey;
+ this.pageClass = pageClass;
+ this.params = params;
+ }
+
+ /**
+ * Represents a page link to a non-Wicket page. Might be external.
+ *
+ * @author James Moger
+ *
+ */
+ public static class OtherPageLink extends PageRegistration {
+
+ private static final long serialVersionUID = 1L;
+
+ public final String url;
+
+ public OtherPageLink(String translationKey, String url) {
+ super(translationKey, null);
+ this.url = url;
+ }
+ }
+
+ /**
+ * Represents a DropDownMenu for the topbar
+ *
+ * @author James Moger
+ *
+ */
+ public static class DropDownMenuRegistration extends PageRegistration {
+
+ private static final long serialVersionUID = 1L;
+
+ public final List<DropDownMenuItem> menuItems;
+
+ public DropDownMenuRegistration(String translationKey, Class<? extends WebPage> pageClass) {
+ super(translationKey, pageClass);
+ menuItems = new ArrayList<DropDownMenuItem>();
+ }
+ }
+
+ /**
+ * A MenuItem for the DropDownMenu.
+ *
+ * @author James Moger
+ *
+ */
+ public static class DropDownMenuItem implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ final PageParameters parameters;
+ final String displayText;
+ final String parameter;
+ final String value;
+ final boolean isSelected;
+
+ /**
+ * Divider constructor.
+ */
+ public DropDownMenuItem() {
+ this(null, null, null, null);
+ }
+
+ /**
+ * Standard Menu Item constructor.
+ *
+ * @param displayText
+ * @param parameter
+ * @param value
+ */
+ public DropDownMenuItem(String displayText, String parameter, String value) {
+ this(displayText, parameter, value, null);
+ }
+
+ /**
+ * Standard Menu Item constructor that preserves aggregate parameters.
+ *
+ * @param displayText
+ * @param parameter
+ * @param value
+ */
+ public DropDownMenuItem(String displayText, String parameter, String value,
+ PageParameters params) {
+ this.displayText = displayText;
+ this.parameter = parameter;
+ this.value = value;
+
+ if (params == null) {
+ // no parameters specified
+ parameters = new PageParameters();
+ setParameter(parameter, value);
+ isSelected = false;
+ } else {
+ parameters = new PageParameters(params);
+ if (parameters.containsKey(parameter)) {
+ isSelected = params.getString(parameter).equals(value);
+ if (isSelected) {
+ // already selected, so remove this enables toggling
+ parameters.remove(parameter);
+ } else {
+ // set the new selection value
+ setParameter(parameter, value);
+ }
+ } else {
+ // not currently selected
+ isSelected = false;
+ setParameter(parameter, value);
+ }
+ }
+ }
+
+ private void setParameter(String parameter, String value) {
+ if (!StringUtils.isEmpty(parameter)) {
+ if (StringUtils.isEmpty(value)) {
+ this.parameters.remove(parameter);
+ } else {
+ this.parameters.put(parameter, value);
+ }
+ }
+ }
+
+ public String formatParameter() {
+ if (StringUtils.isEmpty(parameter) || StringUtils.isEmpty(value)) {
+ return "";
+ }
+ return parameter + "=" + value;
+ }
+
+ public PageParameters getPageParameters() {
+ return parameters;
+ }
+
+ public boolean isDivider() {
+ return displayText == null && value == null && parameter == null;
+ }
+
+ public boolean isSelected() {
+ return isSelected;
+ }
+
+ @Override
+ public int hashCode() {
+ if (isDivider()) {
+ // divider menu item
+ return super.hashCode();
+ }
+ if (StringUtils.isEmpty(displayText)) {
+ return value.hashCode() + parameter.hashCode();
+ }
+ return displayText.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof DropDownMenuItem) {
+ return hashCode() == o.hashCode();
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ if (StringUtils.isEmpty(displayText)) {
+ return formatParameter();
+ }
+ return displayText;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/RequiresAdminRole.java b/src/main/java/com/gitblit/wicket/RequiresAdminRole.java
new file mode 100644
index 00000000..ce2dcfcf
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/RequiresAdminRole.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface RequiresAdminRole {
+}
diff --git a/src/main/java/com/gitblit/wicket/SessionlessForm.java b/src/main/java/com/gitblit/wicket/SessionlessForm.java
new file mode 100644
index 00000000..484e85e3
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/SessionlessForm.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright 2012 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket;
+
+import java.text.MessageFormat;
+
+import org.apache.wicket.Component;
+import org.apache.wicket.PageParameters;
+import org.apache.wicket.markup.ComponentTag;
+import org.apache.wicket.markup.MarkupStream;
+import org.apache.wicket.markup.html.form.StatelessForm;
+import org.apache.wicket.protocol.http.WicketURLDecoder;
+import org.apache.wicket.protocol.http.request.WebRequestCodingStrategy;
+import org.apache.wicket.util.string.AppendingStringBuffer;
+import org.apache.wicket.util.string.Strings;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.wicket.pages.BasePage;
+
+/**
+ * This class is used to create a stateless form that can POST or GET to a
+ * bookmarkable page regardless of the pagemap and even after session expiration
+ * or a server restart.
+ *
+ * The trick is to embed "wicket:bookmarkablePage" as a hidden field of the form.
+ * Wicket already has logic to extract this parameter when it is trying
+ * to determine which page should receive the request.
+ *
+ * The parameters of the containing page can optionally be included as hidden
+ * fields in this form. Note that if a page parameter's name collides with any
+ * child's wicket:id in this form then the page parameter is excluded.
+ *
+ * @author James Moger
+ *
+ */
+public class SessionlessForm<T> extends StatelessForm<T> {
+
+ private static final long serialVersionUID = 1L;
+
+ private static final String HIDDEN_DIV_START = "<div style=\"width:0px;height:0px;position:absolute;left:-100px;top:-100px;overflow:hidden\">";
+
+ private final Class<? extends BasePage> pageClass;
+
+ private final PageParameters pageParameters;
+
+ private final Logger log = LoggerFactory.getLogger(SessionlessForm.class);
+
+ /**
+ * Sessionless forms must have a bookmarkable page class. A bookmarkable
+ * page is defined as a page that has only a default and/or a PageParameter
+ * constructor.
+ *
+ * @param id
+ * @param bookmarkablePageClass
+ */
+ public SessionlessForm(String id, Class<? extends BasePage> bookmarkablePageClass) {
+ this(id, bookmarkablePageClass, null);
+ }
+
+ /**
+ * Sessionless forms must have a bookmarkable page class. A bookmarkable
+ * page is defined as a page that has only a default and/or a PageParameter
+ * constructor.
+ *
+ * @param id
+ * @param bookmarkablePageClass
+ * @param pageParameters
+ */
+ public SessionlessForm(String id, Class<? extends BasePage> bookmarkablePageClass,
+ PageParameters pageParameters) {
+ super(id);
+ this.pageClass = bookmarkablePageClass;
+ this.pageParameters = pageParameters;
+ }
+
+
+ /**
+ * Append an additional hidden input tag that forces Wicket to correctly
+ * determine the destination page class even after a session expiration or
+ * a server restart.
+ *
+ * @param markupStream
+ * The markup stream
+ * @param openTag
+ * The open tag for the body
+ */
+ @Override
+ protected void onComponentTagBody(final MarkupStream markupStream, final ComponentTag openTag)
+ {
+ // render the hidden bookmarkable page field
+ AppendingStringBuffer buffer = new AppendingStringBuffer(HIDDEN_DIV_START);
+ buffer.append("<input type=\"hidden\" name=\"")
+ .append(WebRequestCodingStrategy.BOOKMARKABLE_PAGE_PARAMETER_NAME)
+ .append("\" value=\":")
+ .append(pageClass.getName())
+ .append("\" />");
+
+ // insert the page parameters, if any, as hidden fields as long as they
+ // do not collide with any child wicket:id of the form.
+ if (pageParameters != null) {
+ for (String key : pageParameters.keySet()) {
+ Component c = get(key);
+ if (c != null) {
+ // this form has a field id which matches the
+ // parameter name, skip embedding a hidden value
+ log.warn(MessageFormat.format("Skipping page parameter \"{0}\" from sessionless form hidden fields because it collides with a form child wicket:id", key));
+ continue;
+ }
+ String value = pageParameters.getString(key);
+ buffer.append("<input type=\"hidden\" name=\"")
+ .append(recode(key))
+ .append("\" value=\"")
+ .append(recode(value))
+ .append("\" />");
+ }
+ }
+
+ buffer.append("</div>");
+ getResponse().write(buffer);
+ super.onComponentTagBody(markupStream, openTag);
+ }
+
+ /**
+ * Take URL-encoded query string value, unencode it and return HTML-escaped version
+ *
+ * @param s
+ * value to reencode
+ * @return reencoded value
+ */
+ private String recode(String s) {
+ String un = WicketURLDecoder.QUERY_INSTANCE.decode(s);
+ return Strings.escapeMarkup(un).toString();
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/StringChoiceRenderer.java b/src/main/java/com/gitblit/wicket/StringChoiceRenderer.java
new file mode 100644
index 00000000..58ed4798
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/StringChoiceRenderer.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2012 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket;
+
+import org.apache.wicket.markup.html.form.ChoiceRenderer;
+
+/**
+ * Choice renderer for a palette or list of string values. This renderer
+ * controls the id value of each option such that palettes are case insensitive.
+ *
+ * @author James Moger
+ *
+ */
+public class StringChoiceRenderer extends ChoiceRenderer<String> {
+
+ private static final long serialVersionUID = 1L;
+
+ public StringChoiceRenderer() {
+ super("", "");
+ }
+
+ /**
+ * @see org.apache.wicket.markup.html.form.IChoiceRenderer#getIdValue(java.lang.Object, int)
+ */
+ @Override
+ public String getIdValue(String object, int index)
+ {
+ return object.toLowerCase();
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/WicketUtils.java b/src/main/java/com/gitblit/wicket/WicketUtils.java
new file mode 100644
index 00000000..e4eb29fb
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/WicketUtils.java
@@ -0,0 +1,601 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket;
+
+import java.text.DateFormat;
+import java.text.MessageFormat;
+import java.text.SimpleDateFormat;
+import java.util.Collection;
+import java.util.Date;
+import java.util.List;
+import java.util.TimeZone;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.wicket.Component;
+import org.apache.wicket.PageParameters;
+import org.apache.wicket.Request;
+import org.apache.wicket.behavior.HeaderContributor;
+import org.apache.wicket.behavior.SimpleAttributeModifier;
+import org.apache.wicket.markup.html.IHeaderContributor;
+import org.apache.wicket.markup.html.IHeaderResponse;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.image.ContextImage;
+import org.apache.wicket.protocol.http.WebRequest;
+import org.apache.wicket.resource.ContextRelativeResource;
+import org.eclipse.jgit.diff.DiffEntry.ChangeType;
+import org.wicketstuff.googlecharts.AbstractChartData;
+import org.wicketstuff.googlecharts.IChartData;
+
+import com.gitblit.Constants;
+import com.gitblit.Constants.FederationPullStatus;
+import com.gitblit.GitBlit;
+import com.gitblit.Keys;
+import com.gitblit.models.FederationModel;
+import com.gitblit.models.Metric;
+import com.gitblit.utils.HttpUtils;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.utils.TimeUtils;
+
+public class WicketUtils {
+
+ public static void setCssClass(Component container, String value) {
+ container.add(new SimpleAttributeModifier("class", value));
+ }
+
+ public static void setCssStyle(Component container, String value) {
+ container.add(new SimpleAttributeModifier("style", value));
+ }
+
+ public static void setCssBackground(Component container, String value) {
+ String background = MessageFormat.format("background-color:{0};",
+ StringUtils.getColor(value));
+ container.add(new SimpleAttributeModifier("style", background));
+ }
+
+ public static void setHtmlTooltip(Component container, String value) {
+ container.add(new SimpleAttributeModifier("title", value));
+ }
+
+ public static void setInputPlaceholder(Component container, String value) {
+ container.add(new SimpleAttributeModifier("placeholder", value));
+ }
+
+ public static void setChangeTypeCssClass(Component container, ChangeType type) {
+ switch (type) {
+ case ADD:
+ setCssClass(container, "addition");
+ break;
+ case COPY:
+ case RENAME:
+ setCssClass(container, "rename");
+ break;
+ case DELETE:
+ setCssClass(container, "deletion");
+ break;
+ case MODIFY:
+ setCssClass(container, "modification");
+ break;
+ }
+ }
+
+ public static void setTicketCssClass(Component container, String state) {
+ String css = null;
+ if (state.equals("open")) {
+ css = "label label-important";
+ } else if (state.equals("hold")) {
+ css = "label label-warning";
+ } else if (state.equals("resolved")) {
+ css = "label label-success";
+ } else if (state.equals("invalid")) {
+ css = "label";
+ }
+ if (css != null) {
+ setCssClass(container, css);
+ }
+ }
+
+ public static void setAlternatingBackground(Component c, int i) {
+ String clazz = i % 2 == 0 ? "light" : "dark";
+ setCssClass(c, clazz);
+ }
+
+ public static Label createAuthorLabel(String wicketId, String author) {
+ Label label = new Label(wicketId, author);
+ WicketUtils.setHtmlTooltip(label, author);
+ return label;
+ }
+
+ public static ContextImage getPullStatusImage(String wicketId, FederationPullStatus status) {
+ String filename = null;
+ switch (status) {
+ case MIRRORED:
+ case PULLED:
+ filename = "bullet_green.png";
+ break;
+ case SKIPPED:
+ filename = "bullet_yellow.png";
+ break;
+ case FAILED:
+ filename = "bullet_red.png";
+ break;
+ case EXCLUDED:
+ filename = "bullet_white.png";
+ break;
+ case PENDING:
+ case NOCHANGE:
+ default:
+ filename = "bullet_black.png";
+ }
+ return WicketUtils.newImage(wicketId, filename, status.name());
+ }
+
+ public static ContextImage getFileImage(String wicketId, String filename) {
+ filename = filename.toLowerCase();
+ if (filename.endsWith(".java")) {
+ return newImage(wicketId, "file_java_16x16.png");
+ } else if (filename.endsWith(".rb")) {
+ return newImage(wicketId, "file_ruby_16x16.png");
+ } else if (filename.endsWith(".php")) {
+ return newImage(wicketId, "file_php_16x16.png");
+ } else if (filename.endsWith(".cs")) {
+ return newImage(wicketId, "file_cs_16x16.png");
+ } else if (filename.endsWith(".cpp")) {
+ return newImage(wicketId, "file_cpp_16x16.png");
+ } else if (filename.endsWith(".c")) {
+ return newImage(wicketId, "file_c_16x16.png");
+ } else if (filename.endsWith(".h")) {
+ return newImage(wicketId, "file_h_16x16.png");
+ } else if (filename.endsWith(".sln")) {
+ return newImage(wicketId, "file_vs_16x16.png");
+ } else if (filename.endsWith(".csv") || filename.endsWith(".xls")
+ || filename.endsWith(".xlsx")) {
+ return newImage(wicketId, "file_excel_16x16.png");
+ } else if (filename.endsWith(".doc") || filename.endsWith(".docx")) {
+ return newImage(wicketId, "file_word_16x16.png");
+ } else if (filename.endsWith(".ppt")) {
+ return newImage(wicketId, "file_ppt_16x16.png");
+ } else if (filename.endsWith(".zip")) {
+ return newImage(wicketId, "file_zip_16x16.png");
+ } else if (filename.endsWith(".pdf")) {
+ return newImage(wicketId, "file_acrobat_16x16.png");
+ } else if (filename.endsWith(".htm") || filename.endsWith(".html")) {
+ return newImage(wicketId, "file_world_16x16.png");
+ } else if (filename.endsWith(".xml")) {
+ return newImage(wicketId, "file_code_16x16.png");
+ } else if (filename.endsWith(".properties")) {
+ return newImage(wicketId, "file_settings_16x16.png");
+ }
+
+ List<String> mdExtensions = GitBlit.getStrings(Keys.web.markdownExtensions);
+ for (String ext : mdExtensions) {
+ if (filename.endsWith('.' + ext.toLowerCase())) {
+ return newImage(wicketId, "file_world_16x16.png");
+ }
+ }
+ return newImage(wicketId, "file_16x16.png");
+ }
+
+ public static ContextImage getRegistrationImage(String wicketId, FederationModel registration,
+ Component c) {
+ if (registration.isResultData()) {
+ return WicketUtils.newImage(wicketId, "information_16x16.png",
+ c.getString("gb.federationResults"));
+ } else {
+ return WicketUtils.newImage(wicketId, "arrow_left.png",
+ c.getString("gb.federationRegistration"));
+ }
+ }
+
+ public static ContextImage newClearPixel(String wicketId) {
+ return newImage(wicketId, "pixel.png");
+ }
+
+ public static ContextImage newBlankImage(String wicketId) {
+ return newImage(wicketId, "blank.png");
+ }
+
+ public static ContextImage newImage(String wicketId, String file) {
+ return newImage(wicketId, file, null);
+ }
+
+ public static ContextImage newImage(String wicketId, String file, String tooltip) {
+ ContextImage img = new ContextImage(wicketId, file);
+ if (!StringUtils.isEmpty(tooltip)) {
+ setHtmlTooltip(img, tooltip);
+ }
+ return img;
+ }
+
+ public static Label newIcon(String wicketId, String css) {
+ Label lbl = new Label(wicketId);
+ setCssClass(lbl, css);
+ return lbl;
+ }
+
+ public static Label newBlankIcon(String wicketId) {
+ Label lbl = new Label(wicketId);
+ setCssClass(lbl, "");
+ lbl.setRenderBodyOnly(true);
+ return lbl;
+ }
+
+ public static ContextRelativeResource getResource(String file) {
+ return new ContextRelativeResource(file);
+ }
+
+ public static String getGitblitURL(Request request) {
+ HttpServletRequest req = ((WebRequest) request).getHttpServletRequest();
+ return HttpUtils.getGitblitURL(req);
+ }
+
+ public static HeaderContributor syndicationDiscoveryLink(final String feedTitle,
+ final String url) {
+ return new HeaderContributor(new IHeaderContributor() {
+ private static final long serialVersionUID = 1L;
+
+ public void renderHead(IHeaderResponse response) {
+ String contentType = "application/rss+xml";
+
+ StringBuilder buffer = new StringBuilder();
+ buffer.append("<link rel=\"alternate\" ");
+ buffer.append("type=\"").append(contentType).append("\" ");
+ buffer.append("title=\"").append(feedTitle).append("\" ");
+ buffer.append("href=\"").append(url).append("\" />");
+ response.renderString(buffer.toString());
+ }
+ });
+ }
+
+ public static PageParameters newTokenParameter(String token) {
+ return new PageParameters("t=" + token);
+ }
+
+ public static PageParameters newRegistrationParameter(String url, String name) {
+ return new PageParameters("u=" + url + ",n=" + name);
+ }
+
+ public static PageParameters newUsernameParameter(String username) {
+ return new PageParameters("user=" + username);
+ }
+
+ public static PageParameters newTeamnameParameter(String teamname) {
+ return new PageParameters("team=" + teamname);
+ }
+
+ public static PageParameters newProjectParameter(String projectName) {
+ return new PageParameters("p=" + projectName);
+ }
+
+ public static PageParameters newRepositoryParameter(String repositoryName) {
+ return new PageParameters("r=" + repositoryName);
+ }
+
+ public static PageParameters newObjectParameter(String objectId) {
+ return new PageParameters("h=" + objectId);
+ }
+
+ public static PageParameters newObjectParameter(String repositoryName, String objectId) {
+ if (StringUtils.isEmpty(objectId)) {
+ return newRepositoryParameter(repositoryName);
+ }
+ return new PageParameters("r=" + repositoryName + ",h=" + objectId);
+ }
+
+ public static PageParameters newPathParameter(String repositoryName, String objectId,
+ String path) {
+ if (StringUtils.isEmpty(path)) {
+ return newObjectParameter(repositoryName, objectId);
+ }
+ if (StringUtils.isEmpty(objectId)) {
+ return new PageParameters("r=" + repositoryName + ",f=" + path);
+ }
+ return new PageParameters("r=" + repositoryName + ",h=" + objectId + ",f=" + path);
+ }
+
+ public static PageParameters newLogPageParameter(String repositoryName, String objectId,
+ int pageNumber) {
+ if (pageNumber <= 1) {
+ return newObjectParameter(repositoryName, objectId);
+ }
+ if (StringUtils.isEmpty(objectId)) {
+ return new PageParameters("r=" + repositoryName + ",pg=" + pageNumber);
+ }
+ return new PageParameters("r=" + repositoryName + ",h=" + objectId + ",pg=" + pageNumber);
+ }
+
+ public static PageParameters newHistoryPageParameter(String repositoryName, String objectId,
+ String path, int pageNumber) {
+ if (pageNumber <= 1) {
+ return newObjectParameter(repositoryName, objectId);
+ }
+ if (StringUtils.isEmpty(objectId)) {
+ return new PageParameters("r=" + repositoryName + ",f=" + path + ",pg=" + pageNumber);
+ }
+ return new PageParameters("r=" + repositoryName + ",h=" + objectId + ",f=" + path + ",pg="
+ + pageNumber);
+ }
+
+ public static PageParameters newBlobDiffParameter(String repositoryName, String baseCommitId,
+ String commitId, String path) {
+ if (StringUtils.isEmpty(commitId)) {
+ return new PageParameters("r=" + repositoryName + ",f=" + path + ",hb=" + baseCommitId);
+ }
+ return new PageParameters("r=" + repositoryName + ",h=" + commitId + ",f=" + path + ",hb="
+ + baseCommitId);
+ }
+
+ public static PageParameters newSearchParameter(String repositoryName, String commitId,
+ String search, Constants.SearchType type) {
+ if (StringUtils.isEmpty(commitId)) {
+ return new PageParameters("r=" + repositoryName + ",s=" + search + ",st=" + type.name());
+ }
+ return new PageParameters("r=" + repositoryName + ",h=" + commitId + ",s=" + search
+ + ",st=" + type.name());
+ }
+
+ public static PageParameters newSearchParameter(String repositoryName, String commitId,
+ String search, Constants.SearchType type, int pageNumber) {
+ if (StringUtils.isEmpty(commitId)) {
+ return new PageParameters("r=" + repositoryName + ",s=" + search + ",st=" + type.name()
+ + ",pg=" + pageNumber);
+ }
+ return new PageParameters("r=" + repositoryName + ",h=" + commitId + ",s=" + search
+ + ",st=" + type.name() + ",pg=" + pageNumber);
+ }
+
+ public static String getProjectName(PageParameters params) {
+ return params.getString("p", "");
+ }
+
+ public static String getRepositoryName(PageParameters params) {
+ return params.getString("r", "");
+ }
+
+ public static String getObject(PageParameters params) {
+ return params.getString("h", null);
+ }
+
+ public static String getPath(PageParameters params) {
+ return params.getString("f", null);
+ }
+
+ public static String getBaseObjectId(PageParameters params) {
+ return params.getString("hb", null);
+ }
+
+ public static String getSearchString(PageParameters params) {
+ return params.getString("s", null);
+ }
+
+ public static String getSearchType(PageParameters params) {
+ return params.getString("st", null);
+ }
+
+ public static int getPage(PageParameters params) {
+ // index from 1
+ return params.getInt("pg", 1);
+ }
+
+ public static String getRegEx(PageParameters params) {
+ return params.getString("x", "");
+ }
+
+ public static String getSet(PageParameters params) {
+ return params.getString("set", "");
+ }
+
+ public static String getTeam(PageParameters params) {
+ return params.getString("team", "");
+ }
+
+ public static int getDaysBack(PageParameters params) {
+ return params.getInt("db", 14);
+ }
+
+ public static String getUsername(PageParameters params) {
+ return params.getString("user", "");
+ }
+
+ public static String getTeamname(PageParameters params) {
+ return params.getString("team", "");
+ }
+
+ public static String getToken(PageParameters params) {
+ return params.getString("t", "");
+ }
+
+ public static String getUrlParameter(PageParameters params) {
+ return params.getString("u", "");
+ }
+
+ public static String getNameParameter(PageParameters params) {
+ return params.getString("n", "");
+ }
+
+ public static Label createDateLabel(String wicketId, Date date, TimeZone timeZone, TimeUtils timeUtils) {
+ String format = GitBlit.getString(Keys.web.datestampShortFormat, "MM/dd/yy");
+ DateFormat df = new SimpleDateFormat(format);
+ if (timeZone == null) {
+ timeZone = GitBlit.getTimezone();
+ }
+ df.setTimeZone(timeZone);
+ String dateString;
+ if (date.getTime() == 0) {
+ dateString = "--";
+ } else {
+ dateString = df.format(date);
+ }
+ String title = null;
+ if (date.getTime() <= System.currentTimeMillis()) {
+ // past
+ title = timeUtils.timeAgo(date);
+ }
+ if ((System.currentTimeMillis() - date.getTime()) < 10 * 24 * 60 * 60 * 1000L) {
+ String tmp = dateString;
+ dateString = title;
+ title = tmp;
+ }
+ Label label = new Label(wicketId, dateString);
+ WicketUtils.setCssClass(label, timeUtils.timeAgoCss(date));
+ if (!StringUtils.isEmpty(title)) {
+ WicketUtils.setHtmlTooltip(label, title);
+ }
+ return label;
+ }
+
+ public static Label createTimeLabel(String wicketId, Date date, TimeZone timeZone, TimeUtils timeUtils) {
+ String format = GitBlit.getString(Keys.web.timeFormat, "HH:mm");
+ DateFormat df = new SimpleDateFormat(format);
+ if (timeZone == null) {
+ timeZone = GitBlit.getTimezone();
+ }
+ df.setTimeZone(timeZone);
+ String timeString;
+ if (date.getTime() == 0) {
+ timeString = "--";
+ } else {
+ timeString = df.format(date);
+ }
+ String title = timeUtils.timeAgo(date);
+ Label label = new Label(wicketId, timeString);
+ if (!StringUtils.isEmpty(title)) {
+ WicketUtils.setHtmlTooltip(label, title);
+ }
+ return label;
+ }
+
+ public static Label createDatestampLabel(String wicketId, Date date, TimeZone timeZone, TimeUtils timeUtils) {
+ String format = GitBlit.getString(Keys.web.datestampLongFormat, "EEEE, MMMM d, yyyy");
+ DateFormat df = new SimpleDateFormat(format);
+ if (timeZone == null) {
+ timeZone = GitBlit.getTimezone();
+ }
+ df.setTimeZone(timeZone);
+ String dateString;
+ if (date.getTime() == 0) {
+ dateString = "--";
+ } else {
+ dateString = df.format(date);
+ }
+ String title = null;
+ if (TimeUtils.isToday(date)) {
+ title = timeUtils.today();
+ } else if (TimeUtils.isYesterday(date)) {
+ title = timeUtils.yesterday();
+ } else if (date.getTime() <= System.currentTimeMillis()) {
+ // past
+ title = timeUtils.timeAgo(date);
+ }
+ if ((System.currentTimeMillis() - date.getTime()) < 10 * 24 * 60 * 60 * 1000L) {
+ String tmp = dateString;
+ dateString = title;
+ title = tmp;
+ }
+ Label label = new Label(wicketId, dateString);
+ if (!StringUtils.isEmpty(title)) {
+ WicketUtils.setHtmlTooltip(label, title);
+ }
+ return label;
+ }
+
+ public static Label createTimestampLabel(String wicketId, Date date, TimeZone timeZone, TimeUtils timeUtils) {
+ String format = GitBlit.getString(Keys.web.datetimestampLongFormat,
+ "EEEE, MMMM d, yyyy HH:mm Z");
+ DateFormat df = new SimpleDateFormat(format);
+ if (timeZone == null) {
+ timeZone = GitBlit.getTimezone();
+ }
+ df.setTimeZone(timeZone);
+ String dateString;
+ if (date.getTime() == 0) {
+ dateString = "--";
+ } else {
+ dateString = df.format(date);
+ }
+ String title = null;
+ if (date.getTime() <= System.currentTimeMillis()) {
+ // past
+ title = timeUtils.timeAgo(date);
+ }
+ Label label = new Label(wicketId, dateString);
+ if (!StringUtils.isEmpty(title)) {
+ WicketUtils.setHtmlTooltip(label, title);
+ }
+ return label;
+ }
+
+ public static IChartData getChartData(Collection<Metric> metrics) {
+ final double[] commits = new double[metrics.size()];
+ final double[] tags = new double[metrics.size()];
+ int i = 0;
+ double max = 0;
+ for (Metric m : metrics) {
+ commits[i] = m.count;
+ if (m.tag > 0) {
+ tags[i] = m.count;
+ } else {
+ tags[i] = -1d;
+ }
+ max = Math.max(max, m.count);
+ i++;
+ }
+ IChartData data = new AbstractChartData(max) {
+ private static final long serialVersionUID = 1L;
+
+ public double[][] getData() {
+ return new double[][] { commits, tags };
+ }
+ };
+ return data;
+ }
+
+ public static double maxValue(Collection<Metric> metrics) {
+ double max = Double.MIN_VALUE;
+ for (Metric m : metrics) {
+ if (m.count > max) {
+ max = m.count;
+ }
+ }
+ return max;
+ }
+
+ public static IChartData getScatterData(Collection<Metric> metrics) {
+ final double[] y = new double[metrics.size()];
+ final double[] x = new double[metrics.size()];
+ int i = 0;
+ double max = 0;
+ for (Metric m : metrics) {
+ y[i] = m.count;
+ if (m.duration > 0) {
+ x[i] = m.duration;
+ } else {
+ x[i] = -1d;
+ }
+ max = Math.max(max, m.count);
+ i++;
+ }
+ IChartData data = new AbstractChartData(max) {
+ private static final long serialVersionUID = 1L;
+
+ public double[][] getData() {
+ return new double[][] { x, y };
+ }
+ };
+ return data;
+ }
+
+}
diff --git a/src/main/java/com/gitblit/wicket/charting/GoogleChart.java b/src/main/java/com/gitblit/wicket/charting/GoogleChart.java
new file mode 100644
index 00000000..b6309ffe
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/charting/GoogleChart.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.charting;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+import com.gitblit.utils.StringUtils;
+
+/**
+ * Abstract parent class for Google Charts built with the Visualization API.
+ *
+ * @author James Moger
+ *
+ */
+public abstract class GoogleChart implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+ final String tagId;
+ final String dataName;
+ final String title;
+ final String keyName;
+ final String valueName;
+ final List<ChartValue> values;
+ int width;
+ int height;
+
+ public GoogleChart(String tagId, String title, String keyName, String valueName) {
+ this.tagId = tagId;
+ this.dataName = StringUtils.getSHA1(title).substring(0, 8);
+ this.title = title;
+ this.keyName = keyName;
+ this.valueName = valueName;
+ values = new ArrayList<ChartValue>();
+ }
+
+ public void setWidth(int width) {
+ this.width = width;
+ }
+
+ public void setHeight(int height) {
+ this.height = height;
+ }
+
+ public void addValue(String name, int value) {
+ values.add(new ChartValue(name, value));
+ }
+
+ public void addValue(String name, float value) {
+ values.add(new ChartValue(name, value));
+ }
+
+ public void addValue(String name, double value) {
+ values.add(new ChartValue(name, (float) value));
+ }
+
+ protected abstract void appendChart(StringBuilder sb);
+
+ protected void line(StringBuilder sb, String line) {
+ sb.append(line);
+ sb.append('\n');
+ }
+
+ protected class ChartValue implements Serializable, Comparable<ChartValue> {
+
+ private static final long serialVersionUID = 1L;
+
+ final String name;
+ final float value;
+
+ ChartValue(String name, float value) {
+ this.name = name;
+ this.value = value;
+ }
+
+ @Override
+ public int compareTo(ChartValue o) {
+ // sorts the dataset by largest value first
+ if (value > o.value) {
+ return -1;
+ } else if (value < o.value) {
+ return 1;
+ }
+ return 0;
+ }
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/charting/GoogleCharts.java b/src/main/java/com/gitblit/wicket/charting/GoogleCharts.java
new file mode 100644
index 00000000..77c522b5
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/charting/GoogleCharts.java
@@ -0,0 +1,68 @@
+/*
+ Copyright 2011 gitblit.com.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+package com.gitblit.wicket.charting;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.wicket.markup.html.IHeaderContributor;
+import org.apache.wicket.markup.html.IHeaderResponse;
+
+/**
+ * The Google Visualization API provides interactive JavaScript based charts and
+ * graphs. This class implements the JavaScript header necessary to display
+ * complete graphs and charts.
+ *
+ * @author James Moger
+ *
+ */
+public class GoogleCharts implements IHeaderContributor {
+
+ private static final long serialVersionUID = 1L;
+
+ public final List<GoogleChart> charts = new ArrayList<GoogleChart>();
+
+ public void addChart(GoogleChart chart) {
+ charts.add(chart);
+ }
+
+ @Override
+ public void renderHead(IHeaderResponse response) {
+ // add Google Chart JS API reference
+ response.renderJavascriptReference("https://www.google.com/jsapi");
+
+ // prepare draw chart function
+ StringBuilder sb = new StringBuilder();
+ line(sb, "google.load(\"visualization\", \"1\", {packages:[\"corechart\"]});");
+ line(sb, "google.setOnLoadCallback(drawChart);");
+ line(sb, "function drawChart() {");
+
+ // add charts to header
+ for (GoogleChart chart : charts) {
+ chart.appendChart(sb);
+ }
+
+ // end draw chart function
+ line(sb, "}");
+ response.renderJavascript(sb.toString(), null);
+ }
+
+ private void line(StringBuilder sb, String line) {
+ sb.append(line);
+ sb.append('\n');
+ }
+} \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/charting/GoogleLineChart.java b/src/main/java/com/gitblit/wicket/charting/GoogleLineChart.java
new file mode 100644
index 00000000..fc0bf837
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/charting/GoogleLineChart.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.charting;
+
+import java.text.MessageFormat;
+
+/**
+ * Builds an interactive line chart using the Visualization API.
+ *
+ * @author James Moger
+ *
+ */
+public class GoogleLineChart extends GoogleChart {
+
+ private static final long serialVersionUID = 1L;
+
+ public GoogleLineChart(String tagId, String title, String keyName, String valueName) {
+ super(tagId, title, keyName, valueName);
+ }
+
+ @Override
+ protected void appendChart(StringBuilder sb) {
+ String dName = "data_" + dataName;
+ line(sb, MessageFormat.format("var {0} = new google.visualization.DataTable();", dName));
+ line(sb, MessageFormat.format("{0}.addColumn(''string'', ''{1}'');", dName, keyName));
+ line(sb, MessageFormat.format("{0}.addColumn(''number'', ''{1}'');", dName, valueName));
+ line(sb, MessageFormat.format("{0}.addRows({1,number,0});", dName, values.size()));
+
+ for (int i = 0; i < values.size(); i++) {
+ ChartValue value = values.get(i);
+ line(sb, MessageFormat.format("{0}.setValue({1,number,0}, 0, ''{2}'');", dName, i,
+ value.name));
+ line(sb, MessageFormat.format("{0}.setValue({1,number,0}, 1, {2,number,0.0});", dName,
+ i, value.value));
+ }
+
+ String cName = "chart_" + dataName;
+ line(sb, MessageFormat.format(
+ "var {0} = new google.visualization.LineChart(document.getElementById(''{1}''));",
+ cName, tagId));
+ line(sb,
+ MessageFormat
+ .format("{0}.draw({1}, '{'width: {2,number,0}, height: {3,number,0}, pointSize: 4, chartArea:'{'left:20,top:20'}', vAxis: '{' textPosition: ''none'' '}', legend: ''none'', title: ''{4}'' '}');",
+ cName, dName, width, height, title));
+ line(sb, "");
+ }
+} \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/charting/GooglePieChart.java b/src/main/java/com/gitblit/wicket/charting/GooglePieChart.java
new file mode 100644
index 00000000..119a8248
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/charting/GooglePieChart.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.charting;
+
+import java.text.MessageFormat;
+import java.util.Collections;
+
+import com.gitblit.utils.StringUtils;
+
+/**
+ * Builds an interactive pie chart using the Visualization API.
+ *
+ * @author James Moger
+ *
+ */
+public class GooglePieChart extends GoogleChart {
+
+ private static final long serialVersionUID = 1L;
+
+ public GooglePieChart(String tagId, String title, String keyName, String valueName) {
+ super(tagId, title, keyName, valueName);
+ }
+
+ @Override
+ protected void appendChart(StringBuilder sb) {
+ // create dataset
+ String dName = "data_" + dataName;
+ line(sb, MessageFormat.format("var {0} = new google.visualization.DataTable();", dName));
+ line(sb, MessageFormat.format("{0}.addColumn(''string'', ''{1}'');", dName, keyName));
+ line(sb, MessageFormat.format("{0}.addColumn(''number'', ''{1}'');", dName, valueName));
+ line(sb, MessageFormat.format("{0}.addRows({1,number,0});", dName, values.size()));
+
+ Collections.sort(values);
+
+ StringBuilder colors = new StringBuilder("colors:[");
+ for (int i = 0; i < values.size(); i++) {
+ ChartValue value = values.get(i);
+ colors.append('\'');
+ colors.append(StringUtils.getColor(value.name));
+ colors.append('\'');
+ if (i < values.size() - 1) {
+ colors.append(',');
+ }
+ line(sb, MessageFormat.format("{0}.setValue({1,number,0}, 0, ''{2}'');", dName, i,
+ value.name));
+ line(sb, MessageFormat.format("{0}.setValue({1,number,0}, 1, {2,number,0.0});", dName,
+ i, value.value));
+ }
+ colors.append(']');
+
+ // instantiate chart
+ String cName = "chart_" + dataName;
+ line(sb, MessageFormat.format(
+ "var {0} = new google.visualization.PieChart(document.getElementById(''{1}''));",
+ cName, tagId));
+ line(sb,
+ MessageFormat
+ .format("{0}.draw({1}, '{'width: {2,number,0}, height: {3,number,0}, chartArea:'{'left:20,top:20'}', title: ''{4}'', {5} '}');",
+ cName, dName, width, height, title, colors.toString()));
+ line(sb, "");
+ }
+} \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/ActivityPage.html b/src/main/java/com/gitblit/wicket/pages/ActivityPage.html
new file mode 100644
index 00000000..4b10c2cf
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/ActivityPage.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+<body>
+<wicket:extend>
+ <div class="pageTitle">
+ <h2><wicket:message key="gb.recentActivity"></wicket:message><small> <span class="hidden-phone">/ <span wicket:id="subheader">[days back]</span></span></small></h2>
+ </div>
+ <div class="hidden-phone" style="height: 155px;text-align: center;">
+ <table>
+ <tr>
+ <td><span class="hidden-tablet" id="chartDaily"></span></td>
+ <td><span id="chartRepositories"></span></td>
+ <td><span id="chartAuthors"></span></td>
+ </tr>
+ </table>
+ </div>
+ <div wicket:id="activityPanel" style="padding-top:5px;" >[activity panel]</div>
+</wicket:extend>
+</body>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/ActivityPage.java b/src/main/java/com/gitblit/wicket/pages/ActivityPage.java
new file mode 100644
index 00000000..bceac8f4
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/ActivityPage.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.pages;
+
+import java.text.MessageFormat;
+import java.text.SimpleDateFormat;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.wicket.PageParameters;
+import org.apache.wicket.behavior.HeaderContributor;
+import org.apache.wicket.markup.html.basic.Label;
+
+import com.gitblit.GitBlit;
+import com.gitblit.Keys;
+import com.gitblit.models.Activity;
+import com.gitblit.models.Metric;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.utils.ActivityUtils;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.wicket.PageRegistration;
+import com.gitblit.wicket.PageRegistration.DropDownMenuItem;
+import com.gitblit.wicket.PageRegistration.DropDownMenuRegistration;
+import com.gitblit.wicket.WicketUtils;
+import com.gitblit.wicket.charting.GoogleChart;
+import com.gitblit.wicket.charting.GoogleCharts;
+import com.gitblit.wicket.charting.GoogleLineChart;
+import com.gitblit.wicket.charting.GooglePieChart;
+import com.gitblit.wicket.panels.ActivityPanel;
+
+/**
+ * Activity Page shows a list of recent commits across all visible Gitblit
+ * repositories.
+ *
+ * @author James Moger
+ *
+ */
+public class ActivityPage extends RootPage {
+
+ public ActivityPage(PageParameters params) {
+ super(params);
+ setupPage("", "");
+
+ // parameters
+ int daysBack = WicketUtils.getDaysBack(params);
+ if (daysBack < 1) {
+ daysBack = 14;
+ }
+ String objectId = WicketUtils.getObject(params);
+
+ // determine repositories to view and retrieve the activity
+ List<RepositoryModel> models = getRepositories(params);
+ List<Activity> recentActivity = ActivityUtils.getRecentActivity(models,
+ daysBack, objectId, getTimeZone());
+
+ if (recentActivity.size() == 0) {
+ // no activity, skip graphs and activity panel
+ add(new Label("subheader", MessageFormat.format(getString("gb.recentActivityNone"),
+ daysBack)));
+ add(new Label("activityPanel"));
+ } else {
+ // calculate total commits and total authors
+ int totalCommits = 0;
+ Set<String> uniqueAuthors = new HashSet<String>();
+ for (Activity activity : recentActivity) {
+ totalCommits += activity.getCommitCount();
+ uniqueAuthors.addAll(activity.getAuthorMetrics().keySet());
+ }
+ int totalAuthors = uniqueAuthors.size();
+
+ // add the subheader with stat numbers
+ add(new Label("subheader", MessageFormat.format(getString("gb.recentActivityStats"),
+ daysBack, totalCommits, totalAuthors)));
+
+ // create the activity charts
+ GoogleCharts charts = createCharts(recentActivity);
+ add(new HeaderContributor(charts));
+
+ // add activity panel
+ add(new ActivityPanel("activityPanel", recentActivity));
+ }
+ }
+
+ @Override
+ protected boolean reusePageParameters() {
+ return true;
+ }
+
+ @Override
+ protected void addDropDownMenus(List<PageRegistration> pages) {
+ DropDownMenuRegistration filters = new DropDownMenuRegistration("gb.filters",
+ ActivityPage.class);
+
+ PageParameters currentParameters = getPageParameters();
+ int daysBack = GitBlit.getInteger(Keys.web.activityDuration, 14);
+ if (currentParameters != null && !currentParameters.containsKey("db")) {
+ currentParameters.put("db", daysBack);
+ }
+
+ // preserve time filter options on repository choices
+ filters.menuItems.addAll(getRepositoryFilterItems(currentParameters));
+
+ // preserve repository filter options on time choices
+ filters.menuItems.addAll(getTimeFilterItems(currentParameters));
+
+ if (filters.menuItems.size() > 0) {
+ // Reset Filter
+ filters.menuItems.add(new DropDownMenuItem(getString("gb.reset"), null, null));
+ }
+ pages.add(filters);
+ }
+
+ /**
+ * Creates the daily activity line chart, the active repositories pie chart,
+ * and the active authors pie chart
+ *
+ * @param recentActivity
+ * @return
+ */
+ private GoogleCharts createCharts(List<Activity> recentActivity) {
+ // activity metrics
+ Map<String, Metric> repositoryMetrics = new HashMap<String, Metric>();
+ Map<String, Metric> authorMetrics = new HashMap<String, Metric>();
+
+ // aggregate repository and author metrics
+ for (Activity activity : recentActivity) {
+
+ // aggregate author metrics
+ for (Map.Entry<String, Metric> entry : activity.getAuthorMetrics().entrySet()) {
+ String author = entry.getKey();
+ if (!authorMetrics.containsKey(author)) {
+ authorMetrics.put(author, new Metric(author));
+ }
+ authorMetrics.get(author).count += entry.getValue().count;
+ }
+
+ // aggregate repository metrics
+ for (Map.Entry<String, Metric> entry : activity.getRepositoryMetrics().entrySet()) {
+ String repository = StringUtils.stripDotGit(entry.getKey());
+ if (!repositoryMetrics.containsKey(repository)) {
+ repositoryMetrics.put(repository, new Metric(repository));
+ }
+ repositoryMetrics.get(repository).count += entry.getValue().count;
+ }
+ }
+
+ // build google charts
+ int w = 310;
+ int h = 150;
+ GoogleCharts charts = new GoogleCharts();
+
+ // sort in reverse-chronological order and then reverse that
+ Collections.sort(recentActivity);
+ Collections.reverse(recentActivity);
+
+ // daily line chart
+ GoogleChart chart = new GoogleLineChart("chartDaily", getString("gb.dailyActivity"), "day",
+ getString("gb.commits"));
+ SimpleDateFormat df = new SimpleDateFormat("MMM dd");
+ df.setTimeZone(getTimeZone());
+ for (Activity metric : recentActivity) {
+ chart.addValue(df.format(metric.startDate), metric.getCommitCount());
+ }
+ chart.setWidth(w);
+ chart.setHeight(h);
+ charts.addChart(chart);
+
+ // active repositories pie chart
+ chart = new GooglePieChart("chartRepositories", getString("gb.activeRepositories"),
+ getString("gb.repository"), getString("gb.commits"));
+ for (Metric metric : repositoryMetrics.values()) {
+ chart.addValue(metric.name, metric.count);
+ }
+ chart.setWidth(w);
+ chart.setHeight(h);
+ charts.addChart(chart);
+
+ // active authors pie chart
+ chart = new GooglePieChart("chartAuthors", getString("gb.activeAuthors"),
+ getString("gb.author"), getString("gb.commits"));
+ for (Metric metric : authorMetrics.values()) {
+ chart.addValue(metric.name, metric.count);
+ }
+ chart.setWidth(w);
+ chart.setHeight(h);
+ charts.addChart(chart);
+
+ return charts;
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/pages/BasePage.html b/src/main/java/com/gitblit/wicket/pages/BasePage.html
new file mode 100644
index 00000000..4a642e73
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/BasePage.html
@@ -0,0 +1,62 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+
+ <!-- Head -->
+ <wicket:head>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title wicket:id="title">[page title]</title>
+ <link rel="icon" href="gitblt-favicon.png" type="image/png" />
+
+ <link rel="stylesheet" href="bootstrap/css/bootstrap.css"/>
+ <link rel="stylesheet" type="text/css" href="gitblit.css"/>
+ </wicket:head>
+
+ <body>
+
+ <!-- page content -->
+ <wicket:child />
+
+ <!-- page footer -->
+ <div class="container">
+ <footer class="footer">
+ <p class="pull-right">
+ <a title="gitblit homepage" href="http://gitblit.com/">
+ <span wicket:id="gbVersion"></span>
+ </a>
+ </p>
+ <div wicket:id="userPanel">[user panel]</div>
+ </footer>
+ </div>
+
+ <!-- Override Bootstrap's responsive menu background highlighting -->
+ <style>
+ @media (max-width: 979px) {
+ .nav-collapse .nav > li > a:hover, .nav-collapse .dropdown-menu a:hover {
+ background-color: #000070;
+ }
+
+ .navbar div > ul .dropdown-menu li a {
+ color: #ccc;
+ }
+ }
+ </style>
+
+ <!-- Include scripts at end for faster page loading -->
+ <script type="text/javascript" src="bootstrap/js/jquery.js"></script>
+ <script type="text/javascript" src="bootstrap/js/bootstrap.js"></script>
+ </body>
+
+ <!-- user fragment -->
+ <wicket:fragment wicket:id="userFragment">
+ <span class="userPanel" wicket:id="username"></span>
+ <span class="userPanel" wicket:id="loginLink"></span>
+ <span class="hidden-phone">
+ <span class="userPanel" wicket:id="separator"></span>
+ <span class="userPanel"><a wicket:id="changePasswordLink"><wicket:message key="gb.changePassword"></wicket:message></a></span>
+ </span>
+ </wicket:fragment>
+
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/BasePage.java b/src/main/java/com/gitblit/wicket/pages/BasePage.java
new file mode 100644
index 00000000..5c73df33
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/BasePage.java
@@ -0,0 +1,460 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.pages;
+
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.ResourceBundle;
+import java.util.Set;
+import java.util.TimeZone;
+import java.util.regex.Pattern;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.wicket.Application;
+import org.apache.wicket.MarkupContainer;
+import org.apache.wicket.PageParameters;
+import org.apache.wicket.RedirectToUrlException;
+import org.apache.wicket.RequestCycle;
+import org.apache.wicket.RestartResponseException;
+import org.apache.wicket.markup.html.CSSPackageResource;
+import org.apache.wicket.markup.html.WebPage;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.link.BookmarkablePageLink;
+import org.apache.wicket.markup.html.link.ExternalLink;
+import org.apache.wicket.markup.html.panel.FeedbackPanel;
+import org.apache.wicket.markup.html.panel.Fragment;
+import org.apache.wicket.protocol.http.RequestUtils;
+import org.apache.wicket.protocol.http.WebRequest;
+import org.apache.wicket.protocol.http.WebResponse;
+import org.apache.wicket.protocol.http.servlet.ServletWebRequest;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.Constants;
+import com.gitblit.Constants.AccessPermission;
+import com.gitblit.Constants.AccessRestrictionType;
+import com.gitblit.Constants.AuthorizationControl;
+import com.gitblit.Constants.FederationStrategy;
+import com.gitblit.GitBlit;
+import com.gitblit.Keys;
+import com.gitblit.models.ProjectModel;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.TeamModel;
+import com.gitblit.models.UserModel;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.utils.TimeUtils;
+import com.gitblit.wicket.GitBlitWebSession;
+import com.gitblit.wicket.WicketUtils;
+import com.gitblit.wicket.panels.LinkPanel;
+
+public abstract class BasePage extends WebPage {
+
+ private final Logger logger;
+
+ private transient TimeUtils timeUtils;
+
+ public BasePage() {
+ super();
+ logger = LoggerFactory.getLogger(getClass());
+ customizeHeader();
+ login();
+ }
+
+ public BasePage(PageParameters params) {
+ super(params);
+ logger = LoggerFactory.getLogger(getClass());
+ customizeHeader();
+ login();
+ }
+
+ private void customizeHeader() {
+ if (GitBlit.getBoolean(Keys.web.useResponsiveLayout, true)) {
+ add(CSSPackageResource.getHeaderContribution("bootstrap/css/bootstrap-responsive.css"));
+ }
+ }
+
+ protected String getLanguageCode() {
+ return GitBlitWebSession.get().getLocale().getLanguage();
+ }
+
+ protected String getCountryCode() {
+ return GitBlitWebSession.get().getLocale().getCountry().toLowerCase();
+ }
+
+ protected TimeUtils getTimeUtils() {
+ if (timeUtils == null) {
+ ResourceBundle bundle;
+ try {
+ bundle = ResourceBundle.getBundle("com.gitblit.wicket.GitBlitWebApp", GitBlitWebSession.get().getLocale());
+ } catch (Throwable t) {
+ bundle = ResourceBundle.getBundle("com.gitblit.wicket.GitBlitWebApp");
+ }
+ timeUtils = new TimeUtils(bundle);
+ }
+ return timeUtils;
+ }
+
+ @Override
+ protected void onBeforeRender() {
+ if (GitBlit.isDebugMode()) {
+ // strip Wicket tags in debug mode for jQuery DOM traversal
+ Application.get().getMarkupSettings().setStripWicketTags(true);
+ }
+ super.onBeforeRender();
+ }
+
+ @Override
+ protected void onAfterRender() {
+ if (GitBlit.isDebugMode()) {
+ // restore Wicket debug tags
+ Application.get().getMarkupSettings().setStripWicketTags(false);
+ }
+ super.onAfterRender();
+ }
+
+ private void login() {
+ GitBlitWebSession session = GitBlitWebSession.get();
+ if (session.isLoggedIn() && !session.isSessionInvalidated()) {
+ // already have a session, refresh usermodel to pick up
+ // any changes to permissions or roles (issue-186)
+ UserModel user = GitBlit.self().getUserModel(session.getUser().username);
+ session.setUser(user);
+ return;
+ }
+
+ // try to authenticate by servlet request
+ HttpServletRequest httpRequest = ((WebRequest) getRequestCycle().getRequest()).getHttpServletRequest();
+ UserModel user = GitBlit.self().authenticate(httpRequest);
+
+ // Login the user
+ if (user != null) {
+ // issue 62: fix session fixation vulnerability
+ session.replaceSession();
+ session.setUser(user);
+
+ // Set Cookie
+ WebResponse response = (WebResponse) getRequestCycle().getResponse();
+ GitBlit.self().setCookie(response, user);
+
+ session.continueRequest();
+ }
+ }
+
+ protected void setupPage(String repositoryName, String pageName) {
+ if (repositoryName != null && repositoryName.trim().length() > 0) {
+ add(new Label("title", getServerName() + " - " + repositoryName));
+ } else {
+ add(new Label("title", getServerName()));
+ }
+
+ ExternalLink rootLink = new ExternalLink("rootLink", urlFor(RepositoriesPage.class, null).toString());
+ WicketUtils.setHtmlTooltip(rootLink, GitBlit.getString(Keys.web.siteName, Constants.NAME));
+ add(rootLink);
+
+ // Feedback panel for info, warning, and non-fatal error messages
+ add(new FeedbackPanel("feedback"));
+
+ // footer
+ if (GitBlit.getBoolean(Keys.web.authenticateViewPages, true)
+ || GitBlit.getBoolean(Keys.web.authenticateAdminPages, true)) {
+ UserFragment userFragment = new UserFragment("userPanel", "userFragment", BasePage.this);
+ add(userFragment);
+ } else {
+ add(new Label("userPanel", ""));
+ }
+
+ add(new Label("gbVersion", "v" + Constants.getVersion()));
+ if (GitBlit.getBoolean(Keys.web.aggressiveHeapManagement, false)) {
+ System.gc();
+ }
+ }
+
+ protected Map<AccessRestrictionType, String> getAccessRestrictions() {
+ Map<AccessRestrictionType, String> map = new LinkedHashMap<AccessRestrictionType, String>();
+ for (AccessRestrictionType type : AccessRestrictionType.values()) {
+ switch (type) {
+ case NONE:
+ map.put(type, getString("gb.notRestricted"));
+ break;
+ case PUSH:
+ map.put(type, getString("gb.pushRestricted"));
+ break;
+ case CLONE:
+ map.put(type, getString("gb.cloneRestricted"));
+ break;
+ case VIEW:
+ map.put(type, getString("gb.viewRestricted"));
+ break;
+ }
+ }
+ return map;
+ }
+
+ protected Map<AccessPermission, String> getAccessPermissions() {
+ Map<AccessPermission, String> map = new LinkedHashMap<AccessPermission, String>();
+ for (AccessPermission type : AccessPermission.values()) {
+ switch (type) {
+ case NONE:
+ map.put(type, MessageFormat.format(getString("gb.noPermission"), type.code));
+ break;
+ case EXCLUDE:
+ map.put(type, MessageFormat.format(getString("gb.excludePermission"), type.code));
+ break;
+ case VIEW:
+ map.put(type, MessageFormat.format(getString("gb.viewPermission"), type.code));
+ break;
+ case CLONE:
+ map.put(type, MessageFormat.format(getString("gb.clonePermission"), type.code));
+ break;
+ case PUSH:
+ map.put(type, MessageFormat.format(getString("gb.pushPermission"), type.code));
+ break;
+ case CREATE:
+ map.put(type, MessageFormat.format(getString("gb.createPermission"), type.code));
+ break;
+ case DELETE:
+ map.put(type, MessageFormat.format(getString("gb.deletePermission"), type.code));
+ break;
+ case REWIND:
+ map.put(type, MessageFormat.format(getString("gb.rewindPermission"), type.code));
+ break;
+ }
+ }
+ return map;
+ }
+
+ protected Map<FederationStrategy, String> getFederationTypes() {
+ Map<FederationStrategy, String> map = new LinkedHashMap<FederationStrategy, String>();
+ for (FederationStrategy type : FederationStrategy.values()) {
+ switch (type) {
+ case EXCLUDE:
+ map.put(type, getString("gb.excludeFromFederation"));
+ break;
+ case FEDERATE_THIS:
+ map.put(type, getString("gb.federateThis"));
+ break;
+ case FEDERATE_ORIGIN:
+ map.put(type, getString("gb.federateOrigin"));
+ break;
+ }
+ }
+ return map;
+ }
+
+ protected Map<AuthorizationControl, String> getAuthorizationControls() {
+ Map<AuthorizationControl, String> map = new LinkedHashMap<AuthorizationControl, String>();
+ for (AuthorizationControl type : AuthorizationControl.values()) {
+ switch (type) {
+ case AUTHENTICATED:
+ map.put(type, getString("gb.allowAuthenticatedDescription"));
+ break;
+ case NAMED:
+ map.put(type, getString("gb.allowNamedDescription"));
+ break;
+ }
+ }
+ return map;
+ }
+
+ protected TimeZone getTimeZone() {
+ return GitBlit.getBoolean(Keys.web.useClientTimezone, false) ? GitBlitWebSession.get()
+ .getTimezone() : GitBlit.getTimezone();
+ }
+
+ protected String getServerName() {
+ ServletWebRequest servletWebRequest = (ServletWebRequest) getRequest();
+ HttpServletRequest req = servletWebRequest.getHttpServletRequest();
+ return req.getServerName();
+ }
+
+ public static String getRepositoryUrl(RepositoryModel repository) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(WicketUtils.getGitblitURL(RequestCycle.get().getRequest()));
+ sb.append(Constants.GIT_PATH);
+ sb.append(repository.name);
+
+ // inject username into repository url if authentication is required
+ if (repository.accessRestriction.exceeds(AccessRestrictionType.NONE)
+ && GitBlitWebSession.get().isLoggedIn()) {
+ String username = GitBlitWebSession.get().getUsername();
+ sb.insert(sb.indexOf("://") + 3, username + "@");
+ }
+ return sb.toString();
+ }
+
+ protected List<ProjectModel> getProjectModels() {
+ final UserModel user = GitBlitWebSession.get().getUser();
+ List<ProjectModel> projects = GitBlit.self().getProjectModels(user, true);
+ return projects;
+ }
+
+ protected List<ProjectModel> getProjects(PageParameters params) {
+ if (params == null) {
+ return getProjectModels();
+ }
+
+ boolean hasParameter = false;
+ String regex = WicketUtils.getRegEx(params);
+ String team = WicketUtils.getTeam(params);
+ int daysBack = params.getInt("db", 0);
+
+ List<ProjectModel> availableModels = getProjectModels();
+ Set<ProjectModel> models = new HashSet<ProjectModel>();
+
+ if (!StringUtils.isEmpty(regex)) {
+ // filter the projects by the regex
+ hasParameter = true;
+ Pattern pattern = Pattern.compile(regex);
+ for (ProjectModel model : availableModels) {
+ if (pattern.matcher(model.name).find()) {
+ models.add(model);
+ }
+ }
+ }
+
+ if (!StringUtils.isEmpty(team)) {
+ // filter the projects by the specified teams
+ hasParameter = true;
+ List<String> teams = StringUtils.getStringsFromValue(team, ",");
+
+ // need TeamModels first
+ List<TeamModel> teamModels = new ArrayList<TeamModel>();
+ for (String name : teams) {
+ TeamModel teamModel = GitBlit.self().getTeamModel(name);
+ if (teamModel != null) {
+ teamModels.add(teamModel);
+ }
+ }
+
+ // brute-force our way through finding the matching models
+ for (ProjectModel projectModel : availableModels) {
+ for (String repositoryName : projectModel.repositories) {
+ for (TeamModel teamModel : teamModels) {
+ if (teamModel.hasRepositoryPermission(repositoryName)) {
+ models.add(projectModel);
+ }
+ }
+ }
+ }
+ }
+
+ if (!hasParameter) {
+ models.addAll(availableModels);
+ }
+
+ // time-filter the list
+ if (daysBack > 0) {
+ Calendar cal = Calendar.getInstance();
+ cal.set(Calendar.HOUR_OF_DAY, 0);
+ cal.set(Calendar.MINUTE, 0);
+ cal.set(Calendar.SECOND, 0);
+ cal.set(Calendar.MILLISECOND, 0);
+ cal.add(Calendar.DATE, -1 * daysBack);
+ Date threshold = cal.getTime();
+ Set<ProjectModel> timeFiltered = new HashSet<ProjectModel>();
+ for (ProjectModel model : models) {
+ if (model.lastChange.after(threshold)) {
+ timeFiltered.add(model);
+ }
+ }
+ models = timeFiltered;
+ }
+
+ List<ProjectModel> list = new ArrayList<ProjectModel>(models);
+ Collections.sort(list);
+ return list;
+ }
+
+ public void warn(String message, Throwable t) {
+ logger.warn(message, t);
+ }
+
+ public void error(String message, boolean redirect) {
+ logger.error(message + " for " + GitBlitWebSession.get().getUsername());
+ if (redirect) {
+ GitBlitWebSession.get().cacheErrorMessage(message);
+ String relativeUrl = urlFor(RepositoriesPage.class, null).toString();
+ String absoluteUrl = RequestUtils.toAbsolutePath(relativeUrl);
+ throw new RedirectToUrlException(absoluteUrl);
+ } else {
+ super.error(message);
+ }
+ }
+
+ public void error(String message, Throwable t, boolean redirect) {
+ logger.error(message, t);
+ if (redirect) {
+ GitBlitWebSession.get().cacheErrorMessage(message);
+ throw new RestartResponseException(getApplication().getHomePage());
+ } else {
+ super.error(message);
+ }
+ }
+
+ public void authenticationError(String message) {
+ logger.error(getRequest().getURL() + " for " + GitBlitWebSession.get().getUsername());
+ if (!GitBlitWebSession.get().isLoggedIn()) {
+ // cache the request if we have not authenticated.
+ // the request will continue after authentication.
+ GitBlitWebSession.get().cacheRequest(getClass());
+ }
+ error(message, true);
+ }
+
+ /**
+ * Panel fragment for displaying login or logout/change_password links.
+ *
+ */
+ static class UserFragment extends Fragment {
+
+ private static final long serialVersionUID = 1L;
+
+ public UserFragment(String id, String markupId, MarkupContainer markupProvider) {
+ super(id, markupId, markupProvider);
+
+ GitBlitWebSession session = GitBlitWebSession.get();
+ if (session.isLoggedIn()) {
+ UserModel user = session.getUser();
+ boolean editCredentials = GitBlit.self().supportsCredentialChanges(user);
+ boolean standardLogin = session.authenticationType.isStandard();
+
+ // username, logout, and change password
+ add(new Label("username", user.getDisplayName() + ":"));
+ add(new LinkPanel("loginLink", null, markupProvider.getString("gb.logout"),
+ LogoutPage.class).setVisible(standardLogin));
+
+ // quick and dirty hack for showing a separator
+ add(new Label("separator", "|").setVisible(standardLogin && editCredentials));
+ add(new BookmarkablePageLink<Void>("changePasswordLink",
+ ChangePasswordPage.class).setVisible(editCredentials));
+ } else {
+ // login
+ add(new Label("username").setVisible(false));
+ add(new Label("loginLink").setVisible(false));
+ add(new Label("separator").setVisible(false));
+ add(new Label("changePasswordLink").setVisible(false));
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/pages/BlamePage.html b/src/main/java/com/gitblit/wicket/pages/BlamePage.html
new file mode 100644
index 00000000..9391eaf0
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/BlamePage.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+
+<body>
+<wicket:extend>
+
+ <!-- blame nav links -->
+ <div class="page_nav2">
+ <a wicket:id="blobLink"><wicket:message key="gb.view"></wicket:message></a> | <a wicket:id="historyLink"><wicket:message key="gb.history"></wicket:message></a> | <a wicket:id="headLink"><wicket:message key="gb.head"></wicket:message></a> | <a wicket:id="commitLink"><wicket:message key="gb.commit"></wicket:message></a> | <a wicket:id="commitDiffLink"><wicket:message key="gb.commitdiff"></wicket:message></a>
+ </div>
+
+ <!-- commit header -->
+ <div wicket:id="commitHeader">[commit header]</div>
+
+ <!-- breadcrumbs -->
+ <div wicket:id="breadcrumbs">[breadcrumbs]</div>
+
+ <!-- blame content -->
+ <table class="annotated" style="margin-bottom:5px;">
+ <tbody>
+ <tr>
+ <th><wicket:message key="gb.commit">[commit]</wicket:message></th>
+ <th><wicket:message key="gb.line">[line]</wicket:message></th>
+ <th><wicket:message key="gb.content">[content]</wicket:message></th>
+ </tr>
+ <tr wicket:id="annotation">
+ <td><span class="sha1" wicket:id="commit"></span></td>
+ <td><span class="sha1" wicket:id="line"></span></td>
+ <td><span class="sha1" wicket:id="data"></span></td>
+ </tr>
+ </tbody>
+ </table>
+
+</wicket:extend>
+</body>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/BlamePage.java b/src/main/java/com/gitblit/wicket/pages/BlamePage.java
new file mode 100644
index 00000000..d76181d2
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/BlamePage.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.pages;
+
+import java.text.DateFormat;
+import java.text.MessageFormat;
+import java.text.SimpleDateFormat;
+import java.util.List;
+
+import org.apache.wicket.PageParameters;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.link.BookmarkablePageLink;
+import org.apache.wicket.markup.repeater.Item;
+import org.apache.wicket.markup.repeater.data.DataView;
+import org.apache.wicket.markup.repeater.data.ListDataProvider;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.revwalk.RevCommit;
+
+import com.gitblit.GitBlit;
+import com.gitblit.Keys;
+import com.gitblit.models.AnnotatedLine;
+import com.gitblit.utils.DiffUtils;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.wicket.WicketUtils;
+import com.gitblit.wicket.panels.CommitHeaderPanel;
+import com.gitblit.wicket.panels.LinkPanel;
+import com.gitblit.wicket.panels.PathBreadcrumbsPanel;
+
+public class BlamePage extends RepositoryPage {
+
+ public BlamePage(PageParameters params) {
+ super(params);
+
+ final String blobPath = WicketUtils.getPath(params);
+
+ RevCommit commit = getCommit();
+
+ add(new BookmarkablePageLink<Void>("blobLink", BlobPage.class,
+ WicketUtils.newPathParameter(repositoryName, objectId, blobPath)));
+ add(new BookmarkablePageLink<Void>("commitLink", CommitPage.class,
+ WicketUtils.newObjectParameter(repositoryName, objectId)));
+ add(new BookmarkablePageLink<Void>("commitDiffLink", CommitDiffPage.class,
+ WicketUtils.newObjectParameter(repositoryName, objectId)));
+
+ // blame page links
+ add(new BookmarkablePageLink<Void>("headLink", BlamePage.class,
+ WicketUtils.newPathParameter(repositoryName, Constants.HEAD, blobPath)));
+ add(new BookmarkablePageLink<Void>("historyLink", HistoryPage.class,
+ WicketUtils.newPathParameter(repositoryName, objectId, blobPath)));
+
+ add(new CommitHeaderPanel("commitHeader", repositoryName, commit));
+
+ add(new PathBreadcrumbsPanel("breadcrumbs", repositoryName, blobPath, objectId));
+
+ String format = GitBlit.getString(Keys.web.datetimestampLongFormat,
+ "EEEE, MMMM d, yyyy HH:mm Z");
+ final DateFormat df = new SimpleDateFormat(format);
+ df.setTimeZone(getTimeZone());
+ List<AnnotatedLine> lines = DiffUtils.blame(getRepository(), blobPath, objectId);
+ ListDataProvider<AnnotatedLine> blameDp = new ListDataProvider<AnnotatedLine>(lines);
+ DataView<AnnotatedLine> blameView = new DataView<AnnotatedLine>("annotation", blameDp) {
+ private static final long serialVersionUID = 1L;
+ private int count;
+ private String lastCommitId = "";
+ private boolean showInitials = true;
+
+ public void populateItem(final Item<AnnotatedLine> item) {
+ AnnotatedLine entry = item.getModelObject();
+ item.add(new Label("line", "" + entry.lineNumber));
+ item.add(new Label("data", StringUtils.escapeForHtml(entry.data, true))
+ .setEscapeModelStrings(false));
+ if (!lastCommitId.equals(entry.commitId)) {
+ lastCommitId = entry.commitId;
+ count++;
+ // show the link for first line
+ LinkPanel commitLink = new LinkPanel("commit", null,
+ getShortObjectId(entry.commitId), CommitPage.class,
+ newCommitParameter(entry.commitId));
+ WicketUtils.setHtmlTooltip(commitLink,
+ MessageFormat.format("{0}, {1}", entry.author, df.format(entry.when)));
+ item.add(commitLink);
+ showInitials = true;
+ } else {
+ if (showInitials) {
+ showInitials = false;
+ // show author initials
+ item.add(new Label("commit", getInitials(entry.author)));
+ } else {
+ // hide the commit link until the next block
+ item.add(new Label("commit").setVisible(false));
+ }
+ }
+ if (count % 2 == 0) {
+ WicketUtils.setCssClass(item, "even");
+ } else {
+ WicketUtils.setCssClass(item, "odd");
+ }
+ }
+ };
+ add(blameView);
+ }
+
+ private String getInitials(String author) {
+ StringBuilder sb = new StringBuilder();
+ String[] chunks = author.split(" ");
+ for (String chunk : chunks) {
+ sb.append(chunk.charAt(0));
+ }
+ return sb.toString().toUpperCase();
+ }
+
+ @Override
+ protected String getPageName() {
+ return getString("gb.blame");
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/pages/BlobDiffPage.html b/src/main/java/com/gitblit/wicket/pages/BlobDiffPage.html
new file mode 100644
index 00000000..c6336429
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/BlobDiffPage.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+
+<body>
+<wicket:extend>
+
+ <!-- blob nav links -->
+ <div class="page_nav2">
+ <a wicket:id="blameLink"><wicket:message key="gb.blame"></wicket:message></a> | <a wicket:id="historyLink"><wicket:message key="gb.history"></wicket:message></a> | <a wicket:id="patchLink"><wicket:message key="gb.patch"></wicket:message></a> | <a wicket:id="commitLink"><wicket:message key="gb.commit"></wicket:message></a> | <a wicket:id="commitDiffLink"><wicket:message key="gb.commitdiff"></wicket:message></a>
+ </div>
+
+ <!-- commit header -->
+ <div wicket:id="commitHeader">[commit header]</div>
+
+ <!-- breadcrumbs -->
+ <div wicket:id="breadcrumbs">[breadcrumbs]</div>
+
+ <!-- diff content -->
+ <pre wicket:id="diffText">[diff text]</pre>
+
+</wicket:extend>
+</body>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/BlobDiffPage.java b/src/main/java/com/gitblit/wicket/pages/BlobDiffPage.java
new file mode 100644
index 00000000..d86d2e63
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/BlobDiffPage.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.pages;
+
+import org.apache.wicket.PageParameters;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.link.BookmarkablePageLink;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+
+import com.gitblit.GitBlit;
+import com.gitblit.Keys;
+import com.gitblit.utils.DiffUtils;
+import com.gitblit.utils.DiffUtils.DiffOutputType;
+import com.gitblit.utils.JGitUtils;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.wicket.WicketUtils;
+import com.gitblit.wicket.panels.CommitHeaderPanel;
+import com.gitblit.wicket.panels.PathBreadcrumbsPanel;
+
+public class BlobDiffPage extends RepositoryPage {
+
+ public BlobDiffPage(PageParameters params) {
+ super(params);
+
+ final String blobPath = WicketUtils.getPath(params);
+ final String baseObjectId = WicketUtils.getBaseObjectId(params);
+
+ Repository r = getRepository();
+ RevCommit commit = getCommit();
+
+ DiffOutputType diffType = DiffOutputType.forName(GitBlit.getString(Keys.web.diffStyle,
+ DiffOutputType.GITBLIT.name()));
+
+ String diff;
+ if (StringUtils.isEmpty(baseObjectId)) {
+ // use first parent
+ diff = DiffUtils.getDiff(r, commit, blobPath, diffType);
+ add(new BookmarkablePageLink<Void>("patchLink", PatchPage.class,
+ WicketUtils.newPathParameter(repositoryName, objectId, blobPath)));
+ } else {
+ // base commit specified
+ RevCommit baseCommit = JGitUtils.getCommit(r, baseObjectId);
+ diff = DiffUtils.getDiff(r, baseCommit, commit, blobPath, diffType);
+ add(new BookmarkablePageLink<Void>("patchLink", PatchPage.class,
+ WicketUtils.newBlobDiffParameter(repositoryName, baseObjectId, objectId,
+ blobPath)));
+ }
+
+ add(new BookmarkablePageLink<Void>("commitLink", CommitPage.class,
+ WicketUtils.newObjectParameter(repositoryName, objectId)));
+ add(new BookmarkablePageLink<Void>("commitDiffLink", CommitDiffPage.class,
+ WicketUtils.newObjectParameter(repositoryName, objectId)));
+
+ // diff page links
+ add(new BookmarkablePageLink<Void>("blameLink", BlamePage.class,
+ WicketUtils.newPathParameter(repositoryName, objectId, blobPath)));
+ add(new BookmarkablePageLink<Void>("historyLink", HistoryPage.class,
+ WicketUtils.newPathParameter(repositoryName, objectId, blobPath)));
+
+ add(new CommitHeaderPanel("commitHeader", repositoryName, commit));
+
+ add(new PathBreadcrumbsPanel("breadcrumbs", repositoryName, blobPath, objectId));
+
+ add(new Label("diffText", diff).setEscapeModelStrings(false));
+ }
+
+ @Override
+ protected String getPageName() {
+ return getString("gb.diff");
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/pages/BlobPage.html b/src/main/java/com/gitblit/wicket/pages/BlobPage.html
new file mode 100644
index 00000000..80f061fd
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/BlobPage.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+
+<!-- contribute google-code-prettify resources to the page header -->
+<wicket:head>
+ <wicket:link>
+ <link href="prettify/prettify.css" type="text/css" rel="stylesheet" />
+ <script type="text/javascript" src="prettify/prettify.js"></script>
+ </wicket:link>
+</wicket:head>
+
+<wicket:extend>
+<!-- need to specify body.onload -->
+<body onload="prettyPrint()">
+
+ <!-- blob nav links -->
+ <div class="page_nav2">
+ <a wicket:id="blameLink"><wicket:message key="gb.blame"></wicket:message></a> | <a wicket:id="historyLink"><wicket:message key="gb.history"></wicket:message></a> | <a wicket:id="rawLink"><wicket:message key="gb.raw"></wicket:message></a> | <a wicket:id="headLink"><wicket:message key="gb.head"></wicket:message></a>
+ </div>
+
+ <!-- commit header -->
+ <div wicket:id="commitHeader">[commit header]</div>
+
+ <!-- breadcrumbs -->
+ <div wicket:id="breadcrumbs">[breadcrumbs]</div>
+
+ <!-- blob content -->
+ <pre style="border:0px;" wicket:id="blobText">[blob content]</pre>
+
+ <!-- blob image -->
+ <img wicket:id="blobImage" style="padding-top:5px;"></img>
+
+</body>
+</wicket:extend>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/BlobPage.java b/src/main/java/com/gitblit/wicket/pages/BlobPage.java
new file mode 100644
index 00000000..e2b8546b
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/BlobPage.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.pages;
+
+import java.text.MessageFormat;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.wicket.Component;
+import org.apache.wicket.PageParameters;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.image.Image;
+import org.apache.wicket.markup.html.link.BookmarkablePageLink;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+
+import com.gitblit.GitBlit;
+import com.gitblit.Keys;
+import com.gitblit.utils.JGitUtils;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.wicket.ExternalImage;
+import com.gitblit.wicket.WicketUtils;
+import com.gitblit.wicket.panels.CommitHeaderPanel;
+import com.gitblit.wicket.panels.PathBreadcrumbsPanel;
+
+public class BlobPage extends RepositoryPage {
+
+ public BlobPage(PageParameters params) {
+ super(params);
+
+ Repository r = getRepository();
+ final String blobPath = WicketUtils.getPath(params);
+ String [] encodings = GitBlit.getEncodings();
+
+ if (StringUtils.isEmpty(blobPath)) {
+ // blob by objectid
+
+ add(new BookmarkablePageLink<Void>("blameLink", BlamePage.class,
+ WicketUtils.newPathParameter(repositoryName, objectId, blobPath))
+ .setEnabled(false));
+ add(new BookmarkablePageLink<Void>("historyLink", HistoryPage.class).setEnabled(false));
+ add(new BookmarkablePageLink<Void>("rawLink", RawPage.class,
+ WicketUtils.newPathParameter(repositoryName, objectId, blobPath)));
+ add(new BookmarkablePageLink<Void>("headLink", BlobPage.class).setEnabled(false));
+ add(new CommitHeaderPanel("commitHeader", objectId));
+ add(new PathBreadcrumbsPanel("breadcrumbs", repositoryName, blobPath, objectId));
+ Component c = new Label("blobText", JGitUtils.getStringContent(r, objectId, encodings));
+ WicketUtils.setCssClass(c, "plainprint");
+ add(c);
+ } else {
+ // standard blob view
+ String extension = null;
+ if (blobPath.lastIndexOf('.') > -1) {
+ extension = blobPath.substring(blobPath.lastIndexOf('.') + 1).toLowerCase();
+ }
+
+ // see if we should redirect to the markdown page
+ for (String ext : GitBlit.getStrings(Keys.web.markdownExtensions)) {
+ if (ext.equals(extension)) {
+ setResponsePage(MarkdownPage.class, params);
+ return;
+ }
+ }
+
+ // manually get commit because it can be null
+ RevCommit commit = JGitUtils.getCommit(r, objectId);
+
+ // blob page links
+ add(new BookmarkablePageLink<Void>("blameLink", BlamePage.class,
+ WicketUtils.newPathParameter(repositoryName, objectId, blobPath)));
+ add(new BookmarkablePageLink<Void>("historyLink", HistoryPage.class,
+ WicketUtils.newPathParameter(repositoryName, objectId, blobPath)));
+ add(new BookmarkablePageLink<Void>("rawLink", RawPage.class,
+ WicketUtils.newPathParameter(repositoryName, objectId, blobPath)));
+ add(new BookmarkablePageLink<Void>("headLink", BlobPage.class,
+ WicketUtils.newPathParameter(repositoryName, Constants.HEAD, blobPath)));
+
+ add(new CommitHeaderPanel("commitHeader", repositoryName, commit));
+
+ add(new PathBreadcrumbsPanel("breadcrumbs", repositoryName, blobPath, objectId));
+
+ // Map the extensions to types
+ Map<String, Integer> map = new HashMap<String, Integer>();
+ for (String ext : GitBlit.getStrings(Keys.web.prettyPrintExtensions)) {
+ map.put(ext.toLowerCase(), 1);
+ }
+ for (String ext : GitBlit.getStrings(Keys.web.imageExtensions)) {
+ map.put(ext.toLowerCase(), 2);
+ }
+ for (String ext : GitBlit.getStrings(Keys.web.binaryExtensions)) {
+ map.put(ext.toLowerCase(), 3);
+ }
+
+ if (extension != null) {
+ int type = 0;
+ if (map.containsKey(extension)) {
+ type = map.get(extension);
+ }
+ switch (type) {
+ case 2:
+ // image blobs
+ add(new Label("blobText").setVisible(false));
+ add(new ExternalImage("blobImage", urlFor(RawPage.class, WicketUtils.newPathParameter(repositoryName, objectId, blobPath)).toString()));
+ break;
+ case 3:
+ // binary blobs
+ add(new Label("blobText", "Binary File"));
+ add(new Image("blobImage").setVisible(false));
+ break;
+ default:
+ // plain text
+ String source = JGitUtils.getStringContent(r, commit.getTree(), blobPath, encodings);
+ String table = generateSourceView(source, type == 1);
+ add(new Label("blobText", table).setEscapeModelStrings(false));
+ add(new Image("blobImage").setVisible(false));
+ }
+ } else {
+ // plain text
+ String source = JGitUtils.getStringContent(r, commit.getTree(), blobPath, encodings);
+ String table = generateSourceView(source, false);
+ add(new Label("blobText", table).setEscapeModelStrings(false));
+ add(new Image("blobImage").setVisible(false));
+ }
+ }
+ }
+
+ protected String generateSourceView(String source, boolean prettyPrint) {
+ String [] lines = source.split("\n");
+
+ StringBuilder sb = new StringBuilder();
+ sb.append("<!-- start blob table -->");
+ sb.append("<table width=\"100%\"><tbody><tr>");
+
+ // nums column
+ sb.append("<!-- start nums column -->");
+ sb.append("<td id=\"nums\">");
+ sb.append("<pre>");
+ String numPattern = "<span id=\"L{0}\" class=\"num\">{0}</span>\n";
+ for (int i = 0; i < lines.length; i++) {
+ sb.append(MessageFormat.format(numPattern, "" + (i + 1)));
+ }
+ sb.append("</pre>");
+ sb.append("<!-- end nums column -->");
+ sb.append("</td>");
+
+ sb.append("<!-- start lines column -->");
+ sb.append("<td id=\"lines\">");
+ sb.append("<div class=\"sourceview\">");
+ if (prettyPrint) {
+ sb.append("<pre class=\"prettyprint\">");
+ } else {
+ sb.append("<pre class=\"plainprint\">");
+ }
+ lines = StringUtils.escapeForHtml(source, true).split("\n");
+
+ sb.append("<table width=\"100%\"><tbody>");
+
+ String linePattern = "<tr class=\"{0}\"><td><a href=\"#L{2}\">{1}</a>\r</tr>";
+ for (int i = 0; i < lines.length; i++) {
+ String line = lines[i].replace('\r', ' ');
+ String cssClass = (i % 2 == 0) ? "even" : "odd";
+ sb.append(MessageFormat.format(linePattern, cssClass, line, "" + (i + 1)));
+ }
+ sb.append("</tbody></table></pre>");
+ sb.append("</pre>");
+ sb.append("</div>");
+ sb.append("</td>");
+ sb.append("<!-- end lines column -->");
+
+ sb.append("</tr></tbody></table>");
+ sb.append("<!-- end blob table -->");
+
+ return sb.toString();
+ }
+
+ @Override
+ protected String getPageName() {
+ return getString("gb.view");
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/pages/BranchesPage.html b/src/main/java/com/gitblit/wicket/pages/BranchesPage.html
new file mode 100644
index 00000000..65fd9b9d
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/BranchesPage.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+
+<body>
+<wicket:extend>
+
+ <!-- branches -->
+ <div style="margin-top:5px;" wicket:id="branchesPanel">[branches panel]</div>
+
+</wicket:extend>
+</body>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/BranchesPage.java b/src/main/java/com/gitblit/wicket/pages/BranchesPage.java
new file mode 100644
index 00000000..8684fb3d
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/BranchesPage.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.pages;
+
+import org.apache.wicket.PageParameters;
+
+import com.gitblit.wicket.panels.BranchesPanel;
+
+public class BranchesPage extends RepositoryPage {
+
+ public BranchesPage(PageParameters params) {
+ super(params);
+
+ add(new BranchesPanel("branchesPanel", getRepositoryModel(), getRepository(), -1, isShowAdmin() || isOwner()));
+ }
+
+ @Override
+ protected String getPageName() {
+ return getString("gb.branches");
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/pages/ChangePasswordPage.html b/src/main/java/com/gitblit/wicket/pages/ChangePasswordPage.html
new file mode 100644
index 00000000..36439a91
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/ChangePasswordPage.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+
+ <wicket:extend>
+ <body onload="document.getElementById('password').focus();">
+ <div>
+ <form style="text-align:center;" wicket:id="passwordForm">
+ <center>
+ <table class="plain">
+ <tr>
+ <th><wicket:message key="gb.password"></wicket:message> &nbsp;</th>
+ <td class="edit"><input type="password" wicket:id="password" id="password" size="30" tabindex="1" /></td>
+ </tr>
+ <tr>
+ <th><wicket:message key="gb.confirmPassword"></wicket:message> &nbsp;</th>
+ <td class="edit"><input type="password" wicket:id="confirmPassword" size="30" tabindex="2" /></td>
+ </tr>
+ </table>
+ <div class="form-actions">
+ <input class="btn btn-primary" type="submit" wicket:message="value:gb.save" wicket:id="save" tabindex="3" />
+ <input class="btn" type="submit" wicket:message="value:gb.cancel" wicket:id="cancel" tabindex="4" />
+ </div>
+ </center>
+ </form>
+ </div>
+ </body>
+ </wicket:extend>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/ChangePasswordPage.java b/src/main/java/com/gitblit/wicket/pages/ChangePasswordPage.java
new file mode 100644
index 00000000..c4014208
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/ChangePasswordPage.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.pages;
+
+import java.text.MessageFormat;
+
+import org.apache.wicket.RestartResponseException;
+import org.apache.wicket.markup.html.form.Button;
+import org.apache.wicket.markup.html.form.PasswordTextField;
+import org.apache.wicket.markup.html.form.StatelessForm;
+import org.apache.wicket.model.IModel;
+import org.apache.wicket.model.Model;
+import org.apache.wicket.protocol.http.WebResponse;
+
+import com.gitblit.GitBlit;
+import com.gitblit.GitBlitException;
+import com.gitblit.Keys;
+import com.gitblit.models.UserModel;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.wicket.GitBlitWebSession;
+
+public class ChangePasswordPage extends RootSubPage {
+
+ IModel<String> password = new Model<String>("");
+ IModel<String> confirmPassword = new Model<String>("");
+
+ public ChangePasswordPage() {
+ super();
+
+ if (!GitBlitWebSession.get().isLoggedIn()) {
+ // Change password requires a login
+ throw new RestartResponseException(getApplication().getHomePage());
+ }
+
+ if (!GitBlit.getBoolean(Keys.web.authenticateAdminPages, true)
+ && !GitBlit.getBoolean(Keys.web.authenticateViewPages, false)) {
+ // no authentication enabled
+ throw new RestartResponseException(getApplication().getHomePage());
+ }
+
+ UserModel user = GitBlitWebSession.get().getUser();
+ if (!GitBlit.self().supportsCredentialChanges(user)) {
+ error(MessageFormat.format(getString("gb.userServiceDoesNotPermitPasswordChanges"),
+ GitBlit.getString(Keys.realm.userService, "${baseFolder}/users.conf")), true);
+ }
+
+ setupPage(getString("gb.changePassword"), user.username);
+
+ StatelessForm<Void> form = new StatelessForm<Void>("passwordForm") {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public void onSubmit() {
+ String password = ChangePasswordPage.this.password.getObject();
+ String confirmPassword = ChangePasswordPage.this.confirmPassword.getObject();
+ // ensure passwords match
+ if (!password.equals(confirmPassword)) {
+ error(getString("gb.passwordsDoNotMatch"));
+ return;
+ }
+
+ // ensure password satisfies minimum length requirement
+ int minLength = GitBlit.getInteger(Keys.realm.minPasswordLength, 5);
+ if (minLength < 4) {
+ minLength = 4;
+ }
+ if (password.length() < minLength) {
+ error(MessageFormat.format(getString("gb.passwordTooShort"), minLength));
+ return;
+ }
+
+ UserModel user = GitBlitWebSession.get().getUser();
+
+ // convert to MD5 digest, if appropriate
+ String type = GitBlit.getString(Keys.realm.passwordStorage, "md5");
+ if (type.equalsIgnoreCase("md5")) {
+ // store MD5 digest of password
+ password = StringUtils.MD5_TYPE + StringUtils.getMD5(password);
+ } else if (type.equalsIgnoreCase("combined-md5")) {
+ // store MD5 digest of username+password
+ password = StringUtils.COMBINED_MD5_TYPE
+ + StringUtils.getMD5(user.username.toLowerCase() + password);
+ }
+
+ user.password = password;
+ try {
+ GitBlit.self().updateUserModel(user.username, user, false);
+ if (GitBlit.getBoolean(Keys.web.allowCookieAuthentication, false)) {
+ WebResponse response = (WebResponse) getRequestCycle().getResponse();
+ GitBlit.self().setCookie(response, user);
+ }
+ } catch (GitBlitException e) {
+ error(e.getMessage());
+ return;
+ }
+ setRedirect(false);
+ info(getString("gb.passwordChanged"));
+ setResponsePage(RepositoriesPage.class);
+ }
+ };
+ PasswordTextField passwordField = new PasswordTextField("password", password);
+ passwordField.setResetPassword(false);
+ form.add(passwordField);
+ PasswordTextField confirmPasswordField = new PasswordTextField("confirmPassword",
+ confirmPassword);
+ confirmPasswordField.setResetPassword(false);
+ form.add(confirmPasswordField);
+
+ form.add(new Button("save"));
+ Button cancel = new Button("cancel") {
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public void onSubmit() {
+ setRedirect(false);
+ error(getString("gb.passwordChangeAborted"));
+ setResponsePage(RepositoriesPage.class);
+ }
+ };
+ cancel.setDefaultFormProcessing(false);
+ form.add(cancel);
+
+ add(form);
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/pages/CommitDiffPage.html b/src/main/java/com/gitblit/wicket/pages/CommitDiffPage.html
new file mode 100644
index 00000000..de39f4ca
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/CommitDiffPage.html
@@ -0,0 +1,43 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+
+<body>
+<wicket:extend>
+
+ <!-- commitdiff nav links -->
+ <div class="page_nav2">
+ <wicket:message key="gb.parent"></wicket:message>: <span wicket:id="parentLink">[parent link]</span> | <a wicket:id="patchLink"><wicket:message key="gb.patch"></wicket:message></a> | <a wicket:id="commitLink"><wicket:message key="gb.commit"></wicket:message></a>
+ </div>
+
+ <!-- commit header -->
+ <div wicket:id="commitHeader">[commit header]</div>
+
+ <!-- changed paths -->
+ <div style="padding-top:15px;">
+ <!-- commit legend -->
+ <div class="hidden-phone" style="text-align:right;" wicket:id="commitLegend"></div>
+
+ <div class="header"><i class="icon-file"></i> <wicket:message key="gb.changedFiles">[changed files]</wicket:message></div>
+ </div>
+
+ <table class="pretty">
+ <tr wicket:id="changedPath">
+ <td class="changeType"><span wicket:id="changeType">[change type]</span></td>
+ <td class="path"><span wicket:id="pathName">[commit path]</span></td>
+ <td class="hidden-phone rightAlign">
+ <span class="link">
+ <a wicket:id="patch"><wicket:message key="gb.patch"></wicket:message></a> | <a wicket:id="view"><wicket:message key="gb.view"></wicket:message></a> | <a wicket:id="blame"><wicket:message key="gb.blame"></wicket:message></a> | <a wicket:id="history"><wicket:message key="gb.history"></wicket:message></a>
+ </span>
+ </td>
+ </tr>
+ </table>
+
+ <!-- diff content -->
+ <pre style="padding-top:10px;" wicket:id="diffText">[diff text]</pre>
+
+</wicket:extend>
+</body>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/CommitDiffPage.java b/src/main/java/com/gitblit/wicket/pages/CommitDiffPage.java
new file mode 100644
index 00000000..3ad70742
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/CommitDiffPage.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.pages;
+
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.wicket.PageParameters;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.link.BookmarkablePageLink;
+import org.apache.wicket.markup.html.link.ExternalLink;
+import org.apache.wicket.markup.repeater.Item;
+import org.apache.wicket.markup.repeater.data.DataView;
+import org.apache.wicket.markup.repeater.data.ListDataProvider;
+import org.eclipse.jgit.diff.DiffEntry.ChangeType;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+
+import com.gitblit.GitBlit;
+import com.gitblit.Keys;
+import com.gitblit.models.PathModel.PathChangeModel;
+import com.gitblit.models.SubmoduleModel;
+import com.gitblit.utils.DiffUtils;
+import com.gitblit.utils.DiffUtils.DiffOutputType;
+import com.gitblit.utils.JGitUtils;
+import com.gitblit.wicket.WicketUtils;
+import com.gitblit.wicket.panels.CommitHeaderPanel;
+import com.gitblit.wicket.panels.CommitLegendPanel;
+import com.gitblit.wicket.panels.LinkPanel;
+
+public class CommitDiffPage extends RepositoryPage {
+
+ public CommitDiffPage(PageParameters params) {
+ super(params);
+
+ Repository r = getRepository();
+
+ DiffOutputType diffType = DiffOutputType.forName(GitBlit.getString(Keys.web.diffStyle,
+ DiffOutputType.GITBLIT.name()));
+
+ RevCommit commit = null, otherCommit = null;
+
+ if( objectId.contains("..") )
+ {
+ String[] parts = objectId.split("\\.\\.");
+ commit = getCommit(r, parts[0]);
+ otherCommit = getCommit(r, parts[1]);
+ }
+ else
+ {
+ commit = getCommit();
+ }
+
+ String diff;
+
+ if(otherCommit == null)
+ {
+ diff = DiffUtils.getCommitDiff(r, commit, diffType);
+ }
+ else
+ {
+ diff = DiffUtils.getDiff(r, commit, otherCommit, diffType);
+ }
+
+ List<String> parents = new ArrayList<String>();
+ if (commit.getParentCount() > 0) {
+ for (RevCommit parent : commit.getParents()) {
+ parents.add(parent.name());
+ }
+ }
+
+ // commit page links
+ if (parents.size() == 0) {
+ add(new Label("parentLink", getString("gb.none")));
+ } else {
+ add(new LinkPanel("parentLink", null, parents.get(0).substring(0, 8),
+ CommitDiffPage.class, newCommitParameter(parents.get(0))));
+ }
+ add(new BookmarkablePageLink<Void>("patchLink", PatchPage.class,
+ WicketUtils.newObjectParameter(repositoryName, objectId)));
+ add(new BookmarkablePageLink<Void>("commitLink", CommitPage.class,
+ WicketUtils.newObjectParameter(repositoryName, objectId)));
+
+ add(new CommitHeaderPanel("commitHeader", repositoryName, commit));
+
+ // changed paths list
+ List<PathChangeModel> paths;
+
+ if( otherCommit == null )
+ {
+ paths = JGitUtils.getFilesInCommit(r, commit);
+ }
+ else
+ {
+ paths = JGitUtils.getFilesInCommit(r, otherCommit);
+ }
+
+ add(new CommitLegendPanel("commitLegend", paths));
+ ListDataProvider<PathChangeModel> pathsDp = new ListDataProvider<PathChangeModel>(paths);
+ DataView<PathChangeModel> pathsView = new DataView<PathChangeModel>("changedPath", pathsDp) {
+ private static final long serialVersionUID = 1L;
+ int counter;
+
+ public void populateItem(final Item<PathChangeModel> item) {
+ final PathChangeModel entry = item.getModelObject();
+ Label changeType = new Label("changeType", "");
+ WicketUtils.setChangeTypeCssClass(changeType, entry.changeType);
+ setChangeTypeTooltip(changeType, entry.changeType);
+ item.add(changeType);
+
+ boolean hasSubmodule = false;
+ String submodulePath = null;
+ if (entry.isTree()) {
+ // tree
+ item.add(new LinkPanel("pathName", null, entry.path, TreePage.class,
+ WicketUtils
+ .newPathParameter(repositoryName, entry.commitId, entry.path)));
+ } else if (entry.isSubmodule()) {
+ // submodule
+ String submoduleId = entry.objectId;
+ SubmoduleModel submodule = getSubmodule(entry.path);
+ submodulePath = submodule.gitblitPath;
+ hasSubmodule = submodule.hasSubmodule;
+
+ item.add(new LinkPanel("pathName", "list", entry.path + " @ " +
+ getShortObjectId(submoduleId), TreePage.class,
+ WicketUtils
+ .newPathParameter(submodulePath, submoduleId, "")).setEnabled(hasSubmodule));
+ } else {
+ // blob
+ item.add(new LinkPanel("pathName", "list", entry.path, BlobPage.class,
+ WicketUtils
+ .newPathParameter(repositoryName, entry.commitId, entry.path)));
+ }
+
+ // quick links
+ if (entry.isSubmodule()) {
+ // submodule
+ item.add(new ExternalLink("patch", "").setEnabled(false));
+ item.add(new BookmarkablePageLink<Void>("view", CommitPage.class, WicketUtils
+ .newObjectParameter(submodulePath, entry.objectId)).setEnabled(hasSubmodule));
+ item.add(new ExternalLink("blame", "").setEnabled(false));
+ item.add(new BookmarkablePageLink<Void>("history", HistoryPage.class, WicketUtils
+ .newPathParameter(repositoryName, entry.commitId, entry.path)));
+ } else {
+ // tree or blob
+ item.add(new BookmarkablePageLink<Void>("patch", PatchPage.class, WicketUtils
+ .newPathParameter(repositoryName, entry.commitId, entry.path)));
+ item.add(new BookmarkablePageLink<Void>("view", BlobPage.class, WicketUtils
+ .newPathParameter(repositoryName, entry.commitId, entry.path)));
+ item.add(new BookmarkablePageLink<Void>("blame", BlamePage.class, WicketUtils
+ .newPathParameter(repositoryName, entry.commitId, entry.path)));
+ item.add(new BookmarkablePageLink<Void>("history", HistoryPage.class, WicketUtils
+ .newPathParameter(repositoryName, entry.commitId, entry.path))
+ .setEnabled(!entry.changeType.equals(ChangeType.ADD)));
+ }
+ WicketUtils.setAlternatingBackground(item, counter);
+ counter++;
+ }
+ };
+ add(pathsView);
+ add(new Label("diffText", diff).setEscapeModelStrings(false));
+ }
+
+ @Override
+ protected String getPageName() {
+ return getString("gb.commitdiff");
+ }
+
+ private RevCommit getCommit(Repository r, String rev)
+ {
+ RevCommit otherCommit = JGitUtils.getCommit(r, rev);
+ if (otherCommit == null) {
+ error(MessageFormat.format(getString("gb.failedToFindCommit"), rev, repositoryName, getPageName()), true);
+ }
+ return otherCommit;
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/pages/CommitPage.html b/src/main/java/com/gitblit/wicket/pages/CommitPage.html
new file mode 100644
index 00000000..79a038c9
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/CommitPage.html
@@ -0,0 +1,99 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+
+<body>
+<wicket:extend>
+
+ <!-- commit nav links -->
+ <div class="page_nav2">
+ <wicket:message key="gb.parent"></wicket:message>: <span wicket:id="parentLink">[parent link]</span> | <a wicket:id="patchLink"><wicket:message key="gb.patch"></wicket:message></a> | <span wicket:id="commitdiffLink">[commitdiff link]</span>
+ </div>
+
+ <!-- commit header -->
+ <div wicket:id="commitHeader">[commit header]</div>
+
+ <div class="row">
+
+
+ <div class="span10">
+ <!-- commit info -->
+ <table class="plain">
+ <tr><th><wicket:message key="gb.refs">refs</wicket:message></th><td><div wicket:id="refsPanel">[references]</div></td></tr>
+ <tr><th><wicket:message key="gb.author">author</wicket:message></th><td><span class="sha1" wicket:id="commitAuthor">[author</span></td></tr>
+ <tr><th></th><td><span class="sha1" wicket:id="commitAuthorDate">[author date]</span></td></tr>
+ <tr><th><wicket:message key="gb.committer">committer</wicket:message></th><td><span class="sha1" wicket:id="commitCommitter">[committer]</span></td></tr>
+ <tr><th></th><td><span class="sha1" wicket:id="commitCommitterDate">[commit date]</span></td></tr>
+ <tr class="hidden-phone"><th><wicket:message key="gb.commit">commit</wicket:message></th><td><span class="sha1" wicket:id="commitId">[commit id]</span></td></tr>
+ <tr class="hidden-phone"><th><wicket:message key="gb.tree">tree</wicket:message></th>
+ <td><span class="sha1" wicket:id="commitTree">[commit tree]</span>
+ <span class="link">
+ <a wicket:id="treeLink"><wicket:message key="gb.tree"></wicket:message></a> | <span wicket:id="compressedLinks"></span>
+ </span>
+ </td></tr>
+ <tr class="hidden-phone"><th valign="top"><wicket:message key="gb.parent">parent</wicket:message></th>
+ <td>
+ <span wicket:id="commitParents">
+ <span class="sha1" wicket:id="commitParent">[commit parents]</span>
+ <span class="link">
+ <a wicket:id="view"><wicket:message key="gb.view"></wicket:message></a> | <a wicket:id="diff"><wicket:message key="gb.diff"></wicket:message></a>
+ </span><br/>
+ </span>
+ </td>
+ </tr>
+ </table>
+ </div>
+
+ </div>
+
+ <!-- full message -->
+ <div class="commit_message" wicket:id="fullMessage">[commit message]</div>
+
+ <!-- git notes -->
+ <table class="gitnotes">
+ <tr wicket:id="notes">
+ <td class="info">
+ <table>
+ <tr><td><span wicket:id="refName"></span></td></tr>
+ <tr><td><span class="sha1" wicket:id="authorName"></span></td></tr>
+ <tr><td><span class="sha1" wicket:id="authorDate"></span></td></tr>
+ </table>
+ <!-- Note Author Gravatar -->
+ <span style="vertical-align: top;" wicket:id="noteAuthorAvatar" />
+ </td>
+ <td class="message"><span class="sha1" wicket:id="noteContent"></span></td>
+ </tr>
+ </table>
+
+ <!-- commit legend -->
+ <div class="hidden-phone" style="text-align:right;" wicket:id="commitLegend"></div>
+
+ <!-- header -->
+ <div class="header"><i class="icon-file"></i> <wicket:message key="gb.changedFiles">[changed files]</wicket:message></div>
+
+ <!-- changed paths -->
+ <table class="pretty">
+ <tr wicket:id="changedPath">
+ <td class="changeType"><span wicket:id="changeType">[change type]</span></td>
+ <td class="path"><span wicket:id="pathName">[commit path]</span></td>
+ <td class="hidden-phone rightAlign">
+ <span class="link">
+ <a wicket:id="diff"><wicket:message key="gb.diff"></wicket:message></a> | <a wicket:id="view"><wicket:message key="gb.view"></wicket:message></a> | <a wicket:id="blame"><wicket:message key="gb.blame"></wicket:message></a> | <a wicket:id="history"><wicket:message key="gb.history"></wicket:message></a>
+ </span>
+ </td>
+ </tr>
+ </table>
+
+ <wicket:fragment wicket:id="fullPersonIdent">
+ <span wicket:id="personName"></span><span wicket:id="personAddress"></span>
+ </wicket:fragment>
+
+ <wicket:fragment wicket:id="partialPersonIdent">
+ <span wicket:id="personName"></span>
+ </wicket:fragment>
+
+</wicket:extend>
+</body>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/CommitPage.java b/src/main/java/com/gitblit/wicket/pages/CommitPage.java
new file mode 100644
index 00000000..c5a24c8d
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/CommitPage.java
@@ -0,0 +1,223 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.pages;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.wicket.PageParameters;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.link.BookmarkablePageLink;
+import org.apache.wicket.markup.html.link.ExternalLink;
+import org.apache.wicket.markup.repeater.Item;
+import org.apache.wicket.markup.repeater.data.DataView;
+import org.apache.wicket.markup.repeater.data.ListDataProvider;
+import org.apache.wicket.model.StringResourceModel;
+import org.eclipse.jgit.diff.DiffEntry.ChangeType;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+
+import com.gitblit.Constants;
+import com.gitblit.GitBlit;
+import com.gitblit.models.GitNote;
+import com.gitblit.models.PathModel.PathChangeModel;
+import com.gitblit.models.SubmoduleModel;
+import com.gitblit.utils.JGitUtils;
+import com.gitblit.wicket.WicketUtils;
+import com.gitblit.wicket.panels.CommitHeaderPanel;
+import com.gitblit.wicket.panels.CommitLegendPanel;
+import com.gitblit.wicket.panels.CompressedDownloadsPanel;
+import com.gitblit.wicket.panels.GravatarImage;
+import com.gitblit.wicket.panels.LinkPanel;
+import com.gitblit.wicket.panels.RefsPanel;
+
+public class CommitPage extends RepositoryPage {
+
+ public CommitPage(PageParameters params) {
+ super(params);
+
+ Repository r = getRepository();
+ RevCommit c = getCommit();
+
+ List<String> parents = new ArrayList<String>();
+ if (c.getParentCount() > 0) {
+ for (RevCommit parent : c.getParents()) {
+ parents.add(parent.name());
+ }
+ }
+
+ // commit page links
+ if (parents.size() == 0) {
+ add(new Label("parentLink", "none"));
+ add(new Label("commitdiffLink", getString("gb.commitdiff")));
+ } else {
+ add(new LinkPanel("parentLink", null, getShortObjectId(parents.get(0)),
+ CommitPage.class, newCommitParameter(parents.get(0))));
+ add(new LinkPanel("commitdiffLink", null, new StringResourceModel("gb.commitdiff",
+ this, null), CommitDiffPage.class, WicketUtils.newObjectParameter(
+ repositoryName, objectId)));
+ }
+ add(new BookmarkablePageLink<Void>("patchLink", PatchPage.class,
+ WicketUtils.newObjectParameter(repositoryName, objectId)));
+
+ add(new CommitHeaderPanel("commitHeader", repositoryName, c));
+
+ addRefs(r, c);
+
+ // author
+ add(createPersonPanel("commitAuthor", c.getAuthorIdent(), Constants.SearchType.AUTHOR));
+ add(WicketUtils.createTimestampLabel("commitAuthorDate", c.getAuthorIdent().getWhen(),
+ getTimeZone(), getTimeUtils()));
+
+ // committer
+ add(createPersonPanel("commitCommitter", c.getCommitterIdent(), Constants.SearchType.COMMITTER));
+ add(WicketUtils.createTimestampLabel("commitCommitterDate",
+ c.getCommitterIdent().getWhen(), getTimeZone(), getTimeUtils()));
+
+ add(new Label("commitId", c.getName()));
+
+ add(new LinkPanel("commitTree", "list", c.getTree().getName(), TreePage.class,
+ newCommitParameter()));
+ add(new BookmarkablePageLink<Void>("treeLink", TreePage.class, newCommitParameter()));
+ final String baseUrl = WicketUtils.getGitblitURL(getRequest());
+
+ add(new CompressedDownloadsPanel("compressedLinks", baseUrl, repositoryName, objectId, null));
+
+ // Parent Commits
+ ListDataProvider<String> parentsDp = new ListDataProvider<String>(parents);
+ DataView<String> parentsView = new DataView<String>("commitParents", parentsDp) {
+ private static final long serialVersionUID = 1L;
+
+ public void populateItem(final Item<String> item) {
+ String entry = item.getModelObject();
+ item.add(new LinkPanel("commitParent", "list", entry, CommitPage.class,
+ newCommitParameter(entry)));
+ item.add(new BookmarkablePageLink<Void>("view", CommitPage.class,
+ newCommitParameter(entry)));
+ item.add(new BookmarkablePageLink<Void>("diff", CommitDiffPage.class,
+ newCommitParameter(entry)));
+ }
+ };
+ add(parentsView);
+
+ addFullText("fullMessage", c.getFullMessage(), true);
+
+ // git notes
+ List<GitNote> notes = JGitUtils.getNotesOnCommit(r, c);
+ ListDataProvider<GitNote> notesDp = new ListDataProvider<GitNote>(notes);
+ DataView<GitNote> notesView = new DataView<GitNote>("notes", notesDp) {
+ private static final long serialVersionUID = 1L;
+
+ public void populateItem(final Item<GitNote> item) {
+ GitNote entry = item.getModelObject();
+ item.add(new RefsPanel("refName", repositoryName, Arrays.asList(entry.notesRef)));
+ item.add(createPersonPanel("authorName", entry.notesRef.getAuthorIdent(),
+ Constants.SearchType.AUTHOR));
+ item.add(new GravatarImage("noteAuthorAvatar", entry.notesRef.getAuthorIdent()));
+ item.add(WicketUtils.createTimestampLabel("authorDate", entry.notesRef
+ .getAuthorIdent().getWhen(), getTimeZone(), getTimeUtils()));
+ item.add(new Label("noteContent", GitBlit.self().processCommitMessage(
+ repositoryName, entry.content)).setEscapeModelStrings(false));
+ }
+ };
+ add(notesView.setVisible(notes.size() > 0));
+
+ // changed paths list
+ List<PathChangeModel> paths = JGitUtils.getFilesInCommit(r, c);
+ add(new CommitLegendPanel("commitLegend", paths));
+ ListDataProvider<PathChangeModel> pathsDp = new ListDataProvider<PathChangeModel>(paths);
+ DataView<PathChangeModel> pathsView = new DataView<PathChangeModel>("changedPath", pathsDp) {
+ private static final long serialVersionUID = 1L;
+ int counter;
+
+ public void populateItem(final Item<PathChangeModel> item) {
+ final PathChangeModel entry = item.getModelObject();
+ Label changeType = new Label("changeType", "");
+ WicketUtils.setChangeTypeCssClass(changeType, entry.changeType);
+ setChangeTypeTooltip(changeType, entry.changeType);
+ item.add(changeType);
+
+ boolean hasSubmodule = false;
+ String submodulePath = null;
+ if (entry.isTree()) {
+ // tree
+ item.add(new LinkPanel("pathName", null, entry.path, TreePage.class,
+ WicketUtils
+ .newPathParameter(repositoryName, entry.commitId, entry.path)));
+ } else if (entry.isSubmodule()) {
+ // submodule
+ String submoduleId = entry.objectId;
+ SubmoduleModel submodule = getSubmodule(entry.path);
+ submodulePath = submodule.gitblitPath;
+ hasSubmodule = submodule.hasSubmodule;
+
+ item.add(new LinkPanel("pathName", "list", entry.path + " @ " +
+ getShortObjectId(submoduleId), TreePage.class,
+ WicketUtils.newPathParameter(submodulePath, submoduleId, "")).setEnabled(hasSubmodule));
+ } else {
+ // blob
+ String displayPath = entry.path;
+ String path = entry.path;
+ if (entry.isSymlink()) {
+ path = JGitUtils.getStringContent(getRepository(), getCommit().getTree(), path);
+ displayPath = entry.path + " -> " + path;
+ }
+ item.add(new LinkPanel("pathName", "list", displayPath, BlobPage.class,
+ WicketUtils
+ .newPathParameter(repositoryName, entry.commitId, path)));
+ }
+
+ // quick links
+ if (entry.isSubmodule()) {
+ // submodule
+ item.add(new BookmarkablePageLink<Void>("diff", BlobDiffPage.class, WicketUtils
+ .newPathParameter(repositoryName, entry.commitId, entry.path))
+ .setEnabled(!entry.changeType.equals(ChangeType.ADD)));
+ item.add(new BookmarkablePageLink<Void>("view", CommitPage.class, WicketUtils
+ .newObjectParameter(submodulePath, entry.objectId)).setEnabled(hasSubmodule));
+ item.add(new ExternalLink("blame", "").setEnabled(false));
+ item.add(new BookmarkablePageLink<Void>("history", HistoryPage.class, WicketUtils
+ .newPathParameter(repositoryName, entry.commitId, entry.path))
+ .setEnabled(!entry.changeType.equals(ChangeType.ADD)));
+ } else {
+ // tree or blob
+ item.add(new BookmarkablePageLink<Void>("diff", BlobDiffPage.class, WicketUtils
+ .newPathParameter(repositoryName, entry.commitId, entry.path))
+ .setEnabled(!entry.changeType.equals(ChangeType.ADD)
+ && !entry.changeType.equals(ChangeType.DELETE)));
+ item.add(new BookmarkablePageLink<Void>("view", BlobPage.class, WicketUtils
+ .newPathParameter(repositoryName, entry.commitId, entry.path)));
+ item.add(new BookmarkablePageLink<Void>("blame", BlamePage.class, WicketUtils
+ .newPathParameter(repositoryName, entry.commitId, entry.path))
+ .setEnabled(!entry.changeType.equals(ChangeType.ADD)));
+ item.add(new BookmarkablePageLink<Void>("history", HistoryPage.class, WicketUtils
+ .newPathParameter(repositoryName, entry.commitId, entry.path))
+ .setEnabled(!entry.changeType.equals(ChangeType.ADD)));
+ }
+
+ WicketUtils.setAlternatingBackground(item, counter);
+ counter++;
+ }
+ };
+ add(pathsView);
+ }
+
+ @Override
+ protected String getPageName() {
+ return getString("gb.commit");
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/pages/DocsPage.html b/src/main/java/com/gitblit/wicket/pages/DocsPage.html
new file mode 100644
index 00000000..ad93000c
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/DocsPage.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+
+<body>
+<wicket:extend>
+
+ <!-- header -->
+ <div style="margin-top:5px;" class="header"><i class="icon-book" style="vertical-align: middle;"></i> <b><span wicket:id="header">[header]</span></b></div>
+
+ <!-- documents -->
+ <table style="width:100%" class="pretty">
+ <tr wicket:id="document">
+ <td class="icon"><img wicket:id="docIcon" /></td>
+ <td><span wicket:id="docName"></span></td>
+ <td class="size"><span wicket:id="docSize">[doc size]</span></td>
+ <td class="treeLinks">
+ <span class="hidden-phone link">
+ <a wicket:id="view"><wicket:message key="gb.view"></wicket:message></a> | <a wicket:id="raw"><wicket:message key="gb.raw"></wicket:message></a> | <a wicket:id="blame"><wicket:message key="gb.blame"></wicket:message></a> | <a wicket:id="history"><wicket:message key="gb.history"></wicket:message></a>
+ </span>
+ </td>
+ </tr>
+ </table>
+</wicket:extend>
+</body>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/DocsPage.java b/src/main/java/com/gitblit/wicket/pages/DocsPage.java
new file mode 100644
index 00000000..9ddc98d3
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/DocsPage.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.pages;
+
+import java.util.List;
+
+import org.apache.wicket.PageParameters;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.link.BookmarkablePageLink;
+import org.apache.wicket.markup.repeater.Item;
+import org.apache.wicket.markup.repeater.data.DataView;
+import org.apache.wicket.markup.repeater.data.ListDataProvider;
+import org.eclipse.jgit.lib.Repository;
+
+import com.gitblit.GitBlit;
+import com.gitblit.Keys;
+import com.gitblit.models.PathModel;
+import com.gitblit.utils.ByteFormat;
+import com.gitblit.utils.JGitUtils;
+import com.gitblit.wicket.WicketUtils;
+import com.gitblit.wicket.panels.LinkPanel;
+
+public class DocsPage extends RepositoryPage {
+
+ public DocsPage(PageParameters params) {
+ super(params);
+
+ Repository r = getRepository();
+ List<String> extensions = GitBlit.getStrings(Keys.web.markdownExtensions);
+ List<PathModel> paths = JGitUtils.getDocuments(r, extensions);
+
+ final ByteFormat byteFormat = new ByteFormat();
+
+ add(new Label("header", getString("gb.docs")));
+
+ // documents list
+ ListDataProvider<PathModel> pathsDp = new ListDataProvider<PathModel>(paths);
+ DataView<PathModel> pathsView = new DataView<PathModel>("document", pathsDp) {
+ private static final long serialVersionUID = 1L;
+ int counter;
+
+ public void populateItem(final Item<PathModel> item) {
+ PathModel entry = item.getModelObject();
+ item.add(WicketUtils.newImage("docIcon", "file_world_16x16.png"));
+ item.add(new Label("docSize", byteFormat.format(entry.size)));
+ item.add(new LinkPanel("docName", "list", entry.name, BlobPage.class, WicketUtils
+ .newPathParameter(repositoryName, entry.commitId, entry.path)));
+
+ // links
+ item.add(new BookmarkablePageLink<Void>("view", BlobPage.class, WicketUtils
+ .newPathParameter(repositoryName, entry.commitId, entry.path)));
+ item.add(new BookmarkablePageLink<Void>("raw", RawPage.class, WicketUtils
+ .newPathParameter(repositoryName, entry.commitId, entry.path)));
+ item.add(new BookmarkablePageLink<Void>("blame", BlamePage.class, WicketUtils
+ .newPathParameter(repositoryName, entry.commitId, entry.path)));
+ item.add(new BookmarkablePageLink<Void>("history", HistoryPage.class, WicketUtils
+ .newPathParameter(repositoryName, entry.commitId, entry.path)));
+ WicketUtils.setAlternatingBackground(item, counter);
+ counter++;
+ }
+ };
+ add(pathsView);
+ }
+
+ @Override
+ protected String getPageName() {
+ return getString("gb.docs");
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/pages/EditRepositoryPage.html b/src/main/java/com/gitblit/wicket/pages/EditRepositoryPage.html
new file mode 100644
index 00000000..7fc0de23
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/EditRepositoryPage.html
@@ -0,0 +1,111 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+
+<wicket:extend>
+<body onload="document.getElementById('name').focus();">
+ <form style="padding-top:5px;" wicket:id="editForm">
+
+<div class="tabbable">
+ <!-- tab titles -->
+ <ul class="nav nav-tabs">
+ <li class="active"><a href="#general" data-toggle="tab"><wicket:message key="gb.general"></wicket:message></a></li>
+ <li><a href="#permissions" data-toggle="tab"><wicket:message key="gb.accessPermissions"></wicket:message></a></li>
+ <li><a href="#federation" data-toggle="tab"><wicket:message key="gb.federation"></wicket:message></a></li>
+ <li><a href="#search" data-toggle="tab"><wicket:message key="gb.search"></wicket:message></a></li>
+ <li><a href="#hooks" data-toggle="tab"><wicket:message key="gb.hookScripts"></wicket:message></a></li>
+ </ul>
+
+ <!-- tab content -->
+ <div class="tab-content">
+
+ <!-- general tab -->
+ <div class="tab-pane active" id="general">
+ <table class="plain">
+ <tbody class="settings">
+ <tr><th><wicket:message key="gb.name"></wicket:message></th><td class="edit"><input class="span4" type="text" wicket:id="name" id="name" size="40" tabindex="1" /> &nbsp;<span class="help-inline"><wicket:message key="gb.nameDescription"></wicket:message></span></td></tr>
+ <tr><th><wicket:message key="gb.description"></wicket:message></th><td class="edit"><input class="span4" type="text" wicket:id="description" size="40" tabindex="2" /></td></tr>
+ <tr><th colspan="2"><hr/></th></tr>
+ <tr><th><wicket:message key="gb.origin"></wicket:message></th><td class="edit"><input class="span5" type="text" wicket:id="origin" size="80" tabindex="3" /></td></tr>
+ <tr><th><wicket:message key="gb.headRef"></wicket:message></th><td class="edit"><select class="span3" wicket:id="HEAD" tabindex="4" /> &nbsp;<span class="help-inline"><wicket:message key="gb.headRefDescription"></wicket:message></span></td></tr>
+ <tr><th><wicket:message key="gb.gcPeriod"></wicket:message></th><td class="edit"><select class="span2" wicket:id="gcPeriod" tabindex="5" /> &nbsp;<span class="help-inline"><wicket:message key="gb.gcPeriodDescription"></wicket:message></span></td></tr>
+ <tr><th><wicket:message key="gb.gcThreshold"></wicket:message></th><td class="edit"><input class="span1" type="text" wicket:id="gcThreshold" tabindex="6" /> &nbsp;<span class="help-inline"><wicket:message key="gb.gcThresholdDescription"></wicket:message></span></td></tr>
+ <tr><th colspan="2"><hr/></th></tr>
+ <tr><th><wicket:message key="gb.enableTickets"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="useTickets" tabindex="7" /> &nbsp;<span class="help-inline"><wicket:message key="gb.useTicketsDescription"></wicket:message></span></label></td></tr>
+ <tr><th><wicket:message key="gb.enableDocs"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="useDocs" tabindex="8" /> &nbsp;<span class="help-inline"><wicket:message key="gb.useDocsDescription"></wicket:message></span></label></td></tr>
+ <tr><th><wicket:message key="gb.showRemoteBranches"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="showRemoteBranches" tabindex="9" /> &nbsp;<span class="help-inline"><wicket:message key="gb.showRemoteBranchesDescription"></wicket:message></span></label></td></tr>
+ <tr><th><wicket:message key="gb.showReadme"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="showReadme" tabindex="10" /> &nbsp;<span class="help-inline"><wicket:message key="gb.showReadmeDescription"></wicket:message></span></label></td></tr>
+ <tr><th><wicket:message key="gb.skipSizeCalculation"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="skipSizeCalculation" tabindex="11" /> &nbsp;<span class="help-inline"><wicket:message key="gb.skipSizeCalculationDescription"></wicket:message></span></label></td></tr>
+ <tr><th><wicket:message key="gb.skipSummaryMetrics"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="skipSummaryMetrics" tabindex="12" /> &nbsp;<span class="help-inline"><wicket:message key="gb.skipSummaryMetricsDescription"></wicket:message></span></label></td></tr>
+ <tr><th><wicket:message key="gb.maxActivityCommits"></wicket:message></th><td class="edit"><select class="span2" wicket:id="maxActivityCommits" tabindex="13" /> &nbsp;<span class="help-inline"><wicket:message key="gb.maxActivityCommitsDescription"></wicket:message></span></td></tr>
+ <tr><th colspan="2"><hr/></th></tr>
+ <tr><th><wicket:message key="gb.mailingLists"></wicket:message></th><td class="edit"><input class="span8" type="text" wicket:id="mailingLists" size="40" tabindex="14" /></td></tr>
+ </tbody>
+ </table>
+ </div>
+
+ <!-- access permissions -->
+ <div class="tab-pane" id="permissions">
+ <table class="plain">
+ <tbody class="settings">
+ <tr><th><wicket:message key="gb.owners"></wicket:message></th><td class="edit"><span wicket:id="owners" tabindex="15" /> </td></tr>
+ <tr><th colspan="2"><hr/></th></tr>
+ <tr><th><wicket:message key="gb.accessRestriction"></wicket:message></th><td class="edit"><select class="span4" wicket:id="accessRestriction" tabindex="16" /></td></tr>
+ <tr><th colspan="2"><hr/></th></tr>
+ <tr><th><wicket:message key="gb.authorizationControl"></wicket:message></th><td style="padding:2px;"><span class="authorizationControl" wicket:id="authorizationControl"></span></td></tr>
+ <tr><th colspan="2"><hr/></th></tr>
+ <tr><th><wicket:message key="gb.isFrozen"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="isFrozen" tabindex="17" /> &nbsp;<span class="help-inline"><wicket:message key="gb.isFrozenDescription"></wicket:message></span></label></td></tr>
+ <tr><th><wicket:message key="gb.allowForks"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="allowForks" tabindex="18" /> &nbsp;<span class="help-inline"><wicket:message key="gb.allowForksDescription"></wicket:message></span></label></td></tr>
+ <tr><th><wicket:message key="gb.verifyCommitter"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="verifyCommitter" tabindex="19" /> &nbsp;<span class="help-inline"><wicket:message key="gb.verifyCommitterDescription"></wicket:message></span><br/><span class="help-inline" style="padding-left:10px;"><wicket:message key="gb.verifyCommitterNote"></wicket:message></span></label></td></tr>
+ <tr><th colspan="2"><hr/></th></tr>
+ <tr><th><wicket:message key="gb.userPermissions"></wicket:message></th><td style="padding:2px;"><span wicket:id="users"></span></td></tr>
+ <tr><th colspan="2"><hr/></th></tr>
+ <tr><th><wicket:message key="gb.teamPermissions"></wicket:message></th><td style="padding:2px;"><span wicket:id="teams"></span></td></tr>
+ </tbody>
+ </table>
+ </div>
+
+ <!-- federation -->
+ <div class="tab-pane" id="federation">
+ <table class="plain">
+ <tbody class="settings">
+ <tr><th><wicket:message key="gb.federationStrategy"></wicket:message></th><td class="edit"><select class="span4" wicket:id="federationStrategy" tabindex="20" /></td></tr>
+ <tr><th><wicket:message key="gb.federationSets"></wicket:message></th><td style="padding:2px;"><span wicket:id="federationSets"></span></td></tr>
+ </tbody>
+ </table>
+ </div>
+
+ <!-- search -->
+ <div class="tab-pane" id="search">
+ <table class="plain">
+ <tbody class="settings">
+ <tr><th><wicket:message key="gb.indexedBranches"></wicket:message></th><td style="padding:2px;"><span wicket:id="indexedBranches"></span></td></tr>
+ </tbody>
+ </table>
+ </div>
+
+ <!-- hooks -->
+ <div class="tab-pane" id="hooks">
+ <table class="plain">
+ <tbody class="settings">
+ <tr><th><wicket:message key="gb.preReceiveScripts"></wicket:message><p></p><span wicket:id="inheritedPreReceive"></span></th><td style="padding:2px;"><span wicket:id="preReceiveScripts"></span></td></tr>
+ <tr><th><wicket:message key="gb.postReceiveScripts"></wicket:message><p></p><span wicket:id="inheritedPostReceive"></span></th><td style="padding:2px;"><span wicket:id="postReceiveScripts"></span></td></tr>
+ <div wicket:id="customFieldsSection">
+ <tr><td colspan="2"><h3><wicket:message key="gb.customFields"></wicket:message> &nbsp;<small><wicket:message key="gb.customFieldsDescription"></wicket:message></small></h3></td></tr>
+ <tr wicket:id="customFieldsListView"><th><span wicket:id="customFieldLabel"></span></th><td class="edit"><input class="span8" type="text" wicket:id="customFieldValue" /></td></tr>
+ </div>
+ </tbody>
+ </table>
+ </div>
+ </div>
+
+ <div class="row">
+ <div class="form-actions"><input class="btn btn-primary" type="submit" value="Save" wicket:message="value:gb.save" wicket:id="save" /> &nbsp; <input class="btn" type="submit" value="Cancel" wicket:message="value:gb.cancel" wicket:id="cancel" /></div>
+ </div>
+</div>
+</form>
+</body>
+</wicket:extend>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/EditRepositoryPage.java b/src/main/java/com/gitblit/wicket/pages/EditRepositoryPage.java
new file mode 100644
index 00000000..d68d6550
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/EditRepositoryPage.java
@@ -0,0 +1,696 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.pages;
+
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.wicket.PageParameters;
+import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.ajax.form.AjaxFormChoiceComponentUpdatingBehavior;
+import org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior;
+import org.apache.wicket.behavior.SimpleAttributeModifier;
+import org.apache.wicket.extensions.markup.html.form.palette.Palette;
+import org.apache.wicket.markup.html.WebMarkupContainer;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.form.Button;
+import org.apache.wicket.markup.html.form.CheckBox;
+import org.apache.wicket.markup.html.form.DropDownChoice;
+import org.apache.wicket.markup.html.form.Form;
+import org.apache.wicket.markup.html.form.IChoiceRenderer;
+import org.apache.wicket.markup.html.form.RadioChoice;
+import org.apache.wicket.markup.html.form.TextField;
+import org.apache.wicket.markup.html.list.ListItem;
+import org.apache.wicket.markup.html.list.ListView;
+import org.apache.wicket.model.CompoundPropertyModel;
+import org.apache.wicket.model.IModel;
+import org.apache.wicket.model.Model;
+import org.apache.wicket.model.util.CollectionModel;
+import org.apache.wicket.model.util.ListModel;
+
+import com.gitblit.Constants;
+import com.gitblit.Constants.AccessRestrictionType;
+import com.gitblit.Constants.AuthorizationControl;
+import com.gitblit.Constants.FederationStrategy;
+import com.gitblit.Constants.RegistrantType;
+import com.gitblit.GitBlit;
+import com.gitblit.GitBlitException;
+import com.gitblit.Keys;
+import com.gitblit.models.RegistrantAccessPermission;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.UserModel;
+import com.gitblit.utils.ArrayUtils;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.wicket.GitBlitWebSession;
+import com.gitblit.wicket.StringChoiceRenderer;
+import com.gitblit.wicket.WicketUtils;
+import com.gitblit.wicket.panels.BulletListPanel;
+import com.gitblit.wicket.panels.RegistrantPermissionsPanel;
+
+public class EditRepositoryPage extends RootSubPage {
+
+ private final boolean isCreate;
+
+ private boolean isAdmin;
+
+ RepositoryModel repositoryModel;
+
+ private IModel<String> mailingLists;
+
+ public EditRepositoryPage() {
+ // create constructor
+ super();
+ isCreate = true;
+ RepositoryModel model = new RepositoryModel();
+ String restriction = GitBlit.getString(Keys.git.defaultAccessRestriction, null);
+ model.accessRestriction = AccessRestrictionType.fromName(restriction);
+ String authorization = GitBlit.getString(Keys.git.defaultAuthorizationControl, null);
+ model.authorizationControl = AuthorizationControl.fromName(authorization);
+
+ GitBlitWebSession session = GitBlitWebSession.get();
+ UserModel user = session.getUser();
+ if (user != null && user.canCreate() && !user.canAdmin()) {
+ // personal create permissions, inject personal repository path
+ model.name = user.getPersonalPath() + "/";
+ model.projectPath = user.getPersonalPath();
+ model.addOwner(user.username);
+ // personal repositories are private by default
+ model.accessRestriction = AccessRestrictionType.VIEW;
+ model.authorizationControl = AuthorizationControl.NAMED;
+ }
+
+ setupPage(model);
+ setStatelessHint(false);
+ setOutputMarkupId(true);
+ }
+
+ public EditRepositoryPage(PageParameters params) {
+ // edit constructor
+ super(params);
+ isCreate = false;
+ String name = WicketUtils.getRepositoryName(params);
+ RepositoryModel model = GitBlit.self().getRepositoryModel(name);
+ setupPage(model);
+ setStatelessHint(false);
+ setOutputMarkupId(true);
+ }
+
+ @Override
+ protected boolean requiresPageMap() {
+ return true;
+ }
+
+ protected void setupPage(RepositoryModel model) {
+ this.repositoryModel = model;
+
+ // ensure this user can create or edit this repository
+ checkPermissions(repositoryModel);
+
+ List<String> indexedBranches = new ArrayList<String>();
+ List<String> federationSets = new ArrayList<String>();
+ final List<RegistrantAccessPermission> repositoryUsers = new ArrayList<RegistrantAccessPermission>();
+ final List<RegistrantAccessPermission> repositoryTeams = new ArrayList<RegistrantAccessPermission>();
+ List<String> preReceiveScripts = new ArrayList<String>();
+ List<String> postReceiveScripts = new ArrayList<String>();
+
+ GitBlitWebSession session = GitBlitWebSession.get();
+ final UserModel user = session.getUser() == null ? UserModel.ANONYMOUS : session.getUser();
+ final boolean allowEditName = isCreate || isAdmin || repositoryModel.isUsersPersonalRepository(user.username);
+
+ if (isCreate) {
+ if (user.canAdmin()) {
+ super.setupPage(getString("gb.newRepository"), "");
+ } else {
+ super.setupPage(getString("gb.newRepository"), user.getDisplayName());
+ }
+ } else {
+ super.setupPage(getString("gb.edit"), repositoryModel.name);
+ repositoryUsers.addAll(GitBlit.self().getUserAccessPermissions(repositoryModel));
+ repositoryTeams.addAll(GitBlit.self().getTeamAccessPermissions(repositoryModel));
+ Collections.sort(repositoryUsers);
+ Collections.sort(repositoryTeams);
+
+ federationSets.addAll(repositoryModel.federationSets);
+ if (!ArrayUtils.isEmpty(repositoryModel.indexedBranches)) {
+ indexedBranches.addAll(repositoryModel.indexedBranches);
+ }
+ }
+
+ final String oldName = repositoryModel.name;
+
+ final RegistrantPermissionsPanel usersPalette = new RegistrantPermissionsPanel("users",
+ RegistrantType.USER, GitBlit.self().getAllUsernames(), repositoryUsers, getAccessPermissions());
+ final RegistrantPermissionsPanel teamsPalette = new RegistrantPermissionsPanel("teams",
+ RegistrantType.TEAM, GitBlit.self().getAllTeamnames(), repositoryTeams, getAccessPermissions());
+
+ // owners palette
+ List<String> owners = new ArrayList<String>(repositoryModel.owners);
+ List<String> persons = GitBlit.self().getAllUsernames();
+ final Palette<String> ownersPalette = new Palette<String>("owners", new ListModel<String>(owners), new CollectionModel<String>(
+ persons), new StringChoiceRenderer(), 12, true);
+
+ // indexed local branches palette
+ List<String> allLocalBranches = new ArrayList<String>();
+ allLocalBranches.add(Constants.DEFAULT_BRANCH);
+ allLocalBranches.addAll(repositoryModel.getLocalBranches());
+ boolean luceneEnabled = GitBlit.getBoolean(Keys.web.allowLuceneIndexing, true);
+ final Palette<String> indexedBranchesPalette = new Palette<String>("indexedBranches", new ListModel<String>(
+ indexedBranches), new CollectionModel<String>(allLocalBranches),
+ new StringChoiceRenderer(), 8, false);
+ indexedBranchesPalette.setEnabled(luceneEnabled);
+
+ // federation sets palette
+ List<String> sets = GitBlit.getStrings(Keys.federation.sets);
+ final Palette<String> federationSetsPalette = new Palette<String>("federationSets",
+ new ListModel<String>(federationSets), new CollectionModel<String>(sets),
+ new StringChoiceRenderer(), 8, false);
+
+ // pre-receive palette
+ if (!ArrayUtils.isEmpty(repositoryModel.preReceiveScripts)) {
+ preReceiveScripts.addAll(repositoryModel.preReceiveScripts);
+ }
+ final Palette<String> preReceivePalette = new Palette<String>("preReceiveScripts",
+ new ListModel<String>(preReceiveScripts), new CollectionModel<String>(GitBlit
+ .self().getPreReceiveScriptsUnused(repositoryModel)),
+ new StringChoiceRenderer(), 12, true);
+
+ // post-receive palette
+ if (!ArrayUtils.isEmpty(repositoryModel.postReceiveScripts)) {
+ postReceiveScripts.addAll(repositoryModel.postReceiveScripts);
+ }
+ final Palette<String> postReceivePalette = new Palette<String>("postReceiveScripts",
+ new ListModel<String>(postReceiveScripts), new CollectionModel<String>(GitBlit
+ .self().getPostReceiveScriptsUnused(repositoryModel)),
+ new StringChoiceRenderer(), 12, true);
+
+ // custom fields
+ final Map<String, String> customFieldsMap = GitBlit.getMap(Keys.groovy.customFields);
+ List<String> customKeys = new ArrayList<String>(customFieldsMap.keySet());
+ final ListView<String> customFieldsListView = new ListView<String>("customFieldsListView", customKeys) {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ protected void populateItem(ListItem<String> item) {
+ String key = item.getModelObject();
+ item.add(new Label("customFieldLabel", customFieldsMap.get(key)));
+
+ String value = "";
+ if (repositoryModel.customFields != null && repositoryModel.customFields.containsKey(key)) {
+ value = repositoryModel.customFields.get(key);
+ }
+ TextField<String> field = new TextField<String>("customFieldValue", new Model<String>(value));
+ item.add(field);
+ }
+ };
+ customFieldsListView.setReuseItems(true);
+
+ CompoundPropertyModel<RepositoryModel> rModel = new CompoundPropertyModel<RepositoryModel>(
+ repositoryModel);
+ Form<RepositoryModel> form = new Form<RepositoryModel>("editForm", rModel) {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ protected void onSubmit() {
+ try {
+ // confirm a repository name was entered
+ if (repositoryModel.name == null && StringUtils.isEmpty(repositoryModel.name)) {
+ error(getString("gb.pleaseSetRepositoryName"));
+ return;
+ }
+
+ // ensure name is trimmed
+ repositoryModel.name = repositoryModel.name.trim();
+
+ // automatically convert backslashes to forward slashes
+ repositoryModel.name = repositoryModel.name.replace('\\', '/');
+ // Automatically replace // with /
+ repositoryModel.name = repositoryModel.name.replace("//", "/");
+
+ // prohibit folder paths
+ if (repositoryModel.name.startsWith("/")) {
+ error(getString("gb.illegalLeadingSlash"));
+ return;
+ }
+ if (repositoryModel.name.startsWith("../")) {
+ error(getString("gb.illegalRelativeSlash"));
+ return;
+ }
+ if (repositoryModel.name.contains("/../")) {
+ error(getString("gb.illegalRelativeSlash"));
+ return;
+ }
+ if (repositoryModel.name.endsWith("/")) {
+ repositoryModel.name = repositoryModel.name.substring(0, repositoryModel.name.length() - 1);
+ }
+
+ // confirm valid characters in repository name
+ Character c = StringUtils.findInvalidCharacter(repositoryModel.name);
+ if (c != null) {
+ error(MessageFormat.format(getString("gb.illegalCharacterRepositoryName"),
+ c));
+ return;
+ }
+
+ if (user.canCreate() && !user.canAdmin() && allowEditName) {
+ // ensure repository name begins with the user's path
+ if (!repositoryModel.name.startsWith(user.getPersonalPath())) {
+ error(MessageFormat.format(getString("gb.illegalPersonalRepositoryLocation"),
+ user.getPersonalPath()));
+ return;
+ }
+
+ if (repositoryModel.name.equals(user.getPersonalPath())) {
+ // reset path prefix and show error
+ repositoryModel.name = user.getPersonalPath() + "/";
+ error(getString("gb.pleaseSetRepositoryName"));
+ return;
+ }
+ }
+
+ // confirm access restriction selection
+ if (repositoryModel.accessRestriction == null) {
+ error(getString("gb.selectAccessRestriction"));
+ return;
+ }
+
+ // confirm federation strategy selection
+ if (repositoryModel.federationStrategy == null) {
+ error(getString("gb.selectFederationStrategy"));
+ return;
+ }
+
+ // save federation set preferences
+ if (repositoryModel.federationStrategy.exceeds(FederationStrategy.EXCLUDE)) {
+ repositoryModel.federationSets.clear();
+ Iterator<String> sets = federationSetsPalette.getSelectedChoices();
+ while (sets.hasNext()) {
+ repositoryModel.federationSets.add(sets.next());
+ }
+ }
+
+ // set mailing lists
+ String ml = mailingLists.getObject();
+ if (!StringUtils.isEmpty(ml)) {
+ Set<String> list = new HashSet<String>();
+ for (String address : ml.split("(,|\\s)")) {
+ if (StringUtils.isEmpty(address)) {
+ continue;
+ }
+ list.add(address.toLowerCase());
+ }
+ repositoryModel.mailingLists = new ArrayList<String>(list);
+ }
+
+ // indexed branches
+ List<String> indexedBranches = new ArrayList<String>();
+ Iterator<String> branches = indexedBranchesPalette.getSelectedChoices();
+ while (branches.hasNext()) {
+ indexedBranches.add(branches.next());
+ }
+ repositoryModel.indexedBranches = indexedBranches;
+
+ // owners
+ repositoryModel.owners.clear();
+ Iterator<String> owners = ownersPalette.getSelectedChoices();
+ while (owners.hasNext()) {
+ repositoryModel.addOwner(owners.next());
+ }
+
+ // pre-receive scripts
+ List<String> preReceiveScripts = new ArrayList<String>();
+ Iterator<String> pres = preReceivePalette.getSelectedChoices();
+ while (pres.hasNext()) {
+ preReceiveScripts.add(pres.next());
+ }
+ repositoryModel.preReceiveScripts = preReceiveScripts;
+
+ // post-receive scripts
+ List<String> postReceiveScripts = new ArrayList<String>();
+ Iterator<String> post = postReceivePalette.getSelectedChoices();
+ while (post.hasNext()) {
+ postReceiveScripts.add(post.next());
+ }
+ repositoryModel.postReceiveScripts = postReceiveScripts;
+
+ // custom fields
+ repositoryModel.customFields = new LinkedHashMap<String, String>();
+ for (int i = 0; i < customFieldsListView.size(); i++) {
+ ListItem<String> child = (ListItem<String>) customFieldsListView.get(i);
+ String key = child.getModelObject();
+
+ TextField<String> field = (TextField<String>) child.get("customFieldValue");
+ String value = field.getValue();
+
+ repositoryModel.customFields.put(key, value);
+ }
+
+ // save the repository
+ GitBlit.self().updateRepositoryModel(oldName, repositoryModel, isCreate);
+
+ // repository access permissions
+ if (repositoryModel.accessRestriction.exceeds(AccessRestrictionType.NONE)) {
+ GitBlit.self().setUserAccessPermissions(repositoryModel, repositoryUsers);
+ GitBlit.self().setTeamAccessPermissions(repositoryModel, repositoryTeams);
+ }
+ } catch (GitBlitException e) {
+ error(e.getMessage());
+ return;
+ }
+ setRedirect(false);
+ setResponsePage(RepositoriesPage.class);
+ }
+ };
+
+ // do not let the browser pre-populate these fields
+ form.add(new SimpleAttributeModifier("autocomplete", "off"));
+
+ // field names reflective match RepositoryModel fields
+ form.add(new TextField<String>("name").setEnabled(allowEditName));
+ form.add(new TextField<String>("description"));
+ form.add(ownersPalette);
+ form.add(new CheckBox("allowForks").setEnabled(GitBlit.getBoolean(Keys.web.allowForking, true)));
+ DropDownChoice<AccessRestrictionType> accessRestriction = new DropDownChoice<AccessRestrictionType>("accessRestriction", Arrays
+ .asList(AccessRestrictionType.values()), new AccessRestrictionRenderer());
+ form.add(accessRestriction);
+ form.add(new CheckBox("isFrozen"));
+ // TODO enable origin definition
+ form.add(new TextField<String>("origin").setEnabled(false/* isCreate */));
+
+ // allow relinking HEAD to a branch or tag other than master on edit repository
+ List<String> availableRefs = new ArrayList<String>();
+ if (!ArrayUtils.isEmpty(repositoryModel.availableRefs)) {
+ availableRefs.addAll(repositoryModel.availableRefs);
+ }
+ form.add(new DropDownChoice<String>("HEAD", availableRefs).setEnabled(availableRefs.size() > 0));
+
+ boolean gcEnabled = GitBlit.getBoolean(Keys.git.enableGarbageCollection, false);
+ List<Integer> gcPeriods = Arrays.asList(1, 2, 3, 4, 5, 7, 10, 14 );
+ form.add(new DropDownChoice<Integer>("gcPeriod", gcPeriods, new GCPeriodRenderer()).setEnabled(gcEnabled));
+ form.add(new TextField<String>("gcThreshold").setEnabled(gcEnabled));
+
+ // federation strategies - remove ORIGIN choice if this repository has
+ // no origin.
+ List<FederationStrategy> federationStrategies = new ArrayList<FederationStrategy>(
+ Arrays.asList(FederationStrategy.values()));
+ if (StringUtils.isEmpty(repositoryModel.origin)) {
+ federationStrategies.remove(FederationStrategy.FEDERATE_ORIGIN);
+ }
+ form.add(new DropDownChoice<FederationStrategy>("federationStrategy", federationStrategies,
+ new FederationTypeRenderer()));
+ form.add(new CheckBox("useTickets"));
+ form.add(new CheckBox("useDocs"));
+ form.add(new CheckBox("showRemoteBranches"));
+ form.add(new CheckBox("showReadme"));
+ form.add(new CheckBox("skipSizeCalculation"));
+ form.add(new CheckBox("skipSummaryMetrics"));
+ List<Integer> maxActivityCommits = Arrays.asList(-1, 0, 25, 50, 75, 100, 150, 200, 250, 500 );
+ form.add(new DropDownChoice<Integer>("maxActivityCommits", maxActivityCommits, new MaxActivityCommitsRenderer()));
+
+ mailingLists = new Model<String>(ArrayUtils.isEmpty(repositoryModel.mailingLists) ? ""
+ : StringUtils.flattenStrings(repositoryModel.mailingLists, " "));
+ form.add(new TextField<String>("mailingLists", mailingLists));
+ form.add(indexedBranchesPalette);
+
+ List<AuthorizationControl> acList = Arrays.asList(AuthorizationControl.values());
+ final RadioChoice<AuthorizationControl> authorizationControl = new RadioChoice<Constants.AuthorizationControl>(
+ "authorizationControl", acList, new AuthorizationControlRenderer());
+ form.add(authorizationControl);
+
+ final CheckBox verifyCommitter = new CheckBox("verifyCommitter");
+ verifyCommitter.setOutputMarkupId(true);
+ form.add(verifyCommitter);
+
+ form.add(usersPalette);
+ form.add(teamsPalette);
+ form.add(federationSetsPalette);
+ form.add(preReceivePalette);
+ form.add(new BulletListPanel("inheritedPreReceive", getString("gb.inherited"), GitBlit.self()
+ .getPreReceiveScriptsInherited(repositoryModel)));
+ form.add(postReceivePalette);
+ form.add(new BulletListPanel("inheritedPostReceive", getString("gb.inherited"), GitBlit.self()
+ .getPostReceiveScriptsInherited(repositoryModel)));
+
+ WebMarkupContainer customFieldsSection = new WebMarkupContainer("customFieldsSection");
+ customFieldsSection.add(customFieldsListView);
+ form.add(customFieldsSection.setVisible(!GitBlit.getString(Keys.groovy.customFields, "").isEmpty()));
+
+ // initial enable/disable of permission controls
+ if (repositoryModel.accessRestriction.equals(AccessRestrictionType.NONE)) {
+ // anonymous everything, disable all controls
+ usersPalette.setEnabled(false);
+ teamsPalette.setEnabled(false);
+ authorizationControl.setEnabled(false);
+ verifyCommitter.setEnabled(false);
+ } else {
+ // authenticated something
+ // enable authorization controls
+ authorizationControl.setEnabled(true);
+ verifyCommitter.setEnabled(true);
+
+ boolean allowFineGrainedControls = repositoryModel.authorizationControl.equals(AuthorizationControl.NAMED);
+ usersPalette.setEnabled(allowFineGrainedControls);
+ teamsPalette.setEnabled(allowFineGrainedControls);
+ }
+
+ accessRestriction.add(new AjaxFormComponentUpdatingBehavior("onchange") {
+
+ private static final long serialVersionUID = 1L;
+
+ protected void onUpdate(AjaxRequestTarget target) {
+ // enable/disable permissions panel based on access restriction
+ boolean allowAuthorizationControl = repositoryModel.accessRestriction.exceeds(AccessRestrictionType.NONE);
+ authorizationControl.setEnabled(allowAuthorizationControl);
+ verifyCommitter.setEnabled(allowAuthorizationControl);
+
+ boolean allowFineGrainedControls = allowAuthorizationControl && repositoryModel.authorizationControl.equals(AuthorizationControl.NAMED);
+ usersPalette.setEnabled(allowFineGrainedControls);
+ teamsPalette.setEnabled(allowFineGrainedControls);
+
+ if (allowFineGrainedControls) {
+ repositoryModel.authorizationControl = AuthorizationControl.NAMED;
+ }
+
+ target.addComponent(authorizationControl);
+ target.addComponent(verifyCommitter);
+ target.addComponent(usersPalette);
+ target.addComponent(teamsPalette);
+ }
+ });
+
+ authorizationControl.add(new AjaxFormChoiceComponentUpdatingBehavior() {
+
+ private static final long serialVersionUID = 1L;
+
+ protected void onUpdate(AjaxRequestTarget target) {
+ // enable/disable permissions panel based on access restriction
+ boolean allowAuthorizationControl = repositoryModel.accessRestriction.exceeds(AccessRestrictionType.NONE);
+ authorizationControl.setEnabled(allowAuthorizationControl);
+
+ boolean allowFineGrainedControls = allowAuthorizationControl && repositoryModel.authorizationControl.equals(AuthorizationControl.NAMED);
+ usersPalette.setEnabled(allowFineGrainedControls);
+ teamsPalette.setEnabled(allowFineGrainedControls);
+
+ if (allowFineGrainedControls) {
+ repositoryModel.authorizationControl = AuthorizationControl.NAMED;
+ }
+
+ target.addComponent(authorizationControl);
+ target.addComponent(usersPalette);
+ target.addComponent(teamsPalette);
+ }
+ });
+
+ form.add(new Button("save"));
+ Button cancel = new Button("cancel") {
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public void onSubmit() {
+ setResponsePage(RepositoriesPage.class);
+ }
+ };
+ cancel.setDefaultFormProcessing(false);
+ form.add(cancel);
+
+ add(form);
+ }
+
+ /**
+ * Unfortunately must repeat part of AuthorizaitonStrategy here because that
+ * mechanism does not take PageParameters into consideration, only page
+ * instantiation.
+ *
+ * Repository Owners should be able to edit their repository.
+ */
+ private void checkPermissions(RepositoryModel model) {
+ boolean authenticateAdmin = GitBlit.getBoolean(Keys.web.authenticateAdminPages, true);
+ boolean allowAdmin = GitBlit.getBoolean(Keys.web.allowAdministration, true);
+
+ GitBlitWebSession session = GitBlitWebSession.get();
+ UserModel user = session.getUser();
+
+ if (allowAdmin) {
+ if (authenticateAdmin) {
+ if (user == null) {
+ // No Login Available
+ error(getString("gb.errorAdminLoginRequired"), true);
+ }
+ if (isCreate) {
+ // Create Repository
+ if (!user.canCreate() && !user.canAdmin()) {
+ // Only administrators or permitted users may create
+ error(getString("gb.errorOnlyAdminMayCreateRepository"), true);
+ }
+ } else {
+ // Edit Repository
+ if (user.canAdmin()) {
+ // Admins can edit everything
+ isAdmin = true;
+ return;
+ } else {
+ if (!model.isOwner(user.username)) {
+ // User is not an Admin nor Owner
+ error(getString("gb.errorOnlyAdminOrOwnerMayEditRepository"), true);
+ }
+ }
+ }
+ }
+ } else {
+ // No Administration Permitted
+ error(getString("gb.errorAdministrationDisabled"), true);
+ }
+ }
+
+ private class AccessRestrictionRenderer implements IChoiceRenderer<AccessRestrictionType> {
+
+ private static final long serialVersionUID = 1L;
+
+ private final Map<AccessRestrictionType, String> map;
+
+ public AccessRestrictionRenderer() {
+ map = getAccessRestrictions();
+ }
+
+ @Override
+ public String getDisplayValue(AccessRestrictionType type) {
+ return map.get(type);
+ }
+
+ @Override
+ public String getIdValue(AccessRestrictionType type, int index) {
+ return Integer.toString(index);
+ }
+ }
+
+ private class FederationTypeRenderer implements IChoiceRenderer<FederationStrategy> {
+
+ private static final long serialVersionUID = 1L;
+
+ private final Map<FederationStrategy, String> map;
+
+ public FederationTypeRenderer() {
+ map = getFederationTypes();
+ }
+
+ @Override
+ public String getDisplayValue(FederationStrategy type) {
+ return map.get(type);
+ }
+
+ @Override
+ public String getIdValue(FederationStrategy type, int index) {
+ return Integer.toString(index);
+ }
+ }
+
+ private class AuthorizationControlRenderer implements IChoiceRenderer<AuthorizationControl> {
+
+ private static final long serialVersionUID = 1L;
+
+ private final Map<AuthorizationControl, String> map;
+
+ public AuthorizationControlRenderer() {
+ map = getAuthorizationControls();
+ }
+
+ @Override
+ public String getDisplayValue(AuthorizationControl type) {
+ return map.get(type);
+ }
+
+ @Override
+ public String getIdValue(AuthorizationControl type, int index) {
+ return Integer.toString(index);
+ }
+ }
+
+ private class GCPeriodRenderer implements IChoiceRenderer<Integer> {
+
+ private static final long serialVersionUID = 1L;
+
+ public GCPeriodRenderer() {
+ }
+
+ @Override
+ public String getDisplayValue(Integer value) {
+ if (value == 1) {
+ return getString("gb.duration.oneDay");
+ } else {
+ return MessageFormat.format(getString("gb.duration.days"), value);
+ }
+ }
+
+ @Override
+ public String getIdValue(Integer value, int index) {
+ return Integer.toString(index);
+ }
+ }
+
+ private class MaxActivityCommitsRenderer implements IChoiceRenderer<Integer> {
+
+ private static final long serialVersionUID = 1L;
+
+ public MaxActivityCommitsRenderer() {
+ }
+
+ @Override
+ public String getDisplayValue(Integer value) {
+ if (value == -1) {
+ return getString("gb.excludeFromActivity");
+ } else if (value == 0) {
+ return getString("gb.noMaximum");
+ } else {
+ return value + " " + getString("gb.commits");
+ }
+ }
+
+ @Override
+ public String getIdValue(Integer value, int index) {
+ return Integer.toString(index);
+ }
+ }
+
+}
diff --git a/src/main/java/com/gitblit/wicket/pages/EditTeamPage.html b/src/main/java/com/gitblit/wicket/pages/EditTeamPage.html
new file mode 100644
index 00000000..a60d1715
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/EditTeamPage.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+
+<wicket:extend>
+<body onload="document.getElementById('name').focus();">
+<!-- Team Table -->
+<form style="padding-top:5px;" wicket:id="editForm">
+
+<div class="tabbable">
+ <!-- tab titles -->
+ <ul class="nav nav-tabs">
+ <li class="active"><a href="#general" data-toggle="tab"><wicket:message key="gb.general"></wicket:message></a></li>
+ <li><a href="#permissions" data-toggle="tab"><wicket:message key="gb.accessPermissions"></wicket:message></a></li>
+ </ul>
+
+ <!-- tab content -->
+ <div class="tab-content">
+
+ <!-- general tab -->
+ <div class="tab-pane active" id="general">
+ <table class="plain">
+ <tbody class="settings">
+ <tr><th><wicket:message key="gb.teamName"></wicket:message></th><td class="edit"><input type="text" wicket:id="name" id="name" size="30" tabindex="1" /></td></tr>
+ <tr><th><wicket:message key="gb.canAdmin"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="canAdmin" tabindex="2" /> &nbsp;<span class="help-inline"><wicket:message key="gb.canAdminDescription"></wicket:message></span></label></td></tr>
+ <tr><th><wicket:message key="gb.canCreate"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="canCreate" tabindex="3" /> &nbsp;<span class="help-inline"><wicket:message key="gb.canCreateDescription"></wicket:message></span></label></td></tr>
+ <tr><th><wicket:message key="gb.canFork"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="canFork" tabindex="4" /> &nbsp;<span class="help-inline"><wicket:message key="gb.canForkDescription"></wicket:message></span></label></td></tr>
+ <tr><th><wicket:message key="gb.mailingLists"></wicket:message></th><td class="edit"><input class="span8" type="text" wicket:id="mailingLists" size="40" tabindex="5" /></td></tr>
+ </tbody>
+ </table>
+ </div>
+
+ <!-- access permissions tab -->
+ <div class="tab-pane" id="permissions">
+ <table class="plain">
+ <tbody class="settings">
+ <tr><th><wicket:message key="gb.teamMembers"></wicket:message></th><td style="padding:2px;"><span wicket:id="users"></span></td></tr>
+ <tr><td colspan="2"><hr></hr></td></tr>
+ <tr><th><wicket:message key="gb.repositoryPermissions"></wicket:message></th><td style="padding:2px;"><span wicket:id="repositories"></span></td></tr>
+ </tbody>
+ </table>
+ </div>
+
+ <!-- hooks -->
+ <div class="tab-pane" id="hooks">
+ <table class="plain">
+ <tbody class="settings">
+ <tr><td colspan="2" style="padding-top:10px;"><h3><wicket:message key="gb.hookScripts"></wicket:message> &nbsp;<small><wicket:message key="gb.hookScriptsDescription"></wicket:message></small></h3></td></tr>
+ <tr><th><wicket:message key="gb.preReceiveScripts"></wicket:message><p></p><span wicket:id="inheritedPreReceive"></span></th><td style="padding:2px;"><span wicket:id="preReceiveScripts"></span></td></tr>
+ <tr><th><wicket:message key="gb.postReceiveScripts"></wicket:message><p></p><span wicket:id="inheritedPostReceive"></span></th><td style="padding:2px;"><span wicket:id="postReceiveScripts"></span></td></tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
+</div>
+
+<div class="row">
+ <div class="form-actions"><input class="btn btn-primary" type="submit" value="Save" wicket:message="value:gb.save" wicket:id="save" tabindex="6" /> &nbsp; <input class="btn" type="submit" value="Cancel" wicket:message="value:gb.cancel" wicket:id="cancel" tabindex="7" /></div>
+</div>
+
+</form>
+</body>
+</wicket:extend>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/EditTeamPage.java b/src/main/java/com/gitblit/wicket/pages/EditTeamPage.java
new file mode 100644
index 00000000..8344d387
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/EditTeamPage.java
@@ -0,0 +1,250 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.pages;
+
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.wicket.PageParameters;
+import org.apache.wicket.behavior.SimpleAttributeModifier;
+import org.apache.wicket.extensions.markup.html.form.palette.Palette;
+import org.apache.wicket.markup.html.form.Button;
+import org.apache.wicket.markup.html.form.CheckBox;
+import org.apache.wicket.markup.html.form.Form;
+import org.apache.wicket.markup.html.form.TextField;
+import org.apache.wicket.model.CompoundPropertyModel;
+import org.apache.wicket.model.IModel;
+import org.apache.wicket.model.Model;
+import org.apache.wicket.model.util.CollectionModel;
+import org.apache.wicket.model.util.ListModel;
+
+import com.gitblit.GitBlit;
+import com.gitblit.GitBlitException;
+import com.gitblit.Keys;
+import com.gitblit.Constants.RegistrantType;
+import com.gitblit.models.RegistrantAccessPermission;
+import com.gitblit.models.TeamModel;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.wicket.RequiresAdminRole;
+import com.gitblit.wicket.StringChoiceRenderer;
+import com.gitblit.wicket.WicketUtils;
+import com.gitblit.wicket.panels.BulletListPanel;
+import com.gitblit.wicket.panels.RegistrantPermissionsPanel;
+
+@RequiresAdminRole
+public class EditTeamPage extends RootSubPage {
+
+ private final boolean isCreate;
+
+ private IModel<String> mailingLists;
+
+ public EditTeamPage() {
+ // create constructor
+ super();
+ isCreate = true;
+ setupPage(new TeamModel(""));
+ setStatelessHint(false);
+ setOutputMarkupId(true);
+ }
+
+ public EditTeamPage(PageParameters params) {
+ // edit constructor
+ super(params);
+ isCreate = false;
+ String name = WicketUtils.getTeamname(params);
+ TeamModel model = GitBlit.self().getTeamModel(name);
+ setupPage(model);
+ setStatelessHint(false);
+ setOutputMarkupId(true);
+ }
+
+ @Override
+ protected boolean requiresPageMap() {
+ return true;
+ }
+
+ protected void setupPage(final TeamModel teamModel) {
+ if (isCreate) {
+ super.setupPage(getString("gb.newTeam"), "");
+ } else {
+ super.setupPage(getString("gb.edit"), teamModel.name);
+ }
+
+ CompoundPropertyModel<TeamModel> model = new CompoundPropertyModel<TeamModel>(teamModel);
+
+ List<String> repos = getAccessRestrictedRepositoryList(true, null);
+
+ List<String> teamUsers = new ArrayList<String>(teamModel.users);
+ Collections.sort(teamUsers);
+ List<String> preReceiveScripts = new ArrayList<String>();
+ List<String> postReceiveScripts = new ArrayList<String>();
+
+ final String oldName = teamModel.name;
+ final List<RegistrantAccessPermission> permissions = teamModel.getRepositoryPermissions();
+
+ // users palette
+ final Palette<String> users = new Palette<String>("users", new ListModel<String>(
+ new ArrayList<String>(teamUsers)), new CollectionModel<String>(GitBlit.self()
+ .getAllUsernames()), new StringChoiceRenderer(), 10, false);
+
+ // pre-receive palette
+ if (teamModel.preReceiveScripts != null) {
+ preReceiveScripts.addAll(teamModel.preReceiveScripts);
+ }
+ final Palette<String> preReceivePalette = new Palette<String>("preReceiveScripts",
+ new ListModel<String>(preReceiveScripts), new CollectionModel<String>(GitBlit
+ .self().getPreReceiveScriptsUnused(null)), new StringChoiceRenderer(),
+ 12, true);
+
+ // post-receive palette
+ if (teamModel.postReceiveScripts != null) {
+ postReceiveScripts.addAll(teamModel.postReceiveScripts);
+ }
+ final Palette<String> postReceivePalette = new Palette<String>("postReceiveScripts",
+ new ListModel<String>(postReceiveScripts), new CollectionModel<String>(GitBlit
+ .self().getPostReceiveScriptsUnused(null)), new StringChoiceRenderer(),
+ 12, true);
+
+ Form<TeamModel> form = new Form<TeamModel>("editForm", model) {
+
+ private static final long serialVersionUID = 1L;
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.apache.wicket.markup.html.form.Form#onSubmit()
+ */
+ @Override
+ protected void onSubmit() {
+ String teamname = teamModel.name;
+ if (StringUtils.isEmpty(teamname)) {
+ error(getString("gb.pleaseSetTeamName"));
+ return;
+ }
+ if (isCreate) {
+ TeamModel model = GitBlit.self().getTeamModel(teamname);
+ if (model != null) {
+ error(MessageFormat.format(getString("gb.teamNameUnavailable"), teamname));
+ return;
+ }
+ }
+ // update team permissions
+ for (RegistrantAccessPermission repositoryPermission : permissions) {
+ teamModel.setRepositoryPermission(repositoryPermission.registrant, repositoryPermission.permission);
+ }
+
+ Iterator<String> selectedUsers = users.getSelectedChoices();
+ List<String> members = new ArrayList<String>();
+ while (selectedUsers.hasNext()) {
+ members.add(selectedUsers.next().toLowerCase());
+ }
+ teamModel.users.clear();
+ teamModel.users.addAll(members);
+
+ // set mailing lists
+ String ml = mailingLists.getObject();
+ if (!StringUtils.isEmpty(ml)) {
+ Set<String> list = new HashSet<String>();
+ for (String address : ml.split("(,|\\s)")) {
+ if (StringUtils.isEmpty(address)) {
+ continue;
+ }
+ list.add(address.toLowerCase());
+ }
+ teamModel.mailingLists.clear();
+ teamModel.mailingLists.addAll(list);
+ }
+
+ // pre-receive scripts
+ List<String> preReceiveScripts = new ArrayList<String>();
+ Iterator<String> pres = preReceivePalette.getSelectedChoices();
+ while (pres.hasNext()) {
+ preReceiveScripts.add(pres.next());
+ }
+ teamModel.preReceiveScripts.clear();
+ teamModel.preReceiveScripts.addAll(preReceiveScripts);
+
+ // post-receive scripts
+ List<String> postReceiveScripts = new ArrayList<String>();
+ Iterator<String> post = postReceivePalette.getSelectedChoices();
+ while (post.hasNext()) {
+ postReceiveScripts.add(post.next());
+ }
+ teamModel.postReceiveScripts.clear();
+ teamModel.postReceiveScripts.addAll(postReceiveScripts);
+
+ try {
+ GitBlit.self().updateTeamModel(oldName, teamModel, isCreate);
+ } catch (GitBlitException e) {
+ error(e.getMessage());
+ return;
+ }
+ setRedirect(false);
+ if (isCreate) {
+ // create another team
+ info(MessageFormat.format(getString("gb.teamCreated"),
+ teamModel.name));
+ }
+ // back to users page
+ setResponsePage(UsersPage.class);
+ }
+ };
+
+ // do not let the browser pre-populate these fields
+ form.add(new SimpleAttributeModifier("autocomplete", "off"));
+
+ // not all user services support manipulating team memberships
+ boolean editMemberships = GitBlit.self().supportsTeamMembershipChanges(null);
+
+ // field names reflective match TeamModel fields
+ form.add(new TextField<String>("name"));
+ form.add(new CheckBox("canAdmin"));
+ form.add(new CheckBox("canFork").setEnabled(GitBlit.getBoolean(Keys.web.allowForking, true)));
+ form.add(new CheckBox("canCreate"));
+ form.add(users.setEnabled(editMemberships));
+ mailingLists = new Model<String>(teamModel.mailingLists == null ? ""
+ : StringUtils.flattenStrings(teamModel.mailingLists, " "));
+ form.add(new TextField<String>("mailingLists", mailingLists));
+
+ form.add(new RegistrantPermissionsPanel("repositories", RegistrantType.REPOSITORY,
+ repos, permissions, getAccessPermissions()));
+ form.add(preReceivePalette);
+ form.add(new BulletListPanel("inheritedPreReceive", "inherited", GitBlit.self()
+ .getPreReceiveScriptsInherited(null)));
+ form.add(postReceivePalette);
+ form.add(new BulletListPanel("inheritedPostReceive", "inherited", GitBlit.self()
+ .getPostReceiveScriptsInherited(null)));
+
+ form.add(new Button("save"));
+ Button cancel = new Button("cancel") {
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public void onSubmit() {
+ setResponsePage(UsersPage.class);
+ }
+ };
+ cancel.setDefaultFormProcessing(false);
+ form.add(cancel);
+
+ add(form);
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/pages/EditUserPage.html b/src/main/java/com/gitblit/wicket/pages/EditUserPage.html
new file mode 100644
index 00000000..e79011c8
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/EditUserPage.html
@@ -0,0 +1,78 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+
+<wicket:extend>
+<body onload="document.getElementById('username').focus();">
+<!-- User Table -->
+<form style="padding-top:5px;" wicket:id="editForm">
+
+<div class="tabbable">
+ <!-- tab titles -->
+ <ul class="nav nav-tabs">
+ <li class="active"><a href="#general" data-toggle="tab"><wicket:message key="gb.general"></wicket:message></a></li>
+ <li><a href="#attributes" data-toggle="tab"><wicket:message key="gb.attributes"></wicket:message></a></li>
+ <li><a href="#permissions" data-toggle="tab"><wicket:message key="gb.accessPermissions"></wicket:message></a></li>
+ </ul>
+
+ <!-- tab content -->
+ <div class="tab-content">
+
+ <!-- general tab -->
+ <div class="tab-pane active" id="general">
+ <table class="plain">
+ <tbody class="settings">
+ <tr><th><wicket:message key="gb.username"></wicket:message></th><td class="edit"><input type="text" wicket:id="username" id="username" size="30" tabindex="1" /></td></tr>
+ <tr><th><wicket:message key="gb.password"></wicket:message></th><td class="edit"><input type="password" wicket:id="password" size="30" tabindex="2" /></td></tr>
+ <tr><th><wicket:message key="gb.confirmPassword"></wicket:message></th><td class="edit"><input type="password" wicket:id="confirmPassword" size="30" tabindex="3" /></td></tr>
+ <tr><th><wicket:message key="gb.displayName"></wicket:message></th><td class="edit"><input type="text" wicket:id="displayName" size="30" tabindex="4" /></td></tr>
+ <tr><th><wicket:message key="gb.emailAddress"></wicket:message></th><td class="edit"><input type="text" wicket:id="emailAddress" size="30" tabindex="5" /></td></tr>
+ <tr><th><wicket:message key="gb.canAdmin"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="canAdmin" tabindex="6" /> &nbsp;<span class="help-inline"><wicket:message key="gb.canAdminDescription"></wicket:message></span></label></td></tr>
+ <tr><th><wicket:message key="gb.canCreate"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="canCreate" tabindex="7" /> &nbsp;<span class="help-inline"><wicket:message key="gb.canCreateDescription"></wicket:message></span></label></td></tr>
+ <tr><th><wicket:message key="gb.canFork"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="canFork" tabindex="8" /> &nbsp;<span class="help-inline"><wicket:message key="gb.canForkDescription"></wicket:message></span></label></td></tr>
+ <tr><th><wicket:message key="gb.excludeFromFederation"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="excludeFromFederation" tabindex="9" /> &nbsp;<span class="help-inline"><wicket:message key="gb.excludeFromFederationDescription"></wicket:message></span></label></td></tr>
+ </tbody>
+ </table>
+ </div>
+
+ <!-- attributes tab -->
+ <div class="tab-pane" id="attributes">
+ <table class="plain">
+ <tbody class="settings">
+ <tr><th><wicket:message key="gb.organizationalUnit"></wicket:message> (OU)</th><td class="edit"><input type="text" wicket:id="organizationalUnit" size="30" tabindex="1" /></td></tr>
+ <tr><th><wicket:message key="gb.organization"></wicket:message> (O)</th><td class="edit"><input type="text" wicket:id="organization" size="30" tabindex="2" /></td></tr>
+ <tr><th><wicket:message key="gb.locality"></wicket:message> (L)</th><td class="edit"><input type="text" wicket:id="locality" size="30" tabindex="3" /></td></tr>
+ <tr><th><wicket:message key="gb.stateProvince"></wicket:message> (ST)</th><td class="edit"><input type="text" wicket:id="stateProvince" size="30" tabindex="4" /></td></tr>
+ <tr><th><wicket:message key="gb.countryCode"></wicket:message> (C)</th><td class="edit"><input type="text" wicket:id="countryCode" size="15 " tabindex="5" /></td></tr>
+ </tbody>
+ </table>
+ </div>
+
+ <!-- access permissions tab -->
+ <div class="tab-pane" id="permissions">
+ <table class="plain">
+ <tbody class="settings">
+ <tr><th><wicket:message key="gb.teamMemberships"></wicket:message></th><td style="padding:2px;"><span wicket:id="teams"></span></td></tr>
+ <tr><td colspan="2"><hr></hr></td></tr>
+ <tr><th><wicket:message key="gb.repositoryPermissions"></wicket:message></th>
+ <td style="padding:2px;">
+ <div wicket:id="repositories"></div>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
+</div>
+
+<div class="row">
+ <div class="form-actions"><input class="btn btn-primary" type="submit" value="Save" wicket:message="value:gb.save" wicket:id="save" tabindex="9" /> &nbsp; <input class="btn" type="submit" value="Cancel" wicket:message="value:gb.cancel" wicket:id="cancel" tabindex="10" /></div>
+</div>
+
+</form>
+</body>
+
+</wicket:extend>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/EditUserPage.java b/src/main/java/com/gitblit/wicket/pages/EditUserPage.java
new file mode 100644
index 00000000..c060f237
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/EditUserPage.java
@@ -0,0 +1,261 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.pages;
+
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+import org.apache.wicket.PageParameters;
+import org.apache.wicket.behavior.SimpleAttributeModifier;
+import org.apache.wicket.extensions.markup.html.form.palette.Palette;
+import org.apache.wicket.markup.html.form.Button;
+import org.apache.wicket.markup.html.form.CheckBox;
+import org.apache.wicket.markup.html.form.Form;
+import org.apache.wicket.markup.html.form.PasswordTextField;
+import org.apache.wicket.markup.html.form.TextField;
+import org.apache.wicket.model.CompoundPropertyModel;
+import org.apache.wicket.model.Model;
+import org.apache.wicket.model.util.CollectionModel;
+import org.apache.wicket.model.util.ListModel;
+
+import com.gitblit.Constants.RegistrantType;
+import com.gitblit.GitBlit;
+import com.gitblit.GitBlitException;
+import com.gitblit.Keys;
+import com.gitblit.models.RegistrantAccessPermission;
+import com.gitblit.models.TeamModel;
+import com.gitblit.models.UserModel;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.wicket.RequiresAdminRole;
+import com.gitblit.wicket.StringChoiceRenderer;
+import com.gitblit.wicket.WicketUtils;
+import com.gitblit.wicket.panels.RegistrantPermissionsPanel;
+
+@RequiresAdminRole
+public class EditUserPage extends RootSubPage {
+
+ private final boolean isCreate;
+
+ public EditUserPage() {
+ // create constructor
+ super();
+ if (!GitBlit.self().supportsAddUser()) {
+ error(MessageFormat.format(getString("gb.userServiceDoesNotPermitAddUser"),
+ GitBlit.getString(Keys.realm.userService, "${baseFolder}/users.conf")), true);
+ }
+ isCreate = true;
+ setupPage(new UserModel(""));
+ setStatelessHint(false);
+ setOutputMarkupId(true);
+ }
+
+ public EditUserPage(PageParameters params) {
+ // edit constructor
+ super(params);
+ isCreate = false;
+ String name = WicketUtils.getUsername(params);
+ UserModel model = GitBlit.self().getUserModel(name);
+ setupPage(model);
+ setStatelessHint(false);
+ setOutputMarkupId(true);
+ }
+
+ @Override
+ protected boolean requiresPageMap() {
+ return true;
+ }
+
+ protected void setupPage(final UserModel userModel) {
+ if (isCreate) {
+ super.setupPage(getString("gb.newUser"), "");
+ } else {
+ super.setupPage(getString("gb.edit"), userModel.username);
+ }
+
+ final Model<String> confirmPassword = new Model<String>(
+ StringUtils.isEmpty(userModel.password) ? "" : userModel.password);
+ CompoundPropertyModel<UserModel> model = new CompoundPropertyModel<UserModel>(userModel);
+
+ // build list of projects including all repositories wildcards
+ List<String> repos = getAccessRestrictedRepositoryList(true, userModel);
+
+ List<String> userTeams = new ArrayList<String>();
+ for (TeamModel team : userModel.teams) {
+ userTeams.add(team.name);
+ }
+ Collections.sort(userTeams);
+
+ final String oldName = userModel.username;
+ final List<RegistrantAccessPermission> permissions = GitBlit.self().getUserAccessPermissions(userModel);
+
+ final Palette<String> teams = new Palette<String>("teams", new ListModel<String>(
+ new ArrayList<String>(userTeams)), new CollectionModel<String>(GitBlit.self()
+ .getAllTeamnames()), new StringChoiceRenderer(), 10, false);
+ Form<UserModel> form = new Form<UserModel>("editForm", model) {
+
+ private static final long serialVersionUID = 1L;
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.apache.wicket.markup.html.form.Form#onSubmit()
+ */
+ @Override
+ protected void onSubmit() {
+ if (StringUtils.isEmpty(userModel.username)) {
+ error(getString("gb.pleaseSetUsername"));
+ return;
+ }
+ // force username to lower-case
+ userModel.username = userModel.username.toLowerCase();
+ String username = userModel.username;
+ if (isCreate) {
+ UserModel model = GitBlit.self().getUserModel(username);
+ if (model != null) {
+ error(MessageFormat.format(getString("gb.usernameUnavailable"), username));
+ return;
+ }
+ }
+ boolean rename = !StringUtils.isEmpty(oldName)
+ && !oldName.equalsIgnoreCase(username);
+ if (GitBlit.self().supportsCredentialChanges(userModel)) {
+ if (!userModel.password.equals(confirmPassword.getObject())) {
+ error(getString("gb.passwordsDoNotMatch"));
+ return;
+ }
+ String password = userModel.password;
+ if (!password.toUpperCase().startsWith(StringUtils.MD5_TYPE)
+ && !password.toUpperCase().startsWith(StringUtils.COMBINED_MD5_TYPE)) {
+ // This is a plain text password.
+ // Check length.
+ int minLength = GitBlit.getInteger(Keys.realm.minPasswordLength, 5);
+ if (minLength < 4) {
+ minLength = 4;
+ }
+ if (password.trim().length() < minLength) {
+ error(MessageFormat.format(getString("gb.passwordTooShort"),
+ minLength));
+ return;
+ }
+
+ // Optionally store the password MD5 digest.
+ String type = GitBlit.getString(Keys.realm.passwordStorage, "md5");
+ if (type.equalsIgnoreCase("md5")) {
+ // store MD5 digest of password
+ userModel.password = StringUtils.MD5_TYPE
+ + StringUtils.getMD5(userModel.password);
+ } else if (type.equalsIgnoreCase("combined-md5")) {
+ // store MD5 digest of username+password
+ userModel.password = StringUtils.COMBINED_MD5_TYPE
+ + StringUtils.getMD5(username + userModel.password);
+ }
+ } else if (rename
+ && password.toUpperCase().startsWith(StringUtils.COMBINED_MD5_TYPE)) {
+ error(getString("gb.combinedMd5Rename"));
+ return;
+ }
+ }
+
+ // update user permissions
+ for (RegistrantAccessPermission repositoryPermission : permissions) {
+ userModel.setRepositoryPermission(repositoryPermission.registrant, repositoryPermission.permission);
+ }
+
+ Iterator<String> selectedTeams = teams.getSelectedChoices();
+ userModel.teams.clear();
+ while (selectedTeams.hasNext()) {
+ TeamModel team = GitBlit.self().getTeamModel(selectedTeams.next());
+ if (team == null) {
+ continue;
+ }
+ userModel.teams.add(team);
+ }
+
+ try {
+ GitBlit.self().updateUserModel(oldName, userModel, isCreate);
+ } catch (GitBlitException e) {
+ error(e.getMessage());
+ return;
+ }
+ setRedirect(false);
+ if (isCreate) {
+ // create another user
+ info(MessageFormat.format(getString("gb.userCreated"),
+ userModel.username));
+ setResponsePage(EditUserPage.class);
+ } else {
+ // back to users page
+ setResponsePage(UsersPage.class);
+ }
+ }
+ };
+
+ // do not let the browser pre-populate these fields
+ form.add(new SimpleAttributeModifier("autocomplete", "off"));
+
+ // not all user services support manipulating username and password
+ boolean editCredentials = GitBlit.self().supportsCredentialChanges(userModel);
+
+ // not all user services support manipulating display name
+ boolean editDisplayName = GitBlit.self().supportsDisplayNameChanges(userModel);
+
+ // not all user services support manipulating email address
+ boolean editEmailAddress = GitBlit.self().supportsEmailAddressChanges(userModel);
+
+ // not all user services support manipulating team memberships
+ boolean editTeams = GitBlit.self().supportsTeamMembershipChanges(userModel);
+
+ // field names reflective match UserModel fields
+ form.add(new TextField<String>("username").setEnabled(editCredentials));
+ PasswordTextField passwordField = new PasswordTextField("password");
+ passwordField.setResetPassword(false);
+ form.add(passwordField.setEnabled(editCredentials));
+ PasswordTextField confirmPasswordField = new PasswordTextField("confirmPassword",
+ confirmPassword);
+ confirmPasswordField.setResetPassword(false);
+ form.add(confirmPasswordField.setEnabled(editCredentials));
+ form.add(new TextField<String>("displayName").setEnabled(editDisplayName));
+ form.add(new TextField<String>("emailAddress").setEnabled(editEmailAddress));
+ form.add(new CheckBox("canAdmin"));
+ form.add(new CheckBox("canFork").setEnabled(GitBlit.getBoolean(Keys.web.allowForking, true)));
+ form.add(new CheckBox("canCreate"));
+ form.add(new CheckBox("excludeFromFederation"));
+ form.add(new RegistrantPermissionsPanel("repositories", RegistrantType.REPOSITORY, repos, permissions, getAccessPermissions()));
+ form.add(teams.setEnabled(editTeams));
+
+ form.add(new TextField<String>("organizationalUnit").setEnabled(editDisplayName));
+ form.add(new TextField<String>("organization").setEnabled(editDisplayName));
+ form.add(new TextField<String>("locality").setEnabled(editDisplayName));
+ form.add(new TextField<String>("stateProvince").setEnabled(editDisplayName));
+ form.add(new TextField<String>("countryCode").setEnabled(editDisplayName));
+ form.add(new Button("save"));
+ Button cancel = new Button("cancel") {
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public void onSubmit() {
+ setResponsePage(UsersPage.class);
+ }
+ };
+ cancel.setDefaultFormProcessing(false);
+ form.add(cancel);
+
+ add(form);
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage.html b/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage.html
new file mode 100644
index 00000000..d46a5ded
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+
+<body>
+<wicket:extend>
+
+ <h2>Empty Repository</h2>
+ <p></p>
+ <div class="row">
+ <div class="span10">
+ <div class="alert alert-success">
+ <span wicket:id="repository" style="font-weight: bold;">[repository]</span> is an empty repository and can not be viewed by Gitblit.
+ <p></p>
+ Please push some commits to <span wicket:id="pushurl"></span>
+ <p></p>
+ <hr/>
+ After you have pushed commits you may <b>refresh</b> this page to view your repository.
+ </div>
+ </div>
+ </div>
+
+ <h3>Git Command-Line Syntax</h3>
+ <span style="padding-bottom:5px;">If you do not have a local Git repository, then you should clone this repository, commit some files, and then push your commits back to Gitblit.</span>
+ <p></p>
+ <pre style="padding: 5px 30px;" wicket:id="cloneSyntax"></pre>
+ <p></p>
+ <span style="padding-bottom:5px;">If you already have a local Git repository with commits, then you may add this repository as a remote and push to it.</span>
+ <p></p>
+ <pre wicket:id="remoteSyntax" style="padding: 5px 30px;"></pre>
+ <p></p>
+ <h3>Learn Git</h3>
+ If you are unsure how to use this information, consider reviewing the <a href="http://book.git-scm.com">Git Community Book</a> or <a href="http://progit.org/book" target="_blank">Pro Git</a> for a better understanding on how to use Git.
+ <p></p>
+ <h4>Open Source Git Clients</h4>
+ <ul>
+ <li><a href="http://git-scm.com">Git</a> - the official, command-line Git</li>
+ <li><a href="http://tortoisegit.googlecode.com">TortoiseGit</a> - Windows file explorer integration (requires official, command-line Git)</li>
+ <li><a href="http://eclipse.org/egit">Eclipse/EGit</a> - Git for the Eclipse IDE (based on JGit, like Gitblit)</li>
+ <li><a href="https://code.google.com/p/gitextensions/">Git Extensions</a> - C# frontend for Git that features Windows Explorer and Visual Studio integration</li>
+ <li><a href="http://gitx.laullon.com/">GitX (L)</a> - a Mac OS X Git client</li>
+ </ul>
+ <p></p>
+ <h4>Commercial/Closed-Source Git Clients</h4>
+ <ul>
+ <li><a href="http://www.syntevo.com/smartgit">SmartGit</a> - A Java Git, Mercurial, and SVN client application (requires official, command-line Git)</li>
+ <li><a href="http://www.sourcetreeapp.com/">SourceTree</a> - A free Mac Client for Git, Mercurial, and SVN</li>
+ </ul>
+</wicket:extend>
+</body>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage.java b/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage.java
new file mode 100644
index 00000000..be0dad9e
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.pages;
+
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.wicket.PageParameters;
+import org.apache.wicket.markup.html.basic.Label;
+
+import com.gitblit.GitBlit;
+import com.gitblit.Keys;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.utils.ArrayUtils;
+import com.gitblit.wicket.GitblitRedirectException;
+import com.gitblit.wicket.WicketUtils;
+import com.gitblit.wicket.panels.RepositoryUrlPanel;
+
+public class EmptyRepositoryPage extends RootPage {
+
+ public EmptyRepositoryPage(PageParameters params) {
+ super(params);
+
+ setVersioned(false);
+
+ String repositoryName = WicketUtils.getRepositoryName(params);
+ RepositoryModel repository = GitBlit.self().getRepositoryModel(repositoryName);
+ if (repository == null) {
+ error(getString("gb.canNotLoadRepository") + " " + repositoryName, true);
+ }
+
+ if (repository.hasCommits) {
+ // redirect to the summary page if this repository is not empty
+ throw new GitblitRedirectException(SummaryPage.class, params);
+ }
+
+ setupPage(repositoryName, getString("gb.emptyRepository"));
+
+ List<String> repositoryUrls = new ArrayList<String>();
+
+ if (GitBlit.getBoolean(Keys.git.enableGitServlet, true)) {
+ // add the Gitblit repository url
+ repositoryUrls.add(getRepositoryUrl(repository));
+ }
+ repositoryUrls.addAll(GitBlit.self().getOtherCloneUrls(repositoryName));
+
+ String primaryUrl = ArrayUtils.isEmpty(repositoryUrls) ? "" : repositoryUrls.get(0);
+ add(new Label("repository", repositoryName));
+ add(new RepositoryUrlPanel("pushurl", primaryUrl));
+ add(new Label("cloneSyntax", MessageFormat.format("git clone {0}", repositoryUrls.get(0))));
+ add(new Label("remoteSyntax", MessageFormat.format("git remote add gitblit {0}\ngit push gitblit master", primaryUrl)));
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_es.html b/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_es.html
new file mode 100644
index 00000000..2849fc70
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_es.html
@@ -0,0 +1,56 @@
+
+
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="es">
+
+<body>
+<wicket:extend>
+
+ <h2>Repositorio Vac&iacute;o</h2>
+ <p></p>
+ <div class="row">
+ <div class="span7">
+ <div class="alert alert-success">
+ <span wicket:id="repository" style="font-weight: bold;">[repository]</span> es un repositorio vac&iacute;o y no puede ser visto en Gitblit.
+ <p></p>
+ Por favor, empuja algunas consignas a <span wicket:id="pushurl"></span>
+ <p></p>
+ <hr/>
+ Despu&eacute;s de empujar tus consignas puedes <b>refrescar</b> &eacute;sta p&aacute;gina para ver tu Repositorio.
+ </div>
+ </div>
+ </div>
+
+ <h3>Sintaxis de la L&iacute;nea de Comandos de Git</h3>
+ <span style="padding-bottom:5px;">Si no tienes un Repositiorio local Git, puedes clonar &eacute;ste, consignar algunos archivos, y despu&eacute;s empujar las consignas de vuelta a Gitblit.</span>
+ <p></p>
+ <pre style="padding: 5px 30px;" wicket:id="cloneSyntax"></pre>
+ <p></p>
+ <span style="padding-bottom:5px;">Si ya tienes un repositorio local Git con algunas consignas, puedes a&ntilde;adir &eacute;ste como remoto y empujar desde all&iacute;.</span>
+ <p></p>
+ <pre style="padding: 5px 30px;" wicket:id="remoteSyntax"></pre>
+ <p></p>
+ <h3>Aprender Git</h3>
+ Si no est&aacute;s seguro de como usar esta informaci&oacute;n, &eacute;chale un vistazo al <a href="http://book.git-scm.com">Libro de la cominidad Git</a> o <a href="http://progit.org/book" target="_blank">Pro Git</a> para una mejor compresi&oacute;n de como usar Git.
+ <p></p>
+ <h4>Clientes Git de C&oacute;digo abierto.</h4>
+ <ul>
+ <li><a href="http://git-scm.com">Git</a> - El Git oficial en l&iacute;nea de comandos</li>
+ <li><a href="http://tortoisegit.googlecode.com">TortoiseGit</a> - Explorador de archivos integrado en Windows (necesita Git oficial en l&iacute;nea de comandos)</li>
+ <li><a href="http://eclipse.org/egit">Eclipse/EGit</a> - Git para el IDE de Eclipse (basado en JGit, como Gitblit)</li>
+ <li><a href="https://code.google.com/p/gitextensions/">Git Extensions</a> - Interfaz de usuario gr&aacute;fico Git en C# con integraci&oacute;n en IE y en Visual Studio</li>
+ <li><a href="http://gitx.laullon.com/">GitX (L)</a> - Cliente Git para Mac OS X</li>
+ </ul>
+ <p></p>
+ <h4>Clientes Git comerciales</h4>
+ <ul>
+ <li><a href="http://www.syntevo.com/smartgit">SmartGit</a> - aplicaci&oacute;n Java (necesita Git oficial en l&iacute;nea de comandos)</li>
+ <li><a href="http://www.sourcetreeapp.com/">SourceTree</a> - Un cliente Git gratuito para Mac, Mercurial, y SVN</li>
+ </ul>
+</wicket:extend>
+</body>
+</html>
+
diff --git a/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_ko.html b/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_ko.html
new file mode 100644
index 00000000..591335e4
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_ko.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+
+<body>
+<wicket:extend>
+
+ <h2>비어있는 저장소</h2>
+ <p></p>
+ <div class="row">
+ <div class="span10">
+ <div class="alert alert-success">
+ <span wicket:id="repository" style="font-weight: bold;">[repository]</span> 저장소는 비어 있어서 Gitblit ì—ì„œ ë³¼ 수 없습니다.
+ <p></p>
+ ì´ Git url ì— ì»¤ë°‹í•´ 주세요. <span wicket:id="pushurl"></span>
+ <p></p>
+ <hr/>
+ After you have pushed commits you may <b>refresh</b> this page to view your repository.
+ </div>
+ </div>
+ </div>
+
+ <p></p>
+ <h3>Git 명령어</h3>
+ <span style="padding-bottom:5px;">로컬 Git 저장소가 없다면, ì´ ì €ìž¥ì†Œë¥¼ í´ë¡ (clone) í•œ 후, 몇 파ì¼ì„ 커밋하고, ê·¸ ì»¤ë°‹ì„ Gitblit ì— í‘¸ì‹œ(push) 하세요.</span>
+ <p></p>
+ <pre style="padding: 5px 30px;" wicket:id="cloneSyntax"></pre>
+ <p></p>
+ <span style="padding-bottom:5px;">만약 ì»¤ë°‹ëœ ë¡œì»¬ Git 저장소가 있다면, 다ìŒê³¼ ê°™ì´ ì €ìž¥ì†Œì— ë¦¬ëª¨íŠ¸ë¥¼ 추가하고 푸시(push)í•  수 있습니다.</span>
+ <p></p>
+ <pre style="padding: 5px 30px;" wicket:id="remoteSyntax"></pre>
+ <p></p>
+ <h3>Git 배우기</h3>
+ 만약 ì‚¬ìš©ë²•ì— ìžì‹ ì´ 없다면, Git ì‚¬ìš©ë²•ì„ ë” ìž˜ ì´í•´í•˜ê¸° 위해
+ <a href="http://book.git-scm.com">Git Community Book</a> ë˜ëŠ”
+ <a href="http://progit.org/book" target="_blank">Pro Git</a>,
+ <a href="http://dogfeet.github.com/articles/2012/progit.html" target="_blank">Pro Git 한글</a> ì„ ë³¼ ê²ƒì„ ê³ ë ¤í•´ 보세요.
+ <p></p>
+ <h4>오픈소스 Git í´ë¼ì´ì–¸íŠ¸</h4>
+ <ul>
+ <li><a href="http://git-scm.com">Git</a> - 명령어 기반 ê³µì‹ Git</li>
+ <li><a href="http://tortoisegit.googlecode.com">TortoiseGit</a> - 윈ë„ì˜ íŒŒì¼ íƒìƒ‰ê¸°ì— í†µí•©ëœ UI í´ë¼ì´ì–¸íŠ¸ (명령어 기반 ê³µì‹ Git í•„ìš”)</li>
+ <li><a href="http://eclipse.org/egit">Eclipse/EGit</a> - ì´í´ë¦½ìŠ¤ IDE í”ŒëŸ¬ê·¸ì¸ (Gitblit ê³¼ ê°™ì€ JGit 기반)</li>
+ <li><a href="https://code.google.com/p/gitextensions/">Git Extensions</a> - C# frontend for Git that features Windows Explorer and Visual Studio integration</li>
+ <li><a href="http://gitx.laullon.com/">GitX (L)</a> - a Mac OS X Git client</li>
+ </ul>
+ <p></p>
+ <h4>유료 Git í´ë¼ì´ì–¸íŠ¸</h4>
+ <ul>
+ <li><a href="http://www.syntevo.com/smartgit">SmartGit</a> - ìžë°” 어플리케ì´ì…˜ (명령어 기반 ê³µì‹ Git í•„ìš”)</li>
+ <li><a href="http://www.sourcetreeapp.com/">SourceTree</a> - A free Mac Client for Git, Mercurial, and SVN</li>
+ </ul>
+</wicket:extend>
+</body>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_nl.html b/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_nl.html
new file mode 100644
index 00000000..a8ee2e25
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_nl.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="nl"
+ lang="nl">
+
+<body>
+<wicket:extend>
+
+ <h2>Empty Repository</h2>
+ <p></p>
+ <div class="row">
+ <div class="span10">
+ <div class="alert alert-success">
+ <span wicket:id="repository" style="font-weight: bold;">[repository]</span> is een lege repositorie en kan niet bekeken worden door Gitblit.
+ <p></p>
+ Push aub een paar commitsome commits naar <span wicket:id="pushurl"></span>
+ <p></p>
+ <hr/>
+ Nadat u een paar commits gepushed hebt kunt u deze pagina <b>verversen</b> om de repository te bekijken.
+ </div>
+ </div>
+ </div>
+
+ <h3>Git Command-Line Syntax</h3>
+ <span style="padding-bottom:5px;">Als u geen lokale Git repositorie heeft, kunt u deze repository clonen, er een paar bestanden naar committen en deze commits teug pushen naar Gitblit.</span>
+ <p></p>
+ <pre style="padding: 5px 30px;" wicket:id="cloneSyntax"></pre>
+ <p></p>
+ <span style="padding-bottom:5px;">Als u al een lokale Git repositorie heeft met commits kunt u deze repository als een remote toevoegen en er naar toe pushen.</span>
+ <p></p>
+ <pre wicket:id="remoteSyntax" style="padding: 5px 30px;"></pre>
+ <p></p>
+ <h3>Learn Git</h3>
+ Als u niet goed weet wat u met deze informatie aan moet raden we aan om het <a href="http://book.git-scm.com">Git Community Book</a> of <a href="http://progit.org/book" target="_blank">Pro Git</a> te bestuderen voor een betere begrip van hoe u Git kunt gebruiken.
+ <p></p>
+ <h4>Open Source Git Clients</h4>
+ <ul>
+ <li><a href="http://git-scm.com">Git</a> - de officiele, command-line Git</li>
+ <li><a href="http://tortoisegit.googlecode.com">TortoiseGit</a> - Windows bestandsverkenner ingetratie (officiele command-line Git is wel nodig)</li>
+ <li><a href="http://eclipse.org/egit">Eclipse/EGit</a> - Git voor de Eclipse IDE (gebaseerd op JGit, zoals Gitblit)</li>
+ <li><a href="https://code.google.com/p/gitextensions/">Git Extensions</a> - C# frontend voor Git met Windows Explorer en Visual Studio integratie</li>
+ <li><a href="http://gitx.laullon.com/">GitX (L)</a> - een Mac OS X Git client</li>
+ </ul>
+ <p></p>
+ <h4>Commercial/Closed-Source Git Clients</h4>
+ <ul>
+ <li><a href="http://www.syntevo.com/smartgit">SmartGit</a> - Een Java Git, Mercurial, en SVN client applicatie (officiele command-line Git is wel nodig)</li>
+ <li><a href="http://www.sourcetreeapp.com/">SourceTree</a> - Een gratis Mac Client voor Git, Mercurial, en SVN</li>
+ </ul>
+</wicket:extend>
+</body>
+</html>
diff --git a/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_pl.html b/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_pl.html
new file mode 100644
index 00000000..109899aa
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_pl.html
@@ -0,0 +1,56 @@
+
+
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="es">
+
+<body>
+<wicket:extend>
+
+ <h2>Puste repozytorium</h2>
+ <p></p>
+ <div class="row">
+ <div class="span10">
+ <div class="alert alert-success">
+ <span wicket:id="repository" style="font-weight: bold;">[repository]</span> jest pustym repozytorium i nie mo&#380;e by&#263; zaprezentowane przez Gitblit.
+ <p></p>
+ Wgraj, poprzez push, dowolne zmiany do lokalizacji <span wicket:id="pushurl"></span>
+ <p></p>
+ <hr/>
+ Po wgraniu zmian <b>od&#347;wie&#380;</b> stron&#281;, aby podejrze&#263; repozytorium.
+ </div>
+ </div>
+ </div>
+
+ <h3>Sk&#322;adnia linii polece&#324; GITa</h3>
+ <span style="padding-bottom:5px;">Je&#347;li nie posiadasz lokalnego repozytorium GITa to sklonuj to repozytorium, wgraj dowolne pliki, a nast&#281;pnie wy&#347;lij poprzez push zmiany na Gitblit.</span>
+ <p></p>
+ <pre style="padding: 5px 30px;" wicket:id="cloneSyntax"></pre>
+ <p></p>
+ <span style="padding-bottom:5px;">Gdy posiadasz lokalne repozytorium GITa z dowolnymi zmianami, to mo&#380;esz doda&#263; to repozytorium jako remote i wys&#322;a&#263; do niego zmiany poprzez push.</span>
+ <p></p>
+ <pre style="padding: 5px 30px;" wicket:id="remoteSyntax"></pre>
+ <p></p>
+ <h3>Nauka GITa</h3>
+ Je&#380;eli powy&#380;sze informacje s&#261; dla Ciebie niezrozumia&#322;e, zapoznaj si&#281; z ksi&#261;&#380;k&#261; <a href="http://git-scm.com/book/pl" target="_blank">Pro Git - Wersja PL</a> dla lepszego zrozumienia, jak poprawnie u&#380;ywa&#263; GITa.
+ <p></p>
+ <h4>Darmowi klienci GITa</h4>
+ <ul>
+ <li><a href="http://git-scm.com">Git</a> - Oficjalny klient, dost&#281;pny przez lini&#281; polece&#324;</li>
+ <li><a href="http://tortoisegit.googlecode.com">TortoiseGit</a> - Rozszerzenie eksploratora Windows (wymaga oficjalnego, dost&#281;pnego przez lini&#281; polece&#324; klienta)</li>
+ <li><a href="http://eclipse.org/egit">Eclipse/EGit</a> - GIT dla edytora Eclipse (oparty o JGit, podobnie jak Gitblit)</li>
+ <li><a href="https://code.google.com/p/gitextensions/">Git Extensions</a> - napisana w C# fasada na GIT, udost&#281;pniaj&#261;ca integracj&#281; dla Windows Explorer oraz Visual Studio</li>
+ <li><a href="http://gitx.laullon.com/">GitX (L)</a> - klient GIT na Mac OS X</li>
+ </ul>
+ <p></p>
+ <h4>Komercyjni klienci GITa</h4>
+ <ul>
+ <li><a href="http://www.syntevo.com/smartgit">SmartGit</a> - aplikacja napisana w Javie (wymaga oficjalnego, dost&#281;pnego przez lini&#281; polece&#324; klienta)</li>
+ <li><a href="http://www.sourcetreeapp.com/">SourceTree</a> - darmowy klient GIT, Mercurial i SVN na Mac OS X</li>
+ </ul>
+</wicket:extend>
+</body>
+</html>
+
diff --git a/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_pt_BR.html b/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_pt_BR.html
new file mode 100644
index 00000000..351ef879
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_pt_BR.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="pt-br"
+ lang="pt-br">
+
+<body>
+<wicket:extend>
+
+ <h2>Repositório Vazio</h2>
+ <p></p>
+ <div class="row">
+ <div class="span10">
+ <div class="alert alert-success">
+ <span wicket:id="repository" style="font-weight: bold;">[repository]</span> é um repositório vazio e não pode ser visualizado pelo Gitblit.
+ <p></p>
+ Por favor faça o push de alguns commits para <span wicket:id="pushurl"></span>
+ <p></p>
+ <hr/>
+ Depois de ter feito push você poderá <b>atualizar</b> esta página para visualizar seu repositório.
+ </div>
+ </div>
+ </div>
+
+ <h3>Sintaxe dos comandos do Git</h3>
+ <span style="padding-bottom:5px;">Se você ainda não tem um repositório local do Git, então você deve primeiro clonar este repositório, fazer commit de alguns arquivos e então fazer push desses commits para o Gitblit.</span>
+ <p></p>
+ <pre style="padding: 5px 30px;" wicket:id="cloneSyntax"></pre>
+ <p></p>
+ <span style="padding-bottom:5px;">Se você já tem um repositório Git local com alguns commits, então você deve adicionar este repositório como uma referência remota e então fazer push.</span>
+ <p></p>
+ <pre wicket:id="remoteSyntax" style="padding: 5px 30px;"></pre>
+ <p></p>
+ <h3>Aprenda Git</h3>
+ Se você estiver com dúvidas sobre como ultilizar essas informações, uma sugestão seria dar uma olhada no livro <a href="http://book.git-scm.com">Git Community Book</a> ou <a href="http://progit.org/book" target="_blank">Pro Git</a> para entender melhor como usar o Git.
+ <p></p>
+ <h4>Alguns clients do Git que são Open Source</h4>
+ <ul>
+ <li><a href="http://git-scm.com">Git</a> - o Git oficial através de linhas de comando</li>
+ <li><a href="http://tortoisegit.googlecode.com">TortoiseGit</a> - Faz integração do Explorer do Windows com o Git (por isso requer o Git Oficial)</li>
+ <li><a href="http://eclipse.org/egit">Eclipse/EGit</a> - Git para a IDE Eclipse (baseada no JGit, como o Gitblit)</li>
+ <li><a href="https://code.google.com/p/gitextensions/">Git Extensions</a> - Interface (em C#) para o Git cuja a característica é a integração com o Windows Explorer e o Visual Studio</li>
+ <li><a href="http://gitx.laullon.com/">GitX (L)</a> - um Cliente do Git para Mac OS X</li>
+ </ul>
+ <p></p>
+ <h4>Clients do Git proprietários ou com Código Fechado</h4>
+ <ul>
+ <li><a href="http://www.syntevo.com/smartgit">SmartGit</a> - Aplicação Client (em Java) para Git, Mercurial, e SVN (por isso requer o Git Oficial)</li>
+ <li><a href="http://www.sourcetreeapp.com/">SourceTree</a> - Client gratuito para o Mac que suporta Git, Mercurial e SVN</li>
+ </ul>
+</wicket:extend>
+</body>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_zh_CN.html b/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_zh_CN.html
new file mode 100644
index 00000000..4b21800e
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_zh_CN.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="zh-CN"
+ lang="zh-CN">
+
+<body>
+<wicket:extend>
+
+ <h2>空版本库</h2>
+ <p></p>
+ <div class="row">
+ <div class="span10">
+ <div class="alert alert-success">
+ <span wicket:id="repository" style="font-weight: bold;">[repository]</span> 版本库目å‰ä¸ºç©ºã€‚
+ Gitblit 无法查看。
+ <p></p>
+ 请往此网å€è¿›è¡ŒæŽ¨é€ <span wicket:id="pushurl"></span>
+ <p></p>
+ <hr/>
+ 当你推é€å®Œæ¯•åŽä½ å¯ä»¥ <b>刷新</b> 此页é¢é‡æ–°æŸ¥çœ‹æ‚¨çš„版本库。
+ </div>
+ </div>
+ </div>
+
+ <h3>Git 命令行格å¼</h3>
+ <span style="padding-bottom:5px;">如果您没有本地 Git 版本库, 您å¯ä»¥å…‹éš†æ­¤ç‰ˆæœ¬åº“, æ交一些文件, 然åŽå°†æ‚¨çš„æ交推é€å›žGitblit。</span>
+ <p></p>
+ <pre style="padding: 5px 30px;" wicket:id="cloneSyntax"></pre>
+ <p></p>
+ <span style="padding-bottom:5px;">如果您已ç»æœ‰ä¸€ä¸ªæœ¬åœ°çš„æ交过的版本库, 那么您å¯ä»¥å°†æ­¤ç‰ˆæœ¬åº“加为远程
+ 版本库,并进行推é€ã€‚</span>
+ <p></p>
+ <pre wicket:id="remoteSyntax" style="padding: 5px 30px;"></pre>
+ <p></p>
+ <h3>学习 Git</h3>
+ 如果您ä¸æ˜Žç™½è¿™äº›ä¿¡æ¯ä»€ä¹ˆæ„æ€, 您å¯ä»¥å‚考 <a href="http://book.git-scm.com">Git Community Book</a> 或者 <a href="http://progit.org/book" target="_blank">Pro Git</a> 去更加深入的学习 Git 的用法。
+ <p></p>
+ <h4>å¼€æº Git 客户端</h4>
+ <ul>
+ <li><a href="http://git-scm.com">Git</a> - 官方, 命令行版本 Git</li>
+ <li><a href="http://tortoisegit.googlecode.com">TortoiseGit</a> - 与 Windows 资æºç®¡ç†å™¨é›†æˆ (需è¦å®˜æ–¹, 命令行 Git 的支æŒ)</li>
+ <li><a href="http://eclipse.org/egit">Eclipse/EGit</a> - Git for the Eclipse IDE (基于 JGit, 类似 Gitblit)</li>
+ <li><a href="https://code.google.com/p/gitextensions/">Git Extensions</a> - C# 版本的 Git å‰ç«¯ï¼Œä¸Ž Windows 资æºç®¡ç†å™¨å’Œ Visual Studio 集æˆ</li>
+ <li><a href="http://gitx.laullon.com/">GitX (L)</a> - Mac OS X Git 客户端</li>
+ </ul>
+ <p></p>
+ <h4>商业/é—­æº Git 客户端</h4>
+ <ul>
+ <li><a href="http://www.syntevo.com/smartgit">SmartGit</a> - Java ç‰ˆæœ¬çš„æ”¯æŒ Git, Mercurial å’Œ SVN 客户端应用 (需è¦å®˜æ–¹, 命令行 Git 的支æŒ)</li>
+ <li><a href="http://www.sourcetreeapp.com/">SourceTree</a> - å…费的 Mac Git Mercurial ä»¥åŠ SVN 客户端, Mercurial, and SVN</li>
+ </ul>
+</wicket:extend>
+</body>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/FederationPage.html b/src/main/java/com/gitblit/wicket/pages/FederationPage.html
new file mode 100644
index 00000000..bb39d345
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/FederationPage.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+<body>
+<wicket:extend>
+
+ <div wicket:id="federationTokensPanel">[federation tokens panel]</div>
+
+ <div style="padding-top: 10px;" wicket:id="federationProposalsPanel">[federation proposals panel]</div>
+
+ <div style="padding-top: 10px;" wicket:id="federationRegistrationsPanel">[federation registrations panel]</div>
+
+</wicket:extend>
+</body>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/FederationPage.java b/src/main/java/com/gitblit/wicket/pages/FederationPage.java
new file mode 100644
index 00000000..1f98c172
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/FederationPage.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.pages;
+
+import com.gitblit.GitBlit;
+import com.gitblit.Keys;
+import com.gitblit.wicket.panels.FederationProposalsPanel;
+import com.gitblit.wicket.panels.FederationRegistrationsPanel;
+import com.gitblit.wicket.panels.FederationTokensPanel;
+
+public class FederationPage extends RootPage {
+
+ public FederationPage() {
+ super();
+ setupPage("", "");
+
+ boolean showFederation = showAdmin && GitBlit.canFederate();
+ add(new FederationTokensPanel("federationTokensPanel", showFederation)
+ .setVisible(showFederation));
+ FederationProposalsPanel proposalsPanel = new FederationProposalsPanel(
+ "federationProposalsPanel");
+ if (showFederation) {
+ proposalsPanel.hideIfEmpty();
+ } else {
+ proposalsPanel.setVisible(false);
+ }
+
+ boolean showRegistrations = GitBlit.getBoolean(Keys.web.showFederationRegistrations, false);
+ FederationRegistrationsPanel registrationsPanel = new FederationRegistrationsPanel(
+ "federationRegistrationsPanel");
+ if (showAdmin || showRegistrations) {
+ registrationsPanel.hideIfEmpty();
+ } else {
+ registrationsPanel.setVisible(false);
+ }
+ add(proposalsPanel);
+ add(registrationsPanel);
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/pages/FederationRegistrationPage.html b/src/main/java/com/gitblit/wicket/pages/FederationRegistrationPage.html
new file mode 100644
index 00000000..d7b9bdde
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/FederationRegistrationPage.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+
+<body>
+<wicket:extend>
+ <!-- registration info -->
+ <table class="plain">
+ <tr><th><wicket:message key="gb.url">url</wicket:message></th><td><img style="border:0px;vertical-align:middle;" wicket:id="typeIcon" /> <span wicket:id="url">[url]</span></td></tr>
+ <tr><th><wicket:message key="gb.token">token</wicket:message></th><td><span class="sha1" wicket:id="token">[token]</span></td></tr>
+ <tr><th><wicket:message key="gb.folder">folder</wicket:message></th><td><span wicket:id="folder">[folder]</span></td></tr>
+ <tr><th><wicket:message key="gb.frequency">frequency</wicket:message></th><td><span wicket:id="frequency">[frequency]</span></td></tr>
+ <tr><th><wicket:message key="gb.lastPull">lastPull</wicket:message></th><td><span wicket:id="lastPull">[lastPull]</span></td></tr>
+ <tr><th><wicket:message key="gb.nextPull">nextPull</wicket:message></th><td><span wicket:id="nextPull">[nextPull]</span></td></tr>
+ <tr><th valign="top"><wicket:message key="gb.exclusions">exclusions</wicket:message></th><td><span class="sha1" wicket:id="exclusions">[exclusions]</span></td></tr>
+ <tr><th valign="top"><wicket:message key="gb.inclusions">inclusions</wicket:message></th><td><span class="sha1" wicket:id="inclusions">[inclusions]</span></td></tr>
+ </table>
+
+ <table class="repositories">
+ <tr>
+ <th class="left">
+ <img style="vertical-align: top; border: 1px solid #888; background-color: white;" src="git-black-16x16.png"/>
+ <wicket:message key="gb.repositories">[repositories]</wicket:message>
+ </th>
+ <th class="right"><wicket:message key="gb.status">[status]</wicket:message></th>
+ </tr>
+ <tbody>
+ <tr wicket:id="row">
+ <td class="left"><img style="border:0px;vertical-align:middle;" wicket:id="statusIcon" /><span wicket:id="name">[name]</span></td>
+ <td class="right"><span wicket:id="status">[status]</span></td>
+ </tr>
+ </tbody>
+ </table>
+
+</wicket:extend>
+</body>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/FederationRegistrationPage.java b/src/main/java/com/gitblit/wicket/pages/FederationRegistrationPage.java
new file mode 100644
index 00000000..19c30a5e
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/FederationRegistrationPage.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.pages;
+
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.wicket.PageParameters;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.repeater.Item;
+import org.apache.wicket.markup.repeater.data.DataView;
+import org.apache.wicket.markup.repeater.data.ListDataProvider;
+
+import com.gitblit.GitBlit;
+import com.gitblit.models.FederationModel;
+import com.gitblit.models.FederationModel.RepositoryStatus;
+import com.gitblit.wicket.WicketUtils;
+
+public class FederationRegistrationPage extends RootSubPage {
+
+ public FederationRegistrationPage(PageParameters params) {
+ super(params);
+
+ setStatelessHint(true);
+
+ String url = WicketUtils.getUrlParameter(params);
+ String name = WicketUtils.getNameParameter(params);
+
+ FederationModel registration = GitBlit.self().getFederationRegistration(url, name);
+ if (registration == null) {
+ error(getString("gb.couldNotFindFederationRegistration"), true);
+ }
+
+ setupPage(registration.isResultData() ? getString("gb.federationResults")
+ : getString("gb.federationRegistration"), registration.url);
+
+ add(new Label("url", registration.url));
+ add(WicketUtils.getRegistrationImage("typeIcon", registration, this));
+ add(new Label("frequency", registration.frequency));
+ add(new Label("folder", registration.folder));
+ add(new Label("token", showAdmin ? registration.token : "--"));
+ add(WicketUtils.createTimestampLabel("lastPull", registration.lastPull, getTimeZone(), getTimeUtils()));
+ add(WicketUtils.createTimestampLabel("nextPull", registration.nextPull, getTimeZone(), getTimeUtils()));
+
+ StringBuilder inclusions = new StringBuilder();
+ for (String inc : registration.inclusions) {
+ inclusions.append(inc).append("<br/>");
+ }
+ StringBuilder exclusions = new StringBuilder();
+ for (String ex : registration.exclusions) {
+ exclusions.append(ex).append("<br/>");
+ }
+
+ add(new Label("inclusions", inclusions.toString()).setEscapeModelStrings(false));
+
+ add(new Label("exclusions", exclusions.toString()).setEscapeModelStrings(false));
+
+ List<RepositoryStatus> list = registration.getStatusList();
+ Collections.sort(list);
+ DataView<RepositoryStatus> dataView = new DataView<RepositoryStatus>("row",
+ new ListDataProvider<RepositoryStatus>(list)) {
+ private static final long serialVersionUID = 1L;
+ private int counter;
+
+ @Override
+ protected void onBeforeRender() {
+ super.onBeforeRender();
+ counter = 0;
+ }
+
+ public void populateItem(final Item<RepositoryStatus> item) {
+ final RepositoryStatus entry = item.getModelObject();
+ item.add(WicketUtils.getPullStatusImage("statusIcon", entry.status));
+ item.add(new Label("name", entry.name));
+ item.add(new Label("status", entry.status.name()));
+ WicketUtils.setAlternatingBackground(item, counter);
+ counter++;
+ }
+ };
+ add(dataView);
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/pages/ForkPage.html b/src/main/java/com/gitblit/wicket/pages/ForkPage.html
new file mode 100644
index 00000000..72093696
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/ForkPage.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+
+<body>
+<wicket:head>
+ <noscript>
+ <meta http-equiv="refresh" content="5"></meta>
+ </noscript>
+ <script type="text/javascript"">
+ function doLoad() { setTimeout( "refresh()", 5*1000 ); }
+ function refresh() { window.location.reload(); }
+ </script>
+</wicket:head>
+<wicket:extend>
+<!-- need to specify body.onload -->
+<body onload="doLoad()">
+
+ <div class="row">
+ <div class="span6 offset3">
+ <div style="opacity:0.2;">
+ <center><img style="padding:10px" src="git-black_210x210.png"></img></center>
+ </div>
+ <div wicket:id="forkText" class="pageTitle project" style="border:0;font-weight:bold; text-align:center;">[fork text]</div>
+ </div>
+ <div class="span4 offset4">
+ <div class="progress progress-striped active">
+ <div class="bar" style="width: 100%;"></div>
+ </div>
+ </div>
+ </div>
+</body>
+
+</wicket:extend>
+</body>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/ForkPage.java b/src/main/java/com/gitblit/wicket/pages/ForkPage.java
new file mode 100644
index 00000000..340bd823
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/ForkPage.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2012 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.pages;
+
+import java.text.MessageFormat;
+
+import org.apache.wicket.PageParameters;
+import org.apache.wicket.markup.html.basic.Label;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.GitBlit;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.UserModel;
+import com.gitblit.wicket.GitBlitWebSession;
+import com.gitblit.wicket.GitblitRedirectException;
+import com.gitblit.wicket.WicketUtils;
+
+public class ForkPage extends RepositoryPage {
+
+
+ public ForkPage(PageParameters params) {
+ super(params);
+
+ setVersioned(false);
+
+ GitBlitWebSession session = GitBlitWebSession.get();
+
+ RepositoryModel repository = getRepositoryModel();
+ UserModel user = session.getUser();
+ boolean canFork = user.canFork(repository);
+
+ if (!canFork) {
+ // redirect to the summary page if this repository is not empty
+ GitBlitWebSession.get().cacheErrorMessage(
+ MessageFormat.format(getString("gb.forkNotAuthorized"), repository.name));
+ throw new GitblitRedirectException(SummaryPage.class, WicketUtils.newRepositoryParameter(repository.name));
+ }
+
+ String fork = GitBlit.self().getFork(user.username, repository.name);
+ if (fork != null) {
+ // redirect to user's fork
+ throw new GitblitRedirectException(SummaryPage.class, WicketUtils.newRepositoryParameter(fork));
+ }
+
+ add(new Label("forkText", getString("gb.preparingFork")));
+
+ if (!session.isForking()) {
+ // prepare session
+ session.isForking(true);
+
+ // fork it
+ ForkThread forker = new ForkThread(repository, session);
+ forker.start();
+ }
+ }
+
+ @Override
+ protected boolean allowForkControls() {
+ return false;
+ }
+
+ @Override
+ protected String getPageName() {
+ return "fork";
+ }
+
+ /**
+ * ForkThread does the work of working the repository in a background
+ * thread. The completion status is tracked through a session variable and
+ * monitored by this page.
+ */
+ private static class ForkThread extends Thread {
+
+ private final RepositoryModel repository;
+ private final GitBlitWebSession session;
+
+ public ForkThread(RepositoryModel repository, GitBlitWebSession session) {
+ this.repository = repository;
+ this.session = session;
+ }
+
+ @Override
+ public void run() {
+ UserModel user = session.getUser();
+ try {
+ GitBlit.self().fork(repository, user);
+ } catch (Exception e) {
+ LoggerFactory.getLogger(ForkPage.class).error(MessageFormat.format("Failed to fork {0} for {1}", repository.name, user.username), e);
+ } finally {
+ session.isForking(false);
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/pages/ForksPage.html b/src/main/java/com/gitblit/wicket/pages/ForksPage.html
new file mode 100644
index 00000000..c18d2a49
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/ForksPage.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+
+<body>
+<wicket:extend>
+
+ <div wicket:id="fork">
+ <div>
+ <span wicket:id="anAvatar" style="vertical-align: baseline;font-weight:bold;"></span>
+ <span wicket:id="aProject">[a project]</span> / <span wicket:id="aFork">[a fork]</span>
+ <span style="padding-left:10px;" wicket:id="lastChange"></span>
+ </div>
+ </div>
+
+</wicket:extend>
+</body>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/ForksPage.java b/src/main/java/com/gitblit/wicket/pages/ForksPage.java
new file mode 100644
index 00000000..cc483878
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/ForksPage.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright 2012 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.pages;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.wicket.Component;
+import org.apache.wicket.PageParameters;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.repeater.Item;
+import org.apache.wicket.markup.repeater.data.DataView;
+import org.apache.wicket.markup.repeater.data.ListDataProvider;
+import org.eclipse.jgit.lib.PersonIdent;
+
+import com.gitblit.GitBlit;
+import com.gitblit.Keys;
+import com.gitblit.models.ForkModel;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.UserModel;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.wicket.GitBlitWebSession;
+import com.gitblit.wicket.WicketUtils;
+import com.gitblit.wicket.panels.GravatarImage;
+import com.gitblit.wicket.panels.LinkPanel;
+
+public class ForksPage extends RepositoryPage {
+
+ public ForksPage(PageParameters params) {
+ super(params);
+
+ final RepositoryModel pageRepository = getRepositoryModel();
+
+ ForkModel root = GitBlit.self().getForkNetwork(pageRepository.name);
+ List<FlatFork> network = flatten(root);
+
+ ListDataProvider<FlatFork> forksDp = new ListDataProvider<FlatFork>(network);
+ DataView<FlatFork> forksList = new DataView<FlatFork>("fork", forksDp) {
+ private static final long serialVersionUID = 1L;
+
+ public void populateItem(final Item<FlatFork> item) {
+ FlatFork fork = item.getModelObject();
+ RepositoryModel repository = fork.repository;
+
+ if (repository.isPersonalRepository()) {
+ UserModel user = GitBlit.self().getUserModel(repository.projectPath.substring(1));
+ PersonIdent ident = new PersonIdent(user.getDisplayName(), user.emailAddress == null ? user.getDisplayName() : user.emailAddress);
+ item.add(new GravatarImage("anAvatar", ident, 20));
+ if (pageRepository.equals(repository)) {
+ // do not link to self
+ item.add(new Label("aProject", user.getDisplayName()));
+ } else {
+ item.add(new LinkPanel("aProject", null, user.getDisplayName(), UserPage.class, WicketUtils.newUsernameParameter(user.username)));
+ }
+ } else {
+ Component swatch;
+ if (repository.isBare){
+ swatch = new Label("anAvatar", "&nbsp;").setEscapeModelStrings(false);
+ } else {
+ swatch = new Label("anAvatar", "!");
+ WicketUtils.setHtmlTooltip(swatch, getString("gb.workingCopyWarning"));
+ }
+ WicketUtils.setCssClass(swatch, "repositorySwatch");
+ WicketUtils.setCssBackground(swatch, repository.toString());
+ item.add(swatch);
+ String projectName = repository.projectPath;
+ if (StringUtils.isEmpty(projectName)) {
+ projectName = GitBlit.getString(Keys.web.repositoryRootGroupName, "main");
+ }
+ if (pageRepository.equals(repository)) {
+ // do not link to self
+ item.add(new Label("aProject", projectName));
+ } else {
+ item.add(new LinkPanel("aProject", null, projectName, ProjectPage.class, WicketUtils.newProjectParameter(projectName)));
+ }
+ }
+
+ String repo = StringUtils.getLastPathElement(repository.name);
+ UserModel user = GitBlitWebSession.get().getUser();
+ if (user == null) {
+ user = UserModel.ANONYMOUS;
+ }
+ if (user.canView(repository)) {
+ if (pageRepository.equals(repository)) {
+ // do not link to self
+ item.add(new Label("aFork", StringUtils.stripDotGit(repo)));
+ } else {
+ item.add(new LinkPanel("aFork", null, StringUtils.stripDotGit(repo), SummaryPage.class, WicketUtils.newRepositoryParameter(repository.name)));
+ }
+ item.add(WicketUtils.createDateLabel("lastChange", repository.lastChange, getTimeZone(), getTimeUtils()));
+ } else {
+ item.add(new Label("aFork", repo));
+ item.add(new Label("lastChange").setVisible(false));
+ }
+
+ WicketUtils.setCssStyle(item, "margin-left:" + (32*fork.level) + "px;");
+ if (fork.level == 0) {
+ WicketUtils.setCssClass(item, "forkSource");
+ } else {
+ WicketUtils.setCssClass(item, "forkEntry");
+ }
+ }
+ };
+
+ add(forksList);
+ }
+
+ @Override
+ protected String getPageName() {
+ return getString("gb.forks");
+ }
+
+ protected List<FlatFork> flatten(ForkModel root) {
+ List<FlatFork> list = new ArrayList<FlatFork>();
+ list.addAll(flatten(root, 0));
+ return list;
+ }
+
+ protected List<FlatFork> flatten(ForkModel node, int level) {
+ List<FlatFork> list = new ArrayList<FlatFork>();
+ list.add(new FlatFork(node.repository, level));
+ if (!node.isLeaf()) {
+ for (ForkModel fork : node.forks) {
+ list.addAll(flatten(fork, level + 1));
+ }
+ }
+ return list;
+ }
+
+ private class FlatFork implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ public final RepositoryModel repository;
+ public final int level;
+
+ public FlatFork(RepositoryModel repository, int level) {
+ this.repository = repository;
+ this.level = level;
+ }
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/pages/GitSearchPage.html b/src/main/java/com/gitblit/wicket/pages/GitSearchPage.html
new file mode 100644
index 00000000..9bb1f418
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/GitSearchPage.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+
+<body>
+<wicket:extend>
+
+ <!-- pager links -->
+ <div style="padding-top:5px;">
+ <a wicket:id="firstPageTop"><wicket:message key="gb.pageFirst"></wicket:message></a> | <a wicket:id="prevPageTop"><wicket:message key="gb.pagePrevious"></wicket:message></a> | <a wicket:id="nextPageTop"><wicket:message key="gb.pageNext"></wicket:message></a>
+ </div>
+
+ <!-- history -->
+ <div style="margin-top:5px;" wicket:id="searchPanel">[search panel]</div>
+
+ <!-- pager links -->
+ <div style="padding-bottom:5px;">
+ <a wicket:id="firstPageBottom"><wicket:message key="gb.pageFirst"></wicket:message></a> | <a wicket:id="prevPageBottom"><wicket:message key="gb.pagePrevious"></wicket:message></a> | <a wicket:id="nextPageBottom"><wicket:message key="gb.pageNext"></wicket:message></a>
+ </div>
+
+</wicket:extend>
+</body>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/GitSearchPage.java b/src/main/java/com/gitblit/wicket/pages/GitSearchPage.java
new file mode 100644
index 00000000..6b0714f0
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/GitSearchPage.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.pages;
+
+import org.apache.wicket.PageParameters;
+import org.apache.wicket.markup.html.link.BookmarkablePageLink;
+
+import com.gitblit.Constants;
+import com.gitblit.wicket.WicketUtils;
+import com.gitblit.wicket.panels.SearchPanel;
+
+public class GitSearchPage extends RepositoryPage {
+
+ public GitSearchPage(PageParameters params) {
+ super(params);
+
+ String value = WicketUtils.getSearchString(params);
+ String type = WicketUtils.getSearchType(params);
+ Constants.SearchType searchType = Constants.SearchType.forName(type);
+
+ int pageNumber = WicketUtils.getPage(params);
+ int prevPage = Math.max(0, pageNumber - 1);
+ int nextPage = pageNumber + 1;
+
+ SearchPanel search = new SearchPanel("searchPanel", repositoryName, objectId, value,
+ searchType, getRepository(), -1, pageNumber - 1, getRepositoryModel().showRemoteBranches);
+ boolean hasMore = search.hasMore();
+ add(search);
+
+ add(new BookmarkablePageLink<Void>("firstPageTop", GitSearchPage.class,
+ WicketUtils.newSearchParameter(repositoryName, objectId, value, searchType))
+ .setEnabled(pageNumber > 1));
+ add(new BookmarkablePageLink<Void>("prevPageTop", GitSearchPage.class,
+ WicketUtils.newSearchParameter(repositoryName, objectId, value, searchType,
+ prevPage)).setEnabled(pageNumber > 1));
+ add(new BookmarkablePageLink<Void>("nextPageTop", GitSearchPage.class,
+ WicketUtils.newSearchParameter(repositoryName, objectId, value, searchType,
+ nextPage)).setEnabled(hasMore));
+
+ add(new BookmarkablePageLink<Void>("firstPageBottom", GitSearchPage.class,
+ WicketUtils.newSearchParameter(repositoryName, objectId, value, searchType))
+ .setEnabled(pageNumber > 1));
+ add(new BookmarkablePageLink<Void>("prevPageBottom", GitSearchPage.class,
+ WicketUtils.newSearchParameter(repositoryName, objectId, value, searchType,
+ prevPage)).setEnabled(pageNumber > 1));
+ add(new BookmarkablePageLink<Void>("nextPageBottom", GitSearchPage.class,
+ WicketUtils.newSearchParameter(repositoryName, objectId, value, searchType,
+ nextPage)).setEnabled(hasMore));
+
+ }
+
+ @Override
+ protected String getPageName() {
+ return getString("gb.search");
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/pages/GravatarProfilePage.html b/src/main/java/com/gitblit/wicket/pages/GravatarProfilePage.html
new file mode 100644
index 00000000..0cc0f1fc
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/GravatarProfilePage.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+<body>
+<wicket:extend>
+ <div class="pageTitle">
+ <h2>Gravatar<small> / <span wicket:id="username">[username]</span></small></h2>
+ </div>
+ <img class="gravatar" wicket:id="profileImage"></img>
+ <h2 wicket:id="displayName"></h2>
+ <div style="color:#888;"wicket:id="location"></div>
+ <div style="padding-top:5px;" wicket:id="aboutMe"></div>
+ <p></p>
+ <a wicket:id="profileLink"><wicket:message key="gb.completeGravatarProfile">[Complete profile on Gravatar.com]</wicket:message></a>
+ <p></p>
+</wicket:extend>
+</body>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/GravatarProfilePage.java b/src/main/java/com/gitblit/wicket/pages/GravatarProfilePage.java
new file mode 100644
index 00000000..ee567d75
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/GravatarProfilePage.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.pages;
+
+import java.io.IOException;
+import java.text.MessageFormat;
+
+import org.apache.wicket.PageParameters;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.link.ExternalLink;
+
+import com.gitblit.models.GravatarProfile;
+import com.gitblit.utils.ActivityUtils;
+import com.gitblit.wicket.ExternalImage;
+import com.gitblit.wicket.WicketUtils;
+
+/**
+ * Gravatar Profile Page shows the Gravatar information, if available.
+ *
+ * @author James Moger
+ *
+ */
+public class GravatarProfilePage extends RootPage {
+
+ public GravatarProfilePage(PageParameters params) {
+ super();
+ setupPage("", "");
+ String object = WicketUtils.getObject(params);
+ GravatarProfile profile = null;
+ try {
+ if (object.indexOf('@') > -1) {
+ profile = ActivityUtils.getGravatarProfileFromAddress(object);
+ } else {
+ profile = ActivityUtils.getGravatarProfile(object);
+ }
+ } catch (IOException e) {
+ error(MessageFormat.format(getString("gb.failedToFindGravatarProfile"), object), e, true);
+ }
+
+ if (profile == null) {
+ error(MessageFormat.format(getString("gb.failedToFindGravatarProfile"), object), true);
+ }
+ add(new Label("displayName", profile.displayName));
+ add(new Label("username", profile.preferredUsername));
+ add(new Label("location", profile.currentLocation));
+ add(new Label("aboutMe", profile.aboutMe));
+ ExternalImage image = new ExternalImage("profileImage", profile.thumbnailUrl + "?s=256&d=identicon");
+ add(image);
+ add(new ExternalLink("profileLink", profile.profileUrl));
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/pages/HistoryPage.html b/src/main/java/com/gitblit/wicket/pages/HistoryPage.html
new file mode 100644
index 00000000..f9bd2f69
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/HistoryPage.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+
+<body>
+<wicket:extend>
+
+ <!-- pager links -->
+ <div style="padding-top:5px;">
+ <a wicket:id="firstPageTop"><wicket:message key="gb.pageFirst"></wicket:message></a> | <a wicket:id="prevPageTop"><wicket:message key="gb.pagePrevious"></wicket:message></a> | <a wicket:id="nextPageTop"><wicket:message key="gb.pageNext"></wicket:message></a>
+ </div>
+
+ <!-- history -->
+ <div style="margin-top:5px;" wicket:id="historyPanel">[history panel]</div>
+
+ <!-- pager links -->
+ <div style="padding-bottom:5px;">
+ <a wicket:id="firstPageBottom"><wicket:message key="gb.pageFirst"></wicket:message></a> | <a wicket:id="prevPageBottom"><wicket:message key="gb.pagePrevious"></wicket:message></a> | <a wicket:id="nextPageBottom"><wicket:message key="gb.pageNext"></wicket:message></a>
+ </div>
+
+</wicket:extend>
+</body>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/HistoryPage.java b/src/main/java/com/gitblit/wicket/pages/HistoryPage.java
new file mode 100644
index 00000000..563202e6
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/HistoryPage.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.pages;
+
+import org.apache.wicket.PageParameters;
+import org.apache.wicket.markup.html.link.BookmarkablePageLink;
+
+import com.gitblit.wicket.WicketUtils;
+import com.gitblit.wicket.panels.HistoryPanel;
+
+public class HistoryPage extends RepositoryPage {
+
+ public HistoryPage(PageParameters params) {
+ super(params);
+
+ String path = WicketUtils.getPath(params);
+ int pageNumber = WicketUtils.getPage(params);
+ int prevPage = Math.max(0, pageNumber - 1);
+ int nextPage = pageNumber + 1;
+
+ HistoryPanel history = new HistoryPanel("historyPanel", repositoryName, objectId, path,
+ getRepository(), -1, pageNumber - 1, getRepositoryModel().showRemoteBranches);
+ boolean hasMore = history.hasMore();
+ add(history);
+
+ add(new BookmarkablePageLink<Void>("firstPageTop", HistoryPage.class,
+ WicketUtils.newPathParameter(repositoryName, objectId, path))
+ .setEnabled(pageNumber > 1));
+ add(new BookmarkablePageLink<Void>("prevPageTop", HistoryPage.class,
+ WicketUtils.newHistoryPageParameter(repositoryName, objectId, path, prevPage))
+ .setEnabled(pageNumber > 1));
+ add(new BookmarkablePageLink<Void>("nextPageTop", HistoryPage.class,
+ WicketUtils.newHistoryPageParameter(repositoryName, objectId, path, nextPage))
+ .setEnabled(hasMore));
+
+ add(new BookmarkablePageLink<Void>("firstPageBottom", HistoryPage.class,
+ WicketUtils.newPathParameter(repositoryName, objectId, path))
+ .setEnabled(pageNumber > 1));
+ add(new BookmarkablePageLink<Void>("prevPageBottom", HistoryPage.class,
+ WicketUtils.newHistoryPageParameter(repositoryName, objectId, path, prevPage))
+ .setEnabled(pageNumber > 1));
+ add(new BookmarkablePageLink<Void>("nextPageBottom", HistoryPage.class,
+ WicketUtils.newHistoryPageParameter(repositoryName, objectId, path, nextPage))
+ .setEnabled(hasMore));
+
+ }
+
+ @Override
+ protected String getPageName() {
+ return getString("gb.history");
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/pages/LogPage.html b/src/main/java/com/gitblit/wicket/pages/LogPage.html
new file mode 100644
index 00000000..8e5cb96a
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/LogPage.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+
+<body>
+<wicket:extend>
+
+ <!-- pager links -->
+ <div style="padding-top:5px;">
+ <a wicket:id="firstPageTop"><wicket:message key="gb.pageFirst"></wicket:message></a> | <a wicket:id="prevPageTop"><wicket:message key="gb.pagePrevious"></wicket:message></a> | <a wicket:id="nextPageTop"><wicket:message key="gb.pageNext"></wicket:message></a>
+ </div>
+
+ <!-- log -->
+ <div style="margin-top:5px;" wicket:id="logPanel">[log panel]</div>
+
+ <!-- pager links -->
+ <div style="padding-bottom:5px;">
+ <a wicket:id="firstPageBottom"><wicket:message key="gb.pageFirst"></wicket:message></a> | <a wicket:id="prevPageBottom"><wicket:message key="gb.pagePrevious"></wicket:message></a> | <a wicket:id="nextPageBottom"><wicket:message key="gb.pageNext"></wicket:message></a>
+ </div>
+
+</wicket:extend>
+</body>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/LogPage.java b/src/main/java/com/gitblit/wicket/pages/LogPage.java
new file mode 100644
index 00000000..ee8ddfef
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/LogPage.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.pages;
+
+import org.apache.wicket.PageParameters;
+import org.apache.wicket.markup.html.link.BookmarkablePageLink;
+
+import com.gitblit.utils.StringUtils;
+import com.gitblit.wicket.WicketUtils;
+import com.gitblit.wicket.panels.LogPanel;
+
+public class LogPage extends RepositoryPage {
+
+ public LogPage(PageParameters params) {
+ super(params);
+
+ addSyndicationDiscoveryLink();
+
+ int pageNumber = WicketUtils.getPage(params);
+ int prevPage = Math.max(0, pageNumber - 1);
+ int nextPage = pageNumber + 1;
+ String refid = objectId;
+ if (StringUtils.isEmpty(refid)) {
+ refid = getRepositoryModel().HEAD;
+ }
+ LogPanel logPanel = new LogPanel("logPanel", repositoryName, refid, getRepository(), -1,
+ pageNumber - 1, getRepositoryModel().showRemoteBranches);
+ boolean hasMore = logPanel.hasMore();
+ add(logPanel);
+
+ add(new BookmarkablePageLink<Void>("firstPageTop", LogPage.class,
+ WicketUtils.newObjectParameter(repositoryName, objectId))
+ .setEnabled(pageNumber > 1));
+ add(new BookmarkablePageLink<Void>("prevPageTop", LogPage.class,
+ WicketUtils.newLogPageParameter(repositoryName, objectId, prevPage))
+ .setEnabled(pageNumber > 1));
+ add(new BookmarkablePageLink<Void>("nextPageTop", LogPage.class,
+ WicketUtils.newLogPageParameter(repositoryName, objectId, nextPage))
+ .setEnabled(hasMore));
+
+ add(new BookmarkablePageLink<Void>("firstPageBottom", LogPage.class,
+ WicketUtils.newObjectParameter(repositoryName, objectId))
+ .setEnabled(pageNumber > 1));
+ add(new BookmarkablePageLink<Void>("prevPageBottom", LogPage.class,
+ WicketUtils.newLogPageParameter(repositoryName, objectId, prevPage))
+ .setEnabled(pageNumber > 1));
+ add(new BookmarkablePageLink<Void>("nextPageBottom", LogPage.class,
+ WicketUtils.newLogPageParameter(repositoryName, objectId, nextPage))
+ .setEnabled(hasMore));
+ }
+
+ @Override
+ protected String getPageName() {
+ return getString("gb.log");
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/pages/LogoutPage.html b/src/main/java/com/gitblit/wicket/pages/LogoutPage.html
new file mode 100644
index 00000000..d4077830
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/LogoutPage.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+<body>
+<wicket:extend>
+ <div class="navbar navbar-fixed-top">
+ <div class="navbar-inner">
+ <div class="container">
+ <a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
+ <span class="icon-bar"></span>
+ <span class="icon-bar"></span>
+ <span class="icon-bar"></span>
+ </a>
+ <a class="brand" wicket:id="rootLink">
+ <img src="gitblt_25_white.png" width="79" height="25" alt="gitblit" class="logo"/>
+ </a>
+
+ </div>
+ </div>
+ </div>
+
+ <!-- subclass content -->
+ <div class="container">
+ <div style="text-align:center" wicket:id="feedback">[Feedback Panel]</div>
+
+ <h1><wicket:message key="gb.sessionEnded">[Session has ended]</wicket:message></h1>
+ <p><wicket:message key="gb.closeBrowser">[Please close the browser]</wicket:message></p>
+ </div>
+</wicket:extend>
+</body>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/LogoutPage.java b/src/main/java/com/gitblit/wicket/pages/LogoutPage.java
new file mode 100644
index 00000000..982de0ec
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/LogoutPage.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.pages;
+
+import org.apache.wicket.markup.html.WebPage;
+import org.apache.wicket.protocol.http.WebRequest;
+import org.apache.wicket.protocol.http.WebResponse;
+
+import com.gitblit.GitBlit;
+import com.gitblit.models.UserModel;
+import com.gitblit.wicket.GitBlitWebSession;
+
+public class LogoutPage extends BasePage {
+
+ public LogoutPage() {
+ super();
+ GitBlitWebSession session = GitBlitWebSession.get();
+ UserModel user = session.getUser();
+ GitBlit.self().setCookie((WebResponse) getResponse(), null);
+ GitBlit.self().logout(user);
+ session.invalidate();
+
+ /*
+ * Now check whether the authentication was realized via the Authorization in the header.
+ * If so, it is likely to be cached by the browser, and cannot be undone. Effectively, this means
+ * that you cannot log out...
+ */
+ if ( ((WebRequest)getRequest()).getHttpServletRequest().getHeader("Authorization") != null ) {
+ // authentication will be done via this route anyway, show a page to close the browser:
+ // this will be done by Wicket.
+ setupPage(null, getString("gb.logout"));
+
+ } else {
+ setRedirect(true);
+ setResponsePage(getApplication().getHomePage());
+ } // not via WWW-Auth
+ } // LogoutPage
+} \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/LuceneSearchPage.html b/src/main/java/com/gitblit/wicket/pages/LuceneSearchPage.html
new file mode 100644
index 00000000..aba43de8
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/LuceneSearchPage.html
@@ -0,0 +1,92 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+
+<!-- contribute google-code-prettify resources to the page header -->
+<wicket:head>
+ <wicket:link>
+ <link href="prettify/prettify.css" type="text/css" rel="stylesheet" />
+ <script type="text/javascript" src="prettify/prettify.js"></script>
+ </wicket:link>
+</wicket:head>
+
+<wicket:extend>
+<body onload="document.getElementById('query').focus(); prettyPrint();">
+ <div class="pageTitle">
+ <h2><wicket:message key="gb.search"></wicket:message></h2>
+ </div>
+ <form class="form-inline" wicket:id="searchForm">
+ <div class="row">
+ <div class="span3">
+ <h3><wicket:message key="gb.repositories"></wicket:message></h3>
+ <select wicket:id="repositories" ></select>
+ </div>
+ <div class="span9" style="margin-left:10px">
+ <div>
+ <h3><wicket:message key="gb.query"></wicket:message></h3>
+ <input class="span8" id="query" type="text" wicket:id="query" placeholder="enter search text"></input>
+ <button class="btn btn-primary" type="submit" value="Search"><wicket:message key="gb.search"></wicket:message></button>
+ </div>
+ <div style="margin-top:10px;">
+ <div style="margin-left:0px;" class="span3">
+ <div class="alert alert">
+ <b>type:</b> commit or blob<br/>
+ <b>commit:</b> commit id<br/>
+ <b>path:</b> "path/to/blob"<br/>
+ <b>branch:</b> "refs/heads/master"<br/>
+ <b>author:</b> or <b>committer:</b>
+ </div>
+ </div>
+ <div style="margin-left:10px;" class="span4">
+ <div class="alert alert-info">
+ type:commit AND "bug fix"<br/>
+ type:commit AND author:james*<br/>
+ type:blob AND "int errorCode"<br/>
+ type:blob AND test AND path:*.java<br/>
+ commit:d91e5*
+ </div>
+ </div>
+ <div style="margin-left:10px;" class="span2">
+ <wicket:message key="gb.queryHelp"></wicket:message>
+ </div>
+ </div>
+ </div>
+ </div>
+ </form>
+
+ <div class="row-fluid">
+ <!-- results header -->
+ <div class="span8">
+ <h3><span wicket:id="resultsHeader"></span> <small><br/><span wicket:id="resultsCount"></span></small></h3>
+ </div>
+ <!-- pager links -->
+ <div class="span4" wicket:id="topPager"></div>
+ </div>
+
+ <div class="row-fluid">
+ <!-- search result repeater -->
+ <div class="searchResult" wicket:id="searchResults">
+ <div><i wicket:id="type"></i><span class="summary" wicket:id="summary"></span> <span wicket:id="tags" style="padding-left:10px;"></span></div>
+ <div class="body">
+ <div class="fragment" wicket:id="fragment"></div>
+ <div><span class="author" wicket:id="author"></span> <span class="date" ><wicket:message key="gb.authored"></wicket:message> <span class="date" wicket:id="date"></span></span></div>
+ <span class="repository" wicket:id="repository"></span>:<span class="branch" wicket:id="branch"></span>
+ </div>
+ </div>
+
+ <!-- pager links -->
+ <div wicket:id="bottomPager"></div>
+
+ </div>
+</body>
+
+ <wicket:fragment wicket:id="tagsPanel">
+ <span wicket:id="tag">
+ <span wicket:id="tagLink">[tag]</span>
+ </span>
+ </wicket:fragment>
+
+</wicket:extend>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/LuceneSearchPage.java b/src/main/java/com/gitblit/wicket/pages/LuceneSearchPage.java
new file mode 100644
index 00000000..79795ff2
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/LuceneSearchPage.java
@@ -0,0 +1,257 @@
+/*
+ * Copyright 2012 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.pages;
+
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.wicket.Component;
+import org.apache.wicket.PageParameters;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.form.ListMultipleChoice;
+import org.apache.wicket.markup.html.form.TextField;
+import org.apache.wicket.markup.html.panel.Fragment;
+import org.apache.wicket.markup.repeater.Item;
+import org.apache.wicket.markup.repeater.data.DataView;
+import org.apache.wicket.markup.repeater.data.ListDataProvider;
+import org.apache.wicket.model.Model;
+import org.eclipse.jgit.lib.Constants;
+
+import com.gitblit.Constants.SearchType;
+import com.gitblit.GitBlit;
+import com.gitblit.Keys;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.SearchResult;
+import com.gitblit.models.UserModel;
+import com.gitblit.utils.ArrayUtils;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.wicket.GitBlitWebSession;
+import com.gitblit.wicket.SessionlessForm;
+import com.gitblit.wicket.StringChoiceRenderer;
+import com.gitblit.wicket.WicketUtils;
+import com.gitblit.wicket.panels.LinkPanel;
+import com.gitblit.wicket.panels.PagerPanel;
+
+public class LuceneSearchPage extends RootPage {
+
+ public LuceneSearchPage() {
+ super();
+ setup(null);
+ }
+
+ public LuceneSearchPage(PageParameters params) {
+ super(params);
+ setup(params);
+ }
+
+ private void setup(PageParameters params) {
+ setupPage("", "");
+
+ // default values
+ ArrayList<String> repositories = new ArrayList<String>();
+ String query = "";
+ int page = 1;
+ int pageSize = GitBlit.getInteger(Keys.web.itemsPerPage, 50);
+
+ if (params != null) {
+ String repository = WicketUtils.getRepositoryName(params);
+ if (!StringUtils.isEmpty(repository)) {
+ repositories.add(repository);
+ }
+
+ page = WicketUtils.getPage(params);
+
+ if (params.containsKey("repositories")) {
+ String value = params.getString("repositories", "");
+ List<String> list = StringUtils.getStringsFromValue(value);
+ repositories.addAll(list);
+ }
+
+ if (params.containsKey("query")) {
+ query = params.getString("query", "");
+ } else {
+ String value = WicketUtils.getSearchString(params);
+ String type = WicketUtils.getSearchType(params);
+ com.gitblit.Constants.SearchType searchType = com.gitblit.Constants.SearchType.forName(type);
+ if (!StringUtils.isEmpty(value)) {
+ if (searchType == SearchType.COMMIT) {
+ query = "type:" + searchType.name().toLowerCase() + " AND \"" + value + "\"";
+ } else {
+ query = searchType.name().toLowerCase() + ":\"" + value + "\"";
+ }
+ }
+ }
+ }
+
+ // display user-accessible selections
+ UserModel user = GitBlitWebSession.get().getUser();
+ List<String> availableRepositories = new ArrayList<String>();
+ for (RepositoryModel model : GitBlit.self().getRepositoryModels(user)) {
+ if (model.hasCommits && !ArrayUtils.isEmpty(model.indexedBranches)) {
+ availableRepositories.add(model.name);
+ }
+ }
+ boolean luceneEnabled = GitBlit.getBoolean(Keys.web.allowLuceneIndexing, true);
+ if (luceneEnabled) {
+ if (availableRepositories.size() == 0) {
+ info(getString("gb.noIndexedRepositoriesWarning"));
+ }
+ } else {
+ error(getString("gb.luceneDisabled"));
+ }
+
+ // enforce user-accessible repository selections
+ ArrayList<String> searchRepositories = new ArrayList<String>();
+ for (String selectedRepository : repositories) {
+ if (availableRepositories.contains(selectedRepository)) {
+ searchRepositories.add(selectedRepository);
+ }
+ }
+
+ // search form
+ final Model<String> queryModel = new Model<String>(query);
+ final Model<ArrayList<String>> repositoriesModel = new Model<ArrayList<String>>(searchRepositories);
+ SessionlessForm<Void> form = new SessionlessForm<Void>("searchForm", getClass()) {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public void onSubmit() {
+ String q = queryModel.getObject();
+ if (StringUtils.isEmpty(q)) {
+ error(getString("gb.undefinedQueryWarning"));
+ return;
+ }
+ if (repositoriesModel.getObject().size() == 0) {
+ error(getString("gb.noSelectedRepositoriesWarning"));
+ return;
+ }
+ PageParameters params = new PageParameters();
+ params.put("repositories", StringUtils.flattenStrings(repositoriesModel.getObject()));
+ params.put("query", queryModel.getObject());
+ LuceneSearchPage page = new LuceneSearchPage(params);
+ setResponsePage(page);
+ }
+ };
+
+ ListMultipleChoice<String> selections = new ListMultipleChoice<String>("repositories",
+ repositoriesModel, availableRepositories, new StringChoiceRenderer());
+ selections.setMaxRows(8);
+ form.add(selections.setEnabled(luceneEnabled));
+ form.add(new TextField<String>("query", queryModel).setEnabled(luceneEnabled));
+ add(form.setEnabled(luceneEnabled));
+
+ // execute search
+ final List<SearchResult> results = new ArrayList<SearchResult>();
+ if (!ArrayUtils.isEmpty(searchRepositories) && !StringUtils.isEmpty(query)) {
+ results.addAll(GitBlit.self().search(query, page, pageSize, searchRepositories));
+ }
+
+ // results header
+ if (results.size() == 0) {
+ if (!ArrayUtils.isEmpty(searchRepositories) && !StringUtils.isEmpty(query)) {
+ add(new Label("resultsHeader", query).setRenderBodyOnly(true));
+ add(new Label("resultsCount", getString("gb.noHits")).setRenderBodyOnly(true));
+ } else {
+ add(new Label("resultsHeader").setVisible(false));
+ add(new Label("resultsCount").setVisible(false));
+ }
+ } else {
+ add(new Label("resultsHeader", query).setRenderBodyOnly(true));
+ add(new Label("resultsCount", MessageFormat.format(getString("gb.queryResults"),
+ results.get(0).hitId, results.get(results.size() - 1).hitId, results.get(0).totalHits)).
+ setRenderBodyOnly(true));
+ }
+
+ // search results view
+ ListDataProvider<SearchResult> resultsDp = new ListDataProvider<SearchResult>(results);
+ final DataView<SearchResult> resultsView = new DataView<SearchResult>("searchResults", resultsDp) {
+ private static final long serialVersionUID = 1L;
+ public void populateItem(final Item<SearchResult> item) {
+ final SearchResult sr = item.getModelObject();
+ switch(sr.type) {
+ case commit: {
+ Label icon = WicketUtils.newIcon("type", "icon-refresh");
+ WicketUtils.setHtmlTooltip(icon, "commit");
+ item.add(icon);
+ item.add(new LinkPanel("summary", null, sr.summary, CommitPage.class, WicketUtils.newObjectParameter(sr.repository, sr.commitId)));
+ // show tags
+ Fragment fragment = new Fragment("tags", "tagsPanel", LuceneSearchPage.this);
+ List<String> tags = sr.tags;
+ if (tags == null) {
+ tags = new ArrayList<String>();
+ }
+ ListDataProvider<String> tagsDp = new ListDataProvider<String>(tags);
+ final DataView<String> tagsView = new DataView<String>("tag", tagsDp) {
+ private static final long serialVersionUID = 1L;
+ public void populateItem(final Item<String> item) {
+ String tag = item.getModelObject();
+ Component c = new LinkPanel("tagLink", null, tag, TagPage.class,
+ WicketUtils.newObjectParameter(sr.repository, Constants.R_TAGS + tag));
+ WicketUtils.setCssClass(c, "tagRef");
+ item.add(c);
+ }
+ };
+ fragment.add(tagsView);
+ item.add(fragment);
+ break;
+ }
+ case blob: {
+ Label icon = WicketUtils.newIcon("type", "icon-file");
+ WicketUtils.setHtmlTooltip(icon, "blob");
+ item.add(icon);
+ item.add(new LinkPanel("summary", null, sr.path, BlobPage.class, WicketUtils.newPathParameter(sr.repository, sr.branch, sr.path)));
+ item.add(new Label("tags").setVisible(false));
+ break;
+ }
+ case issue: {
+ Label icon = WicketUtils.newIcon("type", "icon-file");
+ WicketUtils.setHtmlTooltip(icon, "issue");
+ item.add(icon);
+ item.add(new Label("summary", "issue: " + sr.issueId));
+ item.add(new Label("tags").setVisible(false));
+ break;
+ }
+ }
+ item.add(new Label("fragment", sr.fragment).setEscapeModelStrings(false).setVisible(!StringUtils.isEmpty(sr.fragment)));
+ item.add(new LinkPanel("repository", null, sr.repository, SummaryPage.class, WicketUtils.newRepositoryParameter(sr.repository)));
+ if (StringUtils.isEmpty(sr.branch)) {
+ item.add(new Label("branch", "null"));
+ } else {
+ item.add(new LinkPanel("branch", "branch", StringUtils.getRelativePath(Constants.R_HEADS, sr.branch), LogPage.class, WicketUtils.newObjectParameter(sr.repository, sr.branch)));
+ }
+ item.add(new Label("author", sr.author));
+ item.add(WicketUtils.createDatestampLabel("date", sr.date, getTimeZone(), getTimeUtils()));
+ }
+ };
+ add(resultsView.setVisible(results.size() > 0));
+
+ PageParameters pagerParams = new PageParameters();
+ pagerParams.put("repositories", StringUtils.flattenStrings(repositoriesModel.getObject()));
+ pagerParams.put("query", queryModel.getObject());
+
+ boolean showPager = false;
+ int totalPages = 0;
+ if (results.size() > 0) {
+ totalPages = (results.get(0).totalHits / pageSize) + (results.get(0).totalHits % pageSize > 0 ? 1 : 0);
+ showPager = results.get(0).totalHits > pageSize;
+ }
+
+ add(new PagerPanel("topPager", page, totalPages, LuceneSearchPage.class, pagerParams).setVisible(showPager));
+ add(new PagerPanel("bottomPager", page, totalPages, LuceneSearchPage.class, pagerParams).setVisible(showPager));
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/pages/MarkdownPage.html b/src/main/java/com/gitblit/wicket/pages/MarkdownPage.html
new file mode 100644
index 00000000..7900625b
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/MarkdownPage.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+
+<body>
+<wicket:extend>
+ <!-- markdown nav links -->
+ <div class="page_nav2">
+ <a wicket:id="blameLink"><wicket:message key="gb.blame"></wicket:message></a> | <a wicket:id="historyLink"><wicket:message key="gb.history"></wicket:message></a> | <a wicket:id="rawLink"><wicket:message key="gb.raw"></wicket:message></a> | <a wicket:id="headLink"><wicket:message key="gb.head"></wicket:message></a>
+ </div>
+
+ <!-- markdown content -->
+ <div class="markdown" style="padding-bottom:5px;" wicket:id="markdownText">[markdown content]</div>
+</wicket:extend>
+</body>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/MarkdownPage.java b/src/main/java/com/gitblit/wicket/pages/MarkdownPage.java
new file mode 100644
index 00000000..e032cbf9
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/MarkdownPage.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.pages;
+
+import java.text.MessageFormat;
+import java.text.ParseException;
+
+import org.apache.wicket.PageParameters;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.link.BookmarkablePageLink;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+
+import com.gitblit.GitBlit;
+import com.gitblit.utils.JGitUtils;
+import com.gitblit.utils.MarkdownUtils;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.wicket.WicketUtils;
+
+public class MarkdownPage extends RepositoryPage {
+
+ public MarkdownPage(PageParameters params) {
+ super(params);
+
+ final String markdownPath = WicketUtils.getPath(params);
+
+ Repository r = getRepository();
+ RevCommit commit = JGitUtils.getCommit(r, objectId);
+ String [] encodings = GitBlit.getEncodings();
+
+ // markdown page links
+ add(new BookmarkablePageLink<Void>("blameLink", BlamePage.class,
+ WicketUtils.newPathParameter(repositoryName, objectId, markdownPath)));
+ add(new BookmarkablePageLink<Void>("historyLink", HistoryPage.class,
+ WicketUtils.newPathParameter(repositoryName, objectId, markdownPath)));
+ add(new BookmarkablePageLink<Void>("rawLink", RawPage.class, WicketUtils.newPathParameter(
+ repositoryName, objectId, markdownPath)));
+ add(new BookmarkablePageLink<Void>("headLink", MarkdownPage.class,
+ WicketUtils.newPathParameter(repositoryName, Constants.HEAD, markdownPath)));
+
+ // Read raw markdown content and transform it to html
+ String markdownText = JGitUtils.getStringContent(r, commit.getTree(), markdownPath, encodings);
+ String htmlText;
+ try {
+ htmlText = MarkdownUtils.transformMarkdown(markdownText);
+ } catch (ParseException p) {
+ markdownText = MessageFormat.format("<div class=\"alert alert-error\"><strong>{0}:</strong> {1}</div>{2}", getString("gb.error"), getString("gb.markdownFailure"), markdownText);
+ htmlText = StringUtils.breakLinesForHtml(markdownText);
+ }
+
+ // Add the html to the page
+ add(new Label("markdownText", htmlText).setEscapeModelStrings(false));
+ }
+
+ @Override
+ protected String getPageName() {
+ return getString("gb.markdown");
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/pages/MetricsPage.html b/src/main/java/com/gitblit/wicket/pages/MetricsPage.html
new file mode 100644
index 00000000..4aefc798
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/MetricsPage.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+
+<body>
+<wicket:extend>
+<div style="padding-top:10px;">
+ <!-- branch name -->
+ <div><span class="metricsTitle" wicket:id="branchTitle"></span></div>
+
+ <table style="width:100%;">
+ <tr>
+ <!-- branch stats -->
+ <td colspan=2>
+ <h2><wicket:message key="gb.stats"></wicket:message></h2>
+ <span wicket:id="branchStats"></span>
+ </td>
+ </tr>
+ <tr>
+ <!-- commit activity trend -->
+ <td>
+ <h2><wicket:message key="gb.commitActivityTrend"></wicket:message></h2>
+ <div><img wicket:id="commitsChart" /></div>
+ </td>
+ <!-- commit activity by day of week -->
+ <td>
+ <h2><wicket:message key="gb.commitActivityDOW"></wicket:message></h2>
+ <div><img wicket:id="dayOfWeekChart" /></div>
+ </td>
+ </tr>
+ <tr>
+ <!-- commit activity by primary authors -->
+ <td colspan=2>
+ <h2><wicket:message key="gb.commitActivityAuthors"></wicket:message></h2>
+ <div style="text-align: center;"><img wicket:id="authorsChart" /></div>
+ </td>
+ </tr>
+ </table>
+</div>
+</wicket:extend>
+</body>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/MetricsPage.java b/src/main/java/com/gitblit/wicket/pages/MetricsPage.java
new file mode 100644
index 00000000..5904a64a
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/MetricsPage.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.pages;
+
+import java.awt.Color;
+import java.awt.Dimension;
+import java.text.MessageFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+import org.apache.wicket.PageParameters;
+import org.apache.wicket.markup.html.basic.Label;
+import org.eclipse.jgit.lib.Repository;
+import org.wicketstuff.googlecharts.Chart;
+import org.wicketstuff.googlecharts.ChartAxis;
+import org.wicketstuff.googlecharts.ChartAxisType;
+import org.wicketstuff.googlecharts.ChartProvider;
+import org.wicketstuff.googlecharts.ChartType;
+import org.wicketstuff.googlecharts.IChartData;
+import org.wicketstuff.googlecharts.LineStyle;
+import org.wicketstuff.googlecharts.MarkerType;
+import org.wicketstuff.googlecharts.ShapeMarker;
+
+import com.gitblit.models.Metric;
+import com.gitblit.utils.MetricUtils;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.wicket.WicketUtils;
+
+public class MetricsPage extends RepositoryPage {
+
+ public MetricsPage(PageParameters params) {
+ super(params);
+ Repository r = getRepository();
+ if (StringUtils.isEmpty(objectId)) {
+ add(new Label("branchTitle", getRepositoryModel().HEAD));
+ } else {
+ add(new Label("branchTitle", objectId));
+ }
+ Metric metricsTotal = null;
+ List<Metric> metrics = MetricUtils.getDateMetrics(r, objectId, true, null, getTimeZone());
+ metricsTotal = metrics.remove(0);
+ if (metricsTotal == null) {
+ add(new Label("branchStats", ""));
+ } else {
+ add(new Label("branchStats",
+ MessageFormat.format(getString("gb.branchStats"), metricsTotal.count,
+ metricsTotal.tag, getTimeUtils().duration(metricsTotal.duration))));
+ }
+ insertLinePlot("commitsChart", metrics);
+ insertBarPlot("dayOfWeekChart", getDayOfWeekMetrics(r, objectId));
+ insertPieChart("authorsChart", getAuthorMetrics(r, objectId));
+ }
+
+ private void insertLinePlot(String wicketId, List<Metric> metrics) {
+ if ((metrics != null) && (metrics.size() > 0)) {
+ IChartData data = WicketUtils.getChartData(metrics);
+
+ ChartProvider provider = new ChartProvider(new Dimension(400, 100), ChartType.LINE,
+ data);
+ ChartAxis dateAxis = new ChartAxis(ChartAxisType.BOTTOM);
+ dateAxis.setLabels(new String[] { metrics.get(0).name,
+ metrics.get(metrics.size() / 2).name, metrics.get(metrics.size() - 1).name });
+ provider.addAxis(dateAxis);
+
+ ChartAxis commitAxis = new ChartAxis(ChartAxisType.LEFT);
+ commitAxis.setLabels(new String[] { "",
+ String.valueOf((int) WicketUtils.maxValue(metrics)) });
+ provider.addAxis(commitAxis);
+
+ provider.setLineStyles(new LineStyle[] { new LineStyle(2, 4, 0), new LineStyle(0, 4, 1) });
+ provider.addShapeMarker(new ShapeMarker(MarkerType.CIRCLE, Color.BLUE, 1, -1, 5));
+
+ add(new Chart(wicketId, provider));
+ } else {
+ add(WicketUtils.newBlankImage(wicketId));
+ }
+ }
+
+ private void insertBarPlot(String wicketId, List<Metric> metrics) {
+ if ((metrics != null) && (metrics.size() > 0)) {
+ IChartData data = WicketUtils.getChartData(metrics);
+
+ ChartProvider provider = new ChartProvider(new Dimension(400, 100),
+ ChartType.BAR_VERTICAL_SET, data);
+ ChartAxis dateAxis = new ChartAxis(ChartAxisType.BOTTOM);
+ List<String> labels = new ArrayList<String>();
+ for (Metric metric : metrics) {
+ labels.add(metric.name);
+ }
+ dateAxis.setLabels(labels.toArray(new String[labels.size()]));
+ provider.addAxis(dateAxis);
+
+ ChartAxis commitAxis = new ChartAxis(ChartAxisType.LEFT);
+ commitAxis.setLabels(new String[] { "",
+ String.valueOf((int) WicketUtils.maxValue(metrics)) });
+ provider.addAxis(commitAxis);
+
+ add(new Chart(wicketId, provider));
+ } else {
+ add(WicketUtils.newBlankImage(wicketId));
+ }
+ }
+
+ private void insertPieChart(String wicketId, List<Metric> metrics) {
+ if ((metrics != null) && (metrics.size() > 0)) {
+ IChartData data = WicketUtils.getChartData(metrics);
+ List<String> labels = new ArrayList<String>();
+ for (Metric metric : metrics) {
+ labels.add(metric.name);
+ }
+ ChartProvider provider = new ChartProvider(new Dimension(800, 200), ChartType.PIE, data);
+ provider.setPieLabels(labels.toArray(new String[labels.size()]));
+ add(new Chart(wicketId, provider));
+ } else {
+ add(WicketUtils.newBlankImage(wicketId));
+ }
+ }
+
+ private List<Metric> getDayOfWeekMetrics(Repository repository, String objectId) {
+ List<Metric> list = MetricUtils.getDateMetrics(repository, objectId, false, "E", getTimeZone());
+ SimpleDateFormat sdf = new SimpleDateFormat("E");
+ Calendar cal = Calendar.getInstance();
+
+ List<Metric> sorted = new ArrayList<Metric>();
+ int firstDayOfWeek = cal.getFirstDayOfWeek();
+ int dayOfWeek = cal.get(Calendar.DAY_OF_WEEK);
+
+ // rewind date to first day of week
+ cal.add(Calendar.DATE, firstDayOfWeek - dayOfWeek);
+ for (int i = 0; i < 7; i++) {
+ String day = sdf.format(cal.getTime());
+ for (Metric metric : list) {
+ if (metric.name.equals(day)) {
+ sorted.add(metric);
+ list.remove(metric);
+ break;
+ }
+ }
+ cal.add(Calendar.DATE, 1);
+ }
+ return sorted;
+ }
+
+ private List<Metric> getAuthorMetrics(Repository repository, String objectId) {
+ List<Metric> authors = MetricUtils.getAuthorMetrics(repository, objectId, true);
+ Collections.sort(authors, new Comparator<Metric>() {
+ @Override
+ public int compare(Metric o1, Metric o2) {
+ if (o1.count > o2.count) {
+ return -1;
+ } else if (o1.count < o2.count) {
+ return 1;
+ }
+ return 0;
+ }
+ });
+ if (authors.size() > 10) {
+ return authors.subList(0, 9);
+ }
+ return authors;
+ }
+
+ @Override
+ protected String getPageName() {
+ return getString("gb.metrics");
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/pages/PatchPage.html b/src/main/java/com/gitblit/wicket/pages/PatchPage.html
new file mode 100644
index 00000000..719a46d1
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/PatchPage.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+
+<body>
+
+ <!-- patch content -->
+ <pre style="border:0px;" wicket:id="patchText">[patch content]</pre>
+
+</body>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/PatchPage.java b/src/main/java/com/gitblit/wicket/pages/PatchPage.java
new file mode 100644
index 00000000..878cfb45
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/PatchPage.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.pages;
+
+import org.apache.wicket.PageParameters;
+import org.apache.wicket.markup.html.WebPage;
+import org.apache.wicket.markup.html.basic.Label;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+
+import com.gitblit.GitBlit;
+import com.gitblit.utils.DiffUtils;
+import com.gitblit.utils.JGitUtils;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.wicket.GitBlitWebSession;
+import com.gitblit.wicket.WicketUtils;
+
+public class PatchPage extends WebPage {
+
+ public PatchPage(PageParameters params) {
+ super(params);
+
+ if (!params.containsKey("r")) {
+ GitBlitWebSession.get().cacheErrorMessage(getString("gb.repositoryNotSpecified"));
+ redirectToInterceptPage(new RepositoriesPage());
+ return;
+ }
+
+ final String repositoryName = WicketUtils.getRepositoryName(params);
+ final String baseObjectId = WicketUtils.getBaseObjectId(params);
+ final String objectId = WicketUtils.getObject(params);
+ final String blobPath = WicketUtils.getPath(params);
+
+ Repository r = GitBlit.self().getRepository(repositoryName);
+ if (r == null) {
+ GitBlitWebSession.get().cacheErrorMessage(getString("gb.canNotLoadRepository") + " " + repositoryName);
+ redirectToInterceptPage(new RepositoriesPage());
+ return;
+ }
+
+ RevCommit commit = JGitUtils.getCommit(r, objectId);
+ if (commit == null) {
+ GitBlitWebSession.get().cacheErrorMessage(getString("gb.commitIsNull"));
+ redirectToInterceptPage(new RepositoriesPage());
+ return;
+ }
+
+ RevCommit baseCommit = null;
+ if (!StringUtils.isEmpty(baseObjectId)) {
+ baseCommit = JGitUtils.getCommit(r, baseObjectId);
+ }
+ String patch = DiffUtils.getCommitPatch(r, baseCommit, commit, blobPath);
+ add(new Label("patchText", patch));
+ r.close();
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/pages/ProjectPage.html b/src/main/java/com/gitblit/wicket/pages/ProjectPage.html
new file mode 100644
index 00000000..3e73ba52
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/ProjectPage.html
@@ -0,0 +1,70 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+
+<body>
+<wicket:extend>
+
+
+ <div class="row">
+ <div class="span12">
+ <h2><span wicket:id="projectTitle"></span> <small><span wicket:id="projectDescription"></span></small>
+ <a class="hidden-phone hidden-tablet brand" style="text-decoration: none;" wicket:id="syndication" wicket:message="title:gb.feed">
+ <img style="border:0px;vertical-align:middle;" src="feed_16x16.png"></img>
+ </a>
+ </h2>
+ <div class="markdown" wicket:id="projectMessage">[project message]</div>
+ </div>
+ </div>
+
+ <div class="tabbable">
+ <!-- tab titles -->
+ <ul class="nav nav-tabs">
+ <li class="active"><a href="#repositories" data-toggle="tab"><wicket:message key="gb.repositories"></wicket:message></a></li>
+ <li ><a href="#activity" data-toggle="tab"><wicket:message key="gb.activity"></wicket:message></a></li>
+ </ul>
+
+ <!-- tab content -->
+ <div class="tab-content">
+
+ <!-- repositories tab -->
+ <div class="tab-pane active" id="repositories">
+ <!-- markdown -->
+ <div class="row">
+ <div class="span12">
+ <div class="markdown" wicket:id="repositoriesMessage">[repositories message]</div>
+ </div>
+ </div>
+ <div class="row">
+ <div class="span6" style="border-bottom:1px solid #eee;" wicket:id="repositoryList">
+ <span wicket:id="repository"></span>
+ </div>
+ </div>
+ </div>
+
+ <!-- activity tab -->
+ <div class="tab-pane" id="activity">
+ <div class="pageTitle">
+ <h2><wicket:message key="gb.recentActivity"></wicket:message><small> <span class="hidden-phone">/ <span wicket:id="subheader">[days back]</span></span></small></h2>
+ </div>
+
+ <div class="hidden-phone" style="height: 155px;text-align: center;">
+ <table>
+ <tr>
+ <td><span class="hidden-tablet" id="chartDaily"></span></td>
+ <td><span id="chartRepositories"></span></td>
+ <td><span id="chartAuthors"></span></td>
+ </tr>
+ </table>
+ </div>
+
+ <div wicket:id="activityPanel">[activity panel]</div>
+ </div>
+
+ </div>
+ </div>
+</wicket:extend>
+</body>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/ProjectPage.java b/src/main/java/com/gitblit/wicket/pages/ProjectPage.java
new file mode 100644
index 00000000..7eba0331
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/ProjectPage.java
@@ -0,0 +1,355 @@
+/*
+ * Copyright 2012 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.pages;
+
+import java.text.MessageFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.wicket.Component;
+import org.apache.wicket.PageParameters;
+import org.apache.wicket.behavior.HeaderContributor;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.link.ExternalLink;
+import org.apache.wicket.markup.repeater.Item;
+import org.apache.wicket.markup.repeater.data.DataView;
+import org.apache.wicket.markup.repeater.data.ListDataProvider;
+
+import com.gitblit.GitBlit;
+import com.gitblit.Keys;
+import com.gitblit.SyndicationServlet;
+import com.gitblit.models.Activity;
+import com.gitblit.models.Metric;
+import com.gitblit.models.ProjectModel;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.utils.ActivityUtils;
+import com.gitblit.utils.MarkdownUtils;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.wicket.GitBlitWebApp;
+import com.gitblit.wicket.GitBlitWebSession;
+import com.gitblit.wicket.GitblitRedirectException;
+import com.gitblit.wicket.PageRegistration;
+import com.gitblit.wicket.PageRegistration.DropDownMenuItem;
+import com.gitblit.wicket.PageRegistration.DropDownMenuRegistration;
+import com.gitblit.wicket.WicketUtils;
+import com.gitblit.wicket.charting.GoogleChart;
+import com.gitblit.wicket.charting.GoogleCharts;
+import com.gitblit.wicket.charting.GoogleLineChart;
+import com.gitblit.wicket.charting.GooglePieChart;
+import com.gitblit.wicket.panels.ActivityPanel;
+import com.gitblit.wicket.panels.ProjectRepositoryPanel;
+
+public class ProjectPage extends RootPage {
+
+ List<ProjectModel> projectModels = new ArrayList<ProjectModel>();
+
+ public ProjectPage() {
+ super();
+ throw new GitblitRedirectException(GitBlitWebApp.get().getHomePage());
+ }
+
+ public ProjectPage(PageParameters params) {
+ super(params);
+ setup(params);
+ }
+
+ @Override
+ protected boolean reusePageParameters() {
+ return true;
+ }
+
+ private void setup(PageParameters params) {
+ setupPage("", "");
+ // check to see if we should display a login message
+ boolean authenticateView = GitBlit.getBoolean(Keys.web.authenticateViewPages, true);
+ if (authenticateView && !GitBlitWebSession.get().isLoggedIn()) {
+ authenticationError("Please login");
+ return;
+ }
+
+ String projectName = WicketUtils.getProjectName(params);
+ if (StringUtils.isEmpty(projectName)) {
+ throw new GitblitRedirectException(GitBlitWebApp.get().getHomePage());
+ }
+
+ ProjectModel project = getProjectModel(projectName);
+ if (project == null) {
+ throw new GitblitRedirectException(GitBlitWebApp.get().getHomePage());
+ }
+
+ add(new Label("projectTitle", project.getDisplayName()));
+ add(new Label("projectDescription", project.description));
+
+ String feedLink = SyndicationServlet.asLink(getRequest().getRelativePathPrefixToContextRoot(), projectName, null, 0);
+ add(new ExternalLink("syndication", feedLink));
+
+ add(WicketUtils.syndicationDiscoveryLink(SyndicationServlet.getTitle(project.getDisplayName(),
+ null), feedLink));
+
+ // project markdown message
+ String pmessage = transformMarkdown(project.projectMarkdown);
+ Component projectMessage = new Label("projectMessage", pmessage)
+ .setEscapeModelStrings(false).setVisible(pmessage.length() > 0);
+ add(projectMessage);
+
+ // markdown message above repositories list
+ String rmessage = transformMarkdown(project.repositoriesMarkdown);
+ Component repositoriesMessage = new Label("repositoriesMessage", rmessage)
+ .setEscapeModelStrings(false).setVisible(rmessage.length() > 0);
+ add(repositoriesMessage);
+
+ List<RepositoryModel> repositories = getRepositories(params);
+
+ Collections.sort(repositories, new Comparator<RepositoryModel>() {
+ @Override
+ public int compare(RepositoryModel o1, RepositoryModel o2) {
+ // reverse-chronological sort
+ return o2.lastChange.compareTo(o1.lastChange);
+ }
+ });
+
+ final ListDataProvider<RepositoryModel> dp = new ListDataProvider<RepositoryModel>(repositories);
+ DataView<RepositoryModel> dataView = new DataView<RepositoryModel>("repositoryList", dp) {
+ private static final long serialVersionUID = 1L;
+
+ public void populateItem(final Item<RepositoryModel> item) {
+ final RepositoryModel entry = item.getModelObject();
+
+ ProjectRepositoryPanel row = new ProjectRepositoryPanel("repository",
+ getLocalizer(), this, showAdmin, entry, getAccessRestrictions());
+ item.add(row);
+ }
+ };
+ add(dataView);
+
+ // project activity
+ // parameters
+ int daysBack = WicketUtils.getDaysBack(params);
+ if (daysBack < 1) {
+ daysBack = 14;
+ }
+ String objectId = WicketUtils.getObject(params);
+
+ List<Activity> recentActivity = ActivityUtils.getRecentActivity(repositories,
+ daysBack, objectId, getTimeZone());
+ if (recentActivity.size() == 0) {
+ // no activity, skip graphs and activity panel
+ add(new Label("subheader", MessageFormat.format(getString("gb.recentActivityNone"),
+ daysBack)));
+ add(new Label("activityPanel"));
+ } else {
+ // calculate total commits and total authors
+ int totalCommits = 0;
+ Set<String> uniqueAuthors = new HashSet<String>();
+ for (Activity activity : recentActivity) {
+ totalCommits += activity.getCommitCount();
+ uniqueAuthors.addAll(activity.getAuthorMetrics().keySet());
+ }
+ int totalAuthors = uniqueAuthors.size();
+
+ // add the subheader with stat numbers
+ add(new Label("subheader", MessageFormat.format(getString("gb.recentActivityStats"),
+ daysBack, totalCommits, totalAuthors)));
+
+ // create the activity charts
+ GoogleCharts charts = createCharts(recentActivity);
+ add(new HeaderContributor(charts));
+
+ // add activity panel
+ add(new ActivityPanel("activityPanel", recentActivity));
+ }
+ }
+
+ /**
+ * Creates the daily activity line chart, the active repositories pie chart,
+ * and the active authors pie chart
+ *
+ * @param recentActivity
+ * @return
+ */
+ private GoogleCharts createCharts(List<Activity> recentActivity) {
+ // activity metrics
+ Map<String, Metric> repositoryMetrics = new HashMap<String, Metric>();
+ Map<String, Metric> authorMetrics = new HashMap<String, Metric>();
+
+ // aggregate repository and author metrics
+ for (Activity activity : recentActivity) {
+
+ // aggregate author metrics
+ for (Map.Entry<String, Metric> entry : activity.getAuthorMetrics().entrySet()) {
+ String author = entry.getKey();
+ if (!authorMetrics.containsKey(author)) {
+ authorMetrics.put(author, new Metric(author));
+ }
+ authorMetrics.get(author).count += entry.getValue().count;
+ }
+
+ // aggregate repository metrics
+ for (Map.Entry<String, Metric> entry : activity.getRepositoryMetrics().entrySet()) {
+ String repository = StringUtils.stripDotGit(entry.getKey());
+ if (!repositoryMetrics.containsKey(repository)) {
+ repositoryMetrics.put(repository, new Metric(repository));
+ }
+ repositoryMetrics.get(repository).count += entry.getValue().count;
+ }
+ }
+
+ // build google charts
+ int w = 310;
+ int h = 150;
+ GoogleCharts charts = new GoogleCharts();
+
+ // sort in reverse-chronological order and then reverse that
+ Collections.sort(recentActivity);
+ Collections.reverse(recentActivity);
+
+ // daily line chart
+ GoogleChart chart = new GoogleLineChart("chartDaily", getString("gb.dailyActivity"), "day",
+ getString("gb.commits"));
+ SimpleDateFormat df = new SimpleDateFormat("MMM dd");
+ df.setTimeZone(getTimeZone());
+ for (Activity metric : recentActivity) {
+ chart.addValue(df.format(metric.startDate), metric.getCommitCount());
+ }
+ chart.setWidth(w);
+ chart.setHeight(h);
+ charts.addChart(chart);
+
+ // active repositories pie chart
+ chart = new GooglePieChart("chartRepositories", getString("gb.activeRepositories"),
+ getString("gb.repository"), getString("gb.commits"));
+ for (Metric metric : repositoryMetrics.values()) {
+ chart.addValue(metric.name, metric.count);
+ }
+ chart.setWidth(w);
+ chart.setHeight(h);
+ charts.addChart(chart);
+
+ // active authors pie chart
+ chart = new GooglePieChart("chartAuthors", getString("gb.activeAuthors"),
+ getString("gb.author"), getString("gb.commits"));
+ for (Metric metric : authorMetrics.values()) {
+ chart.addValue(metric.name, metric.count);
+ }
+ chart.setWidth(w);
+ chart.setHeight(h);
+ charts.addChart(chart);
+
+ return charts;
+ }
+
+ @Override
+ protected void addDropDownMenus(List<PageRegistration> pages) {
+ PageParameters params = getPageParameters();
+
+ DropDownMenuRegistration projects = new DropDownMenuRegistration("gb.projects",
+ ProjectPage.class);
+ projects.menuItems.addAll(getProjectsMenu());
+ pages.add(0, projects);
+
+ DropDownMenuRegistration menu = new DropDownMenuRegistration("gb.filters",
+ ProjectPage.class);
+ // preserve time filter option on repository choices
+ menu.menuItems.addAll(getRepositoryFilterItems(params));
+
+ // preserve repository filter option on time choices
+ menu.menuItems.addAll(getTimeFilterItems(params));
+
+ if (menu.menuItems.size() > 0) {
+ // Reset Filter
+ menu.menuItems.add(new DropDownMenuItem(getString("gb.reset"), null, null));
+ }
+
+ pages.add(menu);
+ }
+
+ @Override
+ protected List<ProjectModel> getProjectModels() {
+ if (projectModels.isEmpty()) {
+ List<RepositoryModel> repositories = getRepositoryModels();
+ List<ProjectModel> projects = GitBlit.self().getProjectModels(repositories, false);
+ projectModels.addAll(projects);
+ }
+ return projectModels;
+ }
+
+ private ProjectModel getProjectModel(String name) {
+ for (ProjectModel project : getProjectModels()) {
+ if (name.equalsIgnoreCase(project.name)) {
+ return project;
+ }
+ }
+ return null;
+ }
+
+ protected List<DropDownMenuItem> getProjectsMenu() {
+ List<DropDownMenuItem> menu = new ArrayList<DropDownMenuItem>();
+ List<ProjectModel> projects = new ArrayList<ProjectModel>();
+ for (ProjectModel model : getProjectModels()) {
+ if (!model.isUserProject()) {
+ projects.add(model);
+ }
+ }
+ int maxProjects = 15;
+ boolean showAllProjects = projects.size() > maxProjects;
+ if (showAllProjects) {
+
+ // sort by last changed
+ Collections.sort(projects, new Comparator<ProjectModel>() {
+ @Override
+ public int compare(ProjectModel o1, ProjectModel o2) {
+ return o2.lastChange.compareTo(o1.lastChange);
+ }
+ });
+
+ // take most recent subset
+ projects = projects.subList(0, maxProjects);
+
+ // sort those by name
+ Collections.sort(projects);
+ }
+
+ for (ProjectModel project : projects) {
+ menu.add(new DropDownMenuItem(project.getDisplayName(), "p", project.name));
+ }
+ if (showAllProjects) {
+ menu.add(new DropDownMenuItem());
+ menu.add(new DropDownMenuItem("all projects", null, null));
+ }
+ return menu;
+ }
+
+ private String transformMarkdown(String markdown) {
+ String message = "";
+ if (!StringUtils.isEmpty(markdown)) {
+ // Read user-supplied message
+ try {
+ message = MarkdownUtils.transformMarkdown(markdown);
+ } catch (Throwable t) {
+ message = getString("gb.failedToRead") + " " + markdown;
+ warn(message, t);
+ }
+ }
+ return message;
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/pages/ProjectsPage.html b/src/main/java/com/gitblit/wicket/pages/ProjectsPage.html
new file mode 100644
index 00000000..528ed48f
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/ProjectsPage.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+
+<body>
+<wicket:extend>
+ <div class="markdown" style="padding-bottom:5px;" wicket:id="projectsMessage">[projects message]</div>
+
+ <table class="repositories">
+ <thead>
+ <tr>
+ <th class="left">
+ <i class="icon-folder-close" ></i>
+ <wicket:message key="gb.project">Project</wicket:message>
+ </th>
+ <th class="hidden-phone" ><span><wicket:message key="gb.description">Description</wicket:message></span></th>
+ <th class="hidden-phone"><wicket:message key="gb.repositories">Repositories</wicket:message></th>
+ <th><wicket:message key="gb.lastChange">Last Change</wicket:message></th>
+ <th class="right"></th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr wicket:id="project">
+ <td class="left" style="padding-left:3px;" ><span style="padding-left:3px;" wicket:id="projectTitle">[project title]</span></td>
+ <td class="hidden-phone"><span class="list" wicket:id="projectDescription">[project description]</span></td>
+ <td class="hidden-phone" style="padding-right:15px;"><span style="font-size:0.8em;" wicket:id="repositoryCount">[repository count]</span></td>
+ <td><span wicket:id="projectLastChange">[last change]</span></td>
+ <td class="rightAlign"></td>
+ </tr>
+ </tbody>
+ </table>
+
+</wicket:extend>
+</body>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/ProjectsPage.java b/src/main/java/com/gitblit/wicket/pages/ProjectsPage.java
new file mode 100644
index 00000000..7f0b002e
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/ProjectsPage.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright 2012 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.pages;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.wicket.Component;
+import org.apache.wicket.PageParameters;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.repeater.Item;
+import org.apache.wicket.markup.repeater.data.DataView;
+import org.apache.wicket.markup.repeater.data.ListDataProvider;
+import org.apache.wicket.resource.ContextRelativeResource;
+import org.apache.wicket.util.resource.ResourceStreamNotFoundException;
+import org.eclipse.jgit.lib.Constants;
+
+import com.gitblit.GitBlit;
+import com.gitblit.Keys;
+import com.gitblit.models.ProjectModel;
+import com.gitblit.utils.MarkdownUtils;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.wicket.GitBlitWebSession;
+import com.gitblit.wicket.PageRegistration;
+import com.gitblit.wicket.PageRegistration.DropDownMenuItem;
+import com.gitblit.wicket.PageRegistration.DropDownMenuRegistration;
+import com.gitblit.wicket.WicketUtils;
+import com.gitblit.wicket.panels.LinkPanel;
+
+public class ProjectsPage extends RootPage {
+
+ public ProjectsPage() {
+ super();
+ setup(null);
+ }
+
+ public ProjectsPage(PageParameters params) {
+ super(params);
+ setup(params);
+ }
+
+ @Override
+ protected boolean reusePageParameters() {
+ return true;
+ }
+
+ @Override
+ protected List<ProjectModel> getProjectModels() {
+ return GitBlit.self().getProjectModels(getRepositoryModels(), false);
+ }
+
+ private void setup(PageParameters params) {
+ setupPage("", "");
+ // check to see if we should display a login message
+ boolean authenticateView = GitBlit.getBoolean(Keys.web.authenticateViewPages, true);
+ if (authenticateView && !GitBlitWebSession.get().isLoggedIn()) {
+ String messageSource = GitBlit.getString(Keys.web.loginMessage, "gitblit");
+ String message = readMarkdown(messageSource, "login.mkd");
+ Component repositoriesMessage = new Label("projectsMessage", message);
+ add(repositoriesMessage.setEscapeModelStrings(false));
+ add(new Label("projectsPanel"));
+ return;
+ }
+
+ // Load the markdown welcome message
+ String messageSource = GitBlit.getString(Keys.web.repositoriesMessage, "gitblit");
+ String message = readMarkdown(messageSource, "welcome.mkd");
+ Component projectsMessage = new Label("projectsMessage", message).setEscapeModelStrings(
+ false).setVisible(message.length() > 0);
+ add(projectsMessage);
+
+ List<ProjectModel> projects = getProjects(params);
+
+ ListDataProvider<ProjectModel> dp = new ListDataProvider<ProjectModel>(projects);
+
+ DataView<ProjectModel> dataView = new DataView<ProjectModel>("project", dp) {
+ private static final long serialVersionUID = 1L;
+ int counter;
+
+ @Override
+ protected void onBeforeRender() {
+ super.onBeforeRender();
+ counter = 0;
+ }
+
+ public void populateItem(final Item<ProjectModel> item) {
+ final ProjectModel entry = item.getModelObject();
+
+ PageParameters pp = WicketUtils.newProjectParameter(entry.name);
+ item.add(new LinkPanel("projectTitle", "list", entry.getDisplayName(),
+ ProjectPage.class, pp));
+ item.add(new LinkPanel("projectDescription", "list", entry.description,
+ ProjectPage.class, pp));
+
+ item.add(new Label("repositoryCount", entry.repositories.size()
+ + " "
+ + (entry.repositories.size() == 1 ? getString("gb.repository")
+ : getString("gb.repositories"))));
+
+ String lastChange;
+ if (entry.lastChange.getTime() == 0) {
+ lastChange = "--";
+ } else {
+ lastChange = getTimeUtils().timeAgo(entry.lastChange);
+ }
+ Label lastChangeLabel = new Label("projectLastChange", lastChange);
+ item.add(lastChangeLabel);
+ WicketUtils.setCssClass(lastChangeLabel, getTimeUtils()
+ .timeAgoCss(entry.lastChange));
+ WicketUtils.setAlternatingBackground(item, counter);
+ counter++;
+ }
+ };
+ add(dataView);
+
+ // push the panel down if we are hiding the admin controls and the
+ // welcome message
+ if (!showAdmin && !projectsMessage.isVisible()) {
+ WicketUtils.setCssStyle(dataView, "padding-top:5px;");
+ }
+ }
+
+ @Override
+ protected void addDropDownMenus(List<PageRegistration> pages) {
+ PageParameters params = getPageParameters();
+
+ pages.add(0, new PageRegistration("gb.projects", ProjectsPage.class, params));
+
+ DropDownMenuRegistration menu = new DropDownMenuRegistration("gb.filters",
+ ProjectsPage.class);
+ // preserve time filter option on repository choices
+ menu.menuItems.addAll(getRepositoryFilterItems(params));
+
+ // preserve repository filter option on time choices
+ menu.menuItems.addAll(getTimeFilterItems(params));
+
+ if (menu.menuItems.size() > 0) {
+ // Reset Filter
+ menu.menuItems.add(new DropDownMenuItem(getString("gb.reset"), null, null));
+ }
+
+ pages.add(menu);
+ }
+
+ private String readMarkdown(String messageSource, String resource) {
+ String message = "";
+ if (messageSource.equalsIgnoreCase("gitblit")) {
+ // Read default message
+ message = readDefaultMarkdown(resource);
+ } else {
+ // Read user-supplied message
+ if (!StringUtils.isEmpty(messageSource)) {
+ File file = new File(messageSource);
+ if (file.exists()) {
+ try {
+ FileInputStream fis = new FileInputStream(file);
+ InputStreamReader reader = new InputStreamReader(fis,
+ Constants.CHARACTER_ENCODING);
+ message = MarkdownUtils.transformMarkdown(reader);
+ reader.close();
+ } catch (Throwable t) {
+ message = getString("gb.failedToRead") + " " + file;
+ warn(message, t);
+ }
+ } else {
+ message = messageSource + " " + getString("gb.isNotValidFile");
+ }
+ }
+ }
+ return message;
+ }
+
+ private String readDefaultMarkdown(String file) {
+ String base = file.substring(0, file.lastIndexOf('.'));
+ String ext = file.substring(file.lastIndexOf('.'));
+ String lc = getLanguageCode();
+ String cc = getCountryCode();
+
+ // try to read file_en-us.ext, file_en.ext, file.ext
+ List<String> files = new ArrayList<String>();
+ if (!StringUtils.isEmpty(lc)) {
+ if (!StringUtils.isEmpty(cc)) {
+ files.add(base + "_" + lc + "-" + cc + ext);
+ files.add(base + "_" + lc + "_" + cc + ext);
+ }
+ files.add(base + "_" + lc + ext);
+ }
+ files.add(file);
+
+ for (String name : files) {
+ String message;
+ InputStreamReader reader = null;
+ try {
+ ContextRelativeResource res = WicketUtils.getResource(name);
+ InputStream is = res.getResourceStream().getInputStream();
+ reader = new InputStreamReader(is, Constants.CHARACTER_ENCODING);
+ message = MarkdownUtils.transformMarkdown(reader);
+ reader.close();
+ return message;
+ } catch (ResourceStreamNotFoundException t) {
+ continue;
+ } catch (Throwable t) {
+ message = MessageFormat.format(getString("gb.failedToReadMessage"), file);
+ error(message, t, false);
+ return message;
+ } finally {
+ if (reader != null) {
+ try {
+ reader.close();
+ } catch (Exception e) {
+ }
+ }
+ }
+ }
+ return MessageFormat.format(getString("gb.failedToReadMessage"), file);
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/pages/RawPage.java b/src/main/java/com/gitblit/wicket/pages/RawPage.java
new file mode 100644
index 00000000..28e8bae2
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/RawPage.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.pages;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.wicket.IRequestTarget;
+import org.apache.wicket.PageParameters;
+import org.apache.wicket.RequestCycle;
+import org.apache.wicket.markup.html.WebPage;
+import org.apache.wicket.protocol.http.WebResponse;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.GitBlit;
+import com.gitblit.Keys;
+import com.gitblit.utils.JGitUtils;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.wicket.WicketUtils;
+
+public class RawPage extends WebPage {
+
+ private final Logger logger = LoggerFactory.getLogger(getClass().getSimpleName());
+
+ public RawPage(final PageParameters params) {
+ super(params);
+
+ if (!params.containsKey("r")) {
+ error(getString("gb.repositoryNotSpecified"));
+ redirectToInterceptPage(new RepositoriesPage());
+ }
+
+ getRequestCycle().setRequestTarget(new IRequestTarget() {
+ @Override
+ public void detach(RequestCycle requestCycle) {
+ }
+
+ @Override
+ public void respond(RequestCycle requestCycle) {
+ WebResponse response = (WebResponse) requestCycle.getResponse();
+
+ final String repositoryName = WicketUtils.getRepositoryName(params);
+ final String objectId = WicketUtils.getObject(params);
+ final String blobPath = WicketUtils.getPath(params);
+ String[] encodings = GitBlit.getEncodings();
+
+ Repository r = GitBlit.self().getRepository(repositoryName);
+ if (r == null) {
+ error(getString("gb.canNotLoadRepository") + " " + repositoryName);
+ redirectToInterceptPage(new RepositoriesPage());
+ return;
+ }
+
+ if (StringUtils.isEmpty(blobPath)) {
+ // objectid referenced raw view
+ byte [] binary = JGitUtils.getByteContent(r, objectId);
+ response.setContentType("application/octet-stream");
+ response.setContentLength(binary.length);
+ try {
+ response.getOutputStream().write(binary);
+ } catch (Exception e) {
+ logger.error("Failed to write binary response", e);
+ }
+ } else {
+ // standard raw blob view
+ RevCommit commit = JGitUtils.getCommit(r, objectId);
+
+ String filename = blobPath;
+ if (blobPath.indexOf('/') > -1) {
+ filename = blobPath.substring(blobPath.lastIndexOf('/') + 1);
+ }
+
+ String extension = null;
+ if (blobPath.lastIndexOf('.') > -1) {
+ extension = blobPath.substring(blobPath.lastIndexOf('.') + 1);
+ }
+
+ // Map the extensions to types
+ Map<String, Integer> map = new HashMap<String, Integer>();
+ for (String ext : GitBlit.getStrings(Keys.web.imageExtensions)) {
+ map.put(ext.toLowerCase(), 2);
+ }
+ for (String ext : GitBlit.getStrings(Keys.web.binaryExtensions)) {
+ map.put(ext.toLowerCase(), 3);
+ }
+
+ if (extension != null) {
+ int type = 0;
+ if (map.containsKey(extension)) {
+ type = map.get(extension);
+ }
+ switch (type) {
+ case 2:
+ // image blobs
+ byte[] image = JGitUtils.getByteContent(r, commit.getTree(), blobPath, true);
+ response.setContentType("image/" + extension.toLowerCase());
+ response.setContentLength(image.length);
+ try {
+ response.getOutputStream().write(image);
+ } catch (IOException e) {
+ logger.error("Failed to write image response", e);
+ }
+ break;
+ case 3:
+ // binary blobs (download)
+ byte[] binary = JGitUtils.getByteContent(r, commit.getTree(), blobPath, true);
+ response.setContentLength(binary.length);
+ response.setContentType("application/octet-stream");
+ response.setHeader("Content-Disposition", "attachment; filename=\"" + filename + "\"");
+ try {
+ response.getOutputStream().write(binary);
+ } catch (IOException e) {
+ logger.error("Failed to write binary response", e);
+ }
+ break;
+ default:
+ // plain text
+ String content = JGitUtils.getStringContent(r, commit.getTree(),
+ blobPath, encodings);
+ response.setContentType("text/plain; charset=UTF-8");
+ try {
+ response.getOutputStream().write(content.getBytes("UTF-8"));
+ } catch (Exception e) {
+ logger.error("Failed to write text response", e);
+ }
+ }
+
+ } else {
+ // plain text
+ String content = JGitUtils.getStringContent(r, commit.getTree(), blobPath,
+ encodings);
+ response.setContentType("text/plain; charset=UTF-8");
+ try {
+ response.getOutputStream().write(content.getBytes("UTF-8"));
+ } catch (Exception e) {
+ logger.error("Failed to write text response", e);
+ }
+ }
+ }
+ r.close();
+ }
+ });
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/pages/RepositoriesPage.html b/src/main/java/com/gitblit/wicket/pages/RepositoriesPage.html
new file mode 100644
index 00000000..d2d27157
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/RepositoriesPage.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+
+<body>
+<wicket:extend>
+ <div class="markdown" style="padding-bottom:5px;" wicket:id="repositoriesMessage">[repositories message]</div>
+
+ <div wicket:id="repositoriesPanel">[repositories panel]</div>
+</wicket:extend>
+</body>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/RepositoriesPage.java b/src/main/java/com/gitblit/wicket/pages/RepositoriesPage.java
new file mode 100644
index 00000000..4bce77f5
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/RepositoriesPage.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.pages;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.wicket.Component;
+import org.apache.wicket.PageParameters;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.resource.ContextRelativeResource;
+import org.apache.wicket.util.resource.ResourceStreamNotFoundException;
+import org.eclipse.jgit.lib.Constants;
+
+import com.gitblit.GitBlit;
+import com.gitblit.Keys;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.utils.MarkdownUtils;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.wicket.GitBlitWebSession;
+import com.gitblit.wicket.PageRegistration;
+import com.gitblit.wicket.PageRegistration.DropDownMenuItem;
+import com.gitblit.wicket.PageRegistration.DropDownMenuRegistration;
+import com.gitblit.wicket.WicketUtils;
+import com.gitblit.wicket.panels.RepositoriesPanel;
+
+public class RepositoriesPage extends RootPage {
+
+ public RepositoriesPage() {
+ super();
+ setup(null);
+ }
+
+ public RepositoriesPage(PageParameters params) {
+ super(params);
+ setup(params);
+ }
+
+ @Override
+ protected boolean reusePageParameters() {
+ return true;
+ }
+
+ private void setup(PageParameters params) {
+ setupPage("", "");
+ // check to see if we should display a login message
+ boolean authenticateView = GitBlit.getBoolean(Keys.web.authenticateViewPages, true);
+ if (authenticateView && !GitBlitWebSession.get().isLoggedIn()) {
+ String messageSource = GitBlit.getString(Keys.web.loginMessage, "gitblit");
+ String message = readMarkdown(messageSource, "login.mkd");
+ Component repositoriesMessage = new Label("repositoriesMessage", message);
+ add(repositoriesMessage.setEscapeModelStrings(false));
+ add(new Label("repositoriesPanel"));
+ return;
+ }
+
+ // Load the markdown welcome message
+ String messageSource = GitBlit.getString(Keys.web.repositoriesMessage, "gitblit");
+ String message = readMarkdown(messageSource, "welcome.mkd");
+ Component repositoriesMessage = new Label("repositoriesMessage", message)
+ .setEscapeModelStrings(false).setVisible(message.length() > 0);
+ add(repositoriesMessage);
+
+ List<RepositoryModel> repositories = getRepositories(params);
+
+ RepositoriesPanel repositoriesPanel = new RepositoriesPanel("repositoriesPanel", showAdmin,
+ true, repositories, true, getAccessRestrictions());
+ // push the panel down if we are hiding the admin controls and the
+ // welcome message
+ if (!showAdmin && !repositoriesMessage.isVisible()) {
+ WicketUtils.setCssStyle(repositoriesPanel, "padding-top:5px;");
+ }
+ add(repositoriesPanel);
+ }
+
+ @Override
+ protected void addDropDownMenus(List<PageRegistration> pages) {
+ PageParameters params = getPageParameters();
+
+ DropDownMenuRegistration menu = new DropDownMenuRegistration("gb.filters",
+ RepositoriesPage.class);
+ // preserve time filter option on repository choices
+ menu.menuItems.addAll(getRepositoryFilterItems(params));
+
+ // preserve repository filter option on time choices
+ menu.menuItems.addAll(getTimeFilterItems(params));
+
+ if (menu.menuItems.size() > 0) {
+ // Reset Filter
+ menu.menuItems.add(new DropDownMenuItem(getString("gb.reset"), null, null));
+ }
+
+ pages.add(menu);
+ }
+
+ private String readMarkdown(String messageSource, String resource) {
+ String message = "";
+ if (messageSource.equalsIgnoreCase("gitblit")) {
+ // Read default message
+ message = readDefaultMarkdown(resource);
+ } else {
+ // Read user-supplied message
+ if (!StringUtils.isEmpty(messageSource)) {
+ File file = GitBlit.getFileOrFolder(messageSource);
+ if (file.exists()) {
+ try {
+ FileInputStream fis = new FileInputStream(file);
+ InputStreamReader reader = new InputStreamReader(fis,
+ Constants.CHARACTER_ENCODING);
+ message = MarkdownUtils.transformMarkdown(reader);
+ reader.close();
+ } catch (Throwable t) {
+ message = getString("gb.failedToRead") + " " + file;
+ warn(message, t);
+ }
+ } else {
+ message = messageSource + " " + getString("gb.isNotValidFile");
+ }
+ }
+ }
+ return message;
+ }
+
+ private String readDefaultMarkdown(String file) {
+ String base = file.substring(0, file.lastIndexOf('.'));
+ String ext = file.substring(file.lastIndexOf('.'));
+ String lc = getLanguageCode();
+ String cc = getCountryCode();
+
+ // try to read file_en-us.ext, file_en.ext, file.ext
+ List<String> files = new ArrayList<String>();
+ if (!StringUtils.isEmpty(lc)) {
+ if (!StringUtils.isEmpty(cc)) {
+ files.add(base + "_" + lc + "-" + cc + ext);
+ files.add(base + "_" + lc + "_" + cc + ext);
+ }
+ files.add(base + "_" + lc + ext);
+ }
+ files.add(file);
+
+ for (String name : files) {
+ String message;
+ InputStreamReader reader = null;
+ try {
+ ContextRelativeResource res = WicketUtils.getResource(name);
+ InputStream is = res.getResourceStream().getInputStream();
+ reader = new InputStreamReader(is, Constants.CHARACTER_ENCODING);
+ message = MarkdownUtils.transformMarkdown(reader);
+ reader.close();
+ return message;
+ } catch (ResourceStreamNotFoundException t) {
+ continue;
+ } catch (Throwable t) {
+ message = MessageFormat.format(getString("gb.failedToReadMessage"), file);
+ error(message, t, false);
+ return message;
+ } finally {
+ if (reader != null) {
+ try {
+ reader.close();
+ } catch (Exception e) {
+ }
+ }
+ }
+ }
+ return MessageFormat.format(getString("gb.failedToReadMessage"), file);
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/pages/RepositoryPage.html b/src/main/java/com/gitblit/wicket/pages/RepositoryPage.html
new file mode 100644
index 00000000..d49f0188
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/RepositoryPage.html
@@ -0,0 +1,82 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+
+<body>
+ <wicket:extend>
+ <!-- page nav links -->
+ <div class="navbar navbar-fixed-top">
+ <div class="navbar-inner">
+ <div class="container">
+ <a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
+ <span class="icon-bar"></span>
+ <span class="icon-bar"></span>
+ <span class="icon-bar"></span>
+ </a>
+ <a class="brand" wicket:id="rootLink">
+ <img src="gitblt_25_white.png" width="79" height="25" alt="gitblit" class="logo"/>
+ </a>
+
+ <div class="nav-collapse" wicket:id="navPanel"></div>
+
+ <a class="hidden-phone hidden-tablet brand" style="text-decoration: none;" wicket:id="syndication" wicket:message="title:gb.feed">
+ <img style="border:0px;vertical-align:middle;" src="feed_16x16.png"></img>
+ </a>
+
+ <form class="hidden-phone hidden-tablet pull-right" style="margin-top:10px;" wicket:id="searchForm">
+ <span class="search">
+ <select class="small" wicket:id="searchType"/>
+ <input type="text" id="searchBox" wicket:id="searchBox" value=""/>
+ </span>
+ </form>
+ </div>
+ </div>
+ </div>
+
+ <!-- page content -->
+ <div class="container">
+ <div style="text-align:center;" wicket:id="feedback">[Feedback Panel]</div>
+
+ <!-- page header -->
+ <div class="pageTitle">
+ <div class="row">
+ <div class="controls">
+ <span wicket:id="workingCopyIndicator"></span>
+ <span class="hidden-phone hidden-tablet" wicket:id="forksProhibitedIndicator"></span>
+ <div class="hidden-phone btn-group pull-right">
+ <!-- future spot for other repo buttons -->
+ <a class="btn btn-info" wicket:id="myForkLink"><img style="border:0px;vertical-align:middle;" src="fork_16x16.png"></img> <wicket:message key="gb.myFork"></wicket:message></a>
+ <a class="btn btn-info" wicket:id="forkLink"><img style="border:0px;vertical-align:middle;" src="fork_16x16.png"></img> <wicket:message key="gb.fork"></wicket:message></a>
+ </div>
+ </div>
+ <div class="span7">
+ <div><span class="project" wicket:id="projectTitle">[project title]</span>/<img wicket:id="repositoryIcon" style="padding-left: 10px;"></img><span class="repository" wicket:id="repositoryName">[repository name]</span> <span class="hidden-phone"><span wicket:id="pageName">[page name]</span></span></div>
+ <span wicket:id="originRepository">[origin repository]</span>
+ </div>
+ </div>
+ </div>
+
+ <wicket:child />
+ </div>
+
+ <wicket:fragment wicket:id="originFragment">
+ <p class="originRepository"><wicket:message key="gb.forkedFrom">[forked from]</wicket:message> <span wicket:id="originRepository">[origin repository]</span></p>
+ </wicket:fragment>
+
+ <wicket:fragment wicket:id="workingCopyFragment">
+ <div class="pull-right" style="padding-top:0px;margin-bottom:0px;padding-left:5px">
+ <span class="alert alert-info" style="padding: 6px 14px 6px 14px;vertical-align: middle;"><i class="icon-exclamation-sign"></i>&nbsp;<span class="hidden-phone" wicket:id="workingCopy" style="font-weight:bold;">[working copy]</span></span>
+ </div>
+ </wicket:fragment>
+
+ <wicket:fragment wicket:id="forksProhibitedFragment">
+ <div class="pull-right" style="padding-top:0px;margin-bottom:0px;padding-left:5px">
+ <span class="alert alert-error" style="padding: 6px 14px 6px 14px;vertical-align: middle;"><i class="icon-ban-circle"></i>&nbsp;<span class="hidden-phone" wicket:id="forksProhibited" style="font-weight:bold;">[forks prohibited]</span></span>
+ </div>
+ </wicket:fragment>
+
+ </wicket:extend>
+</body>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/RepositoryPage.java b/src/main/java/com/gitblit/wicket/pages/RepositoryPage.java
new file mode 100644
index 00000000..a477b741
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/RepositoryPage.java
@@ -0,0 +1,608 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.pages;
+
+import java.io.Serializable;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.wicket.Component;
+import org.apache.wicket.PageParameters;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.form.DropDownChoice;
+import org.apache.wicket.markup.html.form.TextField;
+import org.apache.wicket.markup.html.link.ExternalLink;
+import org.apache.wicket.markup.html.panel.Fragment;
+import org.apache.wicket.model.IModel;
+import org.apache.wicket.model.Model;
+import org.apache.wicket.protocol.http.RequestUtils;
+import org.apache.wicket.request.target.basic.RedirectRequestTarget;
+import org.eclipse.jgit.diff.DiffEntry.ChangeType;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+
+import com.gitblit.Constants;
+import com.gitblit.GitBlit;
+import com.gitblit.Keys;
+import com.gitblit.PagesServlet;
+import com.gitblit.SyndicationServlet;
+import com.gitblit.models.ProjectModel;
+import com.gitblit.models.RefModel;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.SubmoduleModel;
+import com.gitblit.models.UserModel;
+import com.gitblit.utils.ArrayUtils;
+import com.gitblit.utils.JGitUtils;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.utils.TicgitUtils;
+import com.gitblit.wicket.GitBlitWebSession;
+import com.gitblit.wicket.PageRegistration;
+import com.gitblit.wicket.PageRegistration.OtherPageLink;
+import com.gitblit.wicket.SessionlessForm;
+import com.gitblit.wicket.WicketUtils;
+import com.gitblit.wicket.panels.LinkPanel;
+import com.gitblit.wicket.panels.NavigationPanel;
+import com.gitblit.wicket.panels.RefsPanel;
+
+public abstract class RepositoryPage extends BasePage {
+
+ protected final String projectName;
+ protected final String repositoryName;
+ protected final String objectId;
+
+ private transient Repository r;
+
+ private RepositoryModel m;
+
+ private Map<String, SubmoduleModel> submodules;
+
+ private final Map<String, PageRegistration> registeredPages;
+ private boolean showAdmin;
+ private boolean isOwner;
+
+ public RepositoryPage(PageParameters params) {
+ super(params);
+ repositoryName = WicketUtils.getRepositoryName(params);
+ String root =StringUtils.getFirstPathElement(repositoryName);
+ if (StringUtils.isEmpty(root)) {
+ projectName = GitBlit.getString(Keys.web.repositoryRootGroupName, "main");
+ } else {
+ projectName = root;
+ }
+ objectId = WicketUtils.getObject(params);
+
+ if (StringUtils.isEmpty(repositoryName)) {
+ error(MessageFormat.format(getString("gb.repositoryNotSpecifiedFor"), getPageName()), true);
+ }
+
+ if (!getRepositoryModel().hasCommits) {
+ setResponsePage(EmptyRepositoryPage.class, params);
+ }
+
+ if (getRepositoryModel().isCollectingGarbage) {
+ error(MessageFormat.format(getString("gb.busyCollectingGarbage"), getRepositoryModel().name), true);
+ }
+
+ if (objectId != null) {
+ RefModel branch = null;
+ if ((branch = JGitUtils.getBranch(getRepository(), objectId)) != null) {
+ UserModel user = GitBlitWebSession.get().getUser();
+ if (user == null) {
+ // workaround until get().getUser() is reviewed throughout the app
+ user = UserModel.ANONYMOUS;
+ }
+ boolean canAccess = user.canView(getRepositoryModel(),
+ branch.reference.getName());
+ if (!canAccess) {
+ error(getString("gb.accessDenied"), true);
+ }
+ }
+ }
+
+ // register the available page links for this page and user
+ registeredPages = registerPages();
+
+ // standard page links
+ List<PageRegistration> pages = new ArrayList<PageRegistration>(registeredPages.values());
+ NavigationPanel navigationPanel = new NavigationPanel("navPanel", getClass(), pages);
+ add(navigationPanel);
+
+ add(new ExternalLink("syndication", SyndicationServlet.asLink(getRequest()
+ .getRelativePathPrefixToContextRoot(), repositoryName, null, 0)));
+
+ // add floating search form
+ SearchForm searchForm = new SearchForm("searchForm", repositoryName);
+ add(searchForm);
+ searchForm.setTranslatedAttributes();
+
+ // set stateless page preference
+ setStatelessHint(true);
+ }
+
+ private Map<String, PageRegistration> registerPages() {
+ PageParameters params = null;
+ if (!StringUtils.isEmpty(repositoryName)) {
+ params = WicketUtils.newRepositoryParameter(repositoryName);
+ }
+ Map<String, PageRegistration> pages = new LinkedHashMap<String, PageRegistration>();
+
+ // standard links
+ pages.put("repositories", new PageRegistration("gb.repositories", RepositoriesPage.class));
+ pages.put("summary", new PageRegistration("gb.summary", SummaryPage.class, params));
+ pages.put("log", new PageRegistration("gb.log", LogPage.class, params));
+ pages.put("branches", new PageRegistration("gb.branches", BranchesPage.class, params));
+ pages.put("tags", new PageRegistration("gb.tags", TagsPage.class, params));
+ pages.put("tree", new PageRegistration("gb.tree", TreePage.class, params));
+ if (GitBlit.getBoolean(Keys.web.allowForking, true)) {
+ pages.put("forks", new PageRegistration("gb.forks", ForksPage.class, params));
+ }
+
+ // conditional links
+ Repository r = getRepository();
+ RepositoryModel model = getRepositoryModel();
+
+ // per-repository extra page links
+ if (model.useTickets && TicgitUtils.getTicketsBranch(r) != null) {
+ pages.put("tickets", new PageRegistration("gb.tickets", TicketsPage.class, params));
+ }
+ if (model.useDocs) {
+ pages.put("docs", new PageRegistration("gb.docs", DocsPage.class, params));
+ }
+ if (JGitUtils.getPagesBranch(r) != null) {
+ OtherPageLink pagesLink = new OtherPageLink("gb.pages", PagesServlet.asLink(
+ getRequest().getRelativePathPrefixToContextRoot(), repositoryName, null));
+ pages.put("pages", pagesLink);
+ }
+
+ // Conditionally add edit link
+ showAdmin = false;
+ if (GitBlit.getBoolean(Keys.web.authenticateAdminPages, true)) {
+ boolean allowAdmin = GitBlit.getBoolean(Keys.web.allowAdministration, false);
+ showAdmin = allowAdmin && GitBlitWebSession.get().canAdmin();
+ } else {
+ showAdmin = GitBlit.getBoolean(Keys.web.allowAdministration, false);
+ }
+ isOwner = GitBlitWebSession.get().isLoggedIn()
+ && (model.isOwner(GitBlitWebSession.get()
+ .getUsername()));
+ if (showAdmin || isOwner) {
+ pages.put("edit", new PageRegistration("gb.edit", EditRepositoryPage.class, params));
+ }
+ return pages;
+ }
+
+ protected boolean allowForkControls() {
+ return GitBlit.getBoolean(Keys.web.allowForking, true);
+ }
+
+ @Override
+ protected void setupPage(String repositoryName, String pageName) {
+ String projectName = StringUtils.getFirstPathElement(repositoryName);
+ ProjectModel project = GitBlit.self().getProjectModel(projectName);
+ if (project.isUserProject()) {
+ // user-as-project
+ add(new LinkPanel("projectTitle", null, project.getDisplayName(),
+ UserPage.class, WicketUtils.newUsernameParameter(project.name.substring(1))));
+ } else {
+ // project
+ add(new LinkPanel("projectTitle", null, project.name,
+ ProjectPage.class, WicketUtils.newProjectParameter(project.name)));
+ }
+
+ String name = StringUtils.stripDotGit(repositoryName);
+ if (!StringUtils.isEmpty(projectName) && name.startsWith(projectName)) {
+ name = name.substring(projectName.length() + 1);
+ }
+ add(new LinkPanel("repositoryName", null, name, SummaryPage.class,
+ WicketUtils.newRepositoryParameter(repositoryName)));
+ add(new Label("pageName", pageName).setRenderBodyOnly(true));
+
+ UserModel user = GitBlitWebSession.get().getUser();
+ if (user == null) {
+ user = UserModel.ANONYMOUS;
+ }
+
+ // indicate origin repository
+ RepositoryModel model = getRepositoryModel();
+ if (StringUtils.isEmpty(model.originRepository)) {
+ add(new Label("originRepository").setVisible(false));
+ } else {
+ RepositoryModel origin = GitBlit.self().getRepositoryModel(model.originRepository);
+ if (origin == null) {
+ // no origin repository
+ add(new Label("originRepository").setVisible(false));
+ } else if (!user.canView(origin)) {
+ // show origin repository without link
+ Fragment forkFrag = new Fragment("originRepository", "originFragment", this);
+ forkFrag.add(new Label("originRepository", StringUtils.stripDotGit(model.originRepository)));
+ add(forkFrag);
+ } else {
+ // link to origin repository
+ Fragment forkFrag = new Fragment("originRepository", "originFragment", this);
+ forkFrag.add(new LinkPanel("originRepository", null, StringUtils.stripDotGit(model.originRepository),
+ SummaryPage.class, WicketUtils.newRepositoryParameter(model.originRepository)));
+ add(forkFrag);
+ }
+ }
+
+ // show sparkleshare folder icon
+ if (model.isSparkleshared()) {
+ add(WicketUtils.newImage("repositoryIcon", "folder_star_32x32.png",
+ getString("gb.isSparkleshared")));
+ } else {
+ add(WicketUtils.newClearPixel("repositoryIcon").setVisible(false));
+ }
+
+ if (getRepositoryModel().isBare) {
+ add(new Label("workingCopyIndicator").setVisible(false));
+ } else {
+ Fragment wc = new Fragment("workingCopyIndicator", "workingCopyFragment", this);
+ Label lbl = new Label("workingCopy", getString("gb.workingCopy"));
+ WicketUtils.setHtmlTooltip(lbl, getString("gb.workingCopyWarning"));
+ wc.add(lbl);
+ add(wc);
+ }
+
+ // fork controls
+ if (!allowForkControls() || user == null || !user.isAuthenticated) {
+ // must be logged-in to fork, hide all fork controls
+ add(new ExternalLink("forkLink", "").setVisible(false));
+ add(new ExternalLink("myForkLink", "").setVisible(false));
+ add(new Label("forksProhibitedIndicator").setVisible(false));
+ } else {
+ String fork = GitBlit.self().getFork(user.username, model.name);
+ boolean hasFork = fork != null;
+ boolean canFork = user.canFork(model);
+
+ if (hasFork || !canFork) {
+ // user not allowed to fork or fork already exists or repo forbids forking
+ add(new ExternalLink("forkLink", "").setVisible(false));
+
+ if (user.canFork() && !model.allowForks) {
+ // show forks prohibited indicator
+ Fragment wc = new Fragment("forksProhibitedIndicator", "forksProhibitedFragment", this);
+ Label lbl = new Label("forksProhibited", getString("gb.forksProhibited"));
+ WicketUtils.setHtmlTooltip(lbl, getString("gb.forksProhibitedWarning"));
+ wc.add(lbl);
+ add(wc);
+ } else {
+ // can not fork, no need for forks prohibited indicator
+ add(new Label("forksProhibitedIndicator").setVisible(false));
+ }
+
+ if (hasFork && !fork.equals(model.name)) {
+ // user has fork, view my fork link
+ String url = getRequestCycle().urlFor(SummaryPage.class, WicketUtils.newRepositoryParameter(fork)).toString();
+ add(new ExternalLink("myForkLink", url));
+ } else {
+ // no fork, hide view my fork link
+ add(new ExternalLink("myForkLink", "").setVisible(false));
+ }
+ } else if (canFork) {
+ // can fork and we do not have one
+ add(new Label("forksProhibitedIndicator").setVisible(false));
+ add(new ExternalLink("myForkLink", "").setVisible(false));
+ String url = getRequestCycle().urlFor(ForkPage.class, WicketUtils.newRepositoryParameter(model.name)).toString();
+ add(new ExternalLink("forkLink", url));
+ }
+ }
+
+ super.setupPage(repositoryName, pageName);
+ }
+
+ protected void addSyndicationDiscoveryLink() {
+ add(WicketUtils.syndicationDiscoveryLink(SyndicationServlet.getTitle(repositoryName,
+ objectId), SyndicationServlet.asLink(getRequest()
+ .getRelativePathPrefixToContextRoot(), repositoryName, objectId, 0)));
+ }
+
+ protected Repository getRepository() {
+ if (r == null) {
+ Repository r = GitBlit.self().getRepository(repositoryName);
+ if (r == null) {
+ error(getString("gb.canNotLoadRepository") + " " + repositoryName, true);
+ return null;
+ }
+ this.r = r;
+ }
+ return r;
+ }
+
+ protected RepositoryModel getRepositoryModel() {
+ if (m == null) {
+ RepositoryModel model = GitBlit.self().getRepositoryModel(
+ GitBlitWebSession.get().getUser(), repositoryName);
+ if (model == null) {
+ if (GitBlit.self().hasRepository(repositoryName, true)) {
+ // has repository, but unauthorized
+ authenticationError(getString("gb.unauthorizedAccessForRepository") + " " + repositoryName);
+ } else {
+ // does not have repository
+ error(getString("gb.canNotLoadRepository") + " " + repositoryName, true);
+ }
+ return null;
+ }
+ m = model;
+ }
+ return m;
+ }
+
+ protected RevCommit getCommit() {
+ RevCommit commit = JGitUtils.getCommit(r, objectId);
+ if (commit == null) {
+ error(MessageFormat.format(getString("gb.failedToFindCommit"),
+ objectId, repositoryName, getPageName()), true);
+ }
+ getSubmodules(commit);
+ return commit;
+ }
+
+ private Map<String, SubmoduleModel> getSubmodules(RevCommit commit) {
+ if (submodules == null) {
+ submodules = new HashMap<String, SubmoduleModel>();
+ for (SubmoduleModel model : JGitUtils.getSubmodules(r, commit.getTree())) {
+ submodules.put(model.path, model);
+ }
+ }
+ return submodules;
+ }
+
+ protected SubmoduleModel getSubmodule(String path) {
+ SubmoduleModel model = submodules.get(path);
+ if (model == null) {
+ // undefined submodule?!
+ model = new SubmoduleModel(path.substring(path.lastIndexOf('/') + 1), path, path);
+ model.hasSubmodule = false;
+ model.gitblitPath = model.name;
+ return model;
+ } else {
+ // extract the repository name from the clone url
+ List<String> patterns = GitBlit.getStrings(Keys.git.submoduleUrlPatterns);
+ String submoduleName = StringUtils.extractRepositoryPath(model.url, patterns.toArray(new String[0]));
+
+ // determine the current path for constructing paths relative
+ // to the current repository
+ String currentPath = "";
+ if (repositoryName.indexOf('/') > -1) {
+ currentPath = repositoryName.substring(0, repositoryName.lastIndexOf('/') + 1);
+ }
+
+ // try to locate the submodule repository
+ // prefer bare to non-bare names
+ List<String> candidates = new ArrayList<String>();
+
+ // relative
+ candidates.add(currentPath + StringUtils.stripDotGit(submoduleName));
+ candidates.add(candidates.get(candidates.size() - 1) + ".git");
+
+ // relative, no subfolder
+ if (submoduleName.lastIndexOf('/') > -1) {
+ String name = submoduleName.substring(submoduleName.lastIndexOf('/') + 1);
+ candidates.add(currentPath + StringUtils.stripDotGit(name));
+ candidates.add(currentPath + candidates.get(candidates.size() - 1) + ".git");
+ }
+
+ // absolute
+ candidates.add(StringUtils.stripDotGit(submoduleName));
+ candidates.add(candidates.get(candidates.size() - 1) + ".git");
+
+ // absolute, no subfolder
+ if (submoduleName.lastIndexOf('/') > -1) {
+ String name = submoduleName.substring(submoduleName.lastIndexOf('/') + 1);
+ candidates.add(StringUtils.stripDotGit(name));
+ candidates.add(candidates.get(candidates.size() - 1) + ".git");
+ }
+
+ // create a unique, ordered set of candidate paths
+ Set<String> paths = new LinkedHashSet<String>(candidates);
+ for (String candidate : paths) {
+ if (GitBlit.self().hasRepository(candidate)) {
+ model.hasSubmodule = true;
+ model.gitblitPath = candidate;
+ return model;
+ }
+ }
+
+ // we do not have a copy of the submodule, but we need a path
+ model.gitblitPath = candidates.get(0);
+ return model;
+ }
+ }
+
+ protected String getShortObjectId(String objectId) {
+ return objectId.substring(0, GitBlit.getInteger(Keys.web.shortCommitIdLength, 6));
+ }
+
+ protected void addRefs(Repository r, RevCommit c) {
+ add(new RefsPanel("refsPanel", repositoryName, c, JGitUtils.getAllRefs(r, getRepositoryModel().showRemoteBranches)));
+ }
+
+ protected void addFullText(String wicketId, String text, boolean substituteRegex) {
+ String html = StringUtils.escapeForHtml(text, true);
+ if (substituteRegex) {
+ html = GitBlit.self().processCommitMessage(repositoryName, text);
+ } else {
+ html = StringUtils.breakLinesForHtml(html);
+ }
+ add(new Label(wicketId, html).setEscapeModelStrings(false));
+ }
+
+ protected abstract String getPageName();
+
+ protected Component createPersonPanel(String wicketId, PersonIdent identity,
+ Constants.SearchType searchType) {
+ String name = identity == null ? "" : identity.getName();
+ String address = identity == null ? "" : identity.getEmailAddress();
+ name = StringUtils.removeNewlines(name);
+ address = StringUtils.removeNewlines(address);
+ boolean showEmail = GitBlit.getBoolean(Keys.web.showEmailAddresses, false);
+ if (!showEmail || StringUtils.isEmpty(name) || StringUtils.isEmpty(address)) {
+ String value = name;
+ if (StringUtils.isEmpty(value)) {
+ if (showEmail) {
+ value = address;
+ } else {
+ value = getString("gb.missingUsername");
+ }
+ }
+ Fragment partial = new Fragment(wicketId, "partialPersonIdent", this);
+ LinkPanel link = new LinkPanel("personName", "list", value, GitSearchPage.class,
+ WicketUtils.newSearchParameter(repositoryName, objectId, value, searchType));
+ setPersonSearchTooltip(link, value, searchType);
+ partial.add(link);
+ return partial;
+ } else {
+ Fragment fullPerson = new Fragment(wicketId, "fullPersonIdent", this);
+ LinkPanel nameLink = new LinkPanel("personName", "list", name, GitSearchPage.class,
+ WicketUtils.newSearchParameter(repositoryName, objectId, name, searchType));
+ setPersonSearchTooltip(nameLink, name, searchType);
+ fullPerson.add(nameLink);
+
+ LinkPanel addressLink = new LinkPanel("personAddress", "hidden-phone list", "<" + address + ">",
+ GitSearchPage.class, WicketUtils.newSearchParameter(repositoryName, objectId,
+ address, searchType));
+ setPersonSearchTooltip(addressLink, address, searchType);
+ fullPerson.add(addressLink);
+ return fullPerson;
+ }
+ }
+
+ protected void setPersonSearchTooltip(Component component, String value,
+ Constants.SearchType searchType) {
+ if (searchType.equals(Constants.SearchType.AUTHOR)) {
+ WicketUtils.setHtmlTooltip(component, getString("gb.searchForAuthor") + " " + value);
+ } else if (searchType.equals(Constants.SearchType.COMMITTER)) {
+ WicketUtils.setHtmlTooltip(component, getString("gb.searchForCommitter") + " " + value);
+ }
+ }
+
+ protected void setChangeTypeTooltip(Component container, ChangeType type) {
+ switch (type) {
+ case ADD:
+ WicketUtils.setHtmlTooltip(container, getString("gb.addition"));
+ break;
+ case COPY:
+ case RENAME:
+ WicketUtils.setHtmlTooltip(container, getString("gb.rename"));
+ break;
+ case DELETE:
+ WicketUtils.setHtmlTooltip(container, getString("gb.deletion"));
+ break;
+ case MODIFY:
+ WicketUtils.setHtmlTooltip(container, getString("gb.modification"));
+ break;
+ }
+ }
+
+ @Override
+ protected void onBeforeRender() {
+ // dispose of repository object
+ if (r != null) {
+ r.close();
+ r = null;
+ }
+ // setup page header and footer
+ setupPage(repositoryName, "/ " + getPageName());
+ super.onBeforeRender();
+ }
+
+ protected PageParameters newRepositoryParameter() {
+ return WicketUtils.newRepositoryParameter(repositoryName);
+ }
+
+ protected PageParameters newCommitParameter() {
+ return WicketUtils.newObjectParameter(repositoryName, objectId);
+ }
+
+ protected PageParameters newCommitParameter(String commitId) {
+ return WicketUtils.newObjectParameter(repositoryName, commitId);
+ }
+
+ public boolean isShowAdmin() {
+ return showAdmin;
+ }
+
+ public boolean isOwner() {
+ return isOwner;
+ }
+
+ private class SearchForm extends SessionlessForm<Void> implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ private final String repositoryName;
+
+ private final IModel<String> searchBoxModel = new Model<String>("");
+
+ private final IModel<Constants.SearchType> searchTypeModel = new Model<Constants.SearchType>(
+ Constants.SearchType.COMMIT);
+
+ public SearchForm(String id, String repositoryName) {
+ super(id, RepositoryPage.this.getClass(), RepositoryPage.this.getPageParameters());
+ this.repositoryName = repositoryName;
+ DropDownChoice<Constants.SearchType> searchType = new DropDownChoice<Constants.SearchType>(
+ "searchType", Arrays.asList(Constants.SearchType.values()));
+ searchType.setModel(searchTypeModel);
+ add(searchType.setVisible(GitBlit.getBoolean(Keys.web.showSearchTypeSelection, false)));
+ TextField<String> searchBox = new TextField<String>("searchBox", searchBoxModel);
+ add(searchBox);
+ }
+
+ void setTranslatedAttributes() {
+ WicketUtils.setHtmlTooltip(get("searchType"), getString("gb.searchTypeTooltip"));
+ WicketUtils.setHtmlTooltip(get("searchBox"),
+ MessageFormat.format(getString("gb.searchTooltip"), repositoryName));
+ WicketUtils.setInputPlaceholder(get("searchBox"), getString("gb.search"));
+ }
+
+ @Override
+ public void onSubmit() {
+ Constants.SearchType searchType = searchTypeModel.getObject();
+ String searchString = searchBoxModel.getObject();
+ if (searchString == null) {
+ return;
+ }
+ for (Constants.SearchType type : Constants.SearchType.values()) {
+ if (searchString.toLowerCase().startsWith(type.name().toLowerCase() + ":")) {
+ searchType = type;
+ searchString = searchString.substring(type.name().toLowerCase().length() + 1)
+ .trim();
+ break;
+ }
+ }
+ Class<? extends BasePage> searchPageClass = GitSearchPage.class;
+ RepositoryModel model = GitBlit.self().getRepositoryModel(repositoryName);
+ if (GitBlit.getBoolean(Keys.web.allowLuceneIndexing, true)
+ && !ArrayUtils.isEmpty(model.indexedBranches)) {
+ // this repository is Lucene-indexed
+ searchPageClass = LuceneSearchPage.class;
+ }
+ // use an absolute url to workaround Wicket-Tomcat problems with
+ // mounted url parameters (issue-111)
+ PageParameters params = WicketUtils.newSearchParameter(repositoryName, null, searchString, searchType);
+ String relativeUrl = urlFor(searchPageClass, params).toString();
+ String absoluteUrl = RequestUtils.toAbsolutePath(relativeUrl);
+ getRequestCycle().setRequestTarget(new RedirectRequestTarget(absoluteUrl));
+ }
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/pages/ReviewProposalPage.html b/src/main/java/com/gitblit/wicket/pages/ReviewProposalPage.html
new file mode 100644
index 00000000..6487a0ac
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/ReviewProposalPage.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+
+<body>
+<wicket:extend>
+ <!-- proposal info -->
+ <table class="plain">
+ <tr><th><wicket:message key="gb.received">received</wicket:message></th><td><span wicket:id="received">[received]</span></td></tr>
+ <tr><th><wicket:message key="gb.url">url</wicket:message></th><td><span wicket:id="url">[url]</span></td></tr>
+ <tr><th><wicket:message key="gb.message">message</wicket:message></th><td><span wicket:id="message">[message]</span></td></tr>
+ <tr><th><wicket:message key="gb.type">type</wicket:message></th><td><span wicket:id="tokenType">[token type]</span></td></tr>
+ <tr><th><wicket:message key="gb.token">token</wicket:message></th><td><span class="sha1" wicket:id="token">[token]</span></td></tr>
+ <tr><th valign="top"><wicket:message key="gb.proposal">proposal</wicket:message></th><td><span class="sha1" wicket:id="definition">[definition]</span></td></tr>
+ </table>
+
+ <div wicket:id="repositoriesPanel"></div>
+
+</wicket:extend>
+</body>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/ReviewProposalPage.java b/src/main/java/com/gitblit/wicket/pages/ReviewProposalPage.java
new file mode 100644
index 00000000..e1813861
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/ReviewProposalPage.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.pages;
+
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.wicket.PageParameters;
+import org.apache.wicket.markup.html.basic.Label;
+
+import com.gitblit.Constants.FederationToken;
+import com.gitblit.GitBlit;
+import com.gitblit.Keys;
+import com.gitblit.models.FederationProposal;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.wicket.RequiresAdminRole;
+import com.gitblit.wicket.WicketUtils;
+import com.gitblit.wicket.panels.RepositoriesPanel;
+
+@RequiresAdminRole
+public class ReviewProposalPage extends RootSubPage {
+
+ private final String PROPS_PATTERN = "{0} = {1}\n";
+
+ private final String WEBXML_PATTERN = "\n<context-param>\n\t<param-name>{0}</param-name>\n\t<param-value>{1}</param-value>\n</context-param>\n";
+
+ public ReviewProposalPage(PageParameters params) {
+ super(params);
+
+ final String token = WicketUtils.getToken(params);
+
+ FederationProposal proposal = GitBlit.self().getPendingFederationProposal(token);
+ if (proposal == null) {
+ error(getString("gb.couldNotFindFederationProposal"), true);
+ }
+
+ setupPage(getString("gb.proposals"), proposal.url);
+
+
+ add(new Label("url", proposal.url));
+ add(new Label("message", proposal.message));
+ add(WicketUtils.createTimestampLabel("received", proposal.received, getTimeZone(), getTimeUtils()));
+ add(new Label("token", proposal.token));
+ add(new Label("tokenType", proposal.tokenType.name()));
+
+ String p;
+ if (GitBlit.isGO()) {
+ // gitblit.properties definition
+ p = PROPS_PATTERN;
+ } else {
+ // web.xml definition
+ p = WEBXML_PATTERN;
+ }
+
+ // build proposed definition
+ StringBuilder sb = new StringBuilder();
+ sb.append(asParam(p, proposal.name, "url", proposal.url));
+ sb.append(asParam(p, proposal.name, "token", proposal.token));
+
+ if (FederationToken.USERS_AND_REPOSITORIES.equals(proposal.tokenType)
+ || FederationToken.ALL.equals(proposal.tokenType)) {
+ sb.append(asParam(p, proposal.name, "mergeAccounts", "false"));
+ }
+ sb.append(asParam(p, proposal.name, "frequency",
+ GitBlit.getString(Keys.federation.defaultFrequency, "60 mins")));
+ sb.append(asParam(p, proposal.name, "folder", proposal.name));
+ sb.append(asParam(p, proposal.name, "bare", "true"));
+ sb.append(asParam(p, proposal.name, "mirror", "true"));
+ sb.append(asParam(p, proposal.name, "sendStatus", "true"));
+ sb.append(asParam(p, proposal.name, "notifyOnError", "true"));
+ sb.append(asParam(p, proposal.name, "exclude", ""));
+ sb.append(asParam(p, proposal.name, "include", ""));
+
+ add(new Label("definition", StringUtils.breakLinesForHtml(StringUtils.escapeForHtml(sb
+ .toString().trim(), true))).setEscapeModelStrings(false));
+
+ List<RepositoryModel> repositories = new ArrayList<RepositoryModel>(
+ proposal.repositories.values());
+ RepositoriesPanel repositoriesPanel = new RepositoriesPanel("repositoriesPanel", false,
+ false, repositories, false, getAccessRestrictions());
+ add(repositoriesPanel);
+ }
+
+ private String asParam(String pattern, String name, String key, String value) {
+ return MessageFormat.format(pattern, Keys.federation._ROOT + "." + name + "." + key, value);
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/pages/RootPage.html b/src/main/java/com/gitblit/wicket/pages/RootPage.html
new file mode 100644
index 00000000..b35b1b3a
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/RootPage.html
@@ -0,0 +1,41 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+<body>
+<wicket:extend>
+ <div class="navbar navbar-fixed-top">
+ <div class="navbar-inner">
+ <div class="container">
+ <a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
+ <span class="icon-bar"></span>
+ <span class="icon-bar"></span>
+ <span class="icon-bar"></span>
+ </a>
+ <a class="brand" wicket:id="rootLink">
+ <img src="gitblt_25_white.png" width="79" height="25" alt="gitblit" class="logo"/>
+ </a>
+
+ <div class="nav-collapse" wicket:id="navPanel"></div>
+
+ <form class="pull-right" wicket:id="loginForm" style="padding-top:3px;">
+ <span class="form-search">
+ <input wicket:id="username" class="input-small" type="text" />
+ <input wicket:id="password" class="input-small" type="password" />
+ <button class="btn btn-primary" type="submit"><wicket:message key="gb.login"></wicket:message></button>
+ </span>
+ </form>
+ </div>
+ </div>
+ </div>
+
+ <!-- subclass content -->
+ <div class="container">
+ <div style="text-align:center" wicket:id="feedback">[Feedback Panel]</div>
+
+ <wicket:child/>
+ </div>
+</wicket:extend>
+</body>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/RootPage.java b/src/main/java/com/gitblit/wicket/pages/RootPage.java
new file mode 100644
index 00000000..adcd7b16
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/RootPage.java
@@ -0,0 +1,454 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.pages;
+
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.regex.Pattern;
+
+import org.apache.wicket.PageParameters;
+import org.apache.wicket.markup.html.form.PasswordTextField;
+import org.apache.wicket.markup.html.form.TextField;
+import org.apache.wicket.model.IModel;
+import org.apache.wicket.model.Model;
+import org.apache.wicket.protocol.http.WebResponse;
+
+import com.gitblit.Constants;
+import com.gitblit.GitBlit;
+import com.gitblit.Keys;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.TeamModel;
+import com.gitblit.models.UserModel;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.wicket.GitBlitWebSession;
+import com.gitblit.wicket.PageRegistration;
+import com.gitblit.wicket.PageRegistration.DropDownMenuItem;
+import com.gitblit.wicket.SessionlessForm;
+import com.gitblit.wicket.WicketUtils;
+import com.gitblit.wicket.panels.NavigationPanel;
+
+/**
+ * Root page is a topbar, navigable page like Repositories, Users, or
+ * Federation.
+ *
+ * @author James Moger
+ *
+ */
+public abstract class RootPage extends BasePage {
+
+ boolean showAdmin;
+
+ IModel<String> username = new Model<String>("");
+ IModel<String> password = new Model<String>("");
+ List<RepositoryModel> repositoryModels = new ArrayList<RepositoryModel>();
+
+ public RootPage() {
+ super();
+ }
+
+ public RootPage(PageParameters params) {
+ super(params);
+ }
+
+ @Override
+ protected void setupPage(String repositoryName, String pageName) {
+ boolean authenticateView = GitBlit.getBoolean(Keys.web.authenticateViewPages, false);
+ boolean authenticateAdmin = GitBlit.getBoolean(Keys.web.authenticateAdminPages, true);
+ boolean allowAdmin = GitBlit.getBoolean(Keys.web.allowAdministration, true);
+
+ if (authenticateAdmin) {
+ showAdmin = allowAdmin && GitBlitWebSession.get().canAdmin();
+ // authentication requires state and session
+ setStatelessHint(false);
+ } else {
+ showAdmin = allowAdmin;
+ if (authenticateView) {
+ // authentication requires state and session
+ setStatelessHint(false);
+ } else {
+ // no authentication required, no state and no session required
+ setStatelessHint(true);
+ }
+ }
+ boolean showRegistrations = GitBlit.canFederate()
+ && GitBlit.getBoolean(Keys.web.showFederationRegistrations, false);
+
+ // navigation links
+ List<PageRegistration> pages = new ArrayList<PageRegistration>();
+ pages.add(new PageRegistration("gb.repositories", RepositoriesPage.class,
+ getRootPageParameters()));
+ pages.add(new PageRegistration("gb.activity", ActivityPage.class, getRootPageParameters()));
+ if (GitBlit.getBoolean(Keys.web.allowLuceneIndexing, true)) {
+ pages.add(new PageRegistration("gb.search", LuceneSearchPage.class));
+ }
+ if (showAdmin) {
+ pages.add(new PageRegistration("gb.users", UsersPage.class));
+ }
+ if (showAdmin || showRegistrations) {
+ pages.add(new PageRegistration("gb.federation", FederationPage.class));
+ }
+
+ if (!authenticateView || (authenticateView && GitBlitWebSession.get().isLoggedIn())) {
+ addDropDownMenus(pages);
+ }
+
+ NavigationPanel navPanel = new NavigationPanel("navPanel", getClass(), pages);
+ add(navPanel);
+
+ // login form
+ SessionlessForm<Void> loginForm = new SessionlessForm<Void>("loginForm", getClass(), getPageParameters()) {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public void onSubmit() {
+ String username = RootPage.this.username.getObject();
+ char[] password = RootPage.this.password.getObject().toCharArray();
+
+ UserModel user = GitBlit.self().authenticate(username, password);
+ if (user == null) {
+ error(getString("gb.invalidUsernameOrPassword"));
+ } else if (user.username.equals(Constants.FEDERATION_USER)) {
+ // disallow the federation user from logging in via the
+ // web ui
+ error(getString("gb.invalidUsernameOrPassword"));
+ user = null;
+ } else {
+ loginUser(user);
+ }
+ }
+ };
+ TextField<String> unameField = new TextField<String>("username", username);
+ WicketUtils.setInputPlaceholder(unameField, getString("gb.username"));
+ loginForm.add(unameField);
+ PasswordTextField pwField = new PasswordTextField("password", password);
+ WicketUtils.setInputPlaceholder(pwField, getString("gb.password"));
+ loginForm.add(pwField);
+ add(loginForm);
+
+ if (authenticateView || authenticateAdmin) {
+ loginForm.setVisible(!GitBlitWebSession.get().isLoggedIn());
+ } else {
+ loginForm.setVisible(false);
+ }
+
+ // display an error message cached from a redirect
+ String cachedMessage = GitBlitWebSession.get().clearErrorMessage();
+ if (!StringUtils.isEmpty(cachedMessage)) {
+ error(cachedMessage);
+ } else if (showAdmin) {
+ int pendingProposals = GitBlit.self().getPendingFederationProposals().size();
+ if (pendingProposals == 1) {
+ info(getString("gb.OneProposalToReview"));
+ } else if (pendingProposals > 1) {
+ info(MessageFormat.format(getString("gb.nFederationProposalsToReview"),
+ pendingProposals));
+ }
+ }
+
+ super.setupPage(repositoryName, pageName);
+ }
+
+ private PageParameters getRootPageParameters() {
+ if (reusePageParameters()) {
+ PageParameters pp = getPageParameters();
+ if (pp != null) {
+ PageParameters params = new PageParameters(pp);
+ // remove named project parameter
+ params.remove("p");
+
+ // remove named repository parameter
+ params.remove("r");
+
+ // remove named user parameter
+ params.remove("user");
+
+ // remove days back parameter if it is the default value
+ if (params.containsKey("db")
+ && params.getInt("db") == GitBlit.getInteger(Keys.web.activityDuration, 14)) {
+ params.remove("db");
+ }
+ return params;
+ }
+ }
+ return null;
+ }
+
+ protected boolean reusePageParameters() {
+ return false;
+ }
+
+ private void loginUser(UserModel user) {
+ if (user != null) {
+ // Set the user into the session
+ GitBlitWebSession session = GitBlitWebSession.get();
+ // issue 62: fix session fixation vulnerability
+ session.replaceSession();
+ session.setUser(user);
+
+ // Set Cookie
+ if (GitBlit.getBoolean(Keys.web.allowCookieAuthentication, false)) {
+ WebResponse response = (WebResponse) getRequestCycle().getResponse();
+ GitBlit.self().setCookie(response, user);
+ }
+
+ if (!session.continueRequest()) {
+ PageParameters params = getPageParameters();
+ if (params == null) {
+ // redirect to this page
+ setResponsePage(getClass());
+ } else {
+ // Strip username and password and redirect to this page
+ params.remove("username");
+ params.remove("password");
+ setResponsePage(getClass(), params);
+ }
+ }
+ }
+ }
+
+ protected List<RepositoryModel> getRepositoryModels() {
+ if (repositoryModels.isEmpty()) {
+ final UserModel user = GitBlitWebSession.get().getUser();
+ List<RepositoryModel> repositories = GitBlit.self().getRepositoryModels(user);
+ repositoryModels.addAll(repositories);
+ Collections.sort(repositoryModels);
+ }
+ return repositoryModels;
+ }
+
+ protected void addDropDownMenus(List<PageRegistration> pages) {
+
+ }
+
+ protected List<DropDownMenuItem> getRepositoryFilterItems(PageParameters params) {
+ final UserModel user = GitBlitWebSession.get().getUser();
+ Set<DropDownMenuItem> filters = new LinkedHashSet<DropDownMenuItem>();
+ List<RepositoryModel> repositories = getRepositoryModels();
+
+ // accessible repositories by federation set
+ Map<String, AtomicInteger> setMap = new HashMap<String, AtomicInteger>();
+ for (RepositoryModel repository : repositories) {
+ for (String set : repository.federationSets) {
+ String key = set.toLowerCase();
+ if (setMap.containsKey(key)) {
+ setMap.get(key).incrementAndGet();
+ } else {
+ setMap.put(key, new AtomicInteger(1));
+ }
+ }
+ }
+ if (setMap.size() > 0) {
+ List<String> sets = new ArrayList<String>(setMap.keySet());
+ Collections.sort(sets);
+ for (String set : sets) {
+ filters.add(new DropDownMenuItem(MessageFormat.format("{0} ({1})", set,
+ setMap.get(set).get()), "set", set, params));
+ }
+ // divider
+ filters.add(new DropDownMenuItem());
+ }
+
+ // user's team memberships
+ if (user != null && user.teams.size() > 0) {
+ List<TeamModel> teams = new ArrayList<TeamModel>(user.teams);
+ Collections.sort(teams);
+ for (TeamModel team : teams) {
+ filters.add(new DropDownMenuItem(MessageFormat.format("{0} ({1})", team.name,
+ team.repositories.size()), "team", team.name, params));
+ }
+ // divider
+ filters.add(new DropDownMenuItem());
+ }
+
+ // custom filters
+ String customFilters = GitBlit.getString(Keys.web.customFilters, null);
+ if (!StringUtils.isEmpty(customFilters)) {
+ boolean addedExpression = false;
+ List<String> expressions = StringUtils.getStringsFromValue(customFilters, "!!!");
+ for (String expression : expressions) {
+ if (!StringUtils.isEmpty(expression)) {
+ addedExpression = true;
+ filters.add(new DropDownMenuItem(null, "x", expression, params));
+ }
+ }
+ // if we added any custom expressions, add a divider
+ if (addedExpression) {
+ filters.add(new DropDownMenuItem());
+ }
+ }
+ return new ArrayList<DropDownMenuItem>(filters);
+ }
+
+ protected List<DropDownMenuItem> getTimeFilterItems(PageParameters params) {
+ // days back choices - additive parameters
+ int daysBack = GitBlit.getInteger(Keys.web.activityDuration, 14);
+ if (daysBack < 1) {
+ daysBack = 14;
+ }
+ List<DropDownMenuItem> items = new ArrayList<DropDownMenuItem>();
+ Set<Integer> choicesSet = new HashSet<Integer>(Arrays.asList(daysBack, 14, 28, 60, 90, 180));
+ List<Integer> choices = new ArrayList<Integer>(choicesSet);
+ Collections.sort(choices);
+ String lastDaysPattern = getString("gb.lastNDays");
+ for (Integer db : choices) {
+ String txt = MessageFormat.format(lastDaysPattern, db);
+ items.add(new DropDownMenuItem(txt, "db", db.toString(), params));
+ }
+ items.add(new DropDownMenuItem());
+ return items;
+ }
+
+ protected List<RepositoryModel> getRepositories(PageParameters params) {
+ if (params == null) {
+ return getRepositoryModels();
+ }
+
+ boolean hasParameter = false;
+ String projectName = WicketUtils.getProjectName(params);
+ String userName = WicketUtils.getUsername(params);
+ if (StringUtils.isEmpty(projectName)) {
+ if (!StringUtils.isEmpty(userName)) {
+ projectName = "~" + userName;
+ }
+ }
+ String repositoryName = WicketUtils.getRepositoryName(params);
+ String set = WicketUtils.getSet(params);
+ String regex = WicketUtils.getRegEx(params);
+ String team = WicketUtils.getTeam(params);
+ int daysBack = params.getInt("db", 0);
+
+ List<RepositoryModel> availableModels = getRepositoryModels();
+ Set<RepositoryModel> models = new HashSet<RepositoryModel>();
+
+ if (!StringUtils.isEmpty(repositoryName)) {
+ // try named repository
+ hasParameter = true;
+ for (RepositoryModel model : availableModels) {
+ if (model.name.equalsIgnoreCase(repositoryName)) {
+ models.add(model);
+ break;
+ }
+ }
+ }
+
+ if (!StringUtils.isEmpty(projectName)) {
+ // try named project
+ hasParameter = true;
+ if (projectName.equalsIgnoreCase(GitBlit.getString(Keys.web.repositoryRootGroupName, "main"))) {
+ // root project/group
+ for (RepositoryModel model : availableModels) {
+ if (model.name.indexOf('/') == -1) {
+ models.add(model);
+ }
+ }
+ } else {
+ // named project/group
+ String group = projectName.toLowerCase() + "/";
+ for (RepositoryModel model : availableModels) {
+ if (model.name.toLowerCase().startsWith(group)) {
+ models.add(model);
+ }
+ }
+ }
+ }
+
+ if (!StringUtils.isEmpty(regex)) {
+ // filter the repositories by the regex
+ hasParameter = true;
+ Pattern pattern = Pattern.compile(regex);
+ for (RepositoryModel model : availableModels) {
+ if (pattern.matcher(model.name).find()) {
+ models.add(model);
+ }
+ }
+ }
+
+ if (!StringUtils.isEmpty(set)) {
+ // filter the repositories by the specified sets
+ hasParameter = true;
+ List<String> sets = StringUtils.getStringsFromValue(set, ",");
+ for (RepositoryModel model : availableModels) {
+ for (String curr : sets) {
+ if (model.federationSets.contains(curr)) {
+ models.add(model);
+ }
+ }
+ }
+ }
+
+ if (!StringUtils.isEmpty(team)) {
+ // filter the repositories by the specified teams
+ hasParameter = true;
+ List<String> teams = StringUtils.getStringsFromValue(team, ",");
+
+ // need TeamModels first
+ List<TeamModel> teamModels = new ArrayList<TeamModel>();
+ for (String name : teams) {
+ TeamModel teamModel = GitBlit.self().getTeamModel(name);
+ if (teamModel != null) {
+ teamModels.add(teamModel);
+ }
+ }
+
+ // brute-force our way through finding the matching models
+ for (RepositoryModel repositoryModel : availableModels) {
+ for (TeamModel teamModel : teamModels) {
+ if (teamModel.hasRepositoryPermission(repositoryModel.name)) {
+ models.add(repositoryModel);
+ }
+ }
+ }
+ }
+
+ if (!hasParameter) {
+ models.addAll(availableModels);
+ }
+
+ // time-filter the list
+ if (daysBack > 0) {
+ Calendar cal = Calendar.getInstance();
+ cal.set(Calendar.HOUR_OF_DAY, 0);
+ cal.set(Calendar.MINUTE, 0);
+ cal.set(Calendar.SECOND, 0);
+ cal.set(Calendar.MILLISECOND, 0);
+ cal.add(Calendar.DATE, -1 * daysBack);
+ Date threshold = cal.getTime();
+ Set<RepositoryModel> timeFiltered = new HashSet<RepositoryModel>();
+ for (RepositoryModel model : models) {
+ if (model.lastChange.after(threshold)) {
+ timeFiltered.add(model);
+ }
+ }
+ models = timeFiltered;
+ }
+
+ List<RepositoryModel> list = new ArrayList<RepositoryModel>(models);
+ Collections.sort(list);
+ return list;
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/pages/RootSubPage.html b/src/main/java/com/gitblit/wicket/pages/RootSubPage.html
new file mode 100644
index 00000000..2b109f9b
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/RootSubPage.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+
+<body>
+<wicket:extend>
+ <!-- page header -->
+ <div class="pageTitle">
+ <h2><span wicket:id="pageName">[page name]</span> <small><span wicket:id="pageSubName">[sub name]</span></small></h2>
+ </div>
+
+ <!-- Subclass Content -->
+ <wicket:child/>
+</wicket:extend>
+</body>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/RootSubPage.java b/src/main/java/com/gitblit/wicket/pages/RootSubPage.java
new file mode 100644
index 00000000..e7e12ccc
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/RootSubPage.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.pages;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.wicket.PageParameters;
+import org.apache.wicket.Session;
+import org.apache.wicket.markup.html.basic.Label;
+
+import com.gitblit.Constants.AccessRestrictionType;
+import com.gitblit.Constants.AuthorizationControl;
+import com.gitblit.GitBlit;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.UserModel;
+import com.gitblit.utils.StringUtils;
+
+/**
+ * RootSubPage is a non-topbar navigable RootPage. It also has a page header.
+ *
+ * @author James Moger
+ *
+ */
+public abstract class RootSubPage extends RootPage {
+
+ public RootSubPage() {
+ super();
+ createPageMapIfNeeded();
+ }
+
+ public RootSubPage(PageParameters params) {
+ super(params);
+ createPageMapIfNeeded();
+ }
+
+ protected boolean requiresPageMap() {
+ return false;
+ }
+
+ protected void createPageMapIfNeeded() {
+ if (requiresPageMap()) {
+ // because Gitblit strives for page-statelessness
+ // Wicket seems to get confused as to when it really should
+ // generate a page map for complex pages. Conditionally ensure we
+ // have a page map for complex AJAX pages like the EditNNN pages.
+ Session.get().pageMapForName(null, true);
+ setVersioned(true);
+ }
+ }
+
+ @Override
+ protected void setupPage(String pageName, String subName) {
+ add(new Label("pageName", pageName));
+ if (!StringUtils.isEmpty(subName)) {
+ subName = "/ " + subName;
+ }
+ add(new Label("pageSubName", subName));
+ super.setupPage("", pageName);
+ }
+
+ protected List<String> getAccessRestrictedRepositoryList(boolean includeWildcards, UserModel user) {
+ // build list of access-restricted projects
+ String lastProject = null;
+ List<String> repos = new ArrayList<String>();
+ if (includeWildcards) {
+ // all repositories
+ repos.add(".*");
+ // all repositories excluding personal repositories
+ repos.add("[^~].*");
+ }
+
+ for (String repo : GitBlit.self().getRepositoryList()) {
+ RepositoryModel repositoryModel = GitBlit.self().getRepositoryModel(repo);
+ if (repositoryModel.accessRestriction.exceeds(AccessRestrictionType.NONE)
+ && repositoryModel.authorizationControl.equals(AuthorizationControl.NAMED)) {
+ if (user != null &&
+ (repositoryModel.isOwner(user.username) || repositoryModel.isUsersPersonalRepository(user.username))) {
+ // exclude Owner or personal repositories
+ continue;
+ }
+ if (includeWildcards) {
+ if (lastProject == null || !lastProject.equalsIgnoreCase(repositoryModel.projectPath)) {
+ lastProject = repositoryModel.projectPath.toLowerCase();
+ if (!StringUtils.isEmpty(repositoryModel.projectPath)) {
+ // regex for all repositories within a project
+ repos.add(repositoryModel.projectPath + "/.*");
+ }
+ }
+ }
+ repos.add(repo.toLowerCase());
+ }
+ }
+ return repos;
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/pages/SendProposalPage.html b/src/main/java/com/gitblit/wicket/pages/SendProposalPage.html
new file mode 100644
index 00000000..cb9f3539
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/SendProposalPage.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+
+<wicket:extend>
+<body onload="document.getElementById('myUrl').focus();">
+ <!-- proposal info -->
+ <form wicket:id="editForm">
+ <table class="plain">
+ <tr><th><wicket:message key="gb.url">url</wicket:message></th><td class="edit"><input class="span6" type="text" wicket:id="myUrl" id="myUrl" size="60" /> &nbsp;<i><wicket:message key="gb.myUrlDescription"></wicket:message></i></td></tr>
+ <tr><th><wicket:message key="gb.destinationUrl">destination url</wicket:message></th><td class="edit"><input class="span6" type="text" wicket:id="destinationUrl" size="60" /> &nbsp;<i><wicket:message key="gb.destinationUrlDescription"></wicket:message></i></td></tr>
+ <tr><th valign="top"><wicket:message key="gb.message">message</wicket:message></th><td class="edit"><input class="span8" type="text" wicket:id="message" size="80" /></td></tr>
+ <tr><th><wicket:message key="gb.type">type</wicket:message></th><td><span wicket:id="tokenType">[token type]</span></td></tr>
+ <tr><th><wicket:message key="gb.token">token</wicket:message></th><td><span class="sha1" wicket:id="token">[token]</span></td></tr>
+ <tr><th></th><td class="editButton"><input class="btn btn-primary" type="submit" value="propose" wicket:message="value:gb.sendProposal" wicket:id="save" /> &nbsp; <input class="btn" type="submit" value="Cancel" wicket:message="value:gb.cancel" wicket:id="cancel" /></td></tr>
+ </table>
+ </form>
+
+ <div style="padding-top:10px;" wicket:id="repositoriesPanel"></div>
+</body>
+</wicket:extend>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/SendProposalPage.java b/src/main/java/com/gitblit/wicket/pages/SendProposalPage.java
new file mode 100644
index 00000000..fc5f95b5
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/SendProposalPage.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.pages;
+
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.wicket.PageParameters;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.form.Button;
+import org.apache.wicket.markup.html.form.Form;
+import org.apache.wicket.markup.html.form.TextField;
+import org.apache.wicket.model.CompoundPropertyModel;
+
+import com.gitblit.Constants.FederationProposalResult;
+import com.gitblit.GitBlit;
+import com.gitblit.models.FederationProposal;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.utils.FederationUtils;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.wicket.RequiresAdminRole;
+import com.gitblit.wicket.WicketUtils;
+import com.gitblit.wicket.panels.RepositoriesPanel;
+
+@RequiresAdminRole
+public class SendProposalPage extends RootSubPage {
+
+ public String myUrl;
+
+ public String destinationUrl;
+
+ public String message;
+
+ public SendProposalPage(PageParameters params) {
+ super(params);
+
+ setupPage(getString("gb.sendProposal"), "");
+ setStatelessHint(true);
+
+ final String token = WicketUtils.getToken(params);
+
+ myUrl = WicketUtils.getGitblitURL(getRequest());
+ destinationUrl = "https://";
+
+ // temporary proposal
+ FederationProposal proposal = GitBlit.self().createFederationProposal(myUrl, token);
+ if (proposal == null) {
+ error(getString("gb.couldNotCreateFederationProposal"), true);
+ }
+
+ CompoundPropertyModel<SendProposalPage> model = new CompoundPropertyModel<SendProposalPage>(
+ this);
+
+ Form<SendProposalPage> form = new Form<SendProposalPage>("editForm", model) {
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ protected void onSubmit() {
+ // confirm a repository name was entered
+ if (StringUtils.isEmpty(myUrl)) {
+ error(getString("gb.pleaseSetGitblitUrl"));
+ return;
+ }
+ if (StringUtils.isEmpty(destinationUrl)) {
+ error(getString("gb.pleaseSetDestinationUrl"));
+ return;
+ }
+
+ // build new proposal
+ FederationProposal proposal = GitBlit.self().createFederationProposal(myUrl, token);
+ proposal.url = myUrl;
+ proposal.message = message;
+ try {
+ FederationProposalResult res = FederationUtils
+ .propose(destinationUrl, proposal);
+ switch (res) {
+ case ACCEPTED:
+ info(MessageFormat.format(getString("gb.proposalReceived"),
+ destinationUrl));
+ setResponsePage(RepositoriesPage.class);
+ break;
+ case NO_POKE:
+ error(MessageFormat.format(getString("noGitblitFound"),
+ destinationUrl, myUrl));
+ break;
+ case NO_PROPOSALS:
+ error(MessageFormat.format(getString("gb.noProposals"),
+ destinationUrl));
+ break;
+ case FEDERATION_DISABLED:
+ error(MessageFormat
+ .format(getString("gb.noFederation"),
+ destinationUrl));
+ break;
+ case MISSING_DATA:
+ error(MessageFormat.format(getString("gb.proposalFailed"),
+ destinationUrl));
+ break;
+ case ERROR:
+ error(MessageFormat.format(getString("gb.proposalError"),
+ destinationUrl));
+ break;
+ }
+ } catch (Exception e) {
+ if (!StringUtils.isEmpty(e.getMessage())) {
+ error(e.getMessage());
+ } else {
+ error(getString("gb.failedToSendProposal"));
+ }
+ }
+ }
+ };
+ form.add(new TextField<String>("myUrl"));
+ form.add(new TextField<String>("destinationUrl"));
+ form.add(new TextField<String>("message"));
+ form.add(new Label("tokenType", proposal.tokenType.name()));
+ form.add(new Label("token", proposal.token));
+
+ form.add(new Button("save"));
+ Button cancel = new Button("cancel") {
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public void onSubmit() {
+ setResponsePage(FederationPage.class);
+ }
+ };
+ cancel.setDefaultFormProcessing(false);
+ form.add(cancel);
+ add(form);
+
+ List<RepositoryModel> repositories = new ArrayList<RepositoryModel>(
+ proposal.repositories.values());
+ RepositoriesPanel repositoriesPanel = new RepositoriesPanel("repositoriesPanel", false,
+ false, repositories, false, getAccessRestrictions());
+ add(repositoriesPanel);
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/pages/SummaryPage.html b/src/main/java/com/gitblit/wicket/pages/SummaryPage.html
new file mode 100644
index 00000000..3e85df99
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/SummaryPage.html
@@ -0,0 +1,54 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+<body>
+<wicket:extend>
+
+ <div style="clear:both;">
+ <!-- Repository Activity Chart -->
+ <div class="hidden-phone" style="float:right;">
+ <img class="activityGraph" wicket:id="commitsChart" />
+ </div>
+
+ <!-- Repository info -->
+ <div class="hidden-phone" style="padding-bottom: 10px;">
+ <table class="plain">
+ <tr><th><wicket:message key="gb.description">[description]</wicket:message></th><td><span wicket:id="repositoryDescription">[repository description]</span></td></tr>
+ <tr><th><wicket:message key="gb.owners">[owner]</wicket:message></th><td><span wicket:id="repositoryOwners"><span wicket:id="owner"></span><span wicket:id="comma"></span></span></td></tr>
+ <tr><th><wicket:message key="gb.lastChange">[last change]</wicket:message></th><td><span wicket:id="repositoryLastChange">[repository last change]</span></td></tr>
+ <tr><th><wicket:message key="gb.stats">[stats]</wicket:message></th><td><span wicket:id="branchStats">[branch stats]</span> <span class="link"><a wicket:id="metrics"><wicket:message key="gb.metrics">[metrics]</wicket:message></a></span></td></tr>
+ <tr><th style="vertical-align:top;"><wicket:message key="gb.repositoryUrl">[URL]</wicket:message>&nbsp;<img style="vertical-align: top;padding-left:3px;" wicket:id="accessRestrictionIcon" /></th><td><span wicket:id="repositoryCloneUrl">[repository clone url]</span><div wicket:id="otherUrls"></div></td></tr>
+ </table>
+ </div>
+ </div>
+
+ <!-- commits -->
+ <div style="padding-bottom:15px;" wicket:id="commitsPanel">[commits panel]</div>
+
+ <!-- tags -->
+ <div style="padding-bottom:15px;" wicket:id="tagsPanel">[tags panel]</div>
+
+ <!-- branches -->
+ <div style="padding-bottom:15px;" wicket:id="branchesPanel">[branches panel]</div>
+
+ <!-- markdown readme -->
+ <div wicket:id="readme"></div>
+
+ <wicket:fragment wicket:id="markdownPanel">
+ <div class="header" style="margin-top:0px;" >
+ <i style="vertical-align: middle;" class="icon-book"></i>
+ <span style="font-weight:bold;vertical-align:middle;" wicket:id="readmeFile"></span>
+ </div>
+ <div style="border:1px solid #ddd;border-radius: 0 0 3px 3px;padding: 20px;">
+ <div wicket:id="readmeContent" class="markdown"></div>
+ </div>
+ </wicket:fragment>
+
+ <wicket:fragment wicket:id="ownersFragment">
+
+ </wicket:fragment>
+</wicket:extend>
+</body>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/SummaryPage.java b/src/main/java/com/gitblit/wicket/pages/SummaryPage.java
new file mode 100644
index 00000000..bd40a1b7
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/SummaryPage.java
@@ -0,0 +1,238 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.pages;
+
+import java.awt.Color;
+import java.awt.Dimension;
+import java.text.MessageFormat;
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.wicket.Component;
+import org.apache.wicket.PageParameters;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.link.BookmarkablePageLink;
+import org.apache.wicket.markup.html.panel.Fragment;
+import org.apache.wicket.markup.repeater.Item;
+import org.apache.wicket.markup.repeater.data.DataView;
+import org.apache.wicket.markup.repeater.data.ListDataProvider;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.wicketstuff.googlecharts.Chart;
+import org.wicketstuff.googlecharts.ChartAxis;
+import org.wicketstuff.googlecharts.ChartAxisType;
+import org.wicketstuff.googlecharts.ChartProvider;
+import org.wicketstuff.googlecharts.ChartType;
+import org.wicketstuff.googlecharts.IChartData;
+import org.wicketstuff.googlecharts.LineStyle;
+import org.wicketstuff.googlecharts.MarkerType;
+import org.wicketstuff.googlecharts.ShapeMarker;
+
+import com.gitblit.Constants.AccessRestrictionType;
+import com.gitblit.GitBlit;
+import com.gitblit.Keys;
+import com.gitblit.models.Metric;
+import com.gitblit.models.PathModel;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.UserModel;
+import com.gitblit.utils.ArrayUtils;
+import com.gitblit.utils.JGitUtils;
+import com.gitblit.utils.MarkdownUtils;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.wicket.WicketUtils;
+import com.gitblit.wicket.panels.BranchesPanel;
+import com.gitblit.wicket.panels.LinkPanel;
+import com.gitblit.wicket.panels.LogPanel;
+import com.gitblit.wicket.panels.RepositoryUrlPanel;
+import com.gitblit.wicket.panels.TagsPanel;
+
+public class SummaryPage extends RepositoryPage {
+
+ public SummaryPage(PageParameters params) {
+ super(params);
+
+ int numberCommits = GitBlit.getInteger(Keys.web.summaryCommitCount, 20);
+ if (numberCommits <= 0) {
+ numberCommits = 20;
+ }
+ int numberRefs = GitBlit.getInteger(Keys.web.summaryRefsCount, 5);
+
+ Repository r = getRepository();
+ RepositoryModel model = getRepositoryModel();
+
+ List<Metric> metrics = null;
+ Metric metricsTotal = null;
+ if (!model.skipSummaryMetrics && GitBlit.getBoolean(Keys.web.generateActivityGraph, true)) {
+ metrics = GitBlit.self().getRepositoryDefaultMetrics(model, r);
+ metricsTotal = metrics.remove(0);
+ }
+
+ addSyndicationDiscoveryLink();
+
+ // repository description
+ add(new Label("repositoryDescription", getRepositoryModel().description));
+
+ // owner links
+ final List<String> owners = new ArrayList<String>(getRepositoryModel().owners);
+ ListDataProvider<String> ownersDp = new ListDataProvider<String>(owners);
+ DataView<String> ownersView = new DataView<String>("repositoryOwners", ownersDp) {
+ private static final long serialVersionUID = 1L;
+ int counter = 0;
+ public void populateItem(final Item<String> item) {
+ UserModel ownerModel = GitBlit.self().getUserModel(item.getModelObject());
+ if (ownerModel != null) {
+ item.add(new LinkPanel("owner", null, ownerModel.getDisplayName(), UserPage.class,
+ WicketUtils.newUsernameParameter(ownerModel.username)).setRenderBodyOnly(true));
+ } else {
+ item.add(new Label("owner").setVisible(false));
+ }
+ counter++;
+ item.add(new Label("comma", ",").setVisible(counter < owners.size()));
+ item.setRenderBodyOnly(true);
+ }
+ };
+ ownersView.setRenderBodyOnly(true);
+ add(ownersView);
+
+ add(WicketUtils.createTimestampLabel("repositoryLastChange",
+ JGitUtils.getLastChange(r), getTimeZone(), getTimeUtils()));
+ if (metricsTotal == null) {
+ add(new Label("branchStats", ""));
+ } else {
+ add(new Label("branchStats",
+ MessageFormat.format(getString("gb.branchStats"), metricsTotal.count,
+ metricsTotal.tag, getTimeUtils().duration(metricsTotal.duration))));
+ }
+ add(new BookmarkablePageLink<Void>("metrics", MetricsPage.class,
+ WicketUtils.newRepositoryParameter(repositoryName)));
+
+ List<String> repositoryUrls = new ArrayList<String>();
+
+ if (GitBlit.getBoolean(Keys.git.enableGitServlet, true)) {
+ AccessRestrictionType accessRestriction = getRepositoryModel().accessRestriction;
+ switch (accessRestriction) {
+ case NONE:
+ add(WicketUtils.newClearPixel("accessRestrictionIcon").setVisible(false));
+ break;
+ case PUSH:
+ add(WicketUtils.newImage("accessRestrictionIcon", "lock_go_16x16.png",
+ getAccessRestrictions().get(accessRestriction)));
+ break;
+ case CLONE:
+ add(WicketUtils.newImage("accessRestrictionIcon", "lock_pull_16x16.png",
+ getAccessRestrictions().get(accessRestriction)));
+ break;
+ case VIEW:
+ add(WicketUtils.newImage("accessRestrictionIcon", "shield_16x16.png",
+ getAccessRestrictions().get(accessRestriction)));
+ break;
+ default:
+ add(WicketUtils.newClearPixel("accessRestrictionIcon").setVisible(false));
+ }
+ // add the Gitblit repository url
+ repositoryUrls.add(getRepositoryUrl(getRepositoryModel()));
+ } else {
+ add(WicketUtils.newClearPixel("accessRestrictionIcon").setVisible(false));
+ }
+ repositoryUrls.addAll(GitBlit.self().getOtherCloneUrls(repositoryName));
+
+ String primaryUrl = ArrayUtils.isEmpty(repositoryUrls) ? "" : repositoryUrls.remove(0);
+ add(new RepositoryUrlPanel("repositoryCloneUrl", primaryUrl));
+
+ add(new Label("otherUrls", StringUtils.flattenStrings(repositoryUrls, "<br/>"))
+ .setEscapeModelStrings(false));
+
+ add(new LogPanel("commitsPanel", repositoryName, getRepositoryModel().HEAD, r, numberCommits, 0, getRepositoryModel().showRemoteBranches));
+ add(new TagsPanel("tagsPanel", repositoryName, r, numberRefs).hideIfEmpty());
+ add(new BranchesPanel("branchesPanel", getRepositoryModel(), r, numberRefs, false).hideIfEmpty());
+
+ if (getRepositoryModel().showReadme) {
+ String htmlText = null;
+ String markdownText = null;
+ String readme = null;
+ try {
+ RevCommit head = JGitUtils.getCommit(r, null);
+ List<String> markdownExtensions = GitBlit.getStrings(Keys.web.markdownExtensions);
+ List<PathModel> paths = JGitUtils.getFilesInPath(r, null, head);
+ for (PathModel path : paths) {
+ if (!path.isTree()) {
+ String name = path.name.toLowerCase();
+
+ if (name.startsWith("readme")) {
+ if (name.indexOf('.') > -1) {
+ String ext = name.substring(name.lastIndexOf('.') + 1);
+ if (markdownExtensions.contains(ext)) {
+ readme = path.name;
+ break;
+ }
+ }
+ }
+ }
+ }
+ if (!StringUtils.isEmpty(readme)) {
+ String [] encodings = GitBlit.getEncodings();
+ markdownText = JGitUtils.getStringContent(r, head.getTree(), readme, encodings);
+ htmlText = MarkdownUtils.transformMarkdown(markdownText);
+ }
+ } catch (ParseException p) {
+ markdownText = MessageFormat.format("<div class=\"alert alert-error\"><strong>{0}:</strong> {1}</div>{2}", getString("gb.error"), getString("gb.markdownFailure"), markdownText);
+ htmlText = StringUtils.breakLinesForHtml(markdownText);
+ }
+ Fragment fragment = new Fragment("readme", "markdownPanel");
+ fragment.add(new Label("readmeFile", readme));
+ // Add the html to the page
+ Component content = new Label("readmeContent", htmlText).setEscapeModelStrings(false);
+ fragment.add(content.setVisible(!StringUtils.isEmpty(htmlText)));
+ add(fragment);
+ } else {
+ add(new Label("readme").setVisible(false));
+ }
+
+ // Display an activity line graph
+ insertActivityGraph(metrics);
+ }
+
+ @Override
+ protected String getPageName() {
+ return getString("gb.summary");
+ }
+
+ private void insertActivityGraph(List<Metric> metrics) {
+ if ((metrics != null) && (metrics.size() > 0)
+ && GitBlit.getBoolean(Keys.web.generateActivityGraph, true)) {
+ IChartData data = WicketUtils.getChartData(metrics);
+
+ ChartProvider provider = new ChartProvider(new Dimension(290, 100), ChartType.LINE,
+ data);
+ ChartAxis dateAxis = new ChartAxis(ChartAxisType.BOTTOM);
+ dateAxis.setLabels(new String[] { metrics.get(0).name,
+ metrics.get(metrics.size() / 2).name, metrics.get(metrics.size() - 1).name });
+ provider.addAxis(dateAxis);
+
+ ChartAxis commitAxis = new ChartAxis(ChartAxisType.LEFT);
+ commitAxis.setLabels(new String[] { "",
+ String.valueOf((int) WicketUtils.maxValue(metrics)) });
+ provider.addAxis(commitAxis);
+ provider.setLineStyles(new LineStyle[] { new LineStyle(2, 4, 0), new LineStyle(0, 4, 1) });
+ provider.addShapeMarker(new ShapeMarker(MarkerType.CIRCLE, Color.BLUE, 1, -1, 5));
+
+ add(new Chart("commitsChart", provider));
+ } else {
+ add(WicketUtils.newBlankImage("commitsChart"));
+ }
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/pages/TagPage.html b/src/main/java/com/gitblit/wicket/pages/TagPage.html
new file mode 100644
index 00000000..19f828e3
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/TagPage.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+
+<body>
+<wicket:extend>
+
+ <!-- summary header -->
+ <div style="margin-top: 5px;" class="header" wicket:id="commit">[shortlog header]</div>
+
+ <!-- Tagger Gravatar -->
+ <span style="float:right;vertical-align: top;" wicket:id="taggerAvatar" />
+
+ <!-- commit info -->
+ <table class="plain">
+ <tr><th><wicket:message key="gb.name">[name]</wicket:message></th><td><span wicket:id="tagName">[tag name]</span></td></tr>
+ <tr><th><wicket:message key="gb.tag">[tag]</wicket:message></th><td><span class="sha1" wicket:id="tagId">[tag id]</span></td></tr>
+ <tr><th><wicket:message key="gb.object">[object]</wicket:message></th><td><span class="sha1" wicket:id="taggedObject">[tagged object]</span> <span class="link" wicket:id="taggedObjectType"></span></td></tr>
+ <tr><th><wicket:message key="gb.tagger">[tagger]</wicket:message></th><td><span class="sha1" wicket:id="tagger">[tagger]</span></td></tr>
+ <tr><th></th><td><span class="sha1" wicket:id="tagDate">[tag date]</span></td></tr>
+ </table>
+
+ <!-- full message -->
+ <div style="border-bottom:0px;" class="commit_message" wicket:id="fullMessage">[tag full message]</div>
+
+ <wicket:fragment wicket:id="fullPersonIdent">
+ <span wicket:id="personName"></span><span wicket:id="personAddress"></span>
+ </wicket:fragment>
+
+ <wicket:fragment wicket:id="partialPersonIdent">
+ <span wicket:id="personName"></span>
+ </wicket:fragment>
+
+</wicket:extend>
+</body>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/TagPage.java b/src/main/java/com/gitblit/wicket/pages/TagPage.java
new file mode 100644
index 00000000..91c913d2
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/TagPage.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.pages;
+
+import java.text.MessageFormat;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+
+import org.apache.wicket.PageParameters;
+import org.apache.wicket.markup.html.basic.Label;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.Repository;
+
+import com.gitblit.models.RefModel;
+import com.gitblit.utils.JGitUtils;
+import com.gitblit.wicket.WicketUtils;
+import com.gitblit.wicket.panels.GravatarImage;
+import com.gitblit.wicket.panels.LinkPanel;
+import com.gitblit.wicket.panels.RefsPanel;
+
+public class TagPage extends RepositoryPage {
+
+ public TagPage(PageParameters params) {
+ super(params);
+
+ Repository r = getRepository();
+
+ // Find tag in repository
+ List<RefModel> tags = JGitUtils.getTags(r, true, -1);
+ RefModel tagRef = null;
+ for (RefModel tag : tags) {
+ if (tag.getName().equals(objectId) || tag.getObjectId().getName().equals(objectId)) {
+ tagRef = tag;
+ break;
+ }
+ }
+
+ // Failed to find tag!
+ if (tagRef == null) {
+ error(MessageFormat.format(getString("gb.couldNotFindTag"), objectId), true);
+ }
+
+ // Display tag.
+ Class<? extends RepositoryPage> linkClass;
+ PageParameters linkParameters = newCommitParameter(tagRef.getReferencedObjectId().getName());
+ String typeKey;
+ switch (tagRef.getReferencedObjectType()) {
+ case Constants.OBJ_BLOB:
+ typeKey = "gb.blob";
+ linkClass = BlobPage.class;
+ break;
+ case Constants.OBJ_TREE:
+ typeKey = "gb.tree";
+ linkClass = TreePage.class;
+ break;
+ case Constants.OBJ_COMMIT:
+ default:
+ typeKey = "gb.commit";
+ linkClass = CommitPage.class;
+ break;
+ }
+ add(new LinkPanel("commit", "title", tagRef.displayName, linkClass, linkParameters));
+ add(new GravatarImage("taggerAvatar", tagRef.getAuthorIdent()));
+
+ add(new RefsPanel("tagName", repositoryName, Arrays.asList(tagRef)));
+ add(new Label("tagId", tagRef.getObjectId().getName()));
+ add(new LinkPanel("taggedObject", "list", tagRef.getReferencedObjectId().getName(),
+ linkClass, linkParameters));
+ add(new Label("taggedObjectType", getString(typeKey)));
+
+ add(createPersonPanel("tagger", tagRef.getAuthorIdent(), com.gitblit.Constants.SearchType.AUTHOR));
+ Date when = new Date(0);
+ if (tagRef.getAuthorIdent() != null) {
+ when = tagRef.getAuthorIdent().getWhen();
+ }
+ add(WicketUtils.createTimestampLabel("tagDate", when, getTimeZone(), getTimeUtils()));
+
+ addFullText("fullMessage", tagRef.getFullMessage(), true);
+ }
+
+ @Override
+ protected String getPageName() {
+ return getString("gb.tag");
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/pages/TagsPage.html b/src/main/java/com/gitblit/wicket/pages/TagsPage.html
new file mode 100644
index 00000000..03f1a0d7
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/TagsPage.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+
+<body>
+<wicket:extend>
+
+ <!-- tags panel -->
+ <div style="margin-top:5px;" wicket:id="tagsPanel">[tags panel]</div>
+
+</wicket:extend>
+</body>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/TagsPage.java b/src/main/java/com/gitblit/wicket/pages/TagsPage.java
new file mode 100644
index 00000000..3ddbde9b
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/TagsPage.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.pages;
+
+import org.apache.wicket.PageParameters;
+
+import com.gitblit.wicket.panels.TagsPanel;
+
+public class TagsPage extends RepositoryPage {
+
+ public TagsPage(PageParameters params) {
+ super(params);
+
+ add(new TagsPanel("tagsPanel", repositoryName, getRepository(), -1));
+
+ }
+
+ @Override
+ protected String getPageName() {
+ return getString("gb.tags");
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/pages/TicketPage.html b/src/main/java/com/gitblit/wicket/pages/TicketPage.html
new file mode 100644
index 00000000..ed3eb229
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/TicketPage.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+
+<body>
+<wicket:extend>
+
+ <!-- ticket title -->
+ <div style="font-size:150%;padding-top:5px;padding-bottom:5px;" wicket:id="ticketTitle">[ticket title]</div>
+
+ <!-- ticket info -->
+ <table class="plain">
+ <tr><th><wicket:message key="gb.ticketId">ticket id</wicket:message></th><td><span class="sha1" wicket:id="ticketId">[ticket id]</span></td></tr>
+ <tr><th><wicket:message key="gb.ticketAssigned">assigned</wicket:message></th><td><span wicket:id=ticketHandler>[ticket title]</span></td></tr>
+ <tr><th><wicket:message key="gb.ticketOpenDate">open date</wicket:message></th><td><span wicket:id="ticketOpenDate">[ticket open date]</span></td></tr>
+ <tr><th><wicket:message key="gb.ticketState">state</wicket:message></th><td><span wicket:id="ticketState">[ticket state]</span></td></tr>
+ <tr><th><wicket:message key="gb.tags">tags</wicket:message></th><td><span wicket:id="ticketTags">[ticket tags]</span></td></tr>
+ </table>
+
+ <!-- comments header -->
+ <div class="header"><wicket:message key="gb.ticketComments">comments</wicket:message></div>
+
+ <!-- comments -->
+ <table class="comments">
+ <tbody>
+ <tr valign="top" wicket:id="comment">
+ <td><span class="author" wicket:id="commentAuthor">[comment author]</span><br/>
+ <span class="date" wicket:id="commentDate">[comment date]</span>
+ </td>
+ <td><span wicket:id="commentText">[comment text]</span></td>
+ </tr>
+ </tbody>
+ </table>
+
+</wicket:extend>
+</body>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/TicketPage.java b/src/main/java/com/gitblit/wicket/pages/TicketPage.java
new file mode 100644
index 00000000..57233867
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/TicketPage.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.pages;
+
+import org.apache.wicket.PageParameters;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.repeater.Item;
+import org.apache.wicket.markup.repeater.data.DataView;
+import org.apache.wicket.markup.repeater.data.ListDataProvider;
+import org.eclipse.jgit.lib.Repository;
+
+import com.gitblit.models.TicketModel;
+import com.gitblit.models.TicketModel.Comment;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.utils.TicgitUtils;
+import com.gitblit.wicket.GitBlitWebSession;
+import com.gitblit.wicket.WicketUtils;
+
+public class TicketPage extends RepositoryPage {
+
+ public TicketPage(PageParameters params) {
+ super(params);
+
+ final String ticketFolder = WicketUtils.getPath(params);
+
+ Repository r = getRepository();
+ TicketModel t = TicgitUtils.getTicket(r, ticketFolder);
+
+ add(new Label("ticketTitle", t.title));
+ add(new Label("ticketId", t.id));
+ add(new Label("ticketHandler", t.handler.toLowerCase()));
+ add(WicketUtils.createTimestampLabel("ticketOpenDate", t.date, getTimeZone(), getTimeUtils()));
+ Label stateLabel = new Label("ticketState", t.state);
+ WicketUtils.setTicketCssClass(stateLabel, t.state);
+ add(stateLabel);
+ add(new Label("ticketTags", StringUtils.flattenStrings(t.tags)));
+
+ ListDataProvider<Comment> commentsDp = new ListDataProvider<Comment>(t.comments);
+ DataView<Comment> commentsView = new DataView<Comment>("comment", commentsDp) {
+ private static final long serialVersionUID = 1L;
+ int counter;
+
+ public void populateItem(final Item<Comment> item) {
+ final Comment entry = item.getModelObject();
+ item.add(WicketUtils.createDateLabel("commentDate", entry.date, GitBlitWebSession
+ .get().getTimezone(), getTimeUtils()));
+ item.add(new Label("commentAuthor", entry.author.toLowerCase()));
+ item.add(new Label("commentText", prepareComment(entry.text))
+ .setEscapeModelStrings(false));
+ WicketUtils.setAlternatingBackground(item, counter);
+ counter++;
+ }
+ };
+ add(commentsView);
+ }
+
+ @Override
+ protected String getPageName() {
+ return getString("gb.ticket");
+ }
+
+ private String prepareComment(String comment) {
+ String html = StringUtils.escapeForHtml(comment, false);
+ html = StringUtils.breakLinesForHtml(comment).trim();
+ return html.replaceAll("\\bcommit\\s*([A-Za-z0-9]*)\\b", "<a href=\"/commit/"
+ + repositoryName + "/$1\">commit $1</a>");
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/pages/TicketsPage.html b/src/main/java/com/gitblit/wicket/pages/TicketsPage.html
new file mode 100644
index 00000000..0913dc2c
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/TicketsPage.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+
+<body>
+<wicket:extend>
+
+ <!-- header -->
+ <div style="margin-top:5px;" class="header" wicket:id="header">[header]</div>
+
+ <!-- tickets -->
+ <table class="pretty">
+ <tbody>
+ <tr wicket:id="ticket">
+ <td style="padding:0; margin:0;"><div wicket:id="ticketState">[ticket state]</div></td>
+ <td class="date"><span wicket:id="ticketDate">[ticket date]</span></td>
+ <td class="author"><div wicket:id="ticketHandler">[ticket handler]</div></td>
+ <td><div wicket:id="ticketTitle">[ticket title]</div></td>
+ </tr>
+ </tbody>
+ </table>
+
+</wicket:extend>
+</body>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/TicketsPage.java b/src/main/java/com/gitblit/wicket/pages/TicketsPage.java
new file mode 100644
index 00000000..b68b7e42
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/TicketsPage.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.pages;
+
+import java.util.List;
+
+import org.apache.wicket.PageParameters;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.repeater.Item;
+import org.apache.wicket.markup.repeater.data.DataView;
+import org.apache.wicket.markup.repeater.data.ListDataProvider;
+
+import com.gitblit.models.TicketModel;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.utils.TicgitUtils;
+import com.gitblit.wicket.GitBlitWebSession;
+import com.gitblit.wicket.WicketUtils;
+import com.gitblit.wicket.panels.LinkPanel;
+
+public class TicketsPage extends RepositoryPage {
+
+ public TicketsPage(PageParameters params) {
+ super(params);
+
+ List<TicketModel> tickets = TicgitUtils.getTickets(getRepository());
+
+ // header
+ add(new LinkPanel("header", "title", repositoryName, SummaryPage.class,
+ newRepositoryParameter()));
+
+ ListDataProvider<TicketModel> ticketsDp = new ListDataProvider<TicketModel>(tickets);
+ DataView<TicketModel> ticketsView = new DataView<TicketModel>("ticket", ticketsDp) {
+ private static final long serialVersionUID = 1L;
+ int counter;
+
+ public void populateItem(final Item<TicketModel> item) {
+ final TicketModel entry = item.getModelObject();
+ Label stateLabel = new Label("ticketState", entry.state);
+ WicketUtils.setTicketCssClass(stateLabel, entry.state);
+ item.add(stateLabel);
+ item.add(WicketUtils.createDateLabel("ticketDate", entry.date, GitBlitWebSession
+ .get().getTimezone(), getTimeUtils()));
+ item.add(new Label("ticketHandler", StringUtils.trimString(
+ entry.handler.toLowerCase(), 30)));
+ item.add(new LinkPanel("ticketTitle", "list subject", StringUtils.trimString(
+ entry.title, 80), TicketPage.class, newPathParameter(entry.name)));
+
+ WicketUtils.setAlternatingBackground(item, counter);
+ counter++;
+ }
+ };
+ add(ticketsView);
+ }
+
+ protected PageParameters newPathParameter(String path) {
+ return WicketUtils.newPathParameter(repositoryName, objectId, path);
+ }
+
+ @Override
+ protected String getPageName() {
+ return getString("gb.tickets");
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/pages/TreePage.html b/src/main/java/com/gitblit/wicket/pages/TreePage.html
new file mode 100644
index 00000000..b7e55ed6
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/TreePage.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+
+<body>
+<wicket:extend>
+
+ <!-- blob nav links -->
+ <div class="page_nav2">
+ <a wicket:id="historyLink"><wicket:message key="gb.history"></wicket:message></a> | <a wicket:id="headLink"><wicket:message key="gb.head"></wicket:message></a> | <span wicket:id="compressedLinks"></span>
+ </div>
+
+ <!-- commit header -->
+ <div wicket:id="commitHeader">[commit header]</div>
+
+ <!-- breadcrumbs -->
+ <div wicket:id="breadcrumbs">[breadcrumbs]</div>
+
+ <!-- changed paths -->
+ <table style="width:100%" class="pretty">
+ <tr wicket:id="changedPath">
+ <td class="icon"><img wicket:id="pathIcon" /></td>
+ <td><span wicket:id="pathName"></span></td>
+ <td class="hidden-phone size"><span wicket:id="pathSize">[path size]</span></td>
+ <td class="hidden-phone mode"><span wicket:id="pathPermissions">[path permissions]</span></td>
+ <td class="treeLinks"><span wicket:id="pathLinks">[path links]</span></td>
+ </tr>
+ </table>
+
+ <!-- submodule links -->
+ <wicket:fragment wicket:id="submoduleLinks">
+ <span class="link">
+ <a wicket:id="view"><wicket:message key="gb.view"></wicket:message></a> | <span class="hidden-phone"><a wicket:id="tree"><wicket:message key="gb.tree"></wicket:message></a> | </span><a wicket:id="history"><wicket:message key="gb.history"></wicket:message></a> | <span wicket:id="compressedLinks"></span>
+ </span>
+ </wicket:fragment>
+
+ <!-- tree links -->
+ <wicket:fragment wicket:id="treeLinks">
+ <span class="link">
+ <span class="hidden-phone"><a wicket:id="tree"><wicket:message key="gb.tree"></wicket:message></a> | </span><a wicket:id="history"><wicket:message key="gb.history"></wicket:message></a> | <span wicket:id="compressedLinks"></span>
+ </span>
+ </wicket:fragment>
+
+ <!-- blob links -->
+ <wicket:fragment wicket:id="blobLinks">
+ <span class="link">
+ <span class="hidden-phone"><a wicket:id="view"><wicket:message key="gb.view"></wicket:message></a> | <a wicket:id="raw"><wicket:message key="gb.raw"></wicket:message></a> | <a wicket:id="blame"><wicket:message key="gb.blame"></wicket:message></a> | </span> <a wicket:id="history"><wicket:message key="gb.history"></wicket:message></a>
+ </span>
+ </wicket:fragment>
+
+</wicket:extend>
+</body>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/TreePage.java b/src/main/java/com/gitblit/wicket/pages/TreePage.java
new file mode 100644
index 00000000..bc27f0c2
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/TreePage.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.pages;
+
+import java.util.List;
+
+import org.apache.wicket.PageParameters;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.link.BookmarkablePageLink;
+import org.apache.wicket.markup.html.panel.Fragment;
+import org.apache.wicket.markup.repeater.Item;
+import org.apache.wicket.markup.repeater.data.DataView;
+import org.apache.wicket.markup.repeater.data.ListDataProvider;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+
+import com.gitblit.models.PathModel;
+import com.gitblit.models.SubmoduleModel;
+import com.gitblit.utils.ByteFormat;
+import com.gitblit.utils.JGitUtils;
+import com.gitblit.wicket.WicketUtils;
+import com.gitblit.wicket.panels.CommitHeaderPanel;
+import com.gitblit.wicket.panels.CompressedDownloadsPanel;
+import com.gitblit.wicket.panels.LinkPanel;
+import com.gitblit.wicket.panels.PathBreadcrumbsPanel;
+
+public class TreePage extends RepositoryPage {
+
+ public TreePage(PageParameters params) {
+ super(params);
+
+ final String path = WicketUtils.getPath(params);
+
+ Repository r = getRepository();
+ RevCommit commit = getCommit();
+ List<PathModel> paths = JGitUtils.getFilesInPath(r, path, commit);
+
+ // tree page links
+ add(new BookmarkablePageLink<Void>("historyLink", HistoryPage.class,
+ WicketUtils.newPathParameter(repositoryName, objectId, path)));
+ add(new BookmarkablePageLink<Void>("headLink", TreePage.class,
+ WicketUtils.newPathParameter(repositoryName, Constants.HEAD, path)));
+ add(new CompressedDownloadsPanel("compressedLinks", getRequest()
+ .getRelativePathPrefixToContextRoot(), repositoryName, objectId, path));
+
+ add(new CommitHeaderPanel("commitHeader", repositoryName, commit));
+
+ // breadcrumbs
+ add(new PathBreadcrumbsPanel("breadcrumbs", repositoryName, path, objectId));
+ if (path != null && path.trim().length() > 0) {
+ // add .. parent path entry
+ String parentPath = null;
+ if (path.lastIndexOf('/') > -1) {
+ parentPath = path.substring(0, path.lastIndexOf('/'));
+ }
+ PathModel model = new PathModel("..", parentPath, 0, FileMode.TREE.getBits(), null, objectId);
+ model.isParentPath = true;
+ paths.add(0, model);
+ }
+
+ final ByteFormat byteFormat = new ByteFormat();
+
+ final String baseUrl = WicketUtils.getGitblitURL(getRequest());
+
+ // changed paths list
+ ListDataProvider<PathModel> pathsDp = new ListDataProvider<PathModel>(paths);
+ DataView<PathModel> pathsView = new DataView<PathModel>("changedPath", pathsDp) {
+ private static final long serialVersionUID = 1L;
+ int counter;
+
+ public void populateItem(final Item<PathModel> item) {
+ PathModel entry = item.getModelObject();
+ item.add(new Label("pathPermissions", JGitUtils.getPermissionsFromMode(entry.mode)));
+ if (entry.isParentPath) {
+ // parent .. path
+ item.add(WicketUtils.newBlankImage("pathIcon"));
+ item.add(new Label("pathSize", ""));
+ item.add(new LinkPanel("pathName", null, entry.name, TreePage.class,
+ WicketUtils
+ .newPathParameter(repositoryName, entry.commitId, entry.path)));
+ item.add(new Label("pathLinks", ""));
+ } else {
+ if (entry.isTree()) {
+ // folder/tree link
+ item.add(WicketUtils.newImage("pathIcon", "folder_16x16.png"));
+ item.add(new Label("pathSize", ""));
+ item.add(new LinkPanel("pathName", "list", entry.name, TreePage.class,
+ WicketUtils.newPathParameter(repositoryName, entry.commitId,
+ entry.path)));
+
+ // links
+ Fragment links = new Fragment("pathLinks", "treeLinks", this);
+ links.add(new BookmarkablePageLink<Void>("tree", TreePage.class,
+ WicketUtils.newPathParameter(repositoryName, entry.commitId,
+ entry.path)));
+ links.add(new BookmarkablePageLink<Void>("history", HistoryPage.class,
+ WicketUtils.newPathParameter(repositoryName, entry.commitId,
+ entry.path)));
+ links.add(new CompressedDownloadsPanel("compressedLinks", baseUrl,
+ repositoryName, objectId, entry.path));
+
+ item.add(links);
+ } else if (entry.isSubmodule()) {
+ // submodule
+ String submoduleId = entry.objectId;
+ String submodulePath;
+ boolean hasSubmodule = false;
+ SubmoduleModel submodule = getSubmodule(entry.path);
+ submodulePath = submodule.gitblitPath;
+ hasSubmodule = submodule.hasSubmodule;
+
+ item.add(WicketUtils.newImage("pathIcon", "git-orange-16x16.png"));
+ item.add(new Label("pathSize", ""));
+ item.add(new LinkPanel("pathName", "list", entry.name + " @ " +
+ getShortObjectId(submoduleId), TreePage.class,
+ WicketUtils.newPathParameter(submodulePath, submoduleId, "")).setEnabled(hasSubmodule));
+
+ Fragment links = new Fragment("pathLinks", "submoduleLinks", this);
+ links.add(new BookmarkablePageLink<Void>("view", SummaryPage.class,
+ WicketUtils.newRepositoryParameter(submodulePath)).setEnabled(hasSubmodule));
+ links.add(new BookmarkablePageLink<Void>("tree", TreePage.class,
+ WicketUtils.newPathParameter(submodulePath, submoduleId,
+ "")).setEnabled(hasSubmodule));
+ links.add(new BookmarkablePageLink<Void>("history", HistoryPage.class,
+ WicketUtils.newPathParameter(repositoryName, entry.commitId,
+ entry.path)));
+ links.add(new CompressedDownloadsPanel("compressedLinks", baseUrl,
+ submodulePath, submoduleId, "").setEnabled(hasSubmodule));
+ item.add(links);
+ } else {
+ // blob link
+ String displayPath = entry.name;
+ String path = entry.path;
+ if (entry.isSymlink()) {
+ path = JGitUtils.getStringContent(getRepository(), getCommit().getTree(), path);
+ displayPath = entry.name + " -> " + path;
+ }
+ item.add(WicketUtils.getFileImage("pathIcon", entry.name));
+ item.add(new Label("pathSize", byteFormat.format(entry.size)));
+ item.add(new LinkPanel("pathName", "list", displayPath, BlobPage.class,
+ WicketUtils.newPathParameter(repositoryName, entry.commitId,
+ path)));
+
+ // links
+ Fragment links = new Fragment("pathLinks", "blobLinks", this);
+ links.add(new BookmarkablePageLink<Void>("view", BlobPage.class,
+ WicketUtils.newPathParameter(repositoryName, entry.commitId,
+ path)));
+ links.add(new BookmarkablePageLink<Void>("raw", RawPage.class, WicketUtils
+ .newPathParameter(repositoryName, entry.commitId, path)));
+ links.add(new BookmarkablePageLink<Void>("blame", BlamePage.class,
+ WicketUtils.newPathParameter(repositoryName, entry.commitId,
+ path)));
+ links.add(new BookmarkablePageLink<Void>("history", HistoryPage.class,
+ WicketUtils.newPathParameter(repositoryName, entry.commitId,
+ path)));
+ item.add(links);
+ }
+ }
+ WicketUtils.setAlternatingBackground(item, counter);
+ counter++;
+ }
+ };
+ add(pathsView);
+ }
+
+ @Override
+ protected String getPageName() {
+ return getString("gb.tree");
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/pages/UserPage.html b/src/main/java/com/gitblit/wicket/pages/UserPage.html
new file mode 100644
index 00000000..c7131c09
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/UserPage.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+
+<body>
+<wicket:extend>
+
+ <div class="row">
+ <div class="span4">
+ <div wicket:id="gravatar"></div>
+ <div style="text-align: left;">
+ <h2><span wicket:id="userDisplayName"></span></h2>
+ <div><i class="icon-user"></i> <span wicket:id="userUsername"></span></div>
+ <div><i class="icon-envelope"></i><span wicket:id="userEmail"></span></div>
+ </div>
+ </div>
+
+ <div class="span8">
+ <div class="pull-right">
+ <a class="btn-small" wicket:id="newRepository" style="padding-right:0px;">
+ <i class="icon icon-plus-sign"></i>
+ <wicket:message key="gb.newRepository"></wicket:message>
+ </a>
+ </div>
+ <div class="tabbable">
+ <!-- tab titles -->
+ <ul class="nav nav-tabs">
+ <li class="active"><a href="#repositories" data-toggle="tab"><wicket:message key="gb.repositories"></wicket:message></a></li>
+ </ul>
+
+ <!-- tab content -->
+ <div class="tab-content">
+
+ <!-- repositories tab -->
+ <div class="tab-pane active" id="repositories">
+ <table width="100%">
+ <tbody>
+ <tr wicket:id="repositoryList"><td style="border-bottom:1px solid #eee;"><span wicket:id="repository"></span></td></tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+</wicket:extend>
+</body>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/UserPage.java b/src/main/java/com/gitblit/wicket/pages/UserPage.java
new file mode 100644
index 00000000..f4331dd1
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/UserPage.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2012 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.pages;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+import org.apache.wicket.PageParameters;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.link.BookmarkablePageLink;
+import org.apache.wicket.markup.repeater.Item;
+import org.apache.wicket.markup.repeater.data.DataView;
+import org.apache.wicket.markup.repeater.data.ListDataProvider;
+import org.eclipse.jgit.lib.PersonIdent;
+
+import com.gitblit.GitBlit;
+import com.gitblit.Keys;
+import com.gitblit.models.ProjectModel;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.UserModel;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.wicket.GitBlitWebApp;
+import com.gitblit.wicket.GitBlitWebSession;
+import com.gitblit.wicket.GitblitRedirectException;
+import com.gitblit.wicket.PageRegistration;
+import com.gitblit.wicket.PageRegistration.DropDownMenuItem;
+import com.gitblit.wicket.PageRegistration.DropDownMenuRegistration;
+import com.gitblit.wicket.WicketUtils;
+import com.gitblit.wicket.panels.GravatarImage;
+import com.gitblit.wicket.panels.LinkPanel;
+import com.gitblit.wicket.panels.ProjectRepositoryPanel;
+
+public class UserPage extends RootPage {
+
+ List<ProjectModel> projectModels = new ArrayList<ProjectModel>();
+
+ public UserPage() {
+ super();
+ throw new GitblitRedirectException(GitBlitWebApp.get().getHomePage());
+ }
+
+ public UserPage(PageParameters params) {
+ super(params);
+ setup(params);
+ }
+
+ @Override
+ protected boolean reusePageParameters() {
+ return true;
+ }
+
+ private void setup(PageParameters params) {
+ setupPage("", "");
+ // check to see if we should display a login message
+ boolean authenticateView = GitBlit.getBoolean(Keys.web.authenticateViewPages, true);
+ if (authenticateView && !GitBlitWebSession.get().isLoggedIn()) {
+ authenticationError("Please login");
+ return;
+ }
+
+ String userName = WicketUtils.getUsername(params);
+ if (StringUtils.isEmpty(userName)) {
+ throw new GitblitRedirectException(GitBlitWebApp.get().getHomePage());
+ }
+
+ UserModel user = GitBlit.self().getUserModel(userName);
+ if (user == null) {
+ // construct a temporary user model
+ user = new UserModel(userName);
+ }
+
+ String projectName = "~" + userName;
+
+ ProjectModel project = GitBlit.self().getProjectModel(projectName);
+ if (project == null) {
+ project = new ProjectModel(projectName);
+ }
+
+ add(new Label("userDisplayName", user.getDisplayName()));
+ add(new Label("userUsername", user.username));
+ LinkPanel email = new LinkPanel("userEmail", null, user.emailAddress, "mailto:#");
+ email.setRenderBodyOnly(true);
+ add(email.setVisible(GitBlit.getBoolean(Keys.web.showEmailAddresses, true) && !StringUtils.isEmpty(user.emailAddress)));
+
+ PersonIdent person = new PersonIdent(user.getDisplayName(), user.emailAddress == null ? user.getDisplayName() : user.emailAddress);
+ add(new GravatarImage("gravatar", person, 210));
+
+ UserModel sessionUser = GitBlitWebSession.get().getUser();
+ if (sessionUser != null && user.canCreate() && sessionUser.equals(user)) {
+ // user can create personal repositories
+ add(new BookmarkablePageLink<Void>("newRepository", EditRepositoryPage.class));
+ } else {
+ add(new Label("newRepository").setVisible(false));
+ }
+
+ List<RepositoryModel> repositories = getRepositories(params);
+
+ Collections.sort(repositories, new Comparator<RepositoryModel>() {
+ @Override
+ public int compare(RepositoryModel o1, RepositoryModel o2) {
+ // reverse-chronological sort
+ return o2.lastChange.compareTo(o1.lastChange);
+ }
+ });
+
+ final ListDataProvider<RepositoryModel> dp = new ListDataProvider<RepositoryModel>(repositories);
+ DataView<RepositoryModel> dataView = new DataView<RepositoryModel>("repositoryList", dp) {
+ private static final long serialVersionUID = 1L;
+
+ public void populateItem(final Item<RepositoryModel> item) {
+ final RepositoryModel entry = item.getModelObject();
+
+ ProjectRepositoryPanel row = new ProjectRepositoryPanel("repository",
+ getLocalizer(), this, showAdmin, entry, getAccessRestrictions());
+ item.add(row);
+ }
+ };
+ add(dataView);
+ }
+
+ @Override
+ protected void addDropDownMenus(List<PageRegistration> pages) {
+ PageParameters params = getPageParameters();
+
+ DropDownMenuRegistration menu = new DropDownMenuRegistration("gb.filters",
+ UserPage.class);
+ // preserve time filter option on repository choices
+ menu.menuItems.addAll(getRepositoryFilterItems(params));
+
+ // preserve repository filter option on time choices
+ menu.menuItems.addAll(getTimeFilterItems(params));
+
+ if (menu.menuItems.size() > 0) {
+ // Reset Filter
+ menu.menuItems.add(new DropDownMenuItem(getString("gb.reset"), null, null));
+ }
+
+ pages.add(menu);
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/pages/UsersPage.html b/src/main/java/com/gitblit/wicket/pages/UsersPage.html
new file mode 100644
index 00000000..edb85f7d
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/UsersPage.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+<body>
+<wicket:extend>
+ <div wicket:id="teamsPanel">[teams panel]</div>
+
+ <div wicket:id="usersPanel">[users panel]</div>
+</wicket:extend>
+</body>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/UsersPage.java b/src/main/java/com/gitblit/wicket/pages/UsersPage.java
new file mode 100644
index 00000000..9526deae
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/UsersPage.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.pages;
+
+import com.gitblit.wicket.RequiresAdminRole;
+import com.gitblit.wicket.panels.TeamsPanel;
+import com.gitblit.wicket.panels.UsersPanel;
+
+@RequiresAdminRole
+public class UsersPage extends RootPage {
+
+ public UsersPage() {
+ super();
+ setupPage("", "");
+
+ add(new TeamsPanel("teamsPanel", showAdmin).setVisible(showAdmin));
+
+ add(new UsersPanel("usersPanel", showAdmin).setVisible(showAdmin));
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/pages/prettify/lang-apollo.js b/src/main/java/com/gitblit/wicket/pages/prettify/lang-apollo.js
new file mode 100644
index 00000000..bfc0014c
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/prettify/lang-apollo.js
@@ -0,0 +1,2 @@
+PR.registerLangHandler(PR.createSimpleLexer([["com",/^#[^\r\n]*/,null,"#"],["pln",/^[\t\n\r \xA0]+/,null,"\t\n\r \u00a0"],["str",/^\"(?:[^\"\\]|\\[\s\S])*(?:\"|$)/,null,'"']],[["kwd",/^(?:ADS|AD|AUG|BZF|BZMF|CAE|CAF|CA|CCS|COM|CS|DAS|DCA|DCOM|DCS|DDOUBL|DIM|DOUBLE|DTCB|DTCF|DV|DXCH|EDRUPT|EXTEND|INCR|INDEX|NDX|INHINT|LXCH|MASK|MSK|MP|MSU|NOOP|OVSK|QXCH|RAND|READ|RELINT|RESUME|RETURN|ROR|RXOR|SQUARE|SU|TCR|TCAA|OVSK|TCF|TC|TS|WAND|WOR|WRITE|XCH|XLQ|XXALQ|ZL|ZQ|ADD|ADZ|SUB|SUZ|MPY|MPR|MPZ|DVP|COM|ABS|CLA|CLZ|LDQ|STO|STQ|ALS|LLS|LRS|TRA|TSQ|TMI|TOV|AXT|TIX|DLY|INP|OUT)\s/,
+null],["typ",/^(?:-?GENADR|=MINUS|2BCADR|VN|BOF|MM|-?2CADR|-?[1-6]DNADR|ADRES|BBCON|[SE]?BANK\=?|BLOCK|BNKSUM|E?CADR|COUNT\*?|2?DEC\*?|-?DNCHAN|-?DNPTR|EQUALS|ERASE|MEMORY|2?OCT|REMADR|SETLOC|SUBRO|ORG|BSS|BES|SYN|EQU|DEFINE|END)\s/,null],["lit",/^\'(?:-*(?:\w|\\[\x21-\x7e])(?:[\w-]*|\\[\x21-\x7e])[=!?]?)?/],["pln",/^-*(?:[!-z_]|\\[\x21-\x7e])(?:[\w-]*|\\[\x21-\x7e])[=!?]?/i],["pun",/^[^\w\t\n\r \xA0()\"\\\';]+/]]),["apollo","agc","aea"]) \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/prettify/lang-css.js b/src/main/java/com/gitblit/wicket/pages/prettify/lang-css.js
new file mode 100644
index 00000000..61157f38
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/prettify/lang-css.js
@@ -0,0 +1,2 @@
+PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[ \t\r\n\f]+/,null," \t\r\n\u000c"]],[["str",/^\"(?:[^\n\r\f\\\"]|\\(?:\r\n?|\n|\f)|\\[\s\S])*\"/,null],["str",/^\'(?:[^\n\r\f\\\']|\\(?:\r\n?|\n|\f)|\\[\s\S])*\'/,null],["lang-css-str",/^url\(([^\)\"\']*)\)/i],["kwd",/^(?:url|rgb|\!important|@import|@page|@media|@charset|inherit)(?=[^\-\w]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|(?:\\[0-9a-f]+ ?))(?:[_a-z0-9\-]|\\(?:\\[0-9a-f]+ ?))*)\s*:/i],["com",/^\/\*[^*]*\*+(?:[^\/*][^*]*\*+)*\//],
+["com",/^(?:<!--|--\>)/],["lit",/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],["lit",/^#(?:[0-9a-f]{3}){1,2}/i],["pln",/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i],["pun",/^[^\s\w\'\"]+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[["kwd",/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[["str",/^[^\)\"\']+/]]),["css-str"]) \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/prettify/lang-hs.js b/src/main/java/com/gitblit/wicket/pages/prettify/lang-hs.js
new file mode 100644
index 00000000..00cea7cf
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/prettify/lang-hs.js
@@ -0,0 +1,2 @@
+PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\x0B\x0C\r ]+/,null,"\t\n\u000b\u000c\r "],["str",/^\"(?:[^\"\\\n\x0C\r]|\\[\s\S])*(?:\"|$)/,null,'"'],["str",/^\'(?:[^\'\\\n\x0C\r]|\\[^&])\'?/,null,"'"],["lit",/^(?:0o[0-7]+|0x[\da-f]+|\d+(?:\.\d+)?(?:e[+\-]?\d+)?)/i,null,"0123456789"]],[["com",/^(?:(?:--+(?:[^\r\n\x0C]*)?)|(?:\{-(?:[^-]|-+[^-\}])*-\}))/],["kwd",/^(?:case|class|data|default|deriving|do|else|if|import|in|infix|infixl|infixr|instance|let|module|newtype|of|then|type|where|_)(?=[^a-zA-Z0-9\']|$)/,
+null],["pln",/^(?:[A-Z][\w\']*\.)*[a-zA-Z][\w\']*/],["pun",/^[^\t\n\x0B\x0C\r a-zA-Z0-9\'\"]+/]]),["hs"]) \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/prettify/lang-lisp.js b/src/main/java/com/gitblit/wicket/pages/prettify/lang-lisp.js
new file mode 100644
index 00000000..fab992b8
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/prettify/lang-lisp.js
@@ -0,0 +1,2 @@
+PR.registerLangHandler(PR.createSimpleLexer([["opn",/^\(/,null,"("],["clo",/^\)/,null,")"],["com",/^;[^\r\n]*/,null,";"],["pln",/^[\t\n\r \xA0]+/,null,"\t\n\r \u00a0"],["str",/^\"(?:[^\"\\]|\\[\s\S])*(?:\"|$)/,null,'"']],[["kwd",/^(?:block|c[ad]+r|catch|con[ds]|def(?:ine|un)|do|eq|eql|equal|equalp|eval-when|flet|format|go|if|labels|lambda|let|load-time-value|locally|macrolet|multiple-value-call|nil|progn|progv|quote|require|return-from|setq|symbol-macrolet|t|tagbody|the|throw|unwind)\b/,
+null],["lit",/^[+\-]?(?:0x[0-9a-f]+|\d+\/\d+|(?:\.\d+|\d+(?:\.\d*)?)(?:[ed][+\-]?\d+)?)/i],["lit",/^\'(?:-*(?:\w|\\[\x21-\x7e])(?:[\w-]*|\\[\x21-\x7e])[=!?]?)?/],["pln",/^-*(?:[a-z_]|\\[\x21-\x7e])(?:[\w-]*|\\[\x21-\x7e])[=!?]?/i],["pun",/^[^\w\t\n\r \xA0()\"\\\';]+/]]),["cl","el","lisp","scm"]) \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/prettify/lang-lua.js b/src/main/java/com/gitblit/wicket/pages/prettify/lang-lua.js
new file mode 100644
index 00000000..45d0ba28
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/prettify/lang-lua.js
@@ -0,0 +1,2 @@
+PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\r \xA0]+/,null,"\t\n\r \u00a0"],["str",/^(?:\"(?:[^\"\\]|\\[\s\S])*(?:\"|$)|\'(?:[^\'\\]|\\[\s\S])*(?:\'|$))/,null,"\"'"]],[["com",/^--(?:\[(=*)\[[\s\S]*?(?:\]\1\]|$)|[^\r\n]*)/],["str",/^\[(=*)\[[\s\S]*?(?:\]\1\]|$)/],["kwd",/^(?:and|break|do|else|elseif|end|false|for|function|if|in|local|nil|not|or|repeat|return|then|true|until|while)\b/,null],["lit",/^[+-]?(?:0x[\da-f]+|(?:(?:\.\d+|\d+(?:\.\d*)?)(?:e[+\-]?\d+)?))/i],
+["pln",/^[a-z_]\w*/i],["pun",/^[^\w\t\n\r \xA0][^\w\t\n\r \xA0\"\'\-\+=]*/]]),["lua"]) \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/prettify/lang-ml.js b/src/main/java/com/gitblit/wicket/pages/prettify/lang-ml.js
new file mode 100644
index 00000000..5879726e
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/prettify/lang-ml.js
@@ -0,0 +1,2 @@
+PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\r \xA0]+/,null,"\t\n\r \u00a0"],["com",/^#(?:if[\t\n\r \xA0]+(?:[a-z_$][\w\']*|``[^\r\n\t`]*(?:``|$))|else|endif|light)/i,null,"#"],["str",/^(?:\"(?:[^\"\\]|\\[\s\S])*(?:\"|$)|\'(?:[^\'\\]|\\[\s\S])*(?:\'|$))/,null,"\"'"]],[["com",/^(?:\/\/[^\r\n]*|\(\*[\s\S]*?\*\))/],["kwd",/^(?:abstract|and|as|assert|begin|class|default|delegate|do|done|downcast|downto|elif|else|end|exception|extern|false|finally|for|fun|function|if|in|inherit|inline|interface|internal|lazy|let|match|member|module|mutable|namespace|new|null|of|open|or|override|private|public|rec|return|static|struct|then|to|true|try|type|upcast|use|val|void|when|while|with|yield|asr|land|lor|lsl|lsr|lxor|mod|sig|atomic|break|checked|component|const|constraint|constructor|continue|eager|event|external|fixed|functor|global|include|method|mixin|object|parallel|process|protected|pure|sealed|trait|virtual|volatile)\b/],
+["lit",/^[+\-]?(?:0x[\da-f]+|(?:(?:\.\d+|\d+(?:\.\d*)?)(?:e[+\-]?\d+)?))/i],["pln",/^(?:[a-z_]\w*[!?#]?|``[^\r\n\t`]*(?:``|$))/i],["pun",/^[^\t\n\r \xA0\"\'\w]+/]]),["fs","ml"]) \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/prettify/lang-proto.js b/src/main/java/com/gitblit/wicket/pages/prettify/lang-proto.js
new file mode 100644
index 00000000..f713420c
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/prettify/lang-proto.js
@@ -0,0 +1 @@
+PR.registerLangHandler(PR.sourceDecorator({keywords:"bool bytes default double enum extend extensions false fixed32 fixed64 float group import int32 int64 max message option optional package repeated required returns rpc service sfixed32 sfixed64 sint32 sint64 string syntax to true uint32 uint64",cStyleComments:true}),["proto"]) \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/prettify/lang-scala.js b/src/main/java/com/gitblit/wicket/pages/prettify/lang-scala.js
new file mode 100644
index 00000000..00f4e0c2
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/prettify/lang-scala.js
@@ -0,0 +1,2 @@
+PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\r \xA0]+/,null,"\t\n\r \u00a0"],["str",/^(?:"(?:(?:""(?:""?(?!")|[^\\"]|\\.)*"{0,3})|(?:[^"\r\n\\]|\\.)*"?))/,null,'"'],["lit",/^`(?:[^\r\n\\`]|\\.)*`?/,null,"`"],["pun",/^[!#%&()*+,\-:;<=>?@\[\\\]^{|}~]+/,null,"!#%&()*+,-:;<=>?@[\\]^{|}~"]],[["str",/^'(?:[^\r\n\\']|\\(?:'|[^\r\n']+))'/],["lit",/^'[a-zA-Z_$][\w$]*(?!['$\w])/],["kwd",/^(?:abstract|case|catch|class|def|do|else|extends|final|finally|for|forSome|if|implicit|import|lazy|match|new|object|override|package|private|protected|requires|return|sealed|super|throw|trait|try|type|val|var|while|with|yield)\b/],
+["lit",/^(?:true|false|null|this)\b/],["lit",/^(?:(?:0(?:[0-7]+|X[0-9A-F]+))L?|(?:(?:0|[1-9][0-9]*)(?:(?:\.[0-9]+)?(?:E[+\-]?[0-9]+)?F?|L?))|\\.[0-9]+(?:E[+\-]?[0-9]+)?F?)/i],["typ",/^[$_]*[A-Z][_$A-Z0-9]*[a-z][\w$]*/],["pln",/^[$a-zA-Z_][\w$]*/],["com",/^\/(?:\/.*|\*(?:\/|\**[^*/])*(?:\*+\/?)?)/],["pun",/^(?:\.+|\/)/]]),["scala"]) \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/prettify/lang-sql.js b/src/main/java/com/gitblit/wicket/pages/prettify/lang-sql.js
new file mode 100644
index 00000000..800b13ea
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/prettify/lang-sql.js
@@ -0,0 +1,2 @@
+PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\r \xA0]+/,null,"\t\n\r \u00a0"],["str",/^(?:"(?:[^\"\\]|\\.)*"|'(?:[^\'\\]|\\.)*')/,null,"\"'"]],[["com",/^(?:--[^\r\n]*|\/\*[\s\S]*?(?:\*\/|$))/],["kwd",/^(?:ADD|ALL|ALTER|AND|ANY|AS|ASC|AUTHORIZATION|BACKUP|BEGIN|BETWEEN|BREAK|BROWSE|BULK|BY|CASCADE|CASE|CHECK|CHECKPOINT|CLOSE|CLUSTERED|COALESCE|COLLATE|COLUMN|COMMIT|COMPUTE|CONSTRAINT|CONTAINS|CONTAINSTABLE|CONTINUE|CONVERT|CREATE|CROSS|CURRENT|CURRENT_DATE|CURRENT_TIME|CURRENT_TIMESTAMP|CURRENT_USER|CURSOR|DATABASE|DBCC|DEALLOCATE|DECLARE|DEFAULT|DELETE|DENY|DESC|DISK|DISTINCT|DISTRIBUTED|DOUBLE|DROP|DUMMY|DUMP|ELSE|END|ERRLVL|ESCAPE|EXCEPT|EXEC|EXECUTE|EXISTS|EXIT|FETCH|FILE|FILLFACTOR|FOR|FOREIGN|FREETEXT|FREETEXTTABLE|FROM|FULL|FUNCTION|GOTO|GRANT|GROUP|HAVING|HOLDLOCK|IDENTITY|IDENTITYCOL|IDENTITY_INSERT|IF|IN|INDEX|INNER|INSERT|INTERSECT|INTO|IS|JOIN|KEY|KILL|LEFT|LIKE|LINENO|LOAD|NATIONAL|NOCHECK|NONCLUSTERED|NOT|NULL|NULLIF|OF|OFF|OFFSETS|ON|OPEN|OPENDATASOURCE|OPENQUERY|OPENROWSET|OPENXML|OPTION|OR|ORDER|OUTER|OVER|PERCENT|PLAN|PRECISION|PRIMARY|PRINT|PROC|PROCEDURE|PUBLIC|RAISERROR|READ|READTEXT|RECONFIGURE|REFERENCES|REPLICATION|RESTORE|RESTRICT|RETURN|REVOKE|RIGHT|ROLLBACK|ROWCOUNT|ROWGUIDCOL|RULE|SAVE|SCHEMA|SELECT|SESSION_USER|SET|SETUSER|SHUTDOWN|SOME|STATISTICS|SYSTEM_USER|TABLE|TEXTSIZE|THEN|TO|TOP|TRAN|TRANSACTION|TRIGGER|TRUNCATE|TSEQUAL|UNION|UNIQUE|UPDATE|UPDATETEXT|USE|USER|VALUES|VARYING|VIEW|WAITFOR|WHEN|WHERE|WHILE|WITH|WRITETEXT)(?=[^\w-]|$)/i,
+null],["lit",/^[+-]?(?:0x[\da-f]+|(?:(?:\.\d+|\d+(?:\.\d*)?)(?:e[+\-]?\d+)?))/i],["pln",/^[a-z_][\w-]*/i],["pun",/^[^\w\t\n\r \xA0\"\'][^\w\t\n\r \xA0+\-\"\']*/]]),["sql"]) \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/prettify/lang-vb.js b/src/main/java/com/gitblit/wicket/pages/prettify/lang-vb.js
new file mode 100644
index 00000000..c479c11e
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/prettify/lang-vb.js
@@ -0,0 +1,2 @@
+PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\r \xA0\u2028\u2029]+/,null,"\t\n\r \u00a0\u2028\u2029"],["str",/^(?:[\"\u201C\u201D](?:[^\"\u201C\u201D]|[\"\u201C\u201D]{2})(?:[\"\u201C\u201D]c|$)|[\"\u201C\u201D](?:[^\"\u201C\u201D]|[\"\u201C\u201D]{2})*(?:[\"\u201C\u201D]|$))/i,null,'"\u201c\u201d'],["com",/^[\'\u2018\u2019][^\r\n\u2028\u2029]*/,null,"'\u2018\u2019"]],[["kwd",/^(?:AddHandler|AddressOf|Alias|And|AndAlso|Ansi|As|Assembly|Auto|Boolean|ByRef|Byte|ByVal|Call|Case|Catch|CBool|CByte|CChar|CDate|CDbl|CDec|Char|CInt|Class|CLng|CObj|Const|CShort|CSng|CStr|CType|Date|Decimal|Declare|Default|Delegate|Dim|DirectCast|Do|Double|Each|Else|ElseIf|End|EndIf|Enum|Erase|Error|Event|Exit|Finally|For|Friend|Function|Get|GetType|GoSub|GoTo|Handles|If|Implements|Imports|In|Inherits|Integer|Interface|Is|Let|Lib|Like|Long|Loop|Me|Mod|Module|MustInherit|MustOverride|MyBase|MyClass|Namespace|New|Next|Not|NotInheritable|NotOverridable|Object|On|Option|Optional|Or|OrElse|Overloads|Overridable|Overrides|ParamArray|Preserve|Private|Property|Protected|Public|RaiseEvent|ReadOnly|ReDim|RemoveHandler|Resume|Return|Select|Set|Shadows|Shared|Short|Single|Static|Step|Stop|String|Structure|Sub|SyncLock|Then|Throw|To|Try|TypeOf|Unicode|Until|Variant|Wend|When|While|With|WithEvents|WriteOnly|Xor|EndIf|GoSub|Let|Variant|Wend)\b/i,
+null],["com",/^REM[^\r\n\u2028\u2029]*/i],["lit",/^(?:True\b|False\b|Nothing\b|\d+(?:E[+\-]?\d+[FRD]?|[FRDSIL])?|(?:&H[0-9A-F]+|&O[0-7]+)[SIL]?|\d*\.\d+(?:E[+\-]?\d+)?[FRD]?|#\s+(?:\d+[\-\/]\d+[\-\/]\d+(?:\s+\d+:\d+(?::\d+)?(\s*(?:AM|PM))?)?|\d+:\d+(?::\d+)?(\s*(?:AM|PM))?)\s+#)/i],["pln",/^(?:(?:[a-z]|_\w)\w*|\[(?:[a-z]|_\w)\w*\])/i],["pun",/^[^\w\t\n\r \"\'\[\]\xA0\u2018\u2019\u201C\u201D\u2028\u2029]+/],["pun",/^(?:\[|\])/]]),["vb","vbs"]) \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/prettify/lang-vhdl.js b/src/main/java/com/gitblit/wicket/pages/prettify/lang-vhdl.js
new file mode 100644
index 00000000..dc81a3fe
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/prettify/lang-vhdl.js
@@ -0,0 +1,3 @@
+PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\r \xA0]+/,null,"\t\n\r \u00a0"]],[["str",/^(?:[BOX]?"(?:[^\"]|"")*"|'.')/i],["com",/^--[^\r\n]*/],["kwd",/^(?:abs|access|after|alias|all|and|architecture|array|assert|attribute|begin|block|body|buffer|bus|case|component|configuration|constant|disconnect|downto|else|elsif|end|entity|exit|file|for|function|generate|generic|group|guarded|if|impure|in|inertial|inout|is|label|library|linkage|literal|loop|map|mod|nand|new|next|nor|not|null|of|on|open|or|others|out|package|port|postponed|procedure|process|pure|range|record|register|reject|rem|report|return|rol|ror|select|severity|shared|signal|sla|sll|sra|srl|subtype|then|to|transport|type|unaffected|units|until|use|variable|wait|when|while|with|xnor|xor)(?=[^\w-]|$)/i,
+null],["typ",/^(?:bit|bit_vector|character|boolean|integer|real|time|string|severity_level|positive|natural|signed|unsigned|line|text|std_u?logic(?:_vector)?)(?=[^\w-]|$)/i,null],["typ",/^\'(?:ACTIVE|ASCENDING|BASE|DELAYED|DRIVING|DRIVING_VALUE|EVENT|HIGH|IMAGE|INSTANCE_NAME|LAST_ACTIVE|LAST_EVENT|LAST_VALUE|LEFT|LEFTOF|LENGTH|LOW|PATH_NAME|POS|PRED|QUIET|RANGE|REVERSE_RANGE|RIGHT|RIGHTOF|SIMPLE_NAME|STABLE|SUCC|TRANSACTION|VAL|VALUE)(?=[^\w-]|$)/i,null],["lit",/^\d+(?:_\d+)*(?:#[\w\\.]+#(?:[+\-]?\d+(?:_\d+)*)?|(?:\.\d+(?:_\d+)*)?(?:E[+\-]?\d+(?:_\d+)*)?)/i],
+["pln",/^(?:[a-z]\w*|\\[^\\]*\\)/i],["pun",/^[^\w\t\n\r \xA0\"\'][^\w\t\n\r \xA0\-\"\']*/]]),["vhdl","vhd"]) \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/prettify/lang-wiki.js b/src/main/java/com/gitblit/wicket/pages/prettify/lang-wiki.js
new file mode 100644
index 00000000..3b8fb500
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/prettify/lang-wiki.js
@@ -0,0 +1,2 @@
+PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t \xA0a-gi-z0-9]+/,null,"\t \u00a0abcdefgijklmnopqrstuvwxyz0123456789"],["pun",/^[=*~\^\[\]]+/,null,"=*~^[]"]],[["lang-wiki.meta",/(?:^^|\r\n?|\n)(#[a-z]+)\b/],["lit",/^(?:[A-Z][a-z][a-z0-9]+[A-Z][a-z][a-zA-Z0-9]+)\b/],["lang-",/^\{\{\{([\s\S]+?)\}\}\}/],["lang-",/^`([^\r\n`]+)`/],["str",/^https?:\/\/[^\/?#\s]*(?:\/[^?#\s]*)?(?:\?[^#\s]*)?(?:#\S*)?/i],["pln",/^(?:\r\n|[\s\S])[^#=*~^A-Zh\{`\[\r\n]*/]]),["wiki"]);
+PR.registerLangHandler(PR.createSimpleLexer([["kwd",/^#[a-z]+/i,null,"#"]],[]),["wiki.meta"]) \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/prettify/lang-yaml.js b/src/main/java/com/gitblit/wicket/pages/prettify/lang-yaml.js
new file mode 100644
index 00000000..f2f36070
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/prettify/lang-yaml.js
@@ -0,0 +1,2 @@
+PR.registerLangHandler(PR.createSimpleLexer([["pun",/^[:|>?]+/,null,":|>?"],["dec",/^%(?:YAML|TAG)[^#\r\n]+/,null,"%"],["typ",/^[&]\S+/,null,"&"],["typ",/^!\S*/,null,"!"],["str",/^"(?:[^\\"]|\\.)*(?:"|$)/,null,'"'],["str",/^'(?:[^']|'')*(?:'|$)/,null,"'"],["com",/^#[^\r\n]*/,null,"#"],["pln",/^\s+/,null," \t\r\n"]],[["dec",/^(?:---|\.\.\.)(?:[\r\n]|$)/],["pun",/^-/],["kwd",/^\w+:[ \r\n]/],["pln",/^\w+/]]),
+["yaml","yml"]) \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/prettify/prettify.css b/src/main/java/com/gitblit/wicket/pages/prettify/prettify.css
new file mode 100644
index 00000000..2925d13a
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/prettify/prettify.css
@@ -0,0 +1 @@
+.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun{color:#660}.pln{color:#000}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec{color:#606}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee}@media print{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun{color:#440}.pln{color:#000}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}} \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/prettify/prettify.js b/src/main/java/com/gitblit/wicket/pages/prettify/prettify.js
new file mode 100644
index 00000000..c9161da9
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/prettify/prettify.js
@@ -0,0 +1,33 @@
+window.PR_SHOULD_USE_CONTINUATION=true;window.PR_TAB_WIDTH=8;window.PR_normalizedHtml=window.PR=window.prettyPrintOne=window.prettyPrint=void 0;window._pr_isIE6=function(){var y=navigator&&navigator.userAgent&&navigator.userAgent.match(/\bMSIE ([678])\./);y=y?+y[1]:false;window._pr_isIE6=function(){return y};return y};
+(function(){function y(b){return b.replace(L,"&amp;").replace(M,"&lt;").replace(N,"&gt;")}function H(b,f,i){switch(b.nodeType){case 1:var o=b.tagName.toLowerCase();f.push("<",o);var l=b.attributes,n=l.length;if(n){if(i){for(var r=[],j=n;--j>=0;)r[j]=l[j];r.sort(function(q,m){return q.name<m.name?-1:q.name===m.name?0:1});l=r}for(j=0;j<n;++j){r=l[j];r.specified&&f.push(" ",r.name.toLowerCase(),'="',r.value.replace(L,"&amp;").replace(M,"&lt;").replace(N,"&gt;").replace(X,"&quot;"),'"')}}f.push(">");
+for(l=b.firstChild;l;l=l.nextSibling)H(l,f,i);if(b.firstChild||!/^(?:br|link|img)$/.test(o))f.push("</",o,">");break;case 3:case 4:f.push(y(b.nodeValue));break}}function O(b){function f(c){if(c.charAt(0)!=="\\")return c.charCodeAt(0);switch(c.charAt(1)){case "b":return 8;case "t":return 9;case "n":return 10;case "v":return 11;case "f":return 12;case "r":return 13;case "u":case "x":return parseInt(c.substring(2),16)||c.charCodeAt(1);case "0":case "1":case "2":case "3":case "4":case "5":case "6":case "7":return parseInt(c.substring(1),
+8);default:return c.charCodeAt(1)}}function i(c){if(c<32)return(c<16?"\\x0":"\\x")+c.toString(16);c=String.fromCharCode(c);if(c==="\\"||c==="-"||c==="["||c==="]")c="\\"+c;return c}function o(c){var d=c.substring(1,c.length-1).match(RegExp("\\\\u[0-9A-Fa-f]{4}|\\\\x[0-9A-Fa-f]{2}|\\\\[0-3][0-7]{0,2}|\\\\[0-7]{1,2}|\\\\[\\s\\S]|-|[^-\\\\]","g"));c=[];for(var a=[],k=d[0]==="^",e=k?1:0,h=d.length;e<h;++e){var g=d[e];switch(g){case "\\B":case "\\b":case "\\D":case "\\d":case "\\S":case "\\s":case "\\W":case "\\w":c.push(g);
+continue}g=f(g);var s;if(e+2<h&&"-"===d[e+1]){s=f(d[e+2]);e+=2}else s=g;a.push([g,s]);if(!(s<65||g>122)){s<65||g>90||a.push([Math.max(65,g)|32,Math.min(s,90)|32]);s<97||g>122||a.push([Math.max(97,g)&-33,Math.min(s,122)&-33])}}a.sort(function(v,w){return v[0]-w[0]||w[1]-v[1]});d=[];g=[NaN,NaN];for(e=0;e<a.length;++e){h=a[e];if(h[0]<=g[1]+1)g[1]=Math.max(g[1],h[1]);else d.push(g=h)}a=["["];k&&a.push("^");a.push.apply(a,c);for(e=0;e<d.length;++e){h=d[e];a.push(i(h[0]));if(h[1]>h[0]){h[1]+1>h[0]&&a.push("-");
+a.push(i(h[1]))}}a.push("]");return a.join("")}function l(c){for(var d=c.source.match(RegExp("(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)","g")),a=d.length,k=[],e=0,h=0;e<a;++e){var g=d[e];if(g==="(")++h;else if("\\"===g.charAt(0))if((g=+g.substring(1))&&g<=h)k[g]=-1}for(e=1;e<k.length;++e)if(-1===k[e])k[e]=++n;for(h=e=0;e<a;++e){g=d[e];if(g==="("){++h;if(k[h]===undefined)d[e]="(?:"}else if("\\"===
+g.charAt(0))if((g=+g.substring(1))&&g<=h)d[e]="\\"+k[h]}for(h=e=0;e<a;++e)if("^"===d[e]&&"^"!==d[e+1])d[e]="";if(c.ignoreCase&&r)for(e=0;e<a;++e){g=d[e];c=g.charAt(0);if(g.length>=2&&c==="[")d[e]=o(g);else if(c!=="\\")d[e]=g.replace(/[a-zA-Z]/g,function(s){s=s.charCodeAt(0);return"["+String.fromCharCode(s&-33,s|32)+"]"})}return d.join("")}for(var n=0,r=false,j=false,q=0,m=b.length;q<m;++q){var t=b[q];if(t.ignoreCase)j=true;else if(/[a-z]/i.test(t.source.replace(/\\u[0-9a-f]{4}|\\x[0-9a-f]{2}|\\[^ux]/gi,
+""))){r=true;j=false;break}}var p=[];q=0;for(m=b.length;q<m;++q){t=b[q];if(t.global||t.multiline)throw Error(""+t);p.push("(?:"+l(t)+")")}return RegExp(p.join("|"),j?"gi":"g")}function Y(b){var f=0;return function(i){for(var o=null,l=0,n=0,r=i.length;n<r;++n)switch(i.charAt(n)){case "\t":o||(o=[]);o.push(i.substring(l,n));l=b-f%b;for(f+=l;l>=0;l-=16)o.push(" ".substring(0,l));l=n+1;break;case "\n":f=0;break;default:++f}if(!o)return i;o.push(i.substring(l));return o.join("")}}function I(b,
+f,i,o){if(f){b={source:f,c:b};i(b);o.push.apply(o,b.d)}}function B(b,f){var i={},o;(function(){for(var r=b.concat(f),j=[],q={},m=0,t=r.length;m<t;++m){var p=r[m],c=p[3];if(c)for(var d=c.length;--d>=0;)i[c.charAt(d)]=p;p=p[1];c=""+p;if(!q.hasOwnProperty(c)){j.push(p);q[c]=null}}j.push(/[\0-\uffff]/);o=O(j)})();var l=f.length;function n(r){for(var j=r.c,q=[j,z],m=0,t=r.source.match(o)||[],p={},c=0,d=t.length;c<d;++c){var a=t[c],k=p[a],e=void 0,h;if(typeof k==="string")h=false;else{var g=i[a.charAt(0)];
+if(g){e=a.match(g[1]);k=g[0]}else{for(h=0;h<l;++h){g=f[h];if(e=a.match(g[1])){k=g[0];break}}e||(k=z)}if((h=k.length>=5&&"lang-"===k.substring(0,5))&&!(e&&typeof e[1]==="string")){h=false;k=P}h||(p[a]=k)}g=m;m+=a.length;if(h){h=e[1];var s=a.indexOf(h),v=s+h.length;if(e[2]){v=a.length-e[2].length;s=v-h.length}k=k.substring(5);I(j+g,a.substring(0,s),n,q);I(j+g+s,h,Q(k,h),q);I(j+g+v,a.substring(v),n,q)}else q.push(j+g,k)}r.d=q}return n}function x(b){var f=[],i=[];if(b.tripleQuotedStrings)f.push([A,/^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/,
+null,"'\""]);else b.multiLineStrings?f.push([A,/^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,null,"'\"`"]):f.push([A,/^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,null,"\"'"]);b.verbatimStrings&&i.push([A,/^@\"(?:[^\"]|\"\")*(?:\"|$)/,null]);if(b.hashComments)if(b.cStyleComments){f.push([C,/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/,null,"#"]);i.push([A,/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,
+null])}else f.push([C,/^#[^\r\n]*/,null,"#"]);if(b.cStyleComments){i.push([C,/^\/\/[^\r\n]*/,null]);i.push([C,/^\/\*[\s\S]*?(?:\*\/|$)/,null])}b.regexLiterals&&i.push(["lang-regex",RegExp("^"+Z+"(/(?=[^/*])(?:[^/\\x5B\\x5C]|\\x5C[\\s\\S]|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+/)")]);b=b.keywords.replace(/^\s+|\s+$/g,"");b.length&&i.push([R,RegExp("^(?:"+b.replace(/\s+/g,"|")+")\\b"),null]);f.push([z,/^\s+/,null," \r\n\t\u00a0"]);i.push([J,/^@[a-z_$][a-z_$@0-9]*/i,null],[S,/^@?[A-Z]+[a-z][A-Za-z_$@0-9]*/,
+null],[z,/^[a-z_$][a-z_$@0-9]*/i,null],[J,/^(?:0x[a-f0-9]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+\-]?\d+)?)[a-z]*/i,null,"0123456789"],[E,/^.[^\s\w\.$@\'\"\`\/\#]*/,null]);return B(f,i)}function $(b){function f(D){if(D>r){if(j&&j!==q){n.push("</span>");j=null}if(!j&&q){j=q;n.push('<span class="',j,'">')}var T=y(p(i.substring(r,D))).replace(e?d:c,"$1&#160;");e=k.test(T);n.push(T.replace(a,s));r=D}}var i=b.source,o=b.g,l=b.d,n=[],r=0,j=null,q=null,m=0,t=0,p=Y(window.PR_TAB_WIDTH),c=/([\r\n ]) /g,
+d=/(^| ) /gm,a=/\r\n?|\n/g,k=/[ \r\n]$/,e=true,h=window._pr_isIE6();h=h?b.b.tagName==="PRE"?h===6?"&#160;\r\n":h===7?"&#160;<br>\r":"&#160;\r":"&#160;<br />":"<br />";var g=b.b.className.match(/\blinenums\b(?::(\d+))?/),s;if(g){for(var v=[],w=0;w<10;++w)v[w]=h+'</li><li class="L'+w+'">';var F=g[1]&&g[1].length?g[1]-1:0;n.push('<ol class="linenums"><li class="L',F%10,'"');F&&n.push(' value="',F+1,'"');n.push(">");s=function(){var D=v[++F%10];return j?"</span>"+D+'<span class="'+j+'">':D}}else s=h;
+for(;;)if(m<o.length?t<l.length?o[m]<=l[t]:true:false){f(o[m]);if(j){n.push("</span>");j=null}n.push(o[m+1]);m+=2}else if(t<l.length){f(l[t]);q=l[t+1];t+=2}else break;f(i.length);j&&n.push("</span>");g&&n.push("</li></ol>");b.a=n.join("")}function u(b,f){for(var i=f.length;--i>=0;){var o=f[i];if(G.hasOwnProperty(o))"console"in window&&console.warn("cannot override language handler %s",o);else G[o]=b}}function Q(b,f){b&&G.hasOwnProperty(b)||(b=/^\s*</.test(f)?"default-markup":"default-code");return G[b]}
+function U(b){var f=b.f,i=b.e;b.a=f;try{var o,l=f.match(aa);f=[];var n=0,r=[];if(l)for(var j=0,q=l.length;j<q;++j){var m=l[j];if(m.length>1&&m.charAt(0)==="<"){if(!ba.test(m))if(ca.test(m)){f.push(m.substring(9,m.length-3));n+=m.length-12}else if(da.test(m)){f.push("\n");++n}else if(m.indexOf(V)>=0&&m.replace(/\s(\w+)\s*=\s*(?:\"([^\"]*)\"|'([^\']*)'|(\S+))/g,' $1="$2$3$4"').match(/[cC][lL][aA][sS][sS]=\"[^\"]*\bnocode\b/)){var t=m.match(W)[2],p=1,c;c=j+1;a:for(;c<q;++c){var d=l[c].match(W);if(d&&
+d[2]===t)if(d[1]==="/"){if(--p===0)break a}else++p}if(c<q){r.push(n,l.slice(j,c+1).join(""));j=c}else r.push(n,m)}else r.push(n,m)}else{var a;p=m;var k=p.indexOf("&");if(k<0)a=p;else{for(--k;(k=p.indexOf("&#",k+1))>=0;){var e=p.indexOf(";",k);if(e>=0){var h=p.substring(k+3,e),g=10;if(h&&h.charAt(0)==="x"){h=h.substring(1);g=16}var s=parseInt(h,g);isNaN(s)||(p=p.substring(0,k)+String.fromCharCode(s)+p.substring(e+1))}}a=p.replace(ea,"<").replace(fa,">").replace(ga,"'").replace(ha,'"').replace(ia," ").replace(ja,
+"&")}f.push(a);n+=a.length}}o={source:f.join(""),h:r};var v=o.source;b.source=v;b.c=0;b.g=o.h;Q(i,v)(b);$(b)}catch(w){if("console"in window)console.log(w&&w.stack?w.stack:w)}}var A="str",R="kwd",C="com",S="typ",J="lit",E="pun",z="pln",P="src",V="nocode",Z=function(){for(var b=["!","!=","!==","#","%","%=","&","&&","&&=","&=","(","*","*=","+=",",","-=","->","/","/=",":","::",";","<","<<","<<=","<=","=","==","===",">",">=",">>",">>=",">>>",">>>=","?","@","[","^","^=","^^","^^=","{","|","|=","||","||=",
+"~","break","case","continue","delete","do","else","finally","instanceof","return","throw","try","typeof"],f="(?:^^|[+-]",i=0;i<b.length;++i)f+="|"+b[i].replace(/([^=<>:&a-z])/g,"\\$1");f+=")\\s*";return f}(),L=/&/g,M=/</g,N=/>/g,X=/\"/g,ea=/&lt;/g,fa=/&gt;/g,ga=/&apos;/g,ha=/&quot;/g,ja=/&amp;/g,ia=/&nbsp;/g,ka=/[\r\n]/g,K=null,aa=RegExp("[^<]+|<!--[\\s\\S]*?--\>|<!\\[CDATA\\[[\\s\\S]*?\\]\\]>|</?[a-zA-Z](?:[^>\"']|'[^']*'|\"[^\"]*\")*>|<","g"),ba=/^<\!--/,ca=/^<!\[CDATA\[/,da=/^<br\b/i,W=/^<(\/?)([a-zA-Z][a-zA-Z0-9]*)/,
+la=x({keywords:"break continue do else for if return while auto case char const default double enum extern float goto int long register short signed sizeof static struct switch typedef union unsigned void volatile catch class delete false import new operator private protected public this throw true try typeof alignof align_union asm axiom bool concept concept_map const_cast constexpr decltype dynamic_cast explicit export friend inline late_check mutable namespace nullptr reinterpret_cast static_assert static_cast template typeid typename using virtual wchar_t where break continue do else for if return while auto case char const default double enum extern float goto int long register short signed sizeof static struct switch typedef union unsigned void volatile catch class delete false import new operator private protected public this throw true try typeof abstract boolean byte extends final finally implements import instanceof null native package strictfp super synchronized throws transient as base by checked decimal delegate descending event fixed foreach from group implicit in interface internal into is lock object out override orderby params partial readonly ref sbyte sealed stackalloc string select uint ulong unchecked unsafe ushort var break continue do else for if return while auto case char const default double enum extern float goto int long register short signed sizeof static struct switch typedef union unsigned void volatile catch class delete false import new operator private protected public this throw true try typeof debugger eval export function get null set undefined var with Infinity NaN caller delete die do dump elsif eval exit foreach for goto if import last local my next no our print package redo require sub undef unless until use wantarray while BEGIN END break continue do else for if return while and as assert class def del elif except exec finally from global import in is lambda nonlocal not or pass print raise try with yield False True None break continue do else for if return while alias and begin case class def defined elsif end ensure false in module next nil not or redo rescue retry self super then true undef unless until when yield BEGIN END break continue do else for if return while case done elif esac eval fi function in local set then until ",
+hashComments:true,cStyleComments:true,multiLineStrings:true,regexLiterals:true}),G={};u(la,["default-code"]);u(B([],[[z,/^[^<?]+/],["dec",/^<!\w[^>]*(?:>|$)/],[C,/^<\!--[\s\S]*?(?:-\->|$)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],[E,/^(?:<[%?]|[%?]>)/],["lang-",/^<xmp\b[^>]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-js",/^<script\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^<style\b[^>]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),["default-markup",
+"htm","html","mxml","xhtml","xml","xsl"]);u(B([[z,/^[\s]+/,null," \t\r\n"],["atv",/^(?:\"[^\"]*\"?|\'[^\']*\'?)/,null,"\"'"]],[["tag",/^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],["atn",/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],[E,/^[=<>\/]+/],["lang-js",/^on\w+\s*=\s*\"([^\"]+)\"/i],["lang-js",/^on\w+\s*=\s*\'([^\']+)\'/i],["lang-js",/^on\w+\s*=\s*([^\"\'>\s]+)/i],["lang-css",/^style\s*=\s*\"([^\"]+)\"/i],["lang-css",/^style\s*=\s*\'([^\']+)\'/i],
+["lang-css",/^style\s*=\s*([^\"\'>\s]+)/i]]),["in.tag"]);u(B([],[["atv",/^[\s\S]+/]]),["uq.val"]);u(x({keywords:"break continue do else for if return while auto case char const default double enum extern float goto int long register short signed sizeof static struct switch typedef union unsigned void volatile catch class delete false import new operator private protected public this throw true try typeof alignof align_union asm axiom bool concept concept_map const_cast constexpr decltype dynamic_cast explicit export friend inline late_check mutable namespace nullptr reinterpret_cast static_assert static_cast template typeid typename using virtual wchar_t where ",
+hashComments:true,cStyleComments:true}),["c","cc","cpp","cxx","cyc","m"]);u(x({keywords:"null true false"}),["json"]);u(x({keywords:"break continue do else for if return while auto case char const default double enum extern float goto int long register short signed sizeof static struct switch typedef union unsigned void volatile catch class delete false import new operator private protected public this throw true try typeof abstract boolean byte extends final finally implements import instanceof null native package strictfp super synchronized throws transient as base by checked decimal delegate descending event fixed foreach from group implicit in interface internal into is lock object out override orderby params partial readonly ref sbyte sealed stackalloc string select uint ulong unchecked unsafe ushort var ",
+hashComments:true,cStyleComments:true,verbatimStrings:true}),["cs"]);u(x({keywords:"break continue do else for if return while auto case char const default double enum extern float goto int long register short signed sizeof static struct switch typedef union unsigned void volatile catch class delete false import new operator private protected public this throw true try typeof abstract boolean byte extends final finally implements import instanceof null native package strictfp super synchronized throws transient ",
+cStyleComments:true}),["java"]);u(x({keywords:"break continue do else for if return while case done elif esac eval fi function in local set then until ",hashComments:true,multiLineStrings:true}),["bsh","csh","sh"]);u(x({keywords:"break continue do else for if return while and as assert class def del elif except exec finally from global import in is lambda nonlocal not or pass print raise try with yield False True None ",hashComments:true,multiLineStrings:true,tripleQuotedStrings:true}),["cv","py"]);
+u(x({keywords:"caller delete die do dump elsif eval exit foreach for goto if import last local my next no our print package redo require sub undef unless until use wantarray while BEGIN END ",hashComments:true,multiLineStrings:true,regexLiterals:true}),["perl","pl","pm"]);u(x({keywords:"break continue do else for if return while alias and begin case class def defined elsif end ensure false in module next nil not or redo rescue retry self super then true undef unless until when yield BEGIN END ",hashComments:true,
+multiLineStrings:true,regexLiterals:true}),["rb"]);u(x({keywords:"break continue do else for if return while auto case char const default double enum extern float goto int long register short signed sizeof static struct switch typedef union unsigned void volatile catch class delete false import new operator private protected public this throw true try typeof debugger eval export function get null set undefined var with Infinity NaN ",cStyleComments:true,regexLiterals:true}),["js"]);u(B([],[[A,/^[\s\S]+/]]),
+["regex"]);window.PR_normalizedHtml=H;window.prettyPrintOne=function(b,f){var i={f:b,e:f};U(i);return i.a};window.prettyPrint=function(b){function f(){for(var t=window.PR_SHOULD_USE_CONTINUATION?j.now()+250:Infinity;q<o.length&&j.now()<t;q++){var p=o[q];if(p.className&&p.className.indexOf("prettyprint")>=0){var c=p.className.match(/\blang-(\w+)\b/);if(c)c=c[1];for(var d=false,a=p.parentNode;a;a=a.parentNode)if((a.tagName==="pre"||a.tagName==="code"||a.tagName==="xmp")&&a.className&&a.className.indexOf("prettyprint")>=
+0){d=true;break}if(!d){a=p;if(null===K){d=document.createElement("PRE");d.appendChild(document.createTextNode('<!DOCTYPE foo PUBLIC "foo bar">\n<foo />'));K=!/</.test(d.innerHTML)}if(K){d=a.innerHTML;if("XMP"===a.tagName)d=y(d);else{a=a;if("PRE"===a.tagName)a=true;else if(ka.test(d)){var k="";if(a.currentStyle)k=a.currentStyle.whiteSpace;else if(window.getComputedStyle)k=window.getComputedStyle(a,null).whiteSpace;a=!k||k==="pre"}else a=true;a||(d=d.replace(/(<br\s*\/?>)[\r\n]+/g,"$1").replace(/(?:[\r\n]+[ \t]*)+/g,
+" "))}d=d}else{d=[];for(a=a.firstChild;a;a=a.nextSibling)H(a,d);d=d.join("")}d=d.replace(/(?:\r\n?|\n)$/,"");m={f:d,e:c,b:p};U(m);if(p=m.a){c=m.b;if("XMP"===c.tagName){d=document.createElement("PRE");for(a=0;a<c.attributes.length;++a){k=c.attributes[a];if(k.specified)if(k.name.toLowerCase()==="class")d.className=k.value;else d.setAttribute(k.name,k.value)}d.innerHTML=p;c.parentNode.replaceChild(d,c)}else c.innerHTML=p}}}}if(q<o.length)setTimeout(f,250);else b&&b()}for(var i=[document.getElementsByTagName("pre"),
+document.getElementsByTagName("code"),document.getElementsByTagName("xmp")],o=[],l=0;l<i.length;++l)for(var n=0,r=i[l].length;n<r;++n)o.push(i[l][n]);i=null;var j=Date;j.now||(j={now:function(){return(new Date).getTime()}});var q=0,m;f()};window.PR={combinePrefixPatterns:O,createSimpleLexer:B,registerLangHandler:u,sourceDecorator:x,PR_ATTRIB_NAME:"atn",PR_ATTRIB_VALUE:"atv",PR_COMMENT:C,PR_DECLARATION:"dec",PR_KEYWORD:R,PR_LITERAL:J,PR_NOCODE:V,PR_PLAIN:z,PR_PUNCTUATION:E,PR_SOURCE:P,PR_STRING:A,
+PR_TAG:"tag",PR_TYPE:S}})() \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/panels/ActivityPanel.html b/src/main/java/com/gitblit/wicket/panels/ActivityPanel.html
new file mode 100644
index 00000000..b818e94a
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/ActivityPanel.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+
+<body>
+<wicket:panel>
+
+ <div wicket:id="activity" style="padding-bottom:10px;">
+ <div class="header"><i class="icon-refresh" style="vertical-align: middle;"></i> <span style="font-weight:bold;" wicket:id="title">[title]</span></div>
+ <table class="activity">
+ <tr wicket:id="commit">
+ <td class="hidden-phone date" style="width:60px; vertical-align: middle;text-align: right;padding-right:10px;" ><span wicket:id="time">[time of day]</span></td>
+ <td style="width:10em;text-align:left;vertical-align: middle;">
+ <span wicket:id="repository" class="repositorySwatch">[repository link]</span>
+ </td>
+ <td class="hidden-phone hidden-tablet" style="width:30px;vertical-align: middle;"><span wicket:id="avatar" style="vertical-align: middle;"></span></td>
+ <td style="vertical-align: middle;padding-left:15px;">
+ <img class="hidden-phone hidden-tablet" wicket:id="commitIcon" style="vertical-align: top;"></img>
+ <span wicket:id="message">[shortlog commit link]</span><br/>
+ <span wicket:id="author">[author link]</span> <span class="hidden-phone"><wicket:message key="gb.authored"></wicket:message> <span wicket:id="commitid">[commit id]</span></span> on <span wicket:id="branch"></span>
+ </td>
+ <td class="hidden-phone" style="text-align:right;vertical-align: middle;">
+ <div wicket:id="commitRefs">[commit refs]</div>
+ </td>
+ <td class="hidden-phone rightAlign" style="width:7em;vertical-align: middle;">
+ <span class="link">
+ <a wicket:id="diff" target="_blank"><wicket:message key="gb.diff"></wicket:message></a> | <a wicket:id="tree" target="_blank"><wicket:message key="gb.tree"></wicket:message></a>
+ </span>
+ </td>
+ </tr>
+ </table>
+ </div>
+</wicket:panel>
+</body>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/panels/ActivityPanel.java b/src/main/java/com/gitblit/wicket/panels/ActivityPanel.java
new file mode 100644
index 00000000..669c36be
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/ActivityPanel.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.panels;
+
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.wicket.markup.html.link.BookmarkablePageLink;
+import org.apache.wicket.markup.repeater.Item;
+import org.apache.wicket.markup.repeater.data.DataView;
+import org.apache.wicket.markup.repeater.data.ListDataProvider;
+
+import com.gitblit.Constants;
+import com.gitblit.GitBlit;
+import com.gitblit.Keys;
+import com.gitblit.models.Activity;
+import com.gitblit.models.RepositoryCommit;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.wicket.WicketUtils;
+import com.gitblit.wicket.pages.CommitDiffPage;
+import com.gitblit.wicket.pages.CommitPage;
+import com.gitblit.wicket.pages.GitSearchPage;
+import com.gitblit.wicket.pages.LogPage;
+import com.gitblit.wicket.pages.SummaryPage;
+import com.gitblit.wicket.pages.TreePage;
+
+/**
+ * Renders activity in day-blocks in reverse-chronological order.
+ *
+ * @author James Moger
+ *
+ */
+public class ActivityPanel extends BasePanel {
+
+ private static final long serialVersionUID = 1L;
+
+ public ActivityPanel(String wicketId, List<Activity> recentActivity) {
+ super(wicketId);
+
+ Collections.sort(recentActivity);
+
+ final int shortHashLen = GitBlit.getInteger(Keys.web.shortCommitIdLength, 6);
+ DataView<Activity> activityView = new DataView<Activity>("activity",
+ new ListDataProvider<Activity>(recentActivity)) {
+ private static final long serialVersionUID = 1L;
+
+ public void populateItem(final Item<Activity> activityItem) {
+ final Activity entry = activityItem.getModelObject();
+ activityItem.add(WicketUtils.createDatestampLabel("title", entry.startDate, getTimeZone(), getTimeUtils()));
+
+ // display the commits in chronological order
+ DataView<RepositoryCommit> commits = new DataView<RepositoryCommit>("commit",
+ new ListDataProvider<RepositoryCommit>(entry.getCommits())) {
+ private static final long serialVersionUID = 1L;
+
+ public void populateItem(final Item<RepositoryCommit> commitItem) {
+ final RepositoryCommit commit = commitItem.getModelObject();
+
+ // commit time of day
+ commitItem.add(WicketUtils.createTimeLabel("time", commit.getCommitterIdent()
+ .getWhen(), getTimeZone(), getTimeUtils()));
+
+ // avatar
+ commitItem.add(new GravatarImage("avatar", commit.getAuthorIdent(), 40));
+
+ // merge icon
+ if (commit.getParentCount() > 1) {
+ commitItem.add(WicketUtils.newImage("commitIcon",
+ "commit_merge_16x16.png"));
+ } else {
+ commitItem.add(WicketUtils.newBlankImage("commitIcon").setVisible(false));
+ }
+
+ // author search link
+ String author = commit.getAuthorIdent().getName();
+ LinkPanel authorLink = new LinkPanel("author", "list", author,
+ GitSearchPage.class, WicketUtils.newSearchParameter(commit.repository,
+ commit.getName(), author, Constants.SearchType.AUTHOR), true);
+ setPersonSearchTooltip(authorLink, author, Constants.SearchType.AUTHOR);
+ commitItem.add(authorLink);
+
+ // repository
+ String repoName = StringUtils.stripDotGit(commit.repository);
+ LinkPanel repositoryLink = new LinkPanel("repository", null,
+ repoName, SummaryPage.class,
+ WicketUtils.newRepositoryParameter(commit.repository), true);
+ WicketUtils.setCssBackground(repositoryLink, repoName);
+ commitItem.add(repositoryLink);
+
+ // repository branch
+ LinkPanel branchLink = new LinkPanel("branch", "list", commit.branch,
+ LogPage.class, WicketUtils.newObjectParameter(commit.repository,
+ commit.branch), true);
+ WicketUtils.setCssStyle(branchLink, "color: #008000;");
+ commitItem.add(branchLink);
+
+ LinkPanel commitid = new LinkPanel("commitid", "list subject",
+ commit.getName().substring(0, shortHashLen), CommitPage.class,
+ WicketUtils.newObjectParameter(commit.repository, commit.getName()), true);
+ commitItem.add(commitid);
+
+ // message/commit link
+ String shortMessage = commit.getShortMessage();
+ String trimmedMessage = shortMessage;
+ if (commit.getRefs() != null && commit.getRefs().size() > 0) {
+ trimmedMessage = StringUtils.trimString(shortMessage, Constants.LEN_SHORTLOG_REFS);
+ } else {
+ trimmedMessage = StringUtils.trimString(shortMessage, Constants.LEN_SHORTLOG);
+ }
+ LinkPanel shortlog = new LinkPanel("message", "list subject",
+ trimmedMessage, CommitPage.class, WicketUtils.newObjectParameter(
+ commit.repository, commit.getName()), true);
+ if (!shortMessage.equals(trimmedMessage)) {
+ WicketUtils.setHtmlTooltip(shortlog, shortMessage);
+ }
+ commitItem.add(shortlog);
+
+ // refs
+ commitItem.add(new RefsPanel("commitRefs", commit.repository, commit
+ .getRefs()));
+
+ // diff, tree links
+ commitItem.add(new BookmarkablePageLink<Void>("diff", CommitDiffPage.class,
+ WicketUtils.newObjectParameter(commit.repository, commit.getName()))
+ .setEnabled(commit.getParentCount() > 0));
+ commitItem.add(new BookmarkablePageLink<Void>("tree", TreePage.class,
+ WicketUtils.newObjectParameter(commit.repository, commit.getName())));
+ }
+ };
+ activityItem.add(commits);
+ }
+ };
+ add(activityView);
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/panels/BasePanel.java b/src/main/java/com/gitblit/wicket/panels/BasePanel.java
new file mode 100644
index 00000000..ec879178
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/BasePanel.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.panels;
+
+import java.util.ResourceBundle;
+import java.util.TimeZone;
+
+import org.apache.wicket.AttributeModifier;
+import org.apache.wicket.Component;
+import org.apache.wicket.markup.html.panel.Panel;
+import org.apache.wicket.model.Model;
+
+import com.gitblit.Constants;
+import com.gitblit.GitBlit;
+import com.gitblit.Keys;
+import com.gitblit.utils.TimeUtils;
+import com.gitblit.wicket.GitBlitWebSession;
+import com.gitblit.wicket.WicketUtils;
+
+public abstract class BasePanel extends Panel {
+
+ private static final long serialVersionUID = 1L;
+
+ private transient TimeUtils timeUtils;
+
+ public BasePanel(String wicketId) {
+ super(wicketId);
+ }
+
+ protected TimeZone getTimeZone() {
+ return GitBlit.getBoolean(Keys.web.useClientTimezone, false) ? GitBlitWebSession.get()
+ .getTimezone() : GitBlit.getTimezone();
+ }
+
+ protected TimeUtils getTimeUtils() {
+ if (timeUtils == null) {
+ ResourceBundle bundle;
+ try {
+ bundle = ResourceBundle.getBundle("com.gitblit.wicket.GitBlitWebApp", GitBlitWebSession.get().getLocale());
+ } catch (Throwable t) {
+ bundle = ResourceBundle.getBundle("com.gitblit.wicket.GitBlitWebApp");
+ }
+ timeUtils = new TimeUtils(bundle);
+ }
+ return timeUtils;
+ }
+
+ protected void setPersonSearchTooltip(Component component, String value, Constants.SearchType searchType) {
+ if (searchType.equals(Constants.SearchType.AUTHOR)) {
+ WicketUtils.setHtmlTooltip(component, getString("gb.searchForAuthor") + " " + value);
+ } else if (searchType.equals(Constants.SearchType.COMMITTER)) {
+ WicketUtils.setHtmlTooltip(component, getString("gb.searchForCommitter") + " " + value);
+ }
+ }
+
+ public static class JavascriptEventConfirmation extends AttributeModifier {
+
+ private static final long serialVersionUID = 1L;
+
+ public JavascriptEventConfirmation(String event, String msg) {
+ super(event, true, new Model<String>(msg));
+ }
+
+ protected String newValue(final String currentValue, final String replacementValue) {
+ String prefix = "var conf = confirm('" + replacementValue + "'); "
+ + "if (!conf) return false; ";
+ String result = prefix;
+ if (currentValue != null) {
+ result = prefix + currentValue;
+ }
+ return result;
+ }
+ }
+
+ public static class JavascriptTextPrompt extends AttributeModifier {
+
+ private static final long serialVersionUID = 1L;
+
+ private String initialValue = "";
+
+ public JavascriptTextPrompt(String event, String msg, String value) {
+ super(event, true, new Model<String>(msg));
+ initialValue = value;
+ }
+
+ protected String newValue(final String currentValue, final String message) {
+ String result = "var userText = prompt('" + message + "','"
+ + (initialValue == null ? "" : initialValue) + "'); " + "return userText; ";
+ return result;
+ }
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/panels/BranchesPanel.html b/src/main/java/com/gitblit/wicket/panels/BranchesPanel.html
new file mode 100644
index 00000000..58c86a45
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/BranchesPanel.html
@@ -0,0 +1,52 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+
+<body>
+<wicket:panel>
+
+ <!-- header -->
+ <div class="header"><i class="icon-random" style="vertical-align: middle;"></i> <b><span wicket:id="branches">[branches header]</span></b></div>
+
+ <table class="pretty">
+ <tbody>
+ <tr wicket:id="branch">
+ <td class="date"><span wicket:id="branchDate">[branch date]</span></td>
+ <td><span wicket:id="branchName">[branch name]</span></td>
+ <td class="hidden-phone hidden-tablet author"><span wicket:id="branchAuthor">[branch author]</span></td>
+ <td class="hidden-phone"><span wicket:id="branchLog">[branch log]</span></td>
+ <td class="hidden-phone rightAlign">
+ <span wicket:id="branchLinks"></span>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+
+ <div wicket:id="allBranches">[all branches]</div>
+
+ <!-- branch page links -->
+ <wicket:fragment wicket:id="branchPageLinks">
+ <span class="link">
+ <a wicket:id="log"><wicket:message key="gb.log"></wicket:message></a> | <a wicket:id="tree"><wicket:message key="gb.tree"></wicket:message></a> | <a wicket:id="metrics"><wicket:message key="gb.metrics"></wicket:message></a> | <a wicket:id="syndication"><wicket:message key="gb.feed"></wicket:message></a>
+ </span>
+ </wicket:fragment>
+
+ <!-- branch page admin links -->
+ <wicket:fragment wicket:id="branchPageAdminLinks">
+ <span class="link">
+ <a wicket:id="log"><wicket:message key="gb.log"></wicket:message></a> | <a wicket:id="tree"><wicket:message key="gb.tree"></wicket:message></a> | <a wicket:id="metrics"><wicket:message key="gb.metrics"></wicket:message></a> | <a wicket:id="syndication"><wicket:message key="gb.feed"></wicket:message></a> | <a wicket:id="deleteBranch"><wicket:message key="gb.delete"></wicket:message></a>
+ </span>
+ </wicket:fragment>
+
+ <!-- branch panel links -->
+ <wicket:fragment wicket:id="branchPanelLinks">
+ <span class="link">
+ <a wicket:id="log"><wicket:message key="gb.log"></wicket:message></a> | <a wicket:id="tree"><wicket:message key="gb.tree"></wicket:message></a>
+ </span>
+ </wicket:fragment>
+
+</wicket:panel>
+</body>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/panels/BranchesPanel.java b/src/main/java/com/gitblit/wicket/panels/BranchesPanel.java
new file mode 100644
index 00000000..1262077c
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/BranchesPanel.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.panels;
+
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.link.BookmarkablePageLink;
+import org.apache.wicket.markup.html.link.ExternalLink;
+import org.apache.wicket.markup.html.link.Link;
+import org.apache.wicket.markup.html.panel.Fragment;
+import org.apache.wicket.markup.repeater.Item;
+import org.apache.wicket.markup.repeater.data.DataView;
+import org.apache.wicket.markup.repeater.data.ListDataProvider;
+import org.apache.wicket.model.StringResourceModel;
+import org.eclipse.jgit.lib.Repository;
+
+import com.gitblit.Constants;
+import com.gitblit.GitBlit;
+import com.gitblit.SyndicationServlet;
+import com.gitblit.models.RefModel;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.UserModel;
+import com.gitblit.utils.JGitUtils;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.wicket.GitBlitWebSession;
+import com.gitblit.wicket.WicketUtils;
+import com.gitblit.wicket.pages.BranchesPage;
+import com.gitblit.wicket.pages.CommitPage;
+import com.gitblit.wicket.pages.GitSearchPage;
+import com.gitblit.wicket.pages.LogPage;
+import com.gitblit.wicket.pages.MetricsPage;
+import com.gitblit.wicket.pages.TreePage;
+
+public class BranchesPanel extends BasePanel {
+
+ private static final long serialVersionUID = 1L;
+
+ private final boolean hasBranches;
+
+ public BranchesPanel(String wicketId, final RepositoryModel model, Repository r,
+ final int maxCount, final boolean showAdmin) {
+ super(wicketId);
+
+ // branches
+ List<RefModel> branches = new ArrayList<RefModel>();
+ UserModel user = GitBlitWebSession.get().getUser();
+ if (user == null) {
+ user = UserModel.ANONYMOUS;
+ }
+
+ List<RefModel> localBranches = JGitUtils.getLocalBranches(r, false, -1);
+ for (RefModel refModel : localBranches) {
+ if (user.canView(model, refModel.reference.getName())) {
+ branches.add(refModel);
+ }
+ }
+ if (model.showRemoteBranches) {
+ List<RefModel> remoteBranches = JGitUtils.getRemoteBranches(r, false, -1);
+ for (RefModel refModel : remoteBranches) {
+ if (user.canView(model, refModel.reference.getName())) {
+ branches.add(refModel);
+ }
+ }
+ }
+ Collections.sort(branches);
+ Collections.reverse(branches);
+ if (maxCount > 0 && branches.size() > maxCount) {
+ branches = new ArrayList<RefModel>(branches.subList(0, maxCount));
+ }
+
+ if (maxCount > 0) {
+ // summary page
+ // show branches page link
+ add(new LinkPanel("branches", "title", new StringResourceModel("gb.branches", this,
+ null), BranchesPage.class, WicketUtils.newRepositoryParameter(model.name)));
+ } else {
+ // branches page
+ add(new Label("branches", new StringResourceModel("gb.branches", this, null)));
+ }
+
+ // only allow delete if we have multiple branches
+ final boolean showDelete = showAdmin && branches.size() > 1;
+
+ ListDataProvider<RefModel> branchesDp = new ListDataProvider<RefModel>(branches);
+ DataView<RefModel> branchesView = new DataView<RefModel>("branch", branchesDp) {
+ private static final long serialVersionUID = 1L;
+ int counter;
+
+ public void populateItem(final Item<RefModel> item) {
+ final RefModel entry = item.getModelObject();
+
+ item.add(WicketUtils.createDateLabel("branchDate", entry.getDate(), getTimeZone(), getTimeUtils()));
+
+ item.add(new LinkPanel("branchName", "list name", StringUtils.trimString(
+ entry.displayName, 28), LogPage.class, WicketUtils.newObjectParameter(
+ model.name, entry.getName())));
+
+ String author = entry.getAuthorIdent().getName();
+ LinkPanel authorLink = new LinkPanel("branchAuthor", "list", author,
+ GitSearchPage.class, WicketUtils.newSearchParameter(model.name,
+ entry.getName(), author, Constants.SearchType.AUTHOR));
+ setPersonSearchTooltip(authorLink, author, Constants.SearchType.AUTHOR);
+ item.add(authorLink);
+
+ // short message
+ String shortMessage = entry.getShortMessage();
+ String trimmedMessage = StringUtils.trimString(shortMessage, Constants.LEN_SHORTLOG);
+ LinkPanel shortlog = new LinkPanel("branchLog", "list subject", trimmedMessage,
+ CommitPage.class, WicketUtils.newObjectParameter(model.name,
+ entry.getName()));
+ if (!shortMessage.equals(trimmedMessage)) {
+ WicketUtils.setHtmlTooltip(shortlog, shortMessage);
+ }
+ item.add(shortlog);
+
+ if (maxCount <= 0) {
+ Fragment fragment = new Fragment("branchLinks", showDelete? "branchPageAdminLinks" : "branchPageLinks", this);
+ fragment.add(new BookmarkablePageLink<Void>("log", LogPage.class, WicketUtils
+ .newObjectParameter(model.name, entry.getName())));
+ fragment.add(new BookmarkablePageLink<Void>("tree", TreePage.class, WicketUtils
+ .newObjectParameter(model.name, entry.getName())));
+ fragment.add(new BookmarkablePageLink<Void>("metrics", MetricsPage.class,
+ WicketUtils.newObjectParameter(model.name, entry.getName())));
+ fragment.add(new ExternalLink("syndication", SyndicationServlet.asLink(
+ getRequest().getRelativePathPrefixToContextRoot(), model.name,
+ entry.getName(), 0)));
+ if (showDelete) {
+ fragment.add(createDeleteBranchLink(model, entry));
+ }
+ item.add(fragment);
+ } else {
+ Fragment fragment = new Fragment("branchLinks", "branchPanelLinks", this);
+ fragment.add(new BookmarkablePageLink<Void>("log", LogPage.class, WicketUtils
+ .newObjectParameter(model.name, entry.getName())));
+ fragment.add(new BookmarkablePageLink<Void>("tree", TreePage.class, WicketUtils
+ .newObjectParameter(model.name, entry.getName())));
+ item.add(fragment);
+ }
+ WicketUtils.setAlternatingBackground(item, counter);
+ counter++;
+ }
+ };
+ add(branchesView);
+ if (branches.size() < maxCount || maxCount <= 0) {
+ add(new Label("allBranches", "").setVisible(false));
+ } else {
+ add(new LinkPanel("allBranches", "link", new StringResourceModel("gb.allBranches",
+ this, null), BranchesPage.class, WicketUtils.newRepositoryParameter(model.name)));
+ }
+ // We always have 1 branch
+ hasBranches = (branches.size() > 1)
+ || ((branches.size() == 1) && !branches.get(0).displayName
+ .equalsIgnoreCase("master"));
+ }
+
+ public BranchesPanel hideIfEmpty() {
+ setVisible(hasBranches);
+ return this;
+ }
+
+ private Link<Void> createDeleteBranchLink(final RepositoryModel repositoryModel, final RefModel entry)
+ {
+ Link<Void> deleteLink = new Link<Void>("deleteBranch") {
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public void onClick() {
+ Repository r = GitBlit.self().getRepository(repositoryModel.name);
+ if (r == null) {
+ if (GitBlit.self().isCollectingGarbage(repositoryModel.name)) {
+ error(MessageFormat.format(getString("gb.busyCollectingGarbage"), repositoryModel.name));
+ } else {
+ error(MessageFormat.format("Failed to find repository {0}", repositoryModel.name));
+ }
+ return;
+ }
+ boolean success = JGitUtils.deleteBranchRef(r, entry.getName());
+ r.close();
+ if (success) {
+ info(MessageFormat.format("Branch \"{0}\" deleted", entry.displayName));
+ // redirect to the owning page
+ setResponsePage(getPage().getClass(), WicketUtils.newRepositoryParameter(repositoryModel.name));
+ }
+ else {
+ error(MessageFormat.format("Failed to delete branch \"{0}\"", entry.displayName));
+ }
+ }
+ };
+
+ deleteLink.add(new JavascriptEventConfirmation("onclick", MessageFormat.format(
+ "Delete branch \"{0}\"?", entry.displayName )));
+ return deleteLink;
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/panels/BulletListPanel.html b/src/main/java/com/gitblit/wicket/panels/BulletListPanel.html
new file mode 100644
index 00000000..4d28f498
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/BulletListPanel.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+
+<wicket:panel>
+ <span class="label" wicket:id="header">already specified</span>
+ <ul wicket:id="list">
+ <li wicket:id="listItem">item</li>
+ </ul>
+</wicket:panel>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/panels/BulletListPanel.java b/src/main/java/com/gitblit/wicket/panels/BulletListPanel.java
new file mode 100644
index 00000000..e49223e0
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/BulletListPanel.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.panels;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.panel.Panel;
+import org.apache.wicket.markup.repeater.Item;
+import org.apache.wicket.markup.repeater.data.DataView;
+import org.apache.wicket.markup.repeater.data.ListDataProvider;
+
+public class BulletListPanel extends Panel {
+
+ private static final long serialVersionUID = 1L;
+
+ public BulletListPanel(String id, String header, List<String> list) {
+ super(id);
+ if (list == null) {
+ list = new ArrayList<String>();
+ }
+ add(new Label("header", header).setVisible(list.size() > 0));
+ ListDataProvider<String> listDp = new ListDataProvider<String>(list);
+ DataView<String> listView = new DataView<String>("list", listDp) {
+ private static final long serialVersionUID = 1L;
+
+ public void populateItem(final Item<String> item) {
+ String entry = item.getModelObject();
+ item.add(new Label("listItem", entry));
+ }
+ };
+ add(listView.setVisible(list.size() > 0));
+ }
+} \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/panels/CommitHeaderPanel.html b/src/main/java/com/gitblit/wicket/panels/CommitHeaderPanel.html
new file mode 100644
index 00000000..95304403
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/CommitHeaderPanel.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+
+<wicket:panel>
+ <div class="commitHeader">
+ <div class="row">
+ <div>
+ <span class="pull-right" wicket:id="authorAvatar"></span>
+ </div>
+ <div class="span9">
+ <div wicket:id="shortmessage">[short message]</div>
+ <div wicket:id="author">[author]</div>
+ <div>
+ <span wicket:id="date">[date]</span>
+ <span class="hidden-phone" style="padding-left:20px;color:#888;" wicket:id="commitid">[commit id]</span>
+ </div>
+ </div>
+ </div>
+ </div>
+</wicket:panel>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/panels/CommitHeaderPanel.java b/src/main/java/com/gitblit/wicket/panels/CommitHeaderPanel.java
new file mode 100644
index 00000000..bb960cca
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/CommitHeaderPanel.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.panels;
+
+import org.apache.wicket.markup.html.basic.Label;
+import org.eclipse.jgit.revwalk.RevCommit;
+
+import com.gitblit.Constants;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.wicket.WicketUtils;
+import com.gitblit.wicket.pages.CommitPage;
+
+public class CommitHeaderPanel extends BasePanel {
+
+ private static final long serialVersionUID = 1L;
+
+ public CommitHeaderPanel(String id, String title) {
+ super(id);
+ add(new Label("shortmessage", title));
+ add(new Label("commitid"));
+ add(new Label("author"));
+ add(new Label("date"));
+ }
+
+ public CommitHeaderPanel(String id, String repositoryName, RevCommit c) {
+ super(id);
+ add(new LinkPanel("shortmessage", "title", StringUtils.trimString(c.getShortMessage(),
+ Constants.LEN_SHORTLOG), CommitPage.class,
+ WicketUtils.newObjectParameter(repositoryName, c.getName())));
+ add(new Label("commitid", c.getName()));
+ add(new Label("author", c.getAuthorIdent().getName()));
+ add(WicketUtils.createDateLabel("date", c.getAuthorIdent().getWhen(), getTimeZone(), getTimeUtils()));
+ add(new GravatarImage("authorAvatar", c.getAuthorIdent()));
+ }
+} \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/panels/CommitLegendPanel.html b/src/main/java/com/gitblit/wicket/panels/CommitLegendPanel.html
new file mode 100644
index 00000000..71063626
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/CommitLegendPanel.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+
+<wicket:panel>
+ <div class="commitLegend" wicket:id="legend">
+ <span wicket:id="changeType">[change type]</span>
+ <span wicket:id="description">[description]</span>
+ </div>
+</wicket:panel>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/panels/CommitLegendPanel.java b/src/main/java/com/gitblit/wicket/panels/CommitLegendPanel.java
new file mode 100644
index 00000000..1e906ef7
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/CommitLegendPanel.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.panels;
+
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.panel.Panel;
+import org.apache.wicket.markup.repeater.Item;
+import org.apache.wicket.markup.repeater.data.DataView;
+import org.apache.wicket.markup.repeater.data.ListDataProvider;
+import org.eclipse.jgit.diff.DiffEntry.ChangeType;
+
+import com.gitblit.models.PathModel.PathChangeModel;
+import com.gitblit.wicket.WicketUtils;
+
+public class CommitLegendPanel extends Panel {
+
+ private static final long serialVersionUID = 1L;
+
+ public CommitLegendPanel(String id, List<PathChangeModel> paths) {
+ super(id);
+ final Map<ChangeType, AtomicInteger> stats = getChangedPathsStats(paths);
+ ListDataProvider<ChangeType> legendDp = new ListDataProvider<ChangeType>(
+ new ArrayList<ChangeType>(stats.keySet()));
+ DataView<ChangeType> legendsView = new DataView<ChangeType>("legend", legendDp) {
+ private static final long serialVersionUID = 1L;
+
+ public void populateItem(final Item<ChangeType> item) {
+ ChangeType entry = item.getModelObject();
+
+ Label changeType = new Label("changeType", "");
+ WicketUtils.setChangeTypeCssClass(changeType, entry);
+ item.add(changeType);
+ int count = stats.get(entry).intValue();
+ String description = "";
+ switch (entry) {
+ case ADD:
+ description = MessageFormat.format(getString("gb.filesAdded"), count);
+ break;
+ case MODIFY:
+ description = MessageFormat.format(getString("gb.filesModified"), count);
+ break;
+ case DELETE:
+ description = MessageFormat.format(getString("gb.filesDeleted"), count);
+ break;
+ case COPY:
+ description = MessageFormat.format(getString("gb.filesCopied"), count);
+ break;
+ case RENAME:
+ description = MessageFormat.format(getString("gb.filesRenamed"), count);
+ break;
+ }
+ item.add(new Label("description", description));
+ }
+ };
+ add(legendsView);
+ }
+
+ protected Map<ChangeType, AtomicInteger> getChangedPathsStats(List<PathChangeModel> paths) {
+ Map<ChangeType, AtomicInteger> stats = new HashMap<ChangeType, AtomicInteger>();
+ for (PathChangeModel path : paths) {
+ if (!stats.containsKey(path.changeType)) {
+ stats.put(path.changeType, new AtomicInteger(0));
+ }
+ stats.get(path.changeType).incrementAndGet();
+ }
+ return stats;
+ }
+} \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/panels/CompressedDownloadsPanel.html b/src/main/java/com/gitblit/wicket/panels/CompressedDownloadsPanel.html
new file mode 100644
index 00000000..7123d0a7
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/CompressedDownloadsPanel.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+
+<wicket:panel>
+ <span wicket:id="compressedLinks">
+ <span wicket:id="linkSep">|</span><span wicket:id="compressedLink">ref</span>
+ </span>
+</wicket:panel>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/panels/CompressedDownloadsPanel.java b/src/main/java/com/gitblit/wicket/panels/CompressedDownloadsPanel.java
new file mode 100644
index 00000000..b22c7587
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/CompressedDownloadsPanel.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2012 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.panels;
+
+import java.util.List;
+
+import org.apache.wicket.Component;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.panel.Panel;
+import org.apache.wicket.markup.repeater.Item;
+import org.apache.wicket.markup.repeater.data.DataView;
+import org.apache.wicket.markup.repeater.data.ListDataProvider;
+
+import com.gitblit.DownloadZipServlet;
+import com.gitblit.DownloadZipServlet.Format;
+import com.gitblit.GitBlit;
+import com.gitblit.Keys;
+
+public class CompressedDownloadsPanel extends Panel {
+
+ private static final long serialVersionUID = 1L;
+
+ public CompressedDownloadsPanel(String id, final String baseUrl, final String repositoryName, final String objectId, final String path) {
+ super(id);
+
+ List<String> types = GitBlit.getStrings(Keys.web.compressedDownloads);
+ if (types.isEmpty()) {
+ types.add(Format.zip.name());
+ types.add(Format.gz.name());
+ }
+
+ ListDataProvider<String> refsDp = new ListDataProvider<String>(types);
+ DataView<String> refsView = new DataView<String>("compressedLinks", refsDp) {
+ private static final long serialVersionUID = 1L;
+ int counter;
+
+ @Override
+ protected void onBeforeRender() {
+ super.onBeforeRender();
+ counter = 0;
+ }
+
+ @Override
+ public void populateItem(final Item<String> item) {
+ String compressionType = item.getModelObject();
+ Format format = Format.fromName(compressionType);
+
+ String href = DownloadZipServlet.asLink(baseUrl, repositoryName,
+ objectId, path, format);
+ Component c = new LinkPanel("compressedLink", null, format.name(), href);
+ item.add(c);
+ Label lb = new Label("linkSep", "|");
+ lb.setVisible(counter > 0);
+ lb.setRenderBodyOnly(true);
+ item.add(lb.setEscapeModelStrings(false));
+ item.setRenderBodyOnly(true);
+ counter++;
+ }
+ };
+ add(refsView);
+
+ setVisible(GitBlit.getBoolean(Keys.web.allowZipDownloads, true));
+ }
+} \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/panels/DropDownMenu.html b/src/main/java/com/gitblit/wicket/panels/DropDownMenu.html
new file mode 100644
index 00000000..0a8319ec
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/DropDownMenu.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+
+<wicket:panel>
+ <a class="dropdown-toggle" href="#" data-toggle="dropdown"><span wicket:id="label">label</span><b class="caret"></b></a>
+ <ul class="dropdown-menu">
+ <li wicket:id="menuItems"><span wicket:id="menuItem">[MenuItem]</span></li>
+ </ul>
+</wicket:panel>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/panels/DropDownMenu.java b/src/main/java/com/gitblit/wicket/panels/DropDownMenu.java
new file mode 100644
index 00000000..60a8a3d2
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/DropDownMenu.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.panels;
+
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.panel.Panel;
+import org.apache.wicket.markup.repeater.Item;
+import org.apache.wicket.markup.repeater.data.DataView;
+import org.apache.wicket.markup.repeater.data.ListDataProvider;
+
+import com.gitblit.wicket.PageRegistration.DropDownMenuItem;
+import com.gitblit.wicket.PageRegistration.DropDownMenuRegistration;
+import com.gitblit.wicket.WicketUtils;
+
+public class DropDownMenu extends Panel {
+
+ private static final long serialVersionUID = 1L;
+
+ public DropDownMenu(String id, String label, final DropDownMenuRegistration menu) {
+ super(id);
+
+ add(new Label("label", label).setRenderBodyOnly(true));
+ ListDataProvider<DropDownMenuItem> items = new ListDataProvider<DropDownMenuItem>(
+ menu.menuItems);
+ DataView<DropDownMenuItem> view = new DataView<DropDownMenuItem>("menuItems", items) {
+ private static final long serialVersionUID = 1L;
+
+ public void populateItem(final Item<DropDownMenuItem> item) {
+ DropDownMenuItem entry = item.getModelObject();
+ if (entry.isDivider()) {
+ item.add(new Label("menuItem").setRenderBodyOnly(true));
+ WicketUtils.setCssClass(item, "divider");
+ } else {
+ String icon = null;
+ if (entry.isSelected()) {
+ icon = "icon-ok";
+ } else {
+ icon = "icon-ok-white";
+ }
+ item.add(new LinkPanel("menuItem", icon, null, entry.toString(), menu.pageClass,
+ entry.getPageParameters(), false).setRenderBodyOnly(true));
+ }
+ }
+ };
+ add(view);
+ setRenderBodyOnly(true);
+ }
+} \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/panels/FederationProposalsPanel.html b/src/main/java/com/gitblit/wicket/panels/FederationProposalsPanel.html
new file mode 100644
index 00000000..89324d56
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/FederationProposalsPanel.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+
+<body>
+<wicket:panel>
+
+ <table class="repositories">
+ <tr>
+ <th class="left">
+ <img style="vertical-align: middle; border: 1px solid #888; background-color: white;" src="federated_16x16.png"/>
+ <wicket:message key="gb.proposals">[proposals]</wicket:message>
+ </th>
+ <th><wicket:message key="gb.received">[received]</wicket:message></th>
+ <th><wicket:message key="gb.type">[type]</wicket:message></th>
+ <th><wicket:message key="gb.token">[token]</wicket:message></th>
+ <th class="right"></th>
+ </tr>
+ <tbody>
+ <tr wicket:id="row">
+ <td class="left"><span class="list" wicket:id="url">[field]</span></td>
+ <td><span class="date"" wicket:id="received">[received]</span></td>
+ <td><span wicket:id="tokenType">[token type]</span></td>
+ <td><span class="sha1"" wicket:id="token">[token]</span></td>
+ <td class="rightAlign"><span class="link"><a wicket:id="deleteProposal"><wicket:message key="gb.delete">[delete]</wicket:message></a></span></td>
+ </tr>
+ </tbody>
+ </table>
+
+</wicket:panel>
+</body>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/panels/FederationProposalsPanel.java b/src/main/java/com/gitblit/wicket/panels/FederationProposalsPanel.java
new file mode 100644
index 00000000..3e70ccec
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/FederationProposalsPanel.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.panels;
+
+import java.text.MessageFormat;
+import java.util.List;
+
+import org.apache.wicket.Component;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.link.Link;
+import org.apache.wicket.markup.repeater.Item;
+import org.apache.wicket.markup.repeater.data.DataView;
+import org.apache.wicket.markup.repeater.data.ListDataProvider;
+
+import com.gitblit.GitBlit;
+import com.gitblit.models.FederationProposal;
+import com.gitblit.wicket.WicketUtils;
+import com.gitblit.wicket.pages.ReviewProposalPage;
+
+public class FederationProposalsPanel extends BasePanel {
+
+ private static final long serialVersionUID = 1L;
+
+ private final boolean hasProposals;
+
+ public FederationProposalsPanel(String wicketId) {
+ super(wicketId);
+
+ final List<FederationProposal> list = GitBlit.self().getPendingFederationProposals();
+ hasProposals = list.size() > 0;
+ DataView<FederationProposal> dataView = new DataView<FederationProposal>("row",
+ new ListDataProvider<FederationProposal>(list)) {
+ private static final long serialVersionUID = 1L;
+ private int counter;
+
+ @Override
+ protected void onBeforeRender() {
+ super.onBeforeRender();
+ counter = 0;
+ }
+
+ public void populateItem(final Item<FederationProposal> item) {
+ final FederationProposal entry = item.getModelObject();
+ item.add(new LinkPanel("url", "list", entry.url, ReviewProposalPage.class,
+ WicketUtils.newTokenParameter(entry.token)));
+ item.add(WicketUtils.createDateLabel("received", entry.received, getTimeZone(), getTimeUtils()));
+ item.add(new Label("tokenType", entry.tokenType.name()));
+ item.add(new LinkPanel("token", "list", entry.token, ReviewProposalPage.class,
+ WicketUtils.newTokenParameter(entry.token)));
+
+ Link<Void> deleteLink = new Link<Void>("deleteProposal") {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public void onClick() {
+ if (GitBlit.self().deletePendingFederationProposal(entry)) {
+ list.remove(entry);
+ info(MessageFormat.format("Proposal ''{0}'' deleted.", entry.name));
+ } else {
+ error(MessageFormat.format("Failed to delete proposal ''{0}''!",
+ entry.name));
+ }
+ }
+ };
+ deleteLink.add(new JavascriptEventConfirmation("onclick", MessageFormat.format(
+ "Delete proposal \"{0}\"?", entry.name)));
+ item.add(deleteLink);
+ WicketUtils.setAlternatingBackground(item, counter);
+ counter++;
+ }
+ };
+ add(dataView);
+ }
+
+ public Component hideIfEmpty() {
+ return super.setVisible(hasProposals);
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/panels/FederationRegistrationsPanel.html b/src/main/java/com/gitblit/wicket/panels/FederationRegistrationsPanel.html
new file mode 100644
index 00000000..fbc6f6cb
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/FederationRegistrationsPanel.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+
+<body>
+<wicket:panel>
+
+ <table class="repositories">
+ <tr>
+ <th class="left">
+ <img style="vertical-align: middle; border: 1px solid #888; background-color: white;" src="federated_16x16.png"/>
+ <wicket:message key="gb.registrations">[registrations]</wicket:message>
+ </th>
+ <th><wicket:message key="gb.name">[name]</wicket:message></th>
+ <th><wicket:message key="gb.frequency">[frequency]</wicket:message></th>
+ <th></th>
+ <th><wicket:message key="gb.lastPull">[lastPull]</wicket:message></th>
+ <th><wicket:message key="gb.nextPull">[nextPull]</wicket:message></th>
+ <th class="right"></th>
+ </tr>
+ <tbody>
+ <tr wicket:id="row">
+ <td class="left"><img style="border:0px;vertical-align:middle;" wicket:id="statusIcon" /><span class="list" wicket:id="url">[url]</span></td>
+ <td><span class="list" wicket:id="name">[name]</span></td>
+ <td><span wicket:id="frequency">[frequency]</span></td>
+ <td><img style="border:0px;vertical-align:middle;" wicket:id="typeIcon" /></td>
+ <td><span class="date"" wicket:id="lastPull">[lastPull]</span></td>
+ <td><span class="date"" wicket:id="nextPull">[nextPull]</span></td>
+ <td class="rightAlign"><span class="link"></span></td>
+ </tr>
+ </tbody>
+ </table>
+
+</wicket:panel>
+</body>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/panels/FederationRegistrationsPanel.java b/src/main/java/com/gitblit/wicket/panels/FederationRegistrationsPanel.java
new file mode 100644
index 00000000..ff947175
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/FederationRegistrationsPanel.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.panels;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.wicket.Component;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.repeater.Item;
+import org.apache.wicket.markup.repeater.data.DataView;
+import org.apache.wicket.markup.repeater.data.ListDataProvider;
+
+import com.gitblit.GitBlit;
+import com.gitblit.models.FederationModel;
+import com.gitblit.wicket.WicketUtils;
+import com.gitblit.wicket.pages.FederationRegistrationPage;
+
+public class FederationRegistrationsPanel extends BasePanel {
+
+ private static final long serialVersionUID = 1L;
+
+ private final boolean hasRegistrations;
+
+ public FederationRegistrationsPanel(String wicketId) {
+ super(wicketId);
+
+ final List<FederationModel> list = new ArrayList<FederationModel>(GitBlit.self()
+ .getFederationRegistrations());
+ list.addAll(GitBlit.self().getFederationResultRegistrations());
+ Collections.sort(list);
+ hasRegistrations = list.size() > 0;
+ DataView<FederationModel> dataView = new DataView<FederationModel>("row",
+ new ListDataProvider<FederationModel>(list)) {
+ private static final long serialVersionUID = 1L;
+ private int counter;
+
+ @Override
+ protected void onBeforeRender() {
+ super.onBeforeRender();
+ counter = 0;
+ }
+
+ public void populateItem(final Item<FederationModel> item) {
+ final FederationModel entry = item.getModelObject();
+ item.add(new LinkPanel("url", "list", entry.url, FederationRegistrationPage.class,
+ WicketUtils.newRegistrationParameter(entry.url, entry.name)));
+ item.add(WicketUtils.getPullStatusImage("statusIcon", entry.getLowestStatus()));
+ item.add(new LinkPanel("name", "list", entry.name,
+ FederationRegistrationPage.class, WicketUtils.newRegistrationParameter(
+ entry.url, entry.name)));
+
+ item.add(WicketUtils.getRegistrationImage("typeIcon", entry, this));
+
+ item.add(WicketUtils.createDateLabel("lastPull", entry.lastPull, getTimeZone(), getTimeUtils()));
+ item.add(WicketUtils
+ .createTimestampLabel("nextPull", entry.nextPull, getTimeZone(), getTimeUtils()));
+ item.add(new Label("frequency", entry.frequency));
+ WicketUtils.setAlternatingBackground(item, counter);
+ counter++;
+ }
+ };
+ add(dataView);
+ }
+
+ public Component hideIfEmpty() {
+ return super.setVisible(hasRegistrations);
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/panels/FederationTokensPanel.html b/src/main/java/com/gitblit/wicket/panels/FederationTokensPanel.html
new file mode 100644
index 00000000..dc5307bb
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/FederationTokensPanel.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+
+<body>
+<wicket:panel>
+
+ <div class="admin_nav">
+ <a wicket:id="federatedUsers"><wicket:message key="gb.federatedUserDefinitions">[users]</wicket:message></a>
+ | <a wicket:id="federatedSettings"><wicket:message key="gb.federatedSettingDefinitions">[settings]</wicket:message></a>
+ </div>
+
+ <table class="repositories">
+ <tr>
+ <th class="left">
+ <img style="vertical-align: middle; border: 1px solid #888; background-color: white;" src="federated_16x16.png"/>
+ <wicket:message key="gb.tokens">[tokens]</wicket:message>
+ </th>
+ <th></th>
+ <th class="right"></th>
+ </tr>
+ <tbody>
+ <tr wicket:id="row">
+ <td class="left"><span wicket:id="description"></span></td>
+ <td><span class="sha1"" wicket:id="value">[value]</span></td>
+ <td class="rightAlign"><span class="link"><a wicket:id="repositoryDefinitions"><wicket:message key="gb.federatedRepositoryDefinitions">[repository definitions]</wicket:message></a> | <a wicket:id="send"><wicket:message key="gb.sendProposal">[send proposal]</wicket:message></a></span></td>
+ </tr>
+ </tbody>
+ </table>
+
+</wicket:panel>
+</body>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/panels/FederationTokensPanel.java b/src/main/java/com/gitblit/wicket/panels/FederationTokensPanel.java
new file mode 100644
index 00000000..3454492f
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/FederationTokensPanel.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.panels;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.link.BookmarkablePageLink;
+import org.apache.wicket.markup.html.link.ExternalLink;
+import org.apache.wicket.markup.repeater.Item;
+import org.apache.wicket.markup.repeater.data.DataView;
+import org.apache.wicket.markup.repeater.data.ListDataProvider;
+
+import com.gitblit.Constants.FederationRequest;
+import com.gitblit.Constants.FederationToken;
+import com.gitblit.GitBlit;
+import com.gitblit.Keys;
+import com.gitblit.utils.FederationUtils;
+import com.gitblit.wicket.WicketUtils;
+import com.gitblit.wicket.pages.SendProposalPage;
+
+public class FederationTokensPanel extends BasePanel {
+
+ private static final long serialVersionUID = 1L;
+
+ public FederationTokensPanel(String wicketId, final boolean showFederation) {
+ super(wicketId);
+
+ final String baseUrl = WicketUtils.getGitblitURL(getRequest());
+ add(new ExternalLink("federatedUsers", FederationUtils.asLink(baseUrl, GitBlit.self()
+ .getFederationToken(FederationToken.USERS_AND_REPOSITORIES),
+ FederationRequest.PULL_USERS)));
+
+ add(new ExternalLink("federatedSettings", FederationUtils.asLink(baseUrl, GitBlit
+ .self().getFederationToken(FederationToken.ALL), FederationRequest.PULL_SETTINGS)));
+
+ final List<String[]> data = new ArrayList<String[]>();
+ for (FederationToken token : FederationToken.values()) {
+ data.add(new String[] { token.name(), GitBlit.self().getFederationToken(token), null });
+ }
+ List<String> sets = GitBlit.getStrings(Keys.federation.sets);
+ Collections.sort(sets);
+ for (String set : sets) {
+ data.add(new String[] { FederationToken.REPOSITORIES.name(),
+ GitBlit.self().getFederationToken(set), set });
+ }
+
+ DataView<String[]> dataView = new DataView<String[]>("row", new ListDataProvider<String[]>(
+ data)) {
+ private static final long serialVersionUID = 1L;
+ private int counter;
+
+ @Override
+ protected void onBeforeRender() {
+ super.onBeforeRender();
+ counter = 0;
+ }
+
+ public void populateItem(final Item<String[]> item) {
+ final String[] entry = item.getModelObject();
+ final FederationToken token = FederationToken.fromName(entry[0]);
+ if (entry[2] == null) {
+ // standard federation token
+ item.add(new Label("description", describeToken(token)));
+ } else {
+ // federation set token
+ item.add(new Label("description", entry[2]));
+ }
+ item.add(new Label("value", entry[1]));
+
+ item.add(new ExternalLink("repositoryDefinitions", FederationUtils.asLink(
+ baseUrl, entry[1], FederationRequest.PULL_REPOSITORIES)));
+
+ item.add(new BookmarkablePageLink<Void>("send",
+ SendProposalPage.class, WicketUtils.newTokenParameter(entry[1])));
+
+ WicketUtils.setAlternatingBackground(item, counter);
+ counter++;
+ }
+ };
+ add(dataView.setVisible(showFederation));
+ }
+
+ private String describeToken(FederationToken token) {
+ switch (token) {
+ case ALL:
+ return getString("gb.tokenAllDescription");
+ case USERS_AND_REPOSITORIES:
+ return getString("gb.tokenUnrDescription");
+ case REPOSITORIES:
+ default:
+ return getString("gb.tokenJurDescription");
+ }
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/panels/GravatarImage.html b/src/main/java/com/gitblit/wicket/panels/GravatarImage.html
new file mode 100644
index 00000000..9dda7958
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/GravatarImage.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+<wicket:panel>
+<a href="#" wicket:id="link"><img wicket:id="image"></img></a>
+</wicket:panel>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/panels/GravatarImage.java b/src/main/java/com/gitblit/wicket/panels/GravatarImage.java
new file mode 100644
index 00000000..7f1874f2
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/GravatarImage.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.panels;
+
+import java.text.MessageFormat;
+
+import org.apache.wicket.behavior.SimpleAttributeModifier;
+import org.apache.wicket.markup.html.link.BookmarkablePageLink;
+import org.apache.wicket.markup.html.link.Link;
+import org.apache.wicket.markup.html.panel.Panel;
+import org.eclipse.jgit.lib.PersonIdent;
+
+import com.gitblit.GitBlit;
+import com.gitblit.Keys;
+import com.gitblit.utils.ActivityUtils;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.wicket.ExternalImage;
+import com.gitblit.wicket.WicketUtils;
+import com.gitblit.wicket.pages.GravatarProfilePage;
+
+/**
+ * Represents a Gravatar image and links to the Gravatar profile page.
+ *
+ * @author James Moger
+ *
+ */
+public class GravatarImage extends Panel {
+
+ private static final long serialVersionUID = 1L;
+
+ public GravatarImage(String id, PersonIdent person) {
+ this(id, person, 0);
+ }
+
+ public GravatarImage(String id, PersonIdent person, int width) {
+ this(id, person, width, true);
+ }
+
+ public GravatarImage(String id, PersonIdent person, int width, boolean linked) {
+ super(id);
+
+ String email = person.getEmailAddress() == null ? person.getName().toLowerCase() : person.getEmailAddress().toLowerCase();
+ String hash = StringUtils.getMD5(email);
+ Link<Void> link = new BookmarkablePageLink<Void>("link", GravatarProfilePage.class,
+ WicketUtils.newObjectParameter(hash));
+ link.add(new SimpleAttributeModifier("target", "_blank"));
+ String url = ActivityUtils.getGravatarThumbnailUrl(email, width);
+ ExternalImage image = new ExternalImage("image", url);
+ WicketUtils.setCssClass(image, "gravatar");
+ link.add(image);
+ if (linked) {
+ WicketUtils.setHtmlTooltip(link,
+ MessageFormat.format("View Gravatar profile for {0}", person.getName()));
+ } else {
+ WicketUtils.setHtmlTooltip(link, person.getName());
+ }
+ add(link.setEnabled(linked));
+ setVisible(GitBlit.getBoolean(Keys.web.allowGravatar, true));
+ }
+} \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/panels/HistoryPanel.html b/src/main/java/com/gitblit/wicket/panels/HistoryPanel.html
new file mode 100644
index 00000000..811eeed1
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/HistoryPanel.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+
+<body>
+<wicket:panel>
+
+ <!-- commit header -->
+ <div wicket:id="commitHeader">[commit header]</div>
+
+ <!-- breadcrumbs -->
+ <div wicket:id="breadcrumbs">[breadcrumbs]</div>
+
+ <table class="pretty">
+ <tbody>
+ <tr wicket:id="commit">
+ <td class="date"><span wicket:id="commitDate">[commit date]</span></td>
+ <td class="icon"><img wicket:id="commitIcon" /></td>
+ <td class="hidden-phone author"><span wicket:id="commitAuthor">[commit author]</span></td>
+ <td class="message"><table class="nestedTable"><tr><td><span style="vertical-align:middle;" wicket:id="commitShortMessage">[commit short message]</span></td><td><div style="text-align:right;" wicket:id="commitRefs">[commit refs]</div></td></tr></table></td>
+ <td class="hidden-phone hidden-tablet rightAlign"><span class="link" wicket:id="hashLabel">[hash label]</span><span wicket:id="hashLink">[hash link]</span></td>
+ <td class="hidden-phone rightAlign">
+ <span wicket:id="historyLinks">[history links]</span>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ <div wicket:id="moreHistory">[more...]</div>
+
+ <!-- tree links -->
+ <wicket:fragment wicket:id="treeLinks">
+ <span class="link">
+ <a wicket:id="commitdiff"><wicket:message key="gb.commitdiff"></wicket:message></a>
+ </span>
+ </wicket:fragment>
+
+ <!-- blob links -->
+ <wicket:fragment wicket:id="blobLinks">
+ <span class="link">
+ <a wicket:id="commitdiff"><wicket:message key="gb.commitdiff"></wicket:message></a> | <a wicket:id="difftocurrent"><wicket:message key="gb.difftocurrent"></wicket:message></a>
+ </span>
+ </wicket:fragment>
+
+</wicket:panel>
+</body>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/panels/HistoryPanel.java b/src/main/java/com/gitblit/wicket/panels/HistoryPanel.java
new file mode 100644
index 00000000..e5878635
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/HistoryPanel.java
@@ -0,0 +1,335 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.panels;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.link.BookmarkablePageLink;
+import org.apache.wicket.markup.html.panel.Fragment;
+import org.apache.wicket.markup.repeater.Item;
+import org.apache.wicket.markup.repeater.data.DataView;
+import org.apache.wicket.markup.repeater.data.ListDataProvider;
+import org.apache.wicket.model.StringResourceModel;
+import org.eclipse.jgit.diff.DiffEntry.ChangeType;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
+
+import com.gitblit.Constants;
+import com.gitblit.GitBlit;
+import com.gitblit.Keys;
+import com.gitblit.models.PathModel;
+import com.gitblit.models.SubmoduleModel;
+import com.gitblit.models.PathModel.PathChangeModel;
+import com.gitblit.models.RefModel;
+import com.gitblit.utils.JGitUtils;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.wicket.WicketUtils;
+import com.gitblit.wicket.pages.BlobDiffPage;
+import com.gitblit.wicket.pages.BlobPage;
+import com.gitblit.wicket.pages.CommitDiffPage;
+import com.gitblit.wicket.pages.CommitPage;
+import com.gitblit.wicket.pages.GitSearchPage;
+import com.gitblit.wicket.pages.HistoryPage;
+import com.gitblit.wicket.pages.TreePage;
+
+public class HistoryPanel extends BasePanel {
+
+ private static final long serialVersionUID = 1L;
+
+ private boolean hasMore;
+
+ public HistoryPanel(String wicketId, final String repositoryName, final String objectId,
+ final String path, Repository r, int limit, int pageOffset, boolean showRemoteRefs) {
+ super(wicketId);
+ boolean pageResults = limit <= 0;
+ int itemsPerPage = GitBlit.getInteger(Keys.web.itemsPerPage, 50);
+ if (itemsPerPage <= 1) {
+ itemsPerPage = 50;
+ }
+
+ RevCommit commit = JGitUtils.getCommit(r, objectId);
+ List<PathChangeModel> paths = JGitUtils.getFilesInCommit(r, commit);
+
+ Map<String, SubmoduleModel> submodules = new HashMap<String, SubmoduleModel>();
+ for (SubmoduleModel model : JGitUtils.getSubmodules(r, commit.getTree())) {
+ submodules.put(model.path, model);
+ }
+
+ PathModel matchingPath = null;
+ for (PathModel p : paths) {
+ if (p.path.equals(path)) {
+ matchingPath = p;
+ break;
+ }
+ }
+ if (matchingPath == null) {
+ // path not in commit
+ // manually locate path in tree
+ TreeWalk tw = new TreeWalk(r);
+ tw.reset();
+ tw.setRecursive(true);
+ try {
+ tw.addTree(commit.getTree());
+ tw.setFilter(PathFilterGroup.createFromStrings(Collections.singleton(path)));
+ while (tw.next()) {
+ if (tw.getPathString().equals(path)) {
+ matchingPath = new PathChangeModel(tw.getPathString(), tw.getPathString(), 0, tw
+ .getRawMode(0), tw.getObjectId(0).getName(), commit.getId().getName(),
+ ChangeType.MODIFY);
+ }
+ }
+ } catch (Exception e) {
+ } finally {
+ tw.release();
+ }
+ }
+
+ final boolean isTree = matchingPath == null ? true : matchingPath.isTree();
+ final boolean isSubmodule = matchingPath == null ? true : matchingPath.isSubmodule();
+
+ // submodule
+ SubmoduleModel submodule = getSubmodule(submodules, repositoryName, matchingPath.path);
+ final String submodulePath;
+ final boolean hasSubmodule;
+ if (submodule != null) {
+ submodulePath = submodule.gitblitPath;
+ hasSubmodule = submodule.hasSubmodule;
+ } else {
+ submodulePath = "";
+ hasSubmodule = false;
+ }
+
+ final Map<ObjectId, List<RefModel>> allRefs = JGitUtils.getAllRefs(r, showRemoteRefs);
+ List<RevCommit> commits;
+ if (pageResults) {
+ // Paging result set
+ commits = JGitUtils.getRevLog(r, objectId, path, pageOffset * itemsPerPage,
+ itemsPerPage);
+ } else {
+ // Fixed size result set
+ commits = JGitUtils.getRevLog(r, objectId, path, 0, limit);
+ }
+
+ // inaccurate way to determine if there are more commits.
+ // works unless commits.size() represents the exact end.
+ hasMore = commits.size() >= itemsPerPage;
+
+ add(new CommitHeaderPanel("commitHeader", repositoryName, commit));
+
+ // breadcrumbs
+ add(new PathBreadcrumbsPanel("breadcrumbs", repositoryName, path, objectId));
+
+ final int hashLen = GitBlit.getInteger(Keys.web.shortCommitIdLength, 6);
+ ListDataProvider<RevCommit> dp = new ListDataProvider<RevCommit>(commits);
+ DataView<RevCommit> logView = new DataView<RevCommit>("commit", dp) {
+ private static final long serialVersionUID = 1L;
+ int counter;
+
+ public void populateItem(final Item<RevCommit> item) {
+ final RevCommit entry = item.getModelObject();
+ final Date date = JGitUtils.getCommitDate(entry);
+
+ item.add(WicketUtils.createDateLabel("commitDate", date, getTimeZone(), getTimeUtils()));
+
+ // author search link
+ String author = entry.getAuthorIdent().getName();
+ LinkPanel authorLink = new LinkPanel("commitAuthor", "list", author,
+ GitSearchPage.class,
+ WicketUtils.newSearchParameter(repositoryName, objectId,
+ author, Constants.SearchType.AUTHOR));
+ setPersonSearchTooltip(authorLink, author, Constants.SearchType.AUTHOR);
+ item.add(authorLink);
+
+ // merge icon
+ if (entry.getParentCount() > 1) {
+ item.add(WicketUtils.newImage("commitIcon", "commit_merge_16x16.png"));
+ } else {
+ item.add(WicketUtils.newBlankImage("commitIcon"));
+ }
+
+ String shortMessage = entry.getShortMessage();
+ String trimmedMessage = shortMessage;
+ if (allRefs.containsKey(entry.getId())) {
+ trimmedMessage = StringUtils.trimString(shortMessage, Constants.LEN_SHORTLOG_REFS);
+ } else {
+ trimmedMessage = StringUtils.trimString(shortMessage, Constants.LEN_SHORTLOG);
+ }
+ LinkPanel shortlog = new LinkPanel("commitShortMessage", "list subject",
+ trimmedMessage, CommitPage.class, WicketUtils.newObjectParameter(
+ repositoryName, entry.getName()));
+ if (!shortMessage.equals(trimmedMessage)) {
+ WicketUtils.setHtmlTooltip(shortlog, shortMessage);
+ }
+ item.add(shortlog);
+
+ item.add(new RefsPanel("commitRefs", repositoryName, entry, allRefs));
+
+ if (isTree) {
+ // tree
+ item.add(new Label("hashLabel", getString("gb.tree") + "@"));
+ LinkPanel commitHash = new LinkPanel("hashLink", null, entry.getName().substring(0, hashLen),
+ TreePage.class, WicketUtils.newObjectParameter(
+ repositoryName, entry.getName()));
+ WicketUtils.setCssClass(commitHash, "shortsha1");
+ WicketUtils.setHtmlTooltip(commitHash, entry.getName());
+ item.add(commitHash);
+
+ Fragment links = new Fragment("historyLinks", "treeLinks", this);
+ links.add(new BookmarkablePageLink<Void>("commitdiff", CommitDiffPage.class,
+ WicketUtils.newObjectParameter(repositoryName, entry.getName())));
+ item.add(links);
+ } else if (isSubmodule) {
+ // submodule
+ item.add(new Label("hashLabel", submodulePath + "@"));
+ Repository repository = GitBlit.self().getRepository(repositoryName);
+ String submoduleId = JGitUtils.getSubmoduleCommitId(repository, path, entry);
+ repository.close();
+ LinkPanel commitHash = new LinkPanel("hashLink", null, submoduleId.substring(0, hashLen),
+ TreePage.class, WicketUtils.newObjectParameter(
+ submodulePath, submoduleId));
+ WicketUtils.setCssClass(commitHash, "shortsha1");
+ WicketUtils.setHtmlTooltip(commitHash, submoduleId);
+ item.add(commitHash.setEnabled(hasSubmodule));
+
+ Fragment links = new Fragment("historyLinks", "treeLinks", this);
+ links.add(new BookmarkablePageLink<Void>("commitdiff", CommitDiffPage.class,
+ WicketUtils.newObjectParameter(repositoryName, entry.getName())));
+ item.add(links);
+ } else {
+ // commit
+ item.add(new Label("hashLabel", getString("gb.blob") + "@"));
+ LinkPanel commitHash = new LinkPanel("hashLink", null, entry.getName().substring(0, hashLen),
+ BlobPage.class, WicketUtils.newPathParameter(
+ repositoryName, entry.getName(), path));
+ WicketUtils.setCssClass(commitHash, "sha1");
+ WicketUtils.setHtmlTooltip(commitHash, entry.getName());
+ item.add(commitHash);
+
+ Fragment links = new Fragment("historyLinks", "blobLinks", this);
+ links.add(new BookmarkablePageLink<Void>("commitdiff", CommitDiffPage.class,
+ WicketUtils.newObjectParameter(repositoryName, entry.getName())));
+ links.add(new BookmarkablePageLink<Void>("difftocurrent", BlobDiffPage.class,
+ WicketUtils.newBlobDiffParameter(repositoryName, entry.getName(),
+ objectId, path)).setEnabled(counter > 0));
+ item.add(links);
+ }
+
+ WicketUtils.setAlternatingBackground(item, counter);
+ counter++;
+ }
+ };
+ add(logView);
+
+ // determine to show pager, more, or neither
+ if (limit <= 0) {
+ // no display limit
+ add(new Label("moreHistory", "").setVisible(false));
+ } else {
+ if (pageResults) {
+ // paging
+ add(new Label("moreHistory", "").setVisible(false));
+ } else {
+ // more
+ if (commits.size() == limit) {
+ // show more
+ add(new LinkPanel("moreHistory", "link", new StringResourceModel(
+ "gb.moreHistory", this, null), HistoryPage.class,
+ WicketUtils.newPathParameter(repositoryName, objectId, path)));
+ } else {
+ // no more
+ add(new Label("moreHistory", "").setVisible(false));
+ }
+ }
+ }
+ }
+
+ public boolean hasMore() {
+ return hasMore;
+ }
+
+ protected SubmoduleModel getSubmodule(Map<String, SubmoduleModel> submodules, String repositoryName, String path) {
+ SubmoduleModel model = submodules.get(path);
+ if (model == null) {
+ // undefined submodule?!
+ model = new SubmoduleModel(path.substring(path.lastIndexOf('/') + 1), path, path);
+ model.hasSubmodule = false;
+ model.gitblitPath = model.name;
+ return model;
+ } else {
+ // extract the repository name from the clone url
+ List<String> patterns = GitBlit.getStrings(Keys.git.submoduleUrlPatterns);
+ String submoduleName = StringUtils.extractRepositoryPath(model.url, patterns.toArray(new String[0]));
+
+ // determine the current path for constructing paths relative
+ // to the current repository
+ String currentPath = "";
+ if (repositoryName.indexOf('/') > -1) {
+ currentPath = repositoryName.substring(0, repositoryName.lastIndexOf('/') + 1);
+ }
+
+ // try to locate the submodule repository
+ // prefer bare to non-bare names
+ List<String> candidates = new ArrayList<String>();
+
+ // relative
+ candidates.add(currentPath + StringUtils.stripDotGit(submoduleName));
+ candidates.add(candidates.get(candidates.size() - 1) + ".git");
+
+ // relative, no subfolder
+ if (submoduleName.lastIndexOf('/') > -1) {
+ String name = submoduleName.substring(submoduleName.lastIndexOf('/') + 1);
+ candidates.add(currentPath + StringUtils.stripDotGit(name));
+ candidates.add(currentPath + candidates.get(candidates.size() - 1) + ".git");
+ }
+
+ // absolute
+ candidates.add(StringUtils.stripDotGit(submoduleName));
+ candidates.add(candidates.get(candidates.size() - 1) + ".git");
+
+ // absolute, no subfolder
+ if (submoduleName.lastIndexOf('/') > -1) {
+ String name = submoduleName.substring(submoduleName.lastIndexOf('/') + 1);
+ candidates.add(StringUtils.stripDotGit(name));
+ candidates.add(candidates.get(candidates.size() - 1) + ".git");
+ }
+
+ // create a unique, ordered set of candidate paths
+ Set<String> paths = new LinkedHashSet<String>(candidates);
+ for (String candidate : paths) {
+ if (GitBlit.self().hasRepository(candidate)) {
+ model.hasSubmodule = true;
+ model.gitblitPath = candidate;
+ return model;
+ }
+ }
+
+ // we do not have a copy of the submodule, but we need a path
+ model.gitblitPath = candidates.get(0);
+ return model;
+ }
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/panels/LinkPanel.html b/src/main/java/com/gitblit/wicket/panels/LinkPanel.html
new file mode 100644
index 00000000..714d53ab
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/LinkPanel.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+<wicket:panel>
+<a href="#" wicket:id="link"><i wicket:id="icon" style="padding-right:5px;"></i><span wicket:id="label">[link]</span></a>
+</wicket:panel>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/panels/LinkPanel.java b/src/main/java/com/gitblit/wicket/panels/LinkPanel.java
new file mode 100644
index 00000000..688a9576
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/LinkPanel.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.panels;
+
+import org.apache.wicket.PageParameters;
+import org.apache.wicket.behavior.SimpleAttributeModifier;
+import org.apache.wicket.markup.html.WebPage;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.link.BookmarkablePageLink;
+import org.apache.wicket.markup.html.link.ExternalLink;
+import org.apache.wicket.markup.html.link.Link;
+import org.apache.wicket.markup.html.panel.Panel;
+import org.apache.wicket.model.IModel;
+import org.apache.wicket.model.Model;
+
+import com.gitblit.utils.StringUtils;
+import com.gitblit.wicket.WicketUtils;
+
+public class LinkPanel extends Panel {
+
+ private static final long serialVersionUID = 1L;
+
+ private final IModel<String> labelModel;
+
+ public LinkPanel(String wicketId, String linkCssClass, String label,
+ Class<? extends WebPage> clazz) {
+ this(wicketId, null, linkCssClass, new Model<String>(label), clazz, null, false);
+ }
+
+ public LinkPanel(String wicketId, String linkCssClass, String label,
+ Class<? extends WebPage> clazz, PageParameters parameters) {
+ this(wicketId, null, linkCssClass, new Model<String>(label), clazz, parameters, false);
+ }
+
+ public LinkPanel(String wicketId, String linkCssClass, String label,
+ Class<? extends WebPage> clazz, PageParameters parameters, boolean newWindow) {
+ this(wicketId, null, linkCssClass, new Model<String>(label), clazz, parameters, newWindow);
+ }
+
+ public LinkPanel(String wicketId, String bootstrapIcon, String linkCssClass, String label,
+ Class<? extends WebPage> clazz, PageParameters parameters, boolean newWindow) {
+ this(wicketId, bootstrapIcon, linkCssClass, new Model<String>(label), clazz, parameters, newWindow);
+ }
+
+ public LinkPanel(String wicketId, String linkCssClass, IModel<String> model,
+ Class<? extends WebPage> clazz, PageParameters parameters) {
+ this(wicketId, null, linkCssClass, model, clazz, parameters, false);
+ }
+
+ public LinkPanel(String wicketId, String bootstrapIcon, String linkCssClass, IModel<String> model,
+ Class<? extends WebPage> clazz, PageParameters parameters, boolean newWindow) {
+ super(wicketId);
+ this.labelModel = model;
+ Link<Void> link = null;
+ if (parameters == null) {
+ link = new BookmarkablePageLink<Void>("link", clazz);
+ } else {
+ link = new BookmarkablePageLink<Void>("link", clazz, parameters);
+ }
+ if (newWindow) {
+ link.add(new SimpleAttributeModifier("target", "_blank"));
+ }
+ if (linkCssClass != null) {
+ link.add(new SimpleAttributeModifier("class", linkCssClass));
+ }
+ Label icon = new Label("icon");
+ if (StringUtils.isEmpty(bootstrapIcon)) {
+ link.add(icon.setVisible(false));
+ } else {
+ WicketUtils.setCssClass(icon, bootstrapIcon);
+ link.add(icon);
+ }
+ link.add(new Label("label", labelModel).setRenderBodyOnly(true));
+ add(link);
+ }
+
+ public LinkPanel(String wicketId, String linkCssClass, String label, String href) {
+ this(wicketId, linkCssClass, label, href, false);
+ }
+
+ public LinkPanel(String wicketId, String linkCssClass, String label, String href,
+ boolean newWindow) {
+ super(wicketId);
+ this.labelModel = new Model<String>(label);
+ ExternalLink link = new ExternalLink("link", href);
+ if (newWindow) {
+ link.add(new SimpleAttributeModifier("target", "_blank"));
+ }
+ if (linkCssClass != null) {
+ link.add(new SimpleAttributeModifier("class", linkCssClass));
+ }
+ link.add(new Label("icon").setVisible(false));
+ link.add(new Label("label", labelModel));
+ add(link);
+ }
+
+}
diff --git a/src/main/java/com/gitblit/wicket/panels/LogPanel.html b/src/main/java/com/gitblit/wicket/panels/LogPanel.html
new file mode 100644
index 00000000..2b2605ac
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/LogPanel.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+
+<body>
+<wicket:panel>
+
+ <!-- header -->
+ <div class="header"><i class="icon-refresh" style="vertical-align: middle;"></i> <b><span wicket:id="header">[log header]</span></b></div>
+ <table class="pretty">
+ <tbody>
+ <tr wicket:id="commit">
+ <td class="date" style="width:6em;"><span wicket:id="commitDate">[commit date]</span></td>
+ <td class="hidden-phone author"><span wicket:id="commitAuthor">[commit author]</span></td>
+ <td class="hidden-phone icon"><img wicket:id="commitIcon" /></td>
+ <td class="message"><table class="nestedTable"><tr><td><span style="vertical-align:middle;" wicket:id="commitShortMessage">[commit short message]</span></td><td><div style="text-align:right;" wicket:id="commitRefs">[commit refs]</div></td></tr></table></td>
+ <td class="hidden-phone hidden-tablet rightAlign"><span wicket:id="hashLink">[hash link]</span></td>
+ <td class="hidden-phone hidden-tablet rightAlign">
+ <span class="link">
+ <a wicket:id="diff"><wicket:message key="gb.diff"></wicket:message></a> | <a wicket:id="tree"><wicket:message key="gb.tree"></wicket:message></a>
+ </span>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ <div wicket:id="moreLogs">[more...]</div>
+
+</wicket:panel>
+</body>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/panels/LogPanel.java b/src/main/java/com/gitblit/wicket/panels/LogPanel.java
new file mode 100644
index 00000000..05397642
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/LogPanel.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.panels;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.link.BookmarkablePageLink;
+import org.apache.wicket.markup.repeater.Item;
+import org.apache.wicket.markup.repeater.data.DataView;
+import org.apache.wicket.markup.repeater.data.ListDataProvider;
+import org.apache.wicket.model.StringResourceModel;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+
+import com.gitblit.Constants;
+import com.gitblit.GitBlit;
+import com.gitblit.Keys;
+import com.gitblit.models.RefModel;
+import com.gitblit.utils.JGitUtils;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.wicket.WicketUtils;
+import com.gitblit.wicket.pages.CommitDiffPage;
+import com.gitblit.wicket.pages.CommitPage;
+import com.gitblit.wicket.pages.LogPage;
+import com.gitblit.wicket.pages.GitSearchPage;
+import com.gitblit.wicket.pages.TreePage;
+
+public class LogPanel extends BasePanel {
+
+ private static final long serialVersionUID = 1L;
+
+ private boolean hasMore;
+
+ public LogPanel(String wicketId, final String repositoryName, final String objectId,
+ Repository r, int limit, int pageOffset, boolean showRemoteRefs) {
+ super(wicketId);
+ boolean pageResults = limit <= 0;
+ int itemsPerPage = GitBlit.getInteger(Keys.web.itemsPerPage, 50);
+ if (itemsPerPage <= 1) {
+ itemsPerPage = 50;
+ }
+
+ final Map<ObjectId, List<RefModel>> allRefs = JGitUtils.getAllRefs(r, showRemoteRefs);
+ List<RevCommit> commits;
+ if (pageResults) {
+ // Paging result set
+ commits = JGitUtils.getRevLog(r, objectId, pageOffset * itemsPerPage, itemsPerPage);
+ } else {
+ // Fixed size result set
+ commits = JGitUtils.getRevLog(r, objectId, 0, limit);
+ }
+
+ // inaccurate way to determine if there are more commits.
+ // works unless commits.size() represents the exact end.
+ hasMore = commits.size() >= itemsPerPage;
+
+ // header
+ if (pageResults) {
+ // shortlog page
+ add(new Label("header", objectId));
+ } else {
+ // summary page
+ // show shortlog page link
+ add(new LinkPanel("header", "title", objectId, LogPage.class,
+ WicketUtils.newRepositoryParameter(repositoryName)));
+ }
+
+ final int hashLen = GitBlit.getInteger(Keys.web.shortCommitIdLength, 6);
+ ListDataProvider<RevCommit> dp = new ListDataProvider<RevCommit>(commits);
+ DataView<RevCommit> logView = new DataView<RevCommit>("commit", dp) {
+ private static final long serialVersionUID = 1L;
+ int counter;
+
+ public void populateItem(final Item<RevCommit> item) {
+ final RevCommit entry = item.getModelObject();
+ final Date date = JGitUtils.getCommitDate(entry);
+
+ item.add(WicketUtils.createDateLabel("commitDate", date, getTimeZone(), getTimeUtils()));
+
+ // author search link
+ String author = entry.getAuthorIdent().getName();
+ LinkPanel authorLink = new LinkPanel("commitAuthor", "list", author,
+ GitSearchPage.class, WicketUtils.newSearchParameter(repositoryName,
+ objectId, author, Constants.SearchType.AUTHOR));
+ setPersonSearchTooltip(authorLink, author, Constants.SearchType.AUTHOR);
+ item.add(authorLink);
+
+ // merge icon
+ if (entry.getParentCount() > 1) {
+ item.add(WicketUtils.newImage("commitIcon", "commit_merge_16x16.png"));
+ } else {
+ item.add(WicketUtils.newBlankImage("commitIcon"));
+ }
+
+ // short message
+ String shortMessage = entry.getShortMessage();
+ String trimmedMessage = shortMessage;
+ if (allRefs.containsKey(entry.getId())) {
+ trimmedMessage = StringUtils.trimString(shortMessage, Constants.LEN_SHORTLOG_REFS);
+ } else {
+ trimmedMessage = StringUtils.trimString(shortMessage, Constants.LEN_SHORTLOG);
+ }
+ LinkPanel shortlog = new LinkPanel("commitShortMessage", "list subject",
+ trimmedMessage, CommitPage.class, WicketUtils.newObjectParameter(
+ repositoryName, entry.getName()));
+ if (!shortMessage.equals(trimmedMessage)) {
+ WicketUtils.setHtmlTooltip(shortlog, shortMessage);
+ }
+ item.add(shortlog);
+
+ item.add(new RefsPanel("commitRefs", repositoryName, entry, allRefs));
+
+ // commit hash link
+ LinkPanel commitHash = new LinkPanel("hashLink", null, entry.getName().substring(0, hashLen),
+ CommitPage.class, WicketUtils.newObjectParameter(
+ repositoryName, entry.getName()));
+ WicketUtils.setCssClass(commitHash, "shortsha1");
+ WicketUtils.setHtmlTooltip(commitHash, entry.getName());
+ item.add(commitHash);
+
+ item.add(new BookmarkablePageLink<Void>("diff", CommitDiffPage.class, WicketUtils
+ .newObjectParameter(repositoryName, entry.getName())).setEnabled(entry
+ .getParentCount() > 0));
+ item.add(new BookmarkablePageLink<Void>("tree", TreePage.class, WicketUtils
+ .newObjectParameter(repositoryName, entry.getName())));
+
+ WicketUtils.setAlternatingBackground(item, counter);
+ counter++;
+ }
+ };
+ add(logView);
+
+ // determine to show pager, more, or neither
+ if (limit <= 0) {
+ // no display limit
+ add(new Label("moreLogs", "").setVisible(false));
+ } else {
+ if (pageResults) {
+ // paging
+ add(new Label("moreLogs", "").setVisible(false));
+ } else {
+ // more
+ if (commits.size() == limit) {
+ // show more
+ add(new LinkPanel("moreLogs", "link", new StringResourceModel("gb.moreLogs",
+ this, null), LogPage.class,
+ WicketUtils.newRepositoryParameter(repositoryName)));
+ } else {
+ // no more
+ add(new Label("moreLogs", "").setVisible(false));
+ }
+ }
+ }
+ }
+
+ public boolean hasMore() {
+ return hasMore;
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/panels/NavigationPanel.html b/src/main/java/com/gitblit/wicket/panels/NavigationPanel.html
new file mode 100644
index 00000000..f883e49d
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/NavigationPanel.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+
+<wicket:panel>
+ <ul class="nav">
+ <li wicket:id="navLink"><span wicket:id="link">[link]</span></li>
+ </ul>
+</wicket:panel>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/panels/NavigationPanel.java b/src/main/java/com/gitblit/wicket/panels/NavigationPanel.java
new file mode 100644
index 00000000..558cc716
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/NavigationPanel.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.panels;
+
+import java.util.List;
+
+import org.apache.wicket.Component;
+import org.apache.wicket.markup.html.panel.Panel;
+import org.apache.wicket.markup.repeater.Item;
+import org.apache.wicket.markup.repeater.data.DataView;
+import org.apache.wicket.markup.repeater.data.ListDataProvider;
+
+import com.gitblit.wicket.PageRegistration;
+import com.gitblit.wicket.PageRegistration.DropDownMenuRegistration;
+import com.gitblit.wicket.PageRegistration.OtherPageLink;
+import com.gitblit.wicket.WicketUtils;
+import com.gitblit.wicket.pages.BasePage;
+
+public class NavigationPanel extends Panel {
+
+ private static final long serialVersionUID = 1L;
+
+ public NavigationPanel(String id, final Class<? extends BasePage> pageClass,
+ List<PageRegistration> registeredPages) {
+ super(id);
+
+ ListDataProvider<PageRegistration> refsDp = new ListDataProvider<PageRegistration>(
+ registeredPages);
+ DataView<PageRegistration> refsView = new DataView<PageRegistration>("navLink", refsDp) {
+ private static final long serialVersionUID = 1L;
+
+ public void populateItem(final Item<PageRegistration> item) {
+ PageRegistration entry = item.getModelObject();
+ if (entry instanceof OtherPageLink) {
+ // other link
+ OtherPageLink link = (OtherPageLink) entry;
+ Component c = new LinkPanel("link", null, getString(entry.translationKey), link.url);
+ c.setRenderBodyOnly(true);
+ item.add(c);
+ } else if (entry instanceof DropDownMenuRegistration) {
+ // drop down menu
+ DropDownMenuRegistration reg = (DropDownMenuRegistration) entry;
+ Component c = new DropDownMenu("link", getString(entry.translationKey), reg);
+ c.setRenderBodyOnly(true);
+ item.add(c);
+ WicketUtils.setCssClass(item, "dropdown");
+ } else {
+ // standard page link
+ Component c = new LinkPanel("link", null, getString(entry.translationKey),
+ entry.pageClass, entry.params);
+ c.setRenderBodyOnly(true);
+ if (entry.pageClass.equals(pageClass)) {
+ WicketUtils.setCssClass(item, "active");
+ }
+ item.add(c);
+ }
+ }
+ };
+ add(refsView);
+ }
+} \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/panels/ObjectContainer.java b/src/main/java/com/gitblit/wicket/panels/ObjectContainer.java
new file mode 100644
index 00000000..d7f1f789
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/ObjectContainer.java
@@ -0,0 +1,158 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.panels;
+
+import java.util.List;
+
+import org.apache.wicket.Component;
+import org.apache.wicket.ResourceReference;
+import org.apache.wicket.Response;
+import org.apache.wicket.markup.ComponentTag;
+import org.apache.wicket.markup.MarkupStream;
+import org.apache.wicket.markup.html.WebMarkupContainer;
+import org.apache.wicket.markup.html.panel.Fragment;
+import org.apache.wicket.protocol.http.ClientProperties;
+import org.apache.wicket.protocol.http.WebRequestCycle;
+import org.apache.wicket.protocol.http.WebSession;
+import org.apache.wicket.protocol.http.request.WebClientInfo;
+import org.apache.wicket.request.ClientInfo;
+import org.apache.wicket.util.value.IValueMap;
+
+/**
+ * https://cwiki.apache.org/WICKET/object-container-adding-flash-to-a-wicket-application.html
+ */
+public abstract class ObjectContainer extends WebMarkupContainer {
+
+ private static final long serialVersionUID = 1L;
+
+ // Some general attributes for the object tag:
+ private static final String ATTRIBUTE_CONTENTTYPE = "type";
+ private static final String ATTRIBUTE_CLASSID = "classid";
+ private static final String ATTRIBUTE_CODEBASE = "codebase";
+
+ // This is used for browser specific adjustments
+ private ClientProperties clientProperties = null;
+
+ public ObjectContainer(String id) {
+ super(id);
+ }
+
+ // Set an attribute/property
+ public abstract void setValue(String name, String value);
+
+ // Get an attribute/property
+ public abstract String getValue(String name);
+
+ // Set the object's content type
+ protected abstract String getContentType();
+
+ // Set the object's clsid (for IE)
+ protected abstract String getClsid();
+
+ // Where to get the browser plugin (for IE)
+ protected abstract String getCodebase();
+
+ // Object's valid attribute names
+ protected abstract List<String> getAttributeNames();
+
+ // Object's valid parameter names
+ protected abstract List<String> getParameterNames();
+
+ // Utility function to get the URL for the object's data
+ protected String resolveResource(String src) {
+ // if it's an absolute path, return it:
+ if (src.startsWith("/") || src.startsWith("http://") || src.startsWith("https://"))
+ return (src);
+
+ // use the parent container class to resolve the resource reference
+ Component parent = getParent();
+ if (parent instanceof Fragment) {
+ // must check for fragment, otherwise we end up in Wicket namespace
+ parent = parent.getParent();
+ }
+ if (parent != null) {
+ ResourceReference resRef = new ResourceReference(parent.getClass(), src, false);
+ return (urlFor(resRef).toString());
+ }
+
+ return (src);
+ }
+
+ public void onComponentTag(ComponentTag tag) {
+ super.onComponentTag(tag);
+
+ // get the attributes from the html-source
+ IValueMap attributeMap = tag.getAttributes();
+
+ // set the content type
+ String contentType = getContentType();
+ if (contentType != null && !"".equals(contentType))
+ attributeMap.put(ATTRIBUTE_CONTENTTYPE, contentType);
+
+ // set clsid and codebase for IE
+ if (getClientProperties().isBrowserInternetExplorer()) {
+ String clsid = getClsid();
+ String codeBase = getCodebase();
+
+ if (clsid != null && !"".equals(clsid))
+ attributeMap.put(ATTRIBUTE_CLASSID, clsid);
+ if (codeBase != null && !"".equals(codeBase))
+ attributeMap.put(ATTRIBUTE_CODEBASE, codeBase);
+ }
+
+ // add all attributes
+ for (String name : getAttributeNames()) {
+ String value = getValue(name);
+ if (value != null)
+ attributeMap.put(name, value);
+ }
+ }
+
+ public void onComponentTagBody(MarkupStream markupStream, ComponentTag openTag) {
+ Response response = getResponse();
+ response.write("\n");
+
+ // add all object's parameters:
+ for (String name : getParameterNames()) {
+ String value = getValue(name);
+ if (value != null) {
+ response.write("<param name=\"");
+ response.write(name);
+ response.write("\" value=\"");
+ response.write(value);
+ response.write("\"/>\n");
+ }
+ }
+
+ super.onComponentTagBody(markupStream, openTag);
+ }
+
+ // shortcut to the client properties:
+ protected ClientProperties getClientProperties() {
+ if (clientProperties == null) {
+ ClientInfo clientInfo = WebSession.get().getClientInfo();
+
+ if (clientInfo == null || !(clientInfo instanceof WebClientInfo)) {
+ clientInfo = new WebClientInfo((WebRequestCycle) getRequestCycle());
+ WebSession.get().setClientInfo(clientInfo);
+ }
+
+ clientProperties = ((WebClientInfo) clientInfo).getProperties();
+ }
+ return (clientProperties);
+ }
+} \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/panels/PagerPanel.html b/src/main/java/com/gitblit/wicket/panels/PagerPanel.html
new file mode 100644
index 00000000..099001db
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/PagerPanel.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+<wicket:panel>
+ <div class="pagination pagination-right" style="margin: 0px;">
+ <ul>
+ <li wicket:id="page"><span wicket:id="pageLink"></span></li>
+ </ul>
+ </div>
+</wicket:panel>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/panels/PagerPanel.java b/src/main/java/com/gitblit/wicket/panels/PagerPanel.java
new file mode 100644
index 00000000..a5dbb9ef
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/PagerPanel.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2012 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.panels;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.wicket.PageParameters;
+import org.apache.wicket.markup.html.panel.Panel;
+import org.apache.wicket.markup.repeater.Item;
+import org.apache.wicket.markup.repeater.data.DataView;
+import org.apache.wicket.markup.repeater.data.ListDataProvider;
+
+import com.gitblit.wicket.WicketUtils;
+import com.gitblit.wicket.pages.BasePage;
+
+public class PagerPanel extends Panel {
+
+ private static final long serialVersionUID = 1L;
+
+ public PagerPanel(String wicketId, final int currentPage, final int totalPages,
+ final Class<? extends BasePage> pageClass, final PageParameters baseParams) {
+ super(wicketId);
+ List<PageObject> pages = new ArrayList<PageObject>();
+ int[] deltas;
+ if (currentPage == 1) {
+ // [1], 2, 3, 4, 5
+ deltas = new int[] { 0, 1, 2, 3, 4 };
+ } else if (currentPage == 2) {
+ // 1, [2], 3, 4, 5
+ deltas = new int[] { -1, 0, 1, 2, 3 };
+ } else {
+ // 1, 2, [3], 4, 5
+ deltas = new int[] { -2, -1, 0, 1, 2 };
+ }
+
+ if (totalPages > 0) {
+ pages.add(new PageObject("\u2190", currentPage - 1));
+ }
+ for (int delta : deltas) {
+ int page = currentPage + delta;
+ if (page > 0 && page <= totalPages) {
+ pages.add(new PageObject("" + page, page));
+ }
+ }
+ if (totalPages > 0) {
+ pages.add(new PageObject("\u2192", currentPage + 1));
+ }
+
+ ListDataProvider<PageObject> pagesProvider = new ListDataProvider<PageObject>(pages);
+ final DataView<PageObject> pagesView = new DataView<PageObject>("page", pagesProvider) {
+ private static final long serialVersionUID = 1L;
+
+ public void populateItem(final Item<PageObject> item) {
+ PageObject pageItem = item.getModelObject();
+ PageParameters pageParams = new PageParameters(baseParams);
+ pageParams.put("pg", pageItem.page);
+ LinkPanel link = new LinkPanel("pageLink", null, pageItem.text, pageClass, pageParams);
+ link.setRenderBodyOnly(true);
+ item.add(link);
+ if (pageItem.page == currentPage || pageItem.page < 1 || pageItem.page > totalPages) {
+ WicketUtils.setCssClass(item, "disabled");
+ }
+ }
+ };
+ add(pagesView);
+ }
+
+ private class PageObject implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ String text;
+ int page;
+
+ PageObject(String text, int page) {
+ this.text = text;
+ this.page = page;
+ }
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/panels/PathBreadcrumbsPanel.html b/src/main/java/com/gitblit/wicket/panels/PathBreadcrumbsPanel.html
new file mode 100644
index 00000000..c51ceac8
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/PathBreadcrumbsPanel.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+
+<wicket:panel>
+ <!-- page path links -->
+ <div class="page_path">
+ <ul class="breadcrumb">
+ <li wicket:id="path">
+ <span wicket:id="pathLink"></span> <span class="divider" wicket:id="pathSeparator"></span>
+ </li>
+ </ul>
+ </div>
+</wicket:panel>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/panels/PathBreadcrumbsPanel.java b/src/main/java/com/gitblit/wicket/panels/PathBreadcrumbsPanel.java
new file mode 100644
index 00000000..f6c0e4f8
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/PathBreadcrumbsPanel.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.panels;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.panel.Panel;
+import org.apache.wicket.markup.repeater.Item;
+import org.apache.wicket.markup.repeater.data.DataView;
+import org.apache.wicket.markup.repeater.data.ListDataProvider;
+
+import com.gitblit.wicket.WicketUtils;
+import com.gitblit.wicket.pages.TreePage;
+
+public class PathBreadcrumbsPanel extends Panel {
+
+ private static final long serialVersionUID = 1L;
+
+ private static final String ROOT = "--ROOT--";
+
+ public PathBreadcrumbsPanel(String id, final String repositoryName, String pathName,
+ final String objectId) {
+ super(id);
+ List<BreadCrumb> crumbs = new ArrayList<BreadCrumb>();
+ crumbs.add(new BreadCrumb("[" + repositoryName + "]", ROOT, false));
+
+ if (pathName != null && pathName.length() > 0) {
+ String[] paths = pathName.split("/");
+ StringBuilder sb = new StringBuilder();
+
+ for (int i = 0; i < paths.length; i++) {
+ String path = paths[i];
+ sb.append(path);
+ crumbs.add(new BreadCrumb(path, sb.toString(), i == (paths.length - 1)));
+ sb.append('/');
+ }
+ }
+
+ ListDataProvider<BreadCrumb> crumbsDp = new ListDataProvider<BreadCrumb>(crumbs);
+ DataView<BreadCrumb> pathsView = new DataView<BreadCrumb>("path", crumbsDp) {
+ private static final long serialVersionUID = 1L;
+
+ public void populateItem(final Item<BreadCrumb> item) {
+ final BreadCrumb entry = item.getModelObject();
+ String path = entry.path;
+ if (path.equals(ROOT)) {
+ path = null;
+ }
+ if (entry.isLeaf) {
+ item.add(new Label("pathLink", entry.name));
+ item.add(new Label("pathSeparator", "").setVisible(false));
+ } else {
+ item.add(new LinkPanel("pathLink", null, entry.name, TreePage.class,
+ WicketUtils.newPathParameter(repositoryName, objectId, path)));
+ item.add(new Label("pathSeparator", "/"));
+ }
+ }
+ };
+ add(pathsView);
+ }
+
+ private static class BreadCrumb implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ final String name;
+ final String path;
+ final boolean isLeaf;
+
+ BreadCrumb(String name, String path, boolean isLeaf) {
+ this.name = name;
+ this.path = path;
+ this.isLeaf = isLeaf;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/panels/ProjectRepositoryPanel.html b/src/main/java/com/gitblit/wicket/panels/ProjectRepositoryPanel.html
new file mode 100644
index 00000000..9b621d5a
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/ProjectRepositoryPanel.html
@@ -0,0 +1,80 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+
+<wicket:panel>
+ <wicket:fragment wicket:id="repositoryAdminLinks">
+ <span class="link">
+ <a wicket:id="tree"><wicket:message key="gb.tree"></wicket:message></a>
+ | <a wicket:id="log"><wicket:message key="gb.log"></wicket:message></a>
+ | <a wicket:id="editRepository"><wicket:message key="gb.edit">[edit]</wicket:message></a>
+ | <a wicket:id="deleteRepository"><wicket:message key="gb.delete">[delete]</wicket:message></a>
+ </span>
+ </wicket:fragment>
+
+ <wicket:fragment wicket:id="repositoryOwnerLinks">
+ <span class="link">
+ <a wicket:id="tree"><wicket:message key="gb.tree"></wicket:message></a>
+ | <a wicket:id="log"><wicket:message key="gb.log"></wicket:message></a>
+ | <a wicket:id="editRepository"><wicket:message key="gb.edit">[edit]</wicket:message></a>
+ </span>
+ </wicket:fragment>
+
+ <wicket:fragment wicket:id="repositoryUserLinks">
+ <span class="link">
+ <a wicket:id="tree"><wicket:message key="gb.tree"></wicket:message></a>
+ | <a wicket:id="log"><wicket:message key="gb.log"></wicket:message></a>
+ </span>
+ </wicket:fragment>
+
+ <wicket:fragment wicket:id="originFragment">
+ <p class="originRepository" style="margin-left:20px;" ><wicket:message key="gb.forkedFrom">[forked from]</wicket:message> <span wicket:id="originRepository">[origin repository]</span></p>
+ </wicket:fragment>
+
+ <div>
+ <div style="padding-top:15px;padding-bottom:15px;margin-right:15px;">
+ <div class="pull-right" style="text-align:right;padding-right:15px;">
+ <span wicket:id="repositoryLinks"></span>
+ <div>
+ <img class="inlineIcon" wicket:id="sparkleshareIcon" />
+ <img class="inlineIcon" wicket:id="frozenIcon" />
+ <img class="inlineIcon" wicket:id="federatedIcon" />
+
+ <a style="text-decoration: none;" wicket:id="tickets" wicket:message="title:gb.tickets">
+ <img style="border:0px;vertical-align:middle;" src="bug_16x16.png"></img>
+ </a>
+ <a style="text-decoration: none;" wicket:id="docs" wicket:message="title:gb.docs">
+ <img style="border:0px;vertical-align:middle;" src="book_16x16.png"></img>
+ </a>
+ <a style="text-decoration: none;" wicket:id="syndication" wicket:message="title:gb.feed">
+ <img style="border:0px;vertical-align:middle;" src="feed_16x16.png"></img>
+ </a>
+ </div>
+ <span style="color: #999;font-style:italic;font-size:0.8em;" wicket:id="repositoryOwner">[owner]</span>
+ </div>
+
+ <div class="pageTitle" style="border:0px;">
+ <div>
+ <span class="repositorySwatch" wicket:id="repositorySwatch"></span>
+ <span class="repository" style="padding-left:3px;color:black;" wicket:id="repositoryName">[repository name]</span>
+ <img class="inlineIcon" style="vertical-align:baseline" wicket:id="accessRestrictionIcon" />
+ </div>
+ <span wicket:id="originRepository">[origin repository]</span>
+ </div>
+
+ <div style="padding-left:20px;">
+ <div style="padding-bottom:10px" wicket:id="repositoryDescription">[repository description]</div>
+
+ <div style="color: #999;">
+ <wicket:message key="gb.lastChange">[last change]</wicket:message> <span wicket:id="repositoryLastChange">[last change]</span>,
+ <span style="font-size:0.8em;" wicket:id="repositorySize">[repository size]</span>
+ </div>
+
+ <div class="hidden-phone hidden-tablet" wicket:id="repositoryCloneUrl">[repository clone url]</div>
+ </div>
+ </div>
+ </div>
+</wicket:panel>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/panels/ProjectRepositoryPanel.java b/src/main/java/com/gitblit/wicket/panels/ProjectRepositoryPanel.java
new file mode 100644
index 00000000..7b4ee9f0
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/ProjectRepositoryPanel.java
@@ -0,0 +1,229 @@
+/*
+ * Copyright 2012 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.panels;
+
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.wicket.Component;
+import org.apache.wicket.Localizer;
+import org.apache.wicket.PageParameters;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.link.BookmarkablePageLink;
+import org.apache.wicket.markup.html.link.ExternalLink;
+import org.apache.wicket.markup.html.link.Link;
+import org.apache.wicket.markup.html.panel.Fragment;
+
+import com.gitblit.Constants.AccessRestrictionType;
+import com.gitblit.GitBlit;
+import com.gitblit.Keys;
+import com.gitblit.SyndicationServlet;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.UserModel;
+import com.gitblit.utils.ArrayUtils;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.wicket.GitBlitWebSession;
+import com.gitblit.wicket.WicketUtils;
+import com.gitblit.wicket.pages.BasePage;
+import com.gitblit.wicket.pages.DocsPage;
+import com.gitblit.wicket.pages.EditRepositoryPage;
+import com.gitblit.wicket.pages.LogPage;
+import com.gitblit.wicket.pages.SummaryPage;
+import com.gitblit.wicket.pages.TicketsPage;
+import com.gitblit.wicket.pages.TreePage;
+
+public class ProjectRepositoryPanel extends BasePanel {
+
+ private static final long serialVersionUID = 1L;
+
+ public ProjectRepositoryPanel(String wicketId, Localizer localizer, Component parent,
+ final boolean isAdmin, final RepositoryModel entry,
+ final Map<AccessRestrictionType, String> accessRestrictions) {
+ super(wicketId);
+
+ final boolean showSwatch = GitBlit.getBoolean(Keys.web.repositoryListSwatches, true);
+ final boolean gitServlet = GitBlit.getBoolean(Keys.git.enableGitServlet, true);
+ final boolean showSize = GitBlit.getBoolean(Keys.web.showRepositorySizes, true);
+
+ // repository swatch
+ Component swatch;
+ if (entry.isBare) {
+ swatch = new Label("repositorySwatch", "&nbsp;").setEscapeModelStrings(false);
+ } else {
+ swatch = new Label("repositorySwatch", "!");
+ WicketUtils.setHtmlTooltip(swatch, localizer.getString("gb.workingCopyWarning", parent));
+ }
+ WicketUtils.setCssBackground(swatch, entry.toString());
+ add(swatch);
+ swatch.setVisible(showSwatch);
+
+ PageParameters pp = WicketUtils.newRepositoryParameter(entry.name);
+ add(new LinkPanel("repositoryName", "list", StringUtils.getRelativePath(entry.projectPath,
+ StringUtils.stripDotGit(entry.name)), SummaryPage.class, pp));
+ add(new Label("repositoryDescription", entry.description).setVisible(!StringUtils
+ .isEmpty(entry.description)));
+
+ if (StringUtils.isEmpty(entry.originRepository)) {
+ add(new Label("originRepository").setVisible(false));
+ } else {
+ Fragment forkFrag = new Fragment("originRepository", "originFragment", this);
+ forkFrag.add(new LinkPanel("originRepository", null, StringUtils.stripDotGit(entry.originRepository),
+ SummaryPage.class, WicketUtils.newRepositoryParameter(entry.originRepository)));
+ add(forkFrag);
+ }
+
+ if (entry.isSparkleshared()) {
+ add(WicketUtils.newImage("sparkleshareIcon", "star_16x16.png", localizer.getString("gb.isSparkleshared", parent)));
+ } else {
+ add(WicketUtils.newClearPixel("sparkleshareIcon").setVisible(false));
+ }
+
+ add(new BookmarkablePageLink<Void>("tickets", TicketsPage.class, pp).setVisible(entry.useTickets));
+ add(new BookmarkablePageLink<Void>("docs", DocsPage.class, pp).setVisible(entry.useDocs));
+
+ if (entry.isFrozen) {
+ add(WicketUtils.newImage("frozenIcon", "cold_16x16.png", localizer.getString("gb.isFrozen", parent)));
+ } else {
+ add(WicketUtils.newClearPixel("frozenIcon").setVisible(false));
+ }
+
+ if (entry.isFederated) {
+ add(WicketUtils.newImage("federatedIcon", "federated_16x16.png", localizer.getString("gb.isFederated", parent)));
+ } else {
+ add(WicketUtils.newClearPixel("federatedIcon").setVisible(false));
+ }
+ switch (entry.accessRestriction) {
+ case NONE:
+ add(WicketUtils.newBlankImage("accessRestrictionIcon").setVisible(false));
+ break;
+ case PUSH:
+ add(WicketUtils.newImage("accessRestrictionIcon", "lock_go_16x16.png",
+ accessRestrictions.get(entry.accessRestriction)));
+ break;
+ case CLONE:
+ add(WicketUtils.newImage("accessRestrictionIcon", "lock_pull_16x16.png",
+ accessRestrictions.get(entry.accessRestriction)));
+ break;
+ case VIEW:
+ add(WicketUtils.newImage("accessRestrictionIcon", "shield_16x16.png",
+ accessRestrictions.get(entry.accessRestriction)));
+ break;
+ default:
+ add(WicketUtils.newBlankImage("accessRestrictionIcon"));
+ }
+
+ if (ArrayUtils.isEmpty(entry.owners)) {
+ add(new Label("repositoryOwner").setVisible(false));
+ } else {
+ String owner = "";
+ for (String username : entry.owners) {
+ UserModel ownerModel = GitBlit.self().getUserModel(username);
+
+ if (ownerModel != null) {
+ owner = ownerModel.getDisplayName();
+ }
+ }
+ if (entry.owners.size() > 1) {
+ owner += ", ...";
+ }
+ Label ownerLabel = (new Label("repositoryOwner", owner + " (" +
+ localizer.getString("gb.owner", parent) + ")"));
+ WicketUtils.setHtmlTooltip(ownerLabel, ArrayUtils.toString(entry.owners));
+ add(ownerLabel);
+ }
+
+ UserModel user = GitBlitWebSession.get().getUser();
+ if (user == null) {
+ user = UserModel.ANONYMOUS;
+ }
+ Fragment repositoryLinks;
+ boolean showOwner = entry.isOwner(user.username);
+ // owner of personal repository gets admin powers
+ boolean showAdmin = isAdmin || entry.isUsersPersonalRepository(user.username);
+
+ if (showAdmin || showOwner) {
+ repositoryLinks = new Fragment("repositoryLinks", showAdmin ? "repositoryAdminLinks"
+ : "repositoryOwnerLinks", this);
+ repositoryLinks.add(new BookmarkablePageLink<Void>("editRepository", EditRepositoryPage.class,
+ WicketUtils.newRepositoryParameter(entry.name)));
+ if (showAdmin) {
+ Link<Void> deleteLink = new Link<Void>("deleteRepository") {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public void onClick() {
+ if (GitBlit.self().deleteRepositoryModel(entry)) {
+ // redirect to the owning page
+ if (entry.isPersonalRepository()) {
+ setResponsePage(getPage().getClass(), WicketUtils.newUsernameParameter(entry.projectPath.substring(1)));
+ } else {
+ setResponsePage(getPage().getClass(), WicketUtils.newProjectParameter(entry.projectPath));
+ }
+ } else {
+ error(MessageFormat.format(getString("gb.repositoryDeleteFailed"), entry));
+ }
+ }
+ };
+ deleteLink.add(new JavascriptEventConfirmation("onclick", MessageFormat.format(
+ localizer.getString("gb.deleteRepository", parent), entry)));
+ repositoryLinks.add(deleteLink);
+ }
+ } else {
+ repositoryLinks = new Fragment("repositoryLinks", "repositoryUserLinks", this);
+ }
+
+ repositoryLinks.add(new BookmarkablePageLink<Void>("tree", TreePage.class, WicketUtils
+ .newRepositoryParameter(entry.name)).setEnabled(entry.hasCommits));
+
+ repositoryLinks.add(new BookmarkablePageLink<Void>("log", LogPage.class, WicketUtils
+ .newRepositoryParameter(entry.name)).setEnabled(entry.hasCommits));
+
+ add(repositoryLinks);
+
+ String lastChange;
+ if (entry.lastChange.getTime() == 0) {
+ lastChange = "--";
+ } else {
+ lastChange = getTimeUtils().timeAgo(entry.lastChange);
+ }
+ Label lastChangeLabel = new Label("repositoryLastChange", lastChange);
+ add(lastChangeLabel);
+ WicketUtils.setCssClass(lastChangeLabel, getTimeUtils().timeAgoCss(entry.lastChange));
+
+ if (entry.hasCommits) {
+ // Existing repository
+ add(new Label("repositorySize", entry.size).setVisible(showSize));
+ } else {
+ // New repository
+ add(new Label("repositorySize", localizer.getString("gb.empty", parent)).setEscapeModelStrings(false));
+ }
+
+ add(new ExternalLink("syndication", SyndicationServlet.asLink("", entry.name, null, 0)));
+
+ List<String> repositoryUrls = new ArrayList<String>();
+ if (gitServlet) {
+ // add the Gitblit repository url
+ repositoryUrls.add(BasePage.getRepositoryUrl(entry));
+ }
+ repositoryUrls.addAll(GitBlit.self().getOtherCloneUrls(entry.name));
+
+ String primaryUrl = ArrayUtils.isEmpty(repositoryUrls) ? "" : repositoryUrls.remove(0);
+ add(new RepositoryUrlPanel("repositoryCloneUrl", primaryUrl));
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/panels/RefsPanel.html b/src/main/java/com/gitblit/wicket/panels/RefsPanel.html
new file mode 100644
index 00000000..a3c0ec49
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/RefsPanel.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+
+<wicket:panel>
+ <span wicket:id="ref">
+ <span wicket:id="lineBreak">[LB]</span><span wicket:id="refName">ref</span>
+ </span>
+</wicket:panel>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/panels/RefsPanel.java b/src/main/java/com/gitblit/wicket/panels/RefsPanel.java
new file mode 100644
index 00000000..3ba22c0b
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/RefsPanel.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.panels;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.wicket.Component;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.panel.Panel;
+import org.apache.wicket.markup.repeater.Item;
+import org.apache.wicket.markup.repeater.data.DataView;
+import org.apache.wicket.markup.repeater.data.ListDataProvider;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.revwalk.RevCommit;
+
+import com.gitblit.models.RefModel;
+import com.gitblit.wicket.WicketUtils;
+import com.gitblit.wicket.pages.CommitPage;
+import com.gitblit.wicket.pages.LogPage;
+import com.gitblit.wicket.pages.RepositoryPage;
+import com.gitblit.wicket.pages.TagPage;
+
+public class RefsPanel extends Panel {
+
+ private static final long serialVersionUID = 1L;
+
+ public RefsPanel(String id, final String repositoryName, RevCommit c,
+ Map<ObjectId, List<RefModel>> refs) {
+ this(id, repositoryName, refs.get(c.getId()));
+ }
+
+ public RefsPanel(String id, final String repositoryName, List<RefModel> refs) {
+ super(id);
+ if (refs == null) {
+ refs = new ArrayList<RefModel>();
+ }
+ Collections.sort(refs, new Comparator<RefModel>() {
+ @Override
+ public int compare(RefModel o1, RefModel o2) {
+ // sort remote heads last, otherwise sort by name
+ // this is so we can insert a break on the refs panel
+ // [head][branch][branch][tag][tag]
+ // [remote][remote][remote]
+ boolean remote1 = o1.displayName.startsWith(Constants.R_REMOTES);
+ boolean remote2 = o2.displayName.startsWith(Constants.R_REMOTES);
+ if (remote1 && remote2) {
+ // both are remote heads, sort by name
+ return o1.displayName.compareTo(o2.displayName);
+ }
+ if (remote1) {
+ // o1 is remote, o2 comes first
+ return 1;
+ }
+ if (remote2) {
+ // remote is o2, o1 comes first
+ return -1;
+ }
+ // standard sort
+ return o1.displayName.compareTo(o2.displayName);
+ }
+ });
+
+ // count remote and determine if we should insert a break
+ int remoteCount = 0;
+ for (RefModel ref : refs) {
+ if (ref.displayName.startsWith(Constants.R_REMOTES)) {
+ remoteCount++;
+ }
+ }
+ final boolean shouldBreak = remoteCount < refs.size();
+
+ ListDataProvider<RefModel> refsDp = new ListDataProvider<RefModel>(refs);
+ DataView<RefModel> refsView = new DataView<RefModel>("ref", refsDp) {
+ private static final long serialVersionUID = 1L;
+ private boolean alreadyInsertedBreak = !shouldBreak;
+
+ public void populateItem(final Item<RefModel> item) {
+ RefModel entry = item.getModelObject();
+ String name = entry.displayName;
+ String objectid = entry.getReferencedObjectId().getName();
+ boolean breakLine = false;
+ Class<? extends RepositoryPage> linkClass = CommitPage.class;
+ String cssClass = "";
+ if (name.startsWith(Constants.R_HEADS)) {
+ // local branch
+ linkClass = LogPage.class;
+ name = name.substring(Constants.R_HEADS.length());
+ cssClass = "localBranch";
+ } else if (name.equals(Constants.HEAD)) {
+ // local head
+ linkClass = LogPage.class;
+ cssClass = "headRef";
+ } else if (name.startsWith(Constants.R_REMOTES)) {
+ // remote branch
+ linkClass = LogPage.class;
+ name = name.substring(Constants.R_REMOTES.length());
+ cssClass = "remoteBranch";
+ if (!alreadyInsertedBreak) {
+ breakLine = true;
+ alreadyInsertedBreak = true;
+ }
+ } else if (name.startsWith(Constants.R_TAGS)) {
+ // tag
+ if (entry.isAnnotatedTag()) {
+ linkClass = TagPage.class;
+ objectid = entry.getObjectId().getName();
+ } else {
+ linkClass = CommitPage.class;
+ objectid = entry.getReferencedObjectId().getName();
+ }
+ name = name.substring(Constants.R_TAGS.length());
+ cssClass = "tagRef";
+ } else if (name.startsWith(Constants.R_NOTES)) {
+ // codereview refs
+ linkClass = CommitPage.class;
+ cssClass = "otherRef";
+ } else if (name.startsWith(com.gitblit.Constants.R_GITBLIT)) {
+ // gitblit refs
+ linkClass = LogPage.class;
+ cssClass = "otherRef";
+ name = name.substring(com.gitblit.Constants.R_GITBLIT.length());
+ }
+
+ Component c = new LinkPanel("refName", null, name, linkClass,
+ WicketUtils.newObjectParameter(repositoryName, objectid));
+ WicketUtils.setCssClass(c, cssClass);
+ WicketUtils.setHtmlTooltip(c, name);
+ item.add(c);
+ Label lb = new Label("lineBreak", "<br/>");
+ lb.setVisible(breakLine);
+ lb.setRenderBodyOnly(true);
+ item.add(lb.setEscapeModelStrings(false));
+ item.setRenderBodyOnly(true);
+ }
+ };
+ add(refsView);
+ }
+} \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/panels/RegistrantPermissionsPanel.html b/src/main/java/com/gitblit/wicket/panels/RegistrantPermissionsPanel.html
new file mode 100644
index 00000000..eb82245c
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/RegistrantPermissionsPanel.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+
+<body>
+<wicket:panel>
+
+ <form class="form-inline" wicket:id="permissionToggleForm">
+ <div style="padding-bottom:10px" class="btn-group pull-right" data-toggle="buttons-radio">
+ <a class="btn btn-info" wicket:id="showMutable"><wicket:message key="gb.mutable"></wicket:message></a>
+ <a class="btn btn-info" wicket:id="showSpecified"><wicket:message key="gb.specified"></wicket:message></a>
+ <a class="btn btn-info" wicket:id="showEffective"><wicket:message key="gb.effective"></wicket:message></a>
+ </div>
+ </form>
+
+ <div style="clear:both;" wicket:id="permissionRow">
+ <div style="padding-top:10px;border-left:1px solid #ccc;border-right:1px solid #ccc;" class="row-fluid">
+ <div style="padding-top:5px;padding-left:5px" class="span6"><span wicket:id="registrant"></span></div><div style="padding-top:5px;padding-right:5px;text-align:right;" class="span3"><span class="label" wicket:id="pType">[permission type]</span></div> <select class="input-medium" wicket:id="permission"></select>
+ </div>
+ </div>
+
+ <div style="clear:both; padding-top:15px;" class="row-fluid">
+ <form style="padding: 20px 40px;" class="well form-inline" wicket:id="addPermissionForm">
+ <select class="input-xlarge" wicket:id="registrant"></select> <select class="input-large" wicket:id="permission"></select> <input class="btn btn-success" type="submit" value="Add" wicket:message="value:gb.add" wicket:id="addPermissionButton"/>
+ </form>
+ </div>
+
+ <wicket:fragment wicket:id="repositoryRegistrant">
+ <b><span class="repositorySwatch" wicket:id="repositorySwatch"></span></b> <span wicket:id="repositoryName"></span>
+ </wicket:fragment>
+
+ <wicket:fragment wicket:id="userRegistrant">
+ <span wicket:id="userAvatar"></span> <span wicket:id="userName"></span>
+ </wicket:fragment>
+
+ <wicket:fragment wicket:id="teamRegistrant">
+ <span style="font-weight: bold;" wicket:id="teamName"></span>
+ </wicket:fragment>
+
+</wicket:panel>
+</body>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/panels/RegistrantPermissionsPanel.java b/src/main/java/com/gitblit/wicket/panels/RegistrantPermissionsPanel.java
new file mode 100644
index 00000000..4156cd19
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/RegistrantPermissionsPanel.java
@@ -0,0 +1,340 @@
+/*
+ * Copyright 2012 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.panels;
+
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.wicket.Component;
+import org.apache.wicket.ajax.AjaxRequestTarget;
+import org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior;
+import org.apache.wicket.ajax.markup.html.form.AjaxButton;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.form.DropDownChoice;
+import org.apache.wicket.markup.html.form.Form;
+import org.apache.wicket.markup.html.form.IChoiceRenderer;
+import org.apache.wicket.markup.html.panel.Fragment;
+import org.apache.wicket.markup.repeater.Item;
+import org.apache.wicket.markup.repeater.OddEvenItem;
+import org.apache.wicket.markup.repeater.RefreshingView;
+import org.apache.wicket.markup.repeater.util.ModelIteratorAdapter;
+import org.apache.wicket.model.CompoundPropertyModel;
+import org.apache.wicket.model.IModel;
+import org.eclipse.jgit.lib.PersonIdent;
+
+import com.gitblit.Constants.AccessPermission;
+import com.gitblit.Constants.PermissionType;
+import com.gitblit.Constants.RegistrantType;
+import com.gitblit.GitBlit;
+import com.gitblit.models.RegistrantAccessPermission;
+import com.gitblit.models.UserModel;
+import com.gitblit.utils.DeepCopier;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.wicket.WicketUtils;
+
+/**
+ * Allows user to manipulate registrant access permissions.
+ *
+ * @author James Moger
+ *
+ */
+public class RegistrantPermissionsPanel extends BasePanel {
+
+ private static final long serialVersionUID = 1L;
+
+ public enum Show {
+ specified, mutable, effective;
+
+ public boolean show(RegistrantAccessPermission ap) {
+ switch (this) {
+ case specified:
+ return ap.mutable || ap.isOwner();
+ case mutable:
+ return ap.mutable;
+ case effective:
+ return true;
+ default:
+ return true;
+ }
+ }
+ }
+
+ private Show activeState = Show.mutable;
+
+ public RegistrantPermissionsPanel(String wicketId, RegistrantType registrantType, List<String> allRegistrants, final List<RegistrantAccessPermission> permissions, final Map<AccessPermission, String> translations) {
+ super(wicketId);
+ setOutputMarkupId(true);
+
+ /*
+ * Permission view toggle buttons
+ */
+ Form<Void> permissionToggleForm = new Form<Void>("permissionToggleForm");
+ permissionToggleForm.add(new ShowStateButton("showSpecified", Show.specified));
+ permissionToggleForm.add(new ShowStateButton("showMutable", Show.mutable));
+ permissionToggleForm.add(new ShowStateButton("showEffective", Show.effective));
+ add(permissionToggleForm);
+
+ /*
+ * Permission repeating display
+ */
+ RefreshingView<RegistrantAccessPermission> dataView = new RefreshingView<RegistrantAccessPermission>("permissionRow") {
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ protected Iterator<IModel<RegistrantAccessPermission>> getItemModels() {
+ // the iterator returns RepositoryPermission objects, but we need it to
+ // return models
+ return new ModelIteratorAdapter<RegistrantAccessPermission>(permissions.iterator()) {
+ @Override
+ protected IModel<RegistrantAccessPermission> model(RegistrantAccessPermission permission) {
+ return new CompoundPropertyModel<RegistrantAccessPermission>(permission);
+ }
+ };
+ }
+
+ @Override
+ protected Item<RegistrantAccessPermission> newItem(String id, int index, IModel<RegistrantAccessPermission> model) {
+ // this item sets markup class attribute to either 'odd' or
+ // 'even' for decoration
+ return new OddEvenItem<RegistrantAccessPermission>(id, index, model);
+ }
+
+ public void populateItem(final Item<RegistrantAccessPermission> item) {
+ final RegistrantAccessPermission entry = item.getModelObject();
+ if (RegistrantType.REPOSITORY.equals(entry.registrantType)) {
+ String repoName = StringUtils.stripDotGit(entry.registrant);
+ if (!entry.isMissing() && StringUtils.findInvalidCharacter(repoName) == null) {
+ // repository, strip .git and show swatch
+ Fragment repositoryFragment = new Fragment("registrant", "repositoryRegistrant", RegistrantPermissionsPanel.this);
+ Component swatch = new Label("repositorySwatch", "&nbsp;").setEscapeModelStrings(false);
+ WicketUtils.setCssBackground(swatch, entry.toString());
+ repositoryFragment.add(swatch);
+ Label registrant = new Label("repositoryName", repoName);
+ repositoryFragment.add(registrant);
+ item.add(repositoryFragment);
+ } else {
+ // regex or missing
+ Label label = new Label("registrant", entry.registrant);
+ WicketUtils.setCssStyle(label, "font-weight: bold;");
+ item.add(label);
+ }
+ } else if (RegistrantType.USER.equals(entry.registrantType)) {
+ // user
+ PersonIdent ident = new PersonIdent(entry.registrant, "");
+ UserModel user = GitBlit.self().getUserModel(entry.registrant);
+ if (user != null) {
+ ident = new PersonIdent(user.getDisplayName(), user.emailAddress == null ? user.getDisplayName() : user.emailAddress);
+ }
+
+ Fragment userFragment = new Fragment("registrant", "userRegistrant", RegistrantPermissionsPanel.this);
+ userFragment.add(new GravatarImage("userAvatar", ident, 20, false));
+ userFragment.add(new Label("userName", entry.registrant));
+ item.add(userFragment);
+ } else {
+ // team
+ Fragment teamFragment = new Fragment("registrant", "teamRegistrant", RegistrantPermissionsPanel.this);
+ teamFragment.add(new Label("teamName", entry.registrant));
+ item.add(teamFragment);
+ }
+ switch (entry.permissionType) {
+ case ADMINISTRATOR:
+ Label administrator = new Label("pType", entry.source == null ? getString("gb.administrator") : entry.source);
+ WicketUtils.setHtmlTooltip(administrator, getString("gb.administratorPermission"));
+ WicketUtils.setCssClass(administrator, "label label-inverse");
+ item.add(administrator);
+ break;
+ case OWNER:
+ Label owner = new Label("pType", getString("gb.owner"));
+ WicketUtils.setHtmlTooltip(owner, getString("gb.ownerPermission"));
+ WicketUtils.setCssClass(owner, "label label-info");
+ item.add(owner);
+ break;
+ case TEAM:
+ Label team = new Label("pType", entry.source == null ? getString("gb.team") : entry.source);
+ WicketUtils.setHtmlTooltip(team, MessageFormat.format(getString("gb.teamPermission"), entry.source));
+ WicketUtils.setCssClass(team, "label label-success");
+ item.add(team);
+ break;
+ case REGEX:
+ Label regex = new Label("pType", "regex");
+ if (!StringUtils.isEmpty(entry.source)) {
+ WicketUtils.setHtmlTooltip(regex, MessageFormat.format(getString("gb.regexPermission"), entry.source));
+ }
+ WicketUtils.setCssClass(regex, "label");
+ item.add(regex);
+ break;
+ default:
+ if (entry.isMissing()) {
+ // repository is missing, this permission will be removed on save
+ Label missing = new Label("pType", getString("gb.missing"));
+ WicketUtils.setCssClass(missing, "label label-important");
+ WicketUtils.setHtmlTooltip(missing, getString("gb.missingPermission"));
+ item.add(missing);
+ } else {
+ // standard permission
+ item.add(new Label("pType", "").setVisible(false));
+ }
+ break;
+ }
+
+ item.setVisible(activeState.show(entry));
+
+ // use ajax to get immediate update of permission level change
+ // otherwise we can lose it if they change levels and then add
+ // a new repository permission
+ final DropDownChoice<AccessPermission> permissionChoice = new DropDownChoice<AccessPermission>(
+ "permission", Arrays.asList(AccessPermission.values()), new AccessPermissionRenderer(translations));
+ // only allow changing an explicitly defined permission
+ // this is designed to prevent changing a regex permission in
+ // a repository
+ permissionChoice.setEnabled(entry.mutable);
+ permissionChoice.setOutputMarkupId(true);
+ if (entry.mutable) {
+ permissionChoice.add(new AjaxFormComponentUpdatingBehavior("onchange") {
+
+ private static final long serialVersionUID = 1L;
+
+ protected void onUpdate(AjaxRequestTarget target) {
+ target.addComponent(permissionChoice);
+ }
+ });
+ }
+
+ item.add(permissionChoice);
+ }
+ };
+ add(dataView);
+ setOutputMarkupId(true);
+
+ // filter out registrants we already have permissions for
+ final List<String> registrants = new ArrayList<String>(allRegistrants);
+ for (RegistrantAccessPermission rp : permissions) {
+ if (rp.mutable) {
+ // remove editable duplicates
+ // this allows for specifying an explicit permission
+ registrants.remove(rp.registrant);
+ } else if (rp.isAdmin()) {
+ // administrators can not have their permission changed
+ registrants.remove(rp.registrant);
+ } else if (rp.isOwner()) {
+ // owners can not have their permission changed
+ registrants.remove(rp.registrant);
+ }
+ }
+
+ /*
+ * Add permission form
+ */
+ IModel<RegistrantAccessPermission> addPermissionModel = new CompoundPropertyModel<RegistrantAccessPermission>(new RegistrantAccessPermission(registrantType));
+ Form<RegistrantAccessPermission> addPermissionForm = new Form<RegistrantAccessPermission>("addPermissionForm", addPermissionModel);
+ addPermissionForm.add(new DropDownChoice<String>("registrant", registrants));
+ addPermissionForm.add(new DropDownChoice<AccessPermission>("permission", Arrays
+ .asList(AccessPermission.NEWPERMISSIONS), new AccessPermissionRenderer(translations)));
+ AjaxButton button = new AjaxButton("addPermissionButton", addPermissionForm) {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ protected void onSubmit(AjaxRequestTarget target, Form<?> form) {
+ // add permission to our list
+ RegistrantAccessPermission rp = (RegistrantAccessPermission) form.getModel().getObject();
+ if (rp.permission == null) {
+ return;
+ }
+ RegistrantAccessPermission copy = DeepCopier.copy(rp);
+ if (StringUtils.findInvalidCharacter(copy.registrant) != null) {
+ copy.permissionType = PermissionType.REGEX;
+ copy.source = copy.registrant;
+ }
+ permissions.add(copy);
+
+ // resort permissions after insert to convey idea of eval order
+ Collections.sort(permissions);
+
+ // remove registrant from available choices
+ registrants.remove(rp.registrant);
+
+ // force the panel to refresh
+ target.addComponent(RegistrantPermissionsPanel.this);
+ }
+ };
+ addPermissionForm.add(button);
+
+ // only show add permission form if we have a registrant choice
+ add(addPermissionForm.setVisible(registrants.size() > 0));
+ }
+
+ protected boolean getStatelessHint()
+ {
+ return false;
+ }
+
+
+ private class AccessPermissionRenderer implements IChoiceRenderer<AccessPermission> {
+
+ private static final long serialVersionUID = 1L;
+
+ private final Map<AccessPermission, String> map;
+
+ public AccessPermissionRenderer(Map<AccessPermission, String> map) {
+ this.map = map;
+ }
+
+ @Override
+ public String getDisplayValue(AccessPermission type) {
+ return map.get(type);
+ }
+
+ @Override
+ public String getIdValue(AccessPermission type, int index) {
+ return Integer.toString(index);
+ }
+ }
+
+ private class ShowStateButton extends AjaxButton {
+ private static final long serialVersionUID = 1L;
+
+ Show buttonState;
+
+ public ShowStateButton(String wicketId, Show state) {
+ super(wicketId);
+ this.buttonState = state;
+ setOutputMarkupId(true);
+ }
+
+ @Override
+ protected void onBeforeRender()
+ {
+ String cssClass = "btn";
+ if (buttonState.equals(RegistrantPermissionsPanel.this.activeState)) {
+ cssClass = "btn btn-info active";
+ }
+ WicketUtils.setCssClass(this, cssClass);
+ super.onBeforeRender();
+ }
+
+ @Override
+ protected void onSubmit(AjaxRequestTarget target, Form<?> form) {
+ RegistrantPermissionsPanel.this.activeState = buttonState;
+ target.addComponent(RegistrantPermissionsPanel.this);
+ }
+ };
+}
diff --git a/src/main/java/com/gitblit/wicket/panels/RepositoriesPanel.html b/src/main/java/com/gitblit/wicket/panels/RepositoriesPanel.html
new file mode 100644
index 00000000..81a4c6eb
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/RepositoriesPanel.html
@@ -0,0 +1,107 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+
+<body>
+<wicket:panel>
+
+ <div wicket:id="managementPanel">[management links]</div>
+
+ <table class="repositories">
+ <span wicket:id="headerContent"></span>
+ <tbody>
+ <tr wicket:id="row">
+ <span wicket:id="rowContent"></span>
+ </tr>
+ </tbody>
+ </table>
+
+ <wicket:fragment wicket:id="adminLinks">
+ <!-- page nav links -->
+ <div class="admin_nav">
+ <a class="btn-small" wicket:id="clearCache">
+ <i class="icon icon-remove"></i>
+ <wicket:message key="gb.clearCache"></wicket:message>
+ </a>
+ <a class="btn-small" wicket:id="newRepository" style="padding-right:0px;">
+ <i class="icon icon-plus-sign"></i>
+ <wicket:message key="gb.newRepository"></wicket:message>
+ </a>
+ </div>
+ </wicket:fragment>
+
+ <wicket:fragment wicket:id="personalLinks">
+ <!-- page nav links -->
+ <div class="admin_nav">
+ <a class="btn-small" wicket:id="newRepository" style="padding-right:0px;">
+ <i class="icon icon-plus-sign"></i>
+ <wicket:message key="gb.newRepository"></wicket:message>
+ </a>
+ </div>
+ </wicket:fragment>
+
+ <wicket:fragment wicket:id="repositoryAdminLinks">
+ <span class="link"><a wicket:id="editRepository"><wicket:message key="gb.edit">[edit]</wicket:message></a> | <a wicket:id="deleteRepository"><wicket:message key="gb.delete">[delete]</wicket:message></a></span>
+ </wicket:fragment>
+
+ <wicket:fragment wicket:id="repositoryOwnerLinks">
+ <span class="link"><a wicket:id="editRepository"><wicket:message key="gb.edit">[edit]</wicket:message></a></span>
+ </wicket:fragment>
+
+ <wicket:fragment wicket:id="flatRepositoryHeader">
+ <tr>
+ <th class="left" wicket:id="orderByRepository">
+ <img style="vertical-align: middle;" src="git-black-16x16.png"/>
+ <wicket:message key="gb.repository">Repository</wicket:message>
+ </th>
+ <th class="hidden-phone" wicket:id="orderByDescription"><wicket:message key="gb.description">Description</wicket:message></th>
+ <th class="hidden-tablet hidden-phone" wicket:id="orderByOwner"><wicket:message key="gb.owner">Owner</wicket:message></th>
+ <th class="hidden-phone"></th>
+ <th wicket:id="orderByDate"><wicket:message key="gb.lastChange">Last Change</wicket:message></th>
+ <th class="hidden-phone"></th>
+ <th class="right"></th>
+ </tr>
+ </wicket:fragment>
+
+ <wicket:fragment wicket:id="groupRepositoryHeader">
+ <tr>
+ <th class="left">
+ <img style="vertical-align: middle;" src="git-black-16x16.png"/>
+ <wicket:message key="gb.repository">Repository</wicket:message>
+ </th>
+ <th class="hidden-phone" ><span><wicket:message key="gb.description">Description</wicket:message></span></th>
+ <th class="hidden-tablet hidden-phone"><span><wicket:message key="gb.owner">Owner</wicket:message></span></th>
+ <th class="hidden-phone"></th>
+ <th><wicket:message key="gb.lastChange">Last Change</wicket:message></th>
+ <th class="hidden-phone"></th>
+ <th class="right"></th>
+ </tr>
+ </wicket:fragment>
+
+ <wicket:fragment wicket:id="groupRepositoryRow">
+ <td colspan="1"><span wicket:id="groupName">[group name]</span></td>
+ <td colspan="6" style="padding: 2px;"><span class="hidden-phone" style="font-weight:normal;color:#666;" wicket:id="groupDescription">[description]</span></td>
+ </wicket:fragment>
+
+ <wicket:fragment wicket:id="repositoryRow">
+ <td class="left" style="padding-left:3px;" ><b><span class="repositorySwatch" wicket:id="repositorySwatch"></span></b> <span style="padding-left:3px;" wicket:id="repositoryName">[repository name]</span></td>
+ <td class="hidden-phone"><span class="list" wicket:id="repositoryDescription">[repository description]</span></td>
+ <td class="hidden-tablet hidden-phone author"><span wicket:id="repositoryOwner">[repository owner]</span></td>
+ <td class="hidden-phone" style="text-align: right;padding-right:10px;"><img class="inlineIcon" wicket:id="sparkleshareIcon" /><img class="inlineIcon" wicket:id="forkIcon" /><img class="inlineIcon" wicket:id="ticketsIcon" /><img class="inlineIcon" wicket:id="docsIcon" /><img class="inlineIcon" wicket:id="frozenIcon" /><img class="inlineIcon" wicket:id="federatedIcon" /><img class="inlineIcon" wicket:id="accessRestrictionIcon" /></td>
+ <td><span wicket:id="repositoryLastChange">[last change]</span></td>
+ <td class="hidden-phone" style="text-align: right;padding-right:15px;"><span style="font-size:0.8em;" wicket:id="repositorySize">[repository size]</span></td>
+ <td class="rightAlign">
+ <span class="hidden-phone">
+ <span wicket:id="repositoryLinks"></span>
+ <a style="text-decoration: none;" wicket:id="syndication" wicket:message="title:gb.feed">
+ <img style="border:0px;vertical-align:middle;" src="feed_16x16.png"></img>
+ </a>
+ </span>
+ </td>
+ </wicket:fragment>
+
+</wicket:panel>
+</body>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/panels/RepositoriesPanel.java b/src/main/java/com/gitblit/wicket/panels/RepositoriesPanel.java
new file mode 100644
index 00000000..726af61d
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/RepositoriesPanel.java
@@ -0,0 +1,557 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.panels;
+
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.wicket.Component;
+import org.apache.wicket.PageParameters;
+import org.apache.wicket.extensions.markup.html.repeater.data.sort.OrderByBorder;
+import org.apache.wicket.extensions.markup.html.repeater.util.SortParam;
+import org.apache.wicket.extensions.markup.html.repeater.util.SortableDataProvider;
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.link.BookmarkablePageLink;
+import org.apache.wicket.markup.html.link.ExternalLink;
+import org.apache.wicket.markup.html.link.Link;
+import org.apache.wicket.markup.html.panel.Fragment;
+import org.apache.wicket.markup.repeater.Item;
+import org.apache.wicket.markup.repeater.data.DataView;
+import org.apache.wicket.markup.repeater.data.IDataProvider;
+import org.apache.wicket.markup.repeater.data.ListDataProvider;
+import org.apache.wicket.model.IModel;
+import org.apache.wicket.model.Model;
+
+import com.gitblit.Constants.AccessRestrictionType;
+import com.gitblit.GitBlit;
+import com.gitblit.Keys;
+import com.gitblit.SyndicationServlet;
+import com.gitblit.models.ProjectModel;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.UserModel;
+import com.gitblit.utils.ArrayUtils;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.wicket.GitBlitWebSession;
+import com.gitblit.wicket.WicketUtils;
+import com.gitblit.wicket.pages.BasePage;
+import com.gitblit.wicket.pages.EditRepositoryPage;
+import com.gitblit.wicket.pages.EmptyRepositoryPage;
+import com.gitblit.wicket.pages.ProjectPage;
+import com.gitblit.wicket.pages.RepositoriesPage;
+import com.gitblit.wicket.pages.SummaryPage;
+import com.gitblit.wicket.pages.UserPage;
+
+public class RepositoriesPanel extends BasePanel {
+
+ private static final long serialVersionUID = 1L;
+
+ public RepositoriesPanel(String wicketId, final boolean showAdmin, final boolean showManagement,
+ List<RepositoryModel> models, boolean enableLinks,
+ final Map<AccessRestrictionType, String> accessRestrictionTranslations) {
+ super(wicketId);
+
+ final boolean linksActive = enableLinks;
+ final boolean showSize = GitBlit.getBoolean(Keys.web.showRepositorySizes, true);
+
+ final UserModel user = GitBlitWebSession.get().getUser();
+
+ final IDataProvider<RepositoryModel> dp;
+
+ Fragment managementLinks;
+ if (showAdmin) {
+ // user is admin
+ managementLinks = new Fragment("managementPanel", "adminLinks", this);
+ managementLinks.add(new Link<Void>("clearCache") {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public void onClick() {
+ GitBlit.self().resetRepositoryListCache();
+ setResponsePage(RepositoriesPage.class);
+ }
+ }.setVisible(GitBlit.getBoolean(Keys.git.cacheRepositoryList, true)));
+ managementLinks.add(new BookmarkablePageLink<Void>("newRepository", EditRepositoryPage.class));
+ add(managementLinks);
+ } else if (showManagement && user != null && user.canCreate()) {
+ // user can create personal repositories
+ managementLinks = new Fragment("managementPanel", "personalLinks", this);
+ managementLinks.add(new BookmarkablePageLink<Void>("newRepository", EditRepositoryPage.class));
+ add(managementLinks);
+ } else {
+ // user has no management permissions
+ add (new Label("managementPanel").setVisible(false));
+ }
+
+ if (GitBlit.getString(Keys.web.repositoryListType, "flat").equalsIgnoreCase("grouped")) {
+ List<RepositoryModel> rootRepositories = new ArrayList<RepositoryModel>();
+ Map<String, List<RepositoryModel>> groups = new HashMap<String, List<RepositoryModel>>();
+ for (RepositoryModel model : models) {
+ String rootPath = StringUtils.getRootPath(model.name);
+ if (StringUtils.isEmpty(rootPath)) {
+ // root repository
+ rootRepositories.add(model);
+ } else {
+ // non-root, grouped repository
+ if (!groups.containsKey(rootPath)) {
+ groups.put(rootPath, new ArrayList<RepositoryModel>());
+ }
+ groups.get(rootPath).add(model);
+ }
+ }
+ List<String> roots = new ArrayList<String>(groups.keySet());
+ Collections.sort(roots);
+
+ if (rootRepositories.size() > 0) {
+ // inject the root repositories at the top of the page
+ roots.add(0, "");
+ groups.put("", rootRepositories);
+ }
+
+ List<RepositoryModel> groupedModels = new ArrayList<RepositoryModel>();
+ for (String root : roots) {
+ List<RepositoryModel> subModels = groups.get(root);
+ ProjectModel project = GitBlit.self().getProjectModel(root);
+ GroupRepositoryModel group = new GroupRepositoryModel(project.name, subModels.size());
+ if (project != null) {
+ group.title = project.title;
+ group.description = project.description;
+ }
+ groupedModels.add(group);
+ Collections.sort(subModels);
+ groupedModels.addAll(subModels);
+ }
+ dp = new RepositoriesProvider(groupedModels);
+ } else {
+ dp = new SortableRepositoriesProvider(models);
+ }
+
+ final String baseUrl = WicketUtils.getGitblitURL(getRequest());
+ final boolean showSwatch = GitBlit.getBoolean(Keys.web.repositoryListSwatches, true);
+
+ DataView<RepositoryModel> dataView = new DataView<RepositoryModel>("row", dp) {
+ private static final long serialVersionUID = 1L;
+ int counter;
+ String currGroupName;
+
+ @Override
+ protected void onBeforeRender() {
+ super.onBeforeRender();
+ counter = 0;
+ }
+
+ public void populateItem(final Item<RepositoryModel> item) {
+ final RepositoryModel entry = item.getModelObject();
+ if (entry instanceof GroupRepositoryModel) {
+ GroupRepositoryModel groupRow = (GroupRepositoryModel) entry;
+ currGroupName = entry.name;
+ Fragment row = new Fragment("rowContent", "groupRepositoryRow", this);
+ item.add(row);
+
+ String name = groupRow.name;
+ if (name.charAt(0) == '~') {
+ // user page
+ String username = name.substring(1);
+ UserModel user = GitBlit.self().getUserModel(username);
+ row.add(new LinkPanel("groupName", null, (user == null ? username : user.getDisplayName()) + " (" + groupRow.count + ")", UserPage.class, WicketUtils.newUsernameParameter(username)));
+ row.add(new Label("groupDescription", getString("gb.personalRepositories")));
+ } else {
+ // project page
+ row.add(new LinkPanel("groupName", null, groupRow.toString(), ProjectPage.class, WicketUtils.newProjectParameter(entry.name)));
+ row.add(new Label("groupDescription", entry.description == null ? "":entry.description));
+ }
+ WicketUtils.setCssClass(item, "group");
+ // reset counter so that first row is light background
+ counter = 0;
+ return;
+ }
+ Fragment row = new Fragment("rowContent", "repositoryRow", this);
+ item.add(row);
+
+ // try to strip group name for less cluttered list
+ String repoName = entry.toString();
+ if (!StringUtils.isEmpty(currGroupName) && (repoName.indexOf('/') > -1)) {
+ repoName = repoName.substring(currGroupName.length() + 1);
+ }
+
+ // repository swatch
+ Component swatch;
+ if (entry.isBare){
+ swatch = new Label("repositorySwatch", "&nbsp;").setEscapeModelStrings(false);
+ } else {
+ swatch = new Label("repositorySwatch", "!");
+ WicketUtils.setHtmlTooltip(swatch, getString("gb.workingCopyWarning"));
+ }
+ WicketUtils.setCssBackground(swatch, entry.toString());
+ row.add(swatch);
+ swatch.setVisible(showSwatch);
+
+ if (linksActive) {
+ Class<? extends BasePage> linkPage;
+ if (entry.hasCommits) {
+ // repository has content
+ linkPage = SummaryPage.class;
+ } else {
+ // new/empty repository OR proposed repository
+ linkPage = EmptyRepositoryPage.class;
+ }
+
+ PageParameters pp = WicketUtils.newRepositoryParameter(entry.name);
+ row.add(new LinkPanel("repositoryName", "list", repoName, linkPage, pp));
+ row.add(new LinkPanel("repositoryDescription", "list", entry.description,
+ linkPage, pp));
+ } else {
+ // no links like on a federation page
+ row.add(new Label("repositoryName", repoName));
+ row.add(new Label("repositoryDescription", entry.description));
+ }
+ if (entry.hasCommits) {
+ // Existing repository
+ row.add(new Label("repositorySize", entry.size).setVisible(showSize));
+ } else {
+ // New repository
+ row.add(new Label("repositorySize", "<span class='empty'>(" + getString("gb.empty") + ")</span>")
+ .setEscapeModelStrings(false));
+ }
+
+ if (entry.isSparkleshared()) {
+ row.add(WicketUtils.newImage("sparkleshareIcon", "star_16x16.png",
+ getString("gb.isSparkleshared")));
+ } else {
+ row.add(WicketUtils.newClearPixel("sparkleshareIcon").setVisible(false));
+ }
+
+ if (entry.isFork()) {
+ row.add(WicketUtils.newImage("forkIcon", "commit_divide_16x16.png",
+ getString("gb.isFork")));
+ } else {
+ row.add(WicketUtils.newClearPixel("forkIcon").setVisible(false));
+ }
+
+ if (entry.useTickets) {
+ row.add(WicketUtils.newImage("ticketsIcon", "bug_16x16.png",
+ getString("gb.tickets")));
+ } else {
+ row.add(WicketUtils.newBlankImage("ticketsIcon"));
+ }
+
+ if (entry.useDocs) {
+ row.add(WicketUtils
+ .newImage("docsIcon", "book_16x16.png", getString("gb.docs")));
+ } else {
+ row.add(WicketUtils.newBlankImage("docsIcon"));
+ }
+
+ if (entry.isFrozen) {
+ row.add(WicketUtils.newImage("frozenIcon", "cold_16x16.png",
+ getString("gb.isFrozen")));
+ } else {
+ row.add(WicketUtils.newClearPixel("frozenIcon").setVisible(false));
+ }
+
+ if (entry.isFederated) {
+ row.add(WicketUtils.newImage("federatedIcon", "federated_16x16.png",
+ getString("gb.isFederated")));
+ } else {
+ row.add(WicketUtils.newClearPixel("federatedIcon").setVisible(false));
+ }
+ switch (entry.accessRestriction) {
+ case NONE:
+ row.add(WicketUtils.newBlankImage("accessRestrictionIcon"));
+ break;
+ case PUSH:
+ row.add(WicketUtils.newImage("accessRestrictionIcon", "lock_go_16x16.png",
+ accessRestrictionTranslations.get(entry.accessRestriction)));
+ break;
+ case CLONE:
+ row.add(WicketUtils.newImage("accessRestrictionIcon", "lock_pull_16x16.png",
+ accessRestrictionTranslations.get(entry.accessRestriction)));
+ break;
+ case VIEW:
+ row.add(WicketUtils.newImage("accessRestrictionIcon", "shield_16x16.png",
+ accessRestrictionTranslations.get(entry.accessRestriction)));
+ break;
+ default:
+ row.add(WicketUtils.newBlankImage("accessRestrictionIcon"));
+ }
+
+ String owner = "";
+ if (!ArrayUtils.isEmpty(entry.owners)) {
+ // display first owner
+ for (String username : entry.owners) {
+ UserModel ownerModel = GitBlit.self().getUserModel(username);
+ if (ownerModel != null) {
+ owner = ownerModel.getDisplayName();
+ break;
+ }
+ }
+ if (entry.owners.size() > 1) {
+ owner += ", ...";
+ }
+ }
+ Label ownerLabel = new Label("repositoryOwner", owner);
+ WicketUtils.setHtmlTooltip(ownerLabel, ArrayUtils.toString(entry.owners));
+ row.add(ownerLabel);
+
+ String lastChange;
+ if (entry.lastChange.getTime() == 0) {
+ lastChange = "--";
+ } else {
+ lastChange = getTimeUtils().timeAgo(entry.lastChange);
+ }
+ Label lastChangeLabel = new Label("repositoryLastChange", lastChange);
+ row.add(lastChangeLabel);
+ WicketUtils.setCssClass(lastChangeLabel, getTimeUtils().timeAgoCss(entry.lastChange));
+
+ boolean showOwner = user != null && entry.isOwner(user.username);
+ boolean myPersonalRepository = showOwner && entry.isUsersPersonalRepository(user.username);
+ if (showAdmin || myPersonalRepository) {
+ Fragment repositoryLinks = new Fragment("repositoryLinks",
+ "repositoryAdminLinks", this);
+ repositoryLinks.add(new BookmarkablePageLink<Void>("editRepository",
+ EditRepositoryPage.class, WicketUtils
+ .newRepositoryParameter(entry.name)));
+ Link<Void> deleteLink = new Link<Void>("deleteRepository") {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public void onClick() {
+ if (GitBlit.self().deleteRepositoryModel(entry)) {
+ if (dp instanceof SortableRepositoriesProvider) {
+ info(MessageFormat.format(getString("gb.repositoryDeleted"), entry));
+ ((SortableRepositoriesProvider) dp).remove(entry);
+ } else {
+ setResponsePage(getPage().getClass(), getPage().getPageParameters());
+ }
+ } else {
+ error(MessageFormat.format(getString("gb.repositoryDeleteFailed"), entry));
+ }
+ }
+ };
+ deleteLink.add(new JavascriptEventConfirmation("onclick", MessageFormat.format(
+ getString("gb.deleteRepository"), entry)));
+ repositoryLinks.add(deleteLink);
+ row.add(repositoryLinks);
+ } else if (showOwner) {
+ Fragment repositoryLinks = new Fragment("repositoryLinks",
+ "repositoryOwnerLinks", this);
+ repositoryLinks.add(new BookmarkablePageLink<Void>("editRepository",
+ EditRepositoryPage.class, WicketUtils
+ .newRepositoryParameter(entry.name)));
+ row.add(repositoryLinks);
+ } else {
+ row.add(new Label("repositoryLinks"));
+ }
+ row.add(new ExternalLink("syndication", SyndicationServlet.asLink(baseUrl,
+ entry.name, null, 0)).setVisible(linksActive));
+ WicketUtils.setAlternatingBackground(item, counter);
+ counter++;
+ }
+ };
+ add(dataView);
+
+ if (dp instanceof SortableDataProvider<?>) {
+ // add sortable header
+ SortableDataProvider<?> sdp = (SortableDataProvider<?>) dp;
+ Fragment fragment = new Fragment("headerContent", "flatRepositoryHeader", this);
+ fragment.add(newSort("orderByRepository", SortBy.repository, sdp, dataView));
+ fragment.add(newSort("orderByDescription", SortBy.description, sdp, dataView));
+ fragment.add(newSort("orderByOwner", SortBy.owner, sdp, dataView));
+ fragment.add(newSort("orderByDate", SortBy.date, sdp, dataView));
+ add(fragment);
+ } else {
+ // not sortable
+ Fragment fragment = new Fragment("headerContent", "groupRepositoryHeader", this);
+ add(fragment);
+ }
+ }
+
+ private static class GroupRepositoryModel extends RepositoryModel {
+
+ private static final long serialVersionUID = 1L;
+
+ int count;
+ String title;
+
+ GroupRepositoryModel(String name, int count) {
+ super(name, "", "", new Date(0));
+ this.count = count;
+ }
+
+ @Override
+ public String toString() {
+ return (StringUtils.isEmpty(title) ? name : title) + " (" + count + ")";
+ }
+ }
+
+ protected enum SortBy {
+ repository, description, owner, date;
+ }
+
+ protected OrderByBorder newSort(String wicketId, SortBy field, SortableDataProvider<?> dp,
+ final DataView<?> dataView) {
+ return new OrderByBorder(wicketId, field.name(), dp) {
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ protected void onSortChanged() {
+ dataView.setCurrentPage(0);
+ }
+ };
+ }
+
+ private static class RepositoriesProvider extends ListDataProvider<RepositoryModel> {
+
+ private static final long serialVersionUID = 1L;
+
+ public RepositoriesProvider(List<RepositoryModel> list) {
+ super(list);
+ }
+
+ @Override
+ public List<RepositoryModel> getData() {
+ return super.getData();
+ }
+
+ public void remove(RepositoryModel model) {
+ int index = getData().indexOf(model);
+ RepositoryModel groupModel = null;
+ if (index == (getData().size() - 1)) {
+ // last element
+ if (index > 0) {
+ // previous element is group header, then this is last
+ // repository in group. remove group too.
+ if (getData().get(index - 1) instanceof GroupRepositoryModel) {
+ groupModel = getData().get(index - 1);
+ }
+ }
+ } else if (index < (getData().size() - 1)) {
+ // not last element. check next element for group match.
+ if (getData().get(index - 1) instanceof GroupRepositoryModel
+ && getData().get(index + 1) instanceof GroupRepositoryModel) {
+ // repository is sandwiched by group headers so this
+ // repository is the only element in the group. remove
+ // group.
+ groupModel = getData().get(index - 1);
+ }
+ }
+
+ if (groupModel == null) {
+ // Find the group and decrement the count
+ for (int i = index; i >= 0; i--) {
+ if (getData().get(i) instanceof GroupRepositoryModel) {
+ ((GroupRepositoryModel) getData().get(i)).count--;
+ break;
+ }
+ }
+ } else {
+ // Remove the group header
+ getData().remove(groupModel);
+ }
+
+ getData().remove(model);
+ }
+ }
+
+ private static class SortableRepositoriesProvider extends SortableDataProvider<RepositoryModel> {
+
+ private static final long serialVersionUID = 1L;
+
+ private List<RepositoryModel> list;
+
+ protected SortableRepositoriesProvider(List<RepositoryModel> list) {
+ this.list = list;
+ setSort(SortBy.date.name(), false);
+ }
+
+ public void remove(RepositoryModel model) {
+ list.remove(model);
+ }
+
+ @Override
+ public int size() {
+ if (list == null) {
+ return 0;
+ }
+ return list.size();
+ }
+
+ @Override
+ public IModel<RepositoryModel> model(RepositoryModel header) {
+ return new Model<RepositoryModel>(header);
+ }
+
+ @Override
+ public Iterator<RepositoryModel> iterator(int first, int count) {
+ SortParam sp = getSort();
+ String prop = sp.getProperty();
+ final boolean asc = sp.isAscending();
+
+ if (prop == null || prop.equals(SortBy.date.name())) {
+ Collections.sort(list, new Comparator<RepositoryModel>() {
+ @Override
+ public int compare(RepositoryModel o1, RepositoryModel o2) {
+ if (asc) {
+ return o1.lastChange.compareTo(o2.lastChange);
+ }
+ return o2.lastChange.compareTo(o1.lastChange);
+ }
+ });
+ } else if (prop.equals(SortBy.repository.name())) {
+ Collections.sort(list, new Comparator<RepositoryModel>() {
+ @Override
+ public int compare(RepositoryModel o1, RepositoryModel o2) {
+ if (asc) {
+ return o1.name.compareTo(o2.name);
+ }
+ return o2.name.compareTo(o1.name);
+ }
+ });
+ } else if (prop.equals(SortBy.owner.name())) {
+ Collections.sort(list, new Comparator<RepositoryModel>() {
+ @Override
+ public int compare(RepositoryModel o1, RepositoryModel o2) {
+ String own1 = ArrayUtils.toString(o1.owners);
+ String own2 = ArrayUtils.toString(o2.owners);
+ if (asc) {
+ return own1.compareTo(own2);
+ }
+ return own2.compareTo(own1);
+ }
+ });
+ } else if (prop.equals(SortBy.description.name())) {
+ Collections.sort(list, new Comparator<RepositoryModel>() {
+ @Override
+ public int compare(RepositoryModel o1, RepositoryModel o2) {
+ if (asc) {
+ return o1.description.compareTo(o2.description);
+ }
+ return o2.description.compareTo(o1.description);
+ }
+ });
+ }
+ return list.subList(first, first + count).iterator();
+ }
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/panels/RepositoryUrlPanel.html b/src/main/java/com/gitblit/wicket/panels/RepositoryUrlPanel.html
new file mode 100644
index 00000000..d7c76f13
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/RepositoryUrlPanel.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+
+<wicket:panel>
+ <span wicket:id="repositoryUrl" style="color: blue;">[repository url]</span><span class="hidden-phone hidden-tablet" wicket:id="copyFunction"></span>
+
+ <!-- Plain JavaScript manual copy & paste -->
+ <wicket:fragment wicket:id="jsPanel">
+ <span style="vertical-align:baseline;">
+ <img wicket:id="copyIcon" wicket:message="title:gb.copyToClipboard"></img>
+ </span>
+ </wicket:fragment>
+
+ <!-- flash-based button-press copy & paste -->
+ <wicket:fragment wicket:id="clippyPanel">
+ <object wicket:message="title:gb.copyToClipboard" style="vertical-align:middle;"
+ wicket:id="clippy"
+ width="14"
+ height="14"
+ bgcolor="#ffffff"
+ quality="high"
+ wmode="transparent"
+ scale="noscale"
+ allowScriptAccess="always"></object>
+ </wicket:fragment>
+</wicket:panel>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/panels/RepositoryUrlPanel.java b/src/main/java/com/gitblit/wicket/panels/RepositoryUrlPanel.java
new file mode 100644
index 00000000..58df028b
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/RepositoryUrlPanel.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.panels;
+
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.image.ContextImage;
+import org.apache.wicket.markup.html.panel.Fragment;
+
+import com.gitblit.GitBlit;
+import com.gitblit.Keys;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.wicket.WicketUtils;
+
+public class RepositoryUrlPanel extends BasePanel {
+
+ private static final long serialVersionUID = 1L;
+
+ public RepositoryUrlPanel(String wicketId, String url) {
+ super(wicketId);
+ add(new Label("repositoryUrl", url));
+ if (GitBlit.getBoolean(Keys.web.allowFlashCopyToClipboard, true)) {
+ // clippy: flash-based copy & paste
+ Fragment fragment = new Fragment("copyFunction", "clippyPanel", this);
+ String baseUrl = WicketUtils.getGitblitURL(getRequest());
+ ShockWaveComponent clippy = new ShockWaveComponent("clippy", baseUrl + "/clippy.swf");
+ clippy.setValue("flashVars", "text=" + StringUtils.encodeURL(url));
+ fragment.add(clippy);
+ add(fragment);
+ } else {
+ // javascript: manual copy & paste with modal browser prompt dialog
+ Fragment fragment = new Fragment("copyFunction", "jsPanel", this);
+ ContextImage img = WicketUtils.newImage("copyIcon", "clippy.png");
+ img.add(new JavascriptTextPrompt("onclick", "Copy to Clipboard (Ctrl+C, Enter)", url));
+ fragment.add(img);
+ add(fragment);
+ }
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/panels/SearchPanel.html b/src/main/java/com/gitblit/wicket/panels/SearchPanel.html
new file mode 100644
index 00000000..74af71c5
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/SearchPanel.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+
+<body>
+<wicket:panel>
+
+ <!-- header -->
+ <div wicket:id="commitHeader">[search header]</div>
+
+ <!-- header -->
+ <div style="margin-top:10px;font-weight:bold;" class="header"><wicket:message key="gb.search"></wicket:message>: <span wicket:id="searchString">[search string]</span> (<span wicket:id="searchType">[search type]</span>)</div>
+ <table class="pretty">
+ <tbody>
+ <tr wicket:id="commit">
+ <td class="date"><span wicket:id="commitDate">[commit date]</span></td>
+ <td class="author"><span wicket:id="commitAuthor">[commit author]</span></td>
+ <td class="icon"><img wicket:id="commitIcon" /></td>
+ <td class="message"><table class="nestedTable"><tr><td><span style="vertical-align:middle;" wicket:id="commitShortMessage">[commit short message]</span></td><td><div style="text-align:right;" wicket:id="commitRefs">[commit refs]</div></td></tr></table></td>
+ <td class="rightAlign">
+ <span class="hidden-phone link">
+ <a wicket:id="commit"><wicket:message key="gb.commit"></wicket:message></a> | <a wicket:id="commitdiff"><wicket:message key="gb.commitdiff"></wicket:message></a> | <a wicket:id="tree"><wicket:message key="gb.tree"></wicket:message></a>
+ </span>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+
+</wicket:panel>
+</body>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/panels/SearchPanel.java b/src/main/java/com/gitblit/wicket/panels/SearchPanel.java
new file mode 100644
index 00000000..9d38ab09
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/SearchPanel.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.panels;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.link.BookmarkablePageLink;
+import org.apache.wicket.markup.repeater.Item;
+import org.apache.wicket.markup.repeater.data.DataView;
+import org.apache.wicket.markup.repeater.data.ListDataProvider;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+
+import com.gitblit.Constants;
+import com.gitblit.GitBlit;
+import com.gitblit.Keys;
+import com.gitblit.models.RefModel;
+import com.gitblit.utils.JGitUtils;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.wicket.WicketUtils;
+import com.gitblit.wicket.pages.CommitDiffPage;
+import com.gitblit.wicket.pages.CommitPage;
+import com.gitblit.wicket.pages.GitSearchPage;
+import com.gitblit.wicket.pages.TreePage;
+
+public class SearchPanel extends BasePanel {
+
+ private static final long serialVersionUID = 1L;
+
+ private boolean hasMore;
+
+ public SearchPanel(String wicketId, final String repositoryName, final String objectId,
+ final String value, Constants.SearchType searchType, Repository r, int limit, int pageOffset,
+ boolean showRemoteRefs) {
+ super(wicketId);
+ boolean pageResults = limit <= 0;
+ int itemsPerPage = GitBlit.getInteger(Keys.web.itemsPerPage, 50);
+ if (itemsPerPage <= 1) {
+ itemsPerPage = 50;
+ }
+
+ RevCommit commit = JGitUtils.getCommit(r, objectId);
+
+ final Map<ObjectId, List<RefModel>> allRefs = JGitUtils.getAllRefs(r, showRemoteRefs);
+ List<RevCommit> commits;
+ if (pageResults) {
+ // Paging result set
+ commits = JGitUtils.searchRevlogs(r, objectId, value, searchType, pageOffset
+ * itemsPerPage, itemsPerPage);
+ } else {
+ // Fixed size result set
+ commits = JGitUtils.searchRevlogs(r, objectId, value, searchType, 0, limit);
+ }
+
+ // inaccurate way to determine if there are more commits.
+ // works unless commits.size() represents the exact end.
+ hasMore = commits.size() >= itemsPerPage;
+
+ // header
+ add(new CommitHeaderPanel("commitHeader", repositoryName, commit));
+
+ add(new Label("searchString", value));
+ add(new Label("searchType", searchType.toString()));
+
+ ListDataProvider<RevCommit> dp = new ListDataProvider<RevCommit>(commits);
+ DataView<RevCommit> searchView = new DataView<RevCommit>("commit", dp) {
+ private static final long serialVersionUID = 1L;
+ int counter;
+
+ public void populateItem(final Item<RevCommit> item) {
+ final RevCommit entry = item.getModelObject();
+ final Date date = JGitUtils.getCommitDate(entry);
+
+ item.add(WicketUtils.createDateLabel("commitDate", date, getTimeZone(), getTimeUtils()));
+
+ // author search link
+ String author = entry.getAuthorIdent().getName();
+ LinkPanel authorLink = new LinkPanel("commitAuthor", "list", author,
+ GitSearchPage.class, WicketUtils.newSearchParameter(repositoryName, objectId,
+ author, Constants.SearchType.AUTHOR));
+ setPersonSearchTooltip(authorLink, author, Constants.SearchType.AUTHOR);
+ item.add(authorLink);
+
+ // merge icon
+ if (entry.getParentCount() > 1) {
+ item.add(WicketUtils.newImage("commitIcon", "commit_merge_16x16.png"));
+ } else {
+ item.add(WicketUtils.newBlankImage("commitIcon"));
+ }
+
+ String shortMessage = entry.getShortMessage();
+ String trimmedMessage = shortMessage;
+ if (allRefs.containsKey(entry.getId())) {
+ trimmedMessage = StringUtils.trimString(shortMessage, Constants.LEN_SHORTLOG_REFS);
+ } else {
+ trimmedMessage = StringUtils.trimString(shortMessage, Constants.LEN_SHORTLOG);
+ }
+ LinkPanel shortlog = new LinkPanel("commitShortMessage", "list subject",
+ trimmedMessage, CommitPage.class, WicketUtils.newObjectParameter(
+ repositoryName, entry.getName()));
+ if (!shortMessage.equals(trimmedMessage)) {
+ WicketUtils.setHtmlTooltip(shortlog, shortMessage);
+ }
+ item.add(shortlog);
+
+ item.add(new RefsPanel("commitRefs", repositoryName, entry, allRefs));
+
+ item.add(new BookmarkablePageLink<Void>("commit", CommitPage.class, WicketUtils
+ .newObjectParameter(repositoryName, entry.getName())));
+ item.add(new BookmarkablePageLink<Void>("commitdiff", CommitDiffPage.class,
+ WicketUtils.newObjectParameter(repositoryName, entry.getName())));
+ item.add(new BookmarkablePageLink<Void>("tree", TreePage.class, WicketUtils
+ .newObjectParameter(repositoryName, entry.getName())));
+
+ WicketUtils.setAlternatingBackground(item, counter);
+ counter++;
+ }
+ };
+ add(searchView);
+ }
+
+ public boolean hasMore() {
+ return hasMore;
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/panels/ShockWaveComponent.java b/src/main/java/com/gitblit/wicket/panels/ShockWaveComponent.java
new file mode 100644
index 00000000..fa989453
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/ShockWaveComponent.java
@@ -0,0 +1,205 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.panels;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.wicket.Response;
+import org.apache.wicket.markup.ComponentTag;
+import org.apache.wicket.markup.MarkupStream;
+import org.apache.wicket.util.value.IValueMap;
+
+/**
+ * https://cwiki.apache.org/WICKET/object-container-adding-flash-to-a-wicket-application.html
+ *
+ * @author Jan Kriesten
+ * @author manuelbarzi
+ * @author James Moger
+ *
+ */
+public class ShockWaveComponent extends ObjectContainer {
+ private static final long serialVersionUID = 1L;
+
+ private static final String CONTENTTYPE = "application/x-shockwave-flash";
+ private static final String CLSID = "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000";
+ private static final String CODEBASE = "http://fpdownload.adobe.com/pub/shockwave/cabs/flash/swflash.cab#version=7,0,0,0";
+
+ // valid attributes
+ private static final List<String> attributeNames = Arrays.asList(new String[] { "classid",
+ "width", "height", "codebase", "align", "base", "data", "flashvars" });
+ // valid parameters
+ private static final List<String> parameterNames = Arrays.asList(new String[] { "devicefont",
+ "movie", "play", "loop", "quality", "bgcolor", "scale", "salign", "menu", "wmode",
+ "allowscriptaccess", "seamlesstabbing", "flashvars" });
+
+ // combined options (to iterate over them)
+ private static final List<String> optionNames = new ArrayList<String>(attributeNames.size()
+ + parameterNames.size());
+ static {
+ optionNames.addAll(attributeNames);
+ optionNames.addAll(parameterNames);
+ }
+
+ private Map<String, String> attributes;
+ private Map<String, String> parameters;
+
+ public ShockWaveComponent(String id) {
+ super(id);
+
+ attributes = new HashMap<String, String>();
+ parameters = new HashMap<String, String>();
+ }
+
+ public ShockWaveComponent(String id, String movie) {
+ this(id);
+ setValue("movie", movie);
+ }
+
+ public ShockWaveComponent(String id, String movie, String width, String height) {
+ this(id);
+
+ setValue("movie", movie);
+ setValue("width", width);
+ setValue("height", height);
+ }
+
+ public void setValue(String name, String value) {
+ // IE and other browsers handle movie/data differently. So movie is used
+ // for IE, whereas
+ // data is used for all other browsers. The class uses movie parameter
+ // to handle url and
+ // puts the values to the maps depending on the browser information
+ String parameter = name.toLowerCase();
+ if ("data".equals(parameter))
+ parameter = "movie";
+
+ if ("movie".equals(parameter) && !getClientProperties().isBrowserInternetExplorer())
+ attributes.put("data", value);
+
+ if (attributeNames.contains(parameter))
+ attributes.put(parameter, value);
+ else if (parameterNames.contains(parameter))
+ parameters.put(parameter, value);
+ }
+
+ public String getValue(String name) {
+ String parameter = name.toLowerCase();
+ String value = null;
+
+ if ("data".equals(parameter)) {
+ if (getClientProperties().isBrowserInternetExplorer())
+ return null;
+ parameter = "movie";
+ }
+
+ if (attributeNames.contains(parameter))
+ value = attributes.get(parameter);
+ else if (parameterNames.contains(parameter))
+ value = parameters.get(parameter);
+
+ // special treatment of movie to resolve to the url
+ if (value != null && parameter.equals("movie"))
+ value = resolveResource(value);
+
+ return value;
+ }
+
+ public void onComponentTag(ComponentTag tag) {
+ // get options from the markup
+ IValueMap valueMap = tag.getAttributes();
+
+ // Iterate over valid options
+ for (String s : optionNames) {
+ if (valueMap.containsKey(s)) {
+ // if option isn't set programmatically, set value from markup
+ if (!attributes.containsKey(s) && !parameters.containsKey(s))
+ setValue(s, valueMap.getString(s));
+ // remove attribute - they are added in super.onComponentTag()
+ // to
+ // the right place as attribute or param
+ valueMap.remove(s);
+ }
+ }
+
+ super.onComponentTag(tag);
+ }
+
+ public void onComponentTagBody(MarkupStream markupStream, ComponentTag openTag) {
+
+ super.onComponentTagBody(markupStream, openTag);
+
+ Response response = getResponse();
+
+ // add all object's parameters in embed tag too:
+ response.write("<embed");
+ addParameter(response, "type", CONTENTTYPE);
+ for (String name : getParameterNames()) {
+ String value = getValue(name);
+ if (value != null) {
+ name = "movie".equals(name) ? "src" : name;
+ addParameter(response, name, value);
+ }
+ }
+ for (String name : getAttributeNames()) {
+ if ("width".equals(name) || "height".equals(name)) {
+ String value = getValue(name);
+ if (value != null) {
+ addParameter(response, name, value);
+ }
+ }
+ }
+ response.write(" />\n");
+
+ }
+
+ private void addParameter(Response response, String name, String value) {
+ response.write(" ");
+ response.write(name);
+ response.write("=\"");
+ response.write(value);
+ response.write("\"");
+ }
+
+ @Override
+ protected String getClsid() {
+ return CLSID;
+ }
+
+ @Override
+ protected String getCodebase() {
+ return CODEBASE;
+ }
+
+ @Override
+ protected String getContentType() {
+ return CONTENTTYPE;
+ }
+
+ @Override
+ protected List<String> getAttributeNames() {
+ return attributeNames;
+ }
+
+ @Override
+ protected List<String> getParameterNames() {
+ return parameterNames;
+ }
+} \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/panels/TagsPanel.html b/src/main/java/com/gitblit/wicket/panels/TagsPanel.html
new file mode 100644
index 00000000..ba9f15dd
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/TagsPanel.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+
+<body>
+<wicket:panel>
+
+ <!-- tags -->
+ <div class="header"><i class="icon-tags" style="vertical-align: middle;"></i> <b><span wicket:id="header">[tags header]</span></b></div>
+ <table class="pretty">
+ <tbody>
+ <tr wicket:id="tag">
+ <td class="date"><span wicket:id="tagDate">[tag date]</span></td>
+ <td><b><span wicket:id="tagName">[tag name]</span></b></td>
+ <td class="hidden-phone icon"><img wicket:id="tagIcon" /></td>
+ <td class="hidden-phone"><span wicket:id="tagDescription">[tag description]</span></td>
+ <td class="hidden-phone rightAlign">
+ <span wicket:id="tagLinks"></span>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+
+ <div wicket:id="allTags">[all tags]</div>
+
+ <!-- annotated tag links -->
+ <wicket:fragment wicket:id="annotatedLinks">
+ <span class="link">
+ <a wicket:id="tag"><wicket:message key="gb.tag"></wicket:message></a> | <a wicket:id="commit"><wicket:message key="gb.commit"></wicket:message></a> | <a wicket:id="log"><wicket:message key="gb.log"></wicket:message></a>
+ </span>
+ </wicket:fragment>
+
+ <!-- lightweight tag links -->
+ <wicket:fragment wicket:id="lightweightLinks">
+ <span class="link">
+ <a wicket:id="commit"><wicket:message key="gb.commit"></wicket:message></a> | <a wicket:id="log"><wicket:message key="gb.log"></wicket:message></a>
+ </span>
+ </wicket:fragment>
+
+ <!-- blob tag links -->
+ <wicket:fragment wicket:id="blobLinks">
+ <span class="link">
+ <a wicket:id="tag"><wicket:message key="gb.tag"></wicket:message></a> | <a wicket:id="blob"><wicket:message key="gb.blob"></wicket:message></a> | <a wicket:id="raw"><wicket:message key="gb.raw"></wicket:message></a>
+ </span>
+ </wicket:fragment>
+
+</wicket:panel>
+</body>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/panels/TagsPanel.java b/src/main/java/com/gitblit/wicket/panels/TagsPanel.java
new file mode 100644
index 00000000..2bee6a60
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/TagsPanel.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.panels;
+
+import java.util.List;
+
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.link.BookmarkablePageLink;
+import org.apache.wicket.markup.html.panel.Fragment;
+import org.apache.wicket.markup.repeater.Item;
+import org.apache.wicket.markup.repeater.data.DataView;
+import org.apache.wicket.markup.repeater.data.ListDataProvider;
+import org.apache.wicket.model.StringResourceModel;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.Repository;
+
+import com.gitblit.models.RefModel;
+import com.gitblit.utils.JGitUtils;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.wicket.WicketUtils;
+import com.gitblit.wicket.pages.BlobPage;
+import com.gitblit.wicket.pages.CommitPage;
+import com.gitblit.wicket.pages.LogPage;
+import com.gitblit.wicket.pages.RawPage;
+import com.gitblit.wicket.pages.RepositoryPage;
+import com.gitblit.wicket.pages.TagPage;
+import com.gitblit.wicket.pages.TagsPage;
+import com.gitblit.wicket.pages.TreePage;
+
+public class TagsPanel extends BasePanel {
+
+ private static final long serialVersionUID = 1L;
+
+ private final boolean hasTags;
+
+ public TagsPanel(String wicketId, final String repositoryName, Repository r, final int maxCount) {
+ super(wicketId);
+
+ // header
+ List<RefModel> tags = JGitUtils.getTags(r, false, maxCount);
+ if (maxCount > 0) {
+ // summary page
+ // show tags page link
+ add(new LinkPanel("header", "title", new StringResourceModel("gb.tags", this, null),
+ TagsPage.class, WicketUtils.newRepositoryParameter(repositoryName)));
+ } else {
+ // tags page
+ add(new Label("header", new StringResourceModel("gb.tags", this, null)));
+ }
+
+ ListDataProvider<RefModel> tagsDp = new ListDataProvider<RefModel>(tags);
+ DataView<RefModel> tagView = new DataView<RefModel>("tag", tagsDp) {
+ private static final long serialVersionUID = 1L;
+ int counter;
+
+ public void populateItem(final Item<RefModel> item) {
+ RefModel entry = item.getModelObject();
+
+ item.add(WicketUtils.createDateLabel("tagDate", entry.getDate(), getTimeZone(), getTimeUtils()));
+
+ Class<? extends RepositoryPage> linkClass;
+ switch (entry.getReferencedObjectType()) {
+ case Constants.OBJ_BLOB:
+ linkClass = BlobPage.class;
+ break;
+ case Constants.OBJ_TREE:
+ linkClass = TreePage.class;
+ break;
+ case Constants.OBJ_COMMIT:
+ default:
+ linkClass = CommitPage.class;
+ break;
+ }
+ item.add(new LinkPanel("tagName", "list name", entry.displayName, linkClass,
+ WicketUtils.newObjectParameter(repositoryName, entry
+ .getReferencedObjectId().getName())));
+
+ // workaround for RevTag returning a lengthy shortlog. :(
+ String message = StringUtils.trimString(entry.getShortMessage(),
+ com.gitblit.Constants.LEN_SHORTLOG);
+
+ if (linkClass.equals(BlobPage.class)) {
+ // Blob Tag Object
+ item.add(WicketUtils.newImage("tagIcon", "file_16x16.png"));
+ item.add(new LinkPanel("tagDescription", "list", message, TagPage.class,
+ WicketUtils.newObjectParameter(repositoryName, entry.getObjectId()
+ .getName())));
+
+ Fragment fragment = new Fragment("tagLinks", "blobLinks", this);
+ fragment.add(new BookmarkablePageLink<Void>("tag", TagPage.class, WicketUtils
+ .newObjectParameter(repositoryName, entry.getObjectId().getName()))
+ .setEnabled(entry.isAnnotatedTag()));
+
+ fragment.add(new BookmarkablePageLink<Void>("blob", linkClass, WicketUtils
+ .newObjectParameter(repositoryName, entry.getReferencedObjectId()
+ .getName())));
+
+ fragment.add(new BookmarkablePageLink<Void>("raw", RawPage.class, WicketUtils
+ .newObjectParameter(repositoryName, entry.getReferencedObjectId()
+ .getName())));
+ item.add(fragment);
+ } else {
+ // TODO Tree Tag Object
+ // Standard Tag Object
+ if (entry.isAnnotatedTag()) {
+ item.add(WicketUtils.newImage("tagIcon", "tag_16x16.png"));
+ item.add(new LinkPanel("tagDescription", "list", message, TagPage.class,
+ WicketUtils.newObjectParameter(repositoryName, entry.getObjectId()
+ .getName())));
+
+ Fragment fragment = new Fragment("tagLinks", "annotatedLinks", this);
+ fragment.add(new BookmarkablePageLink<Void>("tag", TagPage.class,
+ WicketUtils.newObjectParameter(repositoryName, entry.getObjectId()
+ .getName())).setEnabled(entry.isAnnotatedTag()));
+
+ fragment.add(new BookmarkablePageLink<Void>("commit", linkClass,
+ WicketUtils.newObjectParameter(repositoryName, entry
+ .getReferencedObjectId().getName())));
+
+ fragment.add(new BookmarkablePageLink<Void>("log", LogPage.class,
+ WicketUtils.newObjectParameter(repositoryName, entry.getName())));
+ item.add(fragment);
+ } else {
+ item.add(WicketUtils.newBlankImage("tagIcon"));
+ item.add(new LinkPanel("tagDescription", "list", message, CommitPage.class,
+ WicketUtils.newObjectParameter(repositoryName, entry.getObjectId()
+ .getName())));
+ Fragment fragment = new Fragment("tagLinks", "lightweightLinks", this);
+ fragment.add(new BookmarkablePageLink<Void>("commit", CommitPage.class,
+ WicketUtils.newObjectParameter(repositoryName, entry
+ .getReferencedObjectId().getName())));
+ fragment.add(new BookmarkablePageLink<Void>("log", LogPage.class,
+ WicketUtils.newObjectParameter(repositoryName, entry.getName())));
+ item.add(fragment);
+ }
+ }
+
+ WicketUtils.setAlternatingBackground(item, counter);
+ counter++;
+ }
+ };
+ add(tagView);
+ if (tags.size() < maxCount || maxCount <= 0) {
+ add(new Label("allTags", "").setVisible(false));
+ } else {
+ add(new LinkPanel("allTags", "link", new StringResourceModel("gb.allTags", this, null),
+ TagsPage.class, WicketUtils.newRepositoryParameter(repositoryName)));
+ }
+
+ hasTags = tags.size() > 0;
+ }
+
+ public TagsPanel hideIfEmpty() {
+ setVisible(hasTags);
+ return this;
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/panels/TeamsPanel.html b/src/main/java/com/gitblit/wicket/panels/TeamsPanel.html
new file mode 100644
index 00000000..ff689292
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/TeamsPanel.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+
+<body>
+<wicket:panel>
+
+ <div wicket:id="adminPanel">[admin links]</div>
+
+ <table class="repositories">
+ <tr>
+ <th class="left">
+ <img style="vertical-align: middle; border: 1px solid #888; background-color: white;" src="users_16x16.png"/>
+ <wicket:message key="gb.teams">[teams]</wicket:message>
+ </th>
+ <th class="hidden-phone" style="width:140px;"><wicket:message key="gb.teamMembers">[team members]</wicket:message></th>
+ <th class="hidden-phone" style="width:100px;"><wicket:message key="gb.repositories">[repositories]</wicket:message></th>
+ <th style="width:80px;" class="right"></th>
+ </tr>
+ <tbody>
+ <tr wicket:id="teamRow">
+ <td class="left" ><div class="list" wicket:id="teamname">[teamname]</div></td>
+ <td class="hidden-phone left" ><div class="list" wicket:id="members">[members]</div></td>
+ <td class="hidden-phone left" ><div class="list" wicket:id="repositories">[repositories]</div></td>
+ <td class="rightAlign"><span wicket:id="teamLinks"></span></td>
+ </tr>
+ </tbody>
+ </table>
+
+ <wicket:fragment wicket:id="adminLinks">
+ <!-- page nav links -->
+ <div class="admin_nav">
+ <a class="btn-small" wicket:id="newTeam" style="padding-right:0px;">
+ <i class="icon icon-plus-sign"></i>
+ <wicket:message key="gb.newTeam"></wicket:message>
+ </a>
+ </div>
+ </wicket:fragment>
+
+ <wicket:fragment wicket:id="teamAdminLinks">
+ <span class="link"><a wicket:id="editTeam"><wicket:message key="gb.edit">[edit]</wicket:message></a> | <a wicket:id="deleteTeam"><wicket:message key="gb.delete">[delete]</wicket:message></a></span>
+ </wicket:fragment>
+
+</wicket:panel>
+</body>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/panels/TeamsPanel.java b/src/main/java/com/gitblit/wicket/panels/TeamsPanel.java
new file mode 100644
index 00000000..b76388b3
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/TeamsPanel.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.panels;
+
+import java.text.MessageFormat;
+import java.util.List;
+
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.link.BookmarkablePageLink;
+import org.apache.wicket.markup.html.link.Link;
+import org.apache.wicket.markup.html.panel.Fragment;
+import org.apache.wicket.markup.repeater.Item;
+import org.apache.wicket.markup.repeater.data.DataView;
+import org.apache.wicket.markup.repeater.data.ListDataProvider;
+
+import com.gitblit.GitBlit;
+import com.gitblit.models.TeamModel;
+import com.gitblit.wicket.WicketUtils;
+import com.gitblit.wicket.pages.EditTeamPage;
+
+public class TeamsPanel extends BasePanel {
+
+ private static final long serialVersionUID = 1L;
+
+ public TeamsPanel(String wicketId, final boolean showAdmin) {
+ super(wicketId);
+
+ Fragment adminLinks = new Fragment("adminPanel", "adminLinks", this);
+ adminLinks.add(new BookmarkablePageLink<Void>("newTeam", EditTeamPage.class));
+ add(adminLinks.setVisible(showAdmin && GitBlit.self().supportsTeamMembershipChanges(null)));
+
+ final List<TeamModel> teams = GitBlit.self().getAllTeams();
+ DataView<TeamModel> teamsView = new DataView<TeamModel>("teamRow",
+ new ListDataProvider<TeamModel>(teams)) {
+ private static final long serialVersionUID = 1L;
+ private int counter;
+
+ @Override
+ protected void onBeforeRender() {
+ super.onBeforeRender();
+ counter = 0;
+ }
+
+ public void populateItem(final Item<TeamModel> item) {
+ final TeamModel entry = item.getModelObject();
+ LinkPanel editLink = new LinkPanel("teamname", "list", entry.name,
+ EditTeamPage.class, WicketUtils.newTeamnameParameter(entry.name));
+ WicketUtils.setHtmlTooltip(editLink, getString("gb.edit") + " " + entry.name);
+ item.add(editLink);
+ item.add(new Label("members", entry.users.size() > 0 ? ("" + entry.users.size())
+ : ""));
+ item.add(new Label("repositories",
+ entry.repositories.size() > 0 ? ("" + entry.repositories.size()) : ""));
+ Fragment teamLinks = new Fragment("teamLinks", "teamAdminLinks", this);
+ teamLinks.add(new BookmarkablePageLink<Void>("editTeam", EditTeamPage.class,
+ WicketUtils.newTeamnameParameter(entry.name)));
+ Link<Void> deleteLink = new Link<Void>("deleteTeam") {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public void onClick() {
+ if (GitBlit.self().deleteTeam(entry.name)) {
+ teams.remove(entry);
+ info(MessageFormat.format("Team ''{0}'' deleted.", entry.name));
+ } else {
+ error(MessageFormat
+ .format("Failed to delete team ''{0}''!", entry.name));
+ }
+ }
+ };
+ deleteLink.add(new JavascriptEventConfirmation("onclick", MessageFormat.format(
+ "Delete team \"{0}\"?", entry.name)));
+ teamLinks.add(deleteLink);
+ item.add(teamLinks);
+
+ WicketUtils.setAlternatingBackground(item, counter);
+ counter++;
+ }
+ };
+ add(teamsView.setVisible(showAdmin));
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/panels/UsersPanel.html b/src/main/java/com/gitblit/wicket/panels/UsersPanel.html
new file mode 100644
index 00000000..80159610
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/UsersPanel.html
@@ -0,0 +1,54 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+
+<body>
+<wicket:panel>
+
+ <div wicket:id="adminPanel">[admin links]</div>
+
+ <table class="repositories">
+ <tr>
+ <th class="left">
+ <img style="vertical-align: middle; border: 1px solid #888; background-color: white;" src="user_16x16.png"/>
+ <wicket:message key="gb.users">[users]</wicket:message>
+ </th>
+ <th class="hidden-phone hidden-tablet left"><wicket:message key="gb.displayName">[display name]</wicket:message></th>
+ <th class="hidden-phone hidden-tablet left"><wicket:message key="gb.emailAddress">[email address]</wicket:message></th>
+ <th class="hidden-phone" style="width:140px;"><wicket:message key="gb.type">[type]</wicket:message></th>
+ <th class="hidden-phone" style="width:140px;"><wicket:message key="gb.teamMemberships">[team memberships]</wicket:message></th>
+ <th class="hidden-phone" style="width:100px;"><wicket:message key="gb.repositories">[repositories]</wicket:message></th>
+ <th style="width:80px;" class="right"></th>
+ </tr>
+ <tbody>
+ <tr wicket:id="userRow">
+ <td class="left" ><span class="list" wicket:id="username">[username]</span></td>
+ <td class="hidden-phone hidden-tablet left" ><span class="list" wicket:id="displayName">[display name]</span></td>
+ <td class="hidden-phone hidden-tablet left" ><span class="list" wicket:id="emailAddress">[email address]</span></td>
+ <td class="hidden-phone left" ><span style="font-size: 0.8em;" wicket:id="accountType">[account type]</span></td>
+ <td class="hidden-phone left" ><span class="list" wicket:id="teams">[team memberships]</span></td>
+ <td class="hidden-phone left" ><span class="list" wicket:id="repositories">[repositories]</span></td>
+ <td class="rightAlign"><span wicket:id="userLinks"></span></td>
+ </tr>
+ </tbody>
+ </table>
+
+ <wicket:fragment wicket:id="adminLinks">
+ <!-- page nav links -->
+ <div class="admin_nav">
+ <a class="btn-small" wicket:id="newUser" style="padding-right:0px;">
+ <i class="icon icon-plus-sign"></i>
+ <wicket:message key="gb.newUser"></wicket:message>
+ </a>
+ </div>
+ </wicket:fragment>
+
+ <wicket:fragment wicket:id="userAdminLinks">
+ <span class="link"><a wicket:id="editUser"><wicket:message key="gb.edit">[edit]</wicket:message></a> | <a wicket:id="deleteUser"><wicket:message key="gb.delete">[delete]</wicket:message></a></span>
+ </wicket:fragment>
+
+</wicket:panel>
+</body>
+</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/panels/UsersPanel.java b/src/main/java/com/gitblit/wicket/panels/UsersPanel.java
new file mode 100644
index 00000000..f5b95e20
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/UsersPanel.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.wicket.panels;
+
+import java.text.MessageFormat;
+import java.util.List;
+
+import org.apache.wicket.markup.html.basic.Label;
+import org.apache.wicket.markup.html.link.BookmarkablePageLink;
+import org.apache.wicket.markup.html.link.Link;
+import org.apache.wicket.markup.html.panel.Fragment;
+import org.apache.wicket.markup.repeater.Item;
+import org.apache.wicket.markup.repeater.data.DataView;
+import org.apache.wicket.markup.repeater.data.ListDataProvider;
+
+import com.gitblit.GitBlit;
+import com.gitblit.models.UserModel;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.wicket.WicketUtils;
+import com.gitblit.wicket.pages.EditUserPage;
+
+public class UsersPanel extends BasePanel {
+
+ private static final long serialVersionUID = 1L;
+
+ public UsersPanel(String wicketId, final boolean showAdmin) {
+ super(wicketId);
+
+ Fragment adminLinks = new Fragment("adminPanel", "adminLinks", this);
+ adminLinks.add(new BookmarkablePageLink<Void>("newUser", EditUserPage.class)
+ .setVisible(GitBlit.self().supportsAddUser()));
+ add(adminLinks.setVisible(showAdmin));
+
+ final List<UserModel> users = GitBlit.self().getAllUsers();
+ DataView<UserModel> usersView = new DataView<UserModel>("userRow",
+ new ListDataProvider<UserModel>(users)) {
+ private static final long serialVersionUID = 1L;
+ private int counter;
+
+ @Override
+ protected void onBeforeRender() {
+ super.onBeforeRender();
+ counter = 0;
+ }
+
+ public void populateItem(final Item<UserModel> item) {
+ final UserModel entry = item.getModelObject();
+ LinkPanel editLink = new LinkPanel("username", "list", entry.username,
+ EditUserPage.class, WicketUtils.newUsernameParameter(entry.username));
+ WicketUtils.setHtmlTooltip(editLink, getString("gb.edit") + " " + entry.getDisplayName());
+ item.add(editLink);
+
+ if (StringUtils.isEmpty(entry.displayName)) {
+ item.add(new Label("displayName").setVisible(false));
+ } else {
+ editLink = new LinkPanel("displayName", "list", entry.getDisplayName(),
+ EditUserPage.class, WicketUtils.newUsernameParameter(entry.username));
+ WicketUtils.setHtmlTooltip(editLink, getString("gb.edit") + " " + entry.getDisplayName());
+ item.add(editLink);
+ }
+
+ if (StringUtils.isEmpty(entry.emailAddress)) {
+ item.add(new Label("emailAddress").setVisible(false));
+ } else {
+ editLink = new LinkPanel("emailAddress", "list", entry.emailAddress,
+ EditUserPage.class, WicketUtils.newUsernameParameter(entry.username));
+ WicketUtils.setHtmlTooltip(editLink, getString("gb.edit") + " " + entry.getDisplayName());
+ item.add(editLink);
+ }
+
+ item.add(new Label("accountType", entry.accountType.name() + (entry.canAdmin() ? ", admin":"")));
+ item.add(new Label("teams", entry.teams.size() > 0 ? ("" + entry.teams.size()) : ""));
+ item.add(new Label("repositories",
+ entry.permissions.size() > 0 ? ("" + entry.permissions.size()) : ""));
+ Fragment userLinks = new Fragment("userLinks", "userAdminLinks", this);
+ userLinks.add(new BookmarkablePageLink<Void>("editUser", EditUserPage.class,
+ WicketUtils.newUsernameParameter(entry.username)));
+ Link<Void> deleteLink = new Link<Void>("deleteUser") {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public void onClick() {
+ if (GitBlit.self().deleteUser(entry.username)) {
+ users.remove(entry);
+ info(MessageFormat.format(getString("gb.userDeleted"), entry.username));
+ } else {
+ error(MessageFormat.format(getString("gb.deleteUserFailed"),
+ entry.username));
+ }
+ }
+ };
+ deleteLink.add(new JavascriptEventConfirmation("onclick", MessageFormat.format(
+ getString("gb.deleteUser"), entry.username)));
+ userLinks.add(deleteLink);
+ item.add(userLinks);
+
+ WicketUtils.setAlternatingBackground(item, counter);
+ counter++;
+ }
+ };
+ add(usersView.setVisible(showAdmin));
+ }
+}
diff --git a/src/main/java/log4j.properties b/src/main/java/log4j.properties
new file mode 100644
index 00000000..dc6dd2b5
--- /dev/null
+++ b/src/main/java/log4j.properties
@@ -0,0 +1,66 @@
+#------------------------------------------------------------------------------
+#
+# The following properties set the logging levels and log appender. The
+# log4j.rootCategory variable defines the default log level and one or more
+# appenders. For the console, use 'S'. For the daily rolling file, use 'R'.
+# For an HTML formatted log, use 'H'.
+#
+# To override the default (rootCategory) log level, define a property of the
+# form (see below for available values):
+#
+# log4j.logger. =
+#
+# Available logger names:
+# TODO
+#
+# Possible Log Levels:
+# FATAL, ERROR, WARN, INFO, DEBUG
+#
+#------------------------------------------------------------------------------
+log4j.rootCategory=INFO, S
+
+#log4j.rootLogger=INFO
+#log4j.logger.org=INFO
+#log4j.logger.com=INFO
+#log4j.logger.net=INFO
+
+#log4j.logger.com.gitblit=DEBUG
+
+log4j.logger.org.apache.wicket=INFO
+log4j.logger.org.apache.wicket.RequestListenerInterface=WARN
+log4j.logger.org.apache.wicket.protocol.http.HttpSessionStore=WARN
+
+#------------------------------------------------------------------------------
+#
+# The following properties configure the console (stdout) appender.
+# See http://logging.apache.org/log4j/docs/api/index.html for details.
+#
+#------------------------------------------------------------------------------
+log4j.appender.S = org.apache.log4j.ConsoleAppender
+log4j.appender.S.layout = org.apache.log4j.PatternLayout
+log4j.appender.S.layout.ConversionPattern = %-5p %m%n
+
+#------------------------------------------------------------------------------
+#
+# The following properties configure the Daily Rolling File appender.
+# See http://logging.apache.org/log4j/docs/api/index.html for details.
+#
+#------------------------------------------------------------------------------
+log4j.appender.R = org.apache.log4j.DailyRollingFileAppender
+log4j.appender.R.File = logs/gitblit.log
+log4j.appender.R.Append = true
+log4j.appender.R.DatePattern = '.'yyy-MM-dd
+log4j.appender.R.layout = org.apache.log4j.PatternLayout
+log4j.appender.R.layout.ConversionPattern = %d{yyyy-MM-dd HH:mm:ss} %c{1} [%p] %m%n
+
+#------------------------------------------------------------------------------
+#
+# The following properties configure the Rolling File appender in HTML.
+# See http://logging.apache.org/log4j/docs/api/index.html for details.
+#
+#------------------------------------------------------------------------------
+log4j.appender.H = org.apache.log4j.RollingFileAppender
+log4j.appender.H.File = logs/gitblit.html
+log4j.appender.H.MaxFileSize = 100KB
+log4j.appender.H.Append = false
+log4j.appender.H.layout = org.apache.log4j.HTMLLayout