James Moger
2014-06-16 6b18b0761b726fd9aef1ebcc21b760378f7d4b5c
Merge release 1.6.0
5 files deleted
65 files added
142 files modified
14893 ■■■■ changed files
.classpath 3 ●●●● patch | view | raw | blame | history
.gitignore 4 ●●● patch | view | raw | blame | history
.gitmodules 3 ●●●●● patch | view | raw | blame | history
HOME.md 5 ●●●●● patch | view | raw | blame | history
README.markdown 49 ●●●● patch | view | raw | blame | history
build.moxie 11 ●●●● patch | view | raw | blame | history
build.xml 136 ●●●●● patch | view | raw | blame | history
gitblit.iml 17 ●●●● patch | view | raw | blame | history
release.template 180 ●●●● patch | view | raw | blame | history
releases.moxie 120 ●●●● patch | view | raw | blame | history
src/main/distrib/data/gitblit.properties 33 ●●●●● patch | view | raw | blame | history
src/main/distrib/data/gitignore @ 097db8 1 ●●●● patch | view | raw | blame | history
src/main/distrib/linux/migrate-tickets.sh 21 ●●●●● patch | view | raw | blame | history
src/main/distrib/linux/reindex-tickets.sh 2 ●●● patch | view | raw | blame | history
src/main/distrib/win/migrate-tickets.cmd 21 ●●●●● patch | view | raw | blame | history
src/main/java/WEB-INF/web.xml 53 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/ConfigUserService.java 29 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/Constants.java 36 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/DaggerModule.java 9 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/FederationClient.java 9 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/GitBlit.java 34 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/MigrateTickets.java 256 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/ReindexTickets.java 2 ●●● patch | view | raw | blame | history
src/main/java/com/gitblit/client/GitblitClient.java 8 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/dagger/DaggerFilter.java 4 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/extensions/GitblitWicketPlugin.java 49 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/extensions/HttpRequestFilter.java 49 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/extensions/LifeCycleListener.java 43 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/extensions/NavLinkExtension.java 40 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/extensions/RepositoryLifeCycleListener.java 45 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/extensions/RepositoryNavLinkExtension.java 42 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/extensions/UserMenuExtension.java 40 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/extensions/UserTeamLifeCycleListener.java 62 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/git/GitblitReceivePack.java 148 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/git/PatchsetReceivePack.java 8 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/manager/GitblitManager.java 25 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/manager/INotificationManager.java 8 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/manager/IRepositoryManager.java 9 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/manager/IRuntimeManager.java 27 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/manager/NotificationManager.java 5 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/manager/RepositoryManager.java 42 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/manager/RuntimeManager.java 39 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/manager/ServicesManager.java 21 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/manager/UserManager.java 124 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/models/Menu.java 302 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/models/NavLink.java 140 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/models/UserPreferences.java 30 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/service/MailService.java 2 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/servlet/AccessRestrictionFilter.java 5 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/servlet/AuthenticationFilter.java 3 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/servlet/EnforceAuthenticationFilter.java 3 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/servlet/FilterRuntimeConfig.java 71 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/servlet/GitFilter.java 5 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/servlet/GitblitContext.java 48 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/servlet/PagesFilter.java 99 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/servlet/PagesServlet.java 302 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/servlet/ProxyFilter.java 86 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/servlet/RawFilter.java 126 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/servlet/RawServlet.java 483 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/servlet/RpcFilter.java 5 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/servlet/RpcServlet.java 49 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/servlet/SyndicationFilter.java 5 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/servlet/SyndicationServlet.java 133 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/tickets/BranchTicketService.java 67 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/tickets/FileTicketService.java 68 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/tickets/ITicketService.java 105 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/tickets/NullTicketService.java 11 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/tickets/QueryResult.java 8 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/tickets/RedisTicketService.java 70 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/tickets/TicketLabel.java 7 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/tickets/TicketMilestone.java 12 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/tickets/TicketNotifier.java 11 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/tickets/commands.md 2 ●●● patch | view | raw | blame | history
src/main/java/com/gitblit/transport/ssh/SshDaemon.java 2 ●●● patch | view | raw | blame | history
src/main/java/com/gitblit/utils/JGitUtils.java 48 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/utils/MarkdownUtils.java 4 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/utils/RpcUtils.java 16 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/utils/SyndicationUtils.java 57 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/GitBlitWebApp.java 128 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/GitBlitWebApp.properties 78 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/GitBlitWebApp_de.properties 745 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/GitBlitWebApp_fr.properties 2 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/GitBlitWebApp_it.properties 745 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/GitBlitWebApp_ko.properties 70 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/GitBlitWebApp_nl.properties 90 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/GitBlitWebApp_no.properties 73 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/GitBlitWebApp_zh_CN.properties 74 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/GitblitWicketApp.java 72 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/MarkupProcessor.java 11 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/PageRegistration.java 243 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/PluginClassResolver.java 120 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/SessionlessForm.java 15 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/TicketsUI.java 211 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/UrlExternalFormComparator.java 39 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/WicketUtils.java 18 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/ActivityPage.java 16 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/BasePage.java 4 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/BlobPage.java 12 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/CommitDiffPage.java 5 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/CommitPage.java 5 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/ComparePage.java 5 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/DashboardPage.java 14 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/DocPage.java 6 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/DocsPage.java 9 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/EditMilestonePage.html 39 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/EditMilestonePage.java 196 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/EditRepositoryPage.html 219 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/EditRepositoryPage.java 435 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/EditTeamPage.java 6 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage.html 87 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage.java 17 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_de.html 60 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_es.html 88 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_it.html 60 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_ko.html 95 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_nl.html 91 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_no.html 60 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_pl.html 86 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_pt_BR.html 86 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_zh_CN.html 89 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/MyTicketsPage.html 84 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/MyTicketsPage.java 392 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/NewMilestonePage.html 37 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/NewMilestonePage.java 140 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/NewRepositoryPage.html 41 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/NewRepositoryPage.java 372 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/ProjectPage.java 30 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/ProjectsPage.java 14 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/RepositoriesPage.java 25 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/RepositoryPage.html 2 ●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/RepositoryPage.java 98 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/RootPage.html 14 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/RootPage.java 1307 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/TeamsPage.html 13 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/TeamsPage.java 30 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/TicketBasePage.java 126 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/TicketPage.java 37 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/TicketsPage.html 89 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/TicketsPage.java 418 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/TreePage.java 6 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/UserPage.html 67 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/UserPage.java 229 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/UsersPage.html 2 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/UsersPage.java 3 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/create_git.md 6 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/existing_git.md 2 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/propose_git.md 2 ●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/AccessPolicyPanel.html 31 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/AccessPolicyPanel.java 199 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/BasePanel.java 4 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/BooleanChoiceOption.html 19 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/BooleanChoiceOption.java 78 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/BooleanOption.html 17 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/BooleanOption.java 54 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/BranchesPanel.html 6 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/BranchesPanel.java 5 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/ChoiceOption.html 19 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/ChoiceOption.java 52 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/DropDownMenu.java 74 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/FilterableRepositoryList.java 3 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/NavigationPanel.java 60 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/ProjectRepositoryPanel.html 9 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/ProjectRepositoryPanel.java 34 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/RegistrantPermissionsPanel.html 2 ●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/RepositoriesPanel.html 24 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/RepositoriesPanel.java 121 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/RepositoryNamePanel.html 30 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/RepositoryNamePanel.java 172 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/SshKeysPanel.html 46 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/SshKeysPanel.java 169 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/TagsPanel.java 11 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/TextAreaOption.html 20 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/TextAreaOption.java 54 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/TextOption.html 20 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/TextOption.java 53 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/TicketListPanel.html 55 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/TicketListPanel.java 242 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/TicketSearchForm.java 78 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/UserTitlePanel.html 16 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/UserTitlePanel.java 32 ●●●●● patch | view | raw | blame | history
src/main/java/login_de.mkd 3 ●●●●● patch | view | raw | blame | history
src/main/java/login_it.mkd 4 ●●●● patch | view | raw | blame | history
src/main/java/pt.py 21 ●●●● patch | view | raw | blame | history
src/main/java/welcome_de.mkd 3 ●●●●● patch | view | raw | blame | history
src/main/java/welcome_it.mkd 3 ●●●●● patch | view | raw | blame | history
src/site/features.mkd 19 ●●●●● patch | view | raw | blame | history
src/site/openshift.mkd 56 ●●●●● patch | view | raw | blame | history
src/site/plugins_extensions.mkd 201 ●●●●● patch | view | raw | blame | history
src/site/plugins_overview.mkd 14 ●●●● patch | view | raw | blame | history
src/site/roadmap.mkd 7 ●●●●● patch | view | raw | blame | history
src/site/rpc.mkd 9 ●●●●● patch | view | raw | blame | history
src/site/setup_express.mkd 64 ●●●●● patch | view | raw | blame | history
src/site/setup_transport_ssh.mkd 22 ●●●●● patch | view | raw | blame | history
src/site/siteindex.mkd 27 ●●●● patch | view | raw | blame | history
src/site/tickets_overview.mkd 25 ●●●● patch | view | raw | blame | history
src/site/tickets_replication.mkd 24 ●●●●● patch | view | raw | blame | history
src/site/tickets_setup.mkd 2 ●●● patch | view | raw | blame | history
src/site/tickets_using.mkd 23 ●●●●● patch | view | raw | blame | history
src/site/upgrade_express.mkd 23 ●●●●● patch | view | raw | blame | history
src/test/config/test-users.conf 6 ●●●● patch | view | raw | blame | history
src/test/java/com/gitblit/tests/AuthenticationManagerTest.java 2 ●●● patch | view | raw | blame | history
src/test/java/com/gitblit/tests/BranchTicketServiceTest.java 4 ●●●● patch | view | raw | blame | history
src/test/java/com/gitblit/tests/FileTicketServiceTest.java 4 ●●●● patch | view | raw | blame | history
src/test/java/com/gitblit/tests/HtpasswdAuthenticationTest.java 8 ●●●● patch | view | raw | blame | history
src/test/java/com/gitblit/tests/LdapAuthenticationTest.java 10 ●●●● patch | view | raw | blame | history
src/test/java/com/gitblit/tests/LuceneExecutorTest.java 4 ●●●● patch | view | raw | blame | history
src/test/java/com/gitblit/tests/RedisTicketServiceTest.java 4 ●●●● patch | view | raw | blame | history
src/test/java/com/gitblit/tests/RedmineAuthenticationTest.java 6 ●●●● patch | view | raw | blame | history
src/test/java/com/gitblit/tests/RpcTests.java 63 ●●●●● patch | view | raw | blame | history
src/test/java/com/gitblit/tests/SyndicationUtilsTest.java 36 ●●●●● patch | view | raw | blame | history
src/test/java/com/gitblit/tests/TicketServiceTest.java 5 ●●●● patch | view | raw | blame | history
src/test/java/com/gitblit/tests/mock/MockRuntimeManager.java 15 ●●●●● patch | view | raw | blame | history
.classpath
@@ -13,7 +13,7 @@
    <classpathentry kind="lib" path="ext/log4j-1.2.17.jar" sourcepath="ext/src/log4j-1.2.17.jar" />
    <classpathentry kind="lib" path="ext/slf4j-api-1.7.5.jar" sourcepath="ext/src/slf4j-api-1.7.5.jar" />
    <classpathentry kind="lib" path="ext/slf4j-log4j12-1.7.5.jar" sourcepath="ext/src/slf4j-log4j12-1.7.5.jar" />
    <classpathentry kind="lib" path="ext/mail-1.4.3.jar" sourcepath="ext/src/mail-1.4.3.jar" />
    <classpathentry kind="lib" path="ext/javax.mail-1.5.1.jar" sourcepath="ext/src/javax.mail-1.5.1.jar" />
    <classpathentry kind="lib" path="ext/javax.servlet-api-3.1.0.jar" sourcepath="ext/src/javax.servlet-api-3.1.0.jar" />
    <classpathentry kind="lib" path="ext/jetty-all-9.1.4.v20140401.jar" sourcepath="ext/src/jetty-all-9.1.4.v20140401.jar" />
    <classpathentry kind="lib" path="ext/wicket-1.4.21.jar" sourcepath="ext/src/wicket-1.4.21.jar" />
@@ -76,6 +76,7 @@
    <classpathentry kind="lib" path="ext/jedis-2.3.1.jar" sourcepath="ext/src/jedis-2.3.1.jar" />
    <classpathentry kind="lib" path="ext/commons-pool2-2.0.jar" sourcepath="ext/src/commons-pool2-2.0.jar" />
    <classpathentry kind="lib" path="ext/pf4j-0.8.0.jar" sourcepath="ext/src/pf4j-0.8.0.jar" />
    <classpathentry kind="lib" path="ext/tika-core-1.5.jar" sourcepath="ext/src/tika-core-1.5.jar" />
    <classpathentry kind="lib" path="ext/junit-4.11.jar" sourcepath="ext/src/junit-4.11.jar" />
    <classpathentry kind="lib" path="ext/hamcrest-core-1.3.jar" sourcepath="ext/src/hamcrest-core-1.3.jar" />
    <classpathentry kind="lib" path="ext/selenium-java-2.28.0.jar" sourcepath="ext/src/selenium-java-2.28.0.jar" />
.gitignore
@@ -24,4 +24,6 @@
/*.sh
/*.factorypath
/src/main/dagger
/src/main/java/.idea
/**/.idea
/**/init.lua
/**/session
.gitmodules
New file
@@ -0,0 +1,3 @@
[submodule "src/main/distrib/data/gitignore"]
    path = src/main/distrib/data/gitignore
    url = https://github.com/github/gitignore.git
HOME.md
@@ -18,11 +18,6 @@
[[src/site/setup_war.mkd]]
[[src/site/upgrade_war.mkd]]
### Gitblit Express for OpenShift
[[src/site/setup_express.mkd]]
[[src/site/upgrade_express.mkd]]
### General Configuration & Administration
[[src/site/setup_authentication.mkd]]
README.markdown
@@ -1,10 +1,11 @@
Gitblit
=================
Gitblit is an open source, pure Java Git solution for managing, viewing, and serving [Git](http://git-scm.com) repositories.
Gitblit is an open source, pure Java Git solution for managing, viewing, and serving [Git](http://git-scm.com) repositories.  It can serve repositories over the GIT, HTTP, and SSH transports; it can authenticate against multiple providers; and it allows you to get up-and-running with an attractive, capable Git server in less than 5 minutes.
More information about Gitblit can be found [here](http://gitblit.com).
[ ![Download](https://api.bintray.com/packages/gitblit/releases/stable/images/download.png) ](https://bintray.com/gitblit/releases/stable/_latestVersion)
<a href='https://bintray.com/gitblit/releases/gitblit/_latestVersion'><img src='https://api.bintray.com/packages/gitblit/releases/gitblit/images/download.png'></a>
License
-------
@@ -20,11 +21,45 @@
Getting help
------------
Read the online documentation available at the [Gitblit website](http://gitblit.com)
Issues, binaries, & sources @ [Google Code](http://code.google.com/p/gitblit)
| Source        | Location                                               |
| ------------- |--------------------------------------------------------|
| Documentation | [Gitblit website](http://gitblit.com)                  |
| Issues        | [Google Code](http://code.google.com/p/gitblit)        |
| Forums        | [Google Groups](https://groups.google.com/forum/#!forum/gitblit) |
| Twitter       | @gitblit or @jamesmoger                                |
| Google+       | +gitblit or +jamesmoger                                |
Contributing
------------
GitHub pull requests or Gitblit Tickets are preferred.  Any contributions must be distributed under the terms of the [Apache Software Foundation license, version 2.0](http://www.apache.org/licenses/LICENSE-2.0).
**Workflow**
Gitblit practices the [git-flow][1] branching model.
- **master** is the current stable release + fixes accumulated since release.
- **develop** is the integration branch for the next major release.
- **ticket/N** are feature or hotfix branches to be merged to **master** or **develop**, as appropriate.
**Feature Development**
Development of new features is mostly done using [Gitblit Tickets][2] hosted at [dev.gitblit.com][3].  This allows continuous dogfooding and improvement of Gitbit's own issue-tracker and pull-request mechanism.
**Release Planning**
Release planning is mostly done using Gitblit Milestones and Gitblit Tickets hosted at [dev.gitblit.com][3].
**Releasing**
When Gitblit is preparing for a release, a **release-{milestone}** branch will be created, tested, & fixed until it is ready to be merged to **master** and tagged as the next major release.  After the release is tagged, the **release-{milestone}** branch will also be merged back into **develop** and then the release branch will be removed.
Building Gitblit
----------------
Gitblit uses submodules.
Make sure to clone using `--recursive` OR to execute `git submodule update --init --recursive`.
[Eclipse](http://eclipse.org) is recommended for development as the project settings are preconfigured.
1. Import the gitblit project into your Eclipse workspace.
@@ -39,4 +74,8 @@
Building Tips & Tricks
----------------------
1. If you are running Ant from an ANSI-capable console, consider setting the `MX_COLOR` environment variable before executing Ant.<pre>set MX_COLOR=true</pre>
2. The build script will honor your Maven proxy settings.  If you need to fine-tune this, please review the [settings.moxie](http://gitblit.github.io/moxie/settings.html) documentation.
2. The build script will honor your Maven proxy settings.  If you need to fine-tune this, please review the [settings.moxie](http://gitblit.github.io/moxie/settings.html) documentation.
[1]: http://nvie.com/posts/a-successful-git-branching-model
[2]: http://gitblit.com/tickets_overview.html
[3]: https://dev.gitblit.com
build.moxie
@@ -3,19 +3,19 @@
#
# Specify minimum Moxie version required to build
requires: 0.9.1
requires: 0.9.2
# Project Metadata
name: Gitblit
description: pure Java Git solution
groupId: com.gitblit
artifactId: gitblit
version: 1.5.2-SNAPSHOT
version: 1.6.0
inceptionYear: 2011
# Current stable release
releaseVersion: 1.5.1
releaseDate: 2014-05-07
releaseVersion: 1.6.0
releaseDate: 2014-06-16
# Project urls
url: 'http://gitblit.com'
@@ -133,7 +133,7 @@
- compile 'log4j:log4j:1.2.17' :war :fedclient :authority
- compile 'org.slf4j:slf4j-api:1.7.5' :war :fedclient :authority
- compile 'org.slf4j:slf4j-log4j12:1.7.5' :war :fedclient :authority
- compile 'javax.mail:mail:1.4.3' :war :authority
- compile 'com.sun.mail:javax.mail:1.5.1' :war :authority
- compile 'javax.servlet:javax.servlet-api:3.1.0' :fedclient
- compile 'org.eclipse.jetty.aggregate:jetty-all:${jetty.version}' @jar
- compile 'org.apache.wicket:wicket:${wicket.version}' :war !org.mockito
@@ -174,6 +174,7 @@
- compile 'commons-codec:commons-codec:1.7' :war
- compile 'redis.clients:jedis:2.3.1' :war
- compile 'ro.fortsoft.pf4j:pf4j:0.8.0' :war
- compile 'org.apache.tika:tika-core:1.5' :war
- test 'junit'
# Dependencies for Selenium web page testing
- test 'org.seleniumhq.selenium:selenium-java:${selenium.version}' @jar
build.xml
@@ -8,7 +8,7 @@
        documentation @ http://gitblit.github.io/moxie
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    -->
    <property name="moxie.version" value="0.9.1" />
    <property name="moxie.version" value="0.9.2" />
    <property name="moxie.url" value="http://gitblit.github.io/moxie/maven" />
    <property name="moxie.jar" value="moxie-toolkit-${moxie.version}.jar" />
    <property name="moxie.dir" value="${user.home}/.moxie" />
@@ -48,7 +48,6 @@
        <property name="manager.zipfile" value="manager-${project.version}.zip" />
        <property name="authority.zipfile" value="authority-${project.version}.zip" />
        <property name="gbapi.zipfile" value="gbapi-${project.version}.zip" />
        <property name="express.zipfile" value="express-${project.version}.zip" />
        <property name="maven.directory" value="${basedir}/../gitblit-maven" />
        <!-- Download links -->
@@ -175,7 +174,18 @@
        <property name="go.dir" value="${project.outputDirectory}/go" />    
        <delete dir="${go.dir}" />
        <local name="webinf" />
        <property name="webinf" value="${project.compileOutputDirectory}/WEB-INF" />
        <prepareDataDirectory toDir="${go.dir}/data" />
        <!-- Copy the web.xml from the prototype web.xml -->
        <copy todir="${webinf}" overwrite="true">
            <fileset file="${project.src.dir}/WEB-INF/web.xml" />
            <filterset>
                <filter token="gb.version" value="${project.version}" />
            </filterset>
        </copy>
        
        <!-- Build jar -->
        <mx:jar destfile="${go.dir}/gitblit.jar" includeresources="true">
@@ -241,10 +251,13 @@
        <!-- Prepare the data directory -->
        <prepareDataDirectory toDir="${webinf}/data" />
        <!-- Build the WAR web.xml from the prototype web.xml -->
        <mx:webxml sourcefile="${project.src.dir}/WEB-INF/web.xml" destfile="${webinf}/web.xml">
            <replace token="@gb.version@" value="${project.version}" />
        </mx:webxml>
        <!-- Copy the web.xml from the prototype web.xml -->
        <copy todir="${webinf}" overwrite="true">
            <fileset file="${project.src.dir}/WEB-INF/web.xml" />
            <filterset>
                <filter token="gb.version" value="${project.version}" />
            </filterset>
        </copy>
        <!-- Gitblit jar -->
        <mx:jar destfile="${webinf}/lib/gitblit.jar" includeresources="false" />
@@ -306,61 +319,6 @@
        <!-- Cleanup -->
        <delete file="${project.targetDirectory}/fedclient.jar" />
        
    </target>
    <!--
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        Build a Gitblit filesystem for deployment to RedHat OpenShift Express
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    -->
    <target name="buildExpress" depends="compile" description="Build exploded WAR file suitable for deployment to OpenShift Express">
        <echo>Building Gitblit Express for RedHat OpenShift ${project.version}</echo>
        <local name="express.dir" />
        <property name="express.dir" value="${project.outputDirectory}/express" />
        <delete dir="${express.dir}" />
        <!-- Create the OpenShift filesystem -->
        <local name="deployments.root" />
        <property name="deployments.root" value="${express.dir}/deployments/ROOT.war"/>
        <mkdir dir="${deployments.root}" />
        <touch file="${express.dir}/deployments/ROOT.war.dodeploy" />
        <local name="webinf" />
        <property name="webinf" value="${deployments.root}/WEB-INF" />
        <!-- Prepare the data directory -->
        <prepareDataDirectory toDir="${webinf}/data" />
        <!-- Build the Express web.xml from the prototype web.xml and gitblit.properties -->
        <!-- THIS FILE IS NOT OVERRIDDEN ONCE IT IS BUILT!!! -->
        <mx:webxml sourcefile="${project.src.dir}/WEB-INF/web.xml" destfile="${webinf}/web.xml"
            propertiesFile="${project.distrib.dir}/data/gitblit.properties"
            skip="server.*">
            <replace token="@gb.version@" value="${project.version}" />
        </mx:webxml>
        <!-- Gitblit classes -->
        <mx:jar destfile="${webinf}/lib/gitblit.jar" includeresources="false" />
        <!-- Build Express Zip file -->
        <mx:zip basedir="${express.dir}" destfile="${project.targetDirectory}/${express.zipfile}">
            <fileset dir="${basedir}">
                <include name="LICENSE" />
                <include name="NOTICE" />
            </fileset>
            <!-- README -->
            <zipfileset fullpath="README.gitblit" file="${project.siteSourceDirectory}/openshift.mkd" />
            <!-- resources -->
            <zipfileset prefix="deployments/ROOT.war" dir="${project.resources.dir}">
                <exclude name="thumbs.db" />
                <exclude name="*.mkd" />
            </zipfileset>
            <!-- include "war" tagged dependencies -->
            <dependencies prefix="deployments/ROOT.war/WEB-INF/lib" tag="war" />
        </mx:zip>
    </target>
@@ -542,11 +500,6 @@
                        <page name="upgrade WAR" src="upgrade_war.mkd" />
                    </menu>
                    <divider />
                    <menu name="Gitblit Express" pager="true" pagerPlacement="bottom" pagerLayout="justified">
                        <page name="setup Express" src="setup_express.mkd" />
                        <page name="upgrade Express" src="upgrade_express.mkd" />
                    </menu>
                    <divider />
                    <menu name="Server Configuration" pager="true" pagerPlacement="bottom" pagerLayout="justified">
                        <page name="administration" src="administration.mkd" />
                        <page name="authentication" src="setup_authentication.mkd" />
@@ -604,7 +557,10 @@
                    <link name="Gitblit GO (Windows)" src="${gc.url}gitblit-${project.releaseVersion}.zip" />
                    <link name="Gitblit GO (Linux/OSX)" src="${gc.url}gitblit-${project.releaseVersion}.tar.gz" />
                    <link name="Gitblit WAR" src="${gc.url}gitblit-${project.releaseVersion}.war" />
                    <link name="Gitblit Express" src="${gc.url}express-${project.releaseVersion}.zip" />
                    <divider />
                    <link name="Gitblit GO (Docker)" src="https://registry.hub.docker.com/u/jmoger/gitblit/" />
                    <divider />
                    <link name="Plugins Registry" src="http://plugins.gitblit.com" />
                    <divider />
                    <link name="Gitblit Manager" src="${gc.url}manager-${project.releaseVersion}.zip" />
                    <link name="Federation Client" src="${gc.url}fedclient-${project.releaseVersion}.zip" />
@@ -620,7 +576,7 @@
                <menu name="links">
                    <link name="dev.gitblit.com (self-hosted)" src="https://dev.gitblit.com" />
                    <divider />
                    <link name="Plugin Registry" src="http://plugins.gitblit.com" />
                    <link name="Plugins Registry" src="http://plugins.gitblit.com" />
                    <divider />
                    <link name="Github" src="${project.scmUrl}" />
                    <link name="Issues" src="${project.issuesUrl}" />
@@ -704,7 +660,7 @@
        Build all binaries and site
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    -->
    <target name="buildAll" depends="buildGO,buildWAR,buildExpress,buildFederationClient,buildManager,buildApiLibrary,buildSite" />
    <target name="buildAll" depends="buildGO,buildWAR,buildFederationClient,buildManager,buildApiLibrary,buildSite" />
    
    <!--
@@ -756,11 +712,6 @@
        <bintrayUpload
            source="${project.targetDirectory}/${gbapi.zipfile}" 
            target="gbapi-${project.version}.zip" />
        <!-- Upload Gitblit Express for RedHat OpenShift -->
        <bintrayUpload
            source="${project.targetDirectory}/${express.zipfile}"
            target="express-${project.version}.zip" />
    </target>
@@ -829,16 +780,38 @@
            </filterset>
        </copy>
        <chmod file="${recipe}" perm="ugo+rx" />
    </target>
    <!--
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        Prepare for the next point release development cycle.
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    -->
    <target name="nextPointReleaseCycle" depends="prepare" description="prepare for the next point release development cycle">
        <!-- next cycle -->
        <mx:version stage="snapshot" incrementNumber="incremental" dryrun="${dryrun}" />
        <mx:commit showtitle="no">
            <message>Reset build identifiers for next development cycle</message>
            <message>Reset build identifiers for next point release cycle</message>
        </mx:commit>        
    </target>
        
    <!--
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        Prepare for the next minor release development cycle.
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    -->
    <target name="nextMinorReleaseCycle" depends="prepare" description="prepare for the next minor release development cycle">
        <!-- next cycle -->
        <mx:version stage="snapshot" incrementNumber="minor" dryrun="${dryrun}" />
        <mx:commit showtitle="no">
            <message>Reset build identifiers for next minor release cycle</message>
        </mx:commit>
    </target>
  <!--
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        Build Gitblit Docs
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -861,11 +834,6 @@
                        <menu name="Gitblit WAR" pager="true" pagerPlacement="bottom" pagerLayout="justified">
                            <page name="setup WAR" src="setup_war.mkd" />
                            <page name="upgrade WAR" src="upgrade_war.mkd" />
                        </menu>
                        <divider />
                        <menu name="Gitblit Express" pager="true" pagerPlacement="bottom" pagerLayout="justified">
                            <page name="setup Express" src="setup_express.mkd" />
                            <page name="upgrade Express" src="upgrade_express.mkd" />
                        </menu>
                        <divider />
                        <menu name="Server Configuration" pager="true" pagerPlacement="bottom" pagerLayout="justified">
@@ -916,7 +884,7 @@
                    <menu name="links">
                        <link name="dev.gitblit.com (self-hosted)" src="https://dev.gitblit.com" />
                        <divider />
                        <link name="Plugin Registry" src="http://plugins.gitblit.com" />
                        <link name="Plugins Registry" src="http://plugins.gitblit.com" />
                        <divider />
                        <link name="Github" src="${project.scmUrl}" />
                        <link name="Issues" src="${project.issuesUrl}" />
@@ -991,6 +959,12 @@
                    <include name="subgit.groovy" />
                </fileset>
            </copy>
            <mkdir dir="@{toDir}/gitignore" />
            <copy todir="@{toDir}/gitignore">
                <fileset dir="${project.distrib.dir}/data/gitignore">
                    <include name="*.gitignore" />
                </fileset>
            </copy>
      </sequential>
    </macrodef>
    
gitblit.iml
@@ -102,13 +102,13 @@
      </library>
    </orderEntry>
    <orderEntry type="module-library">
      <library name="mail-1.4.3.jar">
      <library name="javax.mail-1.5.1.jar">
        <CLASSES>
          <root url="jar://$MODULE_DIR$/ext/mail-1.4.3.jar!/" />
          <root url="jar://$MODULE_DIR$/ext/javax.mail-1.5.1.jar!/" />
        </CLASSES>
        <JAVADOC />
        <SOURCES>
          <root url="jar://$MODULE_DIR$/ext/src/mail-1.4.3.jar!/" />
          <root url="jar://$MODULE_DIR$/ext/src/javax.mail-1.5.1.jar!/" />
        </SOURCES>
      </library>
    </orderEntry>
@@ -790,6 +790,17 @@
        </SOURCES>
      </library>
    </orderEntry>
    <orderEntry type="module-library">
      <library name="tika-core-1.5.jar">
        <CLASSES>
          <root url="jar://$MODULE_DIR$/ext/tika-core-1.5.jar!/" />
        </CLASSES>
        <JAVADOC />
        <SOURCES>
          <root url="jar://$MODULE_DIR$/ext/src/tika-core-1.5.jar!/" />
        </SOURCES>
      </library>
    </orderEntry>
    <orderEntry type="module-library" scope="TEST">
      <library name="junit-4.11.jar">
        <CLASSES>
release.template
@@ -1,84 +1,96 @@
#!/bin/bash
#
# ${project.version} release script
#
# ensure Maven repository is up-to-date
echo ""
echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
echo "Preparing Maven repository"
echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
echo ""
cd ${maven.directory}
git checkout gh-pages
git pull
cd ${project.directory}
# go back one commit to RELEASE commit
echo ""
echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
echo "Checking out ${project.version} RELEASE commit ${project.commitId}"
echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
echo ""
git checkout ${project.commitId}
# build RELEASE artifacts
echo ""
echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
echo "Building ${project.version} RELEASE artifacts"
echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
echo ""
ant clean buildAll buildMavenArtifacts
# commit all generated artifacts and metadata
echo ""
echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
echo "Committing Maven repository ${project.version} RELEASE artifacts"
echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
echo ""
cd ${maven.directory}
git add .
git commit -m "${project.version} artifacts"
cd ${project.directory}
# upload artifacts
echo ""
echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
echo "Uploading ${project.version} artifacts"
echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
echo ""
ant publishBinaries
# build site, update gh-pages, and ftp upload site to hosting provider
echo ""
echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
echo "Building ${project.version} website"
echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
echo ""
ant publishSite
# return to project master
echo ""
echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
echo "Checking out master"
echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
echo ""
git checkout master
# push Maven repository to origin
echo ""
echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
echo "Pushing Maven repository"
echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
echo ""
cd ${maven.directory}
git push origin gh-pages
cd ${project.directory}
# push project branches
echo ""
echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
echo "Pushing master, gh-pages, and tag ${project.tag}"
echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
echo ""
git push origin master gh-pages tag ${project.tag}
#!/bin/bash
#
# ${project.version} release script
#
# ensure Maven repository is up-to-date
echo ""
echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
echo "Preparing Maven repository"
echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
echo ""
cd ${maven.directory}
git checkout gh-pages
git pull
cd ${project.directory}
# go back one commit to RELEASE commit
echo ""
echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
echo "Checking out ${project.version} RELEASE commit ${project.commitId}"
echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
echo ""
git checkout ${project.commitId}
# build RELEASE artifacts
echo ""
echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
echo "Building ${project.version} RELEASE artifacts"
echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
echo ""
ant clean buildAll buildMavenArtifacts
# commit all generated artifacts and metadata
echo ""
echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
echo "Committing Maven repository ${project.version} RELEASE artifacts"
echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
echo ""
cd ${maven.directory}
git add .
git commit -m "${project.version} artifacts"
cd ${project.directory}
# upload artifacts
echo ""
echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
echo "Uploading ${project.version} artifacts"
echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
echo ""
ant publishBinaries
# build site, update gh-pages, and ftp upload site to hosting provider
echo ""
echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
echo "Building ${project.version} website"
echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
echo ""
ant publishSite
# merge to master
echo ""
echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
echo "Merging release ${project.version} to master"
echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
echo ""
git checkout master
git merge -m "Merge release ${project.version}" ${project.commitId}
ant nextPointReleaseCycle
# merge to develop
echo ""
echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
echo "Merging release ${project.version} to develop"
echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
echo ""
git checkout develop
git merge -m "Merge release ${project.version}" ${project.commitId}
ant nextMinorReleaseCycle
# push Maven repository to origin
echo ""
echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
echo "Pushing Maven repository"
echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
echo ""
cd ${maven.directory}
git push origin gh-pages
cd ${project.directory}
# push project branches
echo ""
echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
echo "Pushing master, develop, gh-pages, and tag ${project.tag}"
echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
echo ""
git push origin master develop gh-pages tag ${project.tag}
releases.moxie
@@ -1,19 +1,104 @@
#
# ${project.version} release
# 1.6.0 release
#
r24: {
    title: ${project.name} ${project.version} released
    id: ${project.version}
    date: ${project.buildDate}
    note: ~
    title: Gitblit 1.6.0 released
    id: 1.6.0
    date: 2014-06-16
    note: ''
          The next major release (v1.7.0) will focus on:
          * ticket-75: making projects more useful including the concept of project ownership
          This improvement will require a NON-BACKWARDS-COMPATIBLE migration of repository ownership from the RpeositoryModel to the UserModel
          * ticket-55: facilitating usage of tickets & git-flow in the web ui
          ''
    html: ~
    text: ~
    text: ''
          Highlights:
          * My Tickets page
          * User Preferences web ui
          * SSH key management web ui
          * Basic CRUD pages for ticket milestones
          * Overhaul repository creation, editing, and empty repository pages
          If you are upgrading, you might consider copying the data/gitignore folder to your ${baseFolder} to allow selection & injection of a .gitignore when creating a repository.
          The OpenShift Express build has been dropped. You can deploy GO or WAR on Express so this build is no longer necessary.
          ''
    security: ~
    fixes: ~
    changes: ~
    additions: ~
    dependencyChanges: ~
    contributors: ~
    fixes:
    - Allow ticket responsible selection if anonymous push is enabled (issue-425, ticket-71)
    - Fix failure to generate SSH server keys on ARM (issue-426, ticket-70)
    - Fix flotr2 chart generation failure if a label contained a single-quote (ticket-77)
    - Fix repository cache refresh after ref deletion/addition (issue-433, ticket-82)
    - Fixed cache miss on repository model retrieval (pr-185, ticket-83)
    - Fixed GitBlit static singleton reference in localclone.groovy (issue-436, ticket-84)
    - Removed Ticket responsible team permission exclusion (ticket-87)
    - Fixed SSH daemon thread exhaustion (ticket-89)
    - Fixed Ticket responsible selections not considering the AUTHENTICATED authorization control (ticket-91)
    - Fixed invalid generated SSH url for port 22 (issue-444, ticket-98)
    - Fix cloning repositories with `+` in their names. (revert pr-136, issue-362, ticket-100)
    - Fixed NPE in GitblitClient (ticket-102)
    changes:
    - Split the pages servlet into a raw servlet and a pages servlet. All raw links now use the raw servlet (issue-413, ticket-49)
    - Drop deprecated --set-upstream syntax for -u (ticket-59)
    - BARNUM: Prune deleted branches on fetch (git fetch -p) (ticket-60)
    - BARNUM: Create ticket/N instead of topic/N for pt start N (ticket-61)
    - Move repository deletion functions to the edit repository page AND allow deletion to be disabled (pr-180, ticket-67)
    - Update the Korean translation (pr-184, ticket-69)
    - Update the Dutch translation (pr-191)
    - Overhaul the EmptyRepositoryPage (ticket-73)
    - Overhauled the edit repository page (ticket-76)
    - Process bugtraq links in the ticket description and comments (ticket-78)
    - Exclude personal repositories from the repositories list, by default (issue-419, ticket-95)
    additions:
    - Add My Tickets page (issue-215, ticket-15)
    - Added CRUD functionality for Ticket Milestones (ticket-17)
    - Implemented Ticket migration tool to move between backends (ticket-19)
    - Added extension points for top nav links, root-level pages, repository nav links, user menu links, and http request filters (ticket-23)
    - Added an editor panel in the user profile page to manipulate preferences (issue-108, issue-424, ticket-64)
    - Added an editor panel in the user profile page to manipulate public SSH keys (ticket-64)
    - Add FORK_REPOSITORY RPC request type (issue-371, pr-161, ticket-65)
    - Add object type (ot) parameter for RSS queries to retrieve tag details (pr-165, ticket-66)
    - Add setting to allow STARTTLS without requiring SMTPS (pr-183)
    - Simplified repository creation, offer simple README generation, and insertion of a pre-defined .gitignore file (ticket-76)
    - Added an extension point for monitoring onStartup and onShutdown (ticket-79)
    - Tag server-side merges when incremental push tags are enabled (issue-432, ticket-85)
    - Add a user preference for the clone transport (ticket-90)
    - Add setting to control default thread pool size for miscellaneous background tasks (ticket-92)
    - Add Norwegian transation (pr-186)
    - Add German translation (pr-192)
    - Add Italian translation (pr-196)
    dependencyChanges:
    - Update to javax.mail 1.5.1 (issue-417, ticket-58)
    contributors:
    - James Moger
    - David Ostrovsky
    - Manisha Gayathri
    - Gerard Smyth
    - Christian Buisson
    - Berke Viktor
    - Marcus Hunger
    - Matthias Cullmann
    - Emmeran Seehuber
    - Sascha Vogt
    - Carsten Lenz
    - Matthias Sohn
    - Leif Jantzen
    - Stardrad Yin
    - Jeroen Baten
    - Dongsu Kim
    - Karanbir Singh
    - Tamás Papp
    - GianMaria Romanato
    settings:
    - { name: 'web.allowDeletingNonEmptyRepositories', defaultValue: 'true' }
    - { name: 'web.includePersonalRepositories', defaultValue: 'false' }
    - { name: 'mail.starttls', defaultValue: 'false' }
    - { name: 'execution.defaultThreadPoolSize', defaultValue: '1' }
    - { name: 'git.gitignoreFolder', defaultValue: '${baseFolder}/gitignore' }
}
#
@@ -39,11 +124,12 @@
    - Fix transport determination for SSH urls served on port 22 (issue-421, ticket-63)
    changes:
    - improve French translation (pr-176)
    - simplify current plugin release detection and ignore the currentRelease registry field
    additions:
    - added TortoiseGit client app menu (pr-182)
    - simplify current plugin release detection and ignore the currentRelease registry field
    - split pages servlet into two servlets (issue-413)
    additions: ~
    dependencyChanges:
    - update to Apache MINA/SSHD 0.11.0 (issue-410)
    - added Apache Tiki 1.5 (issue-413)
    contributors:
    - James Moger
    - Julien Kirch
@@ -1328,6 +1414,6 @@
    - James Moger
}
snapshot: &r24
release: &r23
releases: &r[1..23]
snapshot: ~
release: &r24
releases: &r[1..24]
src/main/distrib/data/gitblit.properties
@@ -271,6 +271,11 @@
# SINCE 1.4.0
git.createRepositoriesShared = false
# Directory for gitignore templates used during repository creation.
#
# SINCE 1.6.0
git.gitignoreFolder = ${baseFolder}/gitignore
# Enable JGit-based garbage collection. (!!EXPERIMENTAL!!)
#
# USE AT YOUR OWN RISK!
@@ -613,6 +618,12 @@
# SINCE 1.5.0
plugins.registry = http://plugins.gitblit.com/plugins.json
# Number of threads used to handle miscellaneous tasks in the background.
#
# SINCE 1.6.0
# RESTART REQUIRED
execution.defaultThreadPoolSize = 1
#
# Groovy Integration
#
@@ -765,6 +776,16 @@
#
# SINCE 0.5.0
web.allowCookieAuthentication = true
# Allow deletion of non-empty repositories. This is enforced for all delete vectors.
#
# SINCE 1.6.0
web.allowDeletingNonEmptyRepositories = true
# Setting to include personal repositories in the main repositories list.
#
# SINCE 1.6.0
web.includePersonalRepositories = false
# Config file for storing project metadata
#
@@ -1410,6 +1431,11 @@
# use SMTPs flag
mail.smtps = false
# use STARTTLS flag
#
# SINCE 1.6.0
mail.starttls = false
# if your smtp server requires authentication, supply the credentials here
#
# SINCE 0.6.0
@@ -1779,7 +1805,8 @@
realm.redmine.url = http://example.com/redmine
#
# Server Settings
# Gitblit GO Server Settings
# The following settings only affect the integrated GO variant.
#
# The temporary folder to decompress the embedded gitblit webapp. 
@@ -1789,7 +1816,9 @@
# BASEFOLDER
server.tempFolder = ${baseFolder}/temp
# Specify the maximum number of concurrent http/https worker threads to allow.
# Specify the maximum number of concurrent http/https Jetty worker
# threads to allow.  This setting does not affect other threaded
# daemons and components of Gitblit.
#
# SINCE 1.3.0
# RESTART REQUIRED
src/main/distrib/data/gitignore
New file
@@ -0,0 +1 @@
Subproject commit 097db81c08b138dea7cb031eb18eeb16afe44bdf
src/main/distrib/linux/migrate-tickets.sh
New file
@@ -0,0 +1,21 @@
#!/bin/bash
# --------------------------------------------------------------------------
# This is for migrating Tickets from one service to another.
#
# usage:
#
#     migrate-tickets.sh <outputservice> <baseFolder>
#
# --------------------------------------------------------------------------
if [[ -z $1 || -z $2 ]]; then
    echo "Please specify the output ticket service and your baseFolder!";
    echo "";
    echo "usage:";
    echo "    migrate-tickets <outputservice> <baseFolder>";
    echo "";
    exit 1;
fi
java -cp gitblit.jar:./ext/* com.gitblit.MigrateTickets $1 --baseFolder $2
src/main/distrib/linux/reindex-tickets.sh
@@ -11,7 +11,7 @@
#
# --------------------------------------------------------------------------
if [ -z $1 ]; then
if [[ -z $1 ]]; then
    echo "Please specify your baseFolder!";
    echo "";
    echo "usage:";
src/main/distrib/win/migrate-tickets.cmd
New file
@@ -0,0 +1,21 @@
@REM --------------------------------------------------------------------------
@REM This is for migrating Tickets from one service to another.
@REM
@REM usage:
@REM     migrate-tickets <outputservice> <baseFolder>
@REM
@REM --------------------------------------------------------------------------
@if [%1]==[] goto help
@if [%2]==[] goto help
@java -cp gitblit.jar;"%CD%\ext\*" com.gitblit.MigrateTickets %1 --baseFolder %2
@goto end
:help
@echo "Please specify the output ticket service and your baseFolder!"
@echo
@echo "    migrate-tickets com.gitblit.tickets.RedisTicketService c:/gitblit-data"
@echo
:end
src/main/java/WEB-INF/web.xml
@@ -30,12 +30,8 @@
    </env-entry>
    
    <!-- Gitblit Displayname -->
    <display-name>
        Gitblit - @gb.version@
    </display-name>
    <display-name>Gitblit - @gb.version@</display-name>
    <!-- PARAMS -->
     
<!-- Gitblit Context Listener --><!-- STRIP     
    <listener>
@@ -134,6 +130,21 @@
    </servlet-mapping>    
    <!-- Raw Servlet
         <url-pattern> MUST match:
            * RawFilter
            * com.gitblit.Constants.RAW_PATH
            * Wicket Filter ignorePaths parameter -->
    <servlet>
        <servlet-name>RawServlet</servlet-name>
        <servlet-class>com.gitblit.servlet.RawServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>RawServlet</servlet-name>
        <url-pattern>/raw/*</url-pattern>
    </servlet-mapping>
    <!-- Pages Servlet
         <url-pattern> MUST match: 
            * PagesFilter
@@ -199,6 +210,15 @@
        <url-pattern>/robots.txt</url-pattern>
    </servlet-mapping>
    <filter>
        <filter-name>ProxyFilter</filter-name>
        <filter-class>com.gitblit.servlet.ProxyFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>ProxyFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    <!-- Git Access Restriction Filter
         <url-pattern> MUST match: 
            * GitServlet
@@ -263,7 +283,22 @@
    </filter-mapping>
    <!-- Pges Restriction Filter
    <!-- Branch Restriction Filter
         <url-pattern> MUST match:
            * RawServlet
            * com.gitblit.Constants.BRANCH_PATH
            * Wicket Filter ignorePaths parameter -->
    <filter>
        <filter-name>RawFilter</filter-name>
        <filter-class>com.gitblit.servlet.RawFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>RawFilter</filter-name>
        <url-pattern>/raw/*</url-pattern>
    </filter-mapping>
    <!-- Pages Restriction Filter
         <url-pattern> MUST match: 
            * PagesServlet
            * com.gitblit.Constants.PAGES_PATH
@@ -310,10 +345,12 @@
                 * FederationServlet <url-pattern>
                 * RpcFilter <url-pattern>
                 * RpcServlet <url-pattern>
                 * RawFilter <url-pattern>
                 * RawServlet <url-pattern>
                 * PagesFilter <url-pattern>
                 * PagesServlet <url-pattern>
                 * com.gitblit.Constants.PAGES_PATH -->
            <param-value>r/,git/,pt,feed/,zip/,federation/,rpc/,pages/,robots.txt,logo.png,graph/,sparkleshare/</param-value>
            <param-value>r/,git/,pt,feed/,zip/,federation/,rpc/,raw/,pages/,robots.txt,logo.png,graph/,sparkleshare/</param-value>
        </init-param>
    </filter>
    <filter-mapping>
@@ -321,4 +358,4 @@
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    
</web-app>
</web-app>
src/main/java/com/gitblit/ConfigUserService.java
@@ -24,6 +24,7 @@
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@@ -36,6 +37,7 @@
import com.gitblit.Constants.AccessPermission;
import com.gitblit.Constants.AccountType;
import com.gitblit.Constants.Transport;
import com.gitblit.manager.IRuntimeManager;
import com.gitblit.models.TeamModel;
import com.gitblit.models.UserModel;
@@ -95,6 +97,10 @@
    private static final String STARRED = "starred";
    private static final String LOCALE = "locale";
    private static final String EMAILONMYTICKETCHANGES = "emailMeOnMyTicketChanges";
    private static final String TRANSPORT = "transport";
    private static final String ACCOUNTTYPE = "accountType";
@@ -707,8 +713,21 @@
                config.setBoolean(USER, model.username, DISABLED, true);
            }
            if (model.getPreferences() != null) {
                if (!StringUtils.isEmpty(model.getPreferences().locale)) {
                    config.setString(USER, model.username, LOCALE, model.getPreferences().locale);
                Locale locale = model.getPreferences().getLocale();
                if (locale != null) {
                    String val;
                    if (StringUtils.isEmpty(locale.getCountry())) {
                        val = locale.getLanguage();
                    } else {
                        val = locale.getLanguage() + "_" + locale.getCountry();
                    }
                    config.setString(USER, model.username, LOCALE, val);
                }
                config.setBoolean(USER, model.username, EMAILONMYTICKETCHANGES, model.getPreferences().isEmailMeOnMyTicketChanges());
                if (model.getPreferences().getTransport() != null) {
                    config.setString(USER, model.username, TRANSPORT, model.getPreferences().getTransport().name());
                }
            }
@@ -880,11 +899,15 @@
                    user.stateProvince = config.getString(USER, username, STATEPROVINCE);
                    user.countryCode = config.getString(USER, username, COUNTRYCODE);
                    user.cookie = config.getString(USER, username, COOKIE);
                    user.getPreferences().locale = config.getString(USER, username, LOCALE);
                    if (StringUtils.isEmpty(user.cookie) && !StringUtils.isEmpty(user.password)) {
                        user.cookie = StringUtils.getSHA1(user.username + user.password);
                    }
                    // preferences
                    user.getPreferences().setLocale(config.getString(USER, username, LOCALE));
                    user.getPreferences().setEmailMeOnMyTicketChanges(config.getBoolean(USER, username, EMAILONMYTICKETCHANGES, true));
                    user.getPreferences().setTransport(Transport.fromString(config.getString(USER, username, TRANSPORT)));
                    // user roles
                    Set<String> roles = new HashSet<String>(Arrays.asList(config.getStringList(
                            USER, username, ROLE)));
src/main/java/com/gitblit/Constants.java
@@ -68,6 +68,8 @@
    public static final String SPARKLESHARE_INVITE_PATH = "/sparkleshare/";
    public static final String RAW_PATH = "/raw/";
    public static final String BRANCH_GRAPH_PATH = "/graph/";
    public static final String BORDER = "*****************************************************************";
@@ -119,6 +121,14 @@
    public static final String R_TICKET = "refs/heads/ticket/";
    public static final String R_TICKETS_PATCHSETS = "refs/tickets/";
    public static final String R_MASTER = "refs/heads/master";
    public static final String MASTER = "master";
    public static final String R_DEVELOP = "refs/heads/develop";
    public static final String DEVELOP = "develop";
    public static String getVersion() {
        String v = Constants.class.getPackage().getImplementationVersion();
@@ -348,9 +358,10 @@
     * a client.
     */
    public static enum RpcRequest {
        // Order is important here.  anything above LIST_SETTINGS requires
        // Order is important here.  anything after LIST_SETTINGS requires
        // administrator privileges and web.allowRpcManagement.
        CLEAR_REPOSITORY_CACHE, REINDEX_TICKETS, GET_PROTOCOL, LIST_REPOSITORIES, LIST_BRANCHES, GET_USER, LIST_SETTINGS,
        CLEAR_REPOSITORY_CACHE, REINDEX_TICKETS, GET_PROTOCOL, LIST_REPOSITORIES, LIST_BRANCHES, GET_USER,
        FORK_REPOSITORY, LIST_SETTINGS,
        CREATE_REPOSITORY, EDIT_REPOSITORY, DELETE_REPOSITORY,
        LIST_USERS, CREATE_USER, EDIT_USER, DELETE_USER,
        LIST_TEAMS, CREATE_TEAM, EDIT_TEAM, DELETE_TEAM,
@@ -400,6 +411,27 @@
    }
    /**
     * Enumeration of the feed content object types.
     */
    public static enum FeedObjectType {
        COMMIT, TAG;
        public static FeedObjectType forName(String name) {
            for (FeedObjectType 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 {
src/main/java/com/gitblit/DaggerModule.java
@@ -91,8 +91,11 @@
        return new NotificationManager(settings);
    }
    @Provides @Singleton IUserManager provideUserManager(IRuntimeManager runtimeManager) {
        return new UserManager(runtimeManager);
    @Provides @Singleton IUserManager provideUserManager(
            IRuntimeManager runtimeManager,
            IPluginManager pluginManager) {
        return new UserManager(runtimeManager, pluginManager);
    }
    @Provides @Singleton IAuthenticationManager provideAuthenticationManager(
@@ -131,10 +134,12 @@
    @Provides @Singleton IRepositoryManager provideRepositoryManager(
            IRuntimeManager runtimeManager,
            IPluginManager pluginManager,
            IUserManager userManager) {
        return new RepositoryManager(
                runtimeManager,
                pluginManager,
                userManager);
    }
src/main/java/com/gitblit/FederationClient.java
@@ -94,8 +94,8 @@
        // configure the Gitblit singleton for minimal, non-server operation
        RuntimeManager runtime = new RuntimeManager(settings, baseFolder).start();
        NoopNotificationManager notifications = new NoopNotificationManager().start();
        UserManager users = new UserManager(runtime).start();
        RepositoryManager repositories = new RepositoryManager(runtime, users).start();
        UserManager users = new UserManager(runtime, null).start();
        RepositoryManager repositories = new RepositoryManager(runtime, null, users).start();
        FederationManager federation = new FederationManager(runtime, notifications, repositories).start();
        IGitblit gitblit = new GitblitManager(runtime, null, notifications, users, null, null, repositories, null, federation);
@@ -166,6 +166,11 @@
        }
        @Override
        public boolean isSendingMail() {
            return false;
        }
        @Override
        public void sendMailToAdministrators(String subject, String message) {
        }
src/main/java/com/gitblit/GitBlit.java
@@ -20,6 +20,7 @@
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
@@ -115,6 +116,21 @@
    @Override
    public boolean isServingRepositories() {
        return servicesManager.isServingRepositories();
    }
    @Override
    public boolean isServingHTTP() {
        return servicesManager.isServingHTTP();
    }
    @Override
    public boolean isServingGIT() {
        return servicesManager.isServingGIT();
    }
    @Override
    public boolean isServingSSH() {
        return servicesManager.isServingSSH();
    }
    protected Object [] getModules() {
@@ -249,6 +265,24 @@
            }
        });
        // consider the user's transport preference
        RepositoryUrl preferredUrl = null;
        Transport preferredTransport = user.getPreferences().getTransport();
        if (preferredTransport != null) {
            Iterator<RepositoryUrl> itr = list.iterator();
            while (itr.hasNext()) {
                RepositoryUrl url = itr.next();
                if (url.transport.equals(preferredTransport)) {
                    itr.remove();
                    preferredUrl = url;
                    break;
                }
            }
        }
        if (preferredUrl != null) {
            list.add(0, preferredUrl);
        }
        return list;
    }
src/main/java/com/gitblit/MigrateTickets.java
New file
@@ -0,0 +1,256 @@
/*
 * Copyright 2014 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.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.CmdLineException;
import org.kohsuke.args4j.CmdLineParser;
import org.kohsuke.args4j.Option;
import com.gitblit.manager.IRepositoryManager;
import com.gitblit.manager.IRuntimeManager;
import com.gitblit.manager.RepositoryManager;
import com.gitblit.manager.RuntimeManager;
import com.gitblit.models.RepositoryModel;
import com.gitblit.models.TicketModel;
import com.gitblit.models.TicketModel.Change;
import com.gitblit.tickets.BranchTicketService;
import com.gitblit.tickets.FileTicketService;
import com.gitblit.tickets.ITicketService;
import com.gitblit.tickets.RedisTicketService;
import com.gitblit.utils.StringUtils;
/**
 * A command-line tool to move all tickets from one ticket service to another.
 *
 * @author James Moger
 *
 */
public class MigrateTickets {
    public static void main(String... args) {
        MigrateTickets migrate = new MigrateTickets();
        // 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 (!".".equals(args[i + 1])) {
                    folder = args[i + 1];
                }
                i = i + 1;
            } else {
                filtered.add(arg);
            }
        }
        Params.baseFolder = folder;
        Params params = new Params();
        CmdLineParser parser = new CmdLineParser(params);
        try {
            parser.parseArgument(filtered);
            if (params.help) {
                migrate.usage(parser, null);
                return;
            }
        } catch (CmdLineException t) {
            migrate.usage(parser, t);
            return;
        }
        // load the settings
        FileSettings settings = params.FILESETTINGS;
        if (!StringUtils.isEmpty(params.settingsfile)) {
            if (new File(params.settingsfile).exists()) {
                settings = new FileSettings(params.settingsfile);
            }
        }
        // migrate tickets
        migrate.migrate(new File(Params.baseFolder), settings, params.outputServiceName);
        System.exit(0);
    }
    /**
     * Display the command line usage of MigrateTickets.
     *
     * @param parser
     * @param t
     */
    protected final void usage(CmdLineParser parser, CmdLineException 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 (parser != null) {
            parser.printUsage(System.out);
            System.out
                    .println("\nExample:\n  java -gitblit.jar com.gitblit.MigrateTickets com.gitblit.tickets.RedisTicketService --baseFolder c:\\gitblit-data");
        }
        System.exit(0);
    }
    /**
     * Migrate all tickets
     *
     * @param baseFolder
     * @param settings
     * @param outputServiceName
     */
    protected void migrate(File baseFolder, IStoredSettings settings, String outputServiceName) {
        // disable some services
        settings.overrideSetting(Keys.web.allowLuceneIndexing, false);
        settings.overrideSetting(Keys.git.enableGarbageCollection, false);
        settings.overrideSetting(Keys.git.enableMirroring, false);
        settings.overrideSetting(Keys.web.activityCacheDays, 0);
        settings.overrideSetting(ITicketService.SETTING_UPDATE_DIFFSTATS, false);
        IRuntimeManager runtimeManager = new RuntimeManager(settings, baseFolder).start();
        IRepositoryManager repositoryManager = new RepositoryManager(runtimeManager, null, null).start();
        String inputServiceName = settings.getString(Keys.tickets.service, BranchTicketService.class.getSimpleName());
        if (StringUtils.isEmpty(inputServiceName)) {
            System.err.println(MessageFormat.format("Please define a ticket service in \"{0}\"", Keys.tickets.service));
            System.exit(1);
        }
        ITicketService inputService = null;
        ITicketService outputService = null;
        try {
            inputService = getService(inputServiceName, runtimeManager, repositoryManager);
            outputService = getService(outputServiceName, runtimeManager, repositoryManager);
        } catch (Exception e) {
            e.printStackTrace();
            System.exit(1);
        }
        if (!inputService.isReady()) {
            System.err.println(String.format("%s INPUT service is not ready, check config.", inputService.getClass().getSimpleName()));
            System.exit(1);
        }
        if (!outputService.isReady()) {
            System.err.println(String.format("%s OUTPUT service is not ready, check config.", outputService.getClass().getSimpleName()));
            System.exit(1);
        }
        // migrate tickets
        long start = System.nanoTime();
        long totalTickets = 0;
        long totalChanges = 0;
        for (RepositoryModel repository : repositoryManager.getRepositoryModels(null)) {
            Set<Long> ids = inputService.getIds(repository);
            if (ids == null || ids.isEmpty()) {
                // nothing to migrate
                continue;
            }
            // delete any tickets we may have in the output ticket service
            outputService.deleteAll(repository);
            for (long id : ids) {
                List<Change> journal = inputService.getJournal(repository, id);
                if (journal == null || journal.size() == 0) {
                    continue;
                }
                TicketModel ticket = outputService.createTicket(repository, id, journal.get(0));
                if (ticket == null) {
                    System.err.println(String.format("Failed to migrate %s #%s", repository.name, id));
                    System.exit(1);
                }
                totalTickets++;
                System.out.println(String.format("%s #%s: %s", repository.name, ticket.number, ticket.title));
                for (int i = 1; i < journal.size(); i++) {
                    TicketModel updated = outputService.updateTicket(repository, ticket.number, journal.get(i));
                    if (updated != null) {
                        System.out.println(String.format("   applied change %d", i));
                        totalChanges++;
                    } else {
                        System.err.println(String.format("Failed to apply change %d:\n%s", i, journal.get(i)));
                        System.exit(1);
                    }
                }
            }
        }
        inputService.stop();
        outputService.stop();
        repositoryManager.stop();
        runtimeManager.stop();
        long end = System.nanoTime();
        System.out.println(String.format("Migrated %d tickets composed of %d journal entries in %d seconds",
                totalTickets, totalTickets + totalChanges, TimeUnit.NANOSECONDS.toSeconds(end - start)));
    }
    protected ITicketService getService(String serviceName, IRuntimeManager runtimeManager, IRepositoryManager repositoryManager) throws Exception {
        ITicketService service = null;
        Class<?> serviceClass = Class.forName(serviceName);
        if (RedisTicketService.class.isAssignableFrom(serviceClass)) {
            // Redis ticket service
            service = new RedisTicketService(runtimeManager, null, null, null, repositoryManager).start();
        } else if (BranchTicketService.class.isAssignableFrom(serviceClass)) {
            // Branch ticket service
            service = new BranchTicketService(runtimeManager, null, null, null, repositoryManager).start();
        } else if (FileTicketService.class.isAssignableFrom(serviceClass)) {
            // File ticket service
            service = new FileTicketService(runtimeManager, null, null, null, repositoryManager).start();
        } else {
            System.err.println("Unknown ticket service " + serviceName);
        }
        return service;
    }
    /**
     * Parameters.
     */
    public static class Params {
        public static String baseFolder;
        @Option(name = "--help", aliases = { "-h"}, usage = "Show this help")
        public Boolean help = false;
        private final FileSettings FILESETTINGS = new FileSettings(new File(baseFolder, Constants.PROPERTIES_FILE).getAbsolutePath());
        @Option(name = "--repositoriesFolder", usage = "Git Repositories Folder", metaVar = "PATH")
        public String repositoriesFolder = FILESETTINGS.getString(Keys.git.repositoriesFolder, "git");
        @Option(name = "--settings", usage = "Path to alternative settings", metaVar = "FILE")
        public String settingsfile;
        @Argument(index = 0, required = true, metaVar = "OUTPUTSERVICE", usage = "The destination/output ticket service")
        public String outputServiceName;
    }
}
src/main/java/com/gitblit/ReindexTickets.java
@@ -127,7 +127,7 @@
        settings.overrideSetting(Keys.web.activityCacheDays, 0);
        IRuntimeManager runtimeManager = new RuntimeManager(settings, baseFolder).start();
        IRepositoryManager repositoryManager = new RepositoryManager(runtimeManager, null).start();
        IRepositoryManager repositoryManager = new RepositoryManager(runtimeManager, null, null).start();
        String serviceName = settings.getString(Keys.tickets.service, BranchTicketService.class.getSimpleName());
        if (StringUtils.isEmpty(serviceName)) {
src/main/java/com/gitblit/client/GitblitClient.java
@@ -253,9 +253,11 @@
        // 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);
        if (!ArrayUtils.isEmpty(settings.pushScripts)) {
            for (String script : settings.pushScripts) {
                if (!inherited.contains(script)) {
                    scripts.add(script);
                }
            }
        }
        return scripts;
src/main/java/com/gitblit/dagger/DaggerFilter.java
@@ -36,10 +36,10 @@
    public final void init(FilterConfig filterConfig) throws ServletException {
        ServletContext context = filterConfig.getServletContext();
        ObjectGraph objectGraph = (ObjectGraph) context.getAttribute(DaggerContext.INJECTOR_NAME);
        inject(objectGraph);
        inject(objectGraph, filterConfig);
    }
    protected abstract void inject(ObjectGraph dagger);
    protected abstract void inject(ObjectGraph dagger, FilterConfig filterConfig) throws ServletException;
    @Override
    public void destroy() {
src/main/java/com/gitblit/extensions/GitblitWicketPlugin.java
New file
@@ -0,0 +1,49 @@
/*
 * Copyright 2014 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.extensions;
import org.apache.wicket.Application;
import org.apache.wicket.IInitializer;
import ro.fortsoft.pf4j.PluginWrapper;
import com.gitblit.wicket.GitblitWicketApp;
/**
 * A Gitblit plugin that is allowed to extend the Wicket webapp.
 *
 * @author James Moger
 * @since 1.6.0
 */
public abstract class GitblitWicketPlugin extends GitblitPlugin implements IInitializer  {
    public GitblitWicketPlugin(PluginWrapper wrapper) {
        super(wrapper);
    }
    @Override
    public final void init(Application application) {
        init((GitblitWicketApp) application);
    }
    /**
     * Allows plugins to extend the web application.
     *
     * @param app
     * @since 1.6.0
     */
    protected abstract void init(GitblitWicketApp app);
}
src/main/java/com/gitblit/extensions/HttpRequestFilter.java
New file
@@ -0,0 +1,49 @@
/*
 * Copyright 2014 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.extensions;
import java.io.IOException;
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 ro.fortsoft.pf4j.ExtensionPoint;
/**
 * Extension point to intercept HTTP requests passing through the server.
 *
 * @author David Ostrovsky
 * @since 1.6.0
 *
 */
public abstract class HttpRequestFilter implements Filter, ExtensionPoint {
    @Override
    public void init(FilterConfig config) throws ServletException {
    }
    @Override
    public void destroy() {
    }
    @Override
    public abstract void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException;
}
src/main/java/com/gitblit/extensions/LifeCycleListener.java
New file
@@ -0,0 +1,43 @@
/*
 * Copyright 2014 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.extensions;
import ro.fortsoft.pf4j.ExtensionPoint;
/**
 * Extension point to allow plugins to listen to major Gitblit lifecycle events.
 *
 * @author James Moger
 * @since 1.6.0
 */
public abstract class LifeCycleListener implements ExtensionPoint {
    /**
     * Called after all internal managers have been started.
     * This may be useful for reporting "server is ready" to a monitoring system.
     *
     * @since 1.6.0
     */
    public abstract void onStartup();
    /**
     * Called when the servlet container is gracefully shutting-down the webapp.
     * This is called before the internal managers are stopped.
     *
     *  @since 1.6.0
     */
    public abstract void onShutdown();
}
src/main/java/com/gitblit/extensions/NavLinkExtension.java
New file
@@ -0,0 +1,40 @@
/*
 * Copyright 2014 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.extensions;
import java.util.List;
import ro.fortsoft.pf4j.ExtensionPoint;
import com.gitblit.models.NavLink;
import com.gitblit.models.UserModel;
/**
 * Extension point to contribute top-level navigation links.
 *
 * @author James Moger
 * @since 1.6.0
 *
 */
public abstract class NavLinkExtension implements ExtensionPoint {
    /**
     * @param user
     * @since 1.6.0
     * @return a list of nav links
     */
    public abstract List<NavLink> getNavLinks(UserModel user);
}
src/main/java/com/gitblit/extensions/RepositoryLifeCycleListener.java
New file
@@ -0,0 +1,45 @@
/*
 * Copyright 2014 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.extensions;
import ro.fortsoft.pf4j.ExtensionPoint;
import com.gitblit.models.RepositoryModel;
/**
 * Extension point to allow plugins to listen to major repository lifecycle events.
 *
 * @author James Moger
 * @since 1.6.0
 */
public abstract class RepositoryLifeCycleListener implements ExtensionPoint {
    /**
     * Called after a repository has been created.
     *
     * @param repository
     * @since 1.6.0
     */
    public abstract void onCreation(RepositoryModel repository);
    /**
     * Called after a repository has been deleted.
     *
     * @param repository
     * @since 1.6.0
     */
    public abstract void onDeletion(RepositoryModel repository);
}
src/main/java/com/gitblit/extensions/RepositoryNavLinkExtension.java
New file
@@ -0,0 +1,42 @@
/*
 * Copyright 2014 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.extensions;
import java.util.List;
import ro.fortsoft.pf4j.ExtensionPoint;
import com.gitblit.models.NavLink;
import com.gitblit.models.RepositoryModel;
import com.gitblit.models.UserModel;
/**
 * Extension point to contribute repository page navigation links.
 *
 * @author James Moger
 * @since 1.6.0
 *
 */
public abstract class RepositoryNavLinkExtension implements ExtensionPoint {
    /**
     * @param user
     * @param repository
     * @since 1.6.0
     * @return a list of nav links
     */
    public abstract List<NavLink> getNavLinks(UserModel user, RepositoryModel repository);
}
src/main/java/com/gitblit/extensions/UserMenuExtension.java
New file
@@ -0,0 +1,40 @@
/*
 * Copyright 2014 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.extensions;
import java.util.List;
import ro.fortsoft.pf4j.ExtensionPoint;
import com.gitblit.models.Menu.MenuItem;
import com.gitblit.models.UserModel;
/**
 * Extension point to contribute user menu items.
 *
 * @author James Moger
 * @since 1.6.0
 *
 */
public abstract class UserMenuExtension implements ExtensionPoint {
    /**
     * @param user
     * @since 1.6.0
     * @return a list of menu items
     */
    public abstract List<MenuItem> getMenuItems(UserModel user);
}
src/main/java/com/gitblit/extensions/UserTeamLifeCycleListener.java
New file
@@ -0,0 +1,62 @@
/*
 * Copyright 2014 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.extensions;
import ro.fortsoft.pf4j.ExtensionPoint;
import com.gitblit.models.TeamModel;
import com.gitblit.models.UserModel;
/**
 * Extension point to allow plugins to listen to major user and team lifecycle events.
 *
 * @author James Moger
 * @since 1.6.0
 */
public abstract class UserTeamLifeCycleListener implements ExtensionPoint {
    /**
     * Called after a user has been created.
     *
     * @param user
     * @since 1.6.0
     */
    public abstract void onCreation(UserModel user);
    /**
     * Called after a user has been deleted.
     *
     * @param user
     * @since 1.6.0
     */
    public abstract void onDeletion(UserModel user);
    /**
     * Called after a team has been created.
     *
     * @param team
     * @since 1.6.0
     */
    public abstract void onCreation(TeamModel team);
    /**
     * Called after a team has been deleted.
     *
     * @param team
     * @since 1.6.0
     */
    public abstract void onDeletion(TeamModel team);
}
src/main/java/com/gitblit/git/GitblitReceivePack.java
@@ -331,6 +331,43 @@
            return;
        }
        logRefChange(commands);
        updateIncrementalPushTags(commands);
        updateGitblitRefLog(commands);
        // check for updates pushed to the BranchTicketService branch
        // if the BranchTicketService is active it will reindex, as appropriate
        for (ReceiveCommand cmd : commands) {
            if (Result.OK.equals(cmd.getResult())
                    && BranchTicketService.BRANCH.equals(cmd.getRefName())) {
                rp.getRepository().fireEvent(new ReceiveCommandEvent(repository, cmd));
            }
        }
        // call post-receive plugins
        for (ReceiveHook hook : gitblit.getExtensions(ReceiveHook.class)) {
            try {
                hook.onPostReceive(this, commands);
            } catch (Exception e) {
                LOGGER.error("Failed to execute extension", e);
            }
        }
        // run Groovy hook scripts
        Set<String> scripts = new LinkedHashSet<String>();
        scripts.addAll(gitblit.getPostReceiveScriptsInherited(repository));
        if (!ArrayUtils.isEmpty(repository.postReceiveScripts)) {
            scripts.addAll(repository.postReceiveScripts);
        }
        runGroovy(commands, scripts);
    }
    /**
     * Log the ref changes in the container log.
     *
     * @param commands
     */
    protected void logRefChange(Collection<ReceiveCommand> commands) {
        boolean isRefCreationOrDeletion = false;
        // log ref changes
@@ -362,76 +399,65 @@
        if (isRefCreationOrDeletion) {
            gitblit.resetRepositoryCache(repository.name);
        }
    }
        if (repository.useIncrementalPushTags) {
            // tag each pushed branch tip
            String emailAddress = user.emailAddress == null ? rp.getRefLogIdent().getEmailAddress() : user.emailAddress;
            PersonIdent userIdent = new PersonIdent(user.getDisplayName(), emailAddress);
            for (ReceiveCommand cmd : commands) {
                if (!cmd.getRefName().startsWith(Constants.R_HEADS)) {
                    // only tag branch ref changes
                    continue;
                }
                if (!ReceiveCommand.Type.DELETE.equals(cmd.getType())
                        && ReceiveCommand.Result.OK.equals(cmd.getResult())) {
                    String objectId = cmd.getNewId().getName();
                    String branch = cmd.getRefName().substring(Constants.R_HEADS.length());
                    // get translation based on the server's locale setting
                    String template = Translation.get("gb.incrementalPushTagMessage");
                    String msg = MessageFormat.format(template, branch);
                    String prefix;
                    if (StringUtils.isEmpty(repository.incrementalPushTagPrefix)) {
                        prefix = settings.getString(Keys.git.defaultIncrementalPushTagPrefix, "r");
                    } else {
                        prefix = repository.incrementalPushTagPrefix;
                    }
                    JGitUtils.createIncrementalRevisionTag(
                            rp.getRepository(),
                            objectId,
                            userIdent,
                            prefix,
                            "0",
                            msg);
                }
            }
    /**
     * Optionally update the incremental push tags.
     *
     * @param commands
     */
    protected void updateIncrementalPushTags(Collection<ReceiveCommand> commands) {
        if (!repository.useIncrementalPushTags) {
            return;
        }
        // update push log
        try {
            RefLogUtils.updateRefLog(user, rp.getRepository(), commands);
            LOGGER.debug(MessageFormat.format("{0} push log updated", repository.name));
        } catch (Exception e) {
            LOGGER.error(MessageFormat.format("Failed to update {0} pushlog", repository.name), e);
        }
        // tag each pushed branch tip
        String emailAddress = user.emailAddress == null ? getRefLogIdent().getEmailAddress() : user.emailAddress;
        PersonIdent userIdent = new PersonIdent(user.getDisplayName(), emailAddress);
        // check for updates pushed to the BranchTicketService branch
        // if the BranchTicketService is active it will reindex, as appropriate
        for (ReceiveCommand cmd : commands) {
            if (Result.OK.equals(cmd.getResult())
                    && BranchTicketService.BRANCH.equals(cmd.getRefName())) {
                rp.getRepository().fireEvent(new ReceiveCommandEvent(repository, cmd));
            if (!cmd.getRefName().startsWith(Constants.R_HEADS)) {
                // only tag branch ref changes
                continue;
            }
            if (!ReceiveCommand.Type.DELETE.equals(cmd.getType())
                    && ReceiveCommand.Result.OK.equals(cmd.getResult())) {
                String objectId = cmd.getNewId().getName();
                String branch = cmd.getRefName().substring(Constants.R_HEADS.length());
                // get translation based on the server's locale setting
                String template = Translation.get("gb.incrementalPushTagMessage");
                String msg = MessageFormat.format(template, branch);
                String prefix;
                if (StringUtils.isEmpty(repository.incrementalPushTagPrefix)) {
                    prefix = settings.getString(Keys.git.defaultIncrementalPushTagPrefix, "r");
                } else {
                    prefix = repository.incrementalPushTagPrefix;
                }
                JGitUtils.createIncrementalRevisionTag(
                        getRepository(),
                        objectId,
                        userIdent,
                        prefix,
                        "0",
                        msg);
            }
        }
    }
        // call post-receive plugins
        for (ReceiveHook hook : gitblit.getExtensions(ReceiveHook.class)) {
            try {
                hook.onPostReceive(this, commands);
            } catch (Exception e) {
                LOGGER.error("Failed to execute extension", e);
            }
    /**
     * Update Gitblit's internal reflog.
     *
     * @param commands
     */
    protected void updateGitblitRefLog(Collection<ReceiveCommand> commands) {
        try {
            RefLogUtils.updateRefLog(user, getRepository(), commands);
            LOGGER.debug(MessageFormat.format("{0} reflog updated", repository.name));
        } catch (Exception e) {
            LOGGER.error(MessageFormat.format("Failed to update {0} reflog", repository.name), e);
        }
        // run Groovy hook scripts
        Set<String> scripts = new LinkedHashSet<String>();
        scripts.addAll(gitblit.getPostReceiveScriptsInherited(repository));
        if (!ArrayUtils.isEmpty(repository.postReceiveScripts)) {
            scripts.addAll(repository.postReceiveScripts);
        }
        runGroovy(commands, scripts);
    }
    /** Execute commands to update references. */
src/main/java/com/gitblit/git/PatchsetReceivePack.java
@@ -1202,11 +1202,15 @@
        if (ticket != null) {
            ticketNotifier.queueMailing(ticket);
            // update the reflog with the merge
            if (oldRef != null) {
                ReceiveCommand cmd = new ReceiveCommand(oldRef.getObjectId(),
                        ObjectId.fromString(mergeResult.sha), oldRef.getName());
                RefLogUtils.updateRefLog(user, getRepository(), Arrays.asList(cmd));
                cmd.setResult(Result.OK);
                List<ReceiveCommand> commands = Arrays.asList(cmd);
                logRefChange(commands);
                updateIncrementalPushTags(commands);
                updateGitblitRefLog(commands);
            }
            // call patchset hooks
src/main/java/com/gitblit/manager/GitblitManager.java
@@ -602,6 +602,21 @@
    }
    @Override
    public boolean isServingHTTP() {
        return runtimeManager.isServingHTTP();
    }
    @Override
    public boolean isServingGIT() {
        return runtimeManager.isServingGIT();
    }
    @Override
    public boolean isServingSSH() {
        return runtimeManager.isServingSSH();
    }
    @Override
    public TimeZone getTimezone() {
        return runtimeManager.getTimezone();
    }
@@ -644,6 +659,11 @@
    /*
     * NOTIFICATION MANAGER
     */
    @Override
    public boolean isSendingMail() {
        return notificationManager.isSendingMail();
    }
    @Override
    public void sendMailToAdministrators(String subject, String message) {
@@ -1015,6 +1035,11 @@
    }
    @Override
    public boolean canDelete(RepositoryModel model) {
        return repositoryManager.canDelete(model);
    }
    @Override
    public boolean deleteRepositoryModel(RepositoryModel model) {
        return repositoryManager.deleteRepositoryModel(model);
    }
src/main/java/com/gitblit/manager/INotificationManager.java
@@ -22,6 +22,14 @@
public interface INotificationManager extends IManager {
    /**
     * Returns true if the email service is configured and ready to send notifications.
     *
     * @return true if the email service is operational
     * @since 1.6.0
     */
    boolean isSendingMail();
    /**
     * Notify the administrators by email.
     *
     * @param subject
src/main/java/com/gitblit/manager/IRepositoryManager.java
@@ -339,6 +339,15 @@
    void updateConfiguration(Repository r, RepositoryModel repository);
    /**
     * Returns true if the repository can be deleted.
     *
     * @param model
     * @return true if the repository can be deleted
     * @since 1.6.0
     */
    boolean canDelete(RepositoryModel model);
    /**
     * Deletes the repository from the file system and removes the repository
     * permission from all repository users.
     *
src/main/java/com/gitblit/manager/IRuntimeManager.java
@@ -57,6 +57,33 @@
    boolean isServingRepositories();
    /**
     * Determine if this Gitblit instance is actively serving git repositories
     * over HTTP.
     *
     * @return true if Gitblit is serving repositories over HTTP
      * @since 1.6.0
     */
    boolean isServingHTTP();
    /**
     * Determine if this Gitblit instance is actively serving git repositories
     * over the GIT Daemon protocol.
     *
     * @return true if Gitblit is serving repositories over the GIT Daemon protocol
      * @since 1.6.0
     */
    boolean isServingGIT();
    /**
     * Determine if this Gitblit instance is actively serving git repositories
     * over the SSH protocol.
     *
     * @return true if Gitblit is serving repositories over the SSH protocol
      * @since 1.6.0
     */
    boolean isServingSSH();
    /**
     * Determine if this Gitblit instance is running in debug mode
     *
     * @return true if Gitblit is running in debug mode
src/main/java/com/gitblit/manager/NotificationManager.java
@@ -71,6 +71,11 @@
        return this;
    }
    @Override
    public boolean isSendingMail() {
        return mailService.isReady();
    }
    /**
     * Notify the administrators by email.
     *
src/main/java/com/gitblit/manager/RepositoryManager.java
@@ -66,6 +66,7 @@
import com.gitblit.GitBlitException;
import com.gitblit.IStoredSettings;
import com.gitblit.Keys;
import com.gitblit.extensions.RepositoryLifeCycleListener;
import com.gitblit.models.ForkModel;
import com.gitblit.models.Metric;
import com.gitblit.models.RefModel;
@@ -114,6 +115,8 @@
    private final IRuntimeManager runtimeManager;
    private final IPluginManager pluginManager;
    private final IUserManager userManager;
    private final File repositoriesFolder;
@@ -126,10 +129,12 @@
    public RepositoryManager(
            IRuntimeManager runtimeManager,
            IPluginManager pluginManager,
            IUserManager userManager) {
        this.settings = runtimeManager.getSettings();
        this.runtimeManager = runtimeManager;
        this.pluginManager = pluginManager;
        this.userManager = userManager;
        this.repositoriesFolder = runtimeManager.getFileOrFolder(Keys.git.repositoriesFolder, "${baseFolder}/git");
    }
@@ -1420,6 +1425,16 @@
        removeFromCachedRepositoryList(repositoryName);
        // model will actually be replaced on next load because config is stale
        addToCachedRepositoryList(repository);
        if (isCreate && pluginManager != null) {
            for (RepositoryLifeCycleListener listener : pluginManager.getExtensions(RepositoryLifeCycleListener.class)) {
                try {
                    listener.onCreation(repository);
                } catch (Throwable t) {
                    logger.error(String.format("failed to call plugin onCreation %s", repositoryName), t);
                }
            }
        }
    }
    /**
@@ -1536,6 +1551,17 @@
    }
    /**
     * Returns true if the repository can be deleted.
     *
     * @return true if the repository can be deleted
     */
    @Override
    public boolean canDelete(RepositoryModel repository) {
        return settings.getBoolean(Keys.web.allowDeletingNonEmptyRepositories, true)
                    || !repository.hasCommits;
    }
    /**
     * Deletes the repository from the file system and removes the repository
     * permission from all repository users.
     *
@@ -1556,6 +1582,12 @@
     */
    @Override
    public boolean deleteRepository(String repositoryName) {
        RepositoryModel repository = getRepositoryModel(repositoryName);
        if (!canDelete(repository)) {
            logger.warn("Attempt to delete {} rejected!", repositoryName);
            return false;
        }
        try {
            close(repositoryName);
            // clear the repository cache
@@ -1571,6 +1603,16 @@
                FileUtils.delete(folder, FileUtils.RECURSIVE | FileUtils.RETRY);
                if (userManager.deleteRepositoryRole(repositoryName)) {
                    logger.info(MessageFormat.format("Repository \"{0}\" deleted", repositoryName));
                    if (pluginManager != null) {
                        for (RepositoryLifeCycleListener listener : pluginManager.getExtensions(RepositoryLifeCycleListener.class)) {
                            try {
                                listener.onDeletion(repository);
                            } catch (Throwable t) {
                                logger.error(String.format("failed to call plugin onDeletion %s", repositoryName), t);
                            }
                        }
                    }
                    return true;
                }
            }
src/main/java/com/gitblit/manager/RuntimeManager.java
@@ -119,9 +119,42 @@
     */
    @Override
    public boolean isServingRepositories() {
        return settings.getBoolean(Keys.git.enableGitServlet, true)
                || (settings.getInteger(Keys.git.daemonPort, 0) > 0)
                || (settings.getInteger(Keys.git.sshPort, 0) > 0);
        return isServingHTTP()
                || isServingGIT()
                || isServingSSH();
    }
    /**
     * Determine if this Gitblit instance is actively serving git repositories
     * over the HTTP protocol.
     *
     * @return true if Gitblit is serving repositories over the HTTP protocol
     */
    @Override
    public boolean isServingHTTP() {
        return settings.getBoolean(Keys.git.enableGitServlet, true);
    }
    /**
     * Determine if this Gitblit instance is actively serving git repositories
     * over the Git Daemon protocol.
     *
     * @return true if Gitblit is serving repositories over the Git Daemon protocol
     */
    @Override
    public boolean isServingGIT() {
        return settings.getInteger(Keys.git.daemonPort, 0) > 0;
    }
    /**
     * Determine if this Gitblit instance is actively serving git repositories
     * over the SSH protocol.
     *
     * @return true if Gitblit is serving repositories over the SSH protocol
     */
    @Override
    public boolean isServingSSH() {
        return settings.getInteger(Keys.git.sshPort, 0) > 0;
    }
    /**
src/main/java/com/gitblit/manager/ServicesManager.java
@@ -80,8 +80,9 @@
    public ServicesManager(IGitblit gitblit) {
        this.settings = gitblit.getSettings();
        this.gitblit = gitblit;
        int defaultThreadPoolSize = settings.getInteger(Keys.execution.defaultThreadPoolSize, 1);
        this.idGenerator = new IdGenerator();
        this.workQueue = new WorkQueue(idGenerator, 1);
        this.workQueue = new WorkQueue(idGenerator, defaultThreadPoolSize);
    }
    @Override
@@ -111,9 +112,21 @@
    }
    public boolean isServingRepositories() {
        return settings.getBoolean(Keys.git.enableGitServlet, true)
                || (gitDaemon != null && gitDaemon.isRunning())
                || (sshDaemon != null && sshDaemon.isRunning());
        return isServingHTTP()
                || isServingGIT()
                || isServingSSH();
    }
    public boolean isServingHTTP() {
        return settings.getBoolean(Keys.git.enableGitServlet, true);
    }
    public boolean isServingGIT() {
        return gitDaemon != null && gitDaemon.isRunning();
    }
    public boolean isServingSSH() {
        return sshDaemon != null && sshDaemon.isRunning();
    }
    protected void configureFederation() {
src/main/java/com/gitblit/manager/UserManager.java
@@ -32,6 +32,7 @@
import com.gitblit.IStoredSettings;
import com.gitblit.IUserService;
import com.gitblit.Keys;
import com.gitblit.extensions.UserTeamLifeCycleListener;
import com.gitblit.models.TeamModel;
import com.gitblit.models.UserModel;
import com.gitblit.utils.StringUtils;
@@ -50,13 +51,16 @@
    private final IRuntimeManager runtimeManager;
    private final IPluginManager pluginManager;
    private final Map<String, String> legacyBackingServices;
    private IUserService userService;
    public UserManager(IRuntimeManager runtimeManager) {
    public UserManager(IRuntimeManager runtimeManager, IPluginManager pluginManager) {
        this.settings = runtimeManager.getSettings();
        this.runtimeManager = runtimeManager;
        this.pluginManager = pluginManager;
        // map of legacy realm backing user services
        legacyBackingServices = new HashMap<String, String>();
@@ -209,7 +213,14 @@
     */
    @Override
    public boolean updateUserModel(UserModel model) {
        return userService.updateUserModel(model);
        final boolean isCreate = null == userService.getUserModel(model.username);
        if (userService.updateUserModel(model)) {
            if (isCreate) {
                callCreateUserListeners(model);
            }
            return true;
        }
        return false;
    }
    /**
@@ -236,7 +247,14 @@
     */
    @Override
    public boolean updateUserModel(String username, UserModel model) {
        return userService.updateUserModel(username, model);
        final boolean isCreate = null == userService.getUserModel(username);
        if (userService.updateUserModel(username, model)) {
            if (isCreate) {
                callCreateUserListeners(model);
            }
            return true;
        }
        return false;
    }
    /**
@@ -247,7 +265,11 @@
     */
    @Override
    public boolean deleteUserModel(UserModel model) {
        return userService.deleteUserModel(model);
        if (userService.deleteUserModel(model)) {
            callDeleteUserListeners(model);
            return true;
        }
        return false;
    }
    /**
@@ -262,7 +284,12 @@
            return false;
        }
        String usernameDecoded = StringUtils.decodeUsername(username);
        return userService.deleteUser(usernameDecoded);
        UserModel user = getUserModel(usernameDecoded);
        if (userService.deleteUser(usernameDecoded)) {
            callDeleteUserListeners(user);
            return true;
        }
        return false;
    }
    /**
@@ -349,7 +376,14 @@
     */
    @Override
    public boolean updateTeamModel(TeamModel model) {
        return userService.updateTeamModel(model);
        final boolean isCreate = null == userService.getTeamModel(model.name);
        if (userService.updateTeamModel(model)) {
            if (isCreate) {
                callCreateTeamListeners(model);
            }
            return true;
        }
        return false;
    }
    /**
@@ -377,7 +411,14 @@
     */
    @Override
    public boolean updateTeamModel(String teamname, TeamModel model) {
        return userService.updateTeamModel(teamname, model);
        final boolean isCreate = null == userService.getTeamModel(teamname);
        if (userService.updateTeamModel(teamname, model)) {
            if (isCreate) {
                callCreateTeamListeners(model);
            }
            return true;
        }
        return false;
    }
    /**
@@ -389,7 +430,11 @@
     */
    @Override
    public boolean deleteTeamModel(TeamModel model) {
        return userService.deleteTeamModel(model);
        if (userService.deleteTeamModel(model)) {
            callDeleteTeamListeners(model);
            return true;
        }
        return false;
    }
    /**
@@ -401,7 +446,12 @@
     */
    @Override
    public boolean deleteTeam(String teamname) {
        return userService.deleteTeam(teamname);
        TeamModel team = userService.getTeamModel(teamname);
        if (userService.deleteTeam(teamname)) {
            callDeleteTeamListeners(team);
            return true;
        }
        return false;
    }
    /**
@@ -440,4 +490,60 @@
    public boolean deleteRepositoryRole(String role) {
        return userService.deleteRepositoryRole(role);
    }
    protected void callCreateUserListeners(UserModel user) {
        if (pluginManager == null || user == null) {
            return;
        }
        for (UserTeamLifeCycleListener listener : pluginManager.getExtensions(UserTeamLifeCycleListener.class)) {
            try {
                listener.onCreation(user);
            } catch (Throwable t) {
                logger.error(String.format("failed to call plugin.onCreation%s", user.username), t);
            }
        }
    }
    protected void callCreateTeamListeners(TeamModel team) {
        if (pluginManager == null || team == null) {
            return;
        }
        for (UserTeamLifeCycleListener listener : pluginManager.getExtensions(UserTeamLifeCycleListener.class)) {
            try {
                listener.onCreation(team);
            } catch (Throwable t) {
                logger.error(String.format("failed to call plugin.onCreation %s", team.name), t);
            }
        }
    }
    protected void callDeleteUserListeners(UserModel user) {
        if (pluginManager == null || user == null) {
            return;
        }
        for (UserTeamLifeCycleListener listener : pluginManager.getExtensions(UserTeamLifeCycleListener.class)) {
            try {
                listener.onDeletion(user);
            } catch (Throwable t) {
                logger.error(String.format("failed to call plugin.onDeletion %s", user.username), t);
            }
        }
    }
    protected void callDeleteTeamListeners(TeamModel team) {
        if (pluginManager == null || team == null) {
            return;
        }
        for (UserTeamLifeCycleListener listener : pluginManager.getExtensions(UserTeamLifeCycleListener.class)) {
            try {
                listener.onDeletion(team);
            } catch (Throwable t) {
                logger.error(String.format("failed to call plugin.onDeletion %s", team.name), t);
            }
        }
    }
}
src/main/java/com/gitblit/models/Menu.java
New file
@@ -0,0 +1,302 @@
package com.gitblit.models;
import java.io.Serializable;
import org.apache.wicket.PageParameters;
import org.apache.wicket.markup.html.WebPage;
import com.gitblit.utils.StringUtils;
public class Menu {
    /**
     * A MenuItem for a drop down menu.
     *
     * @author James Moger
     * @since 1.6.0
     */
    public abstract static class MenuItem implements Serializable {
        private static final long serialVersionUID = 1L;
        final String displayText;
        MenuItem(String displayText) {
            this.displayText = displayText;
        }
        @Override
        public int hashCode() {
            return displayText.hashCode();
        }
        @Override
        public boolean equals(Object o) {
            if (o instanceof MenuItem) {
                return hashCode() == o.hashCode();
            }
            return false;
        }
        @Override
        public String toString() {
            return displayText;
        }
    }
    /**
     * A divider for the menu.
     *
     * @since 1.6.0
     */
    public static class MenuDivider extends MenuItem {
        private static final long serialVersionUID = 1L;
        public MenuDivider() {
            super("");
        }
    }
    /**
     * A MenuItem for setting a parameter of the current url.
     *
     * @author James Moger
     *
     */
    public static class ParameterMenuItem extends MenuItem {
        private static final long serialVersionUID = 1L;
        final PageParameters parameters;
        final String parameter;
        final String value;
        final boolean isSelected;
        /**
         * @param displayText
         */
        public ParameterMenuItem(String displayText) {
            this(displayText, null, null, null);
        }
        /**
         * @param displayText
         * @param parameter
         * @param value
         */
        public ParameterMenuItem(String displayText, String parameter, String value) {
            this(displayText, parameter, value, null);
        }
        /**
         * @param displayText
         * @param parameter
         * @param value
         */
        public ParameterMenuItem(String displayText, String parameter, String value,
                PageParameters params) {
            super(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);
                    // set the new selection value
                    setParameter(parameter, value);
                } else {
                    // not currently selected
                    isSelected = false;
                    setParameter(parameter, value);
                }
            }
        }
        protected 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 isSelected() {
            return isSelected;
        }
        @Override
        public int hashCode() {
            if (StringUtils.isEmpty(displayText)) {
                return value.hashCode() + parameter.hashCode();
            }
            return displayText.hashCode();
        }
        @Override
        public boolean equals(Object o) {
            if (o instanceof MenuItem) {
                return hashCode() == o.hashCode();
            }
            return false;
        }
        @Override
        public String toString() {
            if (StringUtils.isEmpty(displayText)) {
                return formatParameter();
            }
            return displayText;
        }
    }
    /**
     * Menu item for toggling a parameter.
     *
     */
    public static class ToggleMenuItem extends ParameterMenuItem {
        private static final long serialVersionUID = 1L;
        /**
         * @param displayText
         * @param parameter
         * @param value
         */
        public ToggleMenuItem(String displayText, String parameter, String value,
                PageParameters params) {
            super(displayText, parameter, value, params);
            if (isSelected) {
                // already selected, so remove this enables toggling
                parameters.remove(parameter);
            }
        }
    }
    /**
     * Menu item for linking to another Wicket page.
     *
     * @since 1.6.0
     */
    public static class PageLinkMenuItem extends MenuItem {
        private static final long serialVersionUID = 1L;
        private final Class<? extends WebPage> pageClass;
        private final PageParameters params;
        /**
         * Page Link Item links to another page.
         *
         * @param displayText
         * @param pageClass
         * @since 1.6.0
         */
        public PageLinkMenuItem(String displayText, Class<? extends WebPage> pageClass) {
            this(displayText, pageClass, null);
        }
        /**
         * Page Link Item links to another page.
         *
         * @param displayText
         * @param pageClass
         * @param params
         * @since 1.6.0
         */
        public PageLinkMenuItem(String displayText, Class<? extends WebPage> pageClass, PageParameters params) {
            super(displayText);
            this.pageClass = pageClass;
            this.params = params;
        }
        /**
         * @return the page class
         * @since 1.6.0
         */
        public Class<? extends WebPage> getPageClass() {
            return pageClass;
        }
        /**
         * @return the page parameters
         * @since 1.6.0
         */
        public PageParameters getPageParameters() {
            return params;
        }
    }
    /**
     * Menu item to link to an external page.
     *
     * @since 1.6.0
     */
    public static class ExternalLinkMenuItem extends MenuItem {
        private static final long serialVersionUID = 1L;
        private final String href;
        private final boolean newWindow;
        /**
         * External Link Item links to something else.
         *
         * @param displayText
         * @param href
         * @since 1.6.0
         */
        public ExternalLinkMenuItem(String displayText, String href) {
            this(displayText, href, false);
        }
        /**
         * External Link Item links to something else.
         *
         * @param displayText
         * @param href
         * @since 1.6.0
         */
        public ExternalLinkMenuItem(String displayText, String href, boolean newWindow) {
            super(displayText);
            this.href = href;
            this.newWindow = newWindow;
        }
        /**
         * @since 1.6.0
         */
        public String getHref() {
            return href;
        }
        /**
         * @since 1.6.0
         */
        public boolean openInNewWindow() {
            return newWindow;
        }
    }
}
src/main/java/com/gitblit/models/NavLink.java
New file
@@ -0,0 +1,140 @@
/*
 * 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 org.apache.wicket.PageParameters;
import org.apache.wicket.markup.html.WebPage;
import com.gitblit.models.Menu.MenuItem;
/**
 * Represents a navigation link for the navigation panel.
 *
 * @author James Moger
 *
 */
public abstract class NavLink implements Serializable {
    private static final long serialVersionUID = 1L;
    public final String translationKey;
    public final boolean hiddenPhone;
    public NavLink(String translationKey, boolean hiddenPhone) {
        this.translationKey = translationKey;
        this.hiddenPhone = hiddenPhone;
    }
    /**
     * Represents a Wicket page link.
     *
     * @author James Moger
     *
     */
    public static class PageNavLink extends NavLink implements Serializable {
        private static final long serialVersionUID = 1L;
        public final Class<? extends WebPage> pageClass;
        public final PageParameters params;
        public PageNavLink(String translationKey, Class<? extends WebPage> pageClass) {
            this(translationKey, pageClass, null);
        }
        public PageNavLink(String translationKey, Class<? extends WebPage> pageClass,
                PageParameters params) {
            this(translationKey, pageClass, params, false);
        }
        public PageNavLink(String translationKey, Class<? extends WebPage> pageClass,
                PageParameters params, boolean hiddenPhone) {
            super(translationKey, hiddenPhone);
            this.pageClass = pageClass;
            this.params = params;
        }
    }
    /**
     * Represents an explicitly href link.
     *
     * @author James Moger
     *
     */
    public static class ExternalNavLink extends NavLink implements Serializable {
        private static final long serialVersionUID = 1L;
        public final String url;
        public ExternalNavLink(String keyOrText, String url) {
            super(keyOrText, false);
            this.url = url;
        }
        public ExternalNavLink(String keyOrText, String url, boolean hiddenPhone) {
            super(keyOrText,  hiddenPhone);
            this.url = url;
        }
    }
    /**
     * Represents a DropDownMenu for the current page.
     *
     * @author James Moger
     *
     */
    public static class DropDownPageMenuNavLink extends PageNavLink implements Serializable {
        private static final long serialVersionUID = 1L;
        public final List<MenuItem> menuItems;
        public DropDownPageMenuNavLink(String keyOrText, Class<? extends WebPage> pageClass) {
            this(keyOrText, pageClass, false);
        }
        public DropDownPageMenuNavLink(String keyOrText, Class<? extends WebPage> pageClass, boolean hiddenPhone) {
            super(keyOrText, pageClass, null, hiddenPhone);
            menuItems = new ArrayList<MenuItem>();
        }
    }
    /**
     * Represents a DropDownMenu.
     *
     * @author James Moger
     *
     */
    public static class DropDownMenuNavLink extends NavLink implements Serializable {
        private static final long serialVersionUID = 1L;
        public final List<MenuItem> menuItems;
        public DropDownMenuNavLink(String keyOrText) {
            this(keyOrText, false);
        }
        public DropDownMenuNavLink(String keyOrText, boolean hiddenPhone) {
            super(keyOrText, hiddenPhone);
            menuItems = new ArrayList<MenuItem>();
        }
    }
}
src/main/java/com/gitblit/models/UserPreferences.java
@@ -23,6 +23,7 @@
import java.util.Map;
import java.util.TreeMap;
import com.gitblit.Constants.Transport;
import com.gitblit.utils.StringUtils;
/**
@@ -37,7 +38,11 @@
    public final String username;
    public String locale;
    private String locale;
    private Boolean emailMeOnMyTicketChanges;
    private Transport transport;
    private final Map<String, UserRepositoryPreferences> repositoryPreferences = new TreeMap<String, UserRepositoryPreferences>();
@@ -56,6 +61,10 @@
            return new Locale(lang, cc);
        }
        return new Locale(locale);
    }
    public void setLocale(String locale) {
        this.locale = locale;
    }
    public UserRepositoryPreferences getRepositoryPreferences(String repositoryName) {
@@ -96,4 +105,23 @@
        Collections.sort(list);
        return list;
    }
    public boolean isEmailMeOnMyTicketChanges() {
        if (emailMeOnMyTicketChanges == null) {
            return true;
        }
        return emailMeOnMyTicketChanges;
    }
    public void setEmailMeOnMyTicketChanges(boolean value) {
        this.emailMeOnMyTicketChanges = value;
    }
    public Transport getTransport() {
        return transport;
    }
    public void setTransport(Transport transport) {
        this.transport = transport;
    }
}
src/main/java/com/gitblit/service/MailService.java
@@ -68,6 +68,7 @@
        final String mailUser = settings.getString(Keys.mail.username, null);
        final String mailPassword = settings.getString(Keys.mail.password, null);
        final boolean smtps = settings.getBoolean(Keys.mail.smtps, false);
        final boolean starttls = settings.getBoolean(Keys.mail.starttls, false);
        boolean authenticate = !StringUtils.isEmpty(mailUser) && !StringUtils.isEmpty(mailPassword);
        String server = settings.getString(Keys.mail.server, "");
        if (StringUtils.isEmpty(server)) {
@@ -86,6 +87,7 @@
        props.setProperty("mail.smtp.port", String.valueOf(port));
        props.setProperty("mail.smtp.auth", String.valueOf(authenticate));
        props.setProperty("mail.smtp.auths", String.valueOf(authenticate));
        props.setProperty("mail.smtp.starttls.enable", String.valueOf(starttls));
        if (isGMail || smtps) {
            props.setProperty("mail.smtp.starttls.enable", "true");
src/main/java/com/gitblit/servlet/AccessRestrictionFilter.java
@@ -19,6 +19,7 @@
import java.text.MessageFormat;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
@@ -54,8 +55,8 @@
    protected IRepositoryManager repositoryManager;
    @Override
    protected void inject(ObjectGraph dagger) {
        super.inject(dagger);
    protected void inject(ObjectGraph dagger, FilterConfig filterConfig) {
        super.inject(dagger, filterConfig);
        this.runtimeManager = dagger.get(IRuntimeManager.class);
        this.repositoryManager = dagger.get(IRepositoryManager.class);
    }
src/main/java/com/gitblit/servlet/AuthenticationFilter.java
@@ -22,6 +22,7 @@
import java.util.Map;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
@@ -62,7 +63,7 @@
    protected IAuthenticationManager authenticationManager;
    @Override
    protected void inject(ObjectGraph dagger) {
    protected void inject(ObjectGraph dagger, FilterConfig filterConfig) {
        this.authenticationManager = dagger.get(IAuthenticationManager.class);
    }
src/main/java/com/gitblit/servlet/EnforceAuthenticationFilter.java
@@ -19,6 +19,7 @@
import java.text.MessageFormat;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
@@ -53,7 +54,7 @@
    private IAuthenticationManager authenticationManager;
    @Override
    protected void inject(ObjectGraph dagger) {
    protected void inject(ObjectGraph dagger, FilterConfig filterConfig) {
        this.settings = dagger.get(IStoredSettings.class);
        this.authenticationManager = dagger.get(IAuthenticationManager.class);
    }
src/main/java/com/gitblit/servlet/FilterRuntimeConfig.java
New file
@@ -0,0 +1,71 @@
/*
 * Copyright 2014 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.servlet;
import java.util.Enumeration;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
import com.gitblit.IStoredSettings;
import com.gitblit.manager.IRuntimeManager;
/**
 * Wraps a filter config and will prefer a setting retrieved from IStoredSettings
 * if one is available.
 *
 * @author James Moger
 * @since 1.6.0
 */
public class FilterRuntimeConfig implements FilterConfig {
    final IRuntimeManager runtime;
    final IStoredSettings settings;
    final String namespace;
    final FilterConfig config;
    public FilterRuntimeConfig(IRuntimeManager runtime, String namespace, FilterConfig config) {
        this.runtime = runtime;
        this.settings = runtime.getSettings();
        this.namespace = namespace;
        this.config = config;
    }
    @Override
    public String getFilterName() {
        return config.getFilterName();
    }
    @Override
    public ServletContext getServletContext() {
        return config.getServletContext();
    }
    @Override
    public String getInitParameter(String name) {
        String key = namespace + "." + name;
        if (settings.hasSettings(key)) {
            String value = settings.getString(key, null);
            return value;
        }
        return config.getInitParameter(name);
    }
    @Override
    public Enumeration<String> getInitParameterNames() {
        return config.getInitParameterNames();
    }
}
src/main/java/com/gitblit/servlet/GitFilter.java
@@ -17,6 +17,7 @@
import java.text.MessageFormat;
import javax.servlet.FilterConfig;
import javax.servlet.http.HttpServletRequest;
import com.gitblit.Constants.AccessRestrictionType;
@@ -53,8 +54,8 @@
    private IFederationManager federationManager;
    @Override
    protected void inject(ObjectGraph dagger) {
        super.inject(dagger);
    protected void inject(ObjectGraph dagger, FilterConfig filterConfig) {
        super.inject(dagger, filterConfig);
        this.settings = dagger.get(IStoredSettings.class);
        this.federationManager = dagger.get(IFederationManager.class);
    }
src/main/java/com/gitblit/servlet/GitblitContext.java
@@ -38,6 +38,7 @@
import com.gitblit.Keys;
import com.gitblit.WebXmlSettings;
import com.gitblit.dagger.DaggerContext;
import com.gitblit.extensions.LifeCycleListener;
import com.gitblit.manager.IAuthenticationManager;
import com.gitblit.manager.IFederationManager;
import com.gitblit.manager.IGitblit;
@@ -174,6 +175,9 @@
        runtime.start();
        managers.add(runtime);
        // create the plugin manager instance but do not start it
        loadManager(injector, IPluginManager.class);
        // start all other managers
        startManager(injector, INotificationManager.class);
        startManager(injector, IUserManager.class);
@@ -191,6 +195,15 @@
        logger.info("");
        logger.info("All managers started.");
        logger.info("");
        IPluginManager pluginManager = injector.get(IPluginManager.class);
        for (LifeCycleListener listener : pluginManager.getExtensions(LifeCycleListener.class)) {
            try {
                listener.onStartup();
            } catch (Throwable t) {
                logger.error(null, t);
            }
        }
    }
    private String lookupBaseFolderFromJndi() {
@@ -205,9 +218,14 @@
        return null;
    }
    protected <X extends IManager> X startManager(ObjectGraph injector, Class<X> clazz) {
        logManager(clazz);
    protected <X extends IManager> X loadManager(ObjectGraph injector, Class<X> clazz) {
        X x = injector.get(clazz);
        return x;
    }
    protected <X extends IManager> X startManager(ObjectGraph injector, Class<X> clazz) {
        X x = loadManager(injector, clazz);
        logManager(clazz);
        x.start();
        managers.add(x);
        return x;
@@ -225,6 +243,16 @@
    @Override
    protected void destroyContext(ServletContext context) {
        logger.info("Gitblit context destroyed by servlet container.");
        IPluginManager pluginManager = getManager(IPluginManager.class);
        for (LifeCycleListener listener : pluginManager.getExtensions(LifeCycleListener.class)) {
            try {
                listener.onShutdown();
            } catch (Throwable t) {
                logger.error(null, t);
            }
        }
        for (IManager manager : managers) {
            logger.debug("stopping {}", manager.getClass().getSimpleName());
            manager.stop();
@@ -352,6 +380,22 @@
            }
        }
        // Copy the included gitignore files to the configured gitignore folder
        String gitignorePath = webxmlSettings.getString(Keys.git.gitignoreFolder, "gitignore");
        File localGitignores = com.gitblit.utils.FileUtils.resolveParameter(Constants.baseFolder$, base, gitignorePath);
        if (!localGitignores.exists()) {
            File warGitignores = new File(contextFolder, "/WEB-INF/data/gitignore");
            if (!warGitignores.equals(localGitignores)) {
                try {
                    com.gitblit.utils.FileUtils.copy(localGitignores, warGitignores.listFiles());
                } catch (IOException e) {
                    logger.error(MessageFormat.format(
                            "Failed to copy included .gitignore files from {0} to {1}",
                            warGitignores, localGitignores));
                }
            }
        }
        // merge the WebXmlSettings into the runtime settings (for backwards-compatibilty)
        runtimeSettings.merge(webxmlSettings);
src/main/java/com/gitblit/servlet/PagesFilter.java
@@ -15,11 +15,6 @@
 */
package com.gitblit.servlet;
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
@@ -28,99 +23,7 @@
 * @author James Moger
 *
 */
public class PagesFilter extends AccessRestrictionFilter {
public class PagesFilter extends RawFilter {
    /**
     * 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 = repositoryManager.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 cloneUrl
     * @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);
    }
}
src/main/java/com/gitblit/servlet/PagesServlet.java
@@ -15,42 +15,10 @@
 */
package com.gitblit.servlet;
import java.io.IOException;
import java.text.MessageFormat;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jgit.lib.FileMode;
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.Constants;
import com.gitblit.IStoredSettings;
import com.gitblit.Keys;
import com.gitblit.dagger.DaggerServlet;
import com.gitblit.manager.IRepositoryManager;
import com.gitblit.models.PathModel;
import com.gitblit.models.RefModel;
import com.gitblit.utils.ArrayUtils;
import com.gitblit.utils.ByteFormat;
import com.gitblit.utils.JGitUtils;
import com.gitblit.utils.MarkdownUtils;
import com.gitblit.utils.StringUtils;
import com.gitblit.wicket.MarkupProcessor;
import com.gitblit.wicket.MarkupProcessor.MarkupDocument;
import dagger.ObjectGraph;
/**
 * Serves the content of a gh-pages branch.
@@ -58,21 +26,10 @@
 * @author James Moger
 *
 */
public class PagesServlet extends DaggerServlet {
public class PagesServlet extends RawServlet {
    private static final long serialVersionUID = 1L;
    private transient Logger logger = LoggerFactory.getLogger(PagesServlet.class);
    private IStoredSettings settings;
    private IRepositoryManager repositoryManager;
    @Override
    protected void inject(ObjectGraph dagger) {
        this.settings = dagger.get(IStoredSettings.class);
        this.repositoryManager = dagger.get(IRepositoryManager.class);
    }
    /**
     * Returns an url to this servlet for the specified parameters.
@@ -89,248 +46,31 @@
        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 = repositoryManager.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);
                return;
            }
            MarkupProcessor processor = new MarkupProcessor(settings);
            String [] encodings = settings.getStrings(Keys.web.blobEncodings).toArray(new String[0]);
            RevTree tree = commit.getTree();
            String res = resource;
            if (res.endsWith("/")) {
                res = res.substring(0, res.length() - 1);
            }
            List<PathModel> pathEntries = JGitUtils.getFilesInPath(r, res, commit);
            byte[] content = null;
            if (pathEntries.isEmpty()) {
                // not a path, a specific resource
                try {
                    String contentType = context.getMimeType(res);
                    if (contentType == null) {
                        contentType = "text/plain";
                    }
                    if (contentType.startsWith("text")) {
                        content = JGitUtils.getStringContent(r, tree, res, encodings).getBytes(
                                Constants.ENCODING);
                    } else {
                        content = JGitUtils.getByteContent(r, tree, res, false);
                    }
                    response.setContentType(contentType);
                } catch (Exception e) {
                }
            } else {
                // path request
                if (!request.getPathInfo().endsWith("/")) {
                    // redirect to trailing '/' url
                    response.sendRedirect(request.getServletPath() + request.getPathInfo() + "/");
                    return;
                }
                Map<String, String> names = new TreeMap<String, String>();
                for (PathModel entry : pathEntries) {
                    names.put(entry.name.toLowerCase(), entry.name);
                }
                List<String> extensions = new ArrayList<String>();
                extensions.add("html");
                extensions.add("htm");
                extensions.addAll(processor.getMarkupExtensions());
                for (String ext : extensions) {
                    String key = "index." + ext;
                    if (names.containsKey(key)) {
                        String fileName = names.get(key);
                        String fullPath = fileName;
                        if (!res.isEmpty()) {
                            fullPath = res + "/" + fileName;
                        }
                        String stringContent = JGitUtils.getStringContent(r, tree, fullPath, encodings);
                        if (stringContent == null) {
                            continue;
                        }
                        content = stringContent.getBytes(Constants.ENCODING);
                        if (content != null) {
                            res = fullPath;
                            // assume text/html unless the servlet container
                            // overrides
                            response.setContentType("text/html; charset=" + Constants.ENCODING);
                            break;
                        }
                    }
                }
            }
            // no content, document list or custom 404 page
            if (ArrayUtils.isEmpty(content)) {
                if (pathEntries.isEmpty()) {
                    // 404
                    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);
                    }
                } else {
                    // document list
                    response.setContentType("text/html");
                    response.getWriter().append("<style>table th, table td { min-width: 150px; text-align: left; }</style>");
                    response.getWriter().append("<table>");
                    response.getWriter().append("<thead><tr><th>path</th><th>mode</th><th>size</th></tr>");
                    response.getWriter().append("</thead>");
                    response.getWriter().append("<tbody>");
                    String pattern = "<tr><td><a href=\"{0}/{1}\">{1}</a></td><td>{2}</td><td>{3}</td></tr>";
                    final ByteFormat byteFormat = new ByteFormat();
                    if (!pathEntries.isEmpty()) {
                        if (pathEntries.get(0).path.indexOf('/') > -1) {
                            // we are in a subdirectory, add parent directory link
                            pathEntries.add(0, new PathModel("..", resource + "/..", 0, FileMode.TREE.getBits(), null, null));
                        }
                    }
                    String basePath = request.getServletPath() + request.getPathInfo();
                    if (basePath.charAt(basePath.length() - 1) == '/') {
                        // strip trailing slash
                        basePath = basePath.substring(0, basePath.length() - 1);
                    }
                    for (PathModel entry : pathEntries) {
                        response.getWriter().append(MessageFormat.format(pattern, basePath, entry.name,
                                JGitUtils.getPermissionsFromMode(entry.mode), byteFormat.format(entry.size)));
                    }
                    response.getWriter().append("</tbody>");
                    response.getWriter().append("</table>");
                }
                return;
            }
            // check to see if we should transform markup files
            String ext = StringUtils.getFileExtension(resource);
            if (processor.getMarkupExtensions().contains(ext)) {
                String markup = new String(content, Constants.ENCODING);
                MarkupDocument markupDoc = processor.parse(repository, commit.getName(), resource, markup);
                content = markupDoc.html.getBytes("UTF-8");
                response.setContentType("text/html; charset=" + Constants.ENCODING);
            }
            try {
                // output the content
                response.setHeader("Cache-Control", "public, max-age=3600, must-revalidate");
                response.setDateHeader("Last-Modified", JGitUtils.getCommitDate(commit).getTime());
                response.getOutputStream().write(content);
                response.flushBuffer();
            } catch (Throwable t) {
                logger.error("Failed to write page to client", t);
            }
        } catch (Throwable t) {
            logger.error("Failed to write page to client", t);
        } finally {
            r.close();
        }
    }
    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 String getBranch(String repository, HttpServletRequest request) {
        return "gh-pages";
    }
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        processRequest(request, response);
    protected String getPath(String repository, String branch, HttpServletRequest request) {
        String pi = request.getPathInfo().substring(1);
        if (pi.equals(repository)) {
            return "";
        }
        String path = pi.substring(pi.indexOf(repository) + repository.length() + 1);
        if (path.endsWith("/")) {
            path = path.substring(0, path.length() - 1);
        }
        return path;
    }
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        processRequest(request, response);
    protected boolean renderIndex() {
        return true;
    }
    @Override
    protected void setContentType(HttpServletResponse response, String contentType) {
        response.setContentType(contentType);;
    }
}
src/main/java/com/gitblit/servlet/ProxyFilter.java
New file
@@ -0,0 +1,86 @@
/*
 * Copyright 2014 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.servlet;
import java.io.IOException;
import java.util.Iterator;
import java.util.List;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import ro.fortsoft.pf4j.PluginWrapper;
import com.gitblit.dagger.DaggerFilter;
import com.gitblit.extensions.HttpRequestFilter;
import com.gitblit.manager.IPluginManager;
import com.gitblit.manager.IRuntimeManager;
import dagger.ObjectGraph;
/**
 * A request filter than allows registered extension request filters to access
 * request data.  The intended purpose is for server monitoring plugins.
 *
 * @author David Ostrovsky
 * @since 1.6.0
 */
public class ProxyFilter extends DaggerFilter {
    private List<HttpRequestFilter> filters;
    @Override
    protected void inject(ObjectGraph dagger, FilterConfig filterConfig) throws ServletException {
        IRuntimeManager runtimeManager = dagger.get(IRuntimeManager.class);
        IPluginManager pluginManager = dagger.get(IPluginManager.class);
        filters = pluginManager.getExtensions(HttpRequestFilter.class);
        for (HttpRequestFilter f : filters) {
            // wrap the filter config for Gitblit settings retrieval
            PluginWrapper pluginWrapper = pluginManager.whichPlugin(f.getClass());
            FilterConfig runtimeConfig = new FilterRuntimeConfig(runtimeManager,
                    pluginWrapper.getPluginId(), filterConfig);
            f.init(runtimeConfig);
        }
    }
    @Override
    public void doFilter(ServletRequest req, ServletResponse res, final FilterChain last)
            throws IOException, ServletException {
        final Iterator<HttpRequestFilter> itr = filters.iterator();
        new FilterChain() {
            @Override
            public void doFilter(ServletRequest req, ServletResponse res) throws IOException,
                    ServletException {
                if (itr.hasNext()) {
                    itr.next().doFilter(req, res, this);
                } else {
                    last.doFilter(req, res);
                }
            }
        }.doFilter(req, res);
    }
    @Override
    public void destroy() {
        for (HttpRequestFilter f : filters) {
            f.destroy();
        }
    }
}
src/main/java/com/gitblit/servlet/RawFilter.java
New file
@@ -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.servlet;
import org.eclipse.jgit.lib.Repository;
import com.gitblit.Constants.AccessRestrictionType;
import com.gitblit.models.RepositoryModel;
import com.gitblit.models.UserModel;
/**
 * The RawFilter is an AccessRestrictionFilter which ensures http branch
 * requests for a view-restricted repository are authenticated and authorized.
 *
 * @author James Moger
 *
 */
public class RawFilter 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 = repositoryManager.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 cloneUrl
     * @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);
    }
}
src/main/java/com/gitblit/servlet/RawServlet.java
New file
@@ -0,0 +1,483 @@
/*
 * Copyright 2014 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.servlet;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.text.MessageFormat;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.tika.Tika;
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;
import com.gitblit.Constants;
import com.gitblit.Keys;
import com.gitblit.dagger.DaggerServlet;
import com.gitblit.manager.IRepositoryManager;
import com.gitblit.manager.IRuntimeManager;
import com.gitblit.models.PathModel;
import com.gitblit.utils.ByteFormat;
import com.gitblit.utils.JGitUtils;
import com.gitblit.utils.MarkdownUtils;
import com.gitblit.utils.StringUtils;
import dagger.ObjectGraph;
/**
 * Serves the content of a branch.
 *
 * @author James Moger
 *
 */
public class RawServlet extends DaggerServlet {
    private static final long serialVersionUID = 1L;
    private transient Logger logger = LoggerFactory.getLogger(RawServlet.class);
    private IRuntimeManager runtimeManager;
    private IRepositoryManager repositoryManager;
    @Override
    protected void inject(ObjectGraph dagger) {
        this.runtimeManager = dagger.get(IRuntimeManager.class);
        this.repositoryManager = dagger.get(IRepositoryManager.class);
    }
    /**
     * Returns an url to this servlet for the specified parameters.
     *
     * @param baseURL
     * @param repository
     * @param branch
     * @param path
     * @return an url
     */
    public static String asLink(String baseURL, String repository, String branch, String path) {
        if (baseURL.length() > 0 && baseURL.charAt(baseURL.length() - 1) == '/') {
            baseURL = baseURL.substring(0, baseURL.length() - 1);
        }
        if (branch != null) {
            char fsc = '!';
            char c = GitblitContext.getManager(IRuntimeManager.class).getSettings().getChar(Keys.web.forwardSlashCharacter, '/');
            if (c != '/') {
                fsc = c;
            }
            branch = branch.replace('/', fsc);
        }
        String encodedPath = path == null ? "" : path.replace(' ', '-');
        try {
            encodedPath = URLEncoder.encode(encodedPath, "UTF-8");
        } catch (UnsupportedEncodingException e) {
        }
        return baseURL + Constants.RAW_PATH + repository + "/" + (branch == null ? "" : (branch + "/" + (path == null ? "" : encodedPath)));
    }
    protected String getBranch(String repository, HttpServletRequest request) {
        String pi = request.getPathInfo();
        String branch = pi.substring(pi.indexOf(repository) + repository.length() + 1);
        int fs = branch.indexOf('/');
        if (fs > -1) {
            branch = branch.substring(0, fs);
        }
        char c = runtimeManager.getSettings().getChar(Keys.web.forwardSlashCharacter, '/');
        return branch.replace('!', '/').replace(c, '/');
    }
    protected String getPath(String repository, String branch, HttpServletRequest request) {
        String base = repository + "/" + branch;
        String pi = request.getPathInfo().substring(1);
        if (pi.equals(base)) {
            return "";
        }
        String path = pi.substring(pi.indexOf(base) + base.length() + 1);
        if (path.endsWith("/")) {
            path = path.substring(0, path.length() - 1);
        }
        return path;
    }
    protected boolean renderIndex() {
        return false;
    }
    /**
     * Retrieves the specified resource from the specified 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 = "";
        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);
            }
            offset += slash;
            r = repositoryManager.getRepository(repository, false);
            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}!",
                        path);
                error(response, mkd);
                return;
            }
            // identify the branch
            String branch = getBranch(repository, request);
            if (StringUtils.isEmpty(branch)) {
                branch = r.getBranch();
                if (branch == null) {
                    // no branches found!  empty?
                    String mkd = MessageFormat.format(
                            "# Error\nSorry, no valid **branch** specified in this url: {0}!",
                            path);
                    error(response, mkd);
                } else {
                    // redirect to default branch
                    String base = request.getRequestURI();
                    String url = base + branch + "/";
                    response.sendRedirect(url);
                }
                return;
            }
            // identify the requested path
            String requestedPath = getPath(repository, branch, request);
            // identify the commit
            RevCommit commit = JGitUtils.getCommit(r, branch);
            if (commit == null) {
                // branch not found!
                String mkd = MessageFormat.format(
                        "# Error\nSorry, the repository {0} does not have a **{1}** branch!",
                        repository, branch);
                error(response, mkd);
                return;
            }
            List<PathModel> pathEntries = JGitUtils.getFilesInPath(r, requestedPath, commit);
            if (pathEntries.isEmpty()) {
                // requested a specific resource
                String file = StringUtils.getLastPathElement(requestedPath);
                try {
                    // query Tika for the content type
                    Tika tika = new Tika();
                    String contentType = tika.detect(file);
                    if (contentType == null) {
                        // ask the container for the content type
                        contentType = context.getMimeType(requestedPath);
                        if (contentType == null) {
                            // still unknown content type, assume binary
                            contentType = "application/octet-stream";
                        }
                    }
                    setContentType(response, contentType);
                    if (isTextType(contentType)) {
                        // load, interpret, and serve text content as UTF-8
                        String [] encodings = runtimeManager.getSettings().getStrings(Keys.web.blobEncodings).toArray(new String[0]);
                        String content = JGitUtils.getStringContent(r, commit.getTree(), requestedPath, encodings);
                        byte [] bytes = content.getBytes(Constants.ENCODING);
                        response.setContentLength(bytes.length);
                        ByteArrayInputStream is = new ByteArrayInputStream(bytes);
                        sendContent(response, JGitUtils.getCommitDate(commit), is);
                    } else {
                        // serve binary content
                        String filename = StringUtils.getLastPathElement(requestedPath);
                        try {
                            String userAgent = request.getHeader("User-Agent");
                            if (userAgent != null && userAgent.indexOf("MSIE 5.5") > -1) {
                                  response.setHeader("Content-Disposition", "filename=\""
                                          +  URLEncoder.encode(filename, Constants.ENCODING) + "\"");
                            } else if (userAgent != null && userAgent.indexOf("MSIE") > -1) {
                                  response.setHeader("Content-Disposition", "attachment; filename=\""
                                          +  URLEncoder.encode(filename, Constants.ENCODING) + "\"");
                            } else {
                                    response.setHeader("Content-Disposition", "attachment; filename=\""
                                          + new String(filename.getBytes(Constants.ENCODING), "latin1") + "\"");
                            }
                        }
                        catch (UnsupportedEncodingException e) {
                            response.setHeader("Content-Disposition", "attachment; filename=\"" + filename + "\"");
                        }
                        // stream binary content directly from the repository
                        streamFromRepo(response, r, commit, requestedPath);
                    }
                    return;
                } catch (Exception e) {
                    logger.error(null, e);
                }
            } else {
                // path request
                if (!request.getPathInfo().endsWith("/")) {
                    // redirect to trailing '/' url
                    response.sendRedirect(request.getServletPath() + request.getPathInfo() + "/");
                    return;
                }
                if (renderIndex()) {
                    // locate and render an index file
                    Map<String, String> names = new TreeMap<String, String>();
                    for (PathModel entry : pathEntries) {
                        names.put(entry.name.toLowerCase(), entry.name);
                    }
                    List<String> extensions = new ArrayList<String>();
                    extensions.add("html");
                    extensions.add("htm");
                    String content = null;
                    for (String ext : extensions) {
                        String key = "index." + ext;
                        if (names.containsKey(key)) {
                            String fileName = names.get(key);
                            String fullPath = fileName;
                            if (!requestedPath.isEmpty()) {
                                fullPath = requestedPath + "/" + fileName;
                            }
                            String [] encodings = runtimeManager.getSettings().getStrings(Keys.web.blobEncodings).toArray(new String[0]);
                            String stringContent = JGitUtils.getStringContent(r, commit.getTree(), fullPath, encodings);
                            if (stringContent == null) {
                                continue;
                            }
                            content = stringContent;
                            requestedPath = fullPath;
                            break;
                        }
                    }
                    response.setContentType("text/html; charset=" + Constants.ENCODING);
                    byte [] bytes = content.getBytes(Constants.ENCODING);
                    response.setContentLength(bytes.length);
                    ByteArrayInputStream is = new ByteArrayInputStream(bytes);
                    sendContent(response, JGitUtils.getCommitDate(commit), is);
                    return;
                }
            }
            // no content, document list or 404 page
            if (pathEntries.isEmpty()) {
                // default 404 page
                String str = MessageFormat.format(
                        "# Error\nSorry, the requested resource **{0}** was not found.",
                        requestedPath);
                response.setStatus(HttpServletResponse.SC_NOT_FOUND);
                error(response, str);
                return;
            } else {
                //
                // directory list
                //
                response.setContentType("text/html");
                response.getWriter().append("<style>table th, table td { min-width: 150px; text-align: left; }</style>");
                response.getWriter().append("<table>");
                response.getWriter().append("<thead><tr><th>path</th><th>mode</th><th>size</th></tr>");
                response.getWriter().append("</thead>");
                response.getWriter().append("<tbody>");
                String pattern = "<tr><td><a href=\"{0}/{1}\">{1}</a></td><td>{2}</td><td>{3}</td></tr>";
                final ByteFormat byteFormat = new ByteFormat();
                if (!pathEntries.isEmpty()) {
                    if (pathEntries.get(0).path.indexOf('/') > -1) {
                        // we are in a subdirectory, add parent directory link
                        String pp = URLEncoder.encode(requestedPath, Constants.ENCODING);
                        pathEntries.add(0, new PathModel("..", pp + "/..", 0, FileMode.TREE.getBits(), null, null));
                    }
                }
                String basePath = request.getServletPath() + request.getPathInfo();
                if (basePath.charAt(basePath.length() - 1) == '/') {
                    // strip trailing slash
                    basePath = basePath.substring(0, basePath.length() - 1);
                }
                for (PathModel entry : pathEntries) {
                    String pp = URLEncoder.encode(entry.name, Constants.ENCODING);
                    response.getWriter().append(MessageFormat.format(pattern, basePath, pp,
                            JGitUtils.getPermissionsFromMode(entry.mode),
                            entry.isFile() ? byteFormat.format(entry.size) : ""));
                }
                response.getWriter().append("</tbody>");
                response.getWriter().append("</table>");
            }
        } catch (Throwable t) {
            logger.error("Failed to write page to client", t);
        } finally {
            r.close();
        }
    }
    protected boolean isTextType(String contentType) {
        if (contentType.startsWith("text/")
                || "application/json".equals(contentType)
                || "application/xml".equals(contentType)) {
            return true;
        }
        return false;
    }
    /**
     * Override all text types to be plain text.
     *
     * @param response
     * @param contentType
     */
    protected void setContentType(HttpServletResponse response, String contentType) {
        if (isTextType(contentType)) {
            response.setContentType("text/plain");
        } else {
            response.setContentType(contentType);
        }
    }
    private void streamFromRepo(HttpServletResponse response, Repository repository,
            RevCommit commit, String requestedPath) throws IOException {
        response.setDateHeader("Last-Modified", JGitUtils.getCommitDate(commit).getTime());
        response.setHeader("Cache-Control", "public, max-age=3600, must-revalidate");
        RevWalk rw = new RevWalk(repository);
        TreeWalk tw = new TreeWalk(repository);
        try {
            tw.reset();
            tw.addTree(commit.getTree());
            PathFilter f = PathFilter.create(requestedPath);
            tw.setFilter(f);
            tw.setRecursive(true);
            MutableObjectId id = new MutableObjectId();
            ObjectReader reader = tw.getObjectReader();
            while (tw.next()) {
                FileMode mode = tw.getFileMode(0);
                if (mode == FileMode.GITLINK || mode == FileMode.TREE) {
                    continue;
                }
                tw.getObjectId(id, 0);
                long len = reader.getObjectSize(id, org.eclipse.jgit.lib.Constants.OBJ_BLOB);
                response.setIntHeader("Content-Length", (int) len);
                ObjectLoader ldr = repository.open(id);
                ldr.copyTo(response.getOutputStream());
            }
        } finally {
            tw.release();
            rw.dispose();
        }
        response.flushBuffer();
    }
    private void sendContent(HttpServletResponse response, Date date, InputStream is) throws ServletException, IOException {
        response.setDateHeader("Last-Modified", date.getTime());
        response.setHeader("Cache-Control", "public, max-age=3600, must-revalidate");
        try {
            byte[] tmp = new byte[8192];
            int len = 0;
            while ((len = is.read(tmp)) > -1) {
                response.getOutputStream().write(tmp, 0, len);
            }
        } finally {
            is.close();
        }
        response.flushBuffer();
    }
    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);
    }
}
src/main/java/com/gitblit/servlet/RpcFilter.java
@@ -19,6 +19,7 @@
import java.text.MessageFormat;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
@@ -53,8 +54,8 @@
    private IRuntimeManager runtimeManager;
    @Override
    protected void inject(ObjectGraph dagger) {
        super.inject(dagger);
    protected void inject(ObjectGraph dagger, FilterConfig filterConfig) {
        super.inject(dagger, filterConfig);
        this.settings = dagger.get(IStoredSettings.class);
        this.runtimeManager = dagger.get(IRuntimeManager.class);
    }
src/main/java/com/gitblit/servlet/RpcServlet.java
@@ -53,13 +53,12 @@
 * Handles remote procedure calls.
 *
 * @author James Moger
 *
 */
public class RpcServlet extends JsonServlet {
    private static final long serialVersionUID = 1L;
    public static final int PROTOCOL_VERSION = 7;
    public static final int PROTOCOL_VERSION = 8;
    private IStoredSettings settings;
@@ -80,12 +79,11 @@
     * @throws java.io.IOException
     */
    @Override
    protected void processRequest(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
    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()));
        logger.info(MessageFormat.format("Rpc {0} request from {1}", reqType, request.getRemoteAddr()));
        UserModel user = (UserModel) request.getUserPrincipal();
@@ -130,7 +128,8 @@
                }
                if (model.isCollectingGarbage) {
                    // skip garbage collecting repository
                    logger.warn(MessageFormat.format("Temporarily excluding {0} from RPC, busy collecting garbage", model.name));
                    logger.warn(MessageFormat.format("Temporarily excluding {0} from RPC, busy collecting garbage",
                            model.name));
                    continue;
                }
                // get local branches
@@ -195,6 +194,33 @@
                gitblit.updateRepositoryModel(model.name, model, true);
            } catch (GitBlitException e) {
                response.setStatus(failureCode);
            }
        } else if (RpcRequest.FORK_REPOSITORY.equals(reqType)) {
            // fork repository
            RepositoryModel origin = gitblit.getRepositoryModel(objectName);
            if (origin == null) {
                // failed to find repository, error is logged by the repository
                // manager
                response.setStatus(failureCode);
            } else {
                if (user == null || !user.canFork(origin)) {
                    logger.error("User {} is not permitted to fork '{}'!", user == null ? "anonymous" : user.username,
                            objectName);
                    response.setStatus(failureCode);
                } else {
                    try {
                        // fork the origin
                        RepositoryModel fork = gitblit.fork(origin, user);
                        if (fork == null) {
                            logger.error("Failed to fork repository '{}'!", objectName);
                            response.setStatus(failureCode);
                        } else {
                            logger.info("User {} has forked '{}'!", user.username, objectName);
                        }
                    } catch (GitBlitException e) {
                        response.setStatus(failureCode);
                    }
                }
            }
        } else if (RpcRequest.EDIT_REPOSITORY.equals(reqType)) {
            // edit repository
@@ -281,7 +307,8 @@
        } else if (RpcRequest.SET_REPOSITORY_MEMBER_PERMISSIONS.equals(reqType)) {
            // set the repository permissions for the specified users
            RepositoryModel model = gitblit.getRepositoryModel(objectName);
            Collection<RegistrantAccessPermission> permissions = deserialize(request, response, RpcUtils.REGISTRANT_PERMISSIONS_TYPE);
            Collection<RegistrantAccessPermission> permissions = deserialize(request, response,
                    RpcUtils.REGISTRANT_PERMISSIONS_TYPE);
            result = gitblit.setUserAccessPermissions(model, permissions);
        } else if (RpcRequest.LIST_REPOSITORY_TEAMS.equals(reqType)) {
            // get repository teams
@@ -297,7 +324,8 @@
        } else if (RpcRequest.SET_REPOSITORY_TEAM_PERMISSIONS.equals(reqType)) {
            // set the repository permissions for the specified teams
            RepositoryModel model = gitblit.getRepositoryModel(objectName);
            Collection<RegistrantAccessPermission> permissions = deserialize(request, response, RpcUtils.REGISTRANT_PERMISSIONS_TYPE);
            Collection<RegistrantAccessPermission> permissions = deserialize(request, response,
                    RpcUtils.REGISTRANT_PERMISSIONS_TYPE);
            result = gitblit.setTeamAccessPermissions(model, permissions);
        } else if (RpcRequest.LIST_FEDERATION_REGISTRATIONS.equals(reqType)) {
            // return the list of federation registrations
@@ -363,8 +391,7 @@
        } else if (RpcRequest.EDIT_SETTINGS.equals(reqType)) {
            // update settings on the server
            if (allowAdmin) {
                Map<String, String> map = deserialize(request, response,
                        RpcUtils.SETTINGS_TYPE);
                Map<String, String> map = deserialize(request, response, RpcUtils.SETTINGS_TYPE);
                gitblit.updateSettings(map);
            } else {
                response.sendError(notAllowedCode);
src/main/java/com/gitblit/servlet/SyndicationFilter.java
@@ -19,6 +19,7 @@
import java.text.MessageFormat;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
@@ -50,8 +51,8 @@
    private IProjectManager projectManager;
    @Override
    protected void inject(ObjectGraph dagger) {
        super.inject(dagger);
    protected void inject(ObjectGraph dagger, FilterConfig filterConfig) {
        super.inject(dagger, filterConfig);
        this.runtimeManager = dagger.get(IRuntimeManager.class);
        this.repositoryManager = dagger.get(IRepositoryManager.class);
        this.projectManager = dagger.get(IProjectManager.class);
src/main/java/com/gitblit/servlet/SyndicationServlet.java
@@ -163,6 +163,15 @@
                searchType = type;
            }
        }
        Constants.FeedObjectType objectType = Constants.FeedObjectType.COMMIT;
        if (!StringUtils.isEmpty(request.getParameter("ot"))) {
            Constants.FeedObjectType type = Constants.FeedObjectType.forName(request.getParameter("ot"));
            if (type != null) {
                objectType = type;
            }
        }
        int length = settings.getInteger(Keys.web.syndicationEntries, 25);
        if (StringUtils.isEmpty(objectId)) {
            objectId = org.eclipse.jgit.lib.Constants.HEAD;
@@ -214,14 +223,7 @@
        boolean mountParameters = settings.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 = settings.getString(Keys.web.canonicalUrl, null);
        if (StringUtils.isEmpty(gitblitUrl)) {
            gitblitUrl = HttpUtils.getGitblitURL(request);
@@ -247,47 +249,92 @@
                feedDescription = model.description;
            }
            List<RevCommit> commits;
            if (StringUtils.isEmpty(searchString)) {
                // standard log/history lookup
                commits = JGitUtils.getRevLog(repository, objectId, offset, length);
            if (objectType == Constants.FeedObjectType.TAG) {
                String urlPattern;
                if (mountParameters) {
                    // mounted parameters
                    urlPattern = "{0}/tag/{1}/{2}";
                } else {
                    // parameterized parameters
                    urlPattern = "{0}/tag/?r={1}&h={2}";
                }
                List<RefModel> tags = JGitUtils.getTags(repository, false, length, offset);
                for (RefModel tag : tags) {
                    FeedEntryModel entry = new FeedEntryModel();
                    entry.title = tag.getName();
                    entry.author = tag.getAuthorIdent().getName();
                    entry.link = MessageFormat.format(urlPattern, gitblitUrl,
                            StringUtils.encodeURL(model.name.replace('/', fsc)), tag.getObjectId().getName());
                    entry.published = tag.getDate();
                    entry.contentType = "text/html";
                    entry.content = tag.getFullMessage();
                    entry.repository = model.name;
                    entry.branch = objectId;
                    entry.tags = new ArrayList<String>();
                    // add tag id and referenced commit id
                    entry.tags.add("tag:" + tag.getObjectId().getName());
                    entry.tags.add("commit:" + tag.getReferencedObjectId().getName());
                    entries.add(entry);
                }
            } else {
                // repository search
                commits = JGitUtils.searchRevlogs(repository, objectId, searchString, searchType,
                        offset, length);
            }
            Map<ObjectId, List<RefModel>> allRefs = JGitUtils.getAllRefs(repository, model.showRemoteBranches);
            BugtraqProcessor processor = new BugtraqProcessor(settings);
            // 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 = processor.processCommitMessage(repository, model, 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());
                String urlPattern;
                if (mountParameters) {
                    // mounted parameters
                    urlPattern = "{0}/commit/{1}/{2}";
                } else {
                    // parameterized parameters
                    urlPattern = "{0}/commit/?r={1}&h={2}";
                }
                // 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());
                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);
                BugtraqProcessor processor = new BugtraqProcessor(settings);
                // 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 = processor.processCommitMessage(repository, model, 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);
                }
                entries.add(entry);
            }
        }
src/main/java/com/gitblit/tickets/BranchTicketService.java
@@ -378,6 +378,37 @@
    }
    /**
     * Returns the assigned ticket ids.
     *
     * @return the assigned ticket ids
     */
    @Override
    public synchronized Set<Long> getIds(RepositoryModel repository) {
        Repository db = repositoryManager.getRepository(repository.name);
        try {
            if (getTicketsBranch(db) == null) {
                return Collections.emptySet();
            }
            Set<Long> ids = new TreeSet<Long>();
            List<PathModel> paths = JGitUtils.getDocuments(db, Arrays.asList("json"), BRANCH);
            for (PathModel path : paths) {
                String name = path.name.substring(path.name.lastIndexOf('/') + 1);
                if (!JOURNAL.equals(name)) {
                    continue;
                }
                String tid = path.path.split("/")[2];
                long ticketId = Long.parseLong(tid);
                ids.add(ticketId);
            }
            return ids;
        } finally {
            if (db != null) {
                db.close();
            }
        }
    }
    /**
     * Assigns a new ticket id.
     *
     * @param repository
@@ -398,16 +429,10 @@
            }
            AtomicLong lastId = lastAssignedId.get(repository.name);
            if (lastId.get() <= 0) {
                List<PathModel> paths = JGitUtils.getDocuments(db, Arrays.asList("json"), BRANCH);
                for (PathModel path : paths) {
                    String name = path.name.substring(path.name.lastIndexOf('/') + 1);
                    if (!JOURNAL.equals(name)) {
                        continue;
                    }
                    String tid = path.path.split("/")[2];
                    long ticketId = Long.parseLong(tid);
                    if (ticketId > lastId.get()) {
                        lastId.set(ticketId);
                Set<Long> ids = getIds(repository);
                for (long id : ids) {
                    if (id > lastId.get()) {
                        lastId.set(id);
                    }
                }
            }
@@ -526,6 +551,28 @@
    }
    /**
     * Retrieves the journal for the ticket.
     *
     * @param repository
     * @param ticketId
     * @return a journal, if it exists, otherwise null
     */
    @Override
    protected List<Change> getJournalImpl(RepositoryModel repository, long ticketId) {
        Repository db = repositoryManager.getRepository(repository.name);
        try {
            List<Change> changes = getJournal(db, ticketId);
            if (ArrayUtils.isEmpty(changes)) {
                log.warn("Empty journal for {}:{}", repository, ticketId);
                return null;
            }
            return changes;
        } finally {
            db.close();
        }
    }
    /**
     * Returns the journal for the specified ticket.
     *
     * @param db
src/main/java/com/gitblit/tickets/FileTicketService.java
@@ -22,6 +22,8 @@
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
@@ -146,6 +148,31 @@
        return hasTicket;
    }
    @Override
    public synchronized Set<Long> getIds(RepositoryModel repository) {
        Set<Long> ids = new TreeSet<Long>();
        Repository db = repositoryManager.getRepository(repository.name);
        try {
            // identify current highest ticket id by scanning the paths in the tip tree
            File dir = new File(db.getDirectory(), TICKETS_PATH);
            dir.mkdirs();
            List<File> journals = findAll(dir, JOURNAL);
            for (File journal : journals) {
                // Reconstruct ticketId from the path
                // id/26/326/journal.json
                String path = FileUtils.getRelativePath(dir, journal);
                String tid = path.split("/")[1];
                long ticketId = Long.parseLong(tid);
                ids.add(ticketId);
            }
        } finally {
            if (db != null) {
                db.close();
            }
        }
        return ids;
    }
    /**
     * Assigns a new ticket id.
     *
@@ -162,18 +189,10 @@
            }
            AtomicLong lastId = lastAssignedId.get(repository.name);
            if (lastId.get() <= 0) {
                // identify current highest ticket id by scanning the paths in the tip tree
                File dir = new File(db.getDirectory(), TICKETS_PATH);
                dir.mkdirs();
                List<File> journals = findAll(dir, JOURNAL);
                for (File journal : journals) {
                    // Reconstruct ticketId from the path
                    // id/26/326/journal.json
                    String path = FileUtils.getRelativePath(dir, journal);
                    String tid = path.split("/")[1];
                    long ticketId = Long.parseLong(tid);
                    if (ticketId > lastId.get()) {
                        lastId.set(ticketId);
                Set<Long> ids = getIds(repository);
                for (long id : ids) {
                    if (id > lastId.get()) {
                        lastId.set(id);
                    }
                }
            }
@@ -284,8 +303,7 @@
    }
    /**
     * Retrieves the ticket from the repository by first looking-up the changeId
     * associated with the ticketId.
     * Retrieves the ticket from the repository.
     *
     * @param repository
     * @param ticketId
@@ -313,6 +331,28 @@
    }
    /**
     * Retrieves the journal for the ticket.
     *
     * @param repository
     * @param ticketId
     * @return a journal, if it exists, otherwise null
     */
    @Override
    protected List<Change> getJournalImpl(RepositoryModel repository, long ticketId) {
        Repository db = repositoryManager.getRepository(repository.name);
        try {
            List<Change> changes = getJournal(db, ticketId);
            if (ArrayUtils.isEmpty(changes)) {
                log.warn("Empty journal for {}:{}", repository, ticketId);
                return null;
            }
            return changes;
        } finally {
            db.close();
        }
    }
    /**
     * Returns the journal for the specified ticket.
     *
     * @param db
src/main/java/com/gitblit/tickets/ITicketService.java
@@ -49,6 +49,7 @@
import com.gitblit.models.TicketModel.Patchset;
import com.gitblit.models.TicketModel.Status;
import com.gitblit.tickets.TicketIndexer.Lucene;
import com.gitblit.utils.DeepCopier;
import com.gitblit.utils.DiffUtils;
import com.gitblit.utils.DiffUtils.DiffStat;
import com.gitblit.utils.StringUtils;
@@ -63,6 +64,8 @@
 *
 */
public abstract class ITicketService {
    public static final String SETTING_UPDATE_DIFFSTATS = "migration.updateDiffstats";
    private static final String LABEL = "label";
@@ -105,6 +108,8 @@
    private final Map<String, List<TicketLabel>> labelsCache;
    private final Map<String, List<TicketMilestone>> milestonesCache;
    private final boolean updateDiffstats;
    private static class TicketKey {
        final String repository;
@@ -163,6 +168,8 @@
        this.labelsCache = new ConcurrentHashMap<String, List<TicketLabel>>();
        this.milestonesCache = new ConcurrentHashMap<String, List<TicketMilestone>>();
        this.updateDiffstats = settings.getBoolean(SETTING_UPDATE_DIFFSTATS, true);
    }
    /**
@@ -244,6 +251,7 @@
     */
    public boolean isAcceptingTicketUpdates(RepositoryModel repository) {
        return isReady()
                && repository.hasCommits
                && repository.isBare
                && !repository.isFrozen
                && !repository.isMirror;
@@ -556,9 +564,10 @@
    public TicketMilestone getMilestone(RepositoryModel repository, String milestone) {
        for (TicketMilestone ms : getMilestones(repository)) {
            if (ms.name.equalsIgnoreCase(milestone)) {
                TicketMilestone tm = DeepCopier.copy(ms);
                String q = QueryBuilder.q(Lucene.rid.matches(repository.getRID())).and(Lucene.milestone.matches(milestone)).build();
                ms.tickets = indexer.queryFor(q, 1, 0, Lucene.number.name(), true);
                return ms;
                tm.tickets = indexer.queryFor(q, 1, 0, Lucene.number.name(), true);
                return tm;
            }
        }
        return null;
@@ -639,6 +648,22 @@
     * @since 1.4.0
     */
    public synchronized boolean renameMilestone(RepositoryModel repository, String oldName, String newName, String createdBy) {
        return renameMilestone(repository, oldName, newName, createdBy, true);
    }
    /**
     * Renames a milestone.
     *
     * @param repository
     * @param oldName
     * @param newName
     * @param createdBy
     * @param notifyOpenTickets
     * @return true if successful
     * @since 1.6.0
     */
    public synchronized boolean renameMilestone(RepositoryModel repository, String oldName,
            String newName, String createdBy, boolean notifyOpenTickets) {
        if (StringUtils.isEmpty(newName)) {
            throw new IllegalArgumentException("new milestone can not be empty!");
        }
@@ -651,7 +676,7 @@
            config.setString(MILESTONE, newName, STATUS, milestone.status.name());
            config.setString(MILESTONE, newName, COLOR, milestone.color);
            if (milestone.due != null) {
                config.setString(MILESTONE, milestone.name, DUE,
                config.setString(MILESTONE, newName, DUE,
                        new SimpleDateFormat(DUE_DATE_PATTERN).format(milestone.due));
            }
            config.save();
@@ -663,9 +688,13 @@
                Change change = new Change(createdBy);
                change.setField(Field.milestone, newName);
                TicketModel ticket = updateTicket(repository, qr.number, change);
                notifier.queueMailing(ticket);
                if (notifyOpenTickets && ticket.isOpen()) {
                    notifier.queueMailing(ticket);
                }
            }
            notifier.sendAll();
            if (notifyOpenTickets) {
                notifier.sendAll();
            }
            return true;
        } catch (IOException e) {
@@ -688,11 +717,27 @@
     * @since 1.4.0
     */
    public synchronized boolean deleteMilestone(RepositoryModel repository, String milestone, String createdBy) {
        return deleteMilestone(repository, milestone, createdBy, true);
    }
    /**
     * Deletes a milestone.
     *
     * @param repository
     * @param milestone
     * @param createdBy
     * @param notifyOpenTickets
     * @return true if successful
     * @since 1.6.0
     */
    public synchronized boolean deleteMilestone(RepositoryModel repository, String milestone,
            String createdBy, boolean notifyOpenTickets) {
        if (StringUtils.isEmpty(milestone)) {
            throw new IllegalArgumentException("milestone can not be empty!");
        }
        Repository db = null;
        try {
            TicketMilestone tm = getMilestone(repository, milestone);
            db = repositoryManager.getRepository(repository.name);
            StoredConfig config = db.getConfig();
            config.unsetSection(MILESTONE, milestone);
@@ -700,6 +745,18 @@
            milestonesCache.remove(repository.name);
            TicketNotifier notifier = createNotifier();
            for (QueryResult qr : tm.tickets) {
                Change change = new Change(createdBy);
                change.setField(Field.milestone, "");
                TicketModel ticket = updateTicket(repository, qr.number, change);
                if (notifyOpenTickets && ticket.isOpen()) {
                    notifier.queueMailing(ticket);
                }
            }
            if (notifyOpenTickets) {
                notifier.sendAll();
            }
            return true;
        } catch (IOException e) {
            log.error("failed to delete milestone " + milestone + " in " + repository, e);
@@ -710,6 +767,15 @@
        }
        return false;
    }
    /**
     * Returns the set of assigned ticket ids in the repository.
     *
     * @param repository
     * @return a set of assigned ticket ids in the repository
     * @since 1.6.0
     */
    public abstract Set<Long> getIds(RepositoryModel repository);
    /**
     * Assigns a new ticket id.
@@ -773,7 +839,7 @@
            ticket = getTicketImpl(repository, ticketId);
            // if ticket exists
            if (ticket != null) {
                if (ticket.hasPatchsets()) {
                if (ticket.hasPatchsets() && updateDiffstats) {
                    Repository r = repositoryManager.getRepository(repository.name);
                    try {
                        Patchset patchset = ticket.getCurrentPatchset();
@@ -806,6 +872,33 @@
     */
    protected abstract TicketModel getTicketImpl(RepositoryModel repository, long ticketId);
    /**
     * Returns the journal used to build a ticket.
     *
     * @param repository
     * @param ticketId
     * @return the journal for the ticket, if it exists, otherwise null
     * @since 1.6.0
     */
    public final List<Change> getJournal(RepositoryModel repository, long ticketId) {
        if (hasTicket(repository, ticketId)) {
            List<Change> journal = getJournalImpl(repository, ticketId);
            return journal;
        }
        return null;
    }
    /**
     * Retrieves the ticket journal.
     *
     * @param repository
     * @param ticketId
     * @return a ticket, if it exists, otherwise null
     * @since 1.6.0
     */
    protected abstract List<Change> getJournalImpl(RepositoryModel repository, long ticketId);
    /**
     * Get the ticket url
     *
src/main/java/com/gitblit/tickets/NullTicketService.java
@@ -17,6 +17,7 @@
import java.util.Collections;
import java.util.List;
import java.util.Set;
import com.gitblit.manager.INotificationManager;
import com.gitblit.manager.IPluginManager;
@@ -78,6 +79,11 @@
    }
    @Override
    public synchronized Set<Long> getIds(RepositoryModel repository) {
        return Collections.emptySet();
    }
    @Override
    public synchronized long assignNewId(RepositoryModel repository) {
        return 0L;
    }
@@ -93,6 +99,11 @@
    }
    @Override
    protected List<Change> getJournalImpl(RepositoryModel repository, long ticketId) {
        return null;
    }
    @Override
    public boolean supportsAttachments() {
        return false;
    }
src/main/java/com/gitblit/tickets/QueryResult.java
@@ -74,6 +74,14 @@
        return type != null && Type.Proposal == type;
    }
    public boolean isOpen() {
        return !status.isClosed();
    }
    public boolean isClosed() {
        return status.isClosed();
    }
    public boolean isMerged() {
        return Status.Merged == status && !StringUtils.isEmpty(mergeSha);
    }
src/main/java/com/gitblit/tickets/RedisTicketService.java
@@ -20,6 +20,7 @@
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
@@ -184,6 +185,30 @@
        return false;
    }
    @Override
    public Set<Long> getIds(RepositoryModel repository) {
        Set<Long> ids = new TreeSet<Long>();
        Jedis jedis = pool.getResource();
        try {// account for migrated tickets
            Set<String> keys = jedis.keys(key(repository, KeyType.journal, "*"));
            for (String tkey : keys) {
                // {repo}:journal:{id}
                String id = tkey.split(":")[2];
                long ticketId = Long.parseLong(id);
                ids.add(ticketId);
            }
        } catch (JedisException e) {
            log.error("failed to assign new ticket id in Redis @ " + getUrl(), e);
            pool.returnBrokenResource(jedis);
            jedis = null;
        } finally {
            if (jedis != null) {
                pool.returnResource(jedis);
            }
        }
        return ids;
    }
    /**
     * Assigns a new ticket id.
     *
@@ -197,7 +222,14 @@
            String key = key(repository, KeyType.counter, null);
            String val = jedis.get(key);
            if (isNull(val)) {
                jedis.set(key, "0");
                long lastId = 0;
                Set<Long> ids = getIds(repository);
                for (long id : ids) {
                    if (id > lastId) {
                        lastId = id;
                    }
                }
                jedis.set(key, "" + lastId);
            }
            long ticketNumber = jedis.incr(key);
            return ticketNumber;
@@ -273,8 +305,7 @@
    }
    /**
     * Retrieves the ticket from the repository by first looking-up the changeId
     * associated with the ticketId.
     * Retrieves the ticket from the repository.
     *
     * @param repository
     * @param ticketId
@@ -312,6 +343,39 @@
    }
    /**
     * Retrieves the journal for the ticket.
     *
     * @param repository
     * @param ticketId
     * @return a journal, if it exists, otherwise null
     */
    @Override
    protected List<Change> getJournalImpl(RepositoryModel repository, long ticketId) {
        Jedis jedis = pool.getResource();
        if (jedis == null) {
            return null;
        }
        try {
            List<Change> changes = getJournal(jedis, repository, ticketId);
            if (ArrayUtils.isEmpty(changes)) {
                log.warn("Empty journal for {}:{}", repository, ticketId);
                return null;
            }
            return changes;
        } catch (JedisException e) {
            log.error("failed to retrieve journal from Redis @ " + getUrl(), e);
            pool.returnBrokenResource(jedis);
            jedis = null;
        } finally {
            if (jedis != null) {
                pool.returnResource(jedis);
            }
        }
        return null;
    }
    /**
     * Returns the journal for the specified ticket.
     *
     * @param repository
src/main/java/com/gitblit/tickets/TicketLabel.java
@@ -30,14 +30,17 @@
    private static final long serialVersionUID = 1L;
    public final String name;
    public String name;
    public String color;
    public List<QueryResult> tickets;
    public TicketLabel(String name) {
        setName(name);
    }
    public void setName(String name) {
        this.name = name;
        this.color = StringUtils.getColor(name);
    }
src/main/java/com/gitblit/tickets/TicketMilestone.java
@@ -38,6 +38,18 @@
        status = Status.Open;
    }
    public boolean isOpen() {
        return status == Status.Open;
    }
    public boolean isOverdue() {
        return due == null ? false : System.currentTimeMillis() > due.getTime();
    }
    public void setDue(Date due) {
        this.due = due;
    }
    public int getProgress() {
        int total = getTotalTickets();
        if (total == 0) {
src/main/java/com/gitblit/tickets/TicketNotifier.java
@@ -545,7 +545,6 @@
                }
            }
        }
        mailing.setRecipients(toAddresses);
        //
        // CC recipients
@@ -554,7 +553,7 @@
        // repository owners
        if (!ArrayUtils.isEmpty(repository.owners)) {
            tos.addAll(repository.owners);
            ccs.addAll(repository.owners);
        }
        // cc users mentioned in last comment
@@ -595,6 +594,14 @@
        }
        ccAddresses.addAll(settings.getStrings(Keys.mail.mailingLists));
        // respect the author's email preference
        UserModel lastAuthor = userManager.getUserModel(lastChange.author);
        if (!lastAuthor.getPreferences().isEmailMeOnMyTicketChanges()) {
            toAddresses.remove(lastAuthor.emailAddress);
            ccAddresses.remove(lastAuthor.emailAddress);
        }
        mailing.setRecipients(toAddresses);
        mailing.setCCs(ccAddresses);
    }
src/main/java/com/gitblit/tickets/commands.md
@@ -6,6 +6,6 @@
To review a rewritten patchset
    git fetch origin && git checkout ${ticketBranch} && git reset --hard origin/${ticketBranch}
    git fetch origin && git checkout -B ${ticketBranch}
src/main/java/com/gitblit/transport/ssh/SshDaemon.java
@@ -142,7 +142,7 @@
    public String formatUrl(String gituser, String servername, String repository) {
        if (sshd.getPort() == DEFAULT_PORT) {
            // standard port
            return MessageFormat.format("{0}@{1}/{2}", gituser, servername,
            return MessageFormat.format("ssh://{0}@{1}/{2}", gituser, servername,
                    repository);
        } else {
            // non-standard port
src/main/java/com/gitblit/utils/JGitUtils.java
@@ -1668,6 +1668,24 @@
    }
    /**
     * 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
     * @param offset
     *            if maxCount provided sets the starting point of the records to return
     * @return list of tags
     */
    public static List<RefModel> getTags(Repository repository, boolean fullName, int maxCount, int offset) {
        return getRefs(repository, Constants.R_TAGS, fullName, maxCount, offset);
    }
    /**
     * Returns the list of local branches in the repository. If repository does
     * not exist or is empty, an empty list is returned.
     *
@@ -1748,6 +1766,27 @@
     */
    private static List<RefModel> getRefs(Repository repository, String refs, boolean fullName,
            int maxCount) {
        return getRefs(repository, refs, fullName, maxCount, 0);
    }
    /**
     * 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
     * @param offset
     *            if maxCount provided sets the starting point of the records to return
     * @return list of references
     */
    private static List<RefModel> getRefs(Repository repository, String refs, boolean fullName,
            int maxCount, int offset) {
        List<RefModel> list = new ArrayList<RefModel>();
        if (maxCount == 0) {
            return list;
@@ -1771,7 +1810,14 @@
            Collections.sort(list);
            Collections.reverse(list);
            if (maxCount > 0 && list.size() > maxCount) {
                list = new ArrayList<RefModel>(list.subList(0, maxCount));
                if (offset < 0) {
                    offset = 0;
                }
                int endIndex = offset + maxCount;
                if (endIndex > list.size()) {
                    endIndex = list.size();
                }
                list = new ArrayList<RefModel>(list.subList(offset, endIndex));
            }
        } catch (IOException e) {
            error(e, repository, "{0} failed to retrieve {1}", refs);
src/main/java/com/gitblit/utils/MarkdownUtils.java
@@ -136,7 +136,7 @@
        String canonicalUrl = settings.getString(Keys.web.canonicalUrl, "https://localhost:8443");
        // emphasize and link mentions
        String mentionReplacement = String.format(" **<a href=\"%1s/user/$1\">@$1</a>**", canonicalUrl);
        String mentionReplacement = String.format(" **[@$1](%1s/user/$1)**", canonicalUrl);
        text = text.replaceAll("\\s@([A-Za-z0-9-_]+)", mentionReplacement);
        // link ticket refs
@@ -146,7 +146,7 @@
        // link commit shas
        int shaLen = settings.getInteger(Keys.web.shortCommitIdLength, 6);
        String commitPattern = MessageFormat.format("\\s([A-Fa-f0-9]'{'{0}'}')([A-Fa-f0-9]'{'{1}'}')", shaLen, 40 - shaLen);
        String commitReplacement = String.format(" <a class='commit' href='%1$s/commit?r=%2$s&h=$1$2'>$1</a>", canonicalUrl, repositoryName);
        String commitReplacement = String.format(" [`$1`](%1$s/commit?r=%2$s&h=$1$2)", canonicalUrl, repositoryName);
        text = text.replaceAll(commitPattern, commitReplacement);
        String html = transformMarkdown(text);
src/main/java/com/gitblit/utils/RpcUtils.java
@@ -203,7 +203,21 @@
    }
    /**
    /**
     * Create a fork of a repository.
     *
     * @param repository
     * @return true if the action succeeded
     * @throws IOException
     */
    public static boolean forkRepository(RepositoryModel repository, String serverUrl,
                                        String account, char[] password) throws IOException {
        return doAction(RpcRequest.FORK_REPOSITORY, repository.name, null, serverUrl, account, password);
    }
    /**
     * Send a revised version of the repository model to the Gitblit server.
     *
     * @param repository
src/main/java/com/gitblit/utils/SyndicationUtils.java
@@ -25,6 +25,7 @@
import java.util.List;
import com.gitblit.Constants;
import com.gitblit.Constants.FeedObjectType;
import com.gitblit.GitBlitException;
import com.gitblit.models.FeedEntryModel;
import com.sun.syndication.feed.synd.SyndCategory;
@@ -137,6 +138,59 @@
     */
    public static List<FeedEntryModel> readFeed(String url, String repository, String branch,
            int numberOfEntries, int page, String username, char[] password) throws IOException {
        return readFeed(url, repository, branch, FeedObjectType.COMMIT, numberOfEntries,
                page, username, password);
    }
    /**
     * Reads tags from the specified repository.
     *
     * @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> readTags(String url, String repository,
            int numberOfEntries, int page, String username, char[] password) throws IOException {
        return readFeed(url, repository, null, FeedObjectType.TAG, numberOfEntries,
                page, username, password);
    }
    /**
     * 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 objectType
     *            the object type to return (optional, COMMIT assummed)
     * @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}
     */
    private static List<FeedEntryModel> readFeed(String url, String repository, String branch,
            FeedObjectType objectType, int numberOfEntries, int page, String username,
            char[] password) throws IOException {
        // build feed url
        List<String> parameters = new ArrayList<String>();
        if (numberOfEntries > 0) {
@@ -148,6 +202,9 @@
        if (!StringUtils.isEmpty(branch)) {
            parameters.add("h=" + branch);
        }
        if (objectType != null) {
            parameters.add("ot=" + objectType.name());
        }
        return readFeed(url, parameters, repository, branch, username, password);
    }
src/main/java/com/gitblit/wicket/GitBlitWebApp.java
@@ -25,11 +25,16 @@
import org.apache.wicket.Request;
import org.apache.wicket.Response;
import org.apache.wicket.Session;
import org.apache.wicket.application.IClassResolver;
import org.apache.wicket.markup.html.WebPage;
import org.apache.wicket.protocol.http.WebApplication;
import ro.fortsoft.pf4j.PluginState;
import ro.fortsoft.pf4j.PluginWrapper;
import com.gitblit.IStoredSettings;
import com.gitblit.Keys;
import com.gitblit.extensions.GitblitWicketPlugin;
import com.gitblit.manager.IAuthenticationManager;
import com.gitblit.manager.IFederationManager;
import com.gitblit.manager.IGitblit;
@@ -51,6 +56,8 @@
import com.gitblit.wicket.pages.ComparePage;
import com.gitblit.wicket.pages.DocPage;
import com.gitblit.wicket.pages.DocsPage;
import com.gitblit.wicket.pages.EditMilestonePage;
import com.gitblit.wicket.pages.EditRepositoryPage;
import com.gitblit.wicket.pages.EditTicketPage;
import com.gitblit.wicket.pages.ExportTicketPage;
import com.gitblit.wicket.pages.FederationRegistrationPage;
@@ -63,26 +70,31 @@
import com.gitblit.wicket.pages.LuceneSearchPage;
import com.gitblit.wicket.pages.MetricsPage;
import com.gitblit.wicket.pages.MyDashboardPage;
import com.gitblit.wicket.pages.MyTicketsPage;
import com.gitblit.wicket.pages.NewMilestonePage;
import com.gitblit.wicket.pages.NewRepositoryPage;
import com.gitblit.wicket.pages.NewTicketPage;
import com.gitblit.wicket.pages.OverviewPage;
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.ReflogPage;
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.TeamsPage;
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 {
public class GitBlitWebApp extends WebApplication implements GitblitWicketApp {
    private final Class<? extends WebPage> homePageClass = MyDashboardPage.class;
    private final Class<? extends WebPage> newRepositoryPageClass = NewRepositoryPage.class;
    private final Map<String, CacheControl> cacheablePages = new HashMap<String, CacheControl>();
@@ -170,7 +182,6 @@
        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("/compare", ComparePage.class, "r", "h");
@@ -180,6 +191,7 @@
        mount("/metrics", MetricsPage.class, "r");
        mount("/blame", BlamePage.class, "r", "h", "f");
        mount("/users", UsersPage.class);
        mount("/teams", TeamsPage.class);
        mount("/logout", LogoutPage.class);
        // setup ticket urls
@@ -187,6 +199,9 @@
        mount("/tickets/new", NewTicketPage.class, "r");
        mount("/tickets/edit", EditTicketPage.class, "r", "h");
        mount("/tickets/export", ExportTicketPage.class, "r", "h");
        mount("/milestones/new", NewMilestonePage.class, "r");
        mount("/milestones/edit", EditMilestonePage.class, "r", "h");
        mount("/mytickets", MyTicketsPage.class, "r", "h");
        // setup the markup document urls
        mount("/docs", DocsPage.class, "r");
@@ -196,6 +211,8 @@
        mount("/proposal", ReviewProposalPage.class, "t");
        mount("/registration", FederationRegistrationPage.class, "u", "n");
        mount("/new", NewRepositoryPage.class);
        mount("/edit", EditRepositoryPage.class, "r");
        mount("/activity", ActivityPage.class, "r", "h");
        mount("/lucene", LuceneSearchPage.class);
        mount("/project", ProjectPage.class, "p");
@@ -204,11 +221,30 @@
        mount("/forks", ForksPage.class, "r");
        mount("/fork", ForkPage.class, "r");
        // allow started Wicket plugins to initialize
        for (PluginWrapper pluginWrapper : pluginManager.getPlugins()) {
            if (PluginState.STARTED != pluginWrapper.getPluginState()) {
                continue;
            }
            if (pluginWrapper.getPlugin() instanceof GitblitWicketPlugin) {
                GitblitWicketPlugin wicketPlugin = (GitblitWicketPlugin) pluginWrapper.getPlugin();
                wicketPlugin.init(this);
            }
        }
         // customize the Wicket class resolver to load from plugins
        IClassResolver coreResolver = getApplicationSettings().getClassResolver();
        PluginClassResolver classResolver = new PluginClassResolver(coreResolver, pluginManager);
        getApplicationSettings().setClassResolver(classResolver);
        getMarkupSettings().setDefaultMarkupEncoding("UTF-8");
        super.init();
    }
    private void mount(String location, Class<? extends WebPage> clazz, String... parameters) {
    /* (non-Javadoc)
     * @see com.gitblit.wicket.Webapp#mount(java.lang.String, java.lang.Class, java.lang.String)
     */
    @Override
    public void mount(String location, Class<? extends WebPage> clazz, String... parameters) {
        if (parameters == null) {
            parameters = new String[] {};
        }
@@ -224,15 +260,30 @@
        }
    }
    /* (non-Javadoc)
     * @see com.gitblit.wicket.Webapp#getHomePage()
     */
    @Override
    public Class<? extends WebPage> getHomePage() {
        return homePageClass;
    }
    public Class<? extends WebPage> getNewRepositoryPage() {
        return newRepositoryPageClass;
    }
    /* (non-Javadoc)
     * @see com.gitblit.wicket.Webapp#isCacheablePage(java.lang.String)
     */
    @Override
    public boolean isCacheablePage(String mountPoint) {
        return cacheablePages.containsKey(mountPoint);
    }
    /* (non-Javadoc)
     * @see com.gitblit.wicket.Webapp#getCacheControl(java.lang.String)
     */
    @Override
    public CacheControl getCacheControl(String mountPoint) {
        return cacheablePages.get(mountPoint);
    }
@@ -248,15 +299,18 @@
        return gitBlitWebSession;
    }
    /* (non-Javadoc)
     * @see com.gitblit.wicket.Webapp#settings()
     */
    @Override
    public IStoredSettings settings() {
        return settings;
    }
    /**
     * Is Gitblit running in debug mode?
     *
     * @return true if Gitblit is running in debug mode
    /* (non-Javadoc)
     * @see com.gitblit.wicket.Webapp#isDebugMode()
     */
    @Override
    public boolean isDebugMode() {
        return runtimeManager.isDebugMode();
    }
@@ -265,58 +319,114 @@
     * These methods look strange... and they are... but they are the first
     * step towards modularization across multiple commits.
     */
    /* (non-Javadoc)
     * @see com.gitblit.wicket.Webapp#getBootDate()
     */
    @Override
    public Date getBootDate() {
        return runtimeManager.getBootDate();
    }
    /* (non-Javadoc)
     * @see com.gitblit.wicket.Webapp#getLastActivityDate()
     */
    @Override
    public Date getLastActivityDate() {
        return repositoryManager.getLastActivityDate();
    }
    /* (non-Javadoc)
     * @see com.gitblit.wicket.Webapp#runtime()
     */
    @Override
    public IRuntimeManager runtime() {
        return runtimeManager;
    }
    /* (non-Javadoc)
     * @see com.gitblit.wicket.Webapp#plugins()
     */
    @Override
    public IPluginManager plugins() {
        return pluginManager;
    }
    /* (non-Javadoc)
     * @see com.gitblit.wicket.Webapp#notifier()
     */
    @Override
    public INotificationManager notifier() {
        return notificationManager;
    }
    /* (non-Javadoc)
     * @see com.gitblit.wicket.Webapp#users()
     */
    @Override
    public IUserManager users() {
        return userManager;
    }
    /* (non-Javadoc)
     * @see com.gitblit.wicket.Webapp#authentication()
     */
    @Override
    public IAuthenticationManager authentication() {
        return authenticationManager;
    }
    /* (non-Javadoc)
     * @see com.gitblit.wicket.Webapp#keys()
     */
    @Override
    public IPublicKeyManager keys() {
        return publicKeyManager;
    }
    /* (non-Javadoc)
     * @see com.gitblit.wicket.Webapp#repositories()
     */
    @Override
    public IRepositoryManager repositories() {
        return repositoryManager;
    }
    /* (non-Javadoc)
     * @see com.gitblit.wicket.Webapp#projects()
     */
    @Override
    public IProjectManager projects() {
        return projectManager;
    }
    /* (non-Javadoc)
     * @see com.gitblit.wicket.Webapp#federation()
     */
    @Override
    public IFederationManager federation() {
        return federationManager;
    }
    /* (non-Javadoc)
     * @see com.gitblit.wicket.Webapp#gitblit()
     */
    @Override
    public IGitblit gitblit() {
        return gitblit;
    }
    /* (non-Javadoc)
     * @see com.gitblit.wicket.Webapp#tickets()
     */
    @Override
    public ITicketService tickets() {
        return gitblit.getTicketService();
    }
    /* (non-Javadoc)
     * @see com.gitblit.wicket.Webapp#getTimezone()
     */
    @Override
    public TimeZone getTimezone() {
        return runtimeManager.getTimezone();
    }
src/main/java/com/gitblit/wicket/GitBlitWebApp.properties
@@ -133,7 +133,7 @@
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.headRefDescription = The default branch that will be cloned and displayed on the Summary page.
gb.federationStrategy = federation strategy
gb.federationRegistration = federation registration
gb.federationResults = federation pull results
@@ -223,8 +223,8 @@
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.indexedBranches = Indexed Branches
gb.indexedBranchesDescription = Select the branches to be indexed by Lucene
gb.noIndexedRepositoriesWarning = none of your repositories are configured for Lucene indexing
gb.undefinedQueryWarning = query is undefined!
gb.noSelectedRepositoriesWarning = please select one or more repositories!
@@ -671,4 +671,74 @@
gb.ticketIsClosed = This ticket is closed.
gb.mergeToDescription = default integration branch for merging ticket patchsets
gb.anonymousCanNotPropose = Anonymous users can not propose patchsets.
gb.youDoNotHaveClonePermission = You are not permitted to clone this repository.
gb.youDoNotHaveClonePermission = You are not permitted to clone this repository.
gb.myTickets = my tickets
gb.yourAssignedTickets = assigned to you
gb.newMilestone = new milestone
gb.editMilestone = edit milestone
gb.deleteMilestone = Delete milestone \"{0}\"?
gb.milestoneDeleteFailed = Failed to delete milestone ''{0}''!
gb.notifyChangedOpenTickets = send notification for changed open tickets
gb.overdue = overdue
gb.openMilestones = open milestones
gb.closedMilestones = closed milestones
gb.administration = administration
gb.plugins = plugins
gb.extensions = extensions
gb.pleaseSelectProject = Please select the project!
gb.accessPolicy = Access Policy
gb.accessPolicyDescription = Choose an access policy to control repository visibility and git permissions.
gb.anonymousPolicy = Anonymous View, Clone, & Push
gb.anonymousPolicyDescription = Anyone can see, clone, and push to this repository.
gb.authenticatedPushPolicy = Restrict Push (Authenticated)
gb.authenticatedPushPolicyDescription = Anyone can see and clone this repository. All authenticated users have RW+ push permission.
gb.namedPushPolicy = Restrict Push (Named)
gb.namedPushPolicyDescription = Anyone can see and clone this repository. You choose who can push.
gb.clonePolicy = Restrict Clone & Push
gb.clonePolicyDescription = Anyone can see this repository. You choose who can clone and push.
gb.viewPolicy  = Restrict View, Clone, & Push
gb.viewPolicyDescription = You choose who can see, clone, and push to this repository.
gb.initialCommit = Initial Commit
gb.initialCommitDescription = This will allow you to <code>git clone</code> this repository immediately. Skip this step if you have already run <code>git init</code> locally.
gb.initWithReadme = Include a README
gb.initWithReadmeDescription = This will generate a simple README document for your repository.
gb.initWithGitignore = Include a .gitignore file
gb.initWithGitignoreDescription = This will insert a config file that instructs your Git clients to ignore files or directories that match defined patterns.
gb.pleaseSelectGitIgnore = Please select a .gitignore file
gb.receive = receive
gb.permissions = permissions
gb.ownersDescription = Owners can manage all repository settings but they are not allowed to rename a repository unless it is their personal repository.
gb.userPermissionsDescription = You can specify individual user permissions. These settings will override team or regex permissions.
gb.teamPermissionsDescription = You can specify individual team permissions. These settings will override regex permissions.
gb.ticketSettings = Ticket Settings
gb.receiveSettings = Receive Settings
gb.receiveSettingsDescription = The receive settings control pushes to the repository.
gb.preReceiveDescription = Pre-receive hooks are executed after commits are received but <em>BEFORE</em> the refs are updated.<p>This is the appropriate hook for rejecting a push.</p>
gb.postReceiveDescription = Post-receive hooks are executed after commits are received but <em>AFTER</em> the refs are updated.<p>This is the appropriate hook for notifications, build triggers, etc.</p>
gb.federationStrategyDescription = Control if and how to federate this repository with another Gitblit.
gb.federationSetsDescription = This repository will be included in the selected federation sets.
gb.miscellaneous = miscellaneous
gb.originDescription = The url from which this repository was cloned.
gb.gc = GC
gb.garbageCollection = Garbage Collection
gb.garbageCollectionDescription = The garbage collector will pack loose objects pushed from clients and will remove unreferenced objects from the repository.
gb.commitMessageRendererDescription = Commit messages can be displayed as plaintext or as rendered markup.
gb.preferences = preferences
gb.accountPreferences = Account Preferences
gb.accountPreferencesDescription = Specify your account preferences
gb.languagePreference = Language Preference
gb.languagePreferenceDescription = Select your preferred translation for Gitblit
gb.emailMeOnMyTicketChanges = Email me on my ticket changes
gb.emailMeOnMyTicketChangesDescription  = Send me an email notification for changes that I make to a ticket
gb.displayNameDescription = The preferred name for display
gb.emailAddressDescription = The primary email address for receiving notifications
gb.sshKeys = SSH Keys
gb.sshKeysDescription = SSH public key authentication is a secure alternative to password authentication
gb.addSshKey = Add SSH Key
gb.key = Key
gb.comment = Comment
gb.sshKeyCommentDescription = Enter an optional comment. If blank, the comment will be extracted from the key data.
gb.permission = Permission
gb.sshKeyPermissionDescription = Specify the access permission for the SSH key
gb.transportPreference = Transport Preference
gb.transportPreferenceDescription = Set the transport that you prefer to use for cloning
src/main/java/com/gitblit/wicket/GitBlitWebApp_de.properties
New file
@@ -0,0 +1,745 @@
gb.repository = Repository
gb.owner = Besitzer
gb.description = Beschreibung
gb.lastChange = Letzte \u00c4nderung
gb.refs = Refs
gb.tag = Tag
gb.tags = Tags
gb.author = Autor
gb.committer = Committer
gb.commit = Commit
gb.age = Alter
gb.tree = Dateibaum
gb.parent = Parent
gb.url = URL
gb.history = Historie
gb.raw = Raw
gb.object = Object
gb.ticketId = Ticket Id
gb.ticketAssigned = Zugewiesen
gb.ticketOpenDate = Erstellt
gb.ticketStatus = Status
gb.ticketComments = Kommentare
gb.view = Ansicht
gb.local = Lokal
gb.remote = Remote
gb.branches = Branches
gb.patch = Patch
gb.diff = Diff
gb.log = Log
gb.moreLogs = Weitere Commits...
gb.allTags = Alle Tags...
gb.allBranches = Alle Branches...
gb.summary = \u00dcbersicht
gb.ticket = Ticket
gb.newRepository = Neues Repository
gb.newUser = Neuer Benutzer
gb.commitdiff = Commitdiff
gb.tickets = Tickets
gb.pageFirst = Anfang
gb.pagePrevious = Zur\u00fcck
gb.pageNext = Weiter
gb.head = HEAD
gb.blame = Blame
gb.login = Login
gb.logout = Logout
gb.username = Benutzername
gb.password = Passwort
gb.tagger = Tagger
gb.moreHistory = Mehr Historie...
gb.difftocurrent = Diff zu aktuellem Stand
gb.search = Suche
gb.searchForAuthor = Suche nach Commits von Autor
gb.searchForCommitter = Suche nach Commits von Committer
gb.addition = Neu
gb.modification = Modifikation
gb.deletion = L\u00f6schung
gb.rename = Umbenennung
gb.metrics = Metriken
gb.stats = Statistiken
gb.markdown = Markdown
gb.changedFiles = Ge\u00e4nderte Dateien
gb.filesAdded = {0} Dateien hinzugef\u00fcgt
gb.filesModified = {0} Dateien ge\u00e4ndert
gb.filesDeleted = {0} Dateien gel\u00f6scht
gb.filesCopied = {0} Dateien kopiert
gb.filesRenamed = {0} Dateien umbenannt
gb.missingUsername = Fehlender Benutzername
gb.edit = Bearbeiten
gb.searchTypeTooltip = Suchart ausw\u00e4hlen
gb.searchTooltip = Suche {0}
gb.delete = L\u00f6schen
gb.docs = Dokumentation
gb.accessRestriction = Zugriffsbeschr\u00e4nkung
gb.name = Name
gb.enableTickets = Tickets aktivieren
gb.enableDocs = Docs aktivieren
gb.save = Speichern
gb.showRemoteBranches = Zeige Remote Branches
gb.editUsers = Benutzer bearbeiten
gb.confirmPassword = Passwort best\u00e4tigen
gb.restrictedRepositories = Zugriffsbeschr\u00e4nkte Repositories
gb.canAdmin = Kann administrieren
gb.notRestricted = Anonymes Anzeigen, Klonen und Pushen
gb.pushRestricted = Authentifiziertes Pushen
gb.cloneRestricted = Authentifiziertes Klonen und Pushen
gb.viewRestricted = Authentifiziertes Anzeigen, Klonen und Pushen
gb.useTicketsDescription = Lesende Darstellung von Ticgit Issues
gb.useDocsDescription = Listet Markdown Dokumentation im Repository auf
gb.showRemoteBranchesDescription = Zeige Remote Branches
gb.canAdminDescription = Kann Gitblit Server administrieren
gb.permittedUsers = Zugelassene Benutzer
gb.isFrozen = ist eingefroren
gb.isFrozenDescription = Verbiete Push
gb.zip = Zip
gb.showReadme = Zeige Readme
gb.showReadmeDescription = Zeigt eine \"Readme\" Markdown Datei auf der Zusammenfassungsseite
gb.nameDescription = Verwenden Sie '/' um Repositories zu gruppieren. Z.B. libraries/mycoollib.git
gb.ownerDescription = Der Besitzer kann Repository-Einstellungen \u00e4ndern
gb.blob = Blob
gb.commitActivityTrend = Commit-Aktivit\u00e4tstrend
gb.commitActivityDOW = Commit-Aktivit\u00e4t nach Wochentag
gb.commitActivityAuthors = Prim\u00e4re Autoren nach Commit-Aktivit\u00e4t
gb.feed = Feed
gb.cancel = Abbrechen
gb.changePassword = Passwort \u00e4ndern
gb.isFederated = ist verbunden
gb.federateThis = Dieses Repository verbinden (Federation)
gb.federateOrigin = Ursprung (origin) verbinden (Federation)
gb.excludeFromFederation = Von Verbindung ausschlie\u00dfen (Federation)
gb.excludeFromFederationDescription = Verbiete verbundenen Gitblit Instanzen, von diesem Konto zu pullen (Federation)
gb.tokens = Federation Tokens
gb.tokenAllDescription = Alle Repositories, Benutzer und Einstellungen
gb.tokenUnrDescription = Alle Repositories und Benutzer
gb.tokenJurDescription = Alle Repositories
gb.federatedRepositoryDefinitions = Repositorydefinitionen
gb.federatedUserDefinitions = Benutzerdefinitionen
gb.federatedSettingDefinitions = Einstellungsdefinitionen
gb.proposals = Verbindungsanfrage (Federation)
gb.received = empfangen
gb.type = Typ
gb.token = Token
gb.repositories = Repositories
gb.proposal = Anfrage
gb.frequency = Frequenz
gb.folder = Ordner
gb.lastPull = Letzter Pull
gb.nextPull = N\u00e4chster Pull
gb.inclusions = Inclusions
gb.exclusions = Exclusions
gb.registration = Registrierungen
gb.registrations = Verbindungsregistrierungen
gb.sendProposal = Vorschlagen
gb.status = Status
gb.origin = Ursprung
gb.headRef = Default Branch (HEAD)
gb.headRefDescription = Der Default Branch, welcher gecloned und auf der Zusammenfassungsseite dargestellt wird
gb.federationStrategy = Verbindungsstrategie
gb.federationRegistration = Verbindungsregistrierung
gb.federationResults = Verbindungs-Pull-Ergebnis
gb.federationSets = federation sets
gb.message = Nachricht
gb.myUrlDescription = Die \u00f6ffentlich zugreifbare URL f\u00fcr Ihre Gitblit-Instanz
gb.destinationUrl = Senden an
gb.destinationUrlDescription = Die URL der Gitblit-Instanz, an welche die Anfrage gestellt werden soll
gb.users = Benutzer
gb.federation = Verbindungen (Federation)
gb.error = Fehler
gb.refresh = Aktualisieren
gb.browse = Durchsuchen
gb.clone = Clonen
gb.filter = Filter
gb.create = Erstellen
gb.servers = Server
gb.recent = K\u00fcrzlich
gb.available = Verf\u00fcgbar
gb.selected = Ausgew\u00e4hlt
gb.size = Gr\u00f6\u00dfe
gb.downloading = Wird heruntergeladen
gb.loading = Wird geladen
gb.starting = Wird gestartet
gb.general = Allgemein
gb.settings = Einstellungen
gb.manage = Verwalten
gb.lastLogin = Letzter Login
gb.skipSizeCalculation = \u00dcberspringe Gr\u00f6\u00dfenberechnung
gb.skipSizeCalculationDescription = Nicht die Gr\u00f6\u00dfe des Repositories berechnen (reduziert die Seitenladezeit)
gb.skipSummaryMetrics = \u00dcberspringe zusammenfassende Metriken
gb.skipSummaryMetricsDescription = Nicht die Metriken auf der Zusammenfassungsseite berechnen (reduziert die Seitenladezeit)
gb.accessLevel = Zugangsebene
gb.default = Default
gb.setDefault = Setze Default
gb.since = seit
gb.status = Status
gb.bootDate = Boot-Zeitpunkt
gb.servletContainer = Servlet container
gb.heapMaximum = Maximaler Heap
gb.heapAllocated = Allokierter Heap
gb.heapUsed = Verwendeter Heap
gb.free = Verf\u00fcgbar
gb.version = Version
gb.releaseDate = Releasedatum
gb.date = Datum
gb.activity = Aktivit\u00e4t
gb.subscribe = Subscribe
gb.branch = Branch
gb.maxHits = Max Hits
gb.recentActivity = K\u00fcrzliche Aktivit\u00e4t
gb.recentActivityStats = In den letzten {0} Tagen / {1} Commit(s) von {2} Autor(en)
gb.recentActivityNone = In den letzten {0} Tagen / Keine
gb.dailyActivity = T\u00e4gliche Aktivit\u00e4t
gb.activeRepositories = Aktive Repositories
gb.activeAuthors = Aktive Autoren
gb.commits = Commits
gb.teams = Teams
gb.teamName = Teamname
gb.teamMembers = Teammitglieder
gb.teamMemberships = Teammitgliedschaften
gb.newTeam = Neues Team
gb.permittedTeams = Zugelassene Teams
gb.emptyRepository = Leeres Repository
gb.repositoryUrl = Repository URL
gb.mailingLists = Mailinglisten
gb.preReceiveScripts = Pre-Receive Skripte
gb.postReceiveScripts = Post-Receive Skripte
gb.hookScripts = Hook Skripte
gb.customFields = Benutzerdefinierte Felder
gb.customFieldsDescription = Benutzerdefinierte Felder, die in Groovy Hooks zur Verf\u00fcgung stehen
gb.accessPermissions = Zugriffsberechtigungen
gb.filters = Filter
gb.generalDescription = Allgemeine Einstellungen
gb.accessPermissionsDescription = Zugriffseinschr\u00e4nkungen nach Benutzern und Teams
gb.accessPermissionsForUserDescription = Erstellen Sie Teammitgliedschaften oder gew\u00e4hren Sie Zugriff auf einzelnen Repositories
gb.accessPermissionsForTeamDescription = Weisen Sie Team Mitglieder zu oder gew\u00e4hren Sie Zugriff auf einzelne Repositories
gb.federationRepositoryDescription = Verbinden Sie dieses Repository mit anderen Gitblit Instanzen (Federation)
gb.hookScriptsDescription = Groovy Skripte beim Pushen auf diese Gitblit Instanz ausf\u00fchren
gb.reset = Reset
gb.pages = Seiten
gb.workingCopy = Arbeitskopie
gb.workingCopyWarning = Dieses Repository besitzt eine Arbeitskopie und kann keine Pushes empfangen
gb.query = Abfrage
gb.queryHelp = Standard Abfragesyntax wird unterst\u00fctzt.<p/><p/>Unter <a target="_new" href="http://lucene.apache.org/core/old_versioned_docs/versions/3_5_0/queryparsersyntax.html">Lucene Query Parser Syntax</a> finden Sie weitere Details.
gb.queryResults = Ergebnisse {0} - {1} ({2} Treffer)
gb.noHits = Keine Treffer
gb.authored = ist Autor von
gb.committed = ist Comitter von
gb.indexedBranches = Indizierte Branches
gb.indexedBranchesDescription = W\u00e4hlen Sie die zu indizierenden Branches aus.
gb.noIndexedRepositoriesWarning = Keines Ihrer Repositories ist f\u00fcr die Indizierung durch Lucene konfiguriert.
gb.undefinedQueryWarning = Abfrage fehlt!
gb.noSelectedRepositoriesWarning = Bitte w\u00e4hlen Sie ein oder mehrere Repositories aus!
gb.luceneDisabled = Indizierung mit Lucene ist deaktiviert
gb.failedtoRead = Fehler beim Lesen
gb.isNotValidFile = ist keine g\u00fcltige Datei
gb.failedToReadMessage = Konnte Standard Nachricht von {0} nicht lesen!
gb.passwordsDoNotMatch = Passw\u00f6rter stimmen nicht \u00fcberein!
gb.passwordTooShort = Passwort ist zu kurz. Die minimale L\u00e4nge betr\u00e4gt {0} Zeichen.
gb.passwordChanged = Passwort erfolgreich ge\u00e4ndert.
gb.passwordChangeAborted = Passwort\u00e4nderung abgebrochen.
gb.pleaseSetRepositoryName = Bitte Repositorynamen angeben!
gb.illegalLeadingSlash = Pfade d\u00fcrfen nicht mit '/' beginnen.
gb.illegalRelativeSlash = Relative Pfade (../) sind nicht erlaubt.
gb.illegalCharacterRepositoryName = Repositoryname enth\u00e4lt nicht erlaubtes Zeichen ''{0}''!
gb.selectAccessRestriction = Bitte w\u00e4hlen Sie die Zugriffsbeschr\u00e4nkung aus!
gb.selectFederationStrategy = Bitte w\u00e4hlen Sie die Verbindungsstrategie (Federation) aus!
gb.pleaseSetTeamName = Bitte geben Sie einen Teamnamen an!
gb.teamNameUnavailable = Der Teamname ''{0}'' ist nicht verf\u00fcgbar.
gb.teamMustSpecifyRepository = Ein Team muss mindestens einem Repository zugewiesen sein.
gb.teamCreated = Neues Team ''{0}'' erfolgreich angelegt.
gb.pleaseSetUsername = Bitte geben Sie einen Benutzernamen an!
gb.usernameUnavailable = Benutzername ''{0}'' ist nicht verf\u00fcgbar.
gb.combinedMd5Rename = Gitblit ist f\u00fcr kombiniertes MD5-Passwort-Hashing konfiguriert. Sie m\u00fcssen beim Umbenennen des Kontos ein neues Passwort angeben.
gb.userCreated = Neuer Benutzer ''{0}'' erfolgreich angelegt.
gb.couldNotFindFederationRegistration = Konnte Verbindungsregistrierung (Federation) nicht finden!
gb.failedToFindGravatarProfile = Das Gravatar Profil f\u00fcr {0} konnte nicht gefunden werden
gb.branchStats = {0} Commit(s) und {1} Tag(s) seit {2}
gb.repositoryNotSpecified = Repository nicht angegeben!
gb.repositoryNotSpecifiedFor = Repository nicht angegeben f\u00fcr {0}!
gb.canNotLoadRepository = Repository kann nicht geladen werden
gb.commitIsNull = Commit ist null
gb.unauthorizedAccessForRepository = Nicht autorisierter Zugriff auf Repository
gb.failedToFindCommit = Commit \"{0}\" konnte nicht in {1} gefunden werden!
gb.couldNotFindFederationProposal = Verbindungsanfrage (Federation) konnte nicht gefunden werden!
gb.invalidUsernameOrPassword = Ung\u00fcltiger Benutzername oder Passwort!
gb.OneProposalToReview = Es gibt 1 unbeantwortete Verbindungsanfrage.
gb.nFederationProposalsToReview = Es gibt {0} unbeantwortete Verbindungsanfragen.
gb.couldNotFindTag = Der Tag {0} konnte nicht gefunden werden
gb.couldNotCreateFederationProposal = Die Verbindungsanfrage (Federation) konnte nicht erstellt werden.
gb.pleaseSetGitblitUrl = Bitte geben Sie Ihre Gitblit URL ein!
gb.pleaseSetDestinationUrl = Bitte geben Sie eine Ziel-URL f\u00fcr Ihre Anfrage ein!
gb.proposalReceived = Anfrage von {0} erfolgreich empfangen.
gb.noGitblitFound = Tut mir leid, {0} fand keine Gitblit Instanz in
gb.noProposals = Tut mir leid, {0} akzeptiert derzeit keine Anfragen.
gb.noFederation = Tut mir leid, {0} kann sich nicht mit anderen Gitblit Instanzen verbinden.
gb.proposalFailed = Tut mir leid, {0} hat keine Anfragedaten emfpangen!
gb.proposalError = Tut mir leid, {0} berichtet, dass ein unerwarteter Fehler aufgetreten ist!
gb.failedToSendProposal = Senden der Anfrage ist fehlgeschlagen!
gb.userServiceDoesNotPermitAddUser = {0} erlaubt das Anlegen von Benutzerkonten nicht.
gb.userServiceDoesNotPermitPasswordChanges = {0} erlaubt das \u00c4ndern von Passworten nicht!
gb.displayName = Anzeigename
gb.emailAddress = Emailadresse
gb.errorAdminLoginRequired = Administrierung erfordert eine Anmeldung
gb.errorOnlyAdminMayCreateRepository = Nur ein Administrator kann ein Repository erstellen
gb.errorOnlyAdminOrOwnerMayEditRepository = Nur ein Administrator oder der Besitzer kann ein Repository bearbeiten
gb.errorAdministrationDisabled = Administrierung ist deaktiviert
gb.lastNDays = letzte {0} Tage
gb.completeGravatarProfile = Komplettes Profil auf Gravatar.com
gb.none = keine
gb.line = Zeile
gb.content = Inhalt
gb.empty = leer
gb.inherited = vererbt
gb.deleteRepository = Repository \"{0}\" l\u00f6schen?
gb.repositoryDeleted = Repository ''{0}'' gel\u00f6scht.
gb.repositoryDeleteFailed = L\u00f6schen von Repository ''{0}'' fehlgeschlagen!
gb.deleteUser = Benutzer \"{0}\" l\u00f6schen?
gb.userDeleted = Benutzer ''{0}'' gel\u00f6scht.
gb.userDeleteFailed = L\u00f6schen von Benutzer ''{0}'' fehlgeschlagen!
gb.time.justNow = Gerade eben
gb.time.today = Heute
gb.time.yesterday = Gestern
gb.time.minsAgo = vor {0} Min.
gb.time.hoursAgo = vor {0} Std.
gb.time.daysAgo = vor {0} Tagen
gb.time.weeksAgo = vor {0} Wochen
gb.time.monthsAgo = vor {0} Monaten
gb.time.oneYearAgo = vor 1 Jahr
gb.time.yearsAgo = vor {0} Jahren
gb.duration.oneDay = 1 Tag
gb.duration.days = {0} Tage
gb.duration.oneMonth = 1 Monat
gb.duration.months = {0} Monaten
gb.duration.oneYear = 1 Jahr
gb.duration.years = {0} Jahren
gb.authorizationControl = Zugriffsrechte
gb.allowAuthenticatedDescription = Allen authentifizierten Benutzern RW+ Recht gew\u00e4hren
gb.allownameddescription = Benutzern und Teams feingranulare Rechte gew\u00e4hren
gb.markdownFailure = Markdown Inhalt konnte nicht geparst werden!
gb.clearCache = Cache leeren
gb.projects = Projekte
gb.project = Projekt
gb.allProjects = Alle Projekte
gb.copyToClipboard = in die Zwischenablage kopieren
gb.fork = Fork
gb.forks = Forks
gb.forkRepository = {0} forken?
gb.repositoryForked = Fork von {0} wurde erstellt
gb.repositoryForkFailed = Fork ist fehlgeschlagen
gb.personalRepositories = Pers\u00f6nliche Repositories
gb.allowForks = Forks erlauben
gb.allowForksDescription = Erlaube authentifizierten Benutzern dieses Repository zu forken
gb.forkedFrom = Fork von
gb.canFork = Kann forken
gb.canForkDescription = Kann autorisierte Repositories als pers\u00f6nliche Repositories forken
gb.myFork = Meinen Fork anschauen
gb.forksProhibited = Forks verboten
gb.forksProhibitedWarning = Dieses Repository verbietet Forks
gb.noForks = {0} hat keine Forks
gb.forkNotAuthorized = Entschuldigung, Sie sind nicht berechtigt einen Fork von {0} zu erstellen
gb.forkInProgress = Fork in Bearbeitung
gb.preparingFork = Ihr Fork wird vorbereitet...
gb.isFork = ist ein Fork
gb.canCreate = Kann Erstellen
gb.canCreateDescription = Kann pers\u00f6nliche Repositories erstellen
gb.illegalPersonalRepositoryLocation = your personal repository must be located at \"{0}\"
gb.verifyCommitter = Committer verifizieren
gb.verifyCommitterDescription = Die Committer Identit\u00e4t muss mit dem Gitblit Benutzerkonto \u00fcbereinstimmen, dass den Push durchf\u00fchrt
gb.verifyCommitterNote = Alle Merge Operationen ben\u00f6tigen "--no-ff" um die Committer Identit\u00e4t zu erzwingen
gb.repositoryPermissions = Repositoryberechtigungen
gb.userPermissions = Benutzerberechtigungen
gb.teamPermissions = Teamberechtigungen
gb.add = Hinzuf\u00fcgen
gb.noPermission = DIESE BERECHTIGUNG L\u00d6SCHEN
gb.excludePermission = {0} (Ausschlie\u00dfen)
gb.viewPermission = {0} (Ansicht)
gb.clonePermission = {0} (Clonen)
gb.pushPermission = {0} (Pushen)
gb.createPermission = {0} (Pushen, Erzeugen von Refs)
gb.deletePermission = {0} (Pushen, Erzeugen und L\u00f6schen von Refs)
gb.rewindPermission = {0} (Pushen, Erzeugen, L\u00f6schen und Umsetzen von Refs)
gb.permission = Berechtigung
gb.regexPermission = Diese Berechtigung ergibt sich aus dem regul\u00e4ren Ausdruck "{0}"
gb.accessDenied = Zugang verweigert
gb.busyCollectingGarbage = Tut mir leid, Gitblit f\u00fchrt gerade eine Garbage Collection in {0} durch
gb.gcPeriod = GC Intervall
gb.gcPeriodDescription = Zeitraum zwischen Garbage Collections
gb.gcThreshold = GC Schwellwert
gb.gcThresholdDescription = Minimale Gr\u00f6\u00dfe von losen Objekten, um eine vorzeitge Garbage Collection zu starten
gb.ownerPermission = Repositorybesitzer
gb.administrator = Administrator
gb.administratorPermission = Gitblit Administrator
gb.team = Team
gb.teamPermission = Berechtigung gesetzt durch Mitgliedschaft im Team \"{0}\"
gb.missing = fehlt!
gb.missingPermission = Das Repository f\u00fcr diese Berechtigung fehlt!
gb.mutable = ver\u00e4nderlich
gb.specified = angegeben
gb.effective = effektiv
gb.organizationalUnit = Organisatorische Einheit
gb.organization = Organisation
gb.locality = Lage
gb.stateProvince = Bundesland oder Provinz
gb.countryCode = L\u00e4ndercode
gb.properties = Eigenschaften
gb.issued = herausgegeben
gb.expires = l\u00e4uft ab
gb.expired = abgelaufen
gb.expiring = l\u00e4uft ab
gb.revoked = widerrufen
gb.serialNumber = Seriennummer
gb.certificates = Zertifkate
gb.newCertificate = Neues Zertifikat
gb.revokeCertificate = Zertifikat widerrufen
gb.sendEmail = Sende E-Mail
gb.passwordHint = Passwort Hinweis
gb.ok = OK
gb.invalidExpirationDate = Ung\u00fcltiges Ablaufdatum!
gb.passwordHintRequired = Passworthinweis ben\u00f6tigt!
gb.viewCertificate = Zertifikat anschauen
gb.subject = Betreff
gb.issuer = Aussteller
gb.validFrom = G\u00fcltig von
gb.validUntil = G\u00fcltig bis
gb.publicKey = \u00d6ffentlicher Schl\u00fcssel
gb.signatureAlgorithm = Signaturalgorithmus
gb.sha1FingerPrint = SHA-1 Fingerabdruck
gb.md5FingerPrint = MD5 Fingerabdruck
gb.reason = Grund
gb.revokeCertificateReason = Bitte w\u00e4hlen Sie einen Grund f\u00fcr den Widerruf des Zertifikats aus
gb.unspecified = nicht angegeben
gb.keyCompromise = Schl\u00fcssel kompromittiert
gb.caCompromise = CA kompromittiert
gb.affiliationChanged = Mitgliedschaft ge\u00e4ndert
gb.superseded = ersetzt
gb.cessationOfOperation = T\u00e4tigkeit eingestellt
gb.privilegeWithdrawn = Privilegien zur\u00fcckgezogen
gb.time.inMinutes = in {0} Minuten
gb.time.inHours = in {0} Stunden
gb.time.inDays = in {0} Tagen
gb.hostname = Hostname
gb.hostnameRequired = Bitte geben Sie einen Hostnamen ein
gb.newSSLCertificate = Neues Server-SSL-Zertifikat
gb.newCertificateDefaults = Voreinstellungen f\u00fcr neue Zertifikate
gb.duration = Dauer
gb.certificateRevoked = Zertifikat {0,number,0} wurde widerufen
gb.clientCertificateGenerated = Neues Client-Zertifikat f\u00fcr {0} wurde erfolgreich erstellt
gb.sslCertificateGenerated = Neues Server-SSL-Zertifikat f\u00fcr {0} wurde erfolgreich erstellt
gb.newClientCertificateMessage = ANMERKUNG:\nDas "Passwort" ist nicht das Passwort des Benutzers, es ist das Passwort, um den Keystore des Benutzers zu sch\u00fctzen. Dieses Passwort wird nicht gespeichert, also m\u00fcssen Sie einen "Hinweis" eingeben. Dieser Hinweis wird in den README Anweisungen des Benutzers hinterlegt.
gb.certificate = Zertifkat
gb.emailCertificateBundle = Client-Zertifikat-Bundle per Email senden
gb.pleaseGenerateClientCertificate = Bitte generieren Sie ein Client Zertifikat f\u00fcr {0}
gb.clientCertificateBundleSent = Client-Zertifikat-Bundel f\u00fcr {0} gesendet
gb.enterKeystorePassword = Bitte geben Sie das Gitblit-Keystore-Passwort ein
gb.warning = Warnung
gb.jcwWarning = Ihrem Java Runtime Environment fehlen die \"JCE Unlimited Strength Jurisdiction Policy\" Dateien.\nDies schr\u00e4nkt die L\u00e4nge der Passw\u00f6rter, die Sie zum Verschl\u00fcsseln Ihrer Keystores verwenden k\u00f6nnen, auf 7 Zeichen ein.\nDiese Policy-Dateien sind ein optionaler Download von Oracle.\n\nM\u00f6chten Sie fortfahren und die Zertifikat-Infrastruktur trotzdem erstellen?\n\nWenn Sie mit Nein antworten, wird Ihr Browser die Oracle Download-Seite \u00f6ffnen, auf welcher Sie die Policy-Dateien herunterladen k\u00f6nnen.
gb.maxActivityCommits = Maximale Commits Aktivit\u00e4t
gb.maxActivityCommitsDescription = Maximale Anzahl von Commits die auf der Aktivit\u00e4tsseite dargestellt werden
gb.noMaximum = Kein Maximum
gb.attributes = Attribute
gb.serveCertificate = Verwende dieses Zertifikat f\u00fcr HTTPS
gb.sslCertificateGeneratedRestart = Neues SSL-Server-Zertifikat f\u00fcr {0} erfolgreich erstellt.\nSie m\u00fcssen Gitblit neu starten, um dieses Zertifikat nutzen zu k\u00f6nnen.\n\nWenn Sie dem '--alias' Parameter starten, m\u00fcssen Sie diesen auf '--alias {0}' setzen.
gb.validity = G\u00fcltigkeit
gb.siteName = Seitenname
gb.siteNameDescription = Kurzer, beschreibender Name Ihres Servers
gb.excludeFromActivity = Von der Aktivi\u00e4tenseite ausschlie\u00dfen
gb.isSparkleshared = Repository ist in einem Sparkleshare
gb.owners = Besitzer
gb.sessionEnded = Die Sitzung wurde geschlossen
gb.closeBrowser = Bitte schlie\u00dfen Sie den Browser um die Sitzung ordentlich zu beenden.
gb.doesNotExistInTree = {0} existiert im Dateibaum {1} nicht
gb.enableIncrementalPushTags = Aktiviere inkrementelle Push-Tags
gb.useIncrementalPushTagsDescription = Erzeuge beim Push automatisch ein Tag mit aufsteigender Revisionsnummer auf der Spitze jedes Branches
gb.incrementalPushTagMessage = Auto-tagged [{0}] branch on push
gb.externalPermissions = {0} Zugangsberechtigungen werden extern verwaltet
gb.viewAccess = Sie haben keinen Gitblit Lese- oder Schreibzugriff
gb.overview = \u00dcbersicht
gb.dashboard = Dashboard
gb.monthlyActivity = Monatliche Aktivit\u00e4t
gb.myProfile = Mein Profil
gb.compare = Vergleichen
gb.manual = Manuell
gb.from = von
gb.to = nach
gb.at = bei
gb.of = von
gb.in = in
gb.moreChanges = alle \u00c4nderungen...
gb.pushedNCommitsTo = Push von {0} Commits nach
gb.pushedOneCommitTo = Push von einem Commit nach
gb.commitsTo = {0} commits nach
gb.oneCommitTo = 1 Commit nach
gb.byNAuthors = von {0} Autoren
gb.byOneAuthor = von {0}
gb.viewComparison = Zeige einen Vergleich von diesen {0} Commits \u00bb
gb.nMoreCommits = {0} weitere Commits \u00bb
gb.oneMoreCommit = 1 weiterer Commit \u00bb
gb.pushedNewTag = Push von neuem Tag
gb.createdNewTag = Neuer Tag angelegt
gb.deletedTag = Tag gel\u00f6scht
gb.pushedNewBranch = Push von neuem Branch durchgef\u00fchrt
gb.createdNewBranch = Neuer Branch angelegt
gb.deletedBranch = Branch gel\u00f6scht
gb.createdNewPullRequest = Neuer Pull Request gestellt
gb.mergedPullRequest = Merge des Pull Request durchgef\u00fchrt
gb.rewind = REWIND
gb.star = Favorisieren
gb.unstar = Nicht mehr favorisieren
gb.stargazers = Stargazers
gb.starredRepositories = Favorisierte Repositories
gb.failedToUpdateUser = Aktualisierung des Benutzerkontos fehlgeschlagen!
gb.myRepositories = Meine Repositories
gb.noActivity = In den letzten {0} Tagen gab es keine Aktivit\u00e4t
gb.findSomeRepositories = finde Repositories
gb.metricAuthorExclusions = Ausschl\u00fcsse von Autor-Metriken
gb.myDashboard = Mein Dashboard
gb.failedToFindAccount = Benutzerkonto "{0}" konnte nicht gefunden werden
gb.reflog = Reflog
gb.active = Aktiv
gb.starred = Favorisiert
gb.owned = Eigene
gb.starredAndOwned = Favorisiert und Eigene
gb.reviewPatchset = Review {0} Patchset {1}
gb.todaysActivityStats = Heute / {1} Commits von {2} Autoren
gb.todaysActivityNone = Heute / keine
gb.noActivityToday = Heute gab es keine Aktivit\u00e4t
gb.anonymousUser= Anonym
gb.commitMessageRenderer = Commit-Message Renderer
gb.diffStat = {0} Einf\u00fcgungen & {1} L\u00f6schungen
gb.home = Startseite
gb.isMirror = Dieses Repository ist ein Mirror
gb.mirrorOf = Mirror von {0}
gb.mirrorWarning = Dieses Repository ist ein Mirror und kann keine Pushes empfangen
gb.docsWelcome1 = Sie k\u00f6nnen Dokumente verwenden, um Ihr Repository zu dokumentieren.
gb.docsWelcome2 = Committen Sie eine README.md oder eine HOME.md Datei, um zu beginnen.
gb.createReadme = Eine README erstellen
gb.responsible = Bearbeiter
gb.createdThisTicket = erstellte dieses Ticket
gb.proposedThisChange = schlug diese \u00c4nderung vor
gb.uploadedPatchsetN = lud Patchset {0} hoch
gb.uploadedPatchsetNRevisionN = lud Patchset {0} Revision {1} hoch
gb.mergedPatchset = Merge des Patchset durchgef\u00fchrt
gb.commented = kommentierte
gb.noDescriptionGiven = keine Beschreibung hinterlegt
gb.toBranch = nach {0}
gb.createdBy = angelegt durch
gb.oneParticipant = {0} Teilnehmer
gb.nParticipants = {0} Teilnehmer
gb.noComments = keine Kommentare
gb.oneComment = {0} Kommentar
gb.nComments = {0} Kommentare
gb.oneAttachment  = {0} Anhang
gb.nAttachments = {0} Anh\u00e4nge
gb.milestone = Meilenstein
gb.compareToMergeBase = Mit Merge-Basis vergleichen
gb.compareToN = mit {0} vergleichen
gb.open = Offen
gb.closed = Geschlossen
gb.merged = Merge erfolgt
gb.ticketPatchset = Ticket {0}, Patchset {1}
gb.patchsetMergeable = Dieses Patchset kann automatisch mit {0} zusammengef\u00fchrt werden.
gb.patchsetMergeableMore = Dieses Patchset kann auch auf der Kommandozeile mit {0} zusammengef\u00fchrt werden.
gb.patchsetAlreadyMerged = Dieses Patcheset wurde mit {0} zusammengef\u00fchrt.
gb.patchsetNotMergeable = Dieses Patchset kann nicht automatisch mit {0} zusammengef\u00fchrt werden.
gb.patchsetNotMergeableMore = Dieses Patchset ben\u00f6tigt einen Rebase oder muss manuell mit {0} zusammengef\u00fchrt werden, um Konflikte zu beheben.
gb.patchsetNotApproved = Diese Patchset-Revision wurde noch nicht zum Zusammenf\u00fchren mit {0} freigegeben.
gb.patchsetNotApprovedMore = Ein Reviewer muss dieses Patchset freigeben.
gb.patchsetVetoedMore = Ein Reviewer hat dieses Patchset abgelehnt.
gb.write = Verfassen
gb.comment = Kommentieren
gb.preview = Vorschau
gb.leaveComment = Hinterlasse einen Kommentar
gb.showHideDetails = Zeige/Verberge Details
gb.acceptNewPatchsets = Patchsets annehmen
gb.acceptNewPatchsetsDescription = Nimm Patchsets an dieses Repository per Push an
gb.acceptNewTickets = Erlaube neue Tickets
gb.acceptNewTicketsDescription = Erlaube das Erstellen von Fehlerberichten, Verbesserungsvorschl\u00e4gen, Aufgaben etc.
gb.requireApproval = Explizite Freigaben erforderlich
gb.requireApprovalDescription = Patchsets m\u00fcssen freigegeben werden, bevor der Merge Button aktiviert wird
gb.topic = Thema
gb.proposalTickets = Vorgeschlagene \u00c4nderungen
gb.bugTickets = Fehler
gb.enhancementTickets = Erweiterungen
gb.taskTickets = Aufgaben
gb.questionTickets = Fragen
gb.requestTickets = Erweiterungen und Aufgaben
gb.yourCreatedTickets = Von mir erstellt
gb.yourWatchedTickets = Von mir beobachtet
gb.mentionsMeTickets = Erw\u00e4hnungen
gb.updatedBy = aktualisiert von
gb.sort = Sortierung
gb.sortNewest = Neueste
gb.sortOldest = \u00c4lteste
gb.sortMostRecentlyUpdated = K\u00fcrzlich aktualisiert
gb.sortLeastRecentlyUpdated = Am l\u00e4ngsten nicht aktualisiert
gb.sortMostComments = Die meisten Kommentare
gb.sortLeastComments = Die wenigsten Kommentare
gb.sortMostPatchsetRevisions = Die meisten Patchset Revisions
gb.sortLeastPatchsetRevisions = Die wenigsten Patchset Revisions
gb.sortMostVotes = Die meisten Stimmen
gb.sortLeastVotes = Die wenigsten Stimmen
gb.topicsAndLabels = Themen & Labels
gb.milestones = Meilensteine
gb.noMilestoneSelected = Kein Meilenstein ausgew\u00e4hlt
gb.notSpecified = nicht angegebene
gb.due = f\u00e4llig am
gb.queries = Suchanfragen
gb.searchTicketsTooltip = Suche {0} Tickets
gb.searchTickets = Search Tickets
gb.new = Neu
gb.newTicket = Neues Ticket
gb.editTicket = Bearbeite Ticket
gb.ticketsWelcome = Sie k\u00f6nnen Tickets verwenden, um Ihre Todo-Liste zu verwalten, Fehler zu diskutieren und bei der Erstellung von Patchsets zusammen zu arbeiten.
gb.createFirstTicket = Erstellen Sie Ihr erstes Ticket
gb.title = \u00dcberschrift
gb.changedStatus = hat Status ge\u00e4ndert
gb.discussion = Diskussion
gb.updated = aktualisiert
gb.proposePatchset = Patchset Vorschlagen
gb.proposePatchsetNote = Bitte schlagen Sie ein Patchset f\u00fcr dieses Ticket vor.
gb.proposeInstructions = Um zu beginnen, erstellen Sie ein Patchset und laden Sie es mit Git hoch. Gitblit verkn\u00fcpft dieses Patchset mit diesem Ticket \u00fcber die Id.
gb.proposeWith = Ein Patchset mit {0} vorschlagen
gb.revisionHistory = Revisionshistorie
gb.merge = Zusammenf\u00fchren
gb.action = Aktion
gb.patchset = Patchset
gb.all = Alle
gb.mergeBase = Merge Basis
gb.checkout = Checkout
gb.checkoutViaCommandLine =  Checkout per Kommandozeile
gb.checkoutViaCommandLineNote = Sie k\u00f6nnen diese \u00c4nderungen in Ihrem Clone des Repository per Checkout lokal testen.
gb.checkoutStep1 = Laden Sie das aktuelle Patchset \u2014 F\u00fchren Sie dies aus Ihrem lokalen Projektverzeichnis heraus aus
gb.checkoutStep2 = F\u00fchren Sie einen Checkout des Patchsets in einen neuen Branch durch und Sichten Sie die \u00c4nerungen
gb.mergingViaCommandLine = Merge per Kommandozeile
gb.mergingViaCommandLineNote = Wenn Sie den Merge Button nicht verwenden m\u00f6chten oder ein automatisches Zusammenf\u00fchren nicht durchgef\u00fchrt werden kann, dann k\u00f6nnen Sie das Zusammenf\u00fchren auch manuell auf der Kommandozeile durchf\u00fchren.
gb.mergeStep1 = Erstellen Sie einen neuen Branch um die \u00c4nderungen zu sichten \u2014 F\u00fchren Sie dies aus Ihrem lokalen Projektverzeichnis heraus aus
gb.mergeStep2 = Bringen Sie die vorgeschlagenen \u00c4nderungen ein und sichten Sie diese
gb.mergeStep3 = F\u00fchren Sie einen Merge durch und aktualisieren Sie den Server (Push)
gb.download = Herunterladen
gb.ptDescription = Das Gitblit Patchset Tool
gb.ptCheckout = Fetch & Checkout des aktuellen Patchset um ein Review des aktuellen Branch durchzuf\u00fchren
gb.ptMerge = Fetch & Merge des aktuellen Patchset in Ihren lokalen Branch
gb.ptDescription1 = Barnum ist ein Kommandozeilen-Werkzeug f\u00fcr Git um die Syntax f\u00fcr die Arbeit mit Gitblit Tickets und Patchsets zu vereinfachen.
gb.ptSimplifiedCollaboration = Vereinfachte Syntax f\u00fcr Zusammenarbeit
gb.ptSimplifiedMerge = Vereinfachte Syntax f\u00fcr das Zusammenf\u00fchren
gb.ptDescription2 = Barnum ben\u00f6tigt Python 3 und den nativen Git-Client.  Es l\u00e4uft unter Windows, Linux und Mac OS X.
gb.stepN = Schritt {0}
gb.watchers = Beobachter
gb.votes = Stimmen
gb.vote = F\u00fcr {0} Abstimmen
gb.watch = {0} beobachten
gb.removeVote = Abstimmen r\u00fcckg\u00e4ngig machen
gb.stopWatching = Beobachten beenden
gb.watching = ich beobachte
gb.comments = Kommentare
gb.addComment = Kommentar hinzuf\u00fcgen
gb.export = Exportieren
gb.oneCommit = Ein Commit
gb.nCommits = {0} Commits
gb.addedOneCommit = 1 Commit hinzugef\u00fcgt
gb.addedNCommits = {0} Commits hinzugef\u00fcgt
gb.commitsInPatchsetN = Commits im Patchset {0}
gb.patchsetN = Patchset {0}
gb.reviewedPatchsetRev = Review von Patchset {0} Revision {1} durchgef\u00fchrt: {2}
gb.review = Review
gb.reviews = Reviews
gb.veto = Abgelehnt
gb.needsImprovement = Ben\u00f6tigt Verbesserungen
gb.looksGood = Sieht gut aus
gb.approve = Akzeptiert
gb.hasNotReviewed = kein Review durchgef\u00fchrt
gb.about = \u00dcber
gb.ticketN = Ticket #{0}
gb.disableUser = Benutzer deaktivieren
gb.disableUserDescription = Verhindert das Authentifizieren dieses Benutzer
gb.any = Alle
gb.milestoneProgress = {0} offen, {1} geschlossen
gb.nOpenTickets = {0} offen
gb.nClosedTickets = {0} geschlossen
gb.nTotalTickets = {0} insgesamt
gb.body = Body
gb.mergeSha = mergeSha
gb.mergeTo = Merge mit
gb.labels = Labels
gb.reviewers = Reviewer
gb.voters = Abstimmende
gb.mentions = Erw\u00e4hnungen
gb.canNotProposePatchset = Kann kein Patchset vorschlagen
gb.repositoryIsMirror = Dieses Repository ist ein Read-Only-Mirror.
gb.repositoryIsFrozen = Dieses Repository ist eingefroren.
gb.repositoryDoesNotAcceptPatchsets = Dieses Repository akzeptiert keine Patchsets.
gb.serverDoesNotAcceptPatchsets = Dieser Server akzeptiert keine Patchsets.
gb.ticketIsClosed = Dieses Ticket ist geschlossen.
gb.mergeToDescription = Standardm\u00e4\u00dfiger Integrationsbranch f\u00fcr den Merge von Ticket Patchsets
gb.anonymousCanNotPropose = Anonyme Benutzer k\u00f6nnen keine Patchsets vorschlagen.
gb.youDoNotHaveClonePermission = Sie sind nicht berechtigt, dieses Repository zu klonen.
gb.myTickets = Meine Tickets
gb.yourAssignedTickets = Mir zugewiesen
gb.newMilestone = Neuer Meilenstein
gb.editMilestone = Meilenstein bearbeiten
gb.deleteMilestone = Meilenstein \"{0}\" l\u00f6schen?
gb.milestoneDeleteFailed = L\u00f6schen des Meilensteins ''{0}'' fehlgeschlagen!
gb.notifyChangedOpenTickets = Sende Benachrichtigungen f\u00fcr ge\u00e4nderte, offene Tickets
gb.overdue = \u00dcberf\u00e4llig
gb.openMilestones = Offene Meilensteine
gb.closedMilestones = Geschlossene Meilensteine
gb.administration = Administration
gb.plugins = Plugins
gb.extensions = Erweiterungen
gb.pleaseSelectProject = Bitte w\u00e4hlen Sie das Projekt!
gb.accessPolicy = Zugriffsrichtlinie
gb.accessPolicyDescription = W\u00e4hlen Sie eine Zugriffsrichtlinie um die Sichtbarkeit des Repositories und die Zugriffsrechte von Git zu steuern.
gb.anonymousPolicy = Anonymes Anzeigen, Klonen und Pushen
gb.anonymousPolicyDescription = Jeder kann dieses Repository sehen, klonen und zu ihm pushen.
gb.authenticatedPushPolicy = Schr\u00e4nke Pushen ein (Authentifiziert)
gb.authenticatedPushPolicyDescription = Jeder kann dieses Repository sehen und klonen. Alle authentifizierten Benutzer haben RW+ Push Berechtigung.
gb.namedPushPolicy = Schr\u00e4nke Pushen ein (Benannt)
gb.namedPushPolicyDescription = Jeder kann dieses Repository sehen und klonen. Sie bestimmen, wer pushen kann.
gb.clonePolicy = Schr\u00e4nke Klonen und Pushen ein
gb.clonePolicyDescription = Jeder kann dieses Repository sehen. Sie bestimmen, wer klonen und pushen kann.
gb.viewPolicy  = Schr\u00e4nke Anzeigen, Klonen und Pushen ein
gb.viewPolicyDescription = Sie bestimmen, wer dieses Repository anzeigen, klonen und zu ihm pushen kann.
gb.initialCommit = Initialer Commit
gb.initialCommitDescription = Dies erlaubt es Ihnen, mit <code>git clone</code> dieses Repository sofort zu klonen. \u00dcberspringen Sie diesen Schritt, falls Sie lokal bereits <code>git init</code> ausgef\u00fchrt haben.
gb.initWithReadme = README erstellen
gb.initWithReadmeDescription = Dies erstellt ein einfaches README Dokument f\u00fcr Ihr Repository.
gb.initWithGitignore = Eine .gitignore Datei erstellen
gb.initWithGitignoreDescription = Dies erstellt eine Konfigurationsdatei, die Ihren Git Client anweist, Dateien und Verzeichnisse zu ignorieren, die bestimmten Mustern entsprechen.
gb.pleaseSelectGitIgnore = Bitte w\u00e4hlen Sie eine .gitignore Datei aus
gb.receive = Empfangen
gb.permissions = Berechtigungen
gb.ownersDescription = Besitzer k\u00f6nnen alle Einstellungen des Repository ver\u00e4ndern, jedoch k\u00f6nnen Sie das Repository nur umbenennen, falls es ein pers\u00f6nliches Repository ist.
gb.userPermissionsDescription = Sie k\u00f6nnen individuelle Benutzerberechtigungen vergeben. Diese Einstellungen \u00fcbersteuern Team- oder Regexberechtigungen.
gb.teamPermissionsDescription = Sie k\u00f6nnen individuelle Teamberechtigungen vergeben. Diese Einstellungen \u00fcbersteuern Regexberechtigungen.
gb.ticketSettings = Ticketeinstellungen
gb.receiveSettings = Empfangseinstellungen
gb.receiveSettingsDescription = Die Empfangseinstellungen kontrollieren Pushes zum Repository.
gb.preReceiveDescription = Pre-Empfangs-Hooks werden ausgef\u00fchrt nachdem alle Commits empfangen wurden, aber <em>BEVOR</em> die Refs aktualisiert werden. <p>Dies ist der geeignete Hook, um einen Push abzulehnen.</p>
gb.postReceiveDescription = Post-Empfangs-Hooks werden ausgef\u00fchrt, nachdem alle Commits empfangen wurden und <em>NACHDEM</em> die Refs aktualisiert wurden. <p>Dies ist der geeignete Hook f\u00fcr Benachrichtigungen, Build-Trigger, etc.</p>
gb.federationStrategyDescription = Bestimmen Sie, ob und wie dieses Repository mit einer anderen Gitblit Instanz verbunden werden kann (Federation).
gb.federationSetsDescription = Dieses Repository wird in den ausgew\u00e4hlten Verbindungssets enthalten sein.
gb.miscellaneous = Sonstiges
gb.originDescription = Die URL, von welcher dieses Repository geklont wurde.
gb.gc = GC
gb.garbageCollection = Garbage Collection
gb.garbageCollectionDescription = Die Garbage Collection b\u00fcndelt freie Objekte, welche von Clients gepusht wurden und entfernt nicht mehr referenzierte Objekte aus dem Repository.
gb.commitMessageRendererDescription = Commit Messages k\u00f6nnen als reiner Text oder gerendertes Markup dargestellt werden.
gb.preferences = Einstellungen
gb.accountPreferences = Kontoeinstellungen
gb.accountPreferencesDescription = Geben Sie Ihre Kontoeinstellungen an
gb.languagePreference = Spracheinstellungen
gb.languagePreferenceDescription = W\u00e4hlen Sie Ihre bevorzugte \u00dcbersetzung f\u00fcr Gitblit
gb.emailMeOnMyTicketChanges = Sende Emails bei eigenen \u00c4nderungen an Tickets
gb.emailMeOnMyTicketChangesDescription  = Sende mir Email-Benachrichtigungen f\u00fcr \u00c4nderungen, die ich an einem Ticket vornehme
gb.displayNameDescription = Bevorzugter Anzeigename
gb.emailAddressDescription = Die prim\u00e4re Emailadresse f\u00fcr den Empfang von Benachrichtigungen
gb.sshKeys = SSH Keys
gb.sshKeysDescription = SSH Public Key Authentifizierung ist eine sichere Alternative zur Authentifizierung mit Passwort
gb.addSshKey = SSH Key hinzuf\u00fcgen
gb.key = Key
gb.comment = Kommentar
gb.sshKeyCommentDescription = Geben Sie optional einen Kommentar ein. Falls Sie dies nicht tun, wird der Kommentar aus dem Key extrahiert.
gb.permission = Berechtigung
gb.sshKeyPermissionDescription = Geben Sie die Zugriffberechtigung f\u00fcr den SSH Key an
gb.transportPreference = \u00dcbertragungseinstellungen
gb.transportPreferenceDescription = Geben Sie die \u00dcbertragungsart an, die Sie f\u00fcr das Klonen bevorzugen
src/main/java/com/gitblit/wicket/GitBlitWebApp_fr.properties
@@ -670,3 +670,5 @@
gb.serverDoesNotAcceptPatchsets = Ce serveur n'accepte pas de patchsets.
gb.ticketIsClosed = Ce ticket est clos.
gb.mergeToDescription = default integration branch for merging ticket patchsets
gb.myTickets = mes tickets
gb.yourAssignedTickets = dont vous êtes responsable
src/main/java/com/gitblit/wicket/GitBlitWebApp_it.properties
New file
@@ -0,0 +1,745 @@
gb.repository = repository
gb.owner = proprietario
gb.description = descrizione
gb.lastChange = ultima modifica
gb.refs = refs
gb.tag = tag
gb.tags = tags
gb.author = autore
gb.committer = committer
gb.commit = commit
gb.age = età
gb.tree = albero
gb.parent = padre
gb.url = URL
gb.history = cronologia
gb.raw = originale
gb.object = oggetto
gb.ticketId = id del ticket
gb.ticketAssigned = assegnato
gb.ticketOpenDate = data di apertura
gb.ticketStatus = stato
gb.ticketComments = commenti
gb.view = vista
gb.local = locale
gb.remote = remoto
gb.branches = rami
gb.patch = patch
gb.diff = differenze
gb.log = log
gb.moreLogs = ulteriori commits...
gb.allTags = tutte le tags...
gb.allBranches = tutti i rami...
gb.summary = riassunto
gb.ticket = ticket
gb.newRepository = nuovo repository
gb.newUser = nuovo utente
gb.commitdiff = differenze di commit
gb.tickets = elenco ticket
gb.pageFirst = prima
gb.pagePrevious = prec
gb.pageNext = succ
gb.head = HEAD
gb.blame = annotazioni
gb.login = login
gb.logout = logout
gb.username = nome utente
gb.password = password
gb.tagger = autore della tag
gb.moreHistory =  ulteriore cronoligia...
gb.difftocurrent = differenze con il corrente
gb.search = ricerca
gb.searchForAuthor = Ricerca commit per autore
gb.searchForCommitter = Ricerca commit per committer
gb.addition = aggiunta
gb.modification = modifica
gb.deletion = cancellazione
gb.rename = rinomina
gb.metrics = metriche
gb.stats = statistiche
gb.markdown = markdown
gb.changedFiles = file modificati
gb.filesAdded = {0} file aggiunti
gb.filesModified = {0} file modificati
gb.filesDeleted = {0} file cancellati
gb.filesCopied = {0} file copiati
gb.filesRenamed = {0} file rinominati
gb.missingUsername = Nome Utente Mancante
gb.edit = modifica
gb.searchTypeTooltip = Seleziona tipo di ricerca
gb.searchTooltip = Ricerca {0}
gb.delete = cancella
gb.docs = documentazione
gb.accessRestriction = restrizioni di accesso
gb.name = nome
gb.enableTickets = abilita supporto ticket
gb.enableDocs = abilita documentazione
gb.save = salva
gb.showRemoteBranches = mostra i rami remoti
gb.editUsers = modifica utenti
gb.confirmPassword = conferma password
gb.restrictedRepositories = repository con restrizioni di accesso
gb.canAdmin = controllo completo
gb.notRestricted = visualizzazione, clone e push anonimi
gb.pushRestricted = push previa autenticazione
gb.cloneRestricted = clone e push previa autenticazione
gb.viewRestricted = visualizzazione, clone e push previa autenticazione
gb.useTicketsDescription = segnalazioni Ticgit distribuite (sola lettura)
gb.useDocsDescription = elenca la documentazione Markdown nel repository
gb.showRemoteBranchesDescription = mostra rami remoti
gb.canAdminDescription = può amministrare il server Gitblit
gb.permittedUsers = utenti autorizzati
gb.isFrozen = sola lettura
gb.isFrozenDescription = operazioni di push vietate
gb.zip = zip
gb.showReadme = mostra readme
gb.showReadmeDescription = mostra un file \"readme\" in formato Markdown nella pagina riassuntiva
gb.nameDescription = utilizza '/' per raggruppare i repository.  p.e. libraries/mycoollib.git
gb.ownerDescription = il proprietario può modificare le impostazioni del repository
gb.blob = blob
gb.commitActivityTrend = tendenza dell'attività di commit
gb.commitActivityDOW = commit per giorno della settimana
gb.commitActivityAuthors = autori principali sulla base dei commit
gb.feed = feed
gb.cancel = annulla
gb.changePassword = cambia password
gb.isFederated = federato
gb.federateThis = attiva federazione su questo repository
gb.federateOrigin = attiva federazione su origin
gb.excludeFromFederation = escludi dalla federazione
gb.excludeFromFederationDescription = impedisci a istanze federate di Gitblit di effettuare il pull su questo utente
gb.tokens = gettoni di federazione
gb.tokenAllDescription = tutti i repository, utenti e impostazioni
gb.tokenUnrDescription = tutti i repository e gli utenti
gb.tokenJurDescription = tutti i repository
gb.federatedRepositoryDefinitions = definizione dei repository
gb.federatedUserDefinitions = definizione degli utenti
gb.federatedSettingDefinitions = definizione delle impostazioni
gb.proposals = proposta di federazione
gb.received = ricevuta
gb.type = tipo
gb.token = gettone
gb.repositories = repository
gb.proposal = proposta
gb.frequency = frequenza
gb.folder = cartella
gb.lastPull = ultimo pull
gb.nextPull = prossimo pull
gb.inclusions = inclusioni
gb.exclusions = esclusioni
gb.registration = registrazione
gb.registrations = registrationi di federazione
gb.sendProposal = proponi
gb.status = stato
gb.origin = origin
gb.headRef = ramo predefinito (HEAD)
gb.headRefDescription = Il ramo predefinito soggetto a clone e visualizzato nella pagina riassuntiva
gb.federationStrategy = strategia di federazione
gb.federationRegistration = registrazione di federazione
gb.federationResults = risultati di pull federato
gb.federationSets = insiemi di federazione
gb.message = messaggio
gb.myUrlDescription = URL pubblico alla tua istanza Gitblit
gb.destinationUrl = invia a
gb.destinationUrlDescription = URL dell'istanza Gitblit a cui inviare la proposta
gb.users = utenti
gb.federation = federazione
gb.error = errore
gb.refresh = aggiorna
gb.browse = sfoglia
gb.clone = clona
gb.filter = filtra
gb.create = crea
gb.servers = server
gb.recent = recenti
gb.available = disponibile
gb.selected = selezionato
gb.size = dimensione
gb.downloading = download in corso
gb.loading = caricamento
gb.starting = avvio
gb.general = generale
gb.settings = impostazioni
gb.manage = gestisci
gb.lastLogin = ultimo login
gb.skipSizeCalculation = non computare le dimensioni
gb.skipSizeCalculationDescription = disabilita la computazione delle dimensioni del repository (velocizza caricamento pagine)
gb.skipSummaryMetrics = ometti il riassunto delle metriche
gb.skipSummaryMetricsDescription = non calcolare le metriche nella pagina riassuntiva (velocizza caricamento pagine)
gb.accessLevel = livello di access
gb.default = predefinito
gb.setDefault = imposta predefinito
gb.since = da
gb.status = stato
gb.bootDate = data di avvio del server
gb.servletContainer = servlet container
gb.heapMaximum = memoria massima
gb.heapAllocated = memoria allocata
gb.heapUsed = memoria utilizzata
gb.free = libero
gb.version = versione
gb.releaseDate = data di rilascio
gb.date = data
gb.activity = attività
gb.subscribe = sottoscrivi
gb.branch = ramo
gb.maxHits = max hits
gb.recentActivity = attività recenti
gb.recentActivityStats = ultimi {0} giorni / {1} commit da {2} autori
gb.recentActivityNone = ultimi {0} giorni / nessuno
gb.dailyActivity = attività giornaliera
gb.activeRepositories = repository attivi
gb.activeAuthors = autori attivi
gb.commits = commit
gb.teams = gruppi
gb.teamName = nome del gruppo
gb.teamMembers = membry del gruppo
gb.teamMemberships = appartenenza al gruppo
gb.newTeam = nuovo gruppo
gb.permittedTeams = gruppi autorizzati
gb.emptyRepository = repository vuoto
gb.repositoryUrl = repository URL
gb.mailingLists = liste di corrispondenza
gb.preReceiveScripts = script eseguiti su pre-receive
gb.postReceiveScripts = script eseguiti su post-receive
gb.hookScripts = script eseguiti su hook di git
gb.customFields = campi peronalizzati
gb.customFieldsDescription = campi personalizzati accessibili a hook scritti in Groovy
gb.accessPermissions = permessi di accesso
gb.filters = filtri
gb.generalDescription = impostazioni comuni
gb.accessPermissionsDescription = riduci l'accesso in base a utenti e gruppi
gb.accessPermissionsForUserDescription = imposta appartenenza a un gruppo o abilita accesso a repository soggetti a restrizioni
gb.accessPermissionsForTeamDescription = imposta i membri di un gruppo e abilita accesso a repository soggetti a restrizioni
gb.federationRepositoryDescription = condividi questo repository con altre istanze Gitblit
gb.hookScriptsDescription = esegue script Groovy quando viene effettuato un push su questo server
gb.reset = reimposta
gb.pages = pagine
gb.workingCopy = copia di lavoro
gb.workingCopyWarning = questo repository è una copia di lavoro a non ammette push
gb.query = interrogazione
gb.queryHelp = La sintassi standard è supportata.<p/><p/>Si vedi <a target="_new" href="http://lucene.apache.org/core/old_versioned_docs/versions/3_5_0/queryparsersyntax.html">Lucene Query Parser Syntax</a> per ulteriori dettagli.
gb.queryResults = risultati {0} - {1} ({2} corrispondenze)
gb.noHits = nessuna corrispondenza
gb.authored = creato da
gb.committed = commit di
gb.indexedBranches = Rami Indicizzati
gb.indexedBranchesDescription = Seleziona i rami che devono essere indicizzati tramite Lucene
gb.noIndexedRepositoriesWarning = nessun repository è soggetto a indicizzazione Lucene
gb.undefinedQueryWarning = interrogazione non specificata!
gb.noSelectedRepositoriesWarning = per favore specifica uno o più repository!
gb.luceneDisabled = Indicizzazione tramite Lucene disabilitata
gb.failedtoRead = Errore di lettura
gb.isNotValidFile = non è un file valido
gb.failedToReadMessage = Errore nella lettura del messaggio da {0}!
gb.passwordsDoNotMatch = La password non corissponde!
gb.passwordTooShort = La password è troppo corta. La lunghezza minima è {0} caratteri.
gb.passwordChanged = Password modificata con successo.
gb.passwordChangeAborted = Modifica della password annullata.
gb.pleaseSetRepositoryName = Specificare il nome del repository!
gb.illegalLeadingSlash = Riferiementi alla cartella principale (/) non sono ammessi.
gb.illegalRelativeSlash = Riferimenti relativi a cartelle (../) non sono ammessi.
gb.illegalCharacterRepositoryName = Carattere non consetito ''{0}'' nel nome del repository!
gb.selectAccessRestriction = Restrizioni di accesso non specificate!
gb.selectFederationStrategy = Strategia di federazione non selezionata!
gb.pleaseSetTeamName = Nome del gruppo non specificato!
gb.teamNameUnavailable = Il nome di gruppo ''{0}'' non è disponibile.
gb.teamMustSpecifyRepository = Un gruppo deve specificare almeno un repository.
gb.teamCreated = Nuovo gruppo ''{0}'' creato con successo.
gb.pleaseSetUsername = Nome utente non specificato!
gb.usernameUnavailable = Il nome utente ''{0}'' non è disponibile.
gb.combinedMd5Rename = Gitblit è configurato per effettuare un hashing delle password di tipo combinato-md5. E' quindi necessario specificare una nuova password quando si rinomina un utenza.
gb.userCreated = Nuovo utente ''{0}'' creato con successo.
gb.couldNotFindFederationRegistration = Impossibile trovare la registrazione di federazione!
gb.failedToFindGravatarProfile = Profilo Gravatar per {0} non reperito!
gb.branchStats = {2} contiene {0} commit e {1} tag
gb.repositoryNotSpecified = Repository non specificato!
gb.repositoryNotSpecifiedFor = Repository non specificato per {0}!
gb.canNotLoadRepository = Impossibile leggere il repository
gb.commitIsNull = Commit nullo
gb.unauthorizedAccessForRepository = Accesso al repository non autorizzato
gb.failedToFindCommit = Cmmit \"{0}\" non trovato in {1}!
gb.couldNotFindFederationProposal = Impossibile trovare la proposta di federazione!
gb.invalidUsernameOrPassword = Nome utente o password non validi!
gb.OneProposalToReview = 1 proposta di federazione è in attesa di valutazione.
gb.nFederationProposalsToReview = Ci sono {0} proposte di federazione in attesa di valutazione
gb.couldNotFindTag = Tag non trovata {0}
gb.couldNotCreateFederationProposal = Impossibile creare proposta di federazione!
gb.pleaseSetGitblitUrl = Gitblit URL non specificato!
gb.pleaseSetDestinationUrl = URL di destinazione non specificato!
gb.proposalReceived = Proposta ricevuta da {0} con successo.
gb.noGitblitFound = {0} non ha potuto trovare una istanza di Gitblit presso {1}.
gb.noProposals = {0} al momento non accetta proposte.
gb.noFederation = {0} non è configurato per creare una federazione con altre istanze Gitblit.
gb.proposalFailed = {0} non ha ricevuto alcuna proposta di federazione!
gb.proposalError = {0} segnala un errore inatteso!
gb.failedToSendProposal = Invio proposta fallito!
gb.userServiceDoesNotPermitAddUser = {0} non consete di aggiungere nuovi utenti!
gb.userServiceDoesNotPermitPasswordChanges = {0} non consente di cambiare la password!
gb.displayName = nome pubblico
gb.emailAddress = indirizzo email
gb.errorAdminLoginRequired = Effettuare l'accesso per utilizzare le funzioni amministrative
gb.errorOnlyAdminMayCreateRepository = Solo un utente amministratore può creare repository
gb.errorOnlyAdminOrOwnerMayEditRepository = Solo un utente amministratore e il proprietario possono modificare un repository
gb.errorAdministrationDisabled = Funzioni amministrative disabilitate
gb.lastNDays = Ultimi {0} giorni
gb.completeGravatarProfile = Completa il profilo su Gravatar.com
gb.none = nessuno
gb.line = linea
gb.content = contenuto
gb.empty = vuoto
gb.inherited = ereditato
gb.deleteRepository = Cancellare il repository \"{0}\"?
gb.repositoryDeleted = Repository ''{0}'' cancellato.
gb.repositoryDeleteFailed = Impossible cancellare il repository ''{0}''!
gb.deleteUser = Cancellare l'utente \"{0}\"?
gb.userDeleted = Utente ''{0}'' cancellato.
gb.userDeleteFailed = Impossibile cancellare l'utente ''{0}''!
gb.time.justNow = in questo istante
gb.time.today = oggi
gb.time.yesterday = ieri
gb.time.minsAgo = {0} minuti fa
gb.time.hoursAgo = {0} ore fa
gb.time.daysAgo = {0} giorni fa
gb.time.weeksAgo = {0} settimane fa
gb.time.monthsAgo = {0} mese fa
gb.time.oneYearAgo = 1 anno fa
gb.time.yearsAgo = {0} anni fa
gb.duration.oneDay = 1 giorno
gb.duration.days = {0} giorni
gb.duration.oneMonth = 1 mese
gb.duration.months = {0} mesi
gb.duration.oneYear = 1 anno
gb.duration.years = {0} anni
gb.authorizationControl = Controlli di autorizzazine
gb.allowAuthenticatedDescription = Consenti lettura/scrittura a tutti gli utenti registrati
gb.allowNamedDescription = Configura permessi specifici per utenti e gruppi
gb.markdownFailure = Impossibile leggere contenuto Markdown!
gb.clearCache = pulisci la cache
gb.projects = progetti
gb.project = progetto
gb.allProjects = tutti i progetti
gb.copyToClipboard = copia negli appunti
gb.fork = ramificazione
gb.forks = ramificazioni
gb.forkRepository = crea una ramificazione {0}?
gb.repositoryForked = ramificazione creata per {0}
gb.repositoryForkFailed= ramificazione fallita
gb.personalRepositories = repository personali
gb.allowForks = consenti ramificazioni
gb.allowForksDescription = consenti agli utenti autorizzati di creare ramificazioni di questo repository
gb.forkedFrom = ramificazione di
gb.canFork = creare ramificazioni
gb.canForkDescription = può creare una ramificazione personale a partire da un repository cui ha accesso
gb.myFork = vedere le mie ramificazioni
gb.forksProhibited = ramificazioni vietate
gb.forksProhibitedWarning = questo repository non ammette ramificazioni
gb.noForks = {0} non ha ramificazioni
gb.forkNotAuthorized = non sei autorizzato a crare una ramificazione di {0}
gb.forkInProgress = ramificazione in corso
gb.preparingFork = ramificazione in preparazione...
gb.isFork = è una ramificazione
gb.canCreate = può creare
gb.canCreateDescription = può creare repository personali
gb.illegalPersonalRepositoryLocation = i tuoi repository personali devono trovarsi \"{0}\"
gb.verifyCommitter = verifica committer
gb.verifyCommitterDescription = controlla che l'identità del committer corrisponda all'utente Gitblit di push
gb.verifyCommitterNote = tutti i merge richiedono "--no-ff" per verificare l'identità del committer
gb.repositoryPermissions = permessi di repository
gb.userPermissions = permessi di utente
gb.teamPermissions = permessi di gruppo
gb.add = aggiungi
gb.noPermission = CANCELLA QUESTO PERMESSO
gb.excludePermission = {0} (esclude)
gb.viewPermission = {0} (visualizza)
gb.clonePermission = {0} (clone)
gb.pushPermission = {0} (push)
gb.createPermission = {0} (push, crea refs)
gb.deletePermission = {0} (push, crea e cancella refs)
gb.rewindPermission = {0} (push, crea cancella e riavvolge ref)
gb.permission = permesso
gb.regexPermission = questo permesso è assegnato tramite espressione regolare \"{0}\"
gb.accessDenied = accesso negato
gb.busyCollectingGarbage = Gitblit sta eseguendo manutenzione su {0}
gb.gcPeriod = Intervallo di GC
gb.gcPeriodDescription = Intervallo di tempo tra un GC e il successivo
gb.gcThreshold = soglia di GC
gb.gcThresholdDescription = soglia minima di oggetti eliminabili che innesca un GC (garbage collection) anticipato
gb.ownerPermission = proprietario del repository
gb.administrator = amministratore
gb.administratorPermission = amministratore di Gitblit
gb.team = gruppo
gb.teamPermission = permesso ottenuto attraverso l'appartenenza al gruppo \"{0}\"
gb.missing = mancante!
gb.missingPermission = il repository per questo permesso è mancante!
gb.mutable = mutabile
gb.specified = specificato
gb.effective = effettivo
gb.organizationalUnit = unità organizzativa
gb.organization = organizzazione
gb.locality = località
gb.stateProvince = stato o provincia
gb.countryCode = codice nazione
gb.properties = proprietà
gb.issued = segnalato
gb.expires = scade
gb.expired = scaduto
gb.expiring = in scadenza
gb.revoked = revocato
gb.serialNumber = numero di serie
gb.certificates = certificati
gb.newCertificate = nuovo certificato
gb.revokeCertificate = revoca certificato
gb.sendEmail = spedisci email
gb.passwordHint = domanda di sicurezza
gb.ok = ok
gb.invalidExpirationDate = data di scadenza invalida!
gb.passwordHintRequired = la domanda di sicurezza è obbligatoria!
gb.viewCertificate = vedi certificato
gb.subject = soggetto
gb.issuer = emesso da
gb.validFrom = valid dal
gb.validUntil = valido fino al
gb.publicKey = chiave pubblica
gb.signatureAlgorithm = algoritml di firma
gb.sha1FingerPrint = firma SHA-1
gb.md5FingerPrint = firma MD5
gb.reason = motivazione
gb.revokeCertificateReason = Per favore selezionare una motivazione per la revoca del certificato
gb.unspecified = non specificata
gb.keyCompromise = chiave compromessa
gb.caCompromise = CA compromessa
gb.affiliationChanged = cambio di affiliazione
gb.superseded = obsoleto
gb.cessationOfOperation = cessata attività
gb.privilegeWithdrawn = privilegio rimosso
gb.time.inMinutes = in {0} minuti
gb.time.inHours = in {0} ore
gb.time.inDays = in {0} giorni
gb.hostname = nome host
gb.hostnameRequired = Per favore specificare un nome host
gb.newSSLCertificate = nuovo certificato SSL per il server
gb.newCertificateDefaults = nuovi valori predefiniti del certificato
gb.duration = durata
gb.certificateRevoked = Il certificato {0,number,0} è stato revocato
gb.clientCertificateGenerated = Nuovo certificato client generato con successo per {0}
gb.sslCertificateGenerated = Nuovo certificato SSL server generato con successo per {0}
gb.newClientCertificateMessage = NOTA:\nLa 'password' non è la password dell'utente, bensì la password per il portachiavi (keystore). Questa password non è salvata, perciò è necessario specificare una domanda di sicurezza che sarà inclusa nelle istruzioni per l'utente (README)
gb.certificate = certificato
gb.emailCertificateBundle = invia in email il pacchetto del certificato
gb.pleaseGenerateClientCertificate = Per favore generare un certificato client per {0}
gb.clientCertificateBundleSent = Il pacchetto contente il certificato client per {0} è stato spedito
gb.enterKeystorePassword = Per favore digita la password per il portachiavi di Gitblit (keystore)
gb.warning = attenzione
gb.jceWarning = Il runtime Java in use non contiene i files \"JCE Unlimited Strength Jurisdiction Policy\".\nQuesto limita la lunghezza della password del portachiavi a 7 caratteri.\nI file mancanti possono essere scaricati dal sito Oracle.\n\nVuoi continuare e generare il certificato comunque?\n\nSe rispondi No il tuo browser verrà redirezionato alla pagina di download di Oracle.
gb.maxActivityCommits = numero di commit nella pagina attività
gb.maxActivityCommitsDescription = numero massimo di commit da visualizzare nella pagina attività
gb.noMaximum = nessun limite
gb.attributes = attributi
gb.serveCertificate = utilizza questo certificato per HTTPS
gb.sslCertificateGeneratedRestart = Certificato SSL server generato con successo per {0}.\nE' necessario riavviare Gitblit per utilizzare il nuovo certificato.\n\nSe l'applicazione à avviata con il parametro '--alias', sarà necesario impostare tale parametro a ''--alias {0}''.
gb.validity = validità
gb.siteName = nome del sito
gb.siteNameDescription = nome breve del server
gb.excludeFromActivity = escludi dalla pagina riassuntiva delle attività
gb.isSparkleshared = il repository si trova una cartella Sparkleshare
gb.owners = proprietari
gb.sessionEnded = La sessione è terminata
gb.closeBrowser = Chiudere il browser per terminare correttamente la sessione.
gb.doesNotExistInTree = {0} non esiste nell'albero {1}
gb.enableIncrementalPushTags = abilita push incrementale di tag
gb.useIncrementalPushTagsDescription = A seguito di un push, crea automaticamente una etichetta in cima ad ogni ramificazione utilizzando un numero incrementale
gb.incrementalPushTagMessage = Nuova auto-etichetta su ramificazione [{0}] a seguito di push
gb.externalPermissions = {0} i permessi di accesso sono gestiti su un sistema esterno
gb.viewAccess = Non hai accesso in lettura o scrittura su Gitblit
gb.overview = riassunto
gb.dashboard = cruscotto
gb.monthlyActivity = attività mensile
gb.myProfile = il mio profilo
gb.compare = confronta
gb.manual = manuale
gb.from = da
gb.to = a
gb.at = al
gb.of = di
gb.in = in
gb.moreChanges = tutti i cambiamenti...
gb.pushedNCommitsTo = ha effettuato il push di {0} nuovi commit
gb.pushedOneCommitTo = ha effettuato il push di 1 nuovo commit
gb.commitsTo = {0} crea commits su
gb.oneCommitTo = 1 commit su
gb.byNAuthors = da {0} authori
gb.byOneAuthor = da {0}
gb.viewComparison = confronta qeusti {0} commit \u00bb
gb.nMoreCommits = {0} altri commit \u00bb
gb.oneMoreCommit = 1 altro commit \u00bb
gb.pushedNewTag = ha effettuato il push di una nuova tag
gb.createdNewTag = ha creato una nuova tag
gb.deletedTag = ha cancellato una tag
gb.pushedNewBranch = ha effettuato il push di una ramificazione
gb.createdNewBranch = ha creato una nuova ramificazione
gb.deletedBranch = ha cancellato una ramificazione
gb.createdNewPullRequest = ha creato una richiesta di pull
gb.mergedPullRequest = ha eseguito il merge di una richiesta di pull
gb.rewind = RIAVVOLGERE
gb.star = aggiungi stella
gb.unstar = togli stella
gb.stargazers = astronomi
gb.starredRepositories = repository stellati
gb.failedToUpdateUser = Aggiornamento utente fallito!
gb.myRepositories = I miei repository
gb.noActivity = nessuna attività negli ultimi {0} giorni
gb.findSomeRepositories = trova repository
gb.metricAuthorExclusions = Esclusione autori dalle metriche
gb.myDashboard = il mio cruscotto
gb.failedToFindAccount = utente non trovato ''{0}''
gb.reflog = reflog
gb.active = attivo
gb.starred = stellato
gb.owned = posseduto
gb.starredAndOwned = stellato e posseduto
gb.reviewPatchset = revisione di {0} insiemi di patch {1}
gb.todaysActivityStats = oggi / {1} commit da {2} autori
gb.todaysActivityNone = oggi / nessuno
gb.noActivityToday = nessuna attività oggi
gb.anonymousUser= anonimo
gb.commitMessageRenderer = visualizzatore messaggio di commit
gb.diffStat = {0} aggiunte & {1} cancellazioni
gb.home = home
gb.isMirror = questo repository è uno specchio
gb.mirrorOf = specchio di {0}
gb.mirrorWarning = questo repository è uno specchio e non ammette push
gb.docsWelcome1 = Puoi usare file per documentare il tuo repository
gb.docsWelcome2 = Crea un file README.md o HOME.md per cominciare.
gb.createReadme = crea un README
gb.responsible = responsabile
gb.createdThisTicket = ha creato questo ticket
gb.proposedThisChange = ha proposto questo cambiamento
gb.uploadedPatchsetN = ha fatto l'upload di una patch {0}
gb.uploadedPatchsetNRevisionN = ha aggiornato la patch {0} alla revisione {1}
gb.mergedPatchset = ha effettuato il merge di una patch
gb.commented = ha commentato
gb.noDescriptionGiven = nessuna descrizione
gb.toBranch = a {0}
gb.createdBy = creato da
gb.oneParticipant = {0} partecipante
gb.nParticipants = {0} partecipanti
gb.noComments = nessun comment
gb.oneComment = {0} commento
gb.nComments = {0} commenti
gb.oneAttachment  = {0} allegato
gb.nAttachments = {0} allegati
gb.milestone = milestone
gb.compareToMergeBase = confronta con la base di merge
gb.compareToN = confronta con {0}
gb.open = aperto
gb.closed = chiuso
gb.merged = merge effettuato
gb.ticketPatchset = ticket {0}, patch {1}
gb.patchsetMergeable = Questa patch può essere sottoposta a merge automatico con {0}.
gb.patchsetMergeableMore = Questa patch può anche sottoposta a merge con {0} dalla riga di comando.
gb.patchsetAlreadyMerged = Questa patch è stata sottoposta a merge con {0}.
gb.patchsetNotMergeable = Non è possibile effettuare il merge automatico di questa patch con {0}.
gb.patchsetNotMergeableMore = Questa patch necessita di rebase o merge manuale con {0} per risolvere i conflitti.
gb.patchsetNotApproved = La revisione di questa patch non à stata approvata per il merge con {0}.
gb.patchsetNotApprovedMore = Questa patch richiede approvazione da parte di un revisore.
gb.patchsetVetoedMore = Un revisore ha rifiutato questa patch.
gb.write = scrittura
gb.comment = commento
gb.preview = anteprima
gb.leaveComment = lascia un commento...
gb.showHideDetails = mostra/nascondi dettagli
gb.acceptNewPatchsets = accetta patch
gb.acceptNewPatchsetsDescription = accetta patch per questo repository
gb.acceptNewTickets = consenti nuovi ticket
gb.acceptNewTicketsDescription = consente la creazione di ticket per difetti, migliorie, attività, etc
gb.requireApproval = approvazione richiesta
gb.requireApprovalDescription = le patch devono essere approvate affinchè il bottone merge venga abilitato
gb.topic = argomento
gb.proposalTickets = cambiamenti proposti
gb.bugTickets = difetti
gb.enhancementTickets = migliorie
gb.taskTickets = attività
gb.questionTickets = domande
gb.requestTickets = migliorie e attività
gb.yourCreatedTickets = create da te
gb.yourWatchedTickets = osservate da te
gb.mentionsMeTickets = in cui sei menzionato
gb.updatedBy = aggiornate da
gb.sort = ordina
gb.sortNewest = più nuovi
gb.sortOldest = più vecchi
gb.sortMostRecentlyUpdated = aggiornati più di recente
gb.sortLeastRecentlyUpdated = aggiornati meno di recente
gb.sortMostComments = più commenti
gb.sortLeastComments = meno commenti
gb.sortMostPatchsetRevisions = patch con più revisioni
gb.sortLeastPatchsetRevisions = patch con meno revisioni
gb.sortMostVotes = più votati
gb.sortLeastVotes = meno votati
gb.topicsAndLabels = argomenti ed etichette
gb.milestones = milestone
gb.noMilestoneSelected = nessuna milestone selezionata
gb.notSpecified = non specificato
gb.due = entro
gb.queries = interrogazioni
gb.searchTicketsTooltip = ricerca {0} ticket
gb.searchTickets = ricerca ticket
gb.new = nuovo
gb.newTicket = nuovo ticket
gb.editTicket = modifica ticket
gb.ticketsWelcome = Puoi usare i ticket per creare una lista di cose da fare, discutere i difetti o collaborare su patch.
gb.createFirstTicket = Crea il tuo primo ticket
gb.title = Titolo
gb.changedStatus = ha cambibato lo stato
gb.discussion = discussione
gb.updated = aggiornato
gb.proposePatchset = proponi una patch
gb.proposePatchsetNote = Sei il benvenuto nel proporre una patch per questo ticket.
gb.proposeInstructions = Per iniziare, crea una patch e fanne l'upload con Git. Gitblit collegerà la tua patch a questo ticket per id.
gb.proposeWith = Proponi una  patch con {0}
gb.revisionHistory = cronologia revisioni
gb.merge = merge
gb.action = azione
gb.patchset = patch
gb.all = all
gb.mergeBase = base di merge
gb.checkout = checkout
gb.checkoutViaCommandLine =  Checkout da riga di comando
gb.checkoutViaCommandLineNote = Puoi effettuare il checkout e testare questi cambiamenti localmente tramite il tuo clone di questo repository.
gb.checkoutStep1 = Esegui il fetch della patch corrente \u2014 esegui questo dalla tua cartella di progetto
gb.checkoutStep2 = Esegui il checkout della patch in una nuova ramificazione e revisione
gb.mergingViaCommandLine = Esecuzione del merge via command line
gb.mergingViaCommandLineNote = Se non vuoi utilizzare il bottone di merge, oppure il merge automatico non è possibile, puoi eseguire il merge manuale da riga di comando.
gb.mergeStep1 = Esegui il checkout di un nuovo branch per esaminare i cambiamenti \u2014 esegui questo dalla tua cartella di progetto
gb.mergeStep2 = Apporta i cambiamenti proposti ed esegui la revisione
gb.mergeStep3 = Effettua il merge dei cambiamenti proposti e poi aggiorna il server
gb.download = download
gb.ptDescription = strumento di Gitblit per le patch
gb.ptCheckout = Esegui il fetch e il checkout della patch corrente in una nuova ramificazione per la revisione
gb.ptMerge = Esegui il fetch e il checkout della patch corrente in una tua ramificazione locale
gb.ptDescription1 = Barnum è un accessorio a riga di comando per Git che semplifica la sintassi per lavorare con i ticket Gitblit e le patch
gb.ptSimplifiedCollaboration = sintassi semplificata di collaborazione
gb.ptSimplifiedMerge = sintassi semplificata di merge
gb.ptDescription2 = Barnum richiede Python 3 e il client Git nativo. Funziona su Windows, Linux, and Mac OS X.
gb.stepN = Passo {0}
gb.watchers = osservatori
gb.votes = voti
gb.vote = vota per questo {0}
gb.watch = osserva questo {0}
gb.removeVote = annulla voto
gb.stopWatching = non osservare più
gb.watching = osservati
gb.comments = commenti
gb.addComment = aggiungi un commento
gb.export = esporta
gb.oneCommit = un commit
gb.nCommits = {0} commit
gb.addedOneCommit = aggiunto 1 commit
gb.addedNCommits = aggiunti {0} commit
gb.commitsInPatchsetN = commit nella pactch {0}
gb.patchsetN = patch {0}
gb.reviewedPatchsetRev = patch analizzata {0} revisione {1}: {2}
gb.review = revisione
gb.reviews = revisioni
gb.veto = veto
gb.needsImprovement = da migliorare
gb.looksGood = sembra buona
gb.approve = approvata
gb.hasNotReviewed = non analizzata
gb.about = a proposito di
gb.ticketN = ticket #{0}
gb.disableUser = disabilita utente
gb.disableUserDescription = blocca l'utente impedendone l'autenticazione
gb.any = qualsiasi
gb.milestoneProgress = {0} aperti, {1} chiusi
gb.nOpenTickets = {0} aperti
gb.nClosedTickets = {0} chiusi
gb.nTotalTickets = {0} totali
gb.body = corpo
gb.mergeSha = SHA del merge
gb.mergeTo = merge con
gb.labels = etichette
gb.reviewers = revisori
gb.voters = votanti
gb.mentions = menzioni
gb.canNotProposePatchset = patch non accettate
gb.repositoryIsMirror = Questo repository è uno specchio di sola lettura.
gb.repositoryIsFrozen = Questo repository è congelato.
gb.repositoryDoesNotAcceptPatchsets = Questo repository non ammette patch.
gb.serverDoesNotAcceptPatchsets = Questo server non ammette patch.
gb.ticketIsClosed = Questo ticket è chiuso.
gb.mergeToDescription = ramificazione di integrazione per il merge di patch per ticket
gb.anonymousCanNotPropose = Utenti anonimi non possono proporre patch.
gb.youDoNotHaveClonePermission = Non hai i diritti per effettuare il clone di questo repository.
gb.myTickets = i miei ticket
gb.yourAssignedTickets = assegnati a te
gb.newMilestone = nuova milestone
gb.editMilestone = modifica milestone
gb.deleteMilestone = Cancella milestone \"{0}\"?
gb.milestoneDeleteFailed = Cancellazione di milestone fallita ''{0}''!
gb.notifyChangedOpenTickets = invia notifiche per cambiamenti su ticket aperti
gb.overdue = oltre il termine
gb.openMilestones = milestone aperte
gb.closedMilestones = milestone chiuse
gb.administration = amministrazione
gb.plugins = plugins
gb.extensions = estensioni
gb.pleaseSelectProject = Per favore seleziona il progetto!
gb.accessPolicy = Politica di accesso
gb.accessPolicyDescription = Scegli una politica di accesso per controllare la visibilità del repository e i permessi git.
gb.anonymousPolicy = Visualizzazione, Clone & Push anonimi
gb.anonymousPolicyDescription = Chiunque può visualizzare, clonare ed fare push su questo repository.
gb.authenticatedPushPolicy = Push ristretto (utenti autenticati)
gb.authenticatedPushPolicyDescription = Chiunque può visualizzare e clonare questo repository. Tutti gli utenti autenticati hanno diritto a push.
gb.namedPushPolicy = Push ristretto (utenti specifici)
gb.namedPushPolicyDescription = Chiunque può visualizzare e clonare questo repository. Solo gli utenti selezioni hanno diritto al push.
gb.clonePolicy = Clone & Push ristretti
gb.clonePolicyDescription = Chiunque può visualizzare questo repository. I permessi di clone e push vanno definiti.
gb.viewPolicy  = Visualizzazione, Clone, & Push ristretti
gb.viewPolicyDescription = Vanno definiti i permessi per stabilire chi può visualizzare, clonare ed effettuare il push su questo repository.
gb.initialCommit = Primo commit
gb.initialCommitDescription = Questa funzionalità ti consente di clonare questo repository immediatamente. Salta questo passo se hai già un repository locale inizializzato.
gb.initWithReadme = Includi un README
gb.initWithReadmeDescription = Questa funzionalità genera semplice README per il tuo repository
gb.initWithGitignore = Includi un file .gitignore
gb.initWithGitignoreDescription = Questa funzionalità aggiunge un file di configurazione che istruisce i client Git ad ignorare file o cartelle che corrispondono a determinati criteri di selezione.
gb.pleaseSelectGitIgnore = Scegli un file .gitignore
gb.receive = ricevi
gb.permissions = permessi
gb.ownersDescription = Tutti i proprietari possono gestire tutti le impostazioni del repository ma non è concessa la rinomina del repository tranne nel caso di repository personale.
gb.userPermissionsDescription = E' possibile configurare permessi individuali. Queste impostazioni sovrascrivono quelle ereditate dal gruppo o da associazione tramite espresisone regolare.
gb.teamPermissionsDescription = E' possibile specificare permessi di gruppo. Queste impostazioni sovrascrivono quelle ereditate da associazione tramite espressione regolare.
gb.ticketSettings = Impostazioni Ticket
gb.receiveSettings = Ricezione ticket
gb.receiveSettingsDescription = Le impostazioni di ricezione governano i push al repository
gb.preReceiveDescription = Gli hook di pre-receive sono eseguiti dopo aver ricevuto i commit ma <em>PRIMA</em> che i ref vengano aggiornati.<p>Questo è l'hook da usare per rigettare un push</p>
gb.postReceiveDescription = Gli hook di post-receive sono eseguiti dopo aver ricevuto i commit e <em>DOPO</em> che i ref sono stati aggiornati.<p>Questo è l'hook da usare per inviare notifiche, attivare un sistema di build ecc.</p>
gb.federationStrategyDescription = Stabilisce se e come federare questo repository con un altra istanza Gitblit.
gb.federationSetsDescription = Questo repository sarà incluso nelle federazioni selezionate.
gb.miscellaneous = miscellanea
gb.originDescription = URL da cui questo repository è stato clonato
gb.gc = GC
gb.garbageCollection = Garbage Collection
gb.garbageCollectionDescription = Lo spazzino (GC) comprimerà gli oggetti inviati da client Git and rimuoverà dal repository oggetti non più referenziati.
gb.commitMessageRendererDescription = I messaggi di commit possono essere visualizzati come testo semplice o decorati come markdown
gb.preferences = impostazioni
gb.accountPreferences = Impostazione Utente
gb.accountPreferencesDescription = Imposta le preferenze del tuo utente
gb.languagePreference = Impostazioni di lingua
gb.languagePreferenceDescription = Scegli la tua lingua di traduzione preferita per Gitblit
gb.emailMeOnMyTicketChanges = Inviami una email quando uno dei miei ticket è modificato
gb.emailMeOnMyTicketChangesDescription  = Inviami una email quando io stesso modifico un ticket
gb.displayNameDescription = Nome visualizzato
gb.emailAddressDescription = Indirizzo email principale per la ricezione delle notifiche
gb.sshKeys = Chiavi SSH
gb.sshKeysDescription = Autenticazione tramite chiave pubblica SSH è una alternativa sicura all'autenticazione tramite password
gb.addSshKey = Aggiungi una chiave SSH
gb.key = Chiave
gb.comment = Commento
gb.sshKeyCommentDescription = Aggiungi opzionalmente un commento. Se vuoto, il commento sarà estratto dalla chiave stessa.
gb.permission = Permesso
gb.sshKeyPermissionDescription = Definisci il il livello di accesso per la chiave SSH
gb.transportPreference = Preferenze di trasporto
gb.transportPreferenceDescription = Specifica il protocollo di trasporto che preferisci usare per le operazioni di clone
src/main/java/com/gitblit/wicket/GitBlitWebApp_ko.properties
@@ -672,3 +672,73 @@
gb.mergeToDescription = \ud2f0\ucf13 \ud328\uce58\uc14b\uc744 \uba38\uc9c0\ud560 \uae30\ubcf8 \ud1b5\ud569 \ube0c\ub79c\uce58
gb.anonymousCanNotPropose = \uc775\uba85 \uc0ac\uc6a9\uc790\ub294 \ud328\uce58\uc14b\uc744 \uc81c\uc548\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.
gb.youDoNotHaveClonePermission = \ub2f9\uc2e0\uc740 \uc774 \uc800\uc7a5\uc18c\ub97c \ud074\ub860\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.
gb.myTickets = \ub0b4 \ud2f0\ucf13
gb.yourAssignedTickets = \ub098\uc5d0\uac8c \ud560\ub2f9\ub41c
gb.newMilestone = \uc0c8 \ub9c8\uc77c\uc2a4\ud1a4
gb.editMilestone = \ub9c8\uc77c\uc2a4\ud1a4 \uc218\uc815
gb.deleteMilestone = \ub9c8\uc77c\uc2a4\ud1a4 \"{0}\"\uc744(\ub97c) \uc0ad\uc81c\ud560\uae4c\uc694?
gb.milestoneDeleteFailed = \ub9c8\uc77c\uc2a4\ud1a4 ''{0}'' \uc0ad\uc81c \uc2e4\ud328!
gb.notifyChangedOpenTickets = \uc5f0 \ud2f0\ucf13\uc758 \ubcc0\uacbd \uc54c\ub9bc \uc804\uc1a1
gb.overdue = \uc9c0\uc5f0
gb.openMilestones = \ub9c8\uc77c\uc2a4\ud1a4 \uc5f4\uae30
gb.closedMilestones = \ub2eb\ud78c \ub9c8\uc77c\uc2a4\ud1a4
gb.administration = \uad00\ub9ac
gb.plugins = \ud50c\ub7ec\uadf8\uc778
gb.extensions = \ud655\uc7a5\uae30\ub2a5
gb.pleaseSelectProject = \ud504\ub85c\uc81d\ud2b8\ub97c \uc120\ud0dd\ud574 \uc8fc\uc138\uc694!
gb.accessPolicy = \uc811\uadfc \uc815\ucc45
gb.accessPolicyDescription = \uc800\uc7a5\uc18c \ubcf4\uae30\uc640 git \uad8c\ud55c\uc744 \uc81c\uc5b4\ud558\uae30 \uc704\ud574 \uc811\uadfc \uc815\ucc45\uc744 \uc120\ud0dd\ud558\uc138\uc694.
gb.anonymousPolicy = \uc775\uba85 \ubcf4\uae30, \ud074\ub860 \uadf8\ub9ac\uace0 \ud478\uc2dc
gb.anonymousPolicyDescription = \ub204\uad6c\ub098 \uc774 \uc800\uc7a5\uc18c\ub97c \ubcf4\uae30, \ud074\ub860, \uadf8\ub9ac\uace0 \ud478\uc2dc\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.
gb.authenticatedPushPolicy = \uc81c\ud55c\ub41c \ud478\uc2dc (\uc778\uc99d\ub41c)
gb.authenticatedPushPolicyDescription = \ub204\uad6c\ub098 \uc774 \uc800\uc7a5\uc18c\ub97c \ubcf4\uac70\ub098 \ud074\ub860\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \ubaa8\ub4e0 \uc778\uc99d\ub41c \uc720\uc800\ub294 RW+ \ud478\uc2dc \uad8c\ud55c\uc744 \uac00\uc9d1\ub2c8\ub2e4.
gb.namedPushPolicy = \uc774\ub984\uc73c\ub85c \ud478\uc2dc \uc81c\ud55c
gb.namedPushPolicyDescription = \ub204\uad6c\ub098 \uc774 \uc800\uc7a5\uc18c\ub97c \ubcf4\uac70\ub098 \ud074\ub860\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uc120\ud0dd\ud55c \uc720\uc800\ub9cc \ud478\uc2dc\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.
gb.clonePolicy = \uc81c\ud55c\ub41c \ud074\ub860 & \ud478\uc2dc
gb.clonePolicyDescription = \ub204\uad6c\ub098 \uc774 \uc800\uc7a5\uc18c\ub97c \ubcfc \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uc120\ud0dd\ud55c \uc720\uc800\ub9cc \ud074\ub860\uacfc \ud478\uc2dc\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.
gb.viewPolicy  = \uc81c\ud55c\ub41c \ubcf4\uae30, \ud074\ub860 & \ud478\uc2dc
gb.viewPolicyDescription = \uc120\ud0dd\ud55c \uc720\uc800\ub9cc \uc774 \uc800\uc7a5\uc18c\uc5d0 \ub300\ud574 \ubcf4\uae30, \ud074\ub860 \uadf8\ub9ac\uace0 \ud478\uc2dc \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.
gb.initialCommit = \ucd5c\ucd08 \ucee4\ubc0b
gb.initialCommitDescription = \uc774 \uc800\uc7a5\uc18c\ub97c \uc989\uc2dc <code>git clone</code> \ud560 \uc218 \uc788\ub3c4\ub85d \ud569\ub2c8\ub2e4. \ub85c\uceec\uc5d0\uc11c <code>git init</code> \ud588\ub2e4\uba74 \uc774 \ub2e8\uacc4\ub97c \uac74\ub108\ub6f0\uc138\uc694.
gb.initWithReadme = README \ud3ec\ud568
gb.initWithReadmeDescription = \uc800\uc7a5\uc18c\uc758 \uac04\ub2e8\ud55c README \ubb38\uc11c\ub97c \uc0dd\uc131\ud569\ub2c8\ub2e4.
gb.initWithGitignore = .gitignore \ud30c\uc77c \ud3ec\ud568
gb.initWithGitignoreDescription = Git \ud074\ub77c\uc774\uc5b8\ud2b8\uac00 \uc815\uc758\ub41c \ud328\ud134\uc5d0 \ub530\ub77c \ud30c\uc77c\uc774\ub098 \ub514\ub809\ud1a0\ub9ac\ub97c \ubb34\uc2dc\ud558\ub3c4\ub85d \uc9c0\uc815\ud55c \uc124\uc815\ud30c\uc77c\uc744 \ucd94\uac00\ud569\ub2c8\ub2e4.
gb.pleaseSelectGitIgnore = .gitignore \ud30c\uc77c\uc744 \uc120\ud0dd\ud558\uc138\uc694.
gb.receive = \uc218\uc2e0
gb.permissions = \uad8c\ud55c
gb.ownersDescription = \uc18c\uc720\uc790\ub294 \uc800\uc7a5\uc18c\uc758 \ubaa8\ub4e0 \uc124\uc815\uc744 \uad00\ub9ac\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uadf8\ub7ec\ub098, \uac1c\uc778 \uc800\uc7a5\uc18c\ub97c \uc81c\uc678\ud558\uace0\ub294 \uc800\uc7a5\uc18c \uc774\ub984\uc744 \ubcc0\uacbd\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.
gb.userPermissionsDescription = \uac1c\ubcc4 \uc0ac\uc6a9\uc790 \uad8c\ud55c\uc744 \uc9c0\uc815\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uc774 \uc124\uc815\uc740 \ud300\uc774\ub098 \uc815\uaddc\uc2dd \uad8c\ud55c\uc744 \ubb34\uc2dc\ud569\ub2c8\ub2e4.
gb.teamPermissionsDescription = \uac1c\ubcc4 \ud300 \uad8c\ud55c\uc744 \uc9c0\uc815\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uc774 \uc124\uc815\uc740 \uc815\uaddc\uc2dd \uad8c\ud55c\uc744 \ubb34\uc2dc\ud569\ub2c8\ub2e4.
gb.ticketSettings = \ud2f0\ucf13 \uc124\uc815
gb.receiveSettings = \uc218\uc2e0 \uc124\uc815
gb.receiveSettingsDescription = \uc218\uc2e0 \uc124\uc815\uc740 \uc800\uc7a5\uc18c\uc5d0 \ud478\uc2dc\ud558\ub294 \uac83\uc744 \uc81c\uc5b4\ud569\ub2c8\ub2e4.
gb.preReceiveDescription = Pre-receive \ud6c5\uc740 \ucee4\ubc0b\uc744 \uc218\uc2e0\ud588\uc9c0\ub9cc, refs \uac00 \uc5c5\ub370\uc774\ud2b8 \ub418\uae30 <em>\uc804</em> \uc5d0 \uc2e4\ud589\ub429\ub2c8\ub2e4.<p>\uc774\uac83\uc740 \ud478\uc2dc\ub97c \uac70\ubd80\ud558\uae30\uc5d0 \uc801\uc808\ud55c \ud6c5 \uc785\ub2c8\ub2e4.</p>
gb.postReceiveDescription = Post-receive \ud639\uc740 \ucee4\ubc0b\uc744 \uc218\uc2e0\ud558\uace0, refs \uac00 \uc5c5\ub370\uc774\ud2b8 \ub41c <em>\ud6c4</em> \uc5d0 \uc2e4\ud589\ub429\ub2c8\ub2e4.<p>\uc774\uac83\uc740 \uc54c\ub9bc, \ube4c\ub4dc \ud2b8\ub9ac\uac70 \ub4f1\uc744 \ud558\uae30\uc5d0 \uc801\uc808\ud55c \ud6c5 \uc785\ub2c8\ub2e4.</p>
gb.federationStrategyDescription = \ub2e4\ub978 Gitblit \uacfc \ud398\ub354\ub808\uc774\uc158 \ud558\ub294 \ubc29\ubc95\uc744 \uc81c\uc5b4\ud569\ub2c8\ub2e4.
gb.federationSetsDescription = \uc774 \uc800\uc7a5\uc18c\ub294 \uc120\ud0dd\ub41c \ud398\ub354\ub808\uc774\uc158 \uc14b\uc5d0 \ud3ec\ud568\ub429\ub2c8\ub2e4.
gb.miscellaneous = \uae30\ud0c0
gb.originDescription = \uc774 \uc800\uc7a5\uc18c\uac00 \ud074\ub860\ub41c \uacf3\uc758 url
gb.gc = GC
gb.garbageCollection = \uac00\ube44\uc9c0 \uceec\ub809\uc158
gb.garbageCollectionDescription = \uac00\ube44\uc9c0 \uceec\ub809\ud130\ub294 \ud074\ub77c\uc774\uc5b8\ud2b8\uc5d0\uc11c \ud478\uc2dc\ud55c \ub290\uc2a8\ud55c \uc624\ube0c\uc81d\ud2b8\ub97c \ud328\ud0b9\ud558\uace0, \uc800\uc7a5\uc18c\uc5d0\uc11c \ucc38\uc870\ud558\uc9c0 \uc54a\ub294 \uc624\ube0c\uc81d\ud2b8\ub97c \uc0ad\uc81c\ud569\ub2c8\ub2e4.
gb.commitMessageRendererDescription = \ucee4\ubc0b \uba54\uc2dc\uc9c0\ub294 \ud3c9\ubb38 \ub610\ub294 \ub9c8\ud06c\uc5c5\uc73c\ub85c \ub80c\ub354\ub9c1\ud558\uc5ec \ud45c\uc2dc\ub420 \uc218 \uc788\uc2b5\ub2c8\ub2e4.
gb.preferences = \uc124\uc815
gb.accountPreferences = \uacc4\uc815 \uc124\uc815
gb.accountPreferencesDescription = \uacc4\uc815 \uc124\uc815\uc744 \uc9c0\uc815\ud569\ub2c8\ub2e4.
gb.languagePreference = \uc5b8\uc5b4 \uc124\uc815
gb.languagePreferenceDescription = \uc120\ud638\ud558\ub294 \uc5b8\uc5b4\ub97c \uc120\ud0dd\ud558\uc138\uc694.
gb.emailMeOnMyTicketChanges = \ub0b4 \ud2f0\ucf13\uc774 \ubcc0\uacbd\ub418\uba74 \uc774\uba54\uc77c\ub85c \uc54c\ub9bc
gb.emailMeOnMyTicketChangesDescription  = \ub0b4\uac00 \ub9cc\ub4e0 \ud2f0\ucf13\uc758 \ubcc0\uacbd\ub418\uba74 \ubcc0\uacbd\uc0ac\ud56d\uc744 \ub098\uc758 \uc774\uba54\uc77c\ub85c \uc54c\ub824\uc90c
gb.displayNameDescription = \ud45c\uc2dc\ub420 \uc774\ub984
gb.emailAddressDescription = \uc54c\ub9bc\uc744 \ubc1b\uae30\uc704\ud55c \uc8fc \uc774\uba54\uc77c
gb.sshKeys = SSH \ud0a4
gb.sshKeysDescription = SSH \uacf5\uac1c\ud0a4 \uc778\uc99d\uc740 \ud328\uc2a4\uc6cc\ub4dc \uc778\uc99d\uc744 \ub300\uccb4\ud558\ub294 \uc548\uc804\ud55c \ub300\uc548\uc785\ub2c8\ub2e4.
gb.addSshKey = SSH \ud0a4 \ucd94\uac00
gb.key = \ud0a4
gb.comment = \uc124\uba85
gb.sshKeyCommentDescription = \uc0ac\uc6a9\uc790 \uc120\ud0dd\uc778 \uc124\uba85\uc744 \ucd94\uac00\ud558\uc138\uc694. \ube44\uc6cc \ub450\uba74 \ud0a4 \ub370\uc774\ud130\uc5d0\uc11c \ucd94\ucd9c\ud558\uc5ec \ucc44\uc6cc\uc9d1\ub2c8\ub2e4.
gb.permission = \uad8c\ud55c
gb.sshKeyPermissionDescription = SSH \ud0a4\uc758 \uc811\uc18d \uad8c\ud55c\uc744 \uc9c0\uc815\ud558\uc138\uc694.
gb.transportPreference = \uc804\uc1a1 \uc124\uc815
gb.transportPreferenceDescription = \ud074\ub860\uc2dc \uc0ac\uc6a9\ud560 \uc124\uc815\uc744 \uc9c0\uc815\ud558\uc138\uc694.
src/main/java/com/gitblit/wicket/GitBlitWebApp_nl.properties
@@ -221,8 +221,8 @@
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.authored = geschreven
gb.committed = gecommit
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
@@ -517,7 +517,7 @@
gb.proposedThisChange = stelde deze wijziging voor
gb.uploadedPatchsetN = uploade patchset {0}
gb.uploadedPatchsetNRevisionN = uploade patchset {0} revisie {1}
gb.mergedPatchset = merged patchset
gb.mergedPatchset = gemergede patchset
gb.commented = becommentarieerde
gb.noDescriptionGiven = geen omschrijving gegeven
gb.toBranch = naar {0}
@@ -534,10 +534,10 @@
gb.compareToN = vergelijk met {0}
gb.open = open
gb.closed = gesloten
gb.merged = merged
gb.merged = gemerged
gb.ticketPatchset = ticket {0}, patchset {1}
gb.patchsetMergeable = Deze patchset kan automatisch gemerged worden naar {0}.
gb.patchsetMergeableMore = Deze patchset mag ook gemerged worden naar {0} vanaf de command line.
gb.patchsetMergeableMore = Deze patchset mag ook gemerged worden naar {0} vanaf de commandline.
gb.patchsetAlreadyMerged = Deze patchset is gemerged naar {0}.
gb.patchsetNotMergeable = Deze patchset kan niet automatisch gemerged worden naar {0}.
gb.patchsetNotMergeableMore = Deze patchset moet gerebased of handmatig gemerged worden naar {0} om conflicten op te lossen.
@@ -546,7 +546,7 @@
gb.patchsetVetoedMore = Een reviewer heeft een veto uitgesproken over deze patchset.
gb.write = write
gb.comment = commentaar
gb.preview = preview
gb.preview = voorbeeld
gb.leaveComment = plaats een opmerking...
gb.showHideDetails = toon/verberg details
gb.acceptNewPatchsets = accepteer patchsets
@@ -605,12 +605,12 @@
gb.all = alle
gb.mergeBase = merge base
gb.checkout = checkout
gb.checkoutViaCommandLine =  Checkout via command line
gb.checkoutViaCommandLine =  Checkout via commandline
gb.checkoutViaCommandLineNote = U kunt deze wijzigingen uitchecken en lokaal testen vanuit uw eigen kopie van deze repositorie.
gb.checkoutStep1 = Dowbload the actuele patchset \u2014 run deze vanuit uw eigen projectdirectorie
gb.checkoutStep2 = Check de patchset uit naar een nieuwe branch en review hem
gb.mergingViaCommandLine = Mergen via command line
gb.mergingViaCommandLineNote = Als u de merge knop niet wilt gebruiken of een automatische merge niet kan worden uitgevoerd kunt u een handmatige merge op de command line uitvoeren.
gb.mergingViaCommandLine = Mergen via commandline
gb.mergingViaCommandLineNote = Als u de merge knop niet wilt gebruiken of een automatische merge niet kan worden uitgevoerd kunt u een handmatige merge op de commandline uitvoeren.
gb.mergeStep1 = Check out een nieuwe branch voor het reviewen van wijzigingen \u2014 run deze vanuit uw eigen projectdirectorie
gb.mergeStep2 = Breng de voorgestelde wijzigingen in en review ze
gb.mergeStep3 = Merge de voorgestelde wijzigingen en update de server
@@ -670,3 +670,75 @@
gb.serverDoesNotAcceptPatchsets = Deze server accepteert geen patchsets.
gb.ticketIsClosed = Deze ticket is gesloten.
gb.mergeToDescription = default integratie branch voor het mergen van ticket patchsets
gb.anonymousCanNotPropose = Anonieme gebruikers kunnen geen patchsets voorstellen.
gb.youDoNotHaveClonePermission = U heeft geen rechten voor het clonen van deze repositorie.
ggb.myTickets = mijn tickets
gb.yourAssignedTickets = toegewezen aan mij
gb.newMilestone = nieuwe milestone
gb.editMilestone = wijzig milestone
gb.deleteMilestone = Verwijder milestone \"{0}\"?
gb.milestoneDeleteFailed = Milestone verwijdering ''{0}'' niet gelukt!
gb.notifyChangedOpenTickets = verstuur notificatie voor wijziging open tickets
gb.overdue = te laat
gb.openMilestones = open milestones
gb.closedMilestones = gesloten milestones
gb.administration = administratie
gb.plugins = plugins
gb.extensions = extensies
gb.pleaseSelectProject = Kies aub het project!
gb.accessPolicy = Toegangspolicy
gb.accessPolicyDescription = Kies een toegangspolicy voor het managen van de zichtbaarheid van de repositorie en de git toegangsrechten.
gb.anonymousPolicy = Anoniem View, Clone, & Push
gb.anonymousPolicyDescription = Iedereen mag deze repositorie zien, clonen, en er naartoe pushen.
gb.authenticatedPushPolicy = Beperk Push (Geauthenticeerd)
gb.authenticatedPushPolicyDescription = Iedereen kan deze repositorie zien en clonen. Alle geauthenticeerde gebruikers hebben RW+ push permissie.
gb.namedPushPolicy = Beperk Push (Gebruiker bekend)
gb.namedPushPolicyDescription = Iedereen kan deze repositorie zien en clonen. U bepaalt wie er mag pushen.
gb.clonePolicy = Beperk Clone & Push
gb.clonePolicyDescription = Iedereen kan deze repositorie zien. U bepaalt wie er mag clonen en pushen.
gb.viewPolicy  = Beperk Zien, Clonen en Push
gb.viewPolicyDescription = U bepaalt wie deze repositorie mag zien, clonen en er naar toe pushen.
gb.initialCommit = Initiele Commit
gb.initialCommitDescription = Dit maakt het u mogelijk om meteen <code>git clone</code> deze repositorie. Sla deze stap over als u al <code>git init</code> lokaal heeft uitgevoerd.
gb.initWithReadme = Voeg een README toe
gb.initWithReadmeDescription = Dit zal een eenvoudige README document toevoegen aan uw repositorie.
gb.initWithGitignore = Voeg een .gitignore bestand toe
gb.initWithGitignoreDescription = Dit zal een configuratiebestand toevoegen dat uw Git programma zal instrueren om bepaalde bestanden of directories die aan bepaalde selectiepatronen voldoen te negeren.
gb.pleaseSelectGitIgnore = Kies aub een .gitignore bestand
gb.receive = ontvang
gb.permissions = permissies
gb.ownersDescription = Eigenaren kunnen alle repositoriesettings managen maar het is niet toegestaan een repositorie te hernoemen tenzij het hun persoonlijke repositorie is.
gb.userPermissionsDescription = U kunt afzonderlijke gebruikerspermissies specificeren. Deze instellingen zullen team- of regexpermisies overrulen.
gb.teamPermissionsDescription = U kunt afzonderlijke teamspermissies specificeren. Deze instellingen zullen regexpermisies overrulen.
gb.ticketSettings = Ticket Instellingen
gb.receiveSettings = Ontvangstinstellingen
gb.receiveSettingsDescription = De Ontvangstinstellingen managen pushes naar de repositorie.
gb.preReceiveDescription = Pre-receive hooks worden uitgevoerd na commits zijn ontvangen maar <em>VOORDAT</em> de refs worden geupdated.<p>Dit is de juiste hook voor het afwijzen van een push.</p>
gb.postReceiveDescription = Post-receive hooks worden uitgevoerd na commits zijn ontvangen maar <em>NADAT</em> de refs zijn geupdated.<p>Dit is de juiste hook voor notificaties, build triggers, etc.</p>
gb.federationStrategyDescription = Bepaal of en hoe deze repositorie te federeren met een andere Gitblit.
gb.federationSetsDescription = Deze repository zal worden opgenomen in de geselecteerde federatie sets.
gb.miscellaneous = diversen
gb.originDescription = De url vanaf welke deze repositorie was gecloned.
gb.gc = GC
gb.garbageCollection = Garbage Collection
gb.garbageCollectionDescription = De garbage collector zal losse objecten die gepushed zijn van clients samenvoegen en zal ongereferentieerde objecten uit de repository verwijderen.
gb.commitMessageRendererDescription = Commit meldingen kunnen worden getoond als platte tekst of als gerenderde markup.
gb.preferences = voorkeuren
gb.accountPreferences = Accountvoorkeuren
gb.accountPreferencesDescription = Specificeer uw accountvoorkeuren
gb.languagePreference = Taalvoorkeuren
gb.languagePreferenceDescription = Selecteer uw voorkeursvertaling van voor Gitblit
gb.emailMeOnMyTicketChanges = Email bij ticketwijziging
gb.emailMeOnMyTicketChangesDescription  = Stuur me een emailnotificatie voor wijzigingen die ik aanbreng aan een ticket
gb.displayNameDescription = De gewenste naam om te tonen
gb.emailAddressDescription = Het primaire emailadres voor het ontvangen van notificaties
gb.sshKeys = SSH Sleutels
gb.sshKeysDescription = SSH publiekesleutelauthenticatie is een veilig alternatief voor wachtwoordauthenticatie
gb.addSshKey = Voeg SSH Sleutel toe
gb.key = Sleutel
gb.comment = Opmerking
gb.sshKeyCommentDescription = Voeg een optionele opmerking toe. Indien leeg zal de opmerking uit de sleutelgegevens worden gehaald.
gb.permission = Permissie
gb.sshKeyPermissionDescription = Specificeer de toegangsrechten voor de SSH sleutel
gb.transportPreference = Transportvoorkeuren
gb.transportPreferenceDescription = Stel de transportmethode in die u wenst voor het clonen
src/main/java/com/gitblit/wicket/GitBlitWebApp_no.properties
@@ -100,6 +100,9 @@
gb.commitActivityTrend = aktivitetstrend for commits
gb.commitActivityDOW = commit aktivitet gruppert p\u00e5 ukedag
gb.commitActivityAuthors = prim\u00e6rforfattere etter aktivitet
gb.feed = feed
gb.cancel = avbryt
gb.changePassword = endre passord
gb.isFederated = er federert
gb.federateThis = federer dette repositoriet
gb.federateOrigin = federere origin
@@ -669,3 +672,73 @@
gb.mergeToDescription = standard integration branch for merging av ticket patchsett
gb.anonymousCanNotPropose = Anonyme brukere kan ikke foresl\u00e5 patchsett
gb.youDoNotHaveClonePermission = Du har ikke tillatelse til \u00e5 klone dette repositoriet.
gb.myTickets = mine tickets
gb.yourAssignedTickets = tilordnet deg
gb.newMilestone = ny milep\u00e6l
gb.editMilestone = endre milep\u00e6l
gb.deleteMilestone = Slette  milep\u00e6l \"{0}\"?
gb.milestoneDeleteFailed = Greide ikke å slette milep\u00e6l ''{0}''!
gb.notifyChangedOpenTickets = send meldin om endrede \u00e5pne tickets
gb.overdue = forfalt
gb.openMilestones = \u00e5pne milep\u00e6ler
gb.closedMilestones = lukkede milep\u00e6ler
gb.administration = administrasjon
gb.plugins = plugins
gb.extensions = extensions
gb.pleaseSelectProject = Vennligst velg prosjekt!
gb.accessPolicy = Tilgangspolicy
gb.accessPolicyDescription = Velg en tilgangspolicy for å kontrollere synlighet og tilganger.
gb.anonymousPolicy = Anonym view, clone og push
gb.anonymousPolicyDescription = Alle kan se, clone ofg pushe til dette repositoriet.
gb.authenticatedPushPolicy = Begrens push (Autentisert)
gb.authenticatedPushPolicyDescription = Alle kan se og klone dette repositoriet. Alle autentiserte brukere har lese og skrivetilgang.
gb.namedPushPolicy = Begrens push (navngitt)
gb.namedPushPolicyDescription = Alle kan se og klone repositoriet. Du bestemmer hvem som kan pushe.
gb.clonePolicy = Begrens Clone og Push
gb.clonePolicyDescription = Alle kan se dette repositoriet. Du bestemmer hvem som kan clone og pushe.
gb.viewPolicy  = Begrense View, Clone og Push
gb.viewPolicyDescription = Du bestemmer hvem som kan se, clone og pushe til dette repositoriet.
gb.initialCommit = F\u00F8rste commit
gb.initialCommitDescription = Dette vil la deg kunne <code>git clone</code> repositoriet med en gang. Hopp over dette skrittet hvis du allerede har kj\u0F8rt  if <code>git init</code> lokalt.
gb.initWithReadme = Inkluder en README
gb.initWithReadmeDescription = Dette vil generere en enkel README-fil i ditt repository.
gb.initWithGitignore = Inkluder en .gitignore fil
gb.initWithGitignoreDescription = Dette vil inkludere en konfigurasjonsfil som instruerer git-klienter til å ignorerer filer og kataloger som matcher definerte m\u00F8nstre.
gb.pleaseSelectGitIgnore = Vennligst velg en .gitignore fil
gb.receive = motta
gb.permissions = rettigheter
gb.ownersDescription = Eiere kan administrere alle repositoriets innstillinger men de kan ikke endre p\u00e5 repositoriets navn med mindre det er deres eget repository.
gb.userPermissionsDescription = Du kan spesifisere individuelle brukerrettigheter. Disse innstillingene overstyrer team- eller regul\u00e6ruttrykk-baserte rettigheter.
gb.teamPermissionsDescription = Du kan spesifisere individuelle team-rettigheter. Disse innstillingene overstyrer regul\u00e6ruttrykk-baserte rettigheter..
gb.ticketSettings = Ticket innstillinger
gb.receiveSettings = Innstillinger for mottak av push
gb.receiveSettingsDescription = Kontrollerer pushing til repositoriet.
gb.preReceiveDescription = Pre-receive hooks blir utf\u00F8rt etter en committ er mottatt men <em>F\u00D8</em> refs har blitt oppdatert.<p>Dette er det passende stedet \u00e5 avvise en push.</p>
gb.postReceiveDescription = Post-receive hooks blir utf\u00F8rt etter en committ er mottatt og <em>ETTER</em> refs har blitt oppdatert.<p>Dette er det passende stedet for notifikasjoner, build triggers osv.</p>
gb.federationStrategyDescription = Kontroller om og hvordan dette repositoriet skal federeres med en annen Gitblit instans.
gb.federationSetsDescription = Dette repositoriet vil bli inkludert i de valgte federeringssettene.
gb.miscellaneous = ymse
gb.originDescription = URL'en som dette repositoriet ble klonet fra.
gb.gc = GC
gb.garbageCollection = Garbage Collection
gb.garbageCollectionDescription = Garbage collectoren vil pakke sammen l\u00F8se objekter pushet fra klientene og fjerne objekter det ikke refereres til fra minnet.
gb.commitMessageRendererDescription = Commit-meldinger kan vises som ren tekst, eller som markup.
gb.preferences = innstillinger
gb.accountPreferences = Kontoinnstillinger
gb.accountPreferencesDescription = Velg dine kontoinnstillinger
gb.languagePreference = Foretrukket spr\u00e5k
gb.languagePreferenceDescription = Velg ditt foretrukne spr\u00e5k for Gitblit
gb.emailMeOnMyTicketChanges = Send meg en epost når ticketen min endrer seg
gb.emailMeOnMyTicketChangesDescription  = Send meg en epost for endringer jeg gjør på en ticket.
gb.displayNameDescription = Foretrukket visningsnavn
gb.emailAddressDescription = Prim\u00e6r epostadresse for notifikasjoner
gb.sshKeys = SSH n\u00F8kler
gb.sshKeysDescription = SSH public key autentisering er et sikkert alternativ til autentisering med passord
gb.addSshKey = Legg til SSH n\u00F8kkel
gb.key = N\u00F8kkel
gb.comment = Kommentar
gb.sshKeyCommentDescription = Angi en valgfri kommentar. Kommentaren vil bli ekstrahert fra n\u00F8kkeldataene hvis blank
gb.permission = Tilgang
gb.sshKeyPermissionDescription = Angi tilgangsrettinghet for SSH n\u00F8kkelen
gb.transportPreference = Foretrukket transport
gb.transportPreferenceDescription = Sett transportmetoden du foretrekker for cloning
src/main/java/com/gitblit/wicket/GitBlitWebApp_zh_CN.properties
@@ -669,4 +669,76 @@
gb.repositoryDoesNotAcceptPatchsets = \u5f53\u524d\u7248\u672c\u5e93\u4e0d\u5141\u8bb8\u8865\u4e01\u96c6\u3002
gb.serverDoesNotAcceptPatchsets = \u5f53\u524d\u670d\u52a1\u5668\u4e0d\u5141\u8bb8\u8865\u4e01\u96c6\u3002
gb.ticketIsClosed = \u5f53\u524d\u5de5\u5355\u5df2\u5173\u95ed\u3002
gb.mergeToDescription = \u5408\u5e76\u5de5\u5355\u8865\u4e01\u96c6\u7684\u9ed8\u8ba4\u96c6\u6210\u5206\u652f
gb.mergeToDescription = \u5408\u5e76\u5de5\u5355\u8865\u4e01\u96c6\u7684\u9ed8\u8ba4\u96c6\u6210\u5206\u652f
gb.anonymousCanNotPropose = \u7981\u6b62\u533f\u540d\u7528\u6237\u63d0\u4ea4\u8865\u4e01\u96c6\u3002
gb.youDoNotHaveClonePermission = \u60a8\u6ca1\u6709\u6743\u9650\u514b\u9686\u5f53\u524d\u7248\u672c\u5e93\u3002
gb.myTickets = \u6211\u7684\u5de5\u5355
gb.yourAssignedTickets = \u8d23\u4efb\u5de5\u5355
gb.newMilestone = \u65b0\u5efa milestone
gb.editMilestone = \u7f16\u8f91 milestone
gb.deleteMilestone = \u5220\u9664 milestone \\"{0}\\"?
gb.milestoneDeleteFailed = \u5220\u9664 milestone ''{0}'' \u5931\u8d25!
gb.notifyChangedOpenTickets = \u5bf9\u53d1\u751f\u53d8\u52a8\u7684\u5df2\u5f00\u542f\u5de5\u5355\u53d1\u9001\u901a\u77e5
gb.overdue = \u8fc7\u671f
gb.openMilestones = \u5df2\u5f00\u542f milestones
gb.closedMilestones = \u5df2\u5173\u95ed milestones
gb.administration = \u7ba1\u7406
gb.plugins = \u63d2\u4ef6
gb.extensions = \u6269\u5c55
gb.pleaseSelectProject = \u8bf7\u9009\u62e9\u9879\u76ee!
gb.accessPolicy = \u8bbf\u95ee\u7b56\u7565
gb.accessPolicyDescription = \u8bf7\u9009\u62e9\u4e00\u4e2a\u63a7\u5236\u7248\u672c\u5e93\u53ef\u89c1\u6027\u4ee5\u53caGit\u8bbf\u95ee\u6743\u9650\u7684\u8bbf\u95ee\u7b56\u7565\u3002
gb.anonymousPolicy = \u533f\u540d\u67e5\u770b, \u514b\u9686\u548c\u63a8\u9001
gb.anonymousPolicyDescription = \u4efb\u4f55\u4eba\u90fd\u53ef\u4ee5\u67e5\u770b\uff0c\u514b\u9686\u4ee5\u53ca\u63a8\u9001\u81f3\u6b64\u7248\u672c\u5e93\u3002
gb.authenticatedPushPolicy = \u9650\u5236\u63a8\u9001 (\u6388\u6743\u8bbf\u95ee)
gb.authenticatedPushPolicyDescription = \u4efb\u4f55\u4eba\u90fd\u53ef\u4ee5\u67e5\u770b\u4ee5\u53ca\u514b\u9686\u6b64\u7248\u672c\u5e93\u3002\u4efb\u4f55\u5df2\u6388\u6743\u7528\u6237\u62e5\u6709RW+\u63a8\u9001\u6743\u9650\u3002
gb.namedPushPolicy = \u9650\u5236\u63a8\u9001 (\u6307\u5b9a\u7528\u6237)
gb.namedPushPolicyDescription = \u4efb\u4f55\u4eba\u90fd\u53ef\u4ee5\u67e5\u770b\u4ee5\u53ca\u514b\u9686\u6b64\u7248\u672c\u5e93\u3002 \u60a8\u53ef\u4ee5\u9009\u62e9\u62e5\u6709\u63a8\u9001\u6743\u9650\u7684\u7528\u6237\u3002
gb.clonePolicy = \u9650\u5236\u514b\u9686\uff0c\u63a8\u9001
gb.clonePolicyDescription = \u4efb\u4f55\u4eba\u90fd\u53ef\u4ee5\u770b\u5230\u6b64\u7248\u672c\u5e93\u3002\u60a8\u53ef\u4ee5\u9009\u62e9\u62e5\u6709\u514b\u9686\u548c\u63a8\u9001\u6743\u9650\u7684\u7528\u6237\u3002
gb.viewPolicy = \u9650\u5236\u67e5\u770b\uff0c\u514b\u9686\u548c\u63a8\u9001
gb.viewPolicyDescription = \u60a8\u53ef\u4ee5\u9009\u62e9\u62e5\u6709\u67e5\u770b\uff0c\u514b\u9686\u548c\u63a8\u9001\u6743\u9650\u7684\u7528\u6237\u3002
gb.initialCommit = \u521d\u59cb\u5316\u63d0\u4ea4
gb.initialCommitDescription = \u6b64\u529f\u80fd\u76f8\u5f53\u4e8e\u76f4\u63a5\u5bf9\u5f53\u524d\u7248\u672c\u5e93\u8fdb\u884c <code>git clone</code> \u3002 \u5982\u679c\u8df3\u8fc7\u6b64\u6b65\uff0c\u60a8\u5fc5\u987b\u5728\u672c\u5730\u8fdb\u884c <code>git init</code> \u3002
gb.initWithReadme = \u52a0\u5165 README
gb.initWithReadmeDescription = \u6b64\u529f\u80fd\u4f1a\u81ea\u52a8\u751f\u6210\u4e00\u4e2a\u60a8\u7684\u7248\u672c\u5e93\u7684 README \u6587\u4ef6\u3002
gb.initWithGitignore = \u52a0\u5165 .gitignore \u6587\u4ef6
gb.initWithGitignoreDescription = \u6b64\u529f\u80fd\u4f1a\u751f\u6210\u4e00\u4e2a\u914d\u7f6e\u6587\u4ef6\uff0c\u65e8\u5728\u63d0\u793a Git \u5ba2\u6237\u7aef\u5ffd\u7565\u5bf9\u5e94\u7684\u6587\u4ef6\u6216\u6587\u4ef6\u5939\u3002
gb.pleaseSelectGitIgnore = \u8bf7\u9009\u62e9\u4e00\u4e2a .gitignore \u6587\u4ef6
gb.receive = receive
gb.permissions = permissions
gb.ownersDescription = \u7248\u672c\u5e93\u62e5\u6709\u8005\u62e5\u6709\u7248\u672c\u5e93\u7684\u6240\u6709\u7ba1\u7406\u6743\u9650\uff0c\u4f46\u662f\u53ea\u5141\u8bb8\u4fee\u6539\u79c1\u6709\u7248\u672c\u5e93\u7684\u540d\u79f0\u3002
gb.userPermissionsDescription = \u60a8\u53ef\u4ee5\u8bbe\u7f6e\u79c1\u6709\u7528\u6237\u6743\u9650\u3002 \u6b64\u8bbe\u7f6e\u4f1a\u8986\u76d6\u56e2\u961f\u6743\u9650\u4ee5\u53caregex\u6743\u9650\u3002
gb.teamPermissionsDescription = \u60a8\u53ef\u4ee5\u8bbe\u7f6e\u79c1\u6709\u56e2\u961f\u6743\u9650\u3002 \u6b64\u8bbe\u7f6e\u4f1a\u8986\u76d6regex\u6743\u9650\u3002
gb.ticketSettings = \u5de5\u5355\u8bbe\u7f6e
gb.receiveSettings = Receive \u8bbe\u7f6e
gb.receiveSettingsDescription = Receive\u8bbe\u7f6e\u8bbe\u5b9a\u63a8\u9001\u81f3\u7248\u672c\u5e93\u65f6\u7684\u884c\u4e3a\u3002
gb.preReceiveDescription = Pre-receive hooks \u4f1a\u5728\u63a8\u9001\u63a5\u6536\u540e\uff0c refs \u66f4\u65b0<em>\u4e4b\u524d</em>\u6267\u884c\u3002<p>\u8fd9\u79cdhook\u662f\u8fdb\u884c\u63a8\u9001\u62d2\u7edd\u7684\u597d\u5de5\u5177\u3002</p>
gb.postReceiveDescription = Post-receive hooks \u4f1a\u5728\u63a8\u9001\u63a5\u6536\u540e\uff0c refs \u66f4\u65b0<em>\u4e4b\u540e</em>\u6267\u884c\u3002<p>\u8fd9\u79cdhook\u662f\u8fdb\u884c\u901a\u77e5\uff0c\u4f7f\u7528\u6784\u5efa\u89e6\u53d1\u5668\u7b49\u7684\u597d\u5de5\u5177\u3002</p>
gb.federationStrategyDescription = \u8bbe\u7f6e\u662f\u5426\u4ee5\u53ca\u5982\u4f55\u5c06\u5f53\u524d\u7248\u672c\u5e93\u4e0e\u5176\u4ed6Gitblit\u8fdb\u884cfederate\u3002
gb.federationSetsDescription = \u5f53\u524d\u7248\u672c\u5e93\u5c06\u4f1a\u88ab\u5305\u542b\u8fdb\u9009\u5b9a\u7684federation\u96c6\u4e2d\u3002
gb.miscellaneous = miscellaneous
gb.originDescription = \u5f53\u524d\u7248\u672c\u5e93\u7684\u514b\u9686\u6e90\u5730\u5740\u3002
gb.gc = GC
gb.garbageCollection = \u5783\u573e\u6536\u96c6
gb.garbageCollectionDescription = \u5783\u573e\u6536\u96c6\u5668\u4f1a\u5c06\u5ba2\u6237\u7aef\u6240\u53d1\u9001\u7684\u677e\u6563\u6587\u4ef6\u6253\u5305\u5e76\u5220\u9664\u5f53\u524d\u7248\u672c\u5e93\u4e2d\u672a\u88ab\u5f15\u7528\u7684\u5bf9\u8c61\u3002
gb.commitMessageRendererDescription = \u53ef\u4ee5\u5c06\u63d0\u4ea4\u4fe1\u606f\u663e\u793a\u4e3a\u7eaf\u6587\u672c\u6216\u8005\u5df2\u6392\u7248\u7684Markup\u6587\u672c
gb.preferences = \u504f\u597d
gb.accountPreferences = \u7528\u6237\u504f\u597d
gb.accountPreferencesDescription = \u8bbe\u7f6e\u60a8\u7684\u7528\u6237\u504f\u597d
gb.languagePreference = \u8bed\u8a00\u504f\u597d
gb.languagePreferenceDescription = \u9009\u62e9\u60a8\u559c\u6b22\u7684Gitblit\u7ffb\u8bd1
gb.emailMeOnMyTicketChanges = \u5728\u6211\u7684\u5de5\u5355\u53d1\u751f\u53d8\u5316\u540e\u90ae\u4ef6\u901a\u77e5\u6211
gb.emailMeOnMyTicketChangesDescription = \u5bf9\u6211\u5728\u5de5\u5355\u4e2d\u4f5c\u51fa\u7684\u4fee\u6539\u53d1\u9001\u90ae\u4ef6\u901a\u77e5\u3002
gb.displayNameDescription = \u9009\u62e9\u663e\u793a\u540d\u79f0
gb.emailAddressDescription = \u63a5\u6536\u901a\u77e5\u7684\u4e3b\u8981\u90ae\u7bb1\u5730\u5740
gb.sshKeys = SSH Keys
gb.sshKeysDescription = SSH \u516c\u7ea6\u8ba4\u8bc1\u662f\u4e00\u79cd\u4e0d\u540c\u4e8e\u5bc6\u7801\u8ba4\u8bc1\u7684\u5b89\u5168\u8ba4\u8bc1\u65b9\u6cd5\u3002
gb.addSshKey = \u6dfb\u52a0 SSH Key
gb.key = Key
gb.comment = \u53d1\u8868
gb.sshKeyCommentDescription = \u8f93\u5165\u8bc4\u8bba\uff08\u53ef\u4e3a\u7a7a\uff09. \u5982\u679c\u4e3a\u7a7a\uff0c \u8bc4\u8bba\u5185\u5bb9\u5c06\u4f1a\u4ece\u4e3b\u8981\u6570\u636e\u4e2d\u63d0\u53d6\u3002
gb.permission = \u6743\u9650
gb.sshKeyPermissionDescription = \u8bbe\u7f6eSSH key\u7684\u8bbf\u95ee\u6743\u9650
gb.transportPreference = Transport \u504f\u597d
gb.transportPreferenceDescription = \u9009\u62e9\u60a8\u7528\u6765\u514b\u9686\u7684 Transport
src/main/java/com/gitblit/wicket/GitblitWicketApp.java
New file
@@ -0,0 +1,72 @@
package com.gitblit.wicket;
import java.util.Date;
import java.util.TimeZone;
import org.apache.wicket.markup.html.WebPage;
import com.gitblit.IStoredSettings;
import com.gitblit.manager.IAuthenticationManager;
import com.gitblit.manager.IFederationManager;
import com.gitblit.manager.IGitblit;
import com.gitblit.manager.INotificationManager;
import com.gitblit.manager.IPluginManager;
import com.gitblit.manager.IProjectManager;
import com.gitblit.manager.IRepositoryManager;
import com.gitblit.manager.IRuntimeManager;
import com.gitblit.manager.IUserManager;
import com.gitblit.tickets.ITicketService;
import com.gitblit.transport.ssh.IPublicKeyManager;
public interface GitblitWicketApp {
    public abstract void mount(String location, Class<? extends WebPage> clazz, String... parameters);
    public abstract Class<? extends WebPage> getHomePage();
    public abstract boolean isCacheablePage(String mountPoint);
    public abstract CacheControl getCacheControl(String mountPoint);
    public abstract IStoredSettings settings();
    /**
     * Is Gitblit running in debug mode?
     *
     * @return true if Gitblit is running in debug mode
     */
    public abstract boolean isDebugMode();
    /*
     * These methods look strange... and they are... but they are the first
     * step towards modularization across multiple commits.
     */
    public abstract Date getBootDate();
    public abstract Date getLastActivityDate();
    public abstract IRuntimeManager runtime();
    public abstract IPluginManager plugins();
    public abstract INotificationManager notifier();
    public abstract IUserManager users();
    public abstract IAuthenticationManager authentication();
    public abstract IPublicKeyManager keys();
    public abstract IRepositoryManager repositories();
    public abstract IProjectManager projects();
    public abstract IFederationManager federation();
    public abstract IGitblit gitblit();
    public abstract ITicketService tickets();
    public abstract TimeZone getTimezone();
}
src/main/java/com/gitblit/wicket/MarkupProcessor.java
@@ -56,11 +56,11 @@
import com.gitblit.IStoredSettings;
import com.gitblit.Keys;
import com.gitblit.models.PathModel;
import com.gitblit.servlet.RawServlet;
import com.gitblit.utils.JGitUtils;
import com.gitblit.utils.MarkdownUtils;
import com.gitblit.utils.StringUtils;
import com.gitblit.wicket.pages.DocPage;
import com.gitblit.wicket.pages.RawPage;
import com.google.common.base.Joiner;
/**
@@ -260,7 +260,8 @@
                if (imagePath.indexOf("://") == -1) {
                    // relative image
                    String path = doc.getRelativePath(imagePath);
                    url = getWicketUrl(RawPage.class, repositoryName, commitId, path);
                    String contextUrl = RequestCycle.get().getRequest().getRelativePathPrefixToContextRoot();
                    url = RawServlet.asLink(contextUrl, repositoryName, commitId, path);
                } else {
                    // absolute image
                    url = imagePath;
@@ -312,7 +313,8 @@
                if (node.url.indexOf("://") == -1) {
                    // repository-relative image link
                    String path = doc.getRelativePath(node.url);
                    String url = getWicketUrl(RawPage.class, repositoryName, commitId, path);
                    String contextUrl = RequestCycle.get().getRequest().getRelativePathPrefixToContextRoot();
                    String url = RawServlet.asLink(contextUrl, repositoryName, commitId, path);
                    return new Rendering(url, text);
                }
                // absolute image link
@@ -325,7 +327,8 @@
                if (url.indexOf("://") == -1) {
                    // repository-relative image link
                    String path = doc.getRelativePath(url);
                    String wurl = getWicketUrl(RawPage.class, repositoryName, commitId, path);
                    String contextUrl = RequestCycle.get().getRequest().getRelativePathPrefixToContextRoot();
                    String wurl = RawServlet.asLink(contextUrl, repositoryName, commitId, path);
                    rendering = new Rendering(wurl, alt);
                } else {
                    // absolute image link
src/main/java/com/gitblit/wicket/PageRegistration.java
File was deleted
src/main/java/com/gitblit/wicket/PluginClassResolver.java
New file
@@ -0,0 +1,120 @@
/*
 * Copyright 2014 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.IOException;
import java.net.URL;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.TreeSet;
import org.apache.wicket.WicketRuntimeException;
import org.apache.wicket.application.IClassResolver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ro.fortsoft.pf4j.PluginState;
import ro.fortsoft.pf4j.PluginWrapper;
import com.gitblit.manager.IPluginManager;
/**
 * Resolves plugin classes and resources.
 */
public class PluginClassResolver implements IClassResolver {
    private static final Logger logger = LoggerFactory.getLogger(PluginClassResolver.class);
    private final IClassResolver coreResolver;
    private final IPluginManager pluginManager;
    public PluginClassResolver(IClassResolver coreResolver, IPluginManager pluginManager) {
        this.coreResolver = coreResolver;
        this.pluginManager = pluginManager;
    }
    @Override
    public Class<?> resolveClass(final String className) throws ClassNotFoundException {
        boolean debugEnabled = logger.isDebugEnabled();
        for (PluginWrapper plugin : pluginManager.getPlugins()) {
            if (PluginState.STARTED != plugin.getPluginState()) {
                // ignore this plugin
                continue;
            }
            try {
                return plugin.getPluginClassLoader().loadClass(className);
            } catch (ClassNotFoundException cnfx) {
                if (debugEnabled) {
                    logger.debug("ClassResolver '{}' cannot find class: '{}'", plugin.getPluginId(), className);
                }
            }
        }
        return coreResolver.resolveClass(className);
    }
    @Override
    public Iterator<URL> getResources(final String name) {
        Set<URL> urls = new TreeSet<URL>(new UrlExternalFormComparator());
        for (PluginWrapper plugin : pluginManager.getPlugins()) {
            if (PluginState.STARTED != plugin.getPluginState()) {
                // ignore this plugin
                continue;
            }
            Iterator<URL> it = getResources(name, plugin);
            while (it.hasNext()) {
                URL url = it.next();
                urls.add(url);
            }
        }
        Iterator<URL> it = coreResolver.getResources(name);
        while (it.hasNext()) {
            URL url = it.next();
            urls.add(url);
        }
        return urls.iterator();
    }
    protected Iterator<URL> getResources(String name, PluginWrapper plugin) {
        HashSet<URL> loadedFiles = new HashSet<URL>();
        try {
            // Try the classloader for the wicket jar/bundle
            Enumeration<URL> resources = plugin.getPluginClassLoader().getResources(name);
            loadResources(resources, loadedFiles);
        } catch (IOException e) {
            throw new WicketRuntimeException(e);
        }
        return loadedFiles.iterator();
    }
    private void loadResources(Enumeration<URL> resources, Set<URL> loadedFiles) {
        if (resources != null) {
            while (resources.hasMoreElements()) {
                final URL url = resources.nextElement();
                if (!loadedFiles.contains(url)) {
                    loadedFiles.add(url);
                }
            }
        }
    }
}
src/main/java/com/gitblit/wicket/SessionlessForm.java
@@ -22,6 +22,7 @@
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.RequestUtils;
import org.apache.wicket.protocol.http.WicketURLDecoder;
import org.apache.wicket.protocol.http.request.WebRequestCodingStrategy;
import org.apache.wicket.util.string.AppendingStringBuffer;
@@ -53,9 +54,9 @@
    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;
    protected final Class<? extends BasePage> pageClass;
    private final PageParameters pageParameters;
    protected final PageParameters pageParameters;
    private final Logger log = LoggerFactory.getLogger(SessionlessForm.class);
@@ -145,4 +146,14 @@
        String un = WicketURLDecoder.QUERY_INSTANCE.decode(s);
        return Strings.escapeMarkup(un).toString();
    }
    protected String getAbsoluteUrl() {
        return getAbsoluteUrl(pageClass, pageParameters);
    }
    protected String getAbsoluteUrl(Class<? extends BasePage> pageClass, PageParameters pageParameters) {
        String relativeUrl = urlFor(pageClass, pageParameters).toString();
        String absoluteUrl = RequestUtils.toAbsolutePath(relativeUrl);
        return absoluteUrl;
    }
}
src/main/java/com/gitblit/wicket/TicketsUI.java
New file
@@ -0,0 +1,211 @@
/*
 * Copyright 2014 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.text.MessageFormat;
import org.apache.wicket.markup.html.basic.Label;
import com.gitblit.models.TicketModel;
import com.gitblit.models.TicketModel.Status;
import com.gitblit.models.TicketModel.Type;
import com.gitblit.utils.StringUtils;
/**
 * Common tickets ui methods and classes.
 *
 * @author James Moger
 *
 */
public class TicketsUI {
    public static final String [] openStatii = new String [] { Status.New.name().toLowerCase(), Status.Open.name().toLowerCase() };
    public static final String [] closedStatii = new String [] { "!" + Status.New.name().toLowerCase(), "!" + Status.Open.name().toLowerCase() };
    public static Label getStateIcon(String wicketId, TicketModel ticket) {
        return getStateIcon(wicketId, ticket.type, ticket.status);
    }
    public static Label getStateIcon(String wicketId, Type type, Status state) {
        Label label = new Label(wicketId);
        if (type == null) {
            type = Type.defaultType;
        }
        switch (type) {
        case Proposal:
            WicketUtils.setCssClass(label, "fa fa-code-fork");
            break;
        case Bug:
            WicketUtils.setCssClass(label, "fa fa-bug");
            break;
        case Enhancement:
            WicketUtils.setCssClass(label, "fa fa-magic");
            break;
        case Question:
            WicketUtils.setCssClass(label, "fa fa-question");
            break;
        default:
            // standard ticket
            WicketUtils.setCssClass(label, "fa fa-ticket");
        }
        WicketUtils.setHtmlTooltip(label, getTypeState(type, state));
        return label;
    }
    public static String getTypeState(Type type, Status state) {
        return state.toString() + " " + type.toString();
    }
    public static String getLozengeClass(Status status, boolean subtle) {
        if (status == null) {
            status = Status.New;
        }
        String css = "";
        switch (status) {
        case Declined:
        case Duplicate:
        case Invalid:
        case Wontfix:
        case Abandoned:
            css = "aui-lozenge-error";
            break;
        case Fixed:
        case Merged:
        case Resolved:
            css = "aui-lozenge-success";
            break;
        case New:
            css = "aui-lozenge-complete";
            break;
        case On_Hold:
            css = "aui-lozenge-current";
            break;
        default:
            css = "";
            break;
        }
        return "aui-lozenge" + (subtle ? " aui-lozenge-subtle": "") + (css.isEmpty() ? "" : " ") + css;
    }
    public static String getStatusClass(Status status) {
        String css = "";
        switch (status) {
        case Declined:
        case Duplicate:
        case Invalid:
        case Wontfix:
        case Abandoned:
            css = "resolution-error";
            break;
        case Fixed:
        case Merged:
        case Resolved:
            css = "resolution-success";
            break;
        case New:
            css = "resolution-complete";
            break;
        case On_Hold:
            css = "resolution-current";
            break;
        default:
            css = "";
            break;
        }
        return "resolution" + (css.isEmpty() ? "" : " ") + css;
    }
    public static class TicketSort implements Serializable {
        private static final long serialVersionUID = 1L;
        public final String name;
        public final String sortBy;
        public final boolean desc;
        public TicketSort(String name, String sortBy, boolean desc) {
            this.name = name;
            this.sortBy = sortBy;
            this.desc = desc;
        }
    }
    public static class Indicator implements Serializable {
        private static final long serialVersionUID = 1L;
        public final String css;
        public final int count;
        public final String tooltip;
        public Indicator(String css, String tooltip) {
            this.css = css;
            this.tooltip = tooltip;
            this.count = 0;
        }
        public Indicator(String css, int count, String pattern) {
            this.css = css;
            this.count = count;
            this.tooltip = StringUtils.isEmpty(pattern) ? "" : MessageFormat.format(pattern, count);
        }
        public String getTooltip() {
            return tooltip;
        }
    }
    public static class TicketQuery implements Serializable, Comparable<TicketQuery> {
        private static final long serialVersionUID = 1L;
        public final String name;
        public final String query;
        public String color;
        public TicketQuery(String name, String query) {
            this.name = name;
            this.query = query;
        }
        public TicketQuery color(String value) {
            this.color = value;
            return this;
        }
        @Override
        public boolean equals(Object o) {
            if (o instanceof TicketQuery) {
                return ((TicketQuery) o).query.equals(query);
            }
            return false;
        }
        @Override
        public int hashCode() {
            return query.hashCode();
        }
        @Override
        public int compareTo(TicketQuery o) {
            return query.compareTo(o.query);
        }
    }
}
src/main/java/com/gitblit/wicket/UrlExternalFormComparator.java
New file
@@ -0,0 +1,39 @@
/*
 * 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;
import java.net.URL;
import java.util.Comparator;
/**
 * A comparator of URL instances.
 *
 * Comparing URLs with their implementation of #equals() is
 * bad because it may cause problems like DNS resolving, or other
 * slow checks. This comparator uses the external form of an URL
 * to make a simple comparison of two Strings.
 *
 * @since 1.5.6
 */
public class UrlExternalFormComparator implements Comparator<URL>
{
    @Override
    public int compare(URL url1, URL url2)
    {
        return url1.toExternalForm().compareTo(url2.toExternalForm());
    }
}
src/main/java/com/gitblit/wicket/WicketUtils.java
@@ -300,7 +300,9 @@
    public static PageParameters newRepositoryParameter(String repositoryName) {
        Map<String, String> parameterMap = new HashMap<String, String>();
        parameterMap.put("r", repositoryName);
        if (!StringUtils.isEmpty(repositoryName)) {
            parameterMap.put("r", repositoryName);
        }
        return new PageParameters(parameterMap);
    }
@@ -443,6 +445,20 @@
        return new PageParameters(parameterMap);
    }
    public static PageParameters newTicketsParameters(String repositoryName, String... states) {
        PageParameters tParams = newRepositoryParameter(repositoryName);
        if (states != null) {
            for (String state : states) {
                tParams.add("status", state);
            }
        }
        return tParams;
    }
    public static PageParameters newOpenTicketsParameter(String repositoryName) {
        return newTicketsParameters(repositoryName, TicketsUI.openStatii);
    }
    public static String getProjectName(PageParameters params) {
        return params.getString("p", "");
    }
src/main/java/com/gitblit/wicket/pages/ActivityPage.java
@@ -31,15 +31,15 @@
import com.gitblit.Keys;
import com.gitblit.models.Activity;
import com.gitblit.models.Menu.ParameterMenuItem;
import com.gitblit.models.NavLink.DropDownPageMenuNavLink;
import com.gitblit.models.Metric;
import com.gitblit.models.NavLink;
import com.gitblit.models.RepositoryModel;
import com.gitblit.utils.ActivityUtils;
import com.gitblit.utils.StringUtils;
import com.gitblit.wicket.CacheControl;
import com.gitblit.wicket.CacheControl.LastModified;
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.Chart;
import com.gitblit.wicket.charting.Charts;
@@ -135,8 +135,8 @@
    }
    @Override
    protected void addDropDownMenus(List<PageRegistration> pages) {
        DropDownMenuRegistration filters = new DropDownMenuRegistration("gb.filters",
    protected void addDropDownMenus(List<NavLink> navLinks) {
        DropDownPageMenuNavLink filters = new DropDownPageMenuNavLink("gb.filters",
                ActivityPage.class);
        PageParameters currentParameters = getPageParameters();
@@ -153,9 +153,9 @@
        if (filters.menuItems.size() > 0) {
            // Reset Filter
            filters.menuItems.add(new DropDownMenuItem(getString("gb.reset"), null, null));
            filters.menuItems.add(new ParameterMenuItem(getString("gb.reset")));
        }
        pages.add(filters);
        navLinks.add(filters);
    }
    /**
@@ -209,7 +209,7 @@
        }
        charts.addChart(chart);
        // active repositories pie chart
        // active repositories pie chart
        chart = charts.createPieChart("chartRepositories", getString("gb.activeRepositories"),
                getString("gb.repository"), getString("gb.commits"));
        for (Metric metric : repositoryMetrics.values()) {
src/main/java/com/gitblit/wicket/pages/BasePage.java
@@ -98,6 +98,10 @@
        }
    }
    protected String getContextUrl() {
        return getRequest().getRelativePathPrefixToContextRoot();
    }
    protected String getCanonicalUrl() {
        return getCanonicalUrl(getClass(), getPageParameters());
    }
src/main/java/com/gitblit/wicket/pages/BlobPage.java
@@ -24,10 +24,12 @@
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.apache.wicket.markup.html.link.ExternalLink;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import com.gitblit.Keys;
import com.gitblit.servlet.RawServlet;
import com.gitblit.utils.JGitUtils;
import com.gitblit.utils.StringUtils;
import com.gitblit.wicket.CacheControl;
@@ -57,8 +59,8 @@
                    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)));
            String rawUrl = RawServlet.asLink(getContextUrl(), repositoryName, objectId, blobPath);
            add(new ExternalLink("rawLink",  rawUrl));
            add(new CommitHeaderPanel("commitHeader", objectId));
            add(new PathBreadcrumbsPanel("breadcrumbs", repositoryName, blobPath, objectId));
            Component c = new Label("blobText", JGitUtils.getStringContent(r, objectId, encodings));
@@ -87,8 +89,8 @@
                    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)));
            String rawUrl = RawServlet.asLink(getContextUrl(), repositoryName, objectId, blobPath);
            add(new ExternalLink("rawLink", rawUrl));
            add(new CommitHeaderPanel("commitHeader", repositoryName, commit));
@@ -115,7 +117,7 @@
                case 2:
                    // image blobs
                    add(new Label("blobText").setVisible(false));
                    add(new ExternalImage("blobImage", urlFor(RawPage.class, WicketUtils.newPathParameter(repositoryName, objectId, blobPath)).toString()));
                    add(new ExternalImage("blobImage", rawUrl));
                    break;
                case 3:
                    // binary blobs
src/main/java/com/gitblit/wicket/pages/CommitDiffPage.java
@@ -34,6 +34,7 @@
import com.gitblit.models.GitNote;
import com.gitblit.models.PathModel.PathChangeModel;
import com.gitblit.models.SubmoduleModel;
import com.gitblit.servlet.RawServlet;
import com.gitblit.utils.DiffUtils;
import com.gitblit.utils.DiffUtils.DiffOutput;
import com.gitblit.utils.DiffUtils.DiffOutputType;
@@ -170,8 +171,8 @@
                    item.add(new BookmarkablePageLink<Void>("view", BlobPage.class, WicketUtils
                            .newPathParameter(repositoryName, entry.commitId, entry.path))
                            .setEnabled(!entry.changeType.equals(ChangeType.DELETE)));
                    item.add(new BookmarkablePageLink<Void>("raw", RawPage.class, WicketUtils
                            .newPathParameter(repositoryName, entry.commitId, entry.path))
                    String rawUrl = RawServlet.asLink(getContextUrl(), repositoryName, entry.commitId, entry.path);
                    item.add(new ExternalLink("raw", rawUrl)
                            .setEnabled(!entry.changeType.equals(ChangeType.DELETE)));
                    item.add(new BookmarkablePageLink<Void>("blame", BlamePage.class, WicketUtils
                            .newPathParameter(repositoryName, entry.commitId, entry.path))
src/main/java/com/gitblit/wicket/pages/CommitPage.java
@@ -35,6 +35,7 @@
import com.gitblit.models.GitNote;
import com.gitblit.models.PathModel.PathChangeModel;
import com.gitblit.models.SubmoduleModel;
import com.gitblit.servlet.RawServlet;
import com.gitblit.utils.JGitUtils;
import com.gitblit.wicket.CacheControl;
import com.gitblit.wicket.CacheControl.LastModified;
@@ -222,8 +223,8 @@
                    item.add(new BookmarkablePageLink<Void>("view", BlobPage.class, WicketUtils
                            .newPathParameter(repositoryName, entry.commitId, entry.path))
                            .setEnabled(!entry.changeType.equals(ChangeType.DELETE)));
                    item.add(new BookmarkablePageLink<Void>("raw", RawPage.class, WicketUtils
                            .newPathParameter(repositoryName, entry.commitId, entry.path))
                    String rawUrl = RawServlet.asLink(getContextUrl(), repositoryName, entry.commitId, entry.path);
                    item.add(new ExternalLink("raw", rawUrl)
                            .setEnabled(!entry.changeType.equals(ChangeType.DELETE)));
                    item.add(new BookmarkablePageLink<Void>("blame", BlamePage.class, WicketUtils
                            .newPathParameter(repositoryName, entry.commitId, entry.path))
src/main/java/com/gitblit/wicket/pages/ComparePage.java
@@ -41,6 +41,7 @@
import com.gitblit.models.RefModel;
import com.gitblit.models.RepositoryModel;
import com.gitblit.models.SubmoduleModel;
import com.gitblit.servlet.RawServlet;
import com.gitblit.utils.DiffUtils;
import com.gitblit.utils.DiffUtils.DiffOutput;
import com.gitblit.utils.DiffUtils.DiffOutputType;
@@ -184,8 +185,8 @@
                        item.add(new BookmarkablePageLink<Void>("view", BlobPage.class, WicketUtils
                                .newPathParameter(repositoryName, endId, entry.path))
                                .setEnabled(!entry.changeType.equals(ChangeType.DELETE)));
                        item.add(new BookmarkablePageLink<Void>("raw", RawPage.class, WicketUtils
                                .newPathParameter(repositoryName, endId, entry.path))
                        String rawUrl = RawServlet.asLink(getContextUrl(), repositoryName, endId, entry.path);
                        item.add(new ExternalLink("raw", rawUrl)
                                .setEnabled(!entry.changeType.equals(ChangeType.DELETE)));
                        item.add(new BookmarkablePageLink<Void>("blame", BlamePage.class, WicketUtils
                                .newPathParameter(repositoryName, endId, entry.path))
src/main/java/com/gitblit/wicket/pages/DashboardPage.java
@@ -36,7 +36,10 @@
import com.gitblit.Keys;
import com.gitblit.models.DailyLogEntry;
import com.gitblit.models.Menu.ParameterMenuItem;
import com.gitblit.models.NavLink.DropDownPageMenuNavLink;
import com.gitblit.models.Metric;
import com.gitblit.models.NavLink;
import com.gitblit.models.RefLogEntry;
import com.gitblit.models.RepositoryCommit;
import com.gitblit.models.RepositoryModel;
@@ -45,9 +48,6 @@
import com.gitblit.utils.RefLogUtils;
import com.gitblit.utils.StringUtils;
import com.gitblit.wicket.GitBlitWebApp;
import com.gitblit.wicket.PageRegistration;
import com.gitblit.wicket.PageRegistration.DropDownMenuItem;
import com.gitblit.wicket.PageRegistration.DropDownMenuRegistration;
import com.gitblit.wicket.charting.Chart;
import com.gitblit.wicket.charting.Charts;
import com.gitblit.wicket.charting.Flotr2Charts;
@@ -141,10 +141,10 @@
    }
    @Override
    protected void addDropDownMenus(List<PageRegistration> pages) {
    protected void addDropDownMenus(List<NavLink> navLinks) {
        PageParameters params = getPageParameters();
        DropDownMenuRegistration menu = new DropDownMenuRegistration("gb.filters",
        DropDownPageMenuNavLink menu = new DropDownPageMenuNavLink("gb.filters",
                GitBlitWebApp.get().getHomePage());
        // preserve repository filter option on time choices
@@ -152,10 +152,10 @@
        if (menu.menuItems.size() > 0) {
            // Reset Filter
            menu.menuItems.add(new DropDownMenuItem(getString("gb.reset"), null, null));
            menu.menuItems.add(new ParameterMenuItem(getString("gb.reset")));
        }
        pages.add(menu);
        navLinks.add(menu);
    }
src/main/java/com/gitblit/wicket/pages/DocPage.java
@@ -20,10 +20,12 @@
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.panel.Fragment;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import com.gitblit.servlet.RawServlet;
import com.gitblit.utils.BugtraqProcessor;
import com.gitblit.utils.JGitUtils;
import com.gitblit.utils.StringUtils;
@@ -87,8 +89,8 @@
                WicketUtils.newPathParameter(repositoryName, objectId, documentPath)));
        fragment.add(new BookmarkablePageLink<Void>("historyLink", HistoryPage.class,
                WicketUtils.newPathParameter(repositoryName, objectId, documentPath)));
        fragment.add(new BookmarkablePageLink<Void>("rawLink", RawPage.class, WicketUtils.newPathParameter(
                repositoryName, objectId, documentPath)));
        String rawUrl = RawServlet.asLink(getContextUrl(), repositoryName, objectId, documentPath);
        fragment.add(new ExternalLink("rawLink", rawUrl));
        fragment.add(new Label("content", markupDoc.html).setEscapeModelStrings(false));
        add(fragment);
src/main/java/com/gitblit/wicket/pages/DocsPage.java
@@ -31,6 +31,7 @@
import org.eclipse.jgit.revwalk.RevCommit;
import com.gitblit.models.PathModel;
import com.gitblit.servlet.RawServlet;
import com.gitblit.utils.ByteFormat;
import com.gitblit.utils.JGitUtils;
import com.gitblit.utils.StringUtils;
@@ -103,8 +104,8 @@
                            WicketUtils.newPathParameter(repositoryName, commitId, doc.documentPath)));
                    item.add(new BookmarkablePageLink<Void>("historyLink", HistoryPage.class,
                            WicketUtils.newPathParameter(repositoryName, commitId, doc.documentPath)));
                    item.add(new BookmarkablePageLink<Void>("rawLink", RawPage.class, WicketUtils.newPathParameter(
                            repositoryName, commitId, doc.documentPath)));
                    String rawUrl = RawServlet.asLink(getContextUrl(), repositoryName, commitId, doc.documentPath);
                    item.add(new ExternalLink("rawLink", rawUrl));
                    // document content
                    String file = StringUtils.getLastPathElement(doc.documentPath);
@@ -145,8 +146,8 @@
                // links
                item.add(new BookmarkablePageLink<Void>("view", DocPage.class, WicketUtils
                        .newPathParameter(repositoryName, id, entry.path)));
                item.add(new BookmarkablePageLink<Void>("raw", RawPage.class, WicketUtils
                        .newPathParameter(repositoryName, id, entry.path)));
                String rawUrl = RawServlet.asLink(getContextUrl(), repositoryName, id, entry.path);
                item.add(new ExternalLink("raw", rawUrl));
                item.add(new BookmarkablePageLink<Void>("blame", BlamePage.class, WicketUtils
                        .newPathParameter(repositoryName, id, entry.path)));
                item.add(new BookmarkablePageLink<Void>("history", HistoryPage.class, WicketUtils
src/main/java/com/gitblit/wicket/pages/EditMilestonePage.html
New file
@@ -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">
<wicket:extend>
<body onload="document.getElementById('name').focus();">
<div class="container">
    <!-- page header -->
    <div class="title" style="font-size: 22px; color: rgb(0, 32, 96);padding: 3px 0px 7px;">
        <span class="project"><wicket:message key="gb.editMilestone"></wicket:message></span>
    </div>
    <form style="padding-top:5px;" wicket:id="editForm">
    <div class="row">
    <div class="span12">
        <!-- Edit Milestone Table -->
        <table class="ticket">
            <tr><th><wicket:message key="gb.milestone"></wicket:message></th><td class="edit"><input class="input-large" type="text" wicket:id="name" id="name"></input></td></tr>
            <tr><th><wicket:message key="gb.due"></wicket:message></th><td class="edit"><input class="input-large" type="text" wicket:id="due"></input> &nbsp;<span class="help-inline" wicket:id="dueFormat"></span></td></tr>
            <tr><th><wicket:message key="gb.status"></wicket:message><span style="color:red;">*</span></th><td class="edit"><select class="input-large" wicket:id="status"></select></td></tr>
            <tr><th></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="notify" /> &nbsp;<span class="help-inline"><wicket:message key="gb.notifyChangedOpenTickets"></wicket:message></span></label></td></tr>
        </table>
    </div>
    </div>
    <div class="row">
    <div class="span12">
        <div class="form-actions"><input class="btn btn-appmenu" 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" /> &nbsp; <input class="btn btn-danger" type="submit" value="Delete" wicket:message="value:gb.delete" wicket:id="delete" /></div>
    </div>
    </div>
    </form>
</div>
</body>
</wicket:extend>
</html>
src/main/java/com/gitblit/wicket/pages/EditMilestonePage.java
New file
@@ -0,0 +1,196 @@
/*
 * Copyright 2014 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.RestartResponseException;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.markup.html.form.AjaxButton;
import org.apache.wicket.extensions.markup.html.form.DateTextField;
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.TextField;
import org.apache.wicket.markup.html.link.Link;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.Model;
import com.gitblit.models.RepositoryModel;
import com.gitblit.models.TicketModel;
import com.gitblit.models.TicketModel.Status;
import com.gitblit.models.UserModel;
import com.gitblit.tickets.TicketMilestone;
import com.gitblit.utils.StringUtils;
import com.gitblit.wicket.GitBlitWebSession;
import com.gitblit.wicket.WicketUtils;
import com.gitblit.wicket.panels.BasePanel.JavascriptEventConfirmation;
/**
 * Page for creating a new milestone.
 *
 * @author James Moger
 *
 */
public class EditMilestonePage extends RepositoryPage {
    private final String oldName;
    private IModel<String> nameModel;
    private IModel<Date> dueModel;
    private IModel<Status> statusModel;
    private IModel<Boolean> notificationModel;
    public EditMilestonePage(PageParameters params) {
        super(params);
        RepositoryModel model = getRepositoryModel();
        if (!app().tickets().isAcceptingTicketUpdates(model)) {
            // ticket service is read-only
            throw new RestartResponseException(TicketsPage.class, WicketUtils.newOpenTicketsParameter(repositoryName));
        }
        UserModel currentUser = GitBlitWebSession.get().getUser();
        if (currentUser == null) {
            currentUser = UserModel.ANONYMOUS;
        }
        if (!currentUser.isAuthenticated || !currentUser.canAdmin(model)) {
            // administration prohibited
            throw new RestartResponseException(TicketsPage.class, WicketUtils.newOpenTicketsParameter(repositoryName));
        }
        oldName = WicketUtils.getObject(params);
        if (StringUtils.isEmpty(oldName)) {
            // milestone not specified
            throw new RestartResponseException(TicketsPage.class, WicketUtils.newOpenTicketsParameter(repositoryName));
        }
        TicketMilestone tm = app().tickets().getMilestone(getRepositoryModel(), oldName);
        if (tm == null) {
            // milestone does not exist
            throw new RestartResponseException(TicketsPage.class, WicketUtils.newOpenTicketsParameter(repositoryName));
        }
        setStatelessHint(false);
        setOutputMarkupId(true);
        Form<Void> form = new Form<Void>("editForm");
        add(form);
        nameModel = Model.of(tm.name);
        dueModel = Model.of(tm.due);
        statusModel = Model.of(tm.status);
        notificationModel = Model.of(true);
        form.add(new TextField<String>("name", nameModel));
        form.add(new DateTextField("due", dueModel, "yyyy-MM-dd"));
        form.add(new Label("dueFormat", "yyyy-MM-dd"));
        form.add(new CheckBox("notify", notificationModel));
        List<Status> statusChoices = Arrays.asList(Status.Open, Status.Closed);
        form.add(new DropDownChoice<TicketModel.Status>("status", statusModel, statusChoices));
        form.add(new AjaxButton("save") {
            private static final long serialVersionUID = 1L;
            @Override
            protected void onSubmit(AjaxRequestTarget target, Form<?> form) {
                String name = nameModel.getObject();
                if (StringUtils.isEmpty(name)) {
                    return;
                }
                Date due = dueModel.getObject();
                Status status = statusModel.getObject();
                boolean rename = !name.equals(oldName);
                boolean notify = notificationModel.getObject();
                UserModel currentUser = GitBlitWebSession.get().getUser();
                String createdBy = currentUser.username;
                TicketMilestone tm = app().tickets().getMilestone(getRepositoryModel(), oldName);
                tm.setName(name);
                tm.setDue(due);
                tm.status = status;
                boolean success = true;
                if (rename) {
                    success = app().tickets().renameMilestone(getRepositoryModel(), oldName, name, createdBy, notify);
                }
                if (success && app().tickets().updateMilestone(getRepositoryModel(), tm, createdBy)) {
                    setResponsePage(TicketsPage.class, WicketUtils.newOpenTicketsParameter(repositoryName));
                } else {
                    // TODO error
                }
            }
        });
        Button cancel = new Button("cancel") {
            private static final long serialVersionUID = 1L;
            @Override
            public void onSubmit() {
                setResponsePage(TicketsPage.class, WicketUtils.newOpenTicketsParameter(repositoryName));
            }
        };
        cancel.setDefaultFormProcessing(false);
        form.add(cancel);
        Link<Void> delete = new Link<Void>("delete") {
            private static final long serialVersionUID = 1L;
            @Override
            public void onClick() {
                UserModel currentUser = GitBlitWebSession.get().getUser();
                String createdBy = currentUser.username;
                boolean notify = notificationModel.getObject();
                if (app().tickets().deleteMilestone(getRepositoryModel(), oldName, createdBy, notify)) {
                    setResponsePage(TicketsPage.class, WicketUtils.newOpenTicketsParameter(repositoryName));
                } else {
                    error(MessageFormat.format(getString("gb.milestoneDeleteFailed"), oldName));
                }
            }
        };
        delete.add(new JavascriptEventConfirmation("onclick", MessageFormat.format(
            getString("gb.deleteMilestone"), oldName)));
        form.add(delete);
    }
    @Override
    protected String getPageName() {
        return getString("gb.editMilestone");
    }
    @Override
    protected Class<? extends BasePage> getRepoNavPageClass() {
        return TicketsPage.class;
    }
}
src/main/java/com/gitblit/wicket/pages/EditRepositoryPage.html
@@ -9,14 +9,17 @@
    <form style="padding-top:5px;" wicket:id="editForm">
<div class="row">
<div class="span12">
<div class="tabbable">
<div class="tabbable tabs-left">
    <!-- 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="#permissions" data-toggle="tab"><wicket:message key="gb.permissions"></wicket:message></a></li>
        <li><a href="#receive" data-toggle="tab"><wicket:message key="gb.receive"></wicket:message></a></li>
        <li><a href="#tickets" data-toggle="tab"><wicket:message key="gb.tickets"></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>
        <li><a href="#gc" data-toggle="tab"><wicket:message key="gb.gc"></wicket:message></a></li>
        <li><a href="#misc" data-toggle="tab"><wicket:message key="gb.miscellaneous"></wicket:message></a></li>
    </ul>
    <!-- tab content -->
@@ -24,99 +27,167 @@
        <!-- 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.acceptNewTickets"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="acceptNewTickets" tabindex="7" /> &nbsp;<span class="help-inline"><wicket:message key="gb.acceptNewTicketsDescription"></wicket:message></span></label></td></tr>
                <tr><th><wicket:message key="gb.acceptNewPatchsets"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="acceptNewPatchsets" tabindex="8" /> &nbsp;<span class="help-inline"><wicket:message key="gb.acceptNewPatchsetsDescription"></wicket:message></span></label></td></tr>
                <tr><th><wicket:message key="gb.requireApproval"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="requireApproval" tabindex="9" /> &nbsp;<span class="help-inline"><wicket:message key="gb.requireApprovalDescription"></wicket:message></span></label></td></tr>
                <tr><th><wicket:message key="gb.mergeTo"></wicket:message></th><td class="edit"><select class="span2" wicket:id="mergeTo" tabindex="10" /> &nbsp;<span class="help-inline"><wicket:message key="gb.mergeToDescription"></wicket:message></span></td></tr>
                <tr><th colspan="2"><hr/></th></tr>
                <tr><th><wicket:message key="gb.enableIncrementalPushTags"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="useIncrementalPushTags" tabindex="11" /> &nbsp;<span class="help-inline"><wicket:message key="gb.useIncrementalPushTagsDescription"></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="12" /> &nbsp;<span class="help-inline"><wicket:message key="gb.showRemoteBranchesDescription"></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="13" /> &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="14" /> &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="15" /> &nbsp;<span class="help-inline"><wicket:message key="gb.maxActivityCommitsDescription"></wicket:message></span></td></tr>
                <tr><th><wicket:message key="gb.metricAuthorExclusions"></wicket:message></th><td class="edit"><input class="span8" type="text" wicket:id="metricAuthorExclusions" size="40" tabindex="16" /></td></tr>
                <tr><th><wicket:message key="gb.commitMessageRenderer"></wicket:message></th><td class="edit"><select class="span2" wicket:id="commitMessageRenderer" tabindex="17" /></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="18" /></td></tr>
            </tbody>
        </table>
            <div wicket:id="namePanel"></div>
            <hr/>
            <div wicket:id="accessPolicyPanel"></div>
        </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="19" /> </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="20" /></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="21" /> &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="22" /> &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="23" /> &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>
            <h4><wicket:message key="gb.owners"></wicket:message></h4>
            <p><wicket:message key="gb.ownersDescription"></wicket:message></p>
            <div wicket:id="owners"></div>
            <hr />
            <h4><wicket:message key="gb.userPermissions"></wicket:message></h4>
            <p><wicket:message key="gb.userPermissionsDescription"></wicket:message></p>
            <div wicket:id="users"></div>
            <hr />
            <h4><wicket:message key="gb.teamPermissions"></wicket:message></h4>
            <p><wicket:message key="gb.teamPermissionsDescription"></wicket:message></p>
            <div wicket:id="teams"></div>
        </div>
        <!-- federation -->
        <div class="tab-pane" id="federation">
        <!-- receive -->
        <div class="tab-pane" id="receive">
            <h4><wicket:message key="gb.receiveSettings"></wicket:message></h4>
            <p><wicket:message key="gb.receiveSettingsDescription"></wicket:message></p>
            <hr/>
            <div wicket:id="isFrozen"></div>
            <div wicket:id="verifyCommitter"></div>
            <div wicket:id="incrementalPushTags"></div>
            <hr />
            <h4><wicket:message key="gb.preReceiveScripts"></wicket:message></h4>
            <p><wicket:message key="gb.preReceiveDescription"></wicket:message></p>
            <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="24" /></td></tr>
                    <tr><th><wicket:message key="gb.federationSets"></wicket:message></th><td style="padding:2px;"><span wicket:id="federationSets"></span></td></tr>
                    <tr>
                        <td style="padding:2px;"><span wicket:id="preReceiveScripts"></span></td>
                        <td style="vertical-align: top;"><span wicket:id="inheritedPreReceive"></span></td>
                    </tr>
                </tbody>
            </table>
            <hr />
            <h4><wicket:message key="gb.postReceiveScripts"></wicket:message></h4>
            <p><wicket:message key="gb.postReceiveDescription"></wicket:message></p>
            <table class="plain">
                <tbody class="settings">
                    <tr>
                        <td style="padding:2px;"><span wicket:id="postReceiveScripts"></span></td>
                        <td style="vertical-align: top;"><span wicket:id="inheritedPostReceive"></span></td>
                    </tr>
                </tbody>
            </table>
            <div wicket:id="customFieldsSection">
                <hr />
                <h4><wicket:message key="gb.customFields"></wicket:message></h4>
                <p><wicket:message key="gb.customFieldsDescription"></wicket:message></p>
                <table class="plain">
                    <tbody class="settings">
                        <tr wicket:id="customFieldsListView"><th><span wicket:id="customFieldLabel"></span></th><td class="edit"><input class="span8" type="text" wicket:id="customFieldValue" /></td></tr>
                    </tbody>
                </table>
            </div>
        </div>
        <!-- tickets tab -->
        <div class="tab-pane" id="tickets">
            <h4><wicket:message key="gb.ticketSettings"></wicket:message></h4>
            <p><wicket:message key="gb.ticketsWelcome"></wicket:message></p>
            <hr/>
            <div wicket:id="acceptNewPatchsets"></div>
            <div wicket:id="acceptNewTickets"></div>
            <div wicket:id="requireApproval"></div>
            <div wicket:id="mergeTo"></div>
        </div>
        <!-- federation -->
        <div class="tab-pane" id="federation">
            <h4><wicket:message key="gb.federation"></wicket:message></h4>
            <p><wicket:message key="gb.federationRepositoryDescription"></wicket:message></p>
            <hr/>
            <div wicket:id="federationStrategy"></div>
            <hr />
            <h4><wicket:message key="gb.federationSets"></wicket:message></h4>
            <p><wicket:message key="gb.federationSetsDescription"></wicket:message></p>
            <div wicket:id="federationSets"></div>
        </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>
            <h4><wicket:message key="gb.indexedBranches"></wicket:message></h4>
            <p><wicket:message key="gb.indexedBranchesDescription"></wicket:message></p>
            <div wicket:id="indexedBranches"></div>
        </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>
        <!-- garbage collection -->
        <div class="tab-pane" id="gc">
            <h4><wicket:message key="gb.garbageCollection"></wicket:message></h4>
            <p><wicket:message key="gb.garbageCollectionDescription"></wicket:message></p>
            <div wicket:id="gcPeriod"></div>
            <div wicket:id="gcThreshold"></div>
        </div>
        <!-- misc -->
        <div class="tab-pane" id="misc">
            <div wicket:id="origin"></div>
            <div wicket:id="head"></div>
            <hr/>
            <div wicket:id="showRemoteBranches"></div>
            <div wicket:id="skipSizeCalculation"></div>
            <div wicket:id="skipSummaryMetrics"></div>
            <div wicket:id="maxActivityCommits"></div>
            <div wicket:id="commitMessageRenderer"></div>
            <div wicket:id="metricAuthorExclusions"></div>
            <div wicket:id="mailingLists"></div>
        </div>
        <div class="form-actions"><input class="btn btn-appmenu" 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" /> &nbsp; <input class="btn btn-danger" type="submit" value="Delete" wicket:message="value:gb.delete" wicket:id="delete" /></div>
        </div>
    </div>
</div>
</div>
</div>
<div class="row">
<div class="span12">
    <div class="form-actions"><input class="btn btn-appmenu" 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>
src/main/java/com/gitblit/wicket/pages/EditRepositoryPage.java
@@ -29,7 +29,6 @@
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;
@@ -40,13 +39,14 @@
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.link.Link;
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.PropertyModel;
import org.apache.wicket.model.util.CollectionModel;
import org.apache.wicket.model.util.ListModel;
import org.eclipse.jgit.lib.Repository;
@@ -68,12 +68,22 @@
import com.gitblit.wicket.GitBlitWebSession;
import com.gitblit.wicket.StringChoiceRenderer;
import com.gitblit.wicket.WicketUtils;
import com.gitblit.wicket.panels.AccessPolicyPanel;
import com.gitblit.wicket.panels.BasePanel.JavascriptEventConfirmation;
import com.gitblit.wicket.panels.BooleanOption;
import com.gitblit.wicket.panels.BulletListPanel;
import com.gitblit.wicket.panels.ChoiceOption;
import com.gitblit.wicket.panels.RegistrantPermissionsPanel;
import com.gitblit.wicket.panels.RepositoryNamePanel;
import com.gitblit.wicket.panels.TextOption;
public class EditRepositoryPage extends RootSubPage {
    private final boolean isCreate;
    RepositoryNamePanel namePanel;
    AccessPolicyPanel accessPolicyPanel;
    private boolean isAdmin;
@@ -194,7 +204,7 @@
            }
        }
        final Palette<UserChoice> ownersPalette = new Palette<UserChoice>("owners", new ListModel<UserChoice>(owners), new CollectionModel<UserChoice>(
              persons), new ChoiceRenderer<UserChoice>(null, "userId"), 12, true);
              persons), new ChoiceRenderer<UserChoice>(null, "userId"), 12, false);
        // indexed local branches palette
        List<String> allLocalBranches = new ArrayList<String>();
@@ -261,59 +271,8 @@
            @Override
            protected void onSubmit() {
                try {
                    // confirm a repository name was entered
                    if (repositoryModel.name == null && StringUtils.isEmpty(repositoryModel.name)) {
                        error(getString("gb.pleaseSetRepositoryName"));
                    if (!namePanel.updateModel(repositoryModel)) {
                        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
@@ -427,30 +386,11 @@
                    return;
                }
                setRedirect(false);
                if (isCreate) {
                    setResponsePage(RepositoriesPage.class);
                } else {
                    setResponsePage(SummaryPage.class, WicketUtils.newRepositoryParameter(repositoryModel.name));
                }
                setResponsePage(SummaryPage.class, WicketUtils.newRepositoryParameter(repositoryModel.name));
            }
        };
        // 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(app().settings().getBoolean(Keys.web.allowForking, true)));
        DropDownChoice<AccessRestrictionType> accessRestriction = new DropDownChoice<AccessRestrictionType>("accessRestriction",
                AccessRestrictionType.choices(app().settings().getBoolean(Keys.git.allowAnonymousPushes, false)), 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
        // Determine available refs & branches
        List<String> availableRefs = new ArrayList<String>();
        List<String> availableBranches = new ArrayList<String>();
        if (!ArrayUtils.isEmpty(repositoryModel.availableRefs)) {
@@ -463,58 +403,79 @@
                }
            }
        }
        form.add(new DropDownChoice<String>("HEAD", availableRefs).setEnabled(availableRefs.size() > 0));
        boolean gcEnabled = app().settings().getBoolean(Keys.git.enableGarbageCollection, false);
        int defaultGcPeriod = app().settings().getInteger(Keys.git.defaultGarbageCollectionPeriod, 7);
        if (repositoryModel.gcPeriod == 0) {
            repositoryModel.gcPeriod = defaultGcPeriod;
        }
        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));
        // do not let the browser pre-populate these fields
        form.add(new SimpleAttributeModifier("autocomplete", "off"));
        // 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("acceptNewPatchsets"));
        form.add(new CheckBox("acceptNewTickets"));
        form.add(new CheckBox("requireApproval"));
        form.add(new DropDownChoice<String>("mergeTo", availableBranches).setEnabled(availableBranches.size() > 0));
        form.add(new CheckBox("useIncrementalPushTags"));
        form.add(new CheckBox("showRemoteBranches"));
        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()));
        metricAuthorExclusions = new Model<String>(ArrayUtils.isEmpty(repositoryModel.metricAuthorExclusions) ? ""
                : StringUtils.flattenStrings(repositoryModel.metricAuthorExclusions, " "));
        form.add(new TextField<String>("metricAuthorExclusions", metricAuthorExclusions));
        //
        //
        // GENERAL
        //
        namePanel = new RepositoryNamePanel("namePanel", repositoryModel);
        namePanel.setEditable(allowEditName);
        form.add(namePanel);
        mailingLists = new Model<String>(ArrayUtils.isEmpty(repositoryModel.mailingLists) ? ""
                : StringUtils.flattenStrings(repositoryModel.mailingLists, " "));
        form.add(new TextField<String>("mailingLists", mailingLists));
        form.add(indexedBranchesPalette);
        // XXX AccessPolicyPanel is defined later.
        List<AuthorizationControl> acList = Arrays.asList(AuthorizationControl.values());
        final RadioChoice<AuthorizationControl> authorizationControl = new RadioChoice<Constants.AuthorizationControl>(
                "authorizationControl", acList, new AuthorizationControlRenderer());
        form.add(authorizationControl);
        form.add(new ChoiceOption<String>("head",
                getString("gb.headRef"),
                getString("gb.headRefDescription"),
                new PropertyModel<String>(repositoryModel, "HEAD"),
                availableRefs));
        final CheckBox verifyCommitter = new CheckBox("verifyCommitter");
        verifyCommitter.setOutputMarkupId(true);
        form.add(verifyCommitter);
        //
        // PERMISSIONS
        //
        form.add(ownersPalette);
        form.add(usersPalette);
        form.add(teamsPalette);
        form.add(federationSetsPalette);
        //
        // TICKETS
        //
        form.add(new BooleanOption("acceptNewPatchsets",
                getString("gb.acceptNewPatchsets"),
                getString("gb.acceptNewPatchsetsDescription"),
                new PropertyModel<Boolean>(repositoryModel, "acceptNewPatchsets")));
        form.add(new BooleanOption("acceptNewTickets",
                getString("gb.acceptNewTickets"),
                getString("gb.acceptNewTicketsDescription"),
                new PropertyModel<Boolean>(repositoryModel, "acceptNewPatchsets")));
        form.add(new BooleanOption("requireApproval",
                getString("gb.requireApproval"),
                getString("gb.requireApprovalDescription"),
                new PropertyModel<Boolean>(repositoryModel, "requireApproval")));
        form.add(new ChoiceOption<String>("mergeTo",
                getString("gb.mergeTo"),
                getString("gb.mergeToDescription"),
                new PropertyModel<String>(repositoryModel, "mergeTo"),
                availableBranches));
        //
        // RECEIVE
        //
        form.add(new BooleanOption("isFrozen",
                getString("gb.isFrozen"),
                getString("gb.isFrozenDescription"),
                new PropertyModel<Boolean>(repositoryModel, "isFrozen")));
        form.add(new BooleanOption("incrementalPushTags",
                getString("gb.enableIncrementalPushTags"),
                getString("gb.useIncrementalPushTagsDescription"),
                new PropertyModel<Boolean>(repositoryModel, "useIncrementalPushTags")));
        final CheckBox verifyCommitter = new CheckBox("checkbox", new PropertyModel<Boolean>(repositoryModel, "verifyCommitter"));
        verifyCommitter.setOutputMarkupId(true);
        form.add(new BooleanOption("verifyCommitter",
                getString("gb.verifyCommitter"),
                getString("gb.verifyCommitterDescription") + "<br/>" + getString("gb.verifyCommitterNote"),
                verifyCommitter).setIsHtmlDescription(true));
        form.add(preReceivePalette);
        form.add(new BulletListPanel("inheritedPreReceive", getString("gb.inherited"), app().repositories()
                .getPreReceiveScriptsInherited(repositoryModel)));
@@ -526,17 +487,125 @@
        customFieldsSection.add(customFieldsListView);
        form.add(customFieldsSection.setVisible(!app().settings().getString(Keys.groovy.customFields, "").isEmpty()));
        //
        // FEDERATION
        //
        List<FederationStrategy> federationStrategies = new ArrayList<FederationStrategy>(
                Arrays.asList(FederationStrategy.values()));
        // federation strategies - remove ORIGIN choice if this repository has no origin.
        if (StringUtils.isEmpty(repositoryModel.origin)) {
            federationStrategies.remove(FederationStrategy.FEDERATE_ORIGIN);
        }
        form.add(new ChoiceOption<FederationStrategy>("federationStrategy",
                getString("gb.federationStrategy"),
                getString("gb.federationStrategyDescription"),
                new DropDownChoice<FederationStrategy>(
                        "choice",
                        new PropertyModel<FederationStrategy>(repositoryModel, "federationStrategy"),
                        federationStrategies,
                        new FederationTypeRenderer())));
        form.add(federationSetsPalette);
        //
        // SEARCH
        //
        form.add(indexedBranchesPalette);
        //
        // GARBAGE COLLECTION
        //
        boolean gcEnabled = app().settings().getBoolean(Keys.git.enableGarbageCollection, false);
        int defaultGcPeriod = app().settings().getInteger(Keys.git.defaultGarbageCollectionPeriod, 7);
        if (repositoryModel.gcPeriod == 0) {
            repositoryModel.gcPeriod = defaultGcPeriod;
        }
        List<Integer> gcPeriods = Arrays.asList(1, 2, 3, 4, 5, 7, 10, 14 );
        form.add(new ChoiceOption<Integer>("gcPeriod",
                getString("gb.gcPeriod"),
                getString("gb.gcPeriodDescription"),
                new DropDownChoice<Integer>("choice",
                        new PropertyModel<Integer>(repositoryModel, "gcPeriod"),
                        gcPeriods,
                        new GCPeriodRenderer())).setEnabled(gcEnabled));
        form.add(new TextOption("gcThreshold",
                getString("gb.gcThreshold"),
                getString("gb.gcThresholdDescription"),
                "span1",
                new PropertyModel<String>(repositoryModel, "gcThreshold")).setEnabled(gcEnabled));
        //
        // MISCELLANEOUS
        //
        form.add(new TextOption("origin",
                getString("gb.origin"),
                getString("gb.originDescription"),
                "span6",
                new PropertyModel<String>(repositoryModel, "origin")).setEnabled(false));
        form.add(new BooleanOption("showRemoteBranches",
                getString("gb.showRemoteBranches"),
                getString("gb.showRemoteBranchesDescription"),
                new PropertyModel<Boolean>(repositoryModel, "showRemoteBranches")));
        form.add(new BooleanOption("skipSizeCalculation",
                getString("gb.skipSizeCalculation"),
                getString("gb.skipSizeCalculationDescription"),
                new PropertyModel<Boolean>(repositoryModel, "skipSizeCalculation")));
        form.add(new BooleanOption("skipSummaryMetrics",
                getString("gb.skipSummaryMetrics"),
                getString("gb.skipSummaryMetricsDescription"),
                new PropertyModel<Boolean>(repositoryModel, "skipSummaryMetrics")));
        List<Integer> maxActivityCommits  = Arrays.asList(-1, 0, 25, 50, 75, 100, 150, 200, 250, 500);
        form.add(new ChoiceOption<Integer>("maxActivityCommits",
                getString("gb.maxActivityCommits"),
                getString("gb.maxActivityCommitsDescription"),
                new DropDownChoice<Integer>("choice",
                        new PropertyModel<Integer>(repositoryModel, "maxActivityCommits"),
                        maxActivityCommits,
                        new MaxActivityCommitsRenderer())));
        List<CommitMessageRenderer> renderers = Arrays.asList(CommitMessageRenderer.values());
        form.add(new ChoiceOption<CommitMessageRenderer>("commitMessageRenderer",
                getString("gb.commitMessageRenderer"),
                getString("gb.commitMessageRendererDescription"),
                new DropDownChoice<CommitMessageRenderer>("choice",
                        new PropertyModel<CommitMessageRenderer>(repositoryModel, "commitMessageRenderer"),
                        renderers)));
        metricAuthorExclusions = new Model<String>(ArrayUtils.isEmpty(repositoryModel.metricAuthorExclusions) ? ""
                : StringUtils.flattenStrings(repositoryModel.metricAuthorExclusions, " "));
        form.add(new TextOption("metricAuthorExclusions",
                getString("gb.metricAuthorExclusions"),
                getString("gb.metricAuthorExclusions"),
                "span6",
                metricAuthorExclusions));
        mailingLists = new Model<String>(ArrayUtils.isEmpty(repositoryModel.mailingLists) ? ""
                : StringUtils.flattenStrings(repositoryModel.mailingLists, " "));
        form.add(new TextOption("mailingLists",
                getString("gb.mailingLists"),
                getString("gb.mailingLists"),
                "span6",
                mailingLists));
        // 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);
@@ -544,15 +613,18 @@
            teamsPalette.setEnabled(allowFineGrainedControls);
        }
        accessRestriction.add(new AjaxFormComponentUpdatingBehavior("onchange") {
        //
        // ACCESS POLICY PANEL (GENERAL)
        //
        AjaxFormChoiceComponentUpdatingBehavior callback = new AjaxFormChoiceComponentUpdatingBehavior() {
            private static final long serialVersionUID = 1L;
            @Override
            protected void onUpdate(AjaxRequestTarget target) {
                // enable/disable permissions panel based on access restriction
                accessPolicyPanel.updateModel(repositoryModel);
                boolean allowAuthorizationControl = repositoryModel.accessRestriction.exceeds(AccessRestrictionType.NONE);
                authorizationControl.setEnabled(allowAuthorizationControl);
                verifyCommitter.setEnabled(allowAuthorizationControl);
                boolean allowFineGrainedControls = allowAuthorizationControl && repositoryModel.authorizationControl.equals(AuthorizationControl.NAMED);
@@ -563,41 +635,19 @@
                    repositoryModel.authorizationControl = AuthorizationControl.NAMED;
                }
                target.addComponent(authorizationControl);
                target.addComponent(verifyCommitter);
                target.addComponent(usersPalette);
                target.addComponent(teamsPalette);
            }
        });
        };
        authorizationControl.add(new AjaxFormChoiceComponentUpdatingBehavior() {
        accessPolicyPanel = new AccessPolicyPanel("accessPolicyPanel", repositoryModel, callback);
        form.add(accessPolicyPanel);
            private static final long serialVersionUID = 1L;
            @Override
            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);
            }
        });
        List<CommitMessageRenderer> renderers = Arrays.asList(CommitMessageRenderer.values());
        DropDownChoice<CommitMessageRenderer> messageRendererChoice = new DropDownChoice<CommitMessageRenderer>("commitMessageRenderer", renderers);
        form.add(messageRendererChoice);
        //
        // FORM CONTROLS
        //
        form.add(new Button("save"));
        Button cancel = new Button("cancel") {
            private static final long serialVersionUID = 1L;
@@ -613,6 +663,46 @@
        };
        cancel.setDefaultFormProcessing(false);
        form.add(cancel);
        // the user can delete if deletions are allowed AND the user is an admin or the personal owner
        // assigned ownership is not sufficient to allow deletion
        boolean canDelete = !isCreate && app().repositories().canDelete(repositoryModel)
                && (user.canAdmin() || user.isMyPersonalRepository(repositoryModel.name));
        Link<Void> delete = new Link<Void>("delete") {
            private static final long serialVersionUID = 1L;
            @Override
            public void onClick() {
                RepositoryModel latestModel = app().repositories().getRepositoryModel(repositoryModel.name);
                boolean canDelete = app().repositories().canDelete(latestModel);
                if (canDelete) {
                    if (app().repositories().deleteRepositoryModel(latestModel)) {
                        info(MessageFormat.format(getString("gb.repositoryDeleted"), latestModel));
                        if (latestModel.isPersonalRepository()) {
                            // redirect to user's profile page
                            String prefix = app().settings().getString(Keys.git.userRepositoryPrefix, "~");
                            String username = latestModel.projectPath.substring(prefix.length());
                            setResponsePage(UserPage.class, WicketUtils.newUsernameParameter(username));
                        } else {
                            // redirect to server repositories page
                            setResponsePage(RepositoriesPage.class);
                        }
                    } else {
                        error(MessageFormat.format(getString("gb.repositoryDeleteFailed"), latestModel));
                    }
                } else {
                    error(MessageFormat.format(getString("gb.repositoryDeleteFailed"), latestModel));
                }
            }
        };
        if (canDelete) {
            delete.add(new JavascriptEventConfirmation("onclick", MessageFormat.format(
                getString("gb.deleteRepository"), repositoryModel)));
        }
        form.add(delete.setVisible(canDelete));
        add(form);
    }
@@ -663,26 +753,6 @@
        }
    }
    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> {
@@ -701,27 +771,6 @@
        @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);
        }
    }
src/main/java/com/gitblit/wicket/pages/EditTeamPage.java
@@ -86,7 +86,7 @@
    @Override
    protected Class<? extends BasePage> getRootNavPageClass() {
        return UsersPage.class;
        return TeamsPage.class;
    }
    protected void setupPage(final TeamModel teamModel) {
@@ -214,7 +214,7 @@
                            teamModel.name));
                }
                // back to users page
                setResponsePage(UsersPage.class);
                setResponsePage(TeamsPage.class);
            }
        };
@@ -249,7 +249,7 @@
            @Override
            public void onSubmit() {
                setResponsePage(UsersPage.class);
                setResponsePage(TeamsPage.class);
            }
        };
        cancel.setDefaultFormProcessing(false);
src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage.html
@@ -7,52 +7,53 @@
<body>
<wicket:extend>
<div class="container">
    <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>
<div class="markdown">
<div class="row">
<div class="span10 offset1">
    <h3><center>Empty Repository</center></h3>
    <div class="alert alert-info">
        <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>
        <hr/>
        After you have pushed commits you may <b>refresh</b> this page to view your repository.
    </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>
        <span style="padding-bottom:5px;">If your repository is meant to be kept in sync with an upstream repository, then you may add it.</span>
        <p></p>
        <pre wicket:id="upstreamSyntax" 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>
    <h3><center>Create a new repository on the command-line</center></h3>
    <pre wicket:id="createSyntax"></pre>
    <h3><center>Push an existing repository from the command-line</center></h3>
    <pre wicket:id="existingSyntax"></pre>
    <div class="span8 offset1">
        <h2><center>Learn Git</center></h2>
        <p>If you are unsure how to use this information, consider reviewing the <a href="http://book.git-scm.com">Git Community Book</a> for a better understanding on how to use Git.</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://rowanj.github.io/gitx/">GitX-dev</a> - a Mac OS X Git client</li>
        </ul>
        <p></p>
        <table>
        <tbody>
            <tr><td><a href="http://git-scm.com">Git</a></td><td>the official, command-line Git</td></tr>
            <tr><td><a href="http://tortoisegit.googlecode.com">TortoiseGit</a></td><td>Windows file explorer integration (requires official, command-line Git)</td></tr>
            <tr><td><a href="http://eclipse.org/egit">Eclipse/EGit</a></td><td>Git for the Eclipse IDE (based on JGit, like Gitblit)</td></tr>
            <tr><td><a href="https://code.google.com/p/gitextensions/">Git Extensions</a></td><td>C# frontend for Git that features Windows Explorer and Visual Studio integration</td></tr>
            <tr><td><a href="http://rowanj.github.io/gitx/">GitX-dev</a></td><td>a Mac OS X Git client</td></tr>
        </tbody>
        </table>
        <h4>Commercial/Closed-Source Git Clients</h4>
        <ul>
            <li><a href="http://www.syntevo.com/smartgithg">SmartGit/Hg</a> - A Java Git and Mercurial client for Windows, Mac, and Linux</li>
            <li><a href="http://www.sourcetreeapp.com/">SourceTree</a> - A free Git and Mercurial client for Windows & Mac</li>
            <li><a href="http://www.git-tower.com/">Tower</a> - a Mac OS X Git client</li>
        </ul>
        <table>
        <tbody>
            <tr><td><a href="http://www.syntevo.com/smartgithg">SmartGit/Hg</a></td><td>A Java Git and Mercurial client for Windows, Mac, and Linux</td></tr>
            <tr><td><a href="http://www.sourcetreeapp.com/">SourceTree</a></td><td>A free Git and Mercurial client for Windows & Mac</td></tr>
            <tr><td><a href="http://www.git-tower.com/">Tower</a></td><td>a Mac OS X Git client</td></tr>
        </tbody>
        </table>
    </div>
</div>
</div>
</div>
</div>
</wicket:extend>    
</body>
src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage.java
@@ -15,7 +15,6 @@
 */
package com.gitblit.wicket.pages;
import java.text.MessageFormat;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
@@ -32,7 +31,7 @@
import com.gitblit.wicket.WicketUtils;
import com.gitblit.wicket.panels.RepositoryUrlPanel;
public class EmptyRepositoryPage extends RootPage {
public class EmptyRepositoryPage extends RepositoryPage {
    public EmptyRepositoryPage(PageParameters params) {
        super(params);
@@ -50,8 +49,6 @@
            throw new GitblitRedirectException(SummaryPage.class, params);
        }
        setupPage(repositoryName, getString("gb.emptyRepository"));
        UserModel user = GitBlitWebSession.get().getUser();
        if (user == null) {
            user = UserModel.ANONYMOUS;
@@ -62,15 +59,17 @@
        RepositoryUrl primaryUrl = repositoryUrls.size() == 0 ? null : repositoryUrls.get(0);
        String url = primaryUrl != null ? primaryUrl.url : "";
        String createSyntax = readResource("create_git.md").replace("${primaryUrl}", url);
        String existingSyntax = readResource("existing_git.md").replace("${primaryUrl}", url);
        add(new Label("repository", repositoryName));
        add(new RepositoryUrlPanel("pushurl", false, user, repository));
        add(new Label("cloneSyntax", MessageFormat.format("git clone {0}", url)));
        add(new Label("remoteSyntax", MessageFormat.format("git remote add origin {0}\ngit push -u origin --all\ngit push -u origin --tags", url)));
        add(new Label("upstreamSyntax", "git remote add upstream <upstream repository url>"));
        add(new Label("createSyntax", createSyntax));
        add(new Label("existingSyntax", existingSyntax));
    }
    @Override
    protected Class<? extends BasePage> getRootNavPageClass() {
        return RepositoriesPage.class;
    protected String getPageName() {
        return getString("gb.summary");
    }
}
src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_de.html
New file
@@ -0,0 +1,60 @@
<!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="container">
<div class="markdown">
<div class="row">
<div class="span10 offset1">
    <h3><center>Leeres Repository</center></h3>
    <div class="alert alert-info">
        <span wicket:id="repository" style="font-weight: bold;">[repository]</span> ist ein leeres Repository und kann von Gitblit nicht angezeigt werden.
        <p></p>
        Bitte pushen Sie einige Commits nach <span wicket:id="pushurl"></span>
        <hr/>
        Nachdem Sie Commits gepusht haben, können Sie diese Seite <b>aktualisieren</b>, um Ihr Repository anzuzeigen.
    </div>
    <h3><center>Neues Respository auf der Kommandozeile erstellen</center></h3>
    <pre wicket:id="createSyntax"></pre>
    <h3><center>Ein existierendes Repository von der Kommandozeile pushen</center></h3>
    <pre wicket:id="existingSyntax"></pre>
    <div class="span8 offset1">
        <h2><center>Git lernen</center></h2>
        <p>Falls Sie unsicher sind, was Sie mit diesen Informationen anfangen sollen, können Sie sich das <a href="http://git-scm.com/book/de">Git Community Buch</a> anschauen, um ein besseres Verständnis über die Verwendung von Git aufzubauen.</p>
        <h4>Open Source Git Clients</h4>
        <table>
        <tbody>
            <tr><td><a href="http://git-scm.com">Git</a></td><td>der offizielle Kommandozeilen-Git-Client</td></tr>
            <tr><td><a href="http://tortoisegit.googlecode.com">TortoiseGit</a></td><td>Windows Datei Explorer Integration (erfordert den offiziellen Kommandozeilen-Client)</td></tr>
            <tr><td><a href="http://eclipse.org/egit">Eclipse/EGit</a></td><td>Git für die Eclipse IDE (basiert auf JGit, ebenso wie Gitblit)</td></tr>
            <tr><td><a href="https://code.google.com/p/gitextensions/">Git Extensions</a></td><td>C# Frontend für Git mit Windows Explorer und Visual Studio Integration</td></tr>
            <tr><td><a href="http://rowanj.github.io/gitx/">GitX-dev</a></td><td>ein Mac OS X Git Client</td></tr>
        </tbody>
        </table>
        <h4>Kommerzielle/Closed-Source Git Clients</h4>
        <table>
        <tbody>
            <tr><td><a href="http://www.syntevo.com/smartgithg">SmartGit/Hg</a></td><td>Ein Java Git und Mercurial Client für Windows, Mac und Linux</td></tr>
            <tr><td><a href="http://www.sourcetreeapp.com/">SourceTree</a></td><td>Ein freier Git und Mercurial Client für Windows und Mac</td></tr>
            <tr><td><a href="http://www.git-tower.com/">Tower</a></td><td>Ein Mac OS X Git Client</td></tr>
        </tbody>
        </table>
    </div>
</div>
</div>
</div>
</div>
</wicket:extend>
</body>
</html>
src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_es.html
@@ -9,52 +9,54 @@
<body>
<wicket:extend>
<div class="container">
        <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>
<div class="markdown">
<div class="row">
<div class="span10 offset1">
    <h3><center>Repositorio Vac&iacute;o</center></h3>
    <div class="alert alert-info">
        <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>
        <hr/>
        Despu&eacute;s de empujar tus consignas puedes <b>refrescar</b> &eacute;sta p&aacute;gina para ver tu repositorio.
    </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>
        <span style="padding-bottom:5px;">Si el repositorio est&aacute; pensado para mantenerse sincronizado con otro repositorio corriente-arriba, entonces puedes a&ntilde;adirlo.</span>
        <p></p>
        <pre wicket:id="upstreamSyntax" style="padding: 5px 30px;"></pre>
        <p></p>
        <h3>Aprender sobre 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>
    <h3><center>Create a new repository on the command-line</center></h3>
    <pre wicket:id="createSyntax"></pre>
    <h3><center>Push an existing repository from the command-line</center></h3>
    <pre wicket:id="existingSyntax"></pre>
    <div class="span8 offset1">
        <h2><center>Aprender sobre Git</center></h2>
        <p>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/es">Libro de la cominidad Git</a> para una mejor compresi&oacute;n de como usar Git.</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://rowanj.github.io/gitx/">GitX-dev</a> - Cliente Git para Mac OS X</li>
        </ul>
        <p></p>
        <table>
        <tbody>
            <tr><td><a href="http://git-scm.com">Git</a></td><td>El Git oficial en l&iacute;nea de comandos</td></tr>
            <tr><td><a href="http://tortoisegit.googlecode.com">TortoiseGit</a></td><td>Explorador de archivos integrado en Windows (necesita Git oficial en l&iacute;nea de comandos)</td></tr>
            <tr><td><a href="http://eclipse.org/egit">Eclipse/EGit</a></td><td>Git para el IDE de Eclipse (basado en JGit, como Gitblit)</td></tr>
            <tr><td><a href="https://code.google.com/p/gitextensions/">Git Extensions</a></td><td>Interfaz de usuario gr&aacute;fico Git en C# con integraci&oacute;n en IE y en Visual Studio</td></tr>
            <tr><td><a href="http://rowanj.github.io/gitx/">GitX-dev</a></td><td>Cliente Git para Mac OS X</td></tr>
        </tbody>
        </table>
        <h4>Clientes Git comerciales</h4>
        <ul>
            <li><a href="http://www.syntevo.com/smartgithg">SmartGit/Hg</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>
            <li><a href="http://www.git-tower.com/">Tower</a> - Cliente Git para Mac OS X</li>
        </ul>
</div>
        <table>
        <tbody>
            <tr><td><a href="http://www.syntevo.com/smartgithg">SmartGit/Hg</a></td><td>aplicaci&oacute;n Java (necesita Git oficial en l&iacute;nea de comandos)</td></tr>
            <tr><td><a href="http://www.sourcetreeapp.com/">SourceTree</a></td><td>Un cliente Git gratuito para Mac, Mercurial, y SVN</td></tr>
            <tr><td><a href="http://www.git-tower.com/">Tower</a></td><td>Cliente Git para Mac OS X</td></tr>
        </tbody>
        </table>
    </div>
</div>
</div>
</div>
</div>
</wicket:extend>    
</body>
</html>
src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_it.html
New file
@@ -0,0 +1,60 @@
<!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="container">
<div class="markdown">
<div class="row">
<div class="span10 offset1">
    <h3><center>Repository vuoto</center></h3>
    <div class="alert alert-info">
        <span wicket:id="repository" style="font-weight: bold;">[repository]</span> è vuoto e non può essere visualizzato con Gitblit.
        <p></p>
        Per favore effettua il push di alcuni commit su <span wicket:id="pushurl"></span>
        <hr/>
        Dopo il push <b>ricarica</b> questa pagina per vedere il contenuto del repository.
    </div>
    <h3><center>Crea un nuovo repository da riga di comando</center></h3>
    <pre wicket:id="createSyntax"></pre>
    <h3><center>Effettua il push da riga di comando</center></h3>
    <pre wicket:id="existingSyntax"></pre>
    <div class="span8 offset1">
        <h2><center>Impara a usare Git</center></h2>
        <p>Se non sei sicuro di quello che stai facendo, prova a leggere <a href="http://book.git-scm.com/it">Git Community Book</a> in italiano per una miglior comprensione dell'utilizzo di Git.</p>
        <h4>Applicazioni client Git open source</h4>
        <table>
        <tbody>
            <tr><td><a href="http://git-scm.com">Git</a></td><td>la versione ufficiale di Git, da riga di comando</td></tr>
            <tr><td><a href="http://tortoisegit.googlecode.com">TortoiseGit</a></td><td>Integrazione per Windows Explorer (richiede la versione ufficiale di Git da riga di comando)</td></tr>
            <tr><td><a href="http://eclipse.org/egit">Eclipse/EGit</a></td><td>Git per ambienti di sviluppo basati su Eclipse (basato su JGit, come Gitblit)</td></tr>
            <tr><td><a href="https://code.google.com/p/gitextensions/">Git Extensions</a></td><td>applicazione C# che integra Git in Windows Explorer e Visual Studio</td></tr>
            <tr><td><a href="http://rowanj.github.io/gitx/">GitX-dev</a></td><td>un client Git per Mac OS X</td></tr>
        </tbody>
        </table>
        <h4>Applicazioni client commerciali/non open source</h4>
        <table>
        <tbody>
            <tr><td><a href="http://www.syntevo.com/smartgithg">SmartGit/Hg</a></td><td>Un client Git e Mercurial scritto in Java per Windows, Mac, and Linux</td></tr>
            <tr><td><a href="http://www.sourcetreeapp.com/">SourceTree</a></td><td>Un client Git e Mercurial gratuito per Windows & Mac</td></tr>
            <tr><td><a href="http://www.git-tower.com/">Tower</a></td><td>Un client Git per Mac OS X</td></tr>
        </tbody>
        </table>
    </div>
</div>
</div>
</div>
</div>
</wicket:extend>
</body>
</html>
src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_ko.html
@@ -7,55 +7,54 @@
<body>
<wicket:extend>
<div class="container">
        <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>
<div class="markdown">
<div class="row">
<div class="span10 offset1">
    <h3><center>비어있는 저장소</center></h3>
    <div class="alert alert-info">
        <span wicket:id="repository" style="font-weight: bold;">[repository]</span> 저장소는 비어 있어서 Gitblit 에서 볼 수 없습니다.
        <p></p>
        이 Git url 에 커밋을 푸시하세요. <span wicket:id="pushurl"></span>
        <hr/>
        커밋을 푸시한 후 이 페이지를 <b>새로고침</b>하면 저장소가 보여질 것입니다.
    </div>
    <h3><center>커맨드라인에서 새 저장소 생성하기</center></h3>
        <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>
        <span style="padding-bottom:5px;">If your repository is meant to be kept in sync with an upstream repository, then you may add it.</span>
        <p></p>
        <pre wicket:id="upstreamSyntax" style="padding: 5px 30px;"></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://rowanj.github.io/gitx/">GitX-dev</a> - a Mac OS X Git client</li>
        </ul>
        <p></p>
        <h4>유료 Git 클라이언트</h4>
        <ul>
            <li><a href="http://www.syntevo.com/smartgithg">SmartGit/Hg</a> - 자바 어플리케이션 (명령어 기반 공식 Git 필요)</li>
            <li><a href="http://www.sourcetreeapp.com/">SourceTree</a> - A free Mac Client for Git, Mercurial, and SVN</li>
            <li><a href="http://www.git-tower.com/">Tower</a> - a Mac OS X Git client</li>
        </ul>
    <pre wicket:id="createSyntax"></pre>
    <h3><center>커맨드라인에서 기존의 저장소를 푸시하기</center></h3>
    <pre wicket:id="existingSyntax"></pre>
    <div class="span8 offset1">
        <h2><center>Git 배우기</center></h2>
        <p>만약 사용법에 자신이 없다면, Git 사용법을 더 잘 이해하기 위해
             <a href="http://book.git-scm.com/ko">Git Community Book</a> 을 볼 것을 고려해 보세요.</p>
        <h4>오픈 소스 Git 클라이언트</h4>
        <table>
        <tbody>
            <tr><td><a href="http://git-scm.com">Git</a></td><td>명령어 기반 공식 Git</td></tr>
            <tr><td><a href="http://tortoisegit.googlecode.com">TortoiseGit</a></td><td>윈도의 파일 탐색기에 통합된 UI 클라이언트 (명령어 기반 공식 Git 필요)</td></tr>
            <tr><td><a href="http://eclipse.org/egit">Eclipse/EGit</a></td><td>이클립스 IDE 플러그인 (Gitblit 과 같은 JGit 기반)</td></tr>
            <tr><td><a href="https://code.google.com/p/gitextensions/">Git Extensions</a></td><td>윈도 탐색기와 비주얼스튜디어를 위한 C#으로 개발된 기능</td></tr>
            <tr><td><a href="http://rowanj.github.io/gitx/">GitX-dev</a></td><td>맥 OS X 용 Git 클라이언트</td></tr>
        </tbody>
        </table>
        <h4>유료/클로즈드 소스 Git 클라이언트</h4>
        <table>
        <tbody>
            <tr><td><a href="http://www.syntevo.com/smartgithg">SmartGit/Hg</a></td><td>자바 어플리케이션 (명령어 기반 공식 Git 필요)</td></tr>
            <tr><td><a href="http://www.sourcetreeapp.com/">SourceTree</a></td><td>윈도와 맥에서 가능한 Git 과 Mercurial용 무료 클라이언트</td></tr>
            <tr><td><a href="http://www.git-tower.com/">Tower</a></td><td>맥 OS X 용 Git 클라이언트</td></tr>
        </tbody>
        </table>
    </div>
</div>
</div>
</div>
</div>
</wicket:extend>    
</body>
src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_nl.html
@@ -7,52 +7,53 @@
<body>
<wicket:extend>
<div class="container">
    <h2>Lege 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 repository en kan niet bekeken worden door Gitblit.
                    <p></p>
                    Push alstublieft een paar commits naar <span wicket:id="pushurl"></span>
                    <p></p>
                    <hr/>
                    Nadat u een commits gepushed heeft, kunt u deze pagina <b>verversen</b> om uw repository te bekijken.
                </div>
            </div>
        </div>
<div class="markdown">
<div class="row">
<div class="span10 offset1">
    <h3><center>Lege Repository</center></h3>
    <div class="alert alert-info">
        <span wicket:id="repository" style="font-weight: bold;">[repository]</span> is een lege repository en kan niet bekeken worden door Gitblit.
        <p></p>
        Push alstublieft een paar commits naar <span wicket:id="pushurl"></span>
        <hr/>
        Nadat u een commits gepushed heeft, kunt u deze pagina <b>verversen</b> om uw repository te bekijken.
    </div>
        
        <h3>Git Command-Line Syntax</h3>
        <span style="padding-bottom:5px;">Als u geen lokale Git repository heeft, kunt u deze repository clonen, er bestanden naar committen en dan uw commits terug 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 repository met commits heeft, kunt u deze repository als een remote toevoegen en er naar pushen.</span>
        <p></p>
        <pre wicket:id="remoteSyntax" style="padding: 5px 30px;"></pre>
        <p></p>
        <span style="padding-bottom:5px;">Indien uw repository bedoeld is om synchroon gehouden te worden met een upstream repository, dan kunt u deze toevoegen.</span>
        <p></p>
        <pre wicket:id="upstreamSyntax" style="padding: 5px 30px;"></pre>
        <p></p>
        <h3>Leer Git</h3>
        Als u niet goed weet wat u met deze informatie aan moet, raden we u 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 beter begrip over 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://rowanj.github.io/gitx/">GitX-dev</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/smartgithg">SmartGit/Hg</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>
            <li><a href="http://www.git-tower.com/">Tower</a> - een Mac OS X Git client</li>
        </ul>
    <h3><center>Maak een nieuwe repositorie via de command-line</center></h3>
    <pre wicket:id="createSyntax"></pre>
    <h3><center>Push een bestaande repositorie vanaf de command-line</center></h3>
    <pre wicket:id="existingSyntax"></pre>
    <div class="span8 offset1">
        <h2><center>Leer Git</center></h2>
        <p>Als u niet goed weet wat u met deze informatie aan moet, raden we u aan om het <a href="http://book.git-scm.com/nl">Git Community Book</a> te bestuderen voor een beter begrip over hoe u Git kunt gebruiken.</p>
        <h4>Open Source Git Programma's</h4>
        <table>
        <tbody>
            <tr><td><a href="http://git-scm.com">Git</a></td><td>de officiele, command-line Git</td></tr>
            <tr><td><a href="http://tortoisegit.googlecode.com">TortoiseGit</a></td><td>Windows bestandsverkenner integratie (officiele command-line Git is wel nodig)</td></tr>
            <tr><td><a href="http://eclipse.org/egit">Eclipse/EGit</a></td><td>Git voor de Eclipse IDE (gebaseerd op JGit, zoals Gitblit)</td></tr>
            <tr><td><a href="https://code.google.com/p/gitextensions/">Git Extensions</a></td><td>C# frontend voor Git met Windows Explorer en Visual Studio integratie</td></tr>
            <tr><td><a href="http://rowanj.github.io/gitx/">GitX-dev</a></td><td>een Mac OS X Git client</td></tr>
        </tbody>
        </table>
        <h4>Commerciele/Closed-Source Git Programma's</h4>
        <table>
        <tbody>
            <tr><td><a href="http://www.syntevo.com/smartgithg">SmartGit/Hg</a></td><td>Een Java Git, Mercurial, en SVN client applicatie (officiele command-line Git is wel nodig)</td></tr>
            <tr><td><a href="http://www.sourcetreeapp.com/">SourceTree</a></td><td>Een gratis Mac Client voor Git en Mercurial</td></tr>
            <tr><td><a href="http://www.git-tower.com/">Tower</a></td><td>een Mac OS X Git client</td></tr>
        </tbody>
        </table>
    </div>
</div>
</div>
</div>
</div>
</wicket:extend>    
</body>
src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_no.html
New file
@@ -0,0 +1,60 @@
<!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="container">
<div class="markdown">
<div class="row">
    <div class="span10 offset1">
    <h3><center>Tomt Repository</center></h3>
    <div class="alert alert-info">
        <span wicket:id="repository" style="font-weight: bold;">[repository]</span> er et tomt repository som ikke kan vises av Gitblit.
                    <p></p>
                    Vennligst push noen endringer inn til <span wicket:id="pushurl"></span>
                    <hr/>
                    Etter at du har pushet noen endringer, kan du <b>oppfriske</b> denne siden for \u00e5 se repositoriet.
        </div>
    <h3><center>Opprett et nytt repository fra kommandolinjen</center></h3>
    <pre wicket:id="createSyntax"></pre>
    <h3><center>Push et eksisternde repository fra kommandolinjen</center></h3>
    <pre wicket:id="existingSyntax"></pre>
    <div class="span8 offset1">
        <h2><center>L\u00e6r Git</center></h2>
        <p>Hvis du er usikker p\u00e5 hvordan du kan benytte denne informasjonen, vurder \u00e5 lese  <a href="http://book.git-scm.com">Git Community Book</a> eller <a href="http://progit.org/book" target="_blank">Pro Git</a> for \u00e5 f\u00e5 en bedre forst\u00e5else av hvordan du kan bruke Git. </p>
        <h4>Open Source Git Clients</h4>
        <table>
        <tbody>
            <tr><td>a href="http://git-scm.com">Git</a> - den offisielle, kommando-linje git</td></tr>
            <tr><td>a href="http://tortoisegit.googlecode.com">TortoiseGit</a> - Windows filutforsker integrasjon (krever den offisielle kommando-linje git versjonen installert</td></tr>
            <tr><td><a href="http://eclipse.org/egit">Eclipse/EGit</a> - Git for Eclipse IDE (basert p\u00e5 JGit, akkurat som Gitblit er)</tr>
            <tr><td><a href="https://code.google.com/p/gitextensions/">Git Extensions</a> - En C# frontend for Git som integrerer med filutforskeren og Visual Studio.</td></tr>
            <tr><td><a href="http://rowanj.github.io/gitx/">GitX-dev</a> - En  git klient for OS X</td></tr>
        </tbody>
        </table>
        <h4>Commercial/Closed-Source Git Clients</h4>
        <table>
        <tbody>
            <tr><td><a href="http://www.syntevo.com/smartgithg">SmartGit/Hg</a> - En Git og Mercurial klient for Windows, Mac, og Linux</td></tr>
            <tr><td><a href="http://www.sourcetreeapp.com/">SourceTree</a> - En gratis Git og Mercurial klient for Windows og Mac</td></tr>
            <tr><td>a href="http://www.git-tower.com/">Tower</a> - En git klient for Mac OS X </td></tr>
        </tbody>
        </table>
    </div>
</div>
</div>
</div>
</div>
</wicket:extend>
</body>
</html>
src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_pl.html
@@ -9,51 +9,53 @@
<body>
<wicket:extend>
<div class="container">
        <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>
<div class="markdown">
<div class="row">
<div class="span10 offset1">
    <h3><center>Puste repozytorium</center></h3>
    <div class="alert alert-info">
        <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>
        <hr/>
        Po wgraniu zmian <b>od&#347;wie&#380;</b> stron&#281;, aby podejrze&#263; repozytorium.
    </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>
        <span style="padding-bottom:5px;">If your repository is meant to be kept in sync with an upstream repository, then you may add it.</span>
        <p></p>
        <pre wicket:id="upstreamSyntax" style="padding: 5px 30px;"></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>
    <h3><center>Create a new repository on the command-line</center></h3>
    <pre wicket:id="createSyntax"></pre>
    <h3><center>Push an existing repository from the command-line</center></h3>
    <pre wicket:id="existingSyntax"></pre>
    <div class="span8 offset1">
        <h2><center>Nauka GITa</center></h2>
        <p>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>
        <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://rowanj.github.io/gitx/">GitX-dev</a> - klient GIT na Mac OS X</li>
        </ul>
        <p></p>
        <table>
        <tbody>
            <tr><td><a href="http://git-scm.com">Git</a></td><td>Oficjalny klient, dost&#281;pny przez lini&#281; polece&#324;</td></tr>
            <tr><td><a href="http://tortoisegit.googlecode.com">TortoiseGit</a></td><td>Rozszerzenie eksploratora Windows (wymaga oficjalnego, dost&#281;pnego przez lini&#281; polece&#324; klienta)</td></tr>
            <tr><td><a href="http://eclipse.org/egit">Eclipse/EGit</a></td><td>GIT dla edytora Eclipse (oparty o JGit, podobnie jak Gitblit)</td></tr>
            <tr><td><a href="https://code.google.com/p/gitextensions/">Git Extensions</a></td><td>napisana w C# fasada na GIT, udost&#281;pniaj&#261;ca integracj&#281; dla Windows Explorer oraz Visual Studio</td></tr>
            <tr><td><a href="http://rowanj.github.io/gitx/">GitX-dev</a></td><td>klient GIT na Mac OS X</td></tr>
        </tbody>
        </table>
        <h4>Komercyjni klienci GITa</h4>
        <ul>
            <li><a href="http://www.syntevo.com/smartgithg">SmartGit/Hg</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>
            <li><a href="http://www.git-tower.com/">Tower</a> - klient GIT na Mac OS X</li>
        </ul>
        <table>
        <tbody>
            <tr><td><a href="http://www.syntevo.com/smartgithg">SmartGit/Hg</a></td><td>aplikacja napisana w Javie (wymaga oficjalnego, dost&#281;pnego przez lini&#281; polece&#324; klienta)</td></tr>
            <tr><td><a href="http://www.sourcetreeapp.com/">SourceTree</a></td><td>darmowy klient GIT, Mercurial i SVN na Mac OS X</td></tr>
            <tr><td><a href="http://www.git-tower.com/">Tower</a></td><td>klient GIT na Mac OS X</td></tr>
        </tbody>
        </table>
    </div>
</div>
</div>
</div>
</div>
</wicket:extend>    
</body>
src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_pt_BR.html
@@ -7,51 +7,53 @@
<body>
<wicket:extend>
<div class="container">
    <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>
<div class="markdown">
<div class="row">
<div class="span10 offset1">
    <h3><center>Repositório Vazio</center></h3>
    <div class="alert alert-info">
        <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>
        <hr/>
        Depois de ter feito push você poderá <b>atualizar</b> esta página para visualizar seu repositório.
    </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>
        <span style="padding-bottom:5px;">If your repository is meant to be kept in sync with an upstream repository, then you may add it.</span>
        <p></p>
        <pre wicket:id="upstreamSyntax" 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>
    <h3><center>Create a new repository on the command-line</center></h3>
    <pre wicket:id="createSyntax"></pre>
    <h3><center>Push an existing repository from the command-line</center></h3>
    <pre wicket:id="existingSyntax"></pre>
    <div class="span8 offset1">
        <h2><center>Aprenda Git</center></h2>
        <p>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/pt-br">Git Community Book</a> para entender melhor como usar o Git.</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://rowanj.github.io/gitx/">GitX-dev</a> - um Cliente do Git para Mac OS X</li>
        </ul>
        <p></p>
        <table>
        <tbody>
            <tr><td><a href="http://git-scm.com">Git</a></td><td>o Git oficial através de linhas de comando</td></tr>
            <tr><td><a href="http://tortoisegit.googlecode.com">TortoiseGit</a></td><td>Faz integração do Explorer do Windows com o Git (por isso requer o Git Oficial)</td></tr>
            <tr><td><a href="http://eclipse.org/egit">Eclipse/EGit</a></td><td>Git para a IDE Eclipse (baseada no JGit, como o Gitblit)</td></tr>
            <tr><td><a href="https://code.google.com/p/gitextensions/">Git Extensions</a></td><td>Interface (em C#) para o Git cuja a característica é a integração com o Windows Explorer e o Visual Studio</td></tr>
            <tr><td><a href="http://rowanj.github.io/gitx/">GitX-dev</a></td><td>um Cliente do Git para Mac OS X</td></tr>
        </tbody>
        </table>
        <h4>Clients do Git proprietários ou com Código Proprietário</h4>
        <ul>
            <li><a href="http://www.syntevo.com/smartgithg">SmartGit/Hg</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>
            <li><a href="http://www.git-tower.com/">Tower</a> - um Cliente do Git para Mac OS X</li>
        </ul>
        <table>
        <tbody>
            <tr><td><a href="http://www.syntevo.com/smartgithg">SmartGit/Hg</a></td><td>Aplicação Client (em Java) para Git e Mercurial (por isso requer o Git Oficial)</td></tr>
            <tr><td><a href="http://www.sourcetreeapp.com/">SourceTree</a></td><td>Client gratuito para o Mac que suporta Git e Mercurial</td></tr>
            <tr><td><a href="http://www.git-tower.com/">Tower</a></td><td>um Cliente do Git para Mac OS X</td></tr>
        </tbody>
        </table>
    </div>
</div>
</div>
</div>
</div>
</wicket:extend>    
</body>
src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_zh_CN.html
@@ -7,53 +7,54 @@
<body>
<wicket:extend>
<div class="container">
    <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>
<div class="markdown">
<div class="row">
<div class="span10 offset1">
    <h3><center>空版本库</center></h3>
    <div class="alert alert-info">
        <span wicket:id="repository" style="font-weight: bold;">[repository]</span> 版本库目前为空。
        Gitblit 无法查看。
        <p></p>
        请往此网址进行推送 <span wicket:id="pushurl"></span>
        <hr/>
        当你推送完毕后你可以 <b>刷新</b> 此页面重新查看您的版本库。
    </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>
        <span style="padding-bottom:5px;">If your repository is meant to be kept in sync with an upstream repository, then you may add it.</span>
        <p></p>
        <pre wicket:id="upstreamSyntax" 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>
    <h3><center>通过命令行创建一个新的版本库</center></h3>
    <pre wicket:id="createSyntax"></pre>
    <h3><center>通过命令行推送一个已存在的版本库</center></h3>
    <pre wicket:id="existingSyntax"></pre>
    <div class="span8 offset1">
        <h2><center>学习 Git</center></h2>
        <p>如果您不明白这些信息什么意思, 您可以参考 <a href="http://book.git-scm.com/zh">Git Community Book</a> 去更加深入的学习 Git 的用法。</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://rowanj.github.io/gitx/">GitX-dev</a> - Mac OS X Git 客户端</li>
        </ul>
        <p></p>
        <table>
        <tbody>
            <tr><td><a href="http://git-scm.com">Git</a></td><td>官方, 命令行版本 Git</td></tr>
            <tr><td><a href="http://tortoisegit.googlecode.com">TortoiseGit</a></td><td>与 Windows 资源管理器集成 (需要官方, 命令行 Git 的支持)</td></tr>
            <tr><td><a href="http://eclipse.org/egit">Eclipse/EGit</a></td><td>Git for the Eclipse IDE (基于 JGit, 类似 Gitblit)</td></tr>
            <tr><td><a href="https://code.google.com/p/gitextensions/">Git Extensions</a></td><td>C# 版本的 Git 前端,与 Windows 资源管理器和 Visual Studio 集成</td></tr>
            <tr><td><a href="http://rowanj.github.io/gitx/">GitX-dev</a></td><td>Mac OS X Git 客户端</td></tr>
        </tbody>
        </table>
        <h4>商业/闭源 Git 客户端</h4>
        <ul>
            <li><a href="http://www.syntevo.com/smartgithg">SmartGit/Hg</a> - Java 版本的支持 Git, Mercurial 和 SVN 客户端应用 (需要官方, 命令行 Git 的支持)</li>
            <li><a href="http://www.sourcetreeapp.com/">SourceTree</a> - 免费的 Mac Git Mercurial 以及 SVN 客户端, Mercurial, and SVN</li>
            <li><a href="http://www.git-tower.com/">Tower</a> - Mac OS X Git 客户端</li>
        </ul>
        <table>
        <tbody>
            <tr><td><a href="http://www.syntevo.com/smartgithg">SmartGit/Hg</a></td><td>Java 版本的支持 Git, Mercurial 和 SVN 客户端应用 </td></tr>
            <tr><td><a href="http://www.sourcetreeapp.com/">SourceTree</a></td><td>免费的 Mac Git Mercurial 以及 SVN 客户端 and Mercurial</td></tr>
            <tr><td><a href="http://www.git-tower.com/">Tower</a></td><td>Mac OS X Git 客户端</td></tr>
        </tbody>
        </table>
    </div>
</div>
</div>
</div>
</div>
</wicket:extend>    
</body>
src/main/java/com/gitblit/wicket/pages/MyTicketsPage.html
New file
@@ -0,0 +1,84 @@
<!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="container">
            <div class="row" style="padding-top:15px;min-height:500px;" >
                <div class="tab-pane active" id="tickets">
                    <!-- query controls -->
                    <div class="span3">
                        <div class="hidden-phone">
                            <div wicket:id="userTitlePanel"></div>
                            <!-- search tickets form -->
                            <form class="form-search" style="margin: 10px 0px;" wicket:id="ticketSearchForm">
                                <div class="input-append">
                                    <input type="text" class="input-medium search-query" style="border-radius: 14px 0 0 14px; padding-left: 14px;" id="ticketSearchBox" wicket:id="ticketSearchBox" value=""/>
                                    <button class="btn" style="border-radius: 0 14px 14px 0px;margin-left:-5px;" type="submit"><i class="icon-search"></i></button>
                                </div>
                            </form>
                            <!--  query list -->
                            <ul class="nav nav-list">
                                <li class="nav-header"><wicket:message key="gb.queries"></wicket:message></li>
                                <li><a wicket:id="changesQuery"><i class="fa fa-code-fork"></i> <wicket:message key="gb.proposalTickets"></wicket:message></a></li>
                                <li><a wicket:id="bugsQuery"><i class="fa fa-bug"></i> <wicket:message key="gb.bugTickets"></wicket:message></a></li>
                                <li><a wicket:id="enhancementsQuery"><i class="fa fa-magic"></i> <wicket:message key="gb.enhancementTickets"></wicket:message></a></li>
                                <li><a wicket:id="tasksQuery"><i class="fa fa-ticket"></i> <wicket:message key="gb.taskTickets"></wicket:message></a></li>
                                <li><a wicket:id="questionsQuery"><i class="fa fa-question"></i> <wicket:message key="gb.questionTickets"></wicket:message></a></li>
                                <li wicket:id="userDivider" class="divider"></li>
                                <li><a wicket:id="createdQuery"><i class="fa fa-user"></i> <wicket:message key="gb.yourCreatedTickets"></wicket:message></a></li>
                                <li><a wicket:id="responsibleQuery"><i class="fa fa-user"></i> <wicket:message key="gb.yourAssignedTickets"></wicket:message></a></li>
                                <li><a wicket:id="watchedQuery"><i class="fa fa-eye"></i> <wicket:message key="gb.yourWatchedTickets"></wicket:message></a></li>
                                <li><a wicket:id="mentionsQuery"><i class="fa fa-comment"></i> <wicket:message key="gb.mentionsMeTickets"></wicket:message></a></li>
                                <li class="divider"></li>
                                <li><a wicket:id="resetQuery"><i class="fa fa-bolt"></i> <wicket:message key="gb.reset"></wicket:message></a></li>
                            </ul>
                        </div>
                    </div>
                    <!-- tickets -->
                    <div class="span9">
                        <div class="btn-toolbar" style="margin-top: 0px;">
                            <div class="btn-group">
                                <a class="btn dropdown-toggle" data-toggle="dropdown" href="#"><wicket:message key="gb.status"></wicket:message>: <span style="font-weight:bold;" wicket:id="selectedStatii"></span> <span class="caret"></span></a>
                                <ul class="dropdown-menu">
                                    <li><a wicket:id="openTickets"><wicket:message key="gb.open"></wicket:message></a></li>
                                    <li><a wicket:id="closedTickets"><wicket:message key="gb.closed"></wicket:message></a></li>
                                    <li><a wicket:id="allTickets"><wicket:message key="gb.all"></wicket:message></a></li>
                                    <li class="divider"></li>
                                    <li wicket:id="statii"><span wicket:id="statusLink"></span></li>
                                </ul>
                            </div>
                            <div class="btn-group">
                                <a class="btn dropdown-toggle" data-toggle="dropdown" href="#"><i class="fa fa-sort"></i> <wicket:message key="gb.sort"></wicket:message>: <span style="font-weight:bold;" wicket:id="currentSort"></span> <span class="caret"></span></a>
                                <ul class="dropdown-menu">
                                    <li wicket:id="sort"><span wicket:id="sortLink"></span></li>
                                </ul>
                            </div>
                            <div class="btn-group pull-right">
                                <div class="pagination pagination-right pagination-small">
                                    <ul>
                                        <li><a wicket:id="prevLink"><i class="fa fa-angle-double-left"></i></a></li>
                                        <li wicket:id="pageLink"><span wicket:id="page"></span></li>
                                        <li><a wicket:id="nextLink"><i class="fa fa-angle-double-right"></i></a></li>
                                    </ul>
                                </div>
                            </div>
                        </div>
                        <div wicket:id="ticketList"></div>
                    </div>
                </div>
            </div>
        </div>
</wicket:extend>
</body>
</html>
src/main/java/com/gitblit/wicket/pages/MyTicketsPage.java
New file
@@ -0,0 +1,392 @@
/*
 * Copyright 2014 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.repeater.Item;
import org.apache.wicket.markup.repeater.data.DataView;
import org.apache.wicket.markup.repeater.data.ListDataProvider;
import com.gitblit.Keys;
import com.gitblit.models.TicketModel;
import com.gitblit.models.TicketModel.Status;
import com.gitblit.models.UserModel;
import com.gitblit.tickets.QueryBuilder;
import com.gitblit.tickets.QueryResult;
import com.gitblit.tickets.TicketIndexer.Lucene;
import com.gitblit.utils.ArrayUtils;
import com.gitblit.utils.StringUtils;
import com.gitblit.wicket.GitBlitWebSession;
import com.gitblit.wicket.TicketsUI;
import com.gitblit.wicket.TicketsUI.TicketSort;
import com.gitblit.wicket.WicketUtils;
import com.gitblit.wicket.panels.LinkPanel;
import com.gitblit.wicket.panels.TicketListPanel;
import com.gitblit.wicket.panels.TicketSearchForm;
import com.gitblit.wicket.panels.UserTitlePanel;
/**
 * My Tickets page
 *
 * @author Christian Buisson
 * @author James Moger
 */
public class MyTicketsPage extends RootPage {
    public MyTicketsPage() {
        this(null);
    }
    public MyTicketsPage(PageParameters params)    {
        super(params);
        setupPage("", getString("gb.myTickets"));
        UserModel currentUser = GitBlitWebSession.get().getUser();
        if (currentUser == null || UserModel.ANONYMOUS.equals(currentUser)) {
            setRedirect(true);
            setResponsePage(getApplication().getHomePage());
            return;
        }
        final String username = currentUser.getName();
        final String[] statiiParam = (params == null) ? TicketsUI.openStatii : params.getStringArray(Lucene.status.name());
        final String assignedToParam = (params == null) ? "" : params.getString(Lucene.responsible.name(), null);
        final String milestoneParam = (params == null) ? "" : params.getString(Lucene.milestone.name(), null);
        final String queryParam = (params == null || StringUtils.isEmpty(params.getString("q", null))) ? "watchedby:" + username : params.getString("q", null);
        final String searchParam = (params == null) ? "" : params.getString("s", null);
        final String sortBy = (params == null) ? "" : Lucene.fromString(params.getString("sort", Lucene.created.name())).name();
        final boolean desc = (params == null) ? true : !"asc".equals(params.getString("direction", "desc"));
        // add the user title panel
        add(new UserTitlePanel("userTitlePanel", currentUser, getString("gb.myTickets")));
        // add search form
        add(new TicketSearchForm("ticketSearchForm", null, searchParam, getClass(), params));
        // standard queries
        add(new BookmarkablePageLink<Void>("changesQuery", MyTicketsPage.class,
                queryParameters(
                        Lucene.type.matches(TicketModel.Type.Proposal.name()),
                        milestoneParam,
                        statiiParam,
                        assignedToParam,
                        sortBy,
                        desc,
                        1)));
        add(new BookmarkablePageLink<Void>("bugsQuery", MyTicketsPage.class,
                queryParameters(
                        Lucene.type.matches(TicketModel.Type.Bug.name()),
                        milestoneParam,
                        statiiParam,
                        assignedToParam,
                        sortBy,
                        desc,
                        1)));
        add(new BookmarkablePageLink<Void>("enhancementsQuery", MyTicketsPage.class,
                queryParameters(
                        Lucene.type.matches(TicketModel.Type.Enhancement.name()),
                        milestoneParam,
                        statiiParam,
                        assignedToParam,
                        sortBy,
                        desc,
                        1)));
        add(new BookmarkablePageLink<Void>("tasksQuery", MyTicketsPage.class,
                queryParameters(
                        Lucene.type.matches(TicketModel.Type.Task.name()),
                        milestoneParam,
                        statiiParam,
                        assignedToParam,
                        sortBy,
                        desc,
                        1)));
        add(new BookmarkablePageLink<Void>("questionsQuery", MyTicketsPage.class,
                queryParameters(
                        Lucene.type.matches(TicketModel.Type.Question.name()),
                        milestoneParam,
                        statiiParam,
                        assignedToParam,
                        sortBy,
                        desc,
                        1)));
        add(new BookmarkablePageLink<Void>("resetQuery", MyTicketsPage.class,
                queryParameters(
                        null,
                        milestoneParam,
                        TicketsUI.openStatii,
                        null,
                        null,
                        true,
                        1)));
        add(new Label("userDivider"));
        add(new BookmarkablePageLink<Void>("createdQuery", MyTicketsPage.class,
                queryParameters(
                        Lucene.createdby.matches(username),
                        milestoneParam,
                        statiiParam,
                        assignedToParam,
                        sortBy,
                        desc,
                        1)));
        add(new BookmarkablePageLink<Void>("watchedQuery", MyTicketsPage.class,
                queryParameters(
                        Lucene.watchedby.matches(username),
                        milestoneParam,
                        statiiParam,
                        assignedToParam,
                        sortBy,
                        desc,
                        1)));
        add(new BookmarkablePageLink<Void>("mentionsQuery", MyTicketsPage.class,
                queryParameters(
                        Lucene.mentions.matches(username),
                        milestoneParam,
                        statiiParam,
                        assignedToParam,
                        sortBy,
                        desc,
                        1)));
        add(new BookmarkablePageLink<Void>("responsibleQuery", MyTicketsPage.class,
                queryParameters(
                        Lucene.responsible.matches(username),
                        milestoneParam,
                        statiiParam,
                        assignedToParam,
                        sortBy,
                        desc,
                        1)));
        // states
        if (ArrayUtils.isEmpty(statiiParam)) {
            add(new Label("selectedStatii", getString("gb.all")));
        } else {
            add(new Label("selectedStatii", StringUtils.flattenStrings(Arrays.asList(statiiParam), ",")));
        }
        add(new BookmarkablePageLink<Void>("openTickets", MyTicketsPage.class, queryParameters(queryParam, milestoneParam, TicketsUI.openStatii, assignedToParam, sortBy, desc, 1)));
        add(new BookmarkablePageLink<Void>("closedTickets", MyTicketsPage.class, queryParameters(queryParam, milestoneParam, TicketsUI.closedStatii, assignedToParam, sortBy, desc, 1)));
        add(new BookmarkablePageLink<Void>("allTickets", MyTicketsPage.class, queryParameters(queryParam, milestoneParam, null, assignedToParam, sortBy, desc, 1)));
        // by status
        List<Status> statii = new ArrayList<Status>(Arrays.asList(Status.values()));
        statii.remove(Status.Closed);
        ListDataProvider<Status> resolutionsDp = new ListDataProvider<Status>(statii);
        DataView<Status> statiiLinks = new DataView<Status>("statii", resolutionsDp) {
            private static final long serialVersionUID = 1L;
            @Override
            public void populateItem(final Item<Status> item) {
                final Status status = item.getModelObject();
                PageParameters p = queryParameters(queryParam, milestoneParam, new String [] { status.name().toLowerCase() }, assignedToParam, sortBy, desc, 1);
                String css = TicketsUI.getStatusClass(status);
                item.add(new LinkPanel("statusLink", css, status.toString(), MyTicketsPage.class, p).setRenderBodyOnly(true));
            }
        };
        add(statiiLinks);
        List<TicketSort> sortChoices = new ArrayList<TicketSort>();
        sortChoices.add(new TicketSort(getString("gb.sortNewest"), Lucene.created.name(), true));
        sortChoices.add(new TicketSort(getString("gb.sortOldest"), Lucene.created.name(), false));
        sortChoices.add(new TicketSort(getString("gb.sortMostRecentlyUpdated"), Lucene.updated.name(), true));
        sortChoices.add(new TicketSort(getString("gb.sortLeastRecentlyUpdated"), Lucene.updated.name(), false));
        sortChoices.add(new TicketSort(getString("gb.sortMostComments"), Lucene.comments.name(), true));
        sortChoices.add(new TicketSort(getString("gb.sortLeastComments"), Lucene.comments.name(), false));
        sortChoices.add(new TicketSort(getString("gb.sortMostPatchsetRevisions"), Lucene.patchsets.name(), true));
        sortChoices.add(new TicketSort(getString("gb.sortLeastPatchsetRevisions"), Lucene.patchsets.name(), false));
        sortChoices.add(new TicketSort(getString("gb.sortMostVotes"), Lucene.votes.name(), true));
        sortChoices.add(new TicketSort(getString("gb.sortLeastVotes"), Lucene.votes.name(), false));
        TicketSort currentSort = sortChoices.get(0);
        for (TicketSort ts : sortChoices) {
            if (ts.sortBy.equals(sortBy) && desc == ts.desc) {
                currentSort = ts;
                break;
            }
        }
        add(new Label("currentSort", currentSort.name));
        ListDataProvider<TicketSort> sortChoicesDp = new ListDataProvider<TicketSort>(sortChoices);
        DataView<TicketSort> sortMenu = new DataView<TicketSort>("sort", sortChoicesDp) {
            private static final long serialVersionUID = 1L;
            @Override
            public void populateItem(final Item<TicketSort> item) {
                final TicketSort ts = item.getModelObject();
                PageParameters params = queryParameters(queryParam, milestoneParam, statiiParam, assignedToParam, ts.sortBy, ts.desc, 1);
                item.add(new LinkPanel("sortLink", null, ts.name, MyTicketsPage.class, params).setRenderBodyOnly(true));
            }
        };
        add(sortMenu);
        // Build Query here
        QueryBuilder qb = new QueryBuilder(queryParam);
        if (!qb.containsField(Lucene.status.name()) && !ArrayUtils.isEmpty(statiiParam)) {
            // specify the states
            boolean not = false;
            QueryBuilder q = new QueryBuilder();
            for (String state : statiiParam) {
                if (state.charAt(0) == '!') {
                    not = true;
                    q.and(Lucene.status.doesNotMatch(state.substring(1)));
                } else {
                    q.or(Lucene.status.matches(state));
                }
            }
            if (not) {
                qb.and(q.toString());
            } else {
                qb.and(q.toSubquery().toString());
            }
        }
        final String luceneQuery;
        if (qb.containsField(Lucene.createdby.name())
                || qb.containsField(Lucene.responsible.name())
                || qb.containsField(Lucene.watchedby.name())) {
            // focused "my tickets" query
            luceneQuery = qb.build();
        } else {
            // general "my tickets" query
            QueryBuilder myQuery = new QueryBuilder();
            myQuery.or(Lucene.createdby.matches(username));
            myQuery.or(Lucene.responsible.matches(username));
            myQuery.or(Lucene.watchedby.matches(username));
            myQuery.and(qb.toSubquery().toString());
            luceneQuery = myQuery.build();
        }
        // paging links
        int page = (params != null) ? Math.max(1, WicketUtils.getPage(params)) : 1;
        int pageSize = app().settings().getInteger(Keys.tickets.perPage, 25);
        List<QueryResult> results;
        if(StringUtils.isEmpty(searchParam)) {
            results = app().tickets().queryFor(luceneQuery, page, pageSize, sortBy, desc);
        } else {
            results = app().tickets().searchFor(null, searchParam, page, pageSize);
        }
        int totalResults = results.size() == 0 ? 0 : results.get(0).totalResults;
        buildPager(queryParam, milestoneParam, statiiParam, assignedToParam, sortBy, desc, page, pageSize, results.size(), totalResults);
        final boolean showSwatch = app().settings().getBoolean(Keys.web.repositoryListSwatches, true);
        add(new TicketListPanel("ticketList", results, showSwatch, true));
    }
    protected PageParameters queryParameters(
            String query,
            String milestone,
            String[] states,
            String assignedTo,
            String sort,
            boolean descending,
            int page) {
        PageParameters params = WicketUtils.newRepositoryParameter("");
        if (!StringUtils.isEmpty(query)) {
            params.add("q", query);
        }
        if (!StringUtils.isEmpty(milestone)) {
            params.add(Lucene.milestone.name(), milestone);
        }
        if (!ArrayUtils.isEmpty(states)) {
            for (String state : states) {
                params.add(Lucene.status.name(), state);
            }
        }
        if (!StringUtils.isEmpty(assignedTo)) {
            params.add(Lucene.responsible.name(), assignedTo);
        }
        if (!StringUtils.isEmpty(sort)) {
            params.add("sort", sort);
        }
        if (!descending) {
            params.add("direction", "asc");
        }
        if (page > 1) {
            params.add("pg", "" + page);
        }
        return params;
    }
    protected void buildPager(
            final String query,
            final String milestone,
            final String [] states,
            final String assignedTo,
            final String sort,
            final boolean desc,
            final int page,
            int pageSize,
            int count,
            int total) {
        boolean showNav = total > (2 * pageSize);
        boolean allowPrev = page > 1;
        boolean allowNext = (pageSize * (page - 1) + count) < total;
        add(new BookmarkablePageLink<Void>("prevLink", MyTicketsPage.class, queryParameters(query, milestone, states, assignedTo, sort, desc, page - 1)).setEnabled(allowPrev).setVisible(showNav));
        add(new BookmarkablePageLink<Void>("nextLink", MyTicketsPage.class, queryParameters(query, milestone, states, assignedTo, sort, desc, page + 1)).setEnabled(allowNext).setVisible(showNav));
        if (total <= pageSize) {
            add(new Label("pageLink").setVisible(false));
            return;
        }
        // determine page numbers to display
        int pages = count == 0 ? 0 : ((total / pageSize) + (total % pageSize == 0 ? 0 : 1));
        // preferred number of pagelinks
        int segments = 5;
        if (pages < segments) {
            // not enough data for preferred number of page links
            segments = pages;
        }
        int minpage = Math.min(Math.max(1, page - 2), pages - (segments - 1));
        int maxpage = Math.min(pages, minpage + (segments - 1));
        List<Integer> sequence = new ArrayList<Integer>();
        for (int i = minpage; i <= maxpage; i++) {
            sequence.add(i);
        }
        ListDataProvider<Integer> pagesDp = new ListDataProvider<Integer>(sequence);
        DataView<Integer> pagesView = new DataView<Integer>("pageLink", pagesDp) {
            private static final long serialVersionUID = 1L;
            @Override
            public void populateItem(final Item<Integer> item) {
                final Integer i = item.getModelObject();
                LinkPanel link = new LinkPanel("page", null, "" + i, MyTicketsPage.class, queryParameters(query, milestone, states, assignedTo, sort, desc, i));
                link.setRenderBodyOnly(true);
                if (i == page) {
                    WicketUtils.setCssClass(item, "active");
                }
                item.add(link);
            }
        };
        add(pagesView);
    }
}
src/main/java/com/gitblit/wicket/pages/NewMilestonePage.html
New file
@@ -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">
<wicket:extend>
<body onload="document.getElementById('name').focus();">
<div class="container">
    <!-- page header -->
    <div class="title" style="font-size: 22px; color: rgb(0, 32, 96);padding: 3px 0px 7px;">
        <span class="project"><wicket:message key="gb.newMilestone"></wicket:message></span>
    </div>
    <form style="padding-top:5px;" wicket:id="editForm">
    <div class="row">
    <div class="span12">
        <!-- New Milestone Table -->
        <table class="ticket">
            <tr><th><wicket:message key="gb.milestone"></wicket:message></th><td class="edit"><input class="input-large" type="text" wicket:id="name" id="name"></input></td></tr>
            <tr><th><wicket:message key="gb.due"></wicket:message></th><td class="edit"><input class="input-large" type="text" wicket:id="due"></input> &nbsp;<span class="help-inline" wicket:id="dueFormat"></span></td></tr>
        </table>
    </div>
    </div>
    <div class="row">
    <div class="span12">
        <div class="form-actions"><input class="btn btn-appmenu" type="submit" value="Create" wicket:message="value:gb.create" wicket:id="create" /> &nbsp; <input class="btn" type="submit" value="Cancel" wicket:message="value:gb.cancel" wicket:id="cancel" /></div>
    </div>
    </div>
    </form>
</div>
</body>
</wicket:extend>
</html>
src/main/java/com/gitblit/wicket/pages/NewMilestonePage.java
New file
@@ -0,0 +1,140 @@
/*
 * Copyright 2014 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.Date;
import org.apache.wicket.PageParameters;
import org.apache.wicket.RestartResponseException;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.markup.html.form.AjaxButton;
import org.apache.wicket.extensions.markup.html.form.DateTextField;
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.IModel;
import org.apache.wicket.model.Model;
import com.gitblit.models.RepositoryModel;
import com.gitblit.models.UserModel;
import com.gitblit.tickets.TicketMilestone;
import com.gitblit.utils.StringUtils;
import com.gitblit.utils.TimeUtils;
import com.gitblit.wicket.GitBlitWebSession;
import com.gitblit.wicket.WicketUtils;
/**
 * Page for creating a new milestone.
 *
 * @author James Moger
 *
 */
public class NewMilestonePage extends RepositoryPage {
    private IModel<String> nameModel;
    private IModel<Date> dueModel;
    public NewMilestonePage(PageParameters params) {
        super(params);
        RepositoryModel model = getRepositoryModel();
        if (!app().tickets().isAcceptingTicketUpdates(model)) {
            // ticket service is read-only
            throw new RestartResponseException(TicketsPage.class, WicketUtils.newOpenTicketsParameter(repositoryName));
        }
        UserModel currentUser = GitBlitWebSession.get().getUser();
        if (currentUser == null) {
            currentUser = UserModel.ANONYMOUS;
        }
        if (!currentUser.isAuthenticated || !currentUser.canAdmin(model)) {
            // administration prohibited
            throw new RestartResponseException(TicketsPage.class, WicketUtils.newOpenTicketsParameter(repositoryName));
        }
        setStatelessHint(false);
        setOutputMarkupId(true);
        Form<Void> form = new Form<Void>("editForm");
        add(form);
        nameModel = Model.of("");
        dueModel = Model.of(new Date(System.currentTimeMillis() + TimeUtils.ONEDAY));
        form.add(new TextField<String>("name", nameModel));
        form.add(new DateTextField("due", dueModel, "yyyy-MM-dd"));
        form.add(new Label("dueFormat", "yyyy-MM-dd"));
        form.add(new AjaxButton("create") {
            private static final long serialVersionUID = 1L;
            @Override
            protected void onSubmit(AjaxRequestTarget target, Form<?> form) {
                String name = nameModel.getObject();
                if (StringUtils.isEmpty(name)) {
                    // invalid name
                    return;
                }
                TicketMilestone milestone = app().tickets().getMilestone(getRepositoryModel(), name);
                if (milestone != null) {
                    // milestone already exists
                    return;
                }
                Date due = dueModel.getObject();
                UserModel currentUser = GitBlitWebSession.get().getUser();
                String createdBy = currentUser.username;
                milestone = app().tickets().createMilestone(getRepositoryModel(), name, createdBy);
                if (milestone != null) {
                    milestone.due = due;
                    app().tickets().updateMilestone(getRepositoryModel(), milestone, createdBy);
                    throw new RestartResponseException(TicketsPage.class, WicketUtils.newOpenTicketsParameter(repositoryName));
                } else {
                    // TODO error
                }
            }
        });
        Button cancel = new Button("cancel") {
            private static final long serialVersionUID = 1L;
            @Override
            public void onSubmit() {
                setResponsePage(TicketsPage.class, WicketUtils.newOpenTicketsParameter(repositoryName));
            }
        };
        cancel.setDefaultFormProcessing(false);
        form.add(cancel);
    }
    @Override
    protected String getPageName() {
        return getString("gb.newMilestone");
    }
    @Override
    protected Class<? extends BasePage> getRepoNavPageClass() {
        return TicketsPage.class;
    }
}
src/main/java/com/gitblit/wicket/pages/NewRepositoryPage.html
New file
@@ -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">
<wicket:extend>
<body onload="document.getElementById('name').focus();">
    <form style="padding-top:5px;" wicket:id="editForm">
<div class="row">
    <div class="span12">
        <div wicket:id="namePanel"></div>
        <hr/>
        <div wicket:id="accessPolicyPanel"></div>
        <hr/>
        <h4><wicket:message key="gb.initialCommit"></wicket:message></h4>
        <p><wicket:message key="gb.initialCommitDescription"></wicket:message></p>
        <div wicket:id="addReadme"></div>
        <div wicket:id="addGitIgnore"></div>
        <div wicket:id="addGitFlow"></div>
    </div>
</div>
<div class="row">
<div class="span12">
    <div class="form-actions"><input class="btn btn-appmenu" type="submit" value="Create" wicket:message="value:gb.create" wicket:id="create" /></div>
</div>
</div>
</form>
</body>
</wicket:extend>
</html>
src/main/java/com/gitblit/wicket/pages/NewRepositoryPage.java
New file
@@ -0,0 +1,372 @@
/*
 * Copyright 2014 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.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.apache.wicket.behavior.SimpleAttributeModifier;
import org.apache.wicket.markup.html.form.Button;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.model.CompoundPropertyModel;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.Model;
import org.eclipse.jgit.dircache.DirCache;
import org.eclipse.jgit.dircache.DirCacheBuilder;
import org.eclipse.jgit.dircache.DirCacheEntry;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.Config;
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 com.gitblit.Constants;
import com.gitblit.Constants.AccessRestrictionType;
import com.gitblit.Constants.AuthorizationControl;
import com.gitblit.GitBlitException;
import com.gitblit.Keys;
import com.gitblit.models.RepositoryModel;
import com.gitblit.models.UserModel;
import com.gitblit.utils.ArrayUtils;
import com.gitblit.utils.FileUtils;
import com.gitblit.utils.StringUtils;
import com.gitblit.wicket.GitBlitWebSession;
import com.gitblit.wicket.WicketUtils;
import com.gitblit.wicket.panels.AccessPolicyPanel;
import com.gitblit.wicket.panels.BooleanChoiceOption;
import com.gitblit.wicket.panels.BooleanOption;
import com.gitblit.wicket.panels.RepositoryNamePanel;
public class NewRepositoryPage extends RootSubPage {
    private final RepositoryModel repositoryModel;
    private IModel<Boolean> addReadmeModel;
    private Model<String> gitignoreModel;
    private IModel<Boolean> addGitflowModel;
    private IModel<Boolean> addGitignoreModel;
    private AccessPolicyPanel accessPolicyPanel;
    private RepositoryNamePanel namePanel;
    public NewRepositoryPage() {
        // create constructor
        super();
        repositoryModel = new RepositoryModel();
        setupPage(getString("gb.newRepository"), "");
        setStatelessHint(false);
        setOutputMarkupId(true);
    }
    @Override
    protected boolean requiresPageMap() {
        return true;
    }
    @Override
    protected Class<? extends BasePage> getRootNavPageClass() {
        return RepositoriesPage.class;
    }
    @Override
    protected void onInitialize() {
        super.onInitialize();
        CompoundPropertyModel<RepositoryModel> rModel = new CompoundPropertyModel<>(repositoryModel);
        Form<RepositoryModel> form = new Form<RepositoryModel>("editForm", rModel) {
            private static final long serialVersionUID = 1L;
            @Override
            protected void onSubmit() {
                try {
                    if (!namePanel.updateModel(repositoryModel)) {
                        return;
                    }
                    accessPolicyPanel.updateModel(repositoryModel);
                    repositoryModel.owners = new ArrayList<String>();
                    repositoryModel.owners.add(GitBlitWebSession.get().getUsername());
                    // setup branch defaults
                    boolean useGitFlow = addGitflowModel.getObject();
                    repositoryModel.HEAD = Constants.R_MASTER;
                    repositoryModel.mergeTo = Constants.MASTER;
                    if (useGitFlow) {
                        // tickets normally merge to develop unless they are hotfixes
                        repositoryModel.mergeTo = Constants.DEVELOP;
                    }
                    repositoryModel.allowForks = app().settings().getBoolean(Keys.web.allowForking, true);
                    // optionally generate an initial commit
                    boolean addReadme = addReadmeModel.getObject();
                    String gitignore = null;
                    boolean addGitignore = addGitignoreModel.getObject();
                    if (addGitignore) {
                        gitignore = gitignoreModel.getObject();
                        if (StringUtils.isEmpty(gitignore)) {
                            throw new GitBlitException(getString("gb.pleaseSelectGitIgnore"));
                        }
                    }
                    // init the repository
                    app().gitblit().updateRepositoryModel(repositoryModel.name, repositoryModel, true);
                    // optionally create an initial commit
                    initialCommit(repositoryModel, addReadme, gitignore, useGitFlow);
                } catch (GitBlitException e) {
                    error(e.getMessage());
                    return;
                }
                setRedirect(true);
                setResponsePage(SummaryPage.class, WicketUtils.newRepositoryParameter(repositoryModel.name));
            }
        };
        // do not let the browser pre-populate these fields
        form.add(new SimpleAttributeModifier("autocomplete", "off"));
        namePanel = new RepositoryNamePanel("namePanel", repositoryModel);
        form.add(namePanel);
        // prepare the default access controls
        AccessRestrictionType defaultRestriction = AccessRestrictionType.fromName(
                app().settings().getString(Keys.git.defaultAccessRestriction, AccessRestrictionType.PUSH.name()));
        if (AccessRestrictionType.NONE == defaultRestriction) {
            defaultRestriction = AccessRestrictionType.PUSH;
        }
        AuthorizationControl defaultControl = AuthorizationControl.fromName(
                app().settings().getString(Keys.git.defaultAuthorizationControl, AuthorizationControl.NAMED.name()));
        if (AuthorizationControl.AUTHENTICATED == defaultControl) {
            defaultRestriction = AccessRestrictionType.PUSH;
        }
        repositoryModel.authorizationControl = defaultControl;
        repositoryModel.accessRestriction = defaultRestriction;
        accessPolicyPanel = new AccessPolicyPanel("accessPolicyPanel", repositoryModel);
        form.add(accessPolicyPanel);
        //
        // initial commit options
        //
        // add README
        addReadmeModel = Model.of(false);
        form.add(new BooleanOption("addReadme",
                getString("gb.initWithReadme"),
                getString("gb.initWithReadmeDescription"),
                addReadmeModel));
        // add .gitignore
        File gitignoreDir = app().runtime().getFileOrFolder(Keys.git.gitignoreFolder, "${baseFolder}/gitignore");
        File [] files = gitignoreDir.listFiles();
        if (files == null) {
            files = new File[0];
        }
        List<String> gitignores = new ArrayList<String>();
        for (File file : files) {
            if (file.isFile() && file.getName().endsWith(".gitignore")) {
                gitignores.add(StringUtils.stripFileExtension(file.getName()));
            }
        }
        Collections.sort(gitignores);
        gitignoreModel = Model.of("");
        addGitignoreModel = Model.of(false);
        form.add(new BooleanChoiceOption<String>("addGitIgnore",
                getString("gb.initWithGitignore"),
                getString("gb.initWithGitignoreDescription"),
                addGitignoreModel,
                gitignoreModel,
                gitignores).setVisible(gitignores.size() > 0));
        // TODO consider gitflow at creation (ticket-55)
        addGitflowModel = Model.of(false);
        form.add(new BooleanOption("addGitFlow",
                "Include a .gitflow file",
                "This will generate a config file which guides Git clients in setting up Gitflow branches.",
                addGitflowModel).setVisible(false));
        form.add(new Button("create"));
        add(form);
    }
    /**
     * Prepare the initial commit for the repository.
     *
     * @param repository
     * @param addReadme
     * @param gitignore
     * @param addGitFlow
     * @return true if an initial commit was created
     */
    protected boolean initialCommit(RepositoryModel repository, boolean addReadme, String gitignore,
            boolean addGitFlow) {
        boolean initialCommit = addReadme || !StringUtils.isEmpty(gitignore) || addGitFlow;
        if (!initialCommit) {
            return false;
        }
        // build an initial commit
        boolean success = false;
        Repository db = app().repositories().getRepository(repositoryModel.name);
        ObjectInserter odi = db.newObjectInserter();
        try {
            UserModel user = GitBlitWebSession.get().getUser();
            PersonIdent author = new PersonIdent(user.getDisplayName(), user.emailAddress);
            DirCache newIndex = DirCache.newInCore();
            DirCacheBuilder indexBuilder = newIndex.builder();
            if (addReadme) {
                // insert a README
                String title = StringUtils.stripDotGit(StringUtils.getLastPathElement(repositoryModel.name));
                String description = repositoryModel.description == null ? "" : repositoryModel.description;
                String readme = String.format("## %s\n\n%s\n\n", title, description);
                byte [] bytes = readme.getBytes(Constants.ENCODING);
                DirCacheEntry entry = new DirCacheEntry("README.md");
                entry.setLength(bytes.length);
                entry.setLastModified(System.currentTimeMillis());
                entry.setFileMode(FileMode.REGULAR_FILE);
                entry.setObjectId(odi.insert(org.eclipse.jgit.lib.Constants.OBJ_BLOB, bytes));
                indexBuilder.add(entry);
            }
            if (!StringUtils.isEmpty(gitignore)) {
                // insert a .gitignore file
                File dir = app().runtime().getFileOrFolder(Keys.git.gitignoreFolder, "${baseFolder}/gitignore");
                File file = new File(dir, gitignore + ".gitignore");
                if (file.exists() && file.length() > 0) {
                    byte [] bytes = FileUtils.readContent(file);
                    if (!ArrayUtils.isEmpty(bytes)) {
                        DirCacheEntry entry = new DirCacheEntry(".gitignore");
                        entry.setLength(bytes.length);
                        entry.setLastModified(System.currentTimeMillis());
                        entry.setFileMode(FileMode.REGULAR_FILE);
                        entry.setObjectId(odi.insert(org.eclipse.jgit.lib.Constants.OBJ_BLOB, bytes));
                        indexBuilder.add(entry);
                    }
                }
            }
            if (addGitFlow) {
                // insert a .gitflow file
                Config config = new Config();
                config.setString("gitflow", null, "masterBranch", Constants.MASTER);
                config.setString("gitflow", null, "developBranch", Constants.DEVELOP);
                config.setString("gitflow", null, "featureBranchPrefix", "feature/");
                config.setString("gitflow", null, "releaseBranchPrefix", "release/");
                config.setString("gitflow", null, "hotfixBranchPrefix", "hotfix/");
                config.setString("gitflow", null, "supportBranchPrefix", "support/");
                config.setString("gitflow", null, "versionTagPrefix", "");
                byte [] bytes = config.toText().getBytes(Constants.ENCODING);
                DirCacheEntry entry = new DirCacheEntry(".gitflow");
                entry.setLength(bytes.length);
                entry.setLastModified(System.currentTimeMillis());
                entry.setFileMode(FileMode.REGULAR_FILE);
                entry.setObjectId(odi.insert(org.eclipse.jgit.lib.Constants.OBJ_BLOB, bytes));
                indexBuilder.add(entry);
            }
            indexBuilder.finish();
            if (newIndex.getEntryCount() == 0) {
                // nothing to commit
                return false;
            }
            ObjectId treeId = newIndex.writeTree(odi);
            // Create a commit object
            CommitBuilder commit = new CommitBuilder();
            commit.setAuthor(author);
            commit.setCommitter(author);
            commit.setEncoding(Constants.ENCODING);
            commit.setMessage("Initial commit");
            commit.setTreeId(treeId);
            // Insert the commit into the repository
            ObjectId commitId = odi.insert(commit);
            odi.flush();
            // set the branch refs
            RevWalk revWalk = new RevWalk(db);
            try {
                // set the master branch
                RevCommit revCommit = revWalk.parseCommit(commitId);
                RefUpdate masterRef = db.updateRef(Constants.R_MASTER);
                masterRef.setNewObjectId(commitId);
                masterRef.setRefLogMessage("commit: " + revCommit.getShortMessage(), false);
                Result masterRC = masterRef.update();
                switch (masterRC) {
                case NEW:
                    success = true;
                    break;
                default:
                    success = false;
                }
                if (addGitFlow) {
                    // set the develop branch for git-flow
                    RefUpdate developRef = db.updateRef(Constants.R_DEVELOP);
                    developRef.setNewObjectId(commitId);
                    developRef.setRefLogMessage("commit: " + revCommit.getShortMessage(), false);
                    Result developRC = developRef.update();
                    switch (developRC) {
                    case NEW:
                        success = true;
                        break;
                    default:
                        success = false;
                    }
                }
            } finally {
                revWalk.release();
            }
        } catch (UnsupportedEncodingException e) {
            logger().error(null, e);
        } catch (IOException e) {
            logger().error(null, e);
        } finally {
            odi.release();
            db.close();
        }
        return success;
    }
}
src/main/java/com/gitblit/wicket/pages/ProjectPage.java
@@ -26,6 +26,11 @@
import org.apache.wicket.markup.html.link.ExternalLink;
import com.gitblit.Keys;
import com.gitblit.models.Menu.MenuDivider;
import com.gitblit.models.Menu.MenuItem;
import com.gitblit.models.Menu.ParameterMenuItem;
import com.gitblit.models.NavLink.DropDownPageMenuNavLink;
import com.gitblit.models.NavLink;
import com.gitblit.models.ProjectModel;
import com.gitblit.models.RepositoryModel;
import com.gitblit.models.UserModel;
@@ -37,9 +42,6 @@
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.FilterableRepositoryList;
@@ -159,10 +161,10 @@
    }
    @Override
    protected void addDropDownMenus(List<PageRegistration> pages) {
    protected void addDropDownMenus(List<NavLink> navLinks) {
        PageParameters params = getPageParameters();
        DropDownMenuRegistration menu = new DropDownMenuRegistration("gb.filters",
        DropDownPageMenuNavLink menu = new DropDownPageMenuNavLink("gb.filters",
                ProjectPage.class);
        // preserve time filter option on repository choices
        menu.menuItems.addAll(getRepositoryFilterItems(params));
@@ -172,15 +174,15 @@
        if (menu.menuItems.size() > 0) {
            // Reset Filter
            menu.menuItems.add(new DropDownMenuItem(getString("gb.reset"), "p", WicketUtils.getProjectName(params)));
            menu.menuItems.add(new ParameterMenuItem(getString("gb.reset"), "p", WicketUtils.getProjectName(params)));
        }
        pages.add(menu);
        navLinks.add(menu);
        DropDownMenuRegistration projects = new DropDownMenuRegistration("gb.projects",
        DropDownPageMenuNavLink projects = new DropDownPageMenuNavLink("gb.projects",
                ProjectPage.class);
        projects.menuItems.addAll(getProjectsMenu());
        pages.add(projects);
        navLinks.add(projects);
    }
    @Override
@@ -202,8 +204,8 @@
        return null;
    }
    protected List<DropDownMenuItem> getProjectsMenu() {
        List<DropDownMenuItem> menu = new ArrayList<DropDownMenuItem>();
    protected List<MenuItem> getProjectsMenu() {
        List<MenuItem> menu = new ArrayList<MenuItem>();
        List<ProjectModel> projects = new ArrayList<ProjectModel>();
        for (ProjectModel model : getProjectModels()) {
            if (!model.isUserProject()) {
@@ -230,11 +232,11 @@
        }
        for (ProjectModel project : projects) {
            menu.add(new DropDownMenuItem(project.getDisplayName(), "p", project.name));
            menu.add(new ParameterMenuItem(project.getDisplayName(), "p", project.name));
        }
        if (showAllProjects) {
            menu.add(new DropDownMenuItem());
            menu.add(new DropDownMenuItem("all projects", null, null));
            menu.add(new MenuDivider());
            menu.add(new ParameterMenuItem("all projects"));
        }
        return menu;
    }
src/main/java/com/gitblit/wicket/pages/ProjectsPage.java
@@ -24,11 +24,11 @@
import org.apache.wicket.markup.repeater.data.ListDataProvider;
import com.gitblit.Keys;
import com.gitblit.models.Menu.ParameterMenuItem;
import com.gitblit.models.NavLink.DropDownPageMenuNavLink;
import com.gitblit.models.NavLink;
import com.gitblit.models.ProjectModel;
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;
@@ -115,10 +115,10 @@
    }
    @Override
    protected void addDropDownMenus(List<PageRegistration> pages) {
    protected void addDropDownMenus(List<NavLink> navLinks) {
        PageParameters params = getPageParameters();
        DropDownMenuRegistration menu = new DropDownMenuRegistration("gb.filters",
        DropDownPageMenuNavLink menu = new DropDownPageMenuNavLink("gb.filters",
                ProjectsPage.class);
        // preserve time filter option on repository choices
        menu.menuItems.addAll(getRepositoryFilterItems(params));
@@ -128,9 +128,9 @@
        if (menu.menuItems.size() > 0) {
            // Reset Filter
            menu.menuItems.add(new DropDownMenuItem(getString("gb.reset"), null, null));
            menu.menuItems.add(new ParameterMenuItem(getString("gb.reset")));
        }
        pages.add(menu);
        navLinks.add(menu);
    }
}
src/main/java/com/gitblit/wicket/pages/RepositoriesPage.java
@@ -21,6 +21,7 @@
import java.io.InputStreamReader;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.apache.wicket.Component;
@@ -29,15 +30,15 @@
import org.eclipse.jgit.lib.Constants;
import com.gitblit.Keys;
import com.gitblit.models.Menu.ParameterMenuItem;
import com.gitblit.models.NavLink;
import com.gitblit.models.NavLink.DropDownPageMenuNavLink;
import com.gitblit.models.RepositoryModel;
import com.gitblit.utils.MarkdownUtils;
import com.gitblit.utils.StringUtils;
import com.gitblit.wicket.CacheControl;
import com.gitblit.wicket.CacheControl.LastModified;
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;
@@ -79,7 +80,17 @@
                .setEscapeModelStrings(false).setVisible(message.length() > 0);
        add(repositoriesMessage);
        // conditionally include personal repositories in this page
        List<RepositoryModel> repositories = getRepositories(params);
        if (!app().settings().getBoolean(Keys.web.includePersonalRepositories, true)) {
            Iterator<RepositoryModel> itr = repositories.iterator();
            while (itr.hasNext()) {
                RepositoryModel rm = itr.next();
                if (rm.isPersonalRepository()) {
                    itr.remove();
                }
            }
        }
        RepositoriesPanel repositoriesPanel = new RepositoriesPanel("repositoriesPanel", showAdmin,
                true, repositories, true, getAccessRestrictions());
@@ -92,10 +103,10 @@
    }
    @Override
    protected void addDropDownMenus(List<PageRegistration> pages) {
    protected void addDropDownMenus(List<NavLink> navLinks) {
        PageParameters params = getPageParameters();
        DropDownMenuRegistration menu = new DropDownMenuRegistration("gb.filters",
        DropDownPageMenuNavLink menu = new DropDownPageMenuNavLink("gb.filters",
                RepositoriesPage.class);
        // preserve time filter option on repository choices
        menu.menuItems.addAll(getRepositoryFilterItems(params));
@@ -105,10 +116,10 @@
        if (menu.menuItems.size() > 0) {
            // Reset Filter
            menu.menuItems.add(new DropDownMenuItem(getString("gb.reset"), null, null));
            menu.menuItems.add(new ParameterMenuItem(getString("gb.reset")));
        }
        pages.add(menu);
        navLinks.add(menu);
    }
    private String readMarkdown(String messageSource, String resource) {
src/main/java/com/gitblit/wicket/pages/RepositoryPage.html
@@ -17,7 +17,7 @@
                                <form class="form-search" style="margin: 0px;" wicket:id="searchForm">
                                    <div class="input-append">
                                        <select class="span2" style="border-radius: 4px;" wicket:id="searchType"/>
                                        <input type="text" class="search-query" style="width: 170px;border-radius: 14px 0 0 14px; padding-left: 14px;" id="searchBox" wicket:id="searchBox" value=""/>
                                        <input type="text" class="input-medium search-query" style="border-radius: 14px 0 0 14px; padding-left: 14px;" id="searchBox" wicket:id="searchBox" value=""/>
                                        <button class="btn" style="border-radius: 0 14px 14px 0px;margin-left:-5px;" type="submit"><i class="icon-search"></i></button>
                                    </div>
                                </form>
src/main/java/com/gitblit/wicket/pages/RepositoryPage.java
@@ -21,7 +21,6 @@
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
@@ -49,6 +48,10 @@
import com.gitblit.Constants;
import com.gitblit.GitBlitException;
import com.gitblit.Keys;
import com.gitblit.extensions.RepositoryNavLinkExtension;
import com.gitblit.models.NavLink;
import com.gitblit.models.NavLink.ExternalNavLink;
import com.gitblit.models.NavLink.PageNavLink;
import com.gitblit.models.ProjectModel;
import com.gitblit.models.RefModel;
import com.gitblit.models.RepositoryModel;
@@ -57,7 +60,6 @@
import com.gitblit.models.UserRepositoryPreferences;
import com.gitblit.servlet.PagesServlet;
import com.gitblit.servlet.SyndicationServlet;
import com.gitblit.tickets.TicketIndexer.Lucene;
import com.gitblit.utils.ArrayUtils;
import com.gitblit.utils.BugtraqProcessor;
import com.gitblit.utils.DeepCopier;
@@ -66,8 +68,6 @@
import com.gitblit.utils.StringUtils;
import com.gitblit.wicket.CacheControl;
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;
@@ -90,7 +90,6 @@
    private Map<String, SubmoduleModel> submodules;
    private final Map<String, PageRegistration> registeredPages;
    private boolean showAdmin;
    private boolean isOwner;
@@ -109,7 +108,7 @@
            error(MessageFormat.format(getString("gb.repositoryNotSpecifiedFor"), getPageName()), true);
        }
        if (!getRepositoryModel().hasCommits) {
        if (!getRepositoryModel().hasCommits && getClass() != EmptyRepositoryPage.class) {
            throw new RestartResponseException(EmptyRepositoryPage.class, params);
        }
@@ -149,12 +148,21 @@
            }
        }
        // register the available page links for this page and user
        registeredPages = registerPages();
        showAdmin = false;
        if (app().settings().getBoolean(Keys.web.authenticateAdminPages, true)) {
            boolean allowAdmin = app().settings().getBoolean(Keys.web.allowAdministration, false);
            showAdmin = allowAdmin && GitBlitWebSession.get().canAdmin();
        } else {
            showAdmin = app().settings().getBoolean(Keys.web.allowAdministration, false);
        }
        isOwner = GitBlitWebSession.get().isLoggedIn()
                && (getRepositoryModel().isOwner(GitBlitWebSession.get().getUsername()));
        // standard page links
        List<PageRegistration> pages = new ArrayList<PageRegistration>(registeredPages.values());
        NavigationPanel navigationPanel = new NavigationPanel("repositoryNavPanel", getRepoNavPageClass(), pages);
        // register the available navigation links for this page and user
        List<NavLink> navLinks = registerNavLinks();
        // standard navigation links
        NavigationPanel navigationPanel = new NavigationPanel("repositoryNavPanel", getRepoNavPageClass(), navLinks);
        add(navigationPanel);
        add(new ExternalLink("syndication", SyndicationServlet.asLink(getRequest()
@@ -182,59 +190,61 @@
        return new BugtraqProcessor(app().settings());
    }
    private Map<String, PageRegistration> registerPages() {
    private List<NavLink> registerNavLinks() {
        PageParameters params = null;
        if (!StringUtils.isEmpty(repositoryName)) {
            params = WicketUtils.newRepositoryParameter(repositoryName);
        }
        Map<String, PageRegistration> pages = new LinkedHashMap<String, PageRegistration>();
        List<NavLink> navLinks = new ArrayList<NavLink>();
        Repository r = getRepository();
        RepositoryModel model = getRepositoryModel();
        // standard links
        if (RefLogUtils.getRefLogBranch(r) == null) {
            pages.put("summary", new PageRegistration("gb.summary", SummaryPage.class, params));
            navLinks.add(new PageNavLink("gb.summary", SummaryPage.class, params));
        } else {
            pages.put("summary", new PageRegistration("gb.summary", SummaryPage.class, params));
//            pages.put("overview", new PageRegistration("gb.overview", OverviewPage.class, params));
            pages.put("reflog", new PageRegistration("gb.reflog", ReflogPage.class, params));
            navLinks.add(new PageNavLink("gb.summary", SummaryPage.class, params));
            //            pages.put("overview", new PageRegistration("gb.overview", OverviewPage.class, params));
            navLinks.add(new PageNavLink("gb.reflog", ReflogPage.class, params));
        }
        pages.put("commits", new PageRegistration("gb.commits", LogPage.class, params));
        pages.put("tree", new PageRegistration("gb.tree", TreePage.class, params));
        if (app().tickets().isReady() && (app().tickets().isAcceptingNewTickets(getRepositoryModel()) || app().tickets().hasTickets(getRepositoryModel()))) {
            PageParameters tParams = new PageParameters(params);
            for (String state : TicketsPage.openStatii) {
                tParams.add(Lucene.status.name(), state);
            }
            pages.put("tickets", new PageRegistration("gb.tickets", TicketsPage.class, tParams));
        if (!model.hasCommits) {
            return navLinks;
        }
        navLinks.add(new PageNavLink("gb.commits", LogPage.class, params));
        navLinks.add(new PageNavLink("gb.tree", TreePage.class, params));
        if (app().tickets().isReady() && (app().tickets().isAcceptingNewTickets(model) || app().tickets().hasTickets(model))) {
            PageParameters tParams = WicketUtils.newOpenTicketsParameter(repositoryName);
            navLinks.add(new PageNavLink("gb.tickets", TicketsPage.class, tParams));
        }
        pages.put("docs", new PageRegistration("gb.docs", DocsPage.class, params, true));
        navLinks.add(new PageNavLink("gb.docs", DocsPage.class, params, true));
        if (app().settings().getBoolean(Keys.web.allowForking, true)) {
            pages.put("forks", new PageRegistration("gb.forks", ForksPage.class, params, true));
            navLinks.add(new PageNavLink("gb.forks", ForksPage.class, params, true));
        }
        pages.put("compare", new PageRegistration("gb.compare", ComparePage.class, params, true));
        navLinks.add(new PageNavLink("gb.compare", ComparePage.class, params, true));
        // conditional links
        // per-repository extra page links
        // per-repository extra navlinks
        if (JGitUtils.getPagesBranch(r) != null) {
            OtherPageLink pagesLink = new OtherPageLink("gb.pages", PagesServlet.asLink(
            ExternalNavLink pagesLink = new ExternalNavLink("gb.pages", PagesServlet.asLink(
                    getRequest().getRelativePathPrefixToContextRoot(), repositoryName, null), true);
            pages.put("pages", pagesLink);
            navLinks.add(pagesLink);
        }
        // Conditionally add edit link
        showAdmin = false;
        if (app().settings().getBoolean(Keys.web.authenticateAdminPages, true)) {
            boolean allowAdmin = app().settings().getBoolean(Keys.web.allowAdministration, false);
            showAdmin = allowAdmin && GitBlitWebSession.get().canAdmin();
        } else {
            showAdmin = app().settings().getBoolean(Keys.web.allowAdministration, false);
        UserModel user = UserModel.ANONYMOUS;
        if (GitBlitWebSession.get().isLoggedIn()) {
            user = GitBlitWebSession.get().getUser();
        }
        isOwner = GitBlitWebSession.get().isLoggedIn()
                && (model.isOwner(GitBlitWebSession.get()
                        .getUsername()));
        return pages;
        // add repository nav link extensions
        List<RepositoryNavLinkExtension> extensions = app().plugins().getExtensions(RepositoryNavLinkExtension.class);
        for (RepositoryNavLinkExtension ext : extensions) {
            navLinks.addAll(ext.getNavLinks(user, model));
        }
        return navLinks;
    }
    protected boolean allowForkControls() {
@@ -306,7 +316,7 @@
        }
        // (un)star link allows a user to star a repository
        if (user.isAuthenticated) {
        if (user.isAuthenticated && model.hasCommits) {
            PageParameters starParams = DeepCopier.copy(getPageParameters());
            starParams.put(PARAM_STAR, !user.getPreferences().isStarredRepository(model.name));
            String toggleStarUrl = getRequestCycle().urlFor(getClass(), starParams).toString();
@@ -333,7 +343,7 @@
        } else {
            String fork = app().repositories().getFork(user.username, model.name);
            boolean hasFork = fork != null;
            boolean canFork = user.canFork(model);
            boolean canFork = user.canFork(model) && model.hasCommits;
            if (hasFork || !canFork) {
                // user not allowed to fork or fork already exists or repo forbids forking
src/main/java/com/gitblit/wicket/pages/RootPage.html
@@ -51,16 +51,18 @@
        <li class="dropdown">
            <a data-toggle="dropdown" class="dropdown-toggle" style="text-decoration: none;" href="#"><span wicket:id="username"></span> <b class="caret"></b></a>
            <ul class="dropdown-menu">
                <li style="color:#ccc;padding-left:15px;font-weight:bold;"><span wicket:id="displayName"></span></li>
                <li class="divider"></li>
                <li><a wicket:id="newRepository"><wicket:message key="gb.newRepository"></wicket:message></a></li>
                <li><a wicket:id="myProfile"><wicket:message key="gb.myProfile"></wicket:message></a></li>
                <li><a wicket:id="changePassword"><wicket:message key="gb.changePassword"></wicket:message></a></li>
                <li class="divider"></li>
                <span wicket:id="standardMenu"></span>
                <span wicket:id="adminMenu"></span>
                <span wicket:id="extensionsMenu"></span>
                <li><a wicket:id="logout"><wicket:message key="gb.logout"></wicket:message></a></li>
            </ul>
        </li>
    </wicket:fragment>
    <wicket:fragment wicket:id="submenuFragment">
        <li style="color:#ccc;padding-left:15px;font-weight:bold;"><span wicket:id="submenuTitle"></span></li>
        <li wicket:id="submenuItem"><span wicket:id="submenuLink"></span></li>
    </wicket:fragment>
    
</wicket:extend>
</body>
src/main/java/com/gitblit/wicket/pages/RootPage.java
@@ -1,599 +1,708 @@
/*
 * 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.TreeSet;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;
import org.apache.wicket.MarkupContainer;
import org.apache.wicket.PageParameters;
import org.apache.wicket.behavior.HeaderContributor;
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.form.PasswordTextField;
import org.apache.wicket.markup.html.form.TextField;
import org.apache.wicket.markup.html.link.BookmarkablePageLink;
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.WebResponse;
import com.gitblit.Constants;
import com.gitblit.Keys;
import com.gitblit.models.RepositoryModel;
import com.gitblit.models.TeamModel;
import com.gitblit.models.UserModel;
import com.gitblit.utils.ModelUtils;
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.DropDownToggleItem;
import com.gitblit.wicket.SessionlessForm;
import com.gitblit.wicket.WicketUtils;
import com.gitblit.wicket.panels.GravatarImage;
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) {
        // CSS header overrides
        add(new HeaderContributor(new IHeaderContributor() {
            private static final long serialVersionUID = 1L;
            @Override
            public void renderHead(IHeaderResponse response) {
                StringBuilder buffer = new StringBuilder();
                buffer.append("<style type=\"text/css\">\n");
                buffer.append(".navbar-inner {\n");
                final String headerBackground = app().settings().getString(Keys.web.headerBackgroundColor, null);
                if (!StringUtils.isEmpty(headerBackground)) {
                    buffer.append(MessageFormat.format("background-color: {0};\n", headerBackground));
                }
                final String headerBorder = app().settings().getString(Keys.web.headerBorderColor, null);
                if (!StringUtils.isEmpty(headerBorder)) {
                    buffer.append(MessageFormat.format("border-bottom: 1px solid {0} !important;\n", headerBorder));
                }
                buffer.append("}\n");
                final String headerBorderFocus = app().settings().getString(Keys.web.headerBorderFocusColor, null);
                if (!StringUtils.isEmpty(headerBorderFocus)) {
                    buffer.append(".navbar ul li:focus, .navbar .active {\n");
                    buffer.append(MessageFormat.format("border-bottom: 4px solid {0};\n", headerBorderFocus));
                    buffer.append("}\n");
                }
                final String headerForeground = app().settings().getString(Keys.web.headerForegroundColor, null);
                if (!StringUtils.isEmpty(headerForeground)) {
                    buffer.append(".navbar ul.nav li a {\n");
                    buffer.append(MessageFormat.format("color: {0};\n", headerForeground));
                    buffer.append("}\n");
                    buffer.append(".navbar ul.nav .active a {\n");
                    buffer.append(MessageFormat.format("color: {0};\n", headerForeground));
                    buffer.append("}\n");
                }
                final String headerHover = app().settings().getString(Keys.web.headerHoverColor, null);
                if (!StringUtils.isEmpty(headerHover)) {
                    buffer.append(".navbar ul.nav li a:hover {\n");
                    buffer.append(MessageFormat.format("color: {0} !important;\n", headerHover));
                    buffer.append("}\n");
                }
                buffer.append("</style>\n");
                response.renderString(buffer.toString());
                }
            }));
        boolean authenticateView = app().settings().getBoolean(Keys.web.authenticateViewPages, false);
        boolean authenticateAdmin = app().settings().getBoolean(Keys.web.authenticateAdminPages, true);
        boolean allowAdmin = app().settings().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);
            }
        }
        if (authenticateView || authenticateAdmin) {
            if (GitBlitWebSession.get().isLoggedIn()) {
                UserMenu userFragment = new UserMenu("userPanel", "userMenuFragment", RootPage.this);
                add(userFragment);
            } else {
                LoginForm loginForm = new LoginForm("userPanel", "loginFormFragment", RootPage.this);
                add(loginForm);
            }
        } else {
            add(new Label("userPanel").setVisible(false));
        }
        boolean showRegistrations = app().federation().canFederate()
                && app().settings().getBoolean(Keys.web.showFederationRegistrations, false);
        // navigation links
        List<PageRegistration> pages = new ArrayList<PageRegistration>();
        if (!authenticateView || (authenticateView && GitBlitWebSession.get().isLoggedIn())) {
            pages.add(new PageRegistration(GitBlitWebSession.get().isLoggedIn() ? "gb.myDashboard" : "gb.dashboard", MyDashboardPage.class,
                    getRootPageParameters()));
            pages.add(new PageRegistration("gb.repositories", RepositoriesPage.class,
                    getRootPageParameters()));
            pages.add(new PageRegistration("gb.activity", ActivityPage.class, getRootPageParameters()));
            if (app().settings().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", getRootNavPageClass(), pages);
        add(navPanel);
        // display an error message cached from a redirect
        String cachedMessage = GitBlitWebSession.get().clearErrorMessage();
        if (!StringUtils.isEmpty(cachedMessage)) {
            error(cachedMessage);
        } else if (showAdmin) {
            int pendingProposals = app().federation().getPendingFederationProposals().size();
            if (pendingProposals == 1) {
                info(getString("gb.OneProposalToReview"));
            } else if (pendingProposals > 1) {
                info(MessageFormat.format(getString("gb.nFederationProposalsToReview"),
                        pendingProposals));
            }
        }
        super.setupPage(repositoryName, pageName);
    }
    protected Class<? extends BasePage> getRootNavPageClass() {
        return getClass();
    }
    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") == app().settings().getInteger(Keys.web.activityDuration, 7)) {
                    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 (app().settings().getBoolean(Keys.web.allowCookieAuthentication, false)) {
                WebResponse response = (WebResponse) getRequestCycle().getResponse();
                app().authentication().setCookie(response.getHttpServletResponse(), 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 = app().repositories().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 DropDownToggleItem(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 DropDownToggleItem(MessageFormat.format("{0} ({1})", team.name,
                        team.repositories.size()), "team", team.name, params));
            }
            // divider
            filters.add(new DropDownMenuItem());
        }
        // custom filters
        String customFilters = app().settings().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 DropDownToggleItem(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 = app().settings().getInteger(Keys.web.activityDuration, 7);
        int maxDaysBack = app().settings().getInteger(Keys.web.activityDurationMaximum, 30);
        if (daysBack < 1) {
            daysBack = 7;
        }
        if (daysBack > maxDaysBack) {
            daysBack = maxDaysBack;
        }
        PageParameters clonedParams;
        if (params == null) {
            clonedParams = new PageParameters();
        } else {
            clonedParams = new PageParameters(params);
        }
        if (!clonedParams.containsKey("db")) {
            clonedParams.put("db",  daysBack);
        }
        List<DropDownMenuItem> items = new ArrayList<DropDownMenuItem>();
        Set<Integer> choicesSet = new TreeSet<Integer>(app().settings().getIntegers(Keys.web.activityDurationChoices));
        if (choicesSet.isEmpty()) {
             choicesSet.addAll(Arrays.asList(1, 3, 7, 14, 21, 28));
        }
        List<Integer> choices = new ArrayList<Integer>(choicesSet);
        Collections.sort(choices);
        String lastDaysPattern = getString("gb.lastNDays");
        for (Integer db : choices) {
            if (db == 1) {
                items.add(new DropDownMenuItem(getString("gb.time.today"), "db", db.toString(), clonedParams));
            } else {
                String txt = MessageFormat.format(lastDaysPattern, db);
                items.add(new DropDownMenuItem(txt, "db", db.toString(), clonedParams));
            }
        }
        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 = ModelUtils.getPersonalPath(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);
        int maxDaysBack = app().settings().getInteger(Keys.web.activityDurationMaximum, 30);
        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(app().settings().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 = app().users().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) {
            if (maxDaysBack > 0 && daysBack > maxDaysBack) {
                daysBack = maxDaysBack;
            }
            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;
    }
    /**
     * Inline login form.
     */
    private class LoginForm extends Fragment {
        private static final long serialVersionUID = 1L;
        public LoginForm(String id, String markupId, MarkupContainer markupProvider) {
            super(id, markupId, markupProvider);
            setRenderBodyOnly(true);
            SessionlessForm<Void> loginForm = new SessionlessForm<Void>("loginForm", RootPage.this.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 = app().authentication().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, markupProvider.getString("gb.username"));
            loginForm.add(unameField);
            PasswordTextField pwField = new PasswordTextField("password", password);
            WicketUtils.setInputPlaceholder(pwField, markupProvider.getString("gb.password"));
            loginForm.add(pwField);
            add(loginForm);
        }
    }
    /**
     * Menu for the authenticated user.
     */
    class UserMenu extends Fragment {
        private static final long serialVersionUID = 1L;
        public UserMenu(String id, String markupId, MarkupContainer markupProvider) {
            super(id, markupId, markupProvider);
            setRenderBodyOnly(true);
            GitBlitWebSession session = GitBlitWebSession.get();
            UserModel user = session.getUser();
            boolean editCredentials = app().authentication().supportsCredentialChanges(user);
            boolean standardLogin = session.authenticationType.isStandard();
            if (app().settings().getBoolean(Keys.web.allowGravatar, true)) {
                add(new GravatarImage("username", user, "navbarGravatar", 20, false));
            } else {
                add(new Label("username", user.getDisplayName()));
            }
            add(new Label("displayName", user.getDisplayName()));
            add(new BookmarkablePageLink<Void>("newRepository",
                    EditRepositoryPage.class).setVisible(user.canAdmin() || user.canCreate()));
            add(new BookmarkablePageLink<Void>("myProfile",
                    UserPage.class, WicketUtils.newUsernameParameter(user.username)));
            add(new BookmarkablePageLink<Void>("changePassword",
                    ChangePasswordPage.class).setVisible(editCredentials));
            add(new BookmarkablePageLink<Void>("logout",
                    LogoutPage.class).setVisible(standardLogin));
        }
    }
}
/*
 * 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.TreeSet;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;
import org.apache.wicket.MarkupContainer;
import org.apache.wicket.PageParameters;
import org.apache.wicket.behavior.HeaderContributor;
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.form.PasswordTextField;
import org.apache.wicket.markup.html.form.TextField;
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.IModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.protocol.http.WebResponse;
import com.gitblit.Constants;
import com.gitblit.Keys;
import com.gitblit.extensions.NavLinkExtension;
import com.gitblit.extensions.UserMenuExtension;
import com.gitblit.models.Menu.ExternalLinkMenuItem;
import com.gitblit.models.Menu.MenuDivider;
import com.gitblit.models.Menu.MenuItem;
import com.gitblit.models.Menu.PageLinkMenuItem;
import com.gitblit.models.Menu.ParameterMenuItem;
import com.gitblit.models.Menu.ToggleMenuItem;
import com.gitblit.models.NavLink;
import com.gitblit.models.NavLink.PageNavLink;
import com.gitblit.models.RepositoryModel;
import com.gitblit.models.TeamModel;
import com.gitblit.models.UserModel;
import com.gitblit.utils.ModelUtils;
import com.gitblit.utils.StringUtils;
import com.gitblit.wicket.GitBlitWebSession;
import com.gitblit.wicket.SessionlessForm;
import com.gitblit.wicket.WicketUtils;
import com.gitblit.wicket.panels.GravatarImage;
import com.gitblit.wicket.panels.LinkPanel;
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) {
        // CSS header overrides
        add(new HeaderContributor(new IHeaderContributor() {
            private static final long serialVersionUID = 1L;
            @Override
            public void renderHead(IHeaderResponse response) {
                StringBuilder buffer = new StringBuilder();
                buffer.append("<style type=\"text/css\">\n");
                buffer.append(".navbar-inner {\n");
                final String headerBackground = app().settings().getString(Keys.web.headerBackgroundColor, null);
                if (!StringUtils.isEmpty(headerBackground)) {
                    buffer.append(MessageFormat.format("background-color: {0};\n", headerBackground));
                }
                final String headerBorder = app().settings().getString(Keys.web.headerBorderColor, null);
                if (!StringUtils.isEmpty(headerBorder)) {
                    buffer.append(MessageFormat.format("border-bottom: 1px solid {0} !important;\n", headerBorder));
                }
                buffer.append("}\n");
                final String headerBorderFocus = app().settings().getString(Keys.web.headerBorderFocusColor, null);
                if (!StringUtils.isEmpty(headerBorderFocus)) {
                    buffer.append(".navbar ul li:focus, .navbar .active {\n");
                    buffer.append(MessageFormat.format("border-bottom: 4px solid {0};\n", headerBorderFocus));
                    buffer.append("}\n");
                }
                final String headerForeground = app().settings().getString(Keys.web.headerForegroundColor, null);
                if (!StringUtils.isEmpty(headerForeground)) {
                    buffer.append(".navbar ul.nav li a {\n");
                    buffer.append(MessageFormat.format("color: {0};\n", headerForeground));
                    buffer.append("}\n");
                    buffer.append(".navbar ul.nav .active a {\n");
                    buffer.append(MessageFormat.format("color: {0};\n", headerForeground));
                    buffer.append("}\n");
                }
                final String headerHover = app().settings().getString(Keys.web.headerHoverColor, null);
                if (!StringUtils.isEmpty(headerHover)) {
                    buffer.append(".navbar ul.nav li a:hover {\n");
                    buffer.append(MessageFormat.format("color: {0} !important;\n", headerHover));
                    buffer.append("}\n");
                }
                buffer.append("</style>\n");
                response.renderString(buffer.toString());
                }
            }));
        boolean authenticateView = app().settings().getBoolean(Keys.web.authenticateViewPages, false);
        boolean authenticateAdmin = app().settings().getBoolean(Keys.web.authenticateAdminPages, true);
        boolean allowAdmin = app().settings().getBoolean(Keys.web.allowAdministration, true);
        boolean allowLucene = app().settings().getBoolean(Keys.web.allowLuceneIndexing, true);
        boolean isLoggedIn = GitBlitWebSession.get().isLoggedIn();
        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);
            }
        }
        if (authenticateView || authenticateAdmin) {
            if (isLoggedIn) {
                UserMenu userFragment = new UserMenu("userPanel", "userMenuFragment", RootPage.this);
                add(userFragment);
            } else {
                LoginForm loginForm = new LoginForm("userPanel", "loginFormFragment", RootPage.this);
                add(loginForm);
            }
        } else {
            add(new Label("userPanel").setVisible(false));
        }
        // navigation links
        List<NavLink> navLinks = new ArrayList<NavLink>();
        if (!authenticateView || (authenticateView && isLoggedIn)) {
            navLinks.add(new PageNavLink(isLoggedIn ? "gb.myDashboard" : "gb.dashboard", MyDashboardPage.class,
                    getRootPageParameters()));
            if (isLoggedIn && app().tickets().isReady()) {
                navLinks.add(new PageNavLink("gb.myTickets", MyTicketsPage.class));
            }
            navLinks.add(new PageNavLink("gb.repositories", RepositoriesPage.class,
                    getRootPageParameters()));
            navLinks.add(new PageNavLink("gb.activity", ActivityPage.class, getRootPageParameters()));
            if (allowLucene) {
                navLinks.add(new PageNavLink("gb.search", LuceneSearchPage.class));
            }
            if (!authenticateView || (authenticateView && isLoggedIn)) {
                addDropDownMenus(navLinks);
            }
            UserModel user = UserModel.ANONYMOUS;
            if (isLoggedIn) {
                user = GitBlitWebSession.get().getUser();
            }
            // add nav link extensions
            List<NavLinkExtension> extensions = app().plugins().getExtensions(NavLinkExtension.class);
            for (NavLinkExtension ext : extensions) {
                navLinks.addAll(ext.getNavLinks(user));
            }
        }
        NavigationPanel navPanel = new NavigationPanel("navPanel", getRootNavPageClass(), navLinks);
        add(navPanel);
        // display an error message cached from a redirect
        String cachedMessage = GitBlitWebSession.get().clearErrorMessage();
        if (!StringUtils.isEmpty(cachedMessage)) {
            error(cachedMessage);
        } else if (showAdmin) {
            int pendingProposals = app().federation().getPendingFederationProposals().size();
            if (pendingProposals == 1) {
                info(getString("gb.OneProposalToReview"));
            } else if (pendingProposals > 1) {
                info(MessageFormat.format(getString("gb.nFederationProposalsToReview"),
                        pendingProposals));
            }
        }
        super.setupPage(repositoryName, pageName);
    }
    protected Class<? extends BasePage> getRootNavPageClass() {
        return getClass();
    }
    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") == app().settings().getInteger(Keys.web.activityDuration, 7)) {
                    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 (app().settings().getBoolean(Keys.web.allowCookieAuthentication, false)) {
                WebResponse response = (WebResponse) getRequestCycle().getResponse();
                app().authentication().setCookie(response.getHttpServletResponse(), 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 = app().repositories().getRepositoryModels(user);
            repositoryModels.addAll(repositories);
            Collections.sort(repositoryModels);
        }
        return repositoryModels;
    }
    protected void addDropDownMenus(List<NavLink> navLinks) {
    }
    protected List<com.gitblit.models.Menu.MenuItem> getRepositoryFilterItems(PageParameters params) {
        final UserModel user = GitBlitWebSession.get().getUser();
        Set<MenuItem> filters = new LinkedHashSet<MenuItem>();
        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 ToggleMenuItem(MessageFormat.format("{0} ({1})", set,
                        setMap.get(set).get()), "set", set, params));
            }
            // divider
            filters.add(new MenuDivider());
        }
        // 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 ToggleMenuItem(MessageFormat.format("{0} ({1})", team.name,
                        team.repositories.size()), "team", team.name, params));
            }
            // divider
            filters.add(new MenuDivider());
        }
        // custom filters
        String customFilters = app().settings().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 ToggleMenuItem(null, "x", expression, params));
                }
            }
            // if we added any custom expressions, add a divider
            if (addedExpression) {
                filters.add(new MenuDivider());
            }
        }
        return new ArrayList<MenuItem>(filters);
    }
    protected List<MenuItem> getTimeFilterItems(PageParameters params) {
        // days back choices - additive parameters
        int daysBack = app().settings().getInteger(Keys.web.activityDuration, 7);
        int maxDaysBack = app().settings().getInteger(Keys.web.activityDurationMaximum, 30);
        if (daysBack < 1) {
            daysBack = 7;
        }
        if (daysBack > maxDaysBack) {
            daysBack = maxDaysBack;
        }
        PageParameters clonedParams;
        if (params == null) {
            clonedParams = new PageParameters();
        } else {
            clonedParams = new PageParameters(params);
        }
        if (!clonedParams.containsKey("db")) {
            clonedParams.put("db",  daysBack);
        }
        List<MenuItem> items = new ArrayList<MenuItem>();
        Set<Integer> choicesSet = new TreeSet<Integer>(app().settings().getIntegers(Keys.web.activityDurationChoices));
        if (choicesSet.isEmpty()) {
             choicesSet.addAll(Arrays.asList(1, 3, 7, 14, 21, 28));
        }
        List<Integer> choices = new ArrayList<Integer>(choicesSet);
        Collections.sort(choices);
        String lastDaysPattern = getString("gb.lastNDays");
        for (Integer db : choices) {
            if (db == 1) {
                items.add(new ParameterMenuItem(getString("gb.time.today"), "db", db.toString(), clonedParams));
            } else {
                String txt = MessageFormat.format(lastDaysPattern, db);
                items.add(new ParameterMenuItem(txt, "db", db.toString(), clonedParams));
            }
        }
        items.add(new MenuDivider());
        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 = ModelUtils.getPersonalPath(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);
        int maxDaysBack = app().settings().getInteger(Keys.web.activityDurationMaximum, 30);
        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(app().settings().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 = app().users().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) {
            if (maxDaysBack > 0 && daysBack > maxDaysBack) {
                daysBack = maxDaysBack;
            }
            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;
    }
    /**
     * Inline login form.
     */
    private class LoginForm extends Fragment {
        private static final long serialVersionUID = 1L;
        public LoginForm(String id, String markupId, MarkupContainer markupProvider) {
            super(id, markupId, markupProvider);
            setRenderBodyOnly(true);
            SessionlessForm<Void> loginForm = new SessionlessForm<Void>("loginForm", RootPage.this.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 = app().authentication().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, markupProvider.getString("gb.username"));
            loginForm.add(unameField);
            PasswordTextField pwField = new PasswordTextField("password", password);
            WicketUtils.setInputPlaceholder(pwField, markupProvider.getString("gb.password"));
            loginForm.add(pwField);
            add(loginForm);
        }
    }
    /**
     * Menu for the authenticated user.
     */
    class UserMenu extends Fragment {
        private static final long serialVersionUID = 1L;
        public UserMenu(String id, String markupId, MarkupContainer markupProvider) {
            super(id, markupId, markupProvider);
            setRenderBodyOnly(true);
        }
        @Override
        protected void onInitialize() {
            super.onInitialize();
            GitBlitWebSession session = GitBlitWebSession.get();
            UserModel user = session.getUser();
            boolean editCredentials = app().authentication().supportsCredentialChanges(user);
            boolean standardLogin = session.authenticationType.isStandard();
            if (app().settings().getBoolean(Keys.web.allowGravatar, true)) {
                add(new GravatarImage("username", user, "navbarGravatar", 20, false));
            } else {
                add(new Label("username", user.getDisplayName()));
            }
            List<MenuItem> standardItems = new ArrayList<MenuItem>();
            standardItems.add(new MenuDivider());
            if (user.canAdmin() || user.canCreate()) {
                standardItems.add(new PageLinkMenuItem("gb.newRepository", app().getNewRepositoryPage()));
            }
            standardItems.add(new PageLinkMenuItem("gb.myProfile", UserPage.class,
                    WicketUtils.newUsernameParameter(user.username)));
            if (editCredentials) {
                standardItems.add(new PageLinkMenuItem("gb.changePassword", ChangePasswordPage.class));
            }
            standardItems.add(new MenuDivider());
            add(newSubmenu("standardMenu", user.getDisplayName(), standardItems));
            if (showAdmin) {
                // admin menu
                List<MenuItem> adminItems = new ArrayList<MenuItem>();
                adminItems.add(new MenuDivider());
                adminItems.add(new PageLinkMenuItem("gb.users", UsersPage.class));
                adminItems.add(new PageLinkMenuItem("gb.teams", TeamsPage.class));
                boolean showRegistrations = app().federation().canFederate()
                        && app().settings().getBoolean(Keys.web.showFederationRegistrations, false);
                if (showRegistrations) {
                    adminItems.add(new PageLinkMenuItem("gb.federation", FederationPage.class));
                }
                adminItems.add(new MenuDivider());
                add(newSubmenu("adminMenu", getString("gb.administration"), adminItems));
            } else {
                add(new Label("adminMenu").setVisible(false));
            }
            // plugin extension items
            List<MenuItem> extensionItems = new ArrayList<MenuItem>();
            List<UserMenuExtension> extensions = app().plugins().getExtensions(UserMenuExtension.class);
            for (UserMenuExtension ext : extensions) {
                List<MenuItem> items = ext.getMenuItems(user);
                extensionItems.addAll(items);
            }
            if (extensionItems.isEmpty()) {
                // no extension items
                add(new Label("extensionsMenu").setVisible(false));
            } else {
                // found extension items
                extensionItems.add(0, new MenuDivider());
                add(newSubmenu("extensionsMenu", getString("gb.extensions"), extensionItems));
                extensionItems.add(new MenuDivider());
            }
            add(new BookmarkablePageLink<Void>("logout",
                    LogoutPage.class).setVisible(standardLogin));
        }
        /**
         * Creates a submenu.  This is not actually submenu because we're using
         * an older Twitter Bootstrap which is pre-submenu.
         *
         * @param wicketId
         * @param submenuTitle
         * @param menuItems
         * @return a submenu fragment
         */
        private Fragment newSubmenu(String wicketId, String submenuTitle, List<MenuItem> menuItems) {
            Fragment submenu = new Fragment(wicketId, "submenuFragment", this);
            submenu.add(new Label("submenuTitle", submenuTitle).setRenderBodyOnly(true));
            ListDataProvider<MenuItem> menuItemsDp = new ListDataProvider<MenuItem>(menuItems);
            DataView<MenuItem> submenuItems = new DataView<MenuItem>("submenuItem", menuItemsDp) {
                private static final long serialVersionUID = 1L;
                @Override
                public void populateItem(final Item<MenuItem> menuItem) {
                    final MenuItem item = menuItem.getModelObject();
                    String name = item.toString();
                    try {
                        // try to lookup translation
                        name = getString(name);
                    } catch (Exception e) {
                    }
                    if (item instanceof PageLinkMenuItem) {
                        // link to another Wicket page
                        PageLinkMenuItem pageLink = (PageLinkMenuItem) item;
                        menuItem.add(new LinkPanel("submenuLink", null, null, name, pageLink.getPageClass(),
                                pageLink.getPageParameters(), false).setRenderBodyOnly(true));
                    } else if (item instanceof ExternalLinkMenuItem) {
                        // link to a specified href
                        ExternalLinkMenuItem extLink = (ExternalLinkMenuItem) item;
                        menuItem.add(new LinkPanel("submenuLink", null, name, extLink.getHref(),
                                extLink.openInNewWindow()).setRenderBodyOnly(true));
                    } else if (item instanceof MenuDivider) {
                        // divider
                        menuItem.add(new Label("submenuLink").setRenderBodyOnly(true));
                        WicketUtils.setCssClass(menuItem, "divider");
                    }
                }
            };
            submenu.add(submenuItems);
            submenu.setRenderBodyOnly(true);
            return submenu;
        }
    }
}
src/main/java/com/gitblit/wicket/pages/TeamsPage.html
New file
@@ -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 class="container">
    <div wicket:id="teamsPanel">[teams panel]</div>
</div>
</wicket:extend>
</body>
</html>
src/main/java/com/gitblit/wicket/pages/TeamsPage.java
New file
@@ -0,0 +1,30 @@
/*
 * 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;
@RequiresAdminRole
public class TeamsPage extends RootPage {
    public TeamsPage() {
        super();
        setupPage("", "");
        add(new TeamsPanel("teamsPanel", showAdmin).setVisible(showAdmin));
    }
}
src/main/java/com/gitblit/wicket/pages/TicketBasePage.java
File was deleted
src/main/java/com/gitblit/wicket/pages/TicketPage.java
@@ -87,6 +87,7 @@
import com.gitblit.utils.StringUtils;
import com.gitblit.utils.TimeUtils;
import com.gitblit.wicket.GitBlitWebSession;
import com.gitblit.wicket.TicketsUI;
import com.gitblit.wicket.WicketUtils;
import com.gitblit.wicket.panels.BasePanel.JavascriptTextPrompt;
import com.gitblit.wicket.panels.CommentPanel;
@@ -103,7 +104,7 @@
 * @author James Moger
 *
 */
public class TicketPage extends TicketBasePage {
public class TicketPage extends RepositoryPage {
    static final String NIL = "<nil>";
@@ -155,7 +156,7 @@
        String href = urlFor(TicketsPage.class, params).toString();
        add(new ExternalLink("ticketNumber", href, "#" + ticket.number));
        Label headerStatus = new Label("headerStatus", ticket.status.toString());
        WicketUtils.setCssClass(headerStatus, getLozengeClass(ticket.status, false));
        WicketUtils.setCssClass(headerStatus, TicketsUI.getLozengeClass(ticket.status, false));
        add(headerStatus);
        add(new Label("ticketTitle", ticket.title));
        if (currentPatchset == null) {
@@ -249,8 +250,12 @@
        } else {
            // link to milestone query
            TicketMilestone milestone = app().tickets().getMilestone(repository, ticket.milestone);
            PageParameters milestoneParameters = new PageParameters();
            milestoneParameters.put("r", repositoryName);
            PageParameters milestoneParameters;
            if (milestone.isOpen()) {
                milestoneParameters = WicketUtils.newOpenTicketsParameter(repositoryName);
            } else {
                milestoneParameters = WicketUtils.newRepositoryParameter(repositoryName);
            }
            milestoneParameters.put(Lucene.milestone.name(), ticket.milestone);
            int progress = 0;
            int open = 0;
@@ -278,7 +283,8 @@
        if (StringUtils.isEmpty(ticket.body)) {
            desc = getString("gb.noDescriptionGiven");
        } else {
            desc = MarkdownUtils.transformGFM(app().settings(), ticket.body, ticket.repository);
            String bugtraq = bugtraqProcessor().processText(getRepository(), repositoryName, ticket.body);
            desc = MarkdownUtils.transformGFM(app().settings(), bugtraq, ticket.repository);
        }
        add(new Label("ticketDescription", desc).setEscapeModelStrings(false));
@@ -318,10 +324,10 @@
         * LARGE STATUS INDICATOR WITH ICON (DISCUSSION TAB->SIDE BAR)
         */
        Fragment ticketStatus = new Fragment("ticketStatus", "ticketStatusFragment", this);
        Label ticketIcon = getStateIcon("ticketIcon", ticket);
        Label ticketIcon = TicketsUI.getStateIcon("ticketIcon", ticket);
        ticketStatus.add(ticketIcon);
        ticketStatus.add(new Label("ticketStatus", ticket.status.toString()));
        WicketUtils.setCssClass(ticketStatus, getLozengeClass(ticket.status, false));
        WicketUtils.setCssClass(ticketStatus, TicketsUI.getLozengeClass(ticket.status, false));
        add(ticketStatus);
@@ -371,7 +377,7 @@
                                setResponsePage(TicketsPage.class, getPageParameters());
                            }
                        };
                        String css = getStatusClass(item.getModel().getObject());
                        String css = TicketsUI.getStatusClass(item.getModel().getObject());
                        WicketUtils.setCssClass(link, css);
                        item.add(link);
                    }
@@ -513,7 +519,7 @@
            add(new Label("ticketTopic").setVisible(false));
        } else {
            // process the topic using the bugtraq config to link things
            String topic = bugtraqProcessor().processPlainCommitMessage(getRepository(), repositoryName, ticket.topic);
            String topic = bugtraqProcessor().processText(getRepository(), repositoryName, ticket.topic);
            add(new Label("ticketTopic", topic).setEscapeModelStrings(false));
        }
@@ -673,7 +679,7 @@
                         */
                        Fragment frag = new Fragment("entry", "statusFragment", this);
                        Label status = new Label("statusChange", entry.getStatus().toString());
                        String css = getLozengeClass(entry.getStatus(), false);
                        String css = TicketsUI.getLozengeClass(entry.getStatus(), false);
                        WicketUtils.setCssClass(status, css);
                        for (IBehavior b : status.getBehaviors()) {
                            if (b instanceof SimpleAttributeModifier) {
@@ -692,7 +698,8 @@
                        /*
                         * COMMENT
                         */
                        String comment = MarkdownUtils.transformGFM(app().settings(), entry.comment.text, repositoryName);
                        String bugtraq = bugtraqProcessor().processText(getRepository(), repositoryName, entry.comment.text);
                        String comment = MarkdownUtils.transformGFM(app().settings(), bugtraq, repositoryName);
                        Fragment frag = new Fragment("entry", "commentFragment", this);
                        Label commentIcon = new Label("commentIcon");
                        if (entry.comment.src == CommentSource.Email) {
@@ -944,7 +951,7 @@
                            case status:
                                // special handling for status
                                Status status = event.getStatus();
                                String css = getLozengeClass(status, true);
                                String css = TicketsUI.getLozengeClass(status, true);
                                value = String.format("<span class=\"%1$s\">%2$s</span>", css, status.toString());
                                break;
                            default:
@@ -1533,14 +1540,14 @@
        switch (type) {
            case Rebase:
            case Rebase_Squash:
                typeCss = getLozengeClass(Status.Declined, false);
                typeCss = TicketsUI.getLozengeClass(Status.Declined, false);
                break;
            case Squash:
            case Amend:
                typeCss = getLozengeClass(Status.On_Hold, false);
                typeCss = TicketsUI.getLozengeClass(Status.On_Hold, false);
                break;
            case Proposal:
                typeCss = getLozengeClass(Status.New, false);
                typeCss = TicketsUI.getLozengeClass(Status.New, false);
                break;
            case FastForward:
            default:
src/main/java/com/gitblit/wicket/pages/TicketsPage.html
@@ -11,7 +11,7 @@
    <div class="hidden-phone pull-right">
        <form class="form-search" style="margin: 0px;" wicket:id="ticketSearchForm">
            <div class="input-append">
                <input type="text" class="search-query" style="width: 170px;border-radius: 14px 0 0 14px; padding-left: 14px;" id="ticketSearchBox" wicket:id="ticketSearchBox" value=""/>
                <input type="text" class="input-medium search-query" style="border-radius: 14px 0 0 14px; padding-left: 14px;" id="ticketSearchBox" wicket:id="ticketSearchBox" value=""/>
                <button class="btn" style="border-radius: 0 14px 14px 0px;margin-left:-5px;" type="submit"><i class="icon-search"></i></button>
            </div>
        </form>
@@ -88,42 +88,7 @@
                </div>
            </div>
        
            <table class="table tickets">
            <tbody>
               <tr wicket:id="ticket">
                   <td class="ticket-list-icon">
                       <i wicket:id="state"></i>
                   </td>
                <td>
                    <span wicket:id="title">[title]</span> <span wicket:id="labels" style="font-weight: normal;color:white;"><span class="label" wicket:id="label"></span></span>
                    <div class="ticket-list-details">
                        <span style="padding-right: 10px;" class="hidden-phone">
                            <wicket:message key="gb.createdBy"></wicket:message>
                            <span style="padding: 0px 2px" wicket:id="createdBy">[createdBy]</span> <span class="date" wicket:id="createDate">[create date]</span>
                        </span>
                        <span wicket:id="indicators" style="white-space:nowrap;"><i wicket:id="icon"></i> <span style="padding-right:10px;" wicket:id="count"></span></span>
                    </div>
                    <div class="hidden-phone" wicket:id="updated"></div>
                </td>
                <td class="ticket-list-state">
                       <span class="badge badge-info" wicket:id="votes"></span>
                </td>
                <td class="hidden-phone ticket-list-state">
                       <i wicket:message="title:gb.watching" style="color:#888;" class="fa fa-eye" wicket:id="watching"></i>
                </td>
                <td class="ticket-list-state">
                       <div wicket:id="status"></div>
                </td>
                <td class="indicators">
                    <div>
                         <b>#<span wicket:id="id">[id]</span></b>
                     </div>
                    <div wicket:id="responsible"></div>
                </td>
               </tr>
            </tbody>
            </table>
            <div wicket:id="ticketList"></div>
        
            <div class="btn-group pull-right">
                    <div class="pagination pagination-right pagination-small">
@@ -139,13 +104,50 @@
    </div>
    <div class="tab-pane" id="milestones">
        <div class="row">
            <div class="span9" wicket:id="milestoneList" style="padding-bottom: 10px;">
                <h3><span wicket:id="milestoneName"></span> <small><span wicket:id="milestoneState"></span></small></h3>
                <i style="color:#888;"class="fa fa-calendar"></i> <span wicket:id="milestoneDue"></span>
            <span class="span12" style="padding-bottom:10px;" wicket:id="newMilestone"></span>
        </div>
        <div class="row">
            <div class="span12"><h2><wicket:message key="gb.openMilestones"></wicket:message></h2></div>
            <div class="span12"><hr/></div>
        </div>
        <div class="row">
            <div class="span4" wicket:id="openMilestonesList" style="padding-bottom: 20px;">
                <div wicket:id="entryPanel"></div>
            </div>
        </div>
        <div class="row">
            <div class="span12"><h2><wicket:message key="gb.closedMilestones"></wicket:message></h2></div>
            <div class="span12"><hr/></div>
        </div>
        <div class="row">
            <div class="span3" wicket:id="closedMilestonesList" style="padding-bottom: 15px;">
                <div wicket:id="entryPanel"></div>
            </div>
        </div>
    </div>
</div>
<wicket:fragment wicket:id="milestoneListFragment">
    <h3><span wicket:id="milestoneName"></span> <small><span wicket:id="milestoneState"></span></small></h3>
    <i style="color:#888;"class="fa fa-calendar"></i> <span wicket:id="milestoneDue"></span> <span wicket:id="editMilestone"></span>
    <div wicket:id="milestonePanel"></div>
</wicket:fragment>
<wicket:fragment wicket:id="openMilestoneFragment">
    <div style="clear:both;padding-bottom: 10px;">
        <div style="margin-bottom: 5px;" class="progress progress-success">
            <div class="bar" wicket:id="progress"></div>
        </div>
        <div class="milestoneOverview">
            <span wicket:id="openTickets" />,
            <span wicket:id="closedTickets" />,
            <span wicket:id="totalTickets" />
        </div>
    </div>
</wicket:fragment>
<wicket:fragment wicket:id="noMilestoneFragment">
<table style="width: 100%;padding-bottom: 5px;">
@@ -201,13 +203,6 @@
        <li class="nav-header"><wicket:message key="gb.topicsAndLabels"></wicket:message></li>
        <li class="dynamicQuery" wicket:id="dynamicQuery"><span><span wicket:id="swatch"></span> <span wicket:id="link"></span></span><span class="pull-right"><i style="font-size: 18px;" wicket:id="checked"></i></span></li>
    </ul>
</wicket:fragment>
<wicket:fragment wicket:id="updatedFragment">
    <div class="ticket-list-details">
        <wicket:message key="gb.updatedBy"></wicket:message>
        <span style="padding: 0px 2px" wicket:id="updatedBy">[updatedBy]</span> <span class="date" wicket:id="updateDate">[update date]</span>
    </div>
</wicket:fragment>
</wicket:extend>
src/main/java/com/gitblit/wicket/pages/TicketsPage.java
@@ -15,11 +15,11 @@
 */
package com.gitblit.wicket.pages;
import java.io.Serializable;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
@@ -28,20 +28,16 @@
import org.apache.wicket.PageParameters;
import org.apache.wicket.behavior.SimpleAttributeModifier;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.form.TextField;
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.IModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.request.target.basic.RedirectRequestTarget;
import com.gitblit.Constants;
import com.gitblit.Constants.AccessPermission;
import com.gitblit.Keys;
import com.gitblit.models.RegistrantAccessPermission;
import com.gitblit.models.RepositoryModel;
import com.gitblit.models.TicketModel;
import com.gitblit.models.TicketModel.Status;
import com.gitblit.models.UserModel;
@@ -54,18 +50,17 @@
import com.gitblit.utils.ArrayUtils;
import com.gitblit.utils.StringUtils;
import com.gitblit.wicket.GitBlitWebSession;
import com.gitblit.wicket.SessionlessForm;
import com.gitblit.wicket.TicketsUI;
import com.gitblit.wicket.TicketsUI.TicketQuery;
import com.gitblit.wicket.TicketsUI.TicketSort;
import com.gitblit.wicket.WicketUtils;
import com.gitblit.wicket.panels.GravatarImage;
import com.gitblit.wicket.panels.LinkPanel;
import com.gitblit.wicket.panels.TicketListPanel;
import com.gitblit.wicket.panels.TicketSearchForm;
public class TicketsPage extends TicketBasePage {
public class TicketsPage extends RepositoryPage {
    final TicketResponsible any;
    public static final String [] openStatii = new String [] { Status.New.name().toLowerCase(), Status.Open.name().toLowerCase() };
    public static final String [] closedStatii = new String [] { "!" + Status.New.name().toLowerCase(), "!" + Status.Open.name().toLowerCase() };
    public TicketsPage(PageParameters params) {
        super(params);
@@ -100,11 +95,8 @@
        final String sortBy = Lucene.fromString(params.getString("sort", Lucene.created.name())).name();
        final boolean desc = !"asc".equals(params.getString("direction", "desc"));
        // add search form
        TicketSearchForm searchForm = new TicketSearchForm("ticketSearchForm", repositoryName, searchParam);
        add(searchForm);
        searchForm.setTranslatedAttributes();
        add(new TicketSearchForm("ticketSearchForm", repositoryName, searchParam, getClass(), params));
        final String activeQuery;
        if (!StringUtils.isEmpty(searchParam)) {
@@ -190,12 +182,12 @@
            milestonePanel.add(new LinkPanel("openTickets", null,
                    MessageFormat.format(getString("gb.nOpenTickets"), currentMilestone.getOpenTickets()),
                    TicketsPage.class,
                    queryParameters(null, currentMilestone.name, openStatii, null, sortBy, desc, 1)));
                    queryParameters(null, currentMilestone.name, TicketsUI.openStatii, null, sortBy, desc, 1)));
            milestonePanel.add(new LinkPanel("closedTickets", null,
                    MessageFormat.format(getString("gb.nClosedTickets"), currentMilestone.getClosedTickets()),
                    TicketsPage.class,
                    queryParameters(null, currentMilestone.name, closedStatii, null, sortBy, desc, 1)));
                    queryParameters(null, currentMilestone.name, TicketsUI.closedStatii, null, sortBy, desc, 1)));
            milestonePanel.add(new Label("totalTickets", MessageFormat.format(getString("gb.nTotalTickets"), currentMilestone.getTotalTickets())));
            add(milestonePanel);
@@ -285,7 +277,7 @@
                queryParameters(
                        null,
                        milestoneParam,
                        openStatii,
                        TicketsUI.openStatii,
                        null,
                        null,
                        true,
@@ -395,8 +387,8 @@
        } else {
            add(new Label("selectedStatii", StringUtils.flattenStrings(Arrays.asList(statiiParam), ",")));
        }
        add(new BookmarkablePageLink<Void>("openTickets", TicketsPage.class, queryParameters(queryParam, milestoneParam, openStatii, assignedToParam, sortBy, desc, 1)));
        add(new BookmarkablePageLink<Void>("closedTickets", TicketsPage.class, queryParameters(queryParam, milestoneParam, closedStatii, assignedToParam, sortBy, desc, 1)));
        add(new BookmarkablePageLink<Void>("openTickets", TicketsPage.class, queryParameters(queryParam, milestoneParam, TicketsUI.openStatii, assignedToParam, sortBy, desc, 1)));
        add(new BookmarkablePageLink<Void>("closedTickets", TicketsPage.class, queryParameters(queryParam, milestoneParam, TicketsUI.closedStatii, assignedToParam, sortBy, desc, 1)));
        add(new BookmarkablePageLink<Void>("allTickets", TicketsPage.class, queryParameters(queryParam, milestoneParam, null, assignedToParam, sortBy, desc, 1)));
        // by status
@@ -410,7 +402,7 @@
            public void populateItem(final Item<Status> item) {
                final Status status = item.getModelObject();
                PageParameters p = queryParameters(queryParam, milestoneParam, new String [] { status.name().toLowerCase() }, assignedToParam, sortBy, desc, 1);
                String css = getStatusClass(status);
                String css = TicketsUI.getStatusClass(status);
                item.add(new LinkPanel("statusLink", css, status.toString(), TicketsPage.class, p).setRenderBodyOnly(true));
            }
        };
@@ -489,195 +481,128 @@
        // paging links
        buildPager(queryParam, milestoneParam, statiiParam, assignedToParam, sortBy, desc, page, pageSize, results.size(), totalResults);
        ListDataProvider<QueryResult> resultsDataProvider = new ListDataProvider<QueryResult>(results);
        DataView<QueryResult> ticketsView = new DataView<QueryResult>("ticket", resultsDataProvider) {
            private static final long serialVersionUID = 1L;
        add(new TicketListPanel("ticketList", results, false, false));
            @Override
            public void populateItem(final Item<QueryResult> item) {
                final QueryResult ticket = item.getModelObject();
                item.add(getStateIcon("state", ticket.type, ticket.status));
                item.add(new Label("id", "" + ticket.number));
                UserModel creator = app().users().getUserModel(ticket.createdBy);
                if (creator != null) {
                    item.add(new LinkPanel("createdBy", null, creator.getDisplayName(),
                        UserPage.class, WicketUtils.newUsernameParameter(ticket.createdBy)));
                } else {
                    item.add(new Label("createdBy", ticket.createdBy));
                }
                item.add(WicketUtils.createDateLabel("createDate", ticket.createdAt, GitBlitWebSession
                        .get().getTimezone(), getTimeUtils(), false));
        // new milestone link
        RepositoryModel repositoryModel = getRepositoryModel();
        final boolean acceptingUpdates = app().tickets().isAcceptingTicketUpdates(repositoryModel)
                 && user != null && user.canAdmin(getRepositoryModel());
        if (acceptingUpdates) {
            add(new LinkPanel("newMilestone", null, getString("gb.newMilestone"),
                NewMilestonePage.class, WicketUtils.newRepositoryParameter(repositoryName)));
        } else {
            add(new Label("newMilestone").setVisible(false));
        }
                if (ticket.updatedAt == null) {
                    item.add(new Label("updated").setVisible(false));
                } else {
                    Fragment updated = new Fragment("updated", "updatedFragment", this);
                    UserModel updater = app().users().getUserModel(ticket.updatedBy);
                    if (updater != null) {
                        updated.add(new LinkPanel("updatedBy", null, updater.getDisplayName(),
                                UserPage.class, WicketUtils.newUsernameParameter(ticket.updatedBy)));
                    } else {
                        updated.add(new Label("updatedBy", ticket.updatedBy));
                    }
                    updated.add(WicketUtils.createDateLabel("updateDate", ticket.updatedAt, GitBlitWebSession
                            .get().getTimezone(), getTimeUtils(), false));
                    item.add(updated);
                }
                item.add(new LinkPanel("title", "list subject", StringUtils.trimString(
                        ticket.title, Constants.LEN_SHORTLOG), TicketsPage.class, newTicketParameter(ticket)));
                ListDataProvider<String> labelsProvider = new ListDataProvider<String>(ticket.getLabels());
                DataView<String> labelsView = new DataView<String>("labels", labelsProvider) {
                    private static final long serialVersionUID = 1L;
                    @Override
                    public void populateItem(final Item<String> labelItem) {
                        String content = bugtraqProcessor().processPlainCommitMessage(getRepository(), repositoryName, labelItem.getModelObject());
                        Label label = new Label("label", content);
                        label.setEscapeModelStrings(false);
                        TicketLabel tLabel = app().tickets().getLabel(getRepositoryModel(), labelItem.getModelObject());
                        String background = MessageFormat.format("background-color:{0};", tLabel.color);
                        label.add(new SimpleAttributeModifier("style", background));
                        labelItem.add(label);
                    }
                };
                item.add(labelsView);
                if (StringUtils.isEmpty(ticket.responsible)) {
                    item.add(new Label("responsible").setVisible(false));
                } else {
                    UserModel responsible = app().users().getUserModel(ticket.responsible);
                    if (responsible == null) {
                        responsible = new UserModel(ticket.responsible);
                    }
                    GravatarImage avatar = new GravatarImage("responsible", responsible.getDisplayName(),
                            responsible.emailAddress, null, 16, true);
                    avatar.setTooltip(getString("gb.responsible") + ": " + responsible.getDisplayName());
                    item.add(avatar);
                }
                // votes indicator
                Label v = new Label("votes", "" + ticket.votesCount);
                WicketUtils.setHtmlTooltip(v, getString("gb.votes"));
                item.add(v.setVisible(ticket.votesCount > 0));
                // watching indicator
                item.add(new Label("watching").setVisible(ticket.isWatching(GitBlitWebSession.get().getUsername())));
                // status indicator
                String css = getLozengeClass(ticket.status, true);
                Label l = new Label("status", ticket.status.toString());
                WicketUtils.setCssClass(l, css);
                item.add(l);
                // add the ticket indicators/icons
                List<Indicator> indicators = new ArrayList<Indicator>();
                // comments
                if (ticket.commentsCount > 0) {
                    int count = ticket.commentsCount;
                    String pattern = "gb.nComments";
                    if (count == 1) {
                        pattern = "gb.oneComment";
                    }
                    indicators.add(new Indicator("fa fa-comment", count, pattern));
                }
                // participants
                if (!ArrayUtils.isEmpty(ticket.participants)) {
                    int count = ticket.participants.size();
                    if (count > 1) {
                        String pattern = "gb.nParticipants";
                        indicators.add(new Indicator("fa fa-user", count, pattern));
                    }
                }
                // attachments
                if (!ArrayUtils.isEmpty(ticket.attachments)) {
                    int count = ticket.attachments.size();
                    String pattern = "gb.nAttachments";
                    if (count == 1) {
                        pattern = "gb.oneAttachment";
                    }
                    indicators.add(new Indicator("fa fa-file", count, pattern));
                }
                // patchset revisions
                if (ticket.patchset != null) {
                    int count = ticket.patchset.commits;
                    String pattern = "gb.nCommits";
                    if (count == 1) {
                        pattern = "gb.oneCommit";
                    }
                    indicators.add(new Indicator("fa fa-code", count, pattern));
                }
                // milestone
                if (!StringUtils.isEmpty(ticket.milestone)) {
                    indicators.add(new Indicator("fa fa-bullseye", ticket.milestone));
                }
                ListDataProvider<Indicator> indicatorsDp = new ListDataProvider<Indicator>(indicators);
                DataView<Indicator> indicatorsView = new DataView<Indicator>("indicators", indicatorsDp) {
                    private static final long serialVersionUID = 1L;
                    @Override
                    public void populateItem(final Item<Indicator> item) {
                        Indicator indicator = item.getModelObject();
                        String tooltip = indicator.getTooltip();
                        Label icon = new Label("icon");
                        WicketUtils.setCssClass(icon, indicator.css);
                        item.add(icon);
                        if (indicator.count > 0) {
                            Label count = new Label("count", "" + indicator.count);
                            item.add(count.setVisible(!StringUtils.isEmpty(tooltip)));
                        } else {
                            item.add(new Label("count").setVisible(false));
                        }
                        WicketUtils.setHtmlTooltip(item, tooltip);
                    }
                };
                item.add(indicatorsView);
        // milestones list
        List<TicketMilestone> openMilestones = new ArrayList<TicketMilestone>();
        List<TicketMilestone> closedMilestones = new ArrayList<TicketMilestone>();
        for (TicketMilestone milestone : app().tickets().getMilestones(repositoryModel)) {
            if (milestone.isOpen()) {
                openMilestones.add(milestone);
            } else {
                closedMilestones.add(milestone);
            }
        };
        add(ticketsView);
        }
        Collections.sort(openMilestones, new Comparator<TicketMilestone>() {
            @Override
            public int compare(TicketMilestone o1, TicketMilestone o2) {
                return o2.due.compareTo(o1.due);
            }
        });
        List<TicketMilestone> allMilestones = app().tickets().getMilestones(getRepositoryModel());
        ListDataProvider<TicketMilestone> allMilestonesDp = new ListDataProvider<TicketMilestone>(allMilestones);
        DataView<TicketMilestone> milestonesList = new DataView<TicketMilestone>("milestoneList", allMilestonesDp) {
        Collections.sort(closedMilestones, new Comparator<TicketMilestone>() {
            @Override
            public int compare(TicketMilestone o1, TicketMilestone o2) {
                return o2.due.compareTo(o1.due);
            }
        });
        DataView<TicketMilestone> openMilestonesList = milestoneList("openMilestonesList", openMilestones, acceptingUpdates);
        add(openMilestonesList);
        DataView<TicketMilestone> closedMilestonesList = milestoneList("closedMilestonesList", closedMilestones, acceptingUpdates);
        add(closedMilestonesList);
    }
    protected DataView<TicketMilestone> milestoneList(String wicketId, List<TicketMilestone> milestones, final boolean acceptingUpdates) {
        ListDataProvider<TicketMilestone> milestonesDp = new ListDataProvider<TicketMilestone>(milestones);
        DataView<TicketMilestone> milestonesList = new DataView<TicketMilestone>(wicketId, milestonesDp) {
            private static final long serialVersionUID = 1L;
            @Override
            public void populateItem(final Item<TicketMilestone> item) {
                Fragment entryPanel = new Fragment("entryPanel", "milestoneListFragment", this);
                item.add(entryPanel);
                final TicketMilestone tm = item.getModelObject();
                PageParameters params = queryParameters(null, tm.name, null, null, null, desc, 1);
                item.add(new LinkPanel("milestoneName", null, tm.name, TicketsPage.class, params).setRenderBodyOnly(true));
                String [] states;
                if (tm.isOpen()) {
                    states = TicketsUI.openStatii;
                } else {
                    states = TicketsUI.closedStatii;
                }
                PageParameters params = queryParameters(null, tm.name, states, null, null, true, 1);
                entryPanel.add(new LinkPanel("milestoneName", null, tm.name, TicketsPage.class, params).setRenderBodyOnly(true));
                String css;
                String status = tm.status.name();
                switch (tm.status) {
                case Open:
                    css = "aui-lozenge aui-lozenge-subtle";
                    if (tm.isOverdue()) {
                        css = "aui-lozenge aui-lozenge-subtle aui-lozenge-error";
                        status = "overdue";
                    } else {
                        css = "aui-lozenge aui-lozenge-subtle";
                    }
                    break;
                default:
                    css = "aui-lozenge";
                    break;
                }
                Label stateLabel = new Label("milestoneState", tm.status.name());
                Label stateLabel = new Label("milestoneState", status);
                WicketUtils.setCssClass(stateLabel, css);
                item.add(stateLabel);
                entryPanel.add(stateLabel);
                if (tm.due == null) {
                    item.add(new Label("milestoneDue", getString("gb.notSpecified")));
                    entryPanel.add(new Label("milestoneDue", getString("gb.notSpecified")));
                } else {
                    item.add(WicketUtils.createDatestampLabel("milestoneDue", tm.due, getTimeZone(), getTimeUtils()));
                    entryPanel.add(WicketUtils.createDatestampLabel("milestoneDue", tm.due, getTimeZone(), getTimeUtils()));
                }
                if (acceptingUpdates) {
                    entryPanel.add(new LinkPanel("editMilestone", null, getString("gb.edit"), EditMilestonePage.class,
                        WicketUtils.newObjectParameter(repositoryName, tm.name)));
                } else {
                    entryPanel.add(new Label("editMilestone").setVisible(false));
                }
                if (tm.isOpen()) {
                    // re-load milestone with query results
                    TicketMilestone m = app().tickets().getMilestone(getRepositoryModel(), tm.name);
                    Fragment milestonePanel = new Fragment("milestonePanel", "openMilestoneFragment", this);
                    Label label = new Label("progress");
                    WicketUtils.setCssStyle(label, "width:" + m.getProgress() + "%;");
                    milestonePanel.add(label);
                    milestonePanel.add(new LinkPanel("openTickets", null,
                            MessageFormat.format(getString("gb.nOpenTickets"), m.getOpenTickets()),
                            TicketsPage.class,
                            queryParameters(null, tm.name, TicketsUI.openStatii, null, null, true, 1)));
                    milestonePanel.add(new LinkPanel("closedTickets", null,
                            MessageFormat.format(getString("gb.nClosedTickets"), m.getClosedTickets()),
                            TicketsPage.class,
                            queryParameters(null, tm.name, TicketsUI.closedStatii, null, null, true, 1)));
                    milestonePanel.add(new Label("totalTickets", MessageFormat.format(getString("gb.nTotalTickets"), m.getTotalTickets())));
                    entryPanel.add(milestonePanel);
                } else {
                    entryPanel.add(new Label("milestonePanel").setVisible(false));
                }
            }
        };
        add(milestonesList);
        return milestonesList;
    }
    protected PageParameters queryParameters(
@@ -779,124 +704,5 @@
            }
        };
        add(pagesView);
    }
    private class Indicator implements Serializable {
        private static final long serialVersionUID = 1L;
        final String css;
        final int count;
        final String tooltip;
        Indicator(String css, String tooltip) {
            this.css = css;
            this.tooltip = tooltip;
            this.count = 0;
        }
        Indicator(String css, int count, String pattern) {
            this.css = css;
            this.count = count;
            this.tooltip = StringUtils.isEmpty(pattern) ? "" : MessageFormat.format(getString(pattern), count);
        }
        String getTooltip() {
            return tooltip;
        }
    }
    private class TicketQuery implements Serializable, Comparable<TicketQuery> {
        private static final long serialVersionUID = 1L;
        final String name;
        final String query;
        String color;
        TicketQuery(String name, String query) {
            this.name = name;
            this.query = query;
        }
        TicketQuery color(String value) {
            this.color = value;
            return this;
        }
        @Override
        public boolean equals(Object o) {
            if (o instanceof TicketQuery) {
                return ((TicketQuery) o).query.equals(query);
            }
            return false;
        }
        @Override
        public int hashCode() {
            return query.hashCode();
        }
        @Override
        public int compareTo(TicketQuery o) {
            return query.compareTo(o.query);
        }
    }
    private class TicketSort implements Serializable {
        private static final long serialVersionUID = 1L;
        final String name;
        final String sortBy;
        final boolean desc;
        TicketSort(String name, String sortBy, boolean desc) {
            this.name = name;
            this.sortBy = sortBy;
            this.desc = desc;
        }
    }
    private class TicketSearchForm extends SessionlessForm<Void> implements Serializable {
        private static final long serialVersionUID = 1L;
        private final String repositoryName;
        private final IModel<String> searchBoxModel;;
        public TicketSearchForm(String id, String repositoryName, String text) {
            super(id, TicketsPage.this.getClass(), TicketsPage.this.getPageParameters());
            this.repositoryName = repositoryName;
            this.searchBoxModel = new Model<String>(text == null ? "" : text);
            TextField<String> searchBox = new TextField<String>("ticketSearchBox", searchBoxModel);
            add(searchBox);
        }
        void setTranslatedAttributes() {
            WicketUtils.setHtmlTooltip(get("ticketSearchBox"),
                    MessageFormat.format(getString("gb.searchTicketsTooltip"), repositoryName));
            WicketUtils.setInputPlaceholder(get("ticketSearchBox"), getString("gb.searchTickets"));
        }
        @Override
        public void onSubmit() {
            String searchString = searchBoxModel.getObject();
            if (StringUtils.isEmpty(searchString)) {
                // redirect to self to avoid wicket page update bug
                String absoluteUrl = getCanonicalUrl();
                getRequestCycle().setRequestTarget(new RedirectRequestTarget(absoluteUrl));
                return;
            }
            // use an absolute url to workaround Wicket-Tomcat problems with
            // mounted url parameters (issue-111)
            PageParameters params = WicketUtils.newRepositoryParameter(repositoryName);
            params.add("s", searchString);
            String absoluteUrl = getCanonicalUrl(TicketsPage.class, params);
            getRequestCycle().setRequestTarget(new RedirectRequestTarget(absoluteUrl));
        }
    }
}
src/main/java/com/gitblit/wicket/pages/TreePage.java
@@ -20,6 +20,7 @@
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.panel.Fragment;
import org.apache.wicket.markup.repeater.Item;
import org.apache.wicket.markup.repeater.data.DataView;
@@ -30,6 +31,7 @@
import com.gitblit.models.PathModel;
import com.gitblit.models.SubmoduleModel;
import com.gitblit.servlet.RawServlet;
import com.gitblit.utils.ByteFormat;
import com.gitblit.utils.JGitUtils;
import com.gitblit.wicket.CacheControl;
@@ -162,8 +164,8 @@
                        links.add(new BookmarkablePageLink<Void>("view", BlobPage.class,
                                WicketUtils.newPathParameter(repositoryName, id,
                                        path)));
                        links.add(new BookmarkablePageLink<Void>("raw", RawPage.class, WicketUtils
                                .newPathParameter(repositoryName, id, path)));
                        String rawUrl = RawServlet.asLink(getContextUrl(), repositoryName, id, path);
                        links.add(new ExternalLink("raw", rawUrl));
                        links.add(new BookmarkablePageLink<Void>("blame", BlamePage.class,
                                WicketUtils.newPathParameter(repositoryName, id,
                                        path)));
src/main/java/com/gitblit/wicket/pages/UserPage.html
@@ -7,27 +7,20 @@
<body>
<wicket:extend>
<div class="container">
    <div class="row" style="padding-top:10px;">
        <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 class="row" style="padding-top:15px;">
        <div class="span3">
            <div wicket:id="userTitlePanel"></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">
    </div>
    <div class="row" style="padding-top:10px;">
        <div class="span12">
            <div class="tabbable tabs-left">
                <!-- 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>
                    <div wicket:id="preferencesLink"></div>
                    <div wicket:id="sshKeysLink"></div>
                </ul>
    
                <!-- tab content -->
@@ -41,11 +34,51 @@
                            </tbody>
                        </table>
                    </div>
                    <!-- preferences tab -->
                    <div wicket:id="preferencesTab"></div>
                    <!-- ssh keys tab -->
                    <div wicket:id="sshKeysTab"></div>
                </div>
            </div>
        </div>
    </div>
</div>
<wicket:fragment wicket:id="preferencesLinkFragment">
    <li><a href="#preferences" data-toggle="tab"><wicket:message key="gb.preferences"></wicket:message></a></li>
</wicket:fragment>
<wicket:fragment wicket:id="sshKeysLinkFragment">
    <li><a href="#ssh" data-toggle="tab"><wicket:message key="gb.sshKeys"></wicket:message></a></li>
</wicket:fragment>
<wicket:fragment wicket:id="preferencesTabFragment">
    <div class="tab-pane" id="preferences">
        <h4><wicket:message key="gb.accountPreferences"></wicket:message></h4>
        <p><wicket:message key="gb.accountPreferencesDescription"></wicket:message></p>
        <hr />
        <form wicket:id="prefsForm">
            <div wicket:id="displayName"></div>
            <div wicket:id="emailAddress"></div>
            <div wicket:id="emailMeOnMyTicketChanges"></div>
            <div wicket:id="language"></div>
            <div wicket:id="transport"></div>
            <div class="form-actions"><input class="btn btn-appmenu" type="submit" value="Save" wicket:message="value:gb.save" wicket:id="save" /></div>
        </form>
    </div>
</wicket:fragment>
<wicket:fragment wicket:id="sshKeysTabFragment">
    <div class="tab-pane" id="ssh">
        <div wicket:id="sshKeysPanel"></div>
    </div>
</wicket:fragment>
</wicket:extend>
</body>
</html>
src/main/java/com/gitblit/wicket/pages/UserPage.java
@@ -15,20 +15,32 @@
 */
package com.gitblit.wicket.pages;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import org.apache.wicket.PageParameters;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.markup.html.form.AjaxButton;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.link.BookmarkablePageLink;
import org.apache.wicket.markup.html.form.Form;
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.PersonIdent;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.Model;
import com.gitblit.Constants.Transport;
import com.gitblit.GitBlitException;
import com.gitblit.Keys;
import com.gitblit.models.Menu.ParameterMenuItem;
import com.gitblit.models.NavLink;
import com.gitblit.models.NavLink.DropDownPageMenuNavLink;
import com.gitblit.models.ProjectModel;
import com.gitblit.models.RepositoryModel;
import com.gitblit.models.UserModel;
@@ -36,13 +48,13 @@
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.BooleanOption;
import com.gitblit.wicket.panels.ChoiceOption;
import com.gitblit.wicket.panels.ProjectRepositoryPanel;
import com.gitblit.wicket.panels.SshKeysPanel;
import com.gitblit.wicket.panels.TextOption;
import com.gitblit.wicket.panels.UserTitlePanel;
public class UserPage extends RootPage {
@@ -83,21 +95,30 @@
            user = new UserModel(userName);
        }
        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(app().settings().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));
        add(new UserTitlePanel("userTitlePanel", user, user.username));
        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));
        boolean isMyProfile = sessionUser != null && sessionUser.equals(user);
        if (isMyProfile) {
            addPreferences(user);
            if (app().gitblit().isServingSSH()) {
                // show the SSH key management tab
                addSshKeys(user);
            } else {
                // SSH daemon is disabled, hide keys tab
                add(new Label("sshKeysLink").setVisible(false));
                add(new Label("sshKeysTab").setVisible(false));
            }
        } else {
            add(new Label("newRepository").setVisible(false));
            // visiting user
            add(new Label("preferencesLink").setVisible(false));
            add(new Label("preferencesTab").setVisible(false));
            add(new Label("sshKeysLink").setVisible(false));
            add(new Label("sshKeysTab").setVisible(false));
        }
        List<RepositoryModel> repositories = getRepositories(params);
@@ -127,10 +148,10 @@
    }
    @Override
    protected void addDropDownMenus(List<PageRegistration> pages) {
    protected void addDropDownMenus(List<NavLink> navLinks) {
        PageParameters params = getPageParameters();
        DropDownMenuRegistration menu = new DropDownMenuRegistration("gb.filters",
        DropDownPageMenuNavLink menu = new DropDownPageMenuNavLink("gb.filters",
                UserPage.class);
        // preserve time filter option on repository choices
        menu.menuItems.addAll(getRepositoryFilterItems(params));
@@ -140,9 +161,173 @@
        if (menu.menuItems.size() > 0) {
            // Reset Filter
            menu.menuItems.add(new DropDownMenuItem(getString("gb.reset"), null, null));
            menu.menuItems.add(new ParameterMenuItem(getString("gb.reset")));
        }
        pages.add(menu);
        navLinks.add(menu);
    }
    private void addPreferences(UserModel user) {
        // add preferences
        Form<Void> prefs = new Form<Void>("prefsForm");
        List<Language> languages = Arrays.asList(
                new Language("Deutsch","de"),
                new Language("English","en"),
                new Language("Español", "es"),
                new Language("Français", "fr"),
                new Language("Italiano", "it"),
                new Language("日本語", "ja"),
                new Language("한국말", "ko"),
                new Language("Nederlands", "nl"),
                new Language("Norsk", "no"),
                new Language("Język Polski", "pl"),
                new Language("Português", "pt_BR"),
                new Language("中文", "zh_CN"));
        Locale locale = user.getPreferences().getLocale();
        if (locale == null) {
            // user has not specified language preference
            // try server default preference
            String lc = app().settings().getString(Keys.web.forceDefaultLocale, null);
            if (StringUtils.isEmpty(lc)) {
                // server default language is not configured
                // try browser preference
                Locale sessionLocale = GitBlitWebSession.get().getLocale();
                if (sessionLocale != null) {
                    locale = sessionLocale;
                }
            } else {
            }
        }
        Language preferredLanguage = null;
        if (locale != null) {
            String localeCode = locale.getLanguage();
            if (!StringUtils.isEmpty(locale.getCountry())) {
                localeCode += "_" + locale.getCountry();
            }
            for (Language language : languages) {
                if (language.code.equals(localeCode)) {
                    // language_COUNTRY match
                    preferredLanguage = language;
                } else if (preferredLanguage != null && language.code.startsWith(locale.getLanguage())) {
                    // language match
                    preferredLanguage = language;
                }
            }
        }
        final IModel<String> displayName = Model.of(user.getDisplayName());
        final IModel<String> emailAddress = Model.of(user.emailAddress == null ? "" : user.emailAddress);
        final IModel<Language> language = Model.of(preferredLanguage);
        final IModel<Boolean> emailMeOnMyTicketChanges = Model.of(user.getPreferences().isEmailMeOnMyTicketChanges());
        final IModel<Transport> transport = Model.of(user.getPreferences().getTransport());
        prefs.add(new TextOption("displayName",
                getString("gb.displayName"),
                getString("gb.displayNameDescription"),
                displayName).setVisible(app().authentication().supportsDisplayNameChanges(user)));
        prefs.add(new TextOption("emailAddress",
                getString("gb.emailAddress"),
                getString("gb.emailAddressDescription"),
                emailAddress).setVisible(app().authentication().supportsEmailAddressChanges(user)));
        prefs.add(new ChoiceOption<Language>("language",
                getString("gb.languagePreference"),
                getString("gb.languagePreferenceDescription"),
                language,
                languages));
        prefs.add(new BooleanOption("emailMeOnMyTicketChanges",
                getString("gb.emailMeOnMyTicketChanges"),
                getString("gb.emailMeOnMyTicketChangesDescription"),
                emailMeOnMyTicketChanges).setVisible(app().notifier().isSendingMail()));
        List<Transport> availableTransports = new ArrayList<>();
        if (app().gitblit().isServingSSH()) {
            availableTransports.add(Transport.SSH);
        }
        if (app().gitblit().isServingHTTP()) {
            availableTransports.add(Transport.HTTPS);
            availableTransports.add(Transport.HTTP);
        }
        if (app().gitblit().isServingGIT()) {
            availableTransports.add(Transport.GIT);
        }
        prefs.add(new ChoiceOption<Transport>("transport",
                getString("gb.transportPreference"),
                getString("gb.transportPreferenceDescription"),
                transport,
                availableTransports));
        prefs.add(new AjaxButton("save") {
            private static final long serialVersionUID = 1L;
            @Override
            protected void onSubmit(AjaxRequestTarget target, Form<?> form) {
                UserModel user = GitBlitWebSession.get().getUser();
                user.displayName = displayName.getObject();
                user.emailAddress = emailAddress.getObject();
                Language lang = language.getObject();
                if (lang != null) {
                    user.getPreferences().setLocale(lang.code);
                }
                user.getPreferences().setEmailMeOnMyTicketChanges(emailMeOnMyTicketChanges.getObject());
                user.getPreferences().setTransport(transport.getObject());
                try {
                    app().gitblit().reviseUser(user.username, user);
                    setRedirect(true);
                    setResponsePage(UserPage.class, WicketUtils.newUsernameParameter(user.username));
                } catch (GitBlitException e) {
                    // logger.error("Failed to update user " + user.username, e);
                    // error(getString("gb.failedToUpdateUser"), false);
                }
            }
        });
        // add the preferences tab
        add(new Fragment("preferencesLink", "preferencesLinkFragment", this).setRenderBodyOnly(true));
        Fragment fragment = new Fragment("preferencesTab", "preferencesTabFragment", this);
        fragment.add(prefs);
        add(fragment.setRenderBodyOnly(true));
    }
    private void addSshKeys(final UserModel user) {
        Fragment keysTab = new Fragment("sshKeysTab", "sshKeysTabFragment", this);
        keysTab.add(new SshKeysPanel("sshKeysPanel", user));
        // add the SSH keys tab
        add(new Fragment("sshKeysLink", "sshKeysLinkFragment", this).setRenderBodyOnly(true));
        add(keysTab.setRenderBodyOnly(true));
    }
    private class Language implements Serializable {
        private static final long serialVersionUID = 1L;
        final String name;
        final String code;
        public Language(String name, String code) {
            this.name = name;
            this.code = code;
        }
        @Override
        public String toString() {
            return name + " (" + code +")";
        }
    }
}
src/main/java/com/gitblit/wicket/pages/UsersPage.html
@@ -6,8 +6,6 @@
<body>
<wicket:extend>
<div class="container">
    <div wicket:id="teamsPanel">[teams panel]</div>
    <div wicket:id="usersPanel">[users panel]</div>
</div>
</wicket:extend>
src/main/java/com/gitblit/wicket/pages/UsersPage.java
@@ -16,7 +16,6 @@
package com.gitblit.wicket.pages;
import com.gitblit.wicket.RequiresAdminRole;
import com.gitblit.wicket.panels.TeamsPanel;
import com.gitblit.wicket.panels.UsersPanel;
@RequiresAdminRole
@@ -25,8 +24,6 @@
    public UsersPage() {
        super();
        setupPage("", "");
        add(new TeamsPanel("teamsPanel", showAdmin).setVisible(showAdmin));
        add(new UsersPanel("usersPanel", showAdmin).setVisible(showAdmin));
    }
src/main/java/com/gitblit/wicket/pages/create_git.md
New file
@@ -0,0 +1,6 @@
    touch README.md
    git init
    git add README.md
    git commit -m "first commit"
    git remote add origin ${primaryUrl}
    git push -u origin master
src/main/java/com/gitblit/wicket/pages/existing_git.md
New file
@@ -0,0 +1,2 @@
    git remote add origin ${primaryUrl}
    git push -u origin master
src/main/java/com/gitblit/wicket/pages/propose_git.md
@@ -2,5 +2,5 @@
    cd ${repo}
    git checkout -b ${reviewBranch} origin/${integrationBranch}
    ...
    git push --set-upstream origin ${reviewBranch}
    git push -u origin ${reviewBranch}
src/main/java/com/gitblit/wicket/panels/AccessPolicyPanel.html
New file
@@ -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">
<body>
<wicket:panel>
    <h4><wicket:message key="gb.accessPolicy"></wicket:message></h4>
    <p><wicket:message key="gb.accessPolicyDescription"></wicket:message></p>
    <div wicket:id="policiesGroup">
        <div wicket:id="policies" style="padding-top:4px;">
            <div>
                <label style="font-weight:bold;margin-bottom:1px;"><input type="radio" wicket:id="radio" /> <img wicket:id="image"></img> <span wicket:id="name"></span></label>
            </div>
            <label class="checkbox" style="color:#777;" wicket:id="description"></label>
        </div>
    </div>
    <div wicket:id="allowForks"></div>
<wicket:fragment wicket:id="allowForksFragment">
    <hr />
    <div wicket:id="allowForks"></div>
</wicket:fragment>
</wicket:panel>
</body>
</html>
src/main/java/com/gitblit/wicket/panels/AccessPolicyPanel.java
New file
@@ -0,0 +1,199 @@
/*
 * Copyright 2014 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.ajax.form.AjaxFormChoiceComponentUpdatingBehavior;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.form.Radio;
import org.apache.wicket.markup.html.form.RadioGroup;
import org.apache.wicket.markup.html.list.ListItem;
import org.apache.wicket.markup.html.list.ListView;
import org.apache.wicket.markup.html.panel.Fragment;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.Model;
import com.gitblit.Constants.AccessRestrictionType;
import com.gitblit.Constants.AuthorizationControl;
import com.gitblit.Keys;
import com.gitblit.models.RepositoryModel;
import com.gitblit.wicket.WicketUtils;
/**
 * A radio group panel of the 5 available authorization/access restriction combinations.
 *
 * @author James Moger
 *
 */
public class AccessPolicyPanel extends BasePanel {
    private static final long serialVersionUID = 1L;
    private final RepositoryModel repository;
    private final AjaxFormChoiceComponentUpdatingBehavior callback;
    private RadioGroup<AccessPolicy> policiesGroup;
    private IModel<Boolean> allowForks;
    public AccessPolicyPanel(String wicketId, RepositoryModel repository) {
        this(wicketId, repository, null);
    }
    public AccessPolicyPanel(String wicketId, RepositoryModel repository, AjaxFormChoiceComponentUpdatingBehavior callback) {
        super(wicketId);
        this.repository = repository;
        this.callback = callback;
    }
    @Override
    protected void onInitialize() {
        super.onInitialize();
        AccessPolicy anonymousPolicy = new AccessPolicy(getString("gb.anonymousPolicy"),
                getString("gb.anonymousPolicyDescription"),
                "blank.png",
                AuthorizationControl.AUTHENTICATED,
                AccessRestrictionType.NONE);
        AccessPolicy authenticatedPushPolicy = new AccessPolicy(getString("gb.authenticatedPushPolicy"),
                getString("gb.authenticatedPushPolicyDescription"),
                "lock_go_16x16.png",
                AuthorizationControl.AUTHENTICATED,
                AccessRestrictionType.PUSH);
        AccessPolicy namedPushPolicy = new AccessPolicy(getString("gb.namedPushPolicy"),
                getString("gb.namedPushPolicyDescription"),
                "lock_go_16x16.png",
                AuthorizationControl.NAMED,
                AccessRestrictionType.PUSH);
        AccessPolicy clonePolicy = new AccessPolicy(getString("gb.clonePolicy"),
                getString("gb.clonePolicyDescription"),
                "lock_pull_16x16.png",
                AuthorizationControl.NAMED,
                AccessRestrictionType.CLONE);
        AccessPolicy viewPolicy = new AccessPolicy(getString("gb.viewPolicy"),
                getString("gb.viewPolicyDescription"),
                "shield_16x16.png",
                AuthorizationControl.NAMED,
                AccessRestrictionType.VIEW);
        List<AccessPolicy> policies = new ArrayList<AccessPolicy>();
        if (app().settings().getBoolean(Keys.git.allowAnonymousPushes, false)) {
            policies.add(anonymousPolicy);
        }
        policies.add(authenticatedPushPolicy);
        policies.add(namedPushPolicy);
        policies.add(clonePolicy);
        policies.add(viewPolicy);
        AccessRestrictionType defaultRestriction = repository.accessRestriction;
        if (defaultRestriction == null) {
            defaultRestriction = AccessRestrictionType.fromName(app().settings().getString(Keys.git.defaultAccessRestriction,
                    AccessRestrictionType.PUSH.name()));
        }
        AuthorizationControl defaultControl = repository.authorizationControl;
        if (defaultControl == null) {
            defaultControl = AuthorizationControl.fromName(app().settings().getString(Keys.git.defaultAuthorizationControl,
                    AuthorizationControl.NAMED.name()));
        }
        AccessPolicy defaultPolicy = namedPushPolicy;
        for (AccessPolicy policy : policies) {
            if (policy.type == defaultRestriction && policy.control == defaultControl) {
                defaultPolicy = policy;
            }
        }
        policiesGroup = new RadioGroup<>("policiesGroup", new Model<AccessPolicy>(defaultPolicy));
        ListView<AccessPolicy> policiesList = new ListView<AccessPolicy>("policies", policies) {
            private static final long serialVersionUID = 1L;
            @Override
            protected void populateItem(ListItem<AccessPolicy> item) {
                AccessPolicy p = item.getModelObject();
                item.add(new Radio<AccessPolicy>("radio", item.getModel()));
                item.add(WicketUtils.newImage("image",  p.image));
                item.add(new Label("name", p.name));
                item.add(new Label("description", p.description));
            }
        };
        policiesGroup.add(policiesList);
        if (callback != null) {
            policiesGroup.add(callback);
            policiesGroup.setOutputMarkupId(true);
        }
        add(policiesGroup);
        allowForks = Model.of(app().settings().getBoolean(Keys.web.allowForking, true));
        if (allowForks.getObject()) {
            Fragment fragment = new Fragment("allowForks", "allowForksFragment", this);
            fragment.add(new BooleanOption("allowForks",
                getString("gb.allowForks"),
                getString("gb.allowForksDescription"),
                allowForks));
            add(fragment);
        } else {
            add(new Label("allowForks").setVisible(false));
        }
        setOutputMarkupId(true);
    }
    public void updateModel(RepositoryModel repository) {
        AccessPolicy policy = policiesGroup.getModelObject();
        repository.authorizationControl = policy.control;
        repository.accessRestriction = policy.type;
        repository.allowForks = allowForks.getObject();
    }
    @Override
    protected boolean getStatelessHint() {
        return false;
    }
    public static class AccessPolicy implements Serializable {
        private static final long serialVersionUID = 1L;
        final String name;
        final String description;
        final String image;
        final AuthorizationControl control;
        final AccessRestrictionType type;
        AccessPolicy(String name, String description, String img, AuthorizationControl control, AccessRestrictionType type) {
            this.name = name;
            this.description = description;
            this.image = img;
            this.control = control;
            this.type = type;
        }
        @Override
        public String toString() {
            return name;
        }
    }
}
src/main/java/com/gitblit/wicket/panels/BasePanel.java
@@ -44,6 +44,10 @@
        return GitBlitWebApp.get();
    }
    protected String getContextUrl() {
        return getRequest().getRelativePathPrefixToContextRoot();
    }
    protected TimeZone getTimeZone() {
        return app().settings().getBoolean(Keys.web.useClientTimezone, false) ? GitBlitWebSession.get()
                .getTimezone() : app().getTimezone();
src/main/java/com/gitblit/wicket/panels/BooleanChoiceOption.html
New file
@@ -0,0 +1,19 @@
<!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 style="padding-top:4px;">
        <div>
            <label style="font-weight:bold;margin-bottom:1px;" class="checkbox"><input type="checkbox" wicket:id="checkbox" /> <span wicket:id="name"></span></label>
        </div>
        <label class="checkbox" style="color:#777;"> <span wicket:id="description"></span>
        <p style="padding-top:5px;"><select class="span3" wicket:id="choice" /></p>
        </label>
    </div>
</wicket:panel>
</body>
</html>
src/main/java/com/gitblit/wicket/panels/BooleanChoiceOption.java
New file
@@ -0,0 +1,78 @@
/*
 * Copyright 2014 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.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.form.CheckBox;
import org.apache.wicket.markup.html.form.DropDownChoice;
import org.apache.wicket.model.IModel;
import org.parboiled.common.StringUtils;
/**
 * A re-usable conditional choice option panel.
 *
 * [x] title
 *     description
 *     [choices]
 *
 * @author James Moger
 *
 */
public class BooleanChoiceOption<T> extends BasePanel {
    private static final long serialVersionUID = 1L;
    final CheckBox checkbox;
    final DropDownChoice<T> choice;
    public BooleanChoiceOption(String wicketId, String title, String description, IModel<Boolean> checkboxModel, IModel<T> choiceModel, List<T> choices) {
        super(wicketId);
        add(new Label("name", title));
        add(new Label("description", description).setVisible(!StringUtils.isEmpty(description)));
        this.checkbox = new CheckBox("checkbox", checkboxModel);
        checkbox.setOutputMarkupId(true);
        this.choice = new DropDownChoice<T>("choice", choiceModel, choices);
        choice.setOutputMarkupId(true);
        setup();
    }
    private void setup() {
        add(checkbox);
        add(choice.setMarkupId("choice").setEnabled(choice.getChoices().size() > 0));
        choice.setEnabled(checkbox.getModelObject());
        checkbox.add(new AjaxFormComponentUpdatingBehavior("onchange") {
            private static final long serialVersionUID = 1L;
            @Override
            protected void onUpdate(AjaxRequestTarget target) {
                choice.setEnabled(checkbox.getModelObject());
                target.addComponent(choice);
                if (!choice.isEnabled()) {
                    choice.setModelObject(null);
                }
            }
        });
    }
}
src/main/java/com/gitblit/wicket/panels/BooleanOption.html
New file
@@ -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:panel>
    <div style="padding-top:4px;">
        <div>
            <label style="font-weight:bold;margin-bottom:1px;" class="checkbox"><input type="checkbox" wicket:id="checkbox" /> <span wicket:id="name"></span></label>
        </div>
        <label class="checkbox" style="color:#777;" wicket:id="description"></label>
    </div>
</wicket:panel>
</body>
</html>
src/main/java/com/gitblit/wicket/panels/BooleanOption.java
New file
@@ -0,0 +1,54 @@
/*
 * Copyright 2014 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.form.CheckBox;
import org.apache.wicket.model.IModel;
import org.parboiled.common.StringUtils;
/**
 * A re-usable checkbox option panel.
 *
 * [x] title
 *     description
 *
 * @author James Moger
 *
 */
public class BooleanOption extends BasePanel {
    private static final long serialVersionUID = 1L;
    public BooleanOption(String wicketId, String title, String description, IModel<Boolean> model) {
        super(wicketId);
        add(new Label("name", title));
        add(new Label("description", description).setVisible(!StringUtils.isEmpty(description)));
        add(new CheckBox("checkbox", model));
    }
    public BooleanOption(String wicketId, String title, String description, CheckBox checkbox) {
        super(wicketId);
        add(new Label("name", title));
        add(new Label("description", description).setVisible(!StringUtils.isEmpty(description)));
        add(checkbox.setMarkupId("checkbox"));
    }
    public BooleanOption setIsHtmlDescription(boolean val) {
        ((Label) get("description")).setEscapeModelStrings(!val);
        return this;
    }
}
src/main/java/com/gitblit/wicket/panels/BranchesPanel.html
@@ -29,21 +29,21 @@
    <!-- 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>
            <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="raw"><wicket:message key="gb.raw"></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>
            <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="raw"><wicket:message key="gb.raw"></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>
            <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="raw"><wicket:message key="gb.raw"></wicket:message></a>
        </span>
    </wicket:fragment>
            
src/main/java/com/gitblit/wicket/panels/BranchesPanel.java
@@ -40,6 +40,7 @@
import com.gitblit.models.RefModel;
import com.gitblit.models.RepositoryModel;
import com.gitblit.models.UserModel;
import com.gitblit.servlet.RawServlet;
import com.gitblit.servlet.SyndicationServlet;
import com.gitblit.utils.CommitCache;
import com.gitblit.utils.JGitUtils;
@@ -143,6 +144,8 @@
                            .newObjectParameter(model.name, entry.getName())));
                    fragment.add(new BookmarkablePageLink<Void>("tree", TreePage.class, WicketUtils
                            .newObjectParameter(model.name, entry.getName())));
                    String rawUrl = RawServlet.asLink(getContextUrl(), model.name, Repository.shortenRefName(entry.getName()), null);
                    fragment.add(new ExternalLink("raw",  rawUrl));
                    fragment.add(new BookmarkablePageLink<Void>("metrics", MetricsPage.class,
                            WicketUtils.newObjectParameter(model.name, entry.getName())));
                    fragment.add(new ExternalLink("syndication", SyndicationServlet.asLink(
@@ -158,6 +161,8 @@
                            .newObjectParameter(model.name, entry.getName())));
                    fragment.add(new BookmarkablePageLink<Void>("tree", TreePage.class, WicketUtils
                            .newObjectParameter(model.name, entry.getName())));
                    String rawUrl = RawServlet.asLink(getContextUrl(), model.name, Repository.shortenRefName(entry.getName()), null);
                    fragment.add(new ExternalLink("raw",  rawUrl));
                    item.add(fragment);
                }
                WicketUtils.setAlternatingBackground(item, counter);
src/main/java/com/gitblit/wicket/panels/ChoiceOption.html
New file
@@ -0,0 +1,19 @@
<!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 style="padding-top:4px;">
        <div style="margin-bottom:1px;">
            <b><span wicket:id="name"></span></b>
        </div>
        <label class="checkbox" style="color:#777;"> <span wicket:id="description"></span>
        <p style="padding-top:5px;"><select class="span3" wicket:id="choice" /></p>
        </label>
    </div>
</wicket:panel>
</body>
</html>
src/main/java/com/gitblit/wicket/panels/ChoiceOption.java
New file
@@ -0,0 +1,52 @@
/*
 * Copyright 2014 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.form.DropDownChoice;
import org.apache.wicket.model.IModel;
import org.parboiled.common.StringUtils;
/**
 * A re-usable choice option panel.
 *
 * title
 *     description
 *     [choices]
 *
 * @author James Moger
 *
 */
public class ChoiceOption<T> extends BasePanel {
    private static final long serialVersionUID = 1L;
    public ChoiceOption(String wicketId, String title, String description, IModel<T> model, List<T> choices) {
        super(wicketId);
        add(new Label("name", title));
        add(new Label("description", description).setVisible(!StringUtils.isEmpty(description)));
        add(new DropDownChoice<>("choice", model, choices).setEnabled(choices.size() > 0));
    }
    public ChoiceOption(String wicketId, String title, String description, DropDownChoice<?> choice) {
        super(wicketId);
        add(new Label("name", title));
        add(new Label("description", description).setVisible(!StringUtils.isEmpty(description)));
        add(choice.setMarkupId("choice").setEnabled(choice.getChoices().size() > 0));
    }
}
src/main/java/com/gitblit/wicket/panels/DropDownMenu.java
@@ -21,38 +21,90 @@
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.models.Menu.ExternalLinkMenuItem;
import com.gitblit.models.Menu.MenuDivider;
import com.gitblit.models.Menu.MenuItem;
import com.gitblit.models.Menu.PageLinkMenuItem;
import com.gitblit.models.Menu.ParameterMenuItem;
import com.gitblit.models.NavLink.DropDownMenuNavLink;
import com.gitblit.models.NavLink.DropDownPageMenuNavLink;
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) {
    public DropDownMenu(String id, String label, final DropDownPageMenuNavLink 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) {
        ListDataProvider<MenuItem> items = new ListDataProvider<MenuItem>(menu.menuItems);
        DataView<MenuItem> view = new DataView<MenuItem>("menuItems", items) {
            private static final long serialVersionUID = 1L;
            @Override
            public void populateItem(final Item<DropDownMenuItem> item) {
                DropDownMenuItem entry = item.getModelObject();
                if (entry.isDivider()) {
            public void populateItem(final Item<MenuItem> item) {
                MenuItem entry = item.getModelObject();
                if (entry instanceof PageLinkMenuItem) {
                    // link to another Wicket page
                    PageLinkMenuItem pageLink = (PageLinkMenuItem) entry;
                    item.add(new LinkPanel("menuItem", null, null, pageLink.toString(), pageLink.getPageClass(),
                            pageLink.getPageParameters(), false).setRenderBodyOnly(true));
                } else if (entry instanceof ExternalLinkMenuItem) {
                    // link to a specified href
                    ExternalLinkMenuItem extLink = (ExternalLinkMenuItem) entry;
                    item.add(new LinkPanel("menuItem", null, extLink.toString(), extLink.getHref(),
                            extLink.openInNewWindow()).setRenderBodyOnly(true));
                } else if (entry instanceof MenuDivider) {
                    // divider
                    item.add(new Label("menuItem").setRenderBodyOnly(true));
                    WicketUtils.setCssClass(item, "divider");
                } else {
                    ParameterMenuItem parameter = (ParameterMenuItem) entry;
                    // parameter link for the current page
                    String icon = null;
                    if (entry.isSelected()) {
                    if (parameter.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));
                            parameter.getPageParameters(), false).setRenderBodyOnly(true));
                }
            }
        };
        add(view);
        setRenderBodyOnly(true);
    }
    public DropDownMenu(String id, String label, final DropDownMenuNavLink menu) {
        super(id);
        add(new Label("label", label).setRenderBodyOnly(true));
        ListDataProvider<MenuItem> items = new ListDataProvider<MenuItem>(menu.menuItems);
        DataView<MenuItem> view = new DataView<MenuItem>("menuItems", items) {
            private static final long serialVersionUID = 1L;
            @Override
            public void populateItem(final Item<MenuItem> item) {
                MenuItem entry = item.getModelObject();
                if (entry instanceof PageLinkMenuItem) {
                    // link to another Wicket page
                    PageLinkMenuItem pageLink = (PageLinkMenuItem) entry;
                    item.add(new LinkPanel("menuItem", null, null, pageLink.toString(), pageLink.getPageClass(),
                            pageLink.getPageParameters(), false).setRenderBodyOnly(true));
                } else if (entry instanceof ExternalLinkMenuItem) {
                    // link to a specified href
                    ExternalLinkMenuItem extLink = (ExternalLinkMenuItem) entry;
                    item.add(new LinkPanel("menuItem", null, extLink.toString(), extLink.getHref(),
                            extLink.openInNewWindow()).setRenderBodyOnly(true));
                } else if (entry instanceof MenuDivider) {
                    // divider
                    item.add(new Label("menuItem").setRenderBodyOnly(true));
                    WicketUtils.setCssClass(item, "divider");
                } else {
                    throw new IllegalArgumentException(String.format("Unexpected menuitem type %s",
                            entry.getClass().getSimpleName()));
                }
            }
        };
src/main/java/com/gitblit/wicket/panels/FilterableRepositoryList.java
@@ -33,7 +33,6 @@
import com.gitblit.wicket.WicketUtils;
import com.gitblit.wicket.freemarker.FreemarkerPanel;
import com.gitblit.wicket.ng.NgController;
import com.gitblit.wicket.pages.EditRepositoryPage;
/**
 * A client-side filterable rich repository list which uses Freemarker, Wicket,
@@ -98,7 +97,7 @@
        }
        if (allowCreate) {
            panel.add(new LinkPanel(ngList + "Button", "btn btn-mini", getString("gb.newRepository"), EditRepositoryPage.class));
            panel.add(new LinkPanel(ngList + "Button", "btn btn-mini", getString("gb.newRepository"), app().getNewRepositoryPage()));
        } else {
            panel.add(new Label(ngList + "Button").setVisible(false));
        }
src/main/java/com/gitblit/wicket/panels/NavigationPanel.java
@@ -23,9 +23,11 @@
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.models.NavLink;
import com.gitblit.models.NavLink.DropDownMenuNavLink;
import com.gitblit.models.NavLink.DropDownPageMenuNavLink;
import com.gitblit.models.NavLink.ExternalNavLink;
import com.gitblit.models.NavLink.PageNavLink;
import com.gitblit.wicket.WicketUtils;
import com.gitblit.wicket.pages.BasePage;
@@ -34,45 +36,59 @@
    private static final long serialVersionUID = 1L;
    public NavigationPanel(String id, final Class<? extends BasePage> pageClass,
            List<PageRegistration> registeredPages) {
            List<NavLink> navLinks) {
        super(id);
        ListDataProvider<PageRegistration> refsDp = new ListDataProvider<PageRegistration>(
                registeredPages);
        DataView<PageRegistration> refsView = new DataView<PageRegistration>("navLink", refsDp) {
        ListDataProvider<NavLink> refsDp = new ListDataProvider<NavLink>(navLinks);
        DataView<NavLink> linksView = new DataView<NavLink>("navLink", refsDp) {
            private static final long serialVersionUID = 1L;
            @Override
            public void populateItem(final Item<PageRegistration> item) {
                PageRegistration entry = item.getModelObject();
                if (entry.hiddenPhone) {
            public void populateItem(final Item<NavLink> item) {
                NavLink navLink = item.getModelObject();
                String linkText = navLink.translationKey;
                try {
                    // try to lookup translation key
                    linkText = getString(navLink.translationKey);
                } catch (Exception e) {
                }
                if (navLink.hiddenPhone) {
                    WicketUtils.setCssClass(item, "hidden-phone");
                }
                if (entry instanceof OtherPageLink) {
                if (navLink instanceof ExternalNavLink) {
                    // other link
                    OtherPageLink link = (OtherPageLink) entry;
                    Component c = new LinkPanel("link", null, getString(entry.translationKey), link.url);
                    ExternalNavLink link = (ExternalNavLink) navLink;
                    Component c = new LinkPanel("link", null, linkText, link.url);
                    c.setRenderBodyOnly(true);
                    item.add(c);
                } else if (entry instanceof DropDownMenuRegistration) {
                } else if (navLink instanceof DropDownPageMenuNavLink) {
                    // drop down menu
                    DropDownMenuRegistration reg = (DropDownMenuRegistration) entry;
                    Component c = new DropDownMenu("link", getString(entry.translationKey), reg);
                    DropDownPageMenuNavLink reg = (DropDownPageMenuNavLink) navLink;
                    Component c = new DropDownMenu("link", linkText, 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);
                } else if (navLink instanceof DropDownMenuNavLink) {
                    // drop down menu
                    DropDownMenuNavLink reg = (DropDownMenuNavLink) navLink;
                    Component c = new DropDownMenu("link", linkText, reg);
                    c.setRenderBodyOnly(true);
                    if (entry.pageClass.equals(pageClass)) {
                    item.add(c);
                    WicketUtils.setCssClass(item, "dropdown");
                } else if (navLink instanceof PageNavLink) {
                    PageNavLink reg = (PageNavLink) navLink;
                    // standard page link
                    Component c = new LinkPanel("link", null, linkText,
                            reg.pageClass, reg.params);
                    c.setRenderBodyOnly(true);
                    if (reg.pageClass.equals(pageClass)) {
                        WicketUtils.setCssClass(item, "active");
                    }
                    item.add(c);
                }
            }
        };
        add(refsView);
        add(linksView);
    }
}
src/main/java/com/gitblit/wicket/panels/ProjectRepositoryPanel.html
@@ -5,15 +5,6 @@
      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>
src/main/java/com/gitblit/wicket/panels/ProjectRepositoryPanel.java
@@ -15,7 +15,6 @@
 */
package com.gitblit.wicket.panels;
import java.text.MessageFormat;
import java.util.Map;
import org.apache.wicket.Component;
@@ -24,7 +23,6 @@
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;
@@ -129,38 +127,10 @@
            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);
        if (user.canAdmin(entry)) {
            repositoryLinks = new Fragment("repositoryLinks", "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 (app().repositories().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);
        }
src/main/java/com/gitblit/wicket/panels/RegistrantPermissionsPanel.html
@@ -16,7 +16,7 @@
    </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:10px;" 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>
src/main/java/com/gitblit/wicket/panels/RepositoriesPanel.html
@@ -42,14 +42,6 @@
        </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">
@@ -60,8 +52,7 @@
            <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>
            <th class="right hidden-phone"></th>
        </tr>
    </wicket:fragment>
    
@@ -75,8 +66,7 @@
            <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>
            <th class="right hidden-phone"></th>
        </tr>
    </wicket:fragment>
    
@@ -91,15 +81,7 @@
        <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="mirrorIcon" /><img class="inlineIcon" wicket:id="forkIcon" /><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>
        <td class="rightAlign hidden-phone" style="text-align: right;padding-right:15px;"><span style="font-size:0.8em;" wicket:id="repositorySize">[repository size]</span></td>
    </wicket:fragment>
    
</wicket:panel>
src/main/java/com/gitblit/wicket/panels/RepositoriesPanel.java
@@ -15,7 +15,6 @@
 */
package com.gitblit.wicket.panels;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
@@ -32,7 +31,6 @@
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;
@@ -47,15 +45,12 @@
import com.gitblit.models.ProjectModel;
import com.gitblit.models.RepositoryModel;
import com.gitblit.models.UserModel;
import com.gitblit.servlet.SyndicationServlet;
import com.gitblit.utils.ArrayUtils;
import com.gitblit.utils.ModelUtils;
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;
@@ -91,12 +86,12 @@
                    setResponsePage(RepositoriesPage.class);
                }
            }.setVisible(app().settings().getBoolean(Keys.git.cacheRepositoryList, true)));
            managementLinks.add(new BookmarkablePageLink<Void>("newRepository", EditRepositoryPage.class));
            managementLinks.add(new BookmarkablePageLink<Void>("newRepository", app().getNewRepositoryPage()));
            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));
            managementLinks.add(new BookmarkablePageLink<Void>("newRepository", app().getNewRepositoryPage()));
            add(managementLinks);
        } else {
            // user has no management permissions
@@ -141,12 +136,11 @@
                Collections.sort(subModels);
                groupedModels.addAll(subModels);
            }
            dp = new RepositoriesProvider(groupedModels);
            dp = new ListDataProvider<RepositoryModel>(groupedModels);
        } else {
            dp = new SortableRepositoriesProvider(models);
        }
        final String baseUrl = WicketUtils.getGitblitURL(getRequest());
        final boolean showSwatch = app().settings().getBoolean(Keys.web.repositoryListSwatches, true);
        DataView<RepositoryModel> dataView = new DataView<RepositoryModel>("row", dp) {
@@ -208,15 +202,7 @@
                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;
                    }
                    Class<? extends BasePage> linkPage = SummaryPage.class;
                    PageParameters pp = WicketUtils.newRepositoryParameter(entry.name);
                    row.add(new LinkPanel("repositoryName", "list", repoName, linkPage, pp));
                    row.add(new LinkPanel("repositoryDescription", "list", entry.description,
@@ -320,48 +306,6 @@
                    WicketUtils.setHtmlTooltip(lastChangeLabel, getString("gb.author") + ": " + entry.lastChangeAuthor);
                }
                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 (app().repositories().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++;
            }
@@ -418,59 +362,6 @@
        };
    }
    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;
@@ -480,10 +371,6 @@
        protected SortableRepositoriesProvider(List<RepositoryModel> list) {
            this.list = list;
            setSort(SortBy.date.name(), false);
        }
        public void remove(RepositoryModel model) {
            list.remove(model);
        }
        @Override
src/main/java/com/gitblit/wicket/panels/RepositoryNamePanel.html
New file
@@ -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">
<body>
<wicket:panel>
    <table class="plain">
        <tbody class="settings">
            <tr>
                <th><wicket:message key="gb.project"></wicket:message></th>
                <th><wicket:message key="gb.name"></wicket:message></th>
            </tr>
            <tr>
                <td><select class="span2" wicket:id="projectPath" /></td>
                <td class="edit"><input class="span4" type="text" wicket:id="name" id="name" /> &nbsp;<span class="help-inline"><wicket:message key="gb.nameDescription"></wicket:message></span></td>
            </tr>
        </tbody>
    </table>
    <div>
        <b><wicket:message key="gb.description"></wicket:message></b><br/>
        <input class="span5" type="text" wicket:id="description" />
    </div>
</wicket:panel>
</body>
</html>
src/main/java/com/gitblit/wicket/panels/RepositoryNamePanel.java
New file
@@ -0,0 +1,172 @@
/*
 * Copyright 2014 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.Set;
import java.util.TreeSet;
import org.apache.wicket.markup.html.form.DropDownChoice;
import org.apache.wicket.markup.html.form.TextField;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.Model;
import com.gitblit.models.ProjectModel;
import com.gitblit.models.RepositoryModel;
import com.gitblit.models.UserModel;
import com.gitblit.utils.StringUtils;
import com.gitblit.wicket.GitBlitWebSession;
/**
 * A panel for naming a repository, specifying it's project, and entering a description.
 *
 * @author James Moger
 *
 */
public class RepositoryNamePanel extends BasePanel {
    private static final long serialVersionUID = 1L;
    private String fullName;
    private final IModel<String> projectPath;
    private DropDownChoice<String> pathChoice;
    private final IModel<String> repoName;
    private TextField<String> nameField;
    public RepositoryNamePanel(String wicketId, RepositoryModel repository) {
        super(wicketId);
        GitBlitWebSession session = GitBlitWebSession.get();
        UserModel user = session.getUser();
        // build project set for repository destination
        String defaultPath = null;
        String defaultName = null;
        Set<String> pathNames = new TreeSet<String>();
        // add the registered/known projects
        for (ProjectModel project : app().projects().getProjectModels(user, false)) {
            // TODO issue-351: user.canAdmin(project)
            if (user.canAdmin()) {
                if (project.isRoot) {
                    pathNames.add("/");
                } else {
                    pathNames.add(project.name + "/");
                }
            }
        }
        // add the user's personal project namespace
        if (user.canAdmin() || user.canCreate()) {
            pathNames.add(user.getPersonalPath() + "/");
        }
        if (!StringUtils.isEmpty(repository.name)) {
            // editing a repository name
            // set the defaultProject to the current repository project
            if (StringUtils.isEmpty(repository.projectPath)) {
                defaultPath = "/";
                defaultName = repository.name;
            } else {
                defaultPath = repository.projectPath + "/";
                defaultName = repository.name.substring(defaultPath.length());
            }
            pathNames.add(defaultPath);
        }
        // if default project is not already set, set preference based on the user permissions
        if (defaultPath == null) {
            if (user.canAdmin()) {
                defaultPath = "/";
            } else if (user.canCreate()) {
                defaultPath = user.getPersonalPath() + "/";
            }
        }
        projectPath = Model.of(defaultPath);
        pathChoice = new DropDownChoice<String>("projectPath", projectPath, new ArrayList<String>(pathNames));
        repoName = Model.of(defaultName);
        nameField = new TextField<String>("name", repoName);
        // only enable project selection if we actually have multiple choices
        add(pathChoice.setEnabled(pathNames.size() > 1));
        add(nameField);
        add(new TextField<String>("description"));
    }
    public void setEditable(boolean editable) {
        // only enable project selection if we actually have multiple choices
        pathChoice.setEnabled(pathChoice.getChoices().size() > 1 && editable);
        nameField.setEnabled(editable);
    }
    public boolean updateModel(RepositoryModel repositoryModel) {
        // confirm a project path was selected
        if (StringUtils.isEmpty(projectPath.getObject())) {
            error(getString("gb.pleaseSelectProject"));
            return false;
        }
        // confirm a repository name was entered
        if (StringUtils.isEmpty(repoName.getObject())) {
            error(getString("gb.pleaseSetRepositoryName"));
            return false;
        }
        String project = projectPath.getObject();
        String name = repoName.getObject();
        fullName = (project + name).trim();
        fullName = fullName.replace('\\', '/');
        fullName = fullName.replace("//", "/");
        if (fullName.charAt(0) == '/') {
            fullName = fullName.substring(1);
        }
        if (fullName.endsWith("/")) {
            fullName = fullName.substring(0, fullName.length() - 1);
        }
        if (fullName.contains("../")) {
            error(getString("gb.illegalRelativeSlash"));
            return false;
        }
        if (fullName.contains("/../")) {
            error(getString("gb.illegalRelativeSlash"));
            return false;
        }
        // confirm valid characters in repository name
        Character c = StringUtils.findInvalidCharacter(fullName);
        if (c != null) {
            error(MessageFormat.format(getString("gb.illegalCharacterRepositoryName"), c));
            return false;
        }
        repositoryModel.name = fullName;
        return true;
    }
    @Override
    protected boolean getStatelessHint() {
        return false;
    }
}
src/main/java/com/gitblit/wicket/panels/SshKeysPanel.html
New file
@@ -0,0 +1,46 @@
<!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>
    <h4><wicket:message key="gb.sshKeys"></wicket:message></h4>
    <p><wicket:message key="gb.sshKeysDescription"></wicket:message></p>
    <hr />
    <div wicket:id="keys">
        <div style="display:inline-block;font-size:2em;padding:10px;">
            <i class="fa fa-key"></i>
        </div>
        <div style="display:inline-block;">
            <div wicket:id="comment" style="font-weight:bold;"></div>
            <pre wicket:id="fingerprint"></pre>
        </div>
        <div style="display:inline-block;padding: 0px 20px">
            <div wicket:id="permission" style="font-weight:bold;"></div>
            <div wicket:id="algorithm"></div>
        </div>
        <div style="display:inline-block;vertical-align:text-bottom;">
            <button class="btn btn-danger" wicket:id="delete"><wicket:message key="gb.delete"></wicket:message></button>
        </div>
        <hr />
    </div>
    <div class="well">
        <form wicket:id="addKeyForm">
            <h4><wicket:message key="gb.addSshKey"></wicket:message></h4>
            <div wicket:id="addKeyData"></div>
            <div wicket:id="addKeyPermission"></div>
            <div wicket:id="addKeyComment"></div>
            <div class="form-actions"><input class="btn btn-appmenu" type="submit" value="Add" wicket:message="value:gb.add" wicket:id="addKeyButton" /></div>
        </form>
    </div>
</wicket:panel>
</body>
</html>
src/main/java/com/gitblit/wicket/panels/SshKeysPanel.java
New file
@@ -0,0 +1,169 @@
/*
 * Copyright 2014 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.Arrays;
import java.util.List;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.markup.html.AjaxLink;
import org.apache.wicket.ajax.markup.html.form.AjaxButton;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.form.Form;
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.IModel;
import org.apache.wicket.model.Model;
import com.gitblit.Constants.AccessPermission;
import com.gitblit.models.UserModel;
import com.gitblit.transport.ssh.SshKey;
import com.gitblit.utils.StringUtils;
import com.gitblit.wicket.GitBlitWebSession;
/**
 * A panel that enumerates and manages SSH public keys using AJAX.
 *
 * @author James Moger
 *
 */
public class SshKeysPanel extends BasePanel {
    private static final long serialVersionUID = 1L;
    private final UserModel user;
    public SshKeysPanel(String wicketId, UserModel user) {
        super(wicketId);
        this.user = user;
    }
    @Override
    protected void onInitialize() {
        super.onInitialize();
        setOutputMarkupId(true);
        final List<SshKey> keys = new ArrayList<SshKey>(app().keys().getKeys(user.username));
        final ListDataProvider<SshKey> dp = new ListDataProvider<SshKey>(keys);
        final DataView<SshKey> keysView = new DataView<SshKey>("keys", dp) {
            private static final long serialVersionUID = 1L;
            @Override
            public void populateItem(final Item<SshKey> item) {
                final SshKey key = item.getModelObject();
                item.add(new Label("comment", key.getComment()));
                item.add(new Label("fingerprint", key.getFingerprint()));
                item.add(new Label("permission", key.getPermission().toString()));
                item.add(new Label("algorithm", key.getAlgorithm()));
                AjaxLink<Void> delete = new AjaxLink<Void>("delete") {
                    private static final long serialVersionUID = 1L;
                    @Override
                    public void onClick(AjaxRequestTarget target) {
                        if (app().keys().removeKey(user.username, key)) {
                            // reset the keys list
                            keys.clear();
                            keys.addAll(app().keys().getKeys(user.username));
                            // update the panel
                            target.addComponent(SshKeysPanel.this);
                        }
                    }
                };
                item.add(delete);
            }
        };
        add(keysView);
        Form<Void> addKeyForm = new Form<Void>("addKeyForm");
        final IModel<String> keyData = Model.of("");
        addKeyForm.add(new TextAreaOption("addKeyData",
                getString("gb.key"),
                null,
                "span5",
                keyData));
        final IModel<AccessPermission> keyPermission = Model.of(AccessPermission.PUSH);
        addKeyForm.add(new ChoiceOption<AccessPermission>("addKeyPermission",
                getString("gb.permission"),
                getString("gb.sshKeyPermissionDescription"),
                keyPermission,
                Arrays.asList(AccessPermission.SSHPERMISSIONS)));
        final IModel<String> keyComment = Model.of("");
        addKeyForm.add(new TextOption("addKeyComment",
                getString("gb.comment"),
                getString("gb.sshKeyCommentDescription"),
                "span5",
                keyComment));
        addKeyForm.add(new AjaxButton("addKeyButton") {
            private static final long serialVersionUID = 1L;
            @Override
            protected void onSubmit(AjaxRequestTarget target, Form<?> form) {
                UserModel user = GitBlitWebSession.get().getUser();
                String data = keyData.getObject();
                if (StringUtils.isEmpty(data)) {
                    // do not submit empty key
                    return;
                }
                SshKey key = new SshKey(data);
                try {
                    key.getPublicKey();
                } catch (Exception e) {
                    // failed to parse the key
                    return;
                }
                AccessPermission permission = keyPermission.getObject();
                key.setPermission(permission);
                String comment  = keyComment.getObject();
                if (!StringUtils.isEmpty(comment)) {
                    key.setComment(comment);
                }
                if (app().keys().addKey(user.username, key)) {
                    // reset add key fields
                    keyData.setObject("");
                    keyPermission.setObject(AccessPermission.PUSH);
                    keyComment.setObject("");
                    // reset the keys list
                    keys.clear();
                    keys.addAll(app().keys().getKeys(user.username));
                    // update the panel
                    target.addComponent(SshKeysPanel.this);
                }
            }
        });
        add(addKeyForm);
    }
}
src/main/java/com/gitblit/wicket/panels/TagsPanel.java
@@ -17,9 +17,11 @@
import java.util.List;
import org.apache.wicket.RequestCycle;
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.Fragment;
import org.apache.wicket.markup.repeater.Item;
import org.apache.wicket.markup.repeater.data.DataView;
@@ -29,13 +31,13 @@
import org.eclipse.jgit.lib.Repository;
import com.gitblit.models.RefModel;
import com.gitblit.servlet.RawServlet;
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.TagPage;
import com.gitblit.wicket.pages.TagsPage;
import com.gitblit.wicket.pages.TreePage;
@@ -113,9 +115,10 @@
                            .newObjectParameter(repositoryName, entry.getReferencedObjectId()
                                    .getName())));
                    fragment.add(new BookmarkablePageLink<Void>("raw", RawPage.class, WicketUtils
                            .newObjectParameter(repositoryName, entry.getReferencedObjectId()
                                    .getName())));
                    String contextUrl = RequestCycle.get().getRequest().getRelativePathPrefixToContextRoot();
                    String rawUrl = RawServlet.asLink(contextUrl, repositoryName, entry.displayName,
                            entry.getReferencedObjectId().getName());
                    fragment.add(new ExternalLink("raw", rawUrl));
                    item.add(fragment);
                } else {
                    // TODO Tree Tag Object
src/main/java/com/gitblit/wicket/panels/TextAreaOption.html
New file
@@ -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:panel>
    <div style="padding-top:4px;">
        <div style="margin-bottom:1px;">
            <b><span wicket:id="name"></span></b>
        </div>
        <label class="checkbox" style="color:#777;"> <span wicket:id="description"></span>
        <p style="padding-top:5px;"><textarea rows="12" class="span5" wicket:id="text"></textarea></p>
        </label>
    </div>
</wicket:panel>
</body>
</html>
src/main/java/com/gitblit/wicket/panels/TextAreaOption.java
New file
@@ -0,0 +1,54 @@
/*
 * Copyright 2014 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.form.TextArea;
import org.apache.wicket.model.IModel;
import com.gitblit.utils.StringUtils;
import com.gitblit.wicket.WicketUtils;
/**
 * A re-usable textarea option panel.
 *
 * title
 *     description
 *     [text
 *           area]
 *
 * @author James Moger
 *
 */
public class TextAreaOption extends BasePanel {
    private static final long serialVersionUID = 1L;
    public TextAreaOption(String wicketId, String title, String description, IModel<String> model) {
        this(wicketId, title, description, null, model);
    }
    public TextAreaOption(String wicketId, String title, String description, String css, IModel<String> model) {
        super(wicketId);
        add(new Label("name", title));
        add(new Label("description", description).setVisible(!StringUtils.isEmpty(description)));
        TextArea<String> tf = new TextArea<String>("text", model);
        if (!StringUtils.isEmpty(css)) {
            WicketUtils.setCssClass(tf, css);
        }
        add(tf);
    }
}
src/main/java/com/gitblit/wicket/panels/TextOption.html
New file
@@ -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:panel>
    <div style="padding-top:4px;">
        <div style="margin-bottom:1px;">
            <b><span wicket:id="name"></span></b>
        </div>
        <label class="checkbox" style="color:#777;"> <span wicket:id="description"></span>
        <p style="padding-top:5px;"><input class="span3" type="text" wicket:id="text" /></p>
        </label>
    </div>
</wicket:panel>
</body>
</html>
src/main/java/com/gitblit/wicket/panels/TextOption.java
New file
@@ -0,0 +1,53 @@
/*
 * Copyright 2014 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.form.TextField;
import org.apache.wicket.model.IModel;
import com.gitblit.utils.StringUtils;
import com.gitblit.wicket.WicketUtils;
/**
 * A re-usable textfield option panel.
 *
 * title
 *     description
 *     [textfield]
 *
 * @author James Moger
 *
 */
public class TextOption extends BasePanel {
    private static final long serialVersionUID = 1L;
    public TextOption(String wicketId, String title, String description, IModel<String> model) {
        this(wicketId, title, description, null, model);
    }
    public TextOption(String wicketId, String title, String description, String css, IModel<String> model) {
        super(wicketId);
        add(new Label("name", title));
        add(new Label("description", description).setVisible(!StringUtils.isEmpty(description)));
        TextField<String> tf = new TextField<String>("text", model);
        if (!StringUtils.isEmpty(css)) {
            WicketUtils.setCssClass(tf, css);
        }
        add(tf);
    }
}
src/main/java/com/gitblit/wicket/panels/TicketListPanel.html
New file
@@ -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:panel>
<table class="table tickets">
    <tbody>
        <tr wicket:id="row">
               <td class="ticket-list-icon">
                   <i wicket:id="state"></i>
               </td>
            <td>
                <span wicket:id="title">[title]</span> <span wicket:id="labels" style="font-weight: normal;color:white;"><span class="label" wicket:id="label"></span></span>
                <div class="ticket-list-details">
                    <span style="padding-right: 10px;" class="hidden-phone">
                        <wicket:message key="gb.createdBy"></wicket:message>
                        <span style="padding: 0px 2px" wicket:id="createdBy">[createdBy]</span> <span class="date" wicket:id="createDate">[create date]</span>
                    </span>
                    <span wicket:id="indicators" style="white-space:nowrap;"><i wicket:id="icon"></i> <span style="padding-right:10px;" wicket:id="count"></span></span>
                </div>
                <div class="hidden-phone" wicket:id="updated"></div>
                <div class="ticket-list-details"><span class="activitySwatch" wicket:id="ticketsLink">[tickets link]</span></div>
            </td>
            <td class="ticket-list-state">
                   <span class="badge badge-info" wicket:id="votes"></span>
            </td>
            <td class="hidden-phone ticket-list-state">
                   <i wicket:message="title:gb.watching" style="color:#888;" class="fa fa-eye" wicket:id="watching"></i>
            </td>
            <td class="ticket-list-state">
                   <div wicket:id="status"></div>
            </td>
            <td class="indicators">
                <div>
                        <b>#<span wicket:id="id">[id]</span></b>
                    </div>
                <div wicket:id="responsible"></div>
            </td>
           </tr>
    </tbody>
</table>
<wicket:fragment wicket:id="updatedFragment">
    <div class="ticket-list-details">
        <wicket:message key="gb.updatedBy"></wicket:message>
        <span style="padding: 0px 2px" wicket:id="updatedBy">[updatedBy]</span> <span class="date" wicket:id="updateDate">[update date]</span>
    </div>
</wicket:fragment>
</wicket:panel>
</body>
</html>
src/main/java/com/gitblit/wicket/panels/TicketListPanel.java
New file
@@ -0,0 +1,242 @@
/*
 * Copyright 2014 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 org.apache.wicket.PageParameters;
import org.apache.wicket.behavior.SimpleAttributeModifier;
import org.apache.wicket.markup.html.basic.Label;
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 com.gitblit.Constants;
import com.gitblit.models.RepositoryModel;
import com.gitblit.models.UserModel;
import com.gitblit.tickets.QueryResult;
import com.gitblit.tickets.TicketLabel;
import com.gitblit.utils.ArrayUtils;
import com.gitblit.utils.BugtraqProcessor;
import com.gitblit.utils.StringUtils;
import com.gitblit.wicket.GitBlitWebSession;
import com.gitblit.wicket.TicketsUI;
import com.gitblit.wicket.TicketsUI.Indicator;
import com.gitblit.wicket.WicketUtils;
import com.gitblit.wicket.pages.TicketsPage;
import com.gitblit.wicket.pages.UserPage;
/**
 *
 * The ticket list panel lists tickets in a table.
 *
 * @author James Moger
 *
 */
public class TicketListPanel extends BasePanel {
    private static final long serialVersionUID = 1L;
    public TicketListPanel(String wicketId, List<QueryResult> list, final boolean showSwatch, final boolean showRepository) {
        super(wicketId);
        final ListDataProvider<QueryResult> dp = new ListDataProvider<QueryResult>(list);
        DataView<QueryResult> dataView = new DataView<QueryResult>("row", dp) {
            private static final long serialVersionUID = 1L;
            @Override
            protected void populateItem(Item<QueryResult> item) {
                final QueryResult ticket = item.getModelObject();
                final RepositoryModel repository = app().repositories().getRepositoryModel(ticket.repository);
                if (showSwatch) {
                    // set repository color
                    String color = StringUtils.getColor(StringUtils.stripDotGit(repository.name));
                    WicketUtils.setCssStyle(item, MessageFormat.format("border-left: 2px solid {0};", color));
                }
                PageParameters tp = WicketUtils.newObjectParameter(ticket.repository, "" + ticket.number);
                if (showRepository) {
                    String name = StringUtils.stripDotGit(ticket.repository);
                    PageParameters rp =  WicketUtils.newOpenTicketsParameter(ticket.repository);
                    LinkPanel link = new LinkPanel("ticketsLink", null, name, TicketsPage.class, rp);
                    WicketUtils.setCssBackground(link, name);
                    item.add(link);
                } else {
                    item.add(new Label("ticketsLink").setVisible(false));
                }
                item.add(TicketsUI.getStateIcon("state", ticket.type, ticket.status));
                item.add(new Label("id", "" + ticket.number));
                UserModel creator = app().users().getUserModel(ticket.createdBy);
                if (creator != null) {
                    item.add(new LinkPanel("createdBy", null, creator.getDisplayName(),
                            UserPage.class, WicketUtils.newUsernameParameter(ticket.createdBy)));
                } else {
                    item.add(new Label("createdBy", ticket.createdBy));
                }
                item.add(WicketUtils.createDateLabel("createDate", ticket.createdAt, GitBlitWebSession
                        .get().getTimezone(), getTimeUtils(), false));
                if (ticket.updatedAt == null) {
                    item.add(new Label("updated").setVisible(false));
                } else {
                    Fragment updated = new Fragment("updated", "updatedFragment", this);
                    UserModel updater = app().users().getUserModel(ticket.updatedBy);
                    if (updater != null) {
                        updated.add(new LinkPanel("updatedBy", null, updater.getDisplayName(),
                                UserPage.class, WicketUtils.newUsernameParameter(ticket.updatedBy)));
                    } else {
                        updated.add(new Label("updatedBy", ticket.updatedBy));
                    }
                    updated.add(WicketUtils.createDateLabel("updateDate", ticket.updatedAt, GitBlitWebSession
                            .get().getTimezone(), getTimeUtils(), false));
                    item.add(updated);
                }
                item.add(new LinkPanel("title", "list subject", StringUtils.trimString(
                        ticket.title, Constants.LEN_SHORTLOG), TicketsPage.class, tp));
                ListDataProvider<String> labelsProvider = new ListDataProvider<String>(ticket.getLabels());
                DataView<String> labelsView = new DataView<String>("labels", labelsProvider) {
                    private static final long serialVersionUID = 1L;
                    @Override
                    public void populateItem(final Item<String> labelItem) {
                        BugtraqProcessor btp  = new BugtraqProcessor(app().settings());
                        Repository db = app().repositories().getRepository(repository.name);
                        String content = btp.processText(db, repository.name, labelItem.getModelObject());
                        db.close();
                        Label label = new Label("label", content);
                        label.setEscapeModelStrings(false);
                        TicketLabel tLabel = app().tickets().getLabel(repository, labelItem.getModelObject());
                        String background = MessageFormat.format("background-color:{0};", tLabel.color);
                        label.add(new SimpleAttributeModifier("style", background));
                        labelItem.add(label);
                    }
                };
                item.add(labelsView);
                if (StringUtils.isEmpty(ticket.responsible)) {
                    item.add(new Label("responsible").setVisible(false));
                } else {
                    UserModel responsible = app().users().getUserModel(ticket.responsible);
                    if (responsible == null) {
                        responsible = new UserModel(ticket.responsible);
                    }
                    GravatarImage avatar = new GravatarImage("responsible", responsible.getDisplayName(),
                            responsible.emailAddress, null, 16, true);
                    avatar.setTooltip(getString("gb.responsible") + ": " + responsible.getDisplayName());
                    item.add(avatar);
                }
                // votes indicator
                Label v = new Label("votes", "" + ticket.votesCount);
                WicketUtils.setHtmlTooltip(v, getString("gb.votes"));
                item.add(v.setVisible(ticket.votesCount > 0));
                // watching indicator
                item.add(new Label("watching").setVisible(ticket.isWatching(GitBlitWebSession.get().getUsername())));
                // status indicator
                String css = TicketsUI.getLozengeClass(ticket.status, true);
                Label l = new Label("status", ticket.status.toString());
                WicketUtils.setCssClass(l, css);
                item.add(l);
                // add the ticket indicators/icons
                List<Indicator> indicators = new ArrayList<Indicator>();
                // comments
                if (ticket.commentsCount > 0) {
                    int count = ticket.commentsCount;
                    String pattern = getString("gb.nComments");
                    if (count == 1) {
                        pattern = getString("gb.oneComment");
                    }
                    indicators.add(new Indicator("fa fa-comment", count, pattern));
                }
                // participants
                if (!ArrayUtils.isEmpty(ticket.participants)) {
                    int count = ticket.participants.size();
                    if (count > 1) {
                        String pattern = getString("gb.nParticipants");
                        indicators.add(new Indicator("fa fa-user", count, pattern));
                    }
                }
                // attachments
                if (!ArrayUtils.isEmpty(ticket.attachments)) {
                    int count = ticket.attachments.size();
                    String pattern = getString("gb.nAttachments");
                    if (count == 1) {
                        pattern = getString("gb.oneAttachment");
                    }
                    indicators.add(new Indicator("fa fa-file", count, pattern));
                }
                // patchset revisions
                if (ticket.patchset != null) {
                    int count = ticket.patchset.commits;
                    String pattern = getString("gb.nCommits");
                    if (count == 1) {
                        pattern = getString("gb.oneCommit");
                    }
                    indicators.add(new Indicator("fa fa-code", count, pattern));
                }
                // milestone
                if (!StringUtils.isEmpty(ticket.milestone)) {
                    indicators.add(new Indicator("fa fa-bullseye", ticket.milestone));
                }
                ListDataProvider<Indicator> indicatorsDp = new ListDataProvider<Indicator>(indicators);
                DataView<Indicator> indicatorsView = new DataView<Indicator>("indicators", indicatorsDp) {
                    private static final long serialVersionUID = 1L;
                    @Override
                    public void populateItem(final Item<Indicator> item) {
                        Indicator indicator = item.getModelObject();
                        String tooltip = indicator.getTooltip();
                        Label icon = new Label("icon");
                        WicketUtils.setCssClass(icon, indicator.css);
                        item.add(icon);
                        if (indicator.count > 0) {
                            Label count = new Label("count", "" + indicator.count);
                            item.add(count.setVisible(!StringUtils.isEmpty(tooltip)));
                        } else {
                            item.add(new Label("count").setVisible(false));
                        }
                        WicketUtils.setHtmlTooltip(item, tooltip);
                    }
                };
                item.add(indicatorsView);
            }
        };
        add(dataView);
    }
}
src/main/java/com/gitblit/wicket/panels/TicketSearchForm.java
New file
@@ -0,0 +1,78 @@
/*
 * Copyright 2014 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.text.MessageFormat;
import org.apache.wicket.PageParameters;
import org.apache.wicket.markup.html.form.TextField;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.request.target.basic.RedirectRequestTarget;
import com.gitblit.utils.StringUtils;
import com.gitblit.wicket.SessionlessForm;
import com.gitblit.wicket.WicketUtils;
import com.gitblit.wicket.pages.BasePage;
public class TicketSearchForm extends SessionlessForm<Void> implements Serializable {
    private static final long serialVersionUID = 1L;
    private final String repositoryName;
    private final IModel<String> searchBoxModel;
    public TicketSearchForm(String id, String repositoryName, String text,
            Class<? extends BasePage> pageClass, PageParameters params) {
        super(id, pageClass, params);
        this.repositoryName = repositoryName;
        this.searchBoxModel = new Model<String>(text == null ? "" : text);
        TextField<String> searchBox = new TextField<String>("ticketSearchBox", searchBoxModel);
        add(searchBox);
    }
    @Override
    protected
    void onInitialize() {
        super.onInitialize();
        WicketUtils.setHtmlTooltip(get("ticketSearchBox"),
                MessageFormat.format(getString("gb.searchTicketsTooltip"), ""));
        WicketUtils.setInputPlaceholder(get("ticketSearchBox"), getString("gb.searchTickets"));
    }
    @Override
    public void onSubmit() {
        String searchString = searchBoxModel.getObject();
        if (StringUtils.isEmpty(searchString)) {
            // redirect to self to avoid wicket page update bug
            String absoluteUrl = getAbsoluteUrl();
            getRequestCycle().setRequestTarget(new RedirectRequestTarget(absoluteUrl));
            return;
        }
        // use an absolute url to workaround Wicket-Tomcat problems with
        // mounted url parameters (issue-111)
        PageParameters params = WicketUtils.newRepositoryParameter(repositoryName);
        params.add("s", searchString);
        String absoluteUrl = getAbsoluteUrl(pageClass, params);
        getRequestCycle().setRequestTarget(new RedirectRequestTarget(absoluteUrl));
    }
}
src/main/java/com/gitblit/wicket/panels/UserTitlePanel.html
New file
@@ -0,0 +1,16 @@
<!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 style="display:inline-block;vertical-align:top;padding: 0px 2px 2px;"><img wicket:id="userGravatar"></img></div>
    <div style="display:inline-block;">
    <div style="font-size:1.5em;" wicket:id="userDisplayName"></div>
    <div style="color:#888;font-size:1.2em;padding-top:4px;"><span wicket:id="userTitle"></span></div>
</div>
</wicket:panel>
</body>
</html>
src/main/java/com/gitblit/wicket/panels/UserTitlePanel.java
New file
@@ -0,0 +1,32 @@
/*
 * Copyright 2014 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 com.gitblit.models.UserModel;
public class UserTitlePanel extends BasePanel {
    private static final long serialVersionUID = 1L;
    public UserTitlePanel(String wicketId, UserModel user, String title) {
        super(wicketId);
        add(new GravatarImage("userGravatar", user, "gravatar", 36, false));
        add(new Label("userDisplayName", user.getDisplayName()));
        add(new Label("userTitle", title));
    }
}
src/main/java/login_de.mkd
New file
@@ -0,0 +1,3 @@
## Bitte melden Sie sich an
Bitte geben Sie Ihre Zugangsdaten ein um auf Gitblit zuzugreifen.
src/main/java/login_it.mkd
New file
@@ -0,0 +1,4 @@
## Login richiesto
Per favore fornisci le tue credenziali per accedere a questo sito Gitblit.
src/main/java/pt.py
@@ -54,9 +54,9 @@
        # fetch all current ticket patchsets
        print("Fetching ticket patchsets from the '{}' repository".format(args.remote))
        if args.quiet:
            __call(['git', 'fetch', args.remote, '--quiet'])
            __call(['git', 'fetch', '-p', args.remote, '--quiet'])
        else:
            __call(['git', 'fetch', args.remote])
            __call(['git', 'fetch', '-p', args.remote])
    else:
        # fetch specific patchset
        __resolve_patchset(args)
@@ -206,6 +206,12 @@
            branches.append(branch.strip())
    branch = 'topic/' + args.topic
    try:
        int(args.topic)
        branch = 'ticket/' + args.topic
    except ValueError:
        pass
    illegals = set(branches) & {'topic', branch}
    # ensure there are no local branch names that will interfere with branch creation
@@ -241,6 +247,13 @@
                curr_branch = branch[1:].strip()
                if curr_branch.startswith('topic/'):
                    topic = curr_branch[6:].strip()
                    try:
                        int(topic)
                        push_ref = topic
                    except ValueError:
                        pass
                if curr_branch.startswith('ticket/'):
                    topic = curr_branch[7:].strip()
                    try:
                        int(topic)
                        push_ref = topic
@@ -288,8 +301,8 @@
        if fields[0] == 'remote' and fields[1].strip().startswith('--> #'):
            # set the upstream branch configuration
            args.id = int(fields[1].strip()[len('--> #'):])
            __call(['git', 'fetch', args.remote])
            __call(['git', 'branch', '--set-upstream-to={}/ticket/{:d}'.format(args.remote, args.id)])
            __call(['git', 'fetch', '-p', args.remote])
            __call(['git', 'branch', '-u', '{}/ticket/{:d}'.format(args.remote, args.id)])
            break
    return
src/main/java/welcome_de.mkd
New file
@@ -0,0 +1,3 @@
## Willkommen bei Gitblit
Eine schnelle und einfache Art und Weise Ihre eigenen [Git](http://www.git-scm.com) Repositories zu hosten.
src/main/java/welcome_it.mkd
New file
@@ -0,0 +1,3 @@
## Benvenuto su Gitblit
Un modo facile e veloce per ospitare o visualizzare i tuoi repository [Git](http://www.git-scm.com).
src/site/features.mkd
@@ -60,15 +60,18 @@
- Customizable regular expression substitution for commit messages (i.e. bug or code review link integration)
- Single text file for users configuration
- Translations
    - English
    - Japanese
    - Spanish
    - Polish
    - Korean
    - Brazilian Portuguese
    - Dutch
    - German (de)
    - English (en)
    - Spanish (es)
    - French (fr)
    - Italian (it)
    - Japanese (ja)
    - Korean (ko)
    - Dutch (nl)
    - Norwegian (no)
    - Polish (pl)
    - Brazilian Portuguese (pt_BR)
    - Simplified Chinese (zh_CN)
    - French
## Gitblit GO Features
- Out-of-the-box integrated stack requiring minimal configuration
src/site/openshift.mkd
File was deleted
src/site/plugins_extensions.mkd
@@ -52,6 +52,37 @@
    public void onUninstall() {
    }
}
/**
 * You can also create Webapp plugins that register pages.
 */
public class ExampleWicketPlugin extends GitblitWicketPlugin {
    @Override
    public void start() {
    }
    @Override
    public void stop() {
    }
    @Override
    public void onInstall() {
    }
    @Override
    public void onUpgrade(Version oldVersion) {
    }
    @Override
    public void onUninstall() {
    }
    @Override
    protected void init(GitblitWicketApp app) {
        app.mount("/logo", LogoPage.class);
        app.mount("/hello", HelloWorldPage.class);
    }
}
```
### SSH Dispatch Command
@@ -65,7 +96,6 @@
import org.kohsuke.args4j.Option;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.gitblit.models.UserModel;
import com.gitblit.transport.ssh.commands.CommandMetaData;
import com.gitblit.transport.ssh.commands.DispatchCommand;
import com.gitblit.transport.ssh.commands.UsageExample;
@@ -185,3 +215,172 @@
}
```
### Request Filter
*SINCE 1.6.0*
You can provide your own custom request filter by subclassing the *HttpRequestFilter* class.
```java
import com.gitblit.extensions.HttpRequestFilter;
import ro.fortsoft.pf4j.Extension;
@Extension
public class MyRequestFilter extends HttpRequestFilter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException {
    }
}
```
### User Menu Items
*SINCE 1.6.0*
You can provide your own user menu items by subclassing the *UserMenuExtension* class.
```java
import java.util.Arrays;
import java.util.List;
import ro.fortsoft.pf4j.Extension;
import com.gitblit.extensions.UserMenuExtension;
import com.gitblit.models.Menu.ExternalLinkMenuItem;
import com.gitblit.models.Menu.MenuItem;
import com.gitblit.models.UserModel;
@Extension
public class MyUserMenuContributor extends UserMenuExtension {
    @Override
    public List<MenuItem> getMenuItems(UserModel user) {
        MenuItem item = new ExternalLinkMenuItem("Github", String.format("https://github.com/%s", user.username));
        return Arrays.asList(item);
    }
}
```
### Navigation Links
*SINCE 1.6.0*
You can provide your own top-level navigation links by subclassing the *NavLinkExtension* class.
```java
import java.util.Arrays;
import java.util.List;
import ro.fortsoft.pf4j.Extension;
import com.gitblit.extensions.NavLinkExtension;
import com.gitblit.models.UserModel;
@Extension
public class MyNavLink extends NavLinkExtension {
    @Override
    public List<NavLink> getNavLinks(UserModel user) {
        NavLink link = new ExternalLinkMenuItem("Github", String.format("https://github.com/%s", user.username));
        return Arrays.asList(link);
    }
}
```
### Server Lifecycle Listener
*SINCE 1.6.0*
You can provide a lifecycle listener to be notified when Gitblit has completely started and just before Gitblit is gracefully terminated.
```java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ro.fortsoft.pf4j.Extension;
import com.gitblit.extensions.LifeCycleListener;
@Extension
public class MyLifeCycleListener extends LifeCycleListener {
    final Logger log = LoggerFactory.getLogger(getClass());
    @Override
    public void onStartup() {
        log.info("Gitblit is Ready!!");
    }
    @Override
    public void onShutdown() {
        log.info("Gitblit is Going Down!!");
    }
}
```
### Repository Lifecycle Listener
*SINCE 1.6.0*
You can provide a lifecycle listener to be notified when Gitblit has created or deleted a repository.
```java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ro.fortsoft.pf4j.Extension;
import com.gitblit.extensions.RepositoryLifeCycleListener;
import com.gitblit.models.RepositoryModel;
@Extension
public class MyRepoLifeCycleListener extends RepositoryLifeCycleListener {
    final Logger log = LoggerFactory.getLogger(getClass());
    @Override
    public void onCreation(RepositoryModel repo) {
        log.info("Gitblit created {}", repo);
    }
    @Override
    public void onDeletion(RepositoryModel repo) {
        log.info("Gitblit deleted {}", repo);
    }
}
```
### User/Team Lifecycle Listener
*SINCE 1.6.0*
You can provide a lifecycle listener to be notified when Gitblit has created or deleted a user or a team.
```java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ro.fortsoft.pf4j.Extension;
import com.gitblit.extensions.UserTeamLifeCycleListener;
import com.gitblit.models.TeamModel;
import com.gitblit.models.UserModel;
@Extension
public class MyUserTeamLifeCycleListener extends UserTeamLifeCycleListener {
    final Logger log = LoggerFactory.getLogger(getClass());
    @Override
    public void onCreation(UserModel user) {
        log.info("Gitblit created user {}", user);
    }
    @Override
    public void onDeletion(UserModel user) {
        log.info("Gitblit deleted user {}", user);
    }
    @Override
    public void onCreation(TeamModel team) {
        log.info("Gitblit created team {}", team);
    }
    @Override
    public void onDeletion(TeamModel team) {
        log.info("Gitblit deleted team {}", team);
    }
}
```
src/site/plugins_overview.mkd
@@ -45,13 +45,15 @@
Through this command interface plugins can be started, stopped, disabled, enabled, installed, uninstalled, listed, etc.  Each command is supports the `--help` argument which will guide you in understanding the options and usage of the command.
You may watch an Asciinema screencast of how to use the SSH transport and the plugin manager [here](https://asciinema.org/a/9342).
### Default Plugin Registry
Gitblit provides a simple default registry of plugins. The registry is a JSON file and it lists plugin metadata and download locations.
    plugins.registry = http://plugins.gitblit.com/plugins.json
The [registry](http://plugins.gitblit.com/plugins.json) is currently hosted in a [Git repository on Github](https://github.com/gitblit/gitblit-registry).  This git repository is also a [Maven-compatible repository](http://plugins.gitblit.com), which hosts some plugin binaries.
The [default plugins registry](http://plugins.gitblit.com) is currently hosted in a [Git repository on Github](https://github.com/gitblit/gitblit-registry).  You can view the default registry file [here](http://plugins.gitblit.com/plugins.json).  The default plugin registry is also a Maven-2 compatible repository.
### Contributing Plugins to the Default Registry
@@ -62,13 +64,3 @@
The `plugins.json` file is parameterized with the `${self}` placeholder.  This parameter is substituted on download with with the source URL of the registry file.  This allows you to clone and serve your own copy of this git repository or just serve your own `plugins.json` on your own network.
Gitblit also supports loading multiple plugin registries.  Just place another **properly formatted** `.json` file in `${baseFolder}/plugins` and Gitblit will load that as an additional registry.
### Mac OSX Fonts
Gitblit's core SSH commands and those in the *powertools* plugin rely on ANSI border characters to provide a pretty presentation of data.  Unfortunately, the fonts provided by Apple - while very nice - don't work well with ANSI border characters.  The following public domain fixed-width, fixed-point, bitmapped fonts work very nicely.  I find the 6x12 font with a line spacing of ~0.8 to be quite acceptable.
[6x12.dfont](6x12.dfont)
[6x13.dfont](6x13.dfont)
[7x13.dfont](7x13.dfont)
[7x14.dfont](7x14.dfont)
src/site/roadmap.mkd
@@ -3,7 +3,6 @@
This is not exactly a formal roadmap but it is a priority list of what might be implemented in future releases.  
This list is volatile and may not reflect what will be in the next release.
* **In-Progress**: Integrate an SSH daemon (issue-369)
* Diff should highlight inserted/removed fragment compared to original line
* Respect Gerrit branch permissions, if found (issue 36)
* Add support for Project owners/administrators (ticket-75)
* Add Project create/update pages
* Integrate improvements for git-flow (ticket-55)
src/site/rpc.mkd
@@ -32,6 +32,7 @@
<tr><th>url parameter</th><th>default</th><th>description</th></tr>
<tr><td colspan='3'><b>standard query</b></td></tr>
<tr><td><em>repository</em></td><td><em>required</em></td><td>repository name is part of the url (see examples below)</td></tr>
<tr><td>ot=</td><td><em>optional</em><br/>default: COMMIT</td><td>object type to return in results. COMMIT or TAG</td></tr>
<tr><td>h=</td><td><em>optional</em><br/>default: HEAD</td><td>starting branch, ref, or commit id</td></tr>
<tr><td>l=</td><td><em>optional</em><br/>default: web.syndicationEntries</td><td>maximum return count</td></tr>
<tr><td>pg=</td><td><em>optional</em><br/>default: 0</td><td>page number for paging<br/>(offset into history = pagenumber*maximum return count)</td></tr>
@@ -57,9 +58,10 @@
<tr><td>Gitblit v0.8.0</td><td>2</td></tr>
<tr><td>Gitblit v0.9.0 - v1.0.0</td><td>3</td></tr>
<tr><td>Gitblit v1.1.0</td><td>4</td></tr>
<tr><td>Gitblit v1.2.0+</td><td>5</td></tr>
<tr><td>Gitblit v1.3.1+</td><td>6</td></tr>
<tr><td>Gitblit v1.4.0+</td><td>7</td></tr>
<tr><td>Gitblit v1.2.0</td><td>5</td></tr>
<tr><td>Gitblit v1.3.1</td><td>6</td></tr>
<tr><td>Gitblit v1.4.0</td><td>7</td></tr>
<tr><td>Gitblit v1.6.0</td><td>8</td></tr>
</tbody>
</table>
@@ -81,6 +83,7 @@
<tr><td>LIST_BRANCHES</td><td>-</td><td>-</td><td>1</td><td>-</td><td>Map&lt;String, List&lt;String&gt;&gt;</td></tr>
<tr><td>LIST_SETTINGS</td><td>-</td><td><em>-</em></td><td>1</td><td>-</td><td>ServerSettings (basic keys)</td></tr>
<tr><td>GET_USER</td><td>user name</td><td>-</td><td>6</td><td>-</td><td>UserModel</td></tr>
<tr><td>FORK_REPOSITORY</td><td>repository name</td><td><em>-</em></td><td>8</td><td>-</td><td>-</td></tr>
<tr><td colspan='6'><em>web.enableRpcManagement=true</em></td></tr>
<tr><td>CREATE_REPOSITORY</td><td>repository name</td><td><em>admin</em></td><td>1</td><td>RepositoryModel</td><td>-</td></tr>
<tr><td>EDIT_REPOSITORY</td><td>repository name</td><td><em>admin</em></td><td>1</td><td>RepositoryModel</td><td>-</td></tr>
src/site/setup_express.mkd
File was deleted
src/site/setup_transport_ssh.mkd
@@ -5,6 +5,8 @@
The SSH transport is a very exciting improvement to Gitblit.  Aside from offering a simple password-less, public key workflow the SSH transport also allows exposes a new approach to interacting with Gitblit: SSH commands.  The Gerrit and Android projects have to be thanked for providing great base SSH code that Gitblit has integrated.
You may watch an Asciinema screencast of using the SSH transport and it's command infrastructure [here](https://asciinema.org/a/9342).
### Cloning & Pushing
By default, Gitblit serves the SSH transport on port 29418, which is the same as Gerrit.  Why was 29418 chosen?  It's likely because it resembles the IANA port assigned to the git protocol (9418).
@@ -21,14 +23,22 @@
    ssh-keygen
**NOTE:** It is important to note that *ssh-keygen* generates a public/private keypair (e.g. id_rsa and id_rsa.pub).  You want to upload the *public* key, which is denoted by the *.pub* file extension.
#### Uploading your public key from the command-line
Then you can upload your *public* key right from the command-line.
    cat ~/.ssh/id_rsa.pub | ssh -l <username> -p 29418 <hostname> keys add
    cat c:\<userfolder>\.ssh\id_rsa.pub | ssh -l <username> -p 29418 <hostname> keys add
**NOTE:** It is important to note that *ssh-keygen* generates a public/private keypair (e.g. id_rsa and id_rsa.pub).  You want to upload the *public* key, which is denoted by the *.pub* file extension.
#### Uploading your public key through the browser
Once you've done both of those steps you should be able to execute the following command without a password prompt.
1. Navigate to your *profile* page from the dropdown user menu.
2. Click the *SSH Keys* tab and paste your public key into the *Add SSH Key* form.
3. Click the *Save* button
Once you ave uploaded your public key you should be able to execute the following command without a password prompt.
    ssh -l <username> -p 29418 <hostname>
@@ -90,4 +100,12 @@
| R          | SSH key may be used to clone/fetch              |
| RW         | SSH key may be used to clone/fetch and push     |
### Mac OSX Fonts
Many of Gitblit's SSH commands rely on ANSI border characters to provide a pretty presentation of data.  Unfortunately, the fonts provided by Apple - while very nice - don't work well with ANSI border characters.  The following public domain fixed-width, fixed-point, bitmapped fonts work very nicely.  I find the 6x12 font with a line spacing of ~0.8 to be quite acceptable.
[6x12.dfont](6x12.dfont)
[6x13.dfont](6x13.dfont)
[7x13.dfont](7x13.dfont)
[7x14.dfont](7x14.dfont)
src/site/siteindex.mkd
@@ -1,14 +1,23 @@
<div class="well" style="margin-left:5px;float:right;width:275px;padding: 10px 10px;">
<script>
   (function(d, s, id) {
       var js, fjs = d.getElementsByTagName(s)[0];
       if (d.getElementById(id)) return;
       js = d.createElement(s); js.id = id;
       js.async = true;
       js.src = "//go.jelastic.com/widgets.js";
       fjs.parentNode.insertBefore(js, fjs);
   }(document, 'script', 'jelastic-jssdk'));
</script>
<div style="text-align:center">
<b>Current Release ${project.releaseVersion} (${project.releaseDate})</b><br/><a href="releasenotes.html">release notes</a>
<div style="padding:5px;"><a style="width:175px;text-decoration:none;" class="btn btn-success" href="%GCURL%gitblit-${project.releaseVersion}.zip">Download Gitblit GO (Windows)</a></div>
<div style="padding:5px;"><a style="width:175px;text-decoration:none;" class="btn btn-success" href="%GCURL%gitblit-${project.releaseVersion}.tar.gz">Download Gitblit GO (Linux/OSX)</a></div>
<div style="padding:5px;"><a style="width:175px;text-decoration:none;" class="btn btn-danger" href="%GCURL%gitblit-${project.releaseVersion}.war">Download Gitblit WAR</a></div>
<div style="padding:5px;"><a style="width:175px;text-decoration:none;" class="btn btn-info" href="%GCURL%express-${project.releaseVersion}.zip">Download Gitblit Express</a></div>
<div style="padding:5px;"><a style="width:175px;text-decoration:none;" class="btn btn-primary" href="%GCURL%manager-${project.releaseVersion}.zip">Download Gitblit Manager</a></div>
        <a href='https://bintray.com/gitblit/releases/gitblit/view?source=watch' alt='Get automatic notifications about new "stable" versions'><img src='https://www.bintray.com/docs/images/bintray_badge_color.png'></a>
    </div>
<div data-manifest="http://1c57d83a4c5f3a21ec25c050d4c5e37b.app.jelastic.com/xssu/cross/download/RDYYHABkAFJbUVlMMVU7RUtDARgATExFCEBuGS4jdQJKRUsEDwIBQmNTTEBI" data-width="280" data-theme="flat-blue" data-text="Get it hosted now!" data-tx-empty="Type your email and click the button" data-tx-invalid-email="Invalid email, please check the spelling" data-tx-error="An error has occurred, please try again later" data-tx-success="Check your email" class="je-app" ></div>
    <div style="padding-top:5px;">
    <table class="table condensed-table">
        <tbody>
@@ -41,20 +50,25 @@
### WAR: For Your Servlet Container
*Gitblit WAR* is what you should download if you already have a servlet container available that you wish to use.  Jetty 6/7/8 and Tomcat 6/7 are known to work.  Generally, any Servlet 2.5 or Servlet 3.0 container should work.
### Express: For the Cloud
*Gitblit Express* is a prepared distribution for [RedHat's OpenShift][rhcloud] cloud service.
### You decide how to use Gitblit
Gitblit can be used as a dumb repository viewer with no administrative controls or user accounts.  
Gitblit can be used as a complete Git stack for cloning, pushing, and repository access control.  
Gitblit can be used without any other Git tooling (including actual Git) or it can cooperate with your established tools.
### All Transports
The SSH, HTTP, & GIT protocols are supported and ready-to-go out of the box.
### Issue tracking with branch-based pull requests
Gitblit blends elements of GitHub, BitBucket, and Gerrit to provide a streamlined collaboration workflow based on branches within the primary repository.
### Easy Remote Management
Administrators can create and manage all repositories, user accounts, and teams from the *Web UI*.  
Administrators can create and manage all repositories, user accounts, and teams from the *JSON RPC interface* using the [Gitblit Manager](http://code.google.com/p/gitblit/downloads/detail?name=%MANAGER%) or your own custom tooling. 
Administrators can create and manage all repositories, user accounts, and teams from the *command-line* using the [Powertools plugin](https://github.com/gitblit/gitblit-powertools-plugin).
Administrators can create and manage all repositories, user accounts, and teams from the *command-line* using SSH & the [Powertools plugin](https://github.com/gitblit/gitblit-powertools-plugin).
### Integration with Your Infrastructure
@@ -81,4 +95,3 @@
[jgit]: http://eclipse.org/jgit "Eclipse JGit Site"
[git]: http://git-scm.com "Official Git Site"
[rhcloud]: https://openshift.redhat.com/app "RedHat OpenShift"
src/site/tickets_overview.mkd
@@ -114,36 +114,23 @@
Additionally, because the patchset is not linked to a user's personal fork it is possible to allow others to collaborate on development.
## Status
#### Features
The Tickets feature is highly functional but there are several areas which need further refinements.
#### What is working
- My Tickets page
- Ticket creation and editing
- Ticket creation on patchset push
- Ticket creation on patchset push (proposal)
- Branch-based pull-requests
- Comments with Markdown syntax support
- Rich email notifications
- Fast-forward patchset updates and patchset rewrites
- Voting
- Watching
- Mentions
- Partial milestone support
- Milestones
- Querying
- Searching
- Is Mergeable test on view ticket page load
- Server-side merge
- Close-on-push of detected merge
- Multiple backend choices
- Server-side merge (testing)
#### TODO
- Implement a My Tickets page (ticket-15)
- Ticket Lifecycle Hooks (ticket-16)
- CRUD pages for Milestones (ticket-17)
- Ticket service migration tool (ticket-19)
- Collapsible ticket description (e.g. AUI expander)
- Edit a comment
- Delete a comment
- REST API for tooling
- Client-side Markdown previews (AngularMarkdown?)
src/site/tickets_replication.mkd
@@ -133,3 +133,27 @@
    curl --insecure --user admin:admin "https://localhost:8443/rpc?req=reindex_tickets"
    curl --insecure --user admin:admin "https://localhost:8443/rpc?req=reindex_tickets&name=gitblit.git"
#### Migrating Tickets between Ticket Services
##### Gitblit GO
Gitblit GO ships with a script that executes the *com.gitblit.MigrateTickets* tool included in the Gitblit jar file.  This tool will migrate *all* tickets in *all* repositories **AND** must be run when Gitblit is offline.
    migrate-tickets <outputservice> <baseFolder>
For example, this would migrate tickets from the current ticket service configured in `c:\gitblit-data\gitblit.properties` to a Redis ticket service.  The Redis service is configured in the same config file so you must be sure to properly setup all appropriate Redis settings.
    migrate-tickets com.gitblit.tickets.RedisTicketService c:\gitblit-data
##### Gitblit WAR
Gitblit WAR does not ship with anything other than the WAR, but you can still migrate tickets offline with a little extra effort.
*Windows*
    java -cp "C:/path/to/WEB-INF/lib/*" com.gitblit.MigrateTickets <outputservice> --baseFolder <baseFolder>
*Linux/Unix/Mac OSX*
    java -cp /path/to/WEB-INF/lib/* com.gitblit.MigrateTickets <outputservice> --baseFolder <baseFolder>
src/site/tickets_setup.mkd
@@ -109,7 +109,7 @@
#### Milestones
Milestones are a way to group tickets together.  Currently milestones are specified at the repository level and are stored in the repository git config file.  Gitblit's internal architecture has all the methods necessary to maintain milestones, but this functionality is not yet exposed through the web ui.  For now you will have to control milestones manually with a text editor.
Milestones are a way to group tickets together.  Milestones are specified for each repository and are stored in the repository git config file.  Repository owners may create milestones through the web ui in the *Tickets* page on the *Milestones* tab.
    [milestone "v1.5.0"]
        status = Open
src/site/tickets_using.mkd
@@ -28,7 +28,7 @@
    ...add a single commit...
    git push origin HEAD:refs/for/new
    # read ticket id from server output
    git branch --set-upstream-to=origin/ticket/{id}
    git branch -u origin/ticket/{id}
### Creating the first Patchset for an Existing Ticket
@@ -42,7 +42,7 @@
    cd repo
    git checkout -b ticket/{id}
    ...add one or more commits...
    git push --set-upstream origin ticket/{id}
    git push -u origin ticket/{id}
### Safely adding commits to a Patchset for an Existing Ticket
@@ -58,6 +58,14 @@
    git pull --ff-only
    ...add one or more commits...
    git push
### Checking-Out a Named Branch for an Existing Ticket with a Patchset
If you prefer to name your local ticket branches rather than using the default integer ids, you can do this with a little more syntax.
    git checkout -b my_fix --track origin/ticket/{id}
This will create a local branch named *my_fix* which tracks the upstream ticket branch.
### Rewriting a Patchset (amend, rebase, squash)
@@ -76,21 +84,18 @@
### Updating your copy of a rewritten Patchset
If a patchset has been rewritten you can no longer simply *pull* to update.  Let's assume your checkout **does not** have any unshared commits - i.e. it represents the previous patchset.  The simplest way to update your branch to the current patchset is to reset it.
If a patchset has been rewritten you can no longer simply *pull* to update.  Let's assume your checkout **does not** have any unshared commits - i.e. it represents the previous patchset.  The simplest way to update your branch to the current patchset is to reset it using the `-B` checkout flag.
    git fetch && git checkout ticket/{id}
    git reset --hard origin/ticket/{id}
    git fetch && git checkout -B ticket/{id}
If you **do** have unshared commits then you'll could make a new temporary branch and then cherry-pick your changes onto the rewritten patchset.
    git branch oldticket ticket/{id}
    git fetch && git checkout ticket/{id}
    git reset --hard origin/ticket/{id}
    git fetch && git checkout -B ticket/{id}
    git cherry-pick <commitid1> <commitid2>
    git branch -D oldticket
Git is a very flexible tool, there are no doubt several other strategies you could use to resolve this situation.  The above solution is just one way.
### Ticket RefSpecs
@@ -148,7 +153,7 @@
### Merging Patchsets
The Gitblit web ui offers a merge button which *should work* but is not fully tested.  Gitblit does verify that you can cleanly merge a patchset to the integration branch.
The Gitblit web ui offers a merge button which will work for clean merges of a patchset to the integration branch.
There are complicated merge scenarios for which it may be best to merge using your Git client.  There are several ways to do this, here is a safe merge strategy which pulls into a new branch and then fast-forwards your integration branch, assuming you were happy with the pull (merge).
src/site/upgrade_express.mkd
File was deleted
src/test/config/test-users.conf
@@ -2,13 +2,9 @@
    password = admin
    cookie = dd94709528bb1c83d08f3088d4043f4742891f4f
    accountType = LOCAL
    emailMeOnMyTicketChanges = true
    role = "#admin"
    role = "#notfederated"
[user "sampleuser"]
    password = sampleuser
    cookie = 6e07ed42149fc166206319faffdfba2e2ec82e43
    accountType = LOCAL
    role = "#none"
[team "admins"]
    role = "#none"
    accountType = LOCAL
src/test/java/com/gitblit/tests/AuthenticationManagerTest.java
@@ -43,7 +43,7 @@
    IAuthenticationManager newAuthenticationManager() {
        RuntimeManager runtime = new RuntimeManager(getSettings(), GitBlitSuite.BASEFOLDER).start();
        users = new UserManager(runtime).start();
        users = new UserManager(runtime, null).start();
        AuthenticationManager auth = new AuthenticationManager(runtime, users).start();
        return auth;
    }
src/test/java/com/gitblit/tests/BranchTicketServiceTest.java
@@ -54,8 +54,8 @@
        IRuntimeManager runtimeManager = new RuntimeManager(settings).start();
        IPluginManager pluginManager = new PluginManager(runtimeManager).start();
        INotificationManager notificationManager = new NotificationManager(settings).start();
        IUserManager userManager = new UserManager(runtimeManager).start();
        IRepositoryManager repositoryManager = new RepositoryManager(runtimeManager, userManager).start();
        IUserManager userManager = new UserManager(runtimeManager, pluginManager).start();
        IRepositoryManager repositoryManager = new RepositoryManager(runtimeManager, pluginManager, userManager).start();
        BranchTicketService service = new BranchTicketService(
                runtimeManager,
src/test/java/com/gitblit/tests/FileTicketServiceTest.java
@@ -53,8 +53,8 @@
        IRuntimeManager runtimeManager = new RuntimeManager(settings).start();
        IPluginManager pluginManager = new PluginManager(runtimeManager).start();
        INotificationManager notificationManager = new NotificationManager(settings).start();
        IUserManager userManager = new UserManager(runtimeManager).start();
        IRepositoryManager repositoryManager = new RepositoryManager(runtimeManager, userManager).start();
        IUserManager userManager = new UserManager(runtimeManager, pluginManager).start();
        IRepositoryManager repositoryManager = new RepositoryManager(runtimeManager, pluginManager, userManager).start();
        FileTicketService service = new FileTicketService(
                runtimeManager,
src/test/java/com/gitblit/tests/HtpasswdAuthenticationTest.java
@@ -75,15 +75,15 @@
    private HtpasswdAuthProvider newHtpasswdAuthentication(IStoredSettings settings) {
        RuntimeManager runtime = new RuntimeManager(settings, GitBlitSuite.BASEFOLDER).start();
        UserManager users = new UserManager(runtime).start();
        UserManager users = new UserManager(runtime, null).start();
        HtpasswdAuthProvider htpasswd = new HtpasswdAuthProvider();
        htpasswd.setup(runtime, users);
        return htpasswd;
    }
    private AuthenticationManager newAuthenticationManager(IStoredSettings settings) {
        RuntimeManager runtime = new RuntimeManager(settings, GitBlitSuite.BASEFOLDER).start();
        UserManager users = new UserManager(runtime).start();
        UserManager users = new UserManager(runtime, null).start();
        HtpasswdAuthProvider htpasswd = new HtpasswdAuthProvider();
        htpasswd.setup(runtime, users);
        AuthenticationManager auth = new AuthenticationManager(runtime, users);
@@ -191,7 +191,7 @@
        assertEquals("leading", user.username);
    }
    @Test
    public void testAuthenticationManager()
    {
src/test/java/com/gitblit/tests/LdapAuthenticationTest.java
@@ -68,7 +68,7 @@
    private static InMemoryDirectoryServer ds;
    private IUserManager userManager;
    private AuthenticationManager auth;
    private MemorySettings settings;
@@ -97,12 +97,12 @@
    private LdapAuthProvider newLdapAuthentication(IStoredSettings settings) {
        RuntimeManager runtime = new RuntimeManager(settings, GitBlitSuite.BASEFOLDER).start();
        userManager = new UserManager(runtime).start();
        userManager = new UserManager(runtime, null).start();
        LdapAuthProvider ldap = new LdapAuthProvider();
        ldap.setup(runtime, userManager);
        return ldap;
    }
    private AuthenticationManager newAuthenticationManager(IStoredSettings settings) {
        RuntimeManager runtime = new RuntimeManager(settings, GitBlitSuite.BASEFOLDER).start();
        AuthenticationManager auth = new AuthenticationManager(runtime, userManager);
@@ -258,7 +258,7 @@
        assertNull(userThreeModel.getTeam("git_admins"));
        assertTrue(userThreeModel.canAdmin);
    }
    @Test
    public void testBindWithUser() {
        settings.put(Keys.realm.ldap.bindpattern, "CN=${username},OU=US,OU=Users,OU=UserControl,OU=MyOrganization,DC=MyDomain");
@@ -267,7 +267,7 @@
        UserModel userOneModel = auth.authenticate("UserOne", "userOnePassword".toCharArray());
        assertNotNull(userOneModel);
        UserModel userOneModelFailedAuth = auth.authenticate("UserOne", "userTwoPassword".toCharArray());
        assertNull(userOneModelFailedAuth);
    }
src/test/java/com/gitblit/tests/LuceneExecutorTest.java
@@ -49,8 +49,8 @@
        MemorySettings settings = new MemorySettings();
        settings.put(Keys.git.repositoriesFolder, GitBlitSuite.REPOSITORIES);
        RuntimeManager runtime = new RuntimeManager(settings, GitBlitSuite.BASEFOLDER).start();
        UserManager users = new UserManager(runtime).start();
        RepositoryManager repos = new RepositoryManager(runtime, users);
        UserManager users = new UserManager(runtime, null).start();
        RepositoryManager repos = new RepositoryManager(runtime, null, users);
        return new LuceneService(settings, repos);
    }
src/test/java/com/gitblit/tests/RedisTicketServiceTest.java
@@ -61,8 +61,8 @@
        IRuntimeManager runtimeManager = new RuntimeManager(settings).start();
        IPluginManager pluginManager = new PluginManager(runtimeManager).start();
        INotificationManager notificationManager = new NotificationManager(settings).start();
        IUserManager userManager = new UserManager(runtimeManager).start();
        IRepositoryManager repositoryManager = new RepositoryManager(runtimeManager, userManager).start();
        IUserManager userManager = new UserManager(runtimeManager, pluginManager).start();
        IRepositoryManager repositoryManager = new RepositoryManager(runtimeManager, pluginManager, userManager).start();
        RedisTicketService service = new RedisTicketService(
                runtimeManager,
src/test/java/com/gitblit/tests/RedmineAuthenticationTest.java
@@ -26,7 +26,7 @@
    RedmineAuthProvider newRedmineAuthentication(IStoredSettings settings) {
        RuntimeManager runtime = new RuntimeManager(settings, GitBlitSuite.BASEFOLDER).start();
        UserManager users = new UserManager(runtime).start();
        UserManager users = new UserManager(runtime, null).start();
        RedmineAuthProvider redmine = new RedmineAuthProvider();
        redmine.setup(runtime, users);
        return redmine;
@@ -35,10 +35,10 @@
    RedmineAuthProvider newRedmineAuthentication() {
        return newRedmineAuthentication(getSettings());
    }
    AuthenticationManager newAuthenticationManager() {
        RuntimeManager runtime = new RuntimeManager(getSettings(), GitBlitSuite.BASEFOLDER).start();
        UserManager users = new UserManager(runtime).start();
        UserManager users = new UserManager(runtime, null).start();
        RedmineAuthProvider redmine = new RedmineAuthProvider();
        redmine.setup(runtime, users);
        redmine.setTestingCurrentUserAsJson(JSON);
src/test/java/com/gitblit/tests/RpcTests.java
@@ -397,4 +397,67 @@
        assertNotNull(branches);
        assertTrue(branches.size() > 0);
    }
    @Test
    public void testFork() throws Exception {
        // test forking by an administrator
        // admins are all-powerful and can fork the unforakable :)
        testFork(account, password, true, true);
        testFork(account, password, false, true);
        // test forking by a permitted normal user
        UserModel forkUser = new UserModel("forkuser");
        forkUser.password = forkUser.username;
        forkUser.canFork = true;
        RpcUtils.deleteUser(forkUser, url, account, password.toCharArray());
        RpcUtils.createUser(forkUser, url, account, password.toCharArray());
        testFork(forkUser.username, forkUser.password, true, true);
        testFork(forkUser.username, forkUser.password, false, false);
        RpcUtils.deleteUser(forkUser, url, account, password.toCharArray());
        // test forking by a non-permitted normal user
        UserModel noForkUser = new UserModel("noforkuser");
        noForkUser.password = noForkUser.username;
        noForkUser.canFork = false;
        RpcUtils.deleteUser(noForkUser, url, account, password.toCharArray());
        RpcUtils.createUser(noForkUser, url, account, password.toCharArray());
        testFork(forkUser.username, forkUser.password, true, false);
        testFork(forkUser.username, forkUser.password, false, false);
        RpcUtils.deleteUser(noForkUser, url, account, password.toCharArray());
    }
    private void testFork(String forkAcct, String forkAcctPassword, boolean allowForks, boolean expectSuccess) throws Exception {
        // test does not exist
        RepositoryModel dne = new RepositoryModel();
        dne.name = "doesNotExist.git";
        assertFalse(String.format("Successfully forked %s!", dne.name),
                RpcUtils.forkRepository(dne, url, forkAcct, forkAcctPassword.toCharArray()));
        // delete any previous fork
        RepositoryModel fork = findRepository(String.format("~%s/helloworld.git", forkAcct));
        if (fork != null) {
            RpcUtils.deleteRepository(fork, url, account, password.toCharArray());
        }
        // update the origin to allow forks or not
        RepositoryModel origin = findRepository("helloworld.git");
        origin.allowForks = allowForks;
        RpcUtils.updateRepository(origin.name, origin, url, account, password.toCharArray());
        // fork the repository
        if (expectSuccess) {
            assertTrue(String.format("Failed to fork %s!", origin.name),
                RpcUtils.forkRepository(origin, url, forkAcct, forkAcctPassword.toCharArray()));
        } else {
            assertFalse(String.format("Successfully forked %s!", origin.name),
                    RpcUtils.forkRepository(origin, url, forkAcct, forkAcctPassword.toCharArray()));
        }
        // attempt another fork
        assertFalse(String.format("Successfully forked %s!", origin.name),
                RpcUtils.forkRepository(origin, url, forkAcct, forkAcctPassword.toCharArray()));
        // delete the fork repository
        RpcUtils.deleteRepository(fork, url, account, password.toCharArray());
    }
}
src/test/java/com/gitblit/tests/SyndicationUtilsTest.java
@@ -21,7 +21,10 @@
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import com.gitblit.Constants.SearchType;
@@ -29,6 +32,20 @@
import com.gitblit.utils.SyndicationUtils;
public class SyndicationUtilsTest extends GitblitUnitTest {
    private static final AtomicBoolean started = new AtomicBoolean(false);
    @BeforeClass
    public static void startGitblit() throws Exception {
        started.set(GitBlitSuite.startGitblit());
    }
    @AfterClass
    public static void stopGitblit() throws Exception {
        if (started.get()) {
            GitBlitSuite.stopGitblit();
        }
    }
    @Test
    public void testSyndication() throws Exception {
@@ -60,7 +77,7 @@
    }
    @Test
    public void testFeedRead() throws Exception {
    public void testFeedReadCommits() throws Exception {
        Set<String> links = new HashSet<String>();
        for (int i = 0; i < 2; i++) {
            List<FeedEntryModel> feed = SyndicationUtils.readFeed(GitBlitSuite.url, "ticgit.git",
@@ -77,6 +94,23 @@
    }
    @Test
    public void testFeedReadTags() throws Exception {
        Set<String> links = new HashSet<String>();
        for (int i = 0; i < 2; i++) {
            List<FeedEntryModel> feed = SyndicationUtils.readTags(GitBlitSuite.url, "test/gitective.git",
                    5, i, GitBlitSuite.account, GitBlitSuite.password.toCharArray());
            assertTrue(feed != null);
            assertTrue(feed.size() > 0);
            assertEquals(5, feed.size());
            for (FeedEntryModel entry : feed) {
                links.add(entry.link);
            }
        }
        // confirm we have 10 unique tags
        assertEquals("Feed pagination failed", 10, links.size());
    }
    @Test
    public void testSearchFeedRead() throws Exception {
        List<FeedEntryModel> feed = SyndicationUtils
                .readSearchFeed(GitBlitSuite.url, "ticgit.git", null, "test", null, 5, 0,
src/test/java/com/gitblit/tests/TicketServiceTest.java
@@ -95,7 +95,7 @@
        // query non-existent ticket
        TicketModel nonExistent = service.getTicket(getRepository(), 0);
        assertNull(nonExistent);
        // create and insert a ticket
        Change c1 = newChange("testCreation() " + Long.toHexString(System.currentTimeMillis()));
        TicketModel ticket = service.createTicket(getRepository(), c1);
@@ -205,6 +205,9 @@
        assertEquals(1, results.size());
        assertTrue(results.get(0).title.startsWith("testUpdates"));
        // check the ids
        assertEquals("[1, 2]", service.getIds(getRepository()).toString());
        // delete all tickets
        for (TicketModel aTicket : allTickets) {
            assertTrue(service.deleteTicket(getRepository(), aTicket.number, "D"));
src/test/java/com/gitblit/tests/mock/MockRuntimeManager.java
@@ -82,6 +82,21 @@
    }
    @Override
    public boolean isServingHTTP() {
        return true;
    }
    @Override
    public boolean isServingGIT() {
        return true;
    }
    @Override
    public boolean isServingSSH() {
        return true;
    }
    @Override
    public boolean isDebugMode() {
        return true;
    }