summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/main/distrib/data/certs/instructions.tmpl_zh_TW111
-rw-r--r--src/main/distrib/data/certs/mail.tmpl_zh_TW5
-rw-r--r--src/main/distrib/data/defaults.properties101
-rw-r--r--src/main/distrib/data/projects_ja.conf5
-rw-r--r--src/main/distrib/linux/add-indexed-branch.sh4
-rw-r--r--src/main/distrib/linux/authority.sh2
-rw-r--r--src/main/distrib/linux/gitblit-stop.sh2
-rw-r--r--src/main/distrib/linux/gitblit.sh2
-rwxr-xr-xsrc/main/distrib/linux/install-service-fedora.sh10
-rw-r--r--src/main/distrib/linux/install-service-freebsd.sh2
-rw-r--r--src/main/distrib/linux/migrate-tickets.sh4
-rw-r--r--src/main/distrib/linux/reindex-tickets.sh4
-rw-r--r--src/main/distrib/linux/service-centos.sh6
-rw-r--r--src/main/distrib/linux/service-freebsd.sh49
-rw-r--r--src/main/distrib/linux/service-ubuntu.sh2
-rw-r--r--src/main/distrib/win/add-indexed-branch.cmd4
-rw-r--r--src/main/distrib/win/authority.cmd9
-rw-r--r--src/main/distrib/win/gitblit-stop.cmd2
-rw-r--r--src/main/distrib/win/gitblit.cmd9
-rw-r--r--src/main/distrib/win/installService.cmd26
-rw-r--r--src/main/distrib/win/migrate-tickets.cmd12
-rw-r--r--src/main/distrib/win/reindex-tickets.cmd12
-rw-r--r--src/main/distrib/win/uninstallService.cmd6
-rw-r--r--src/main/java/WEB-INF/web.xml8
-rw-r--r--src/main/java/WEB-INF/weblogic.xml4
-rw-r--r--src/main/java/com/gitblit/ConfigUserService.java4
-rw-r--r--src/main/java/com/gitblit/Constants.java37
-rw-r--r--src/main/java/com/gitblit/GitBlitServer.java15
-rw-r--r--src/main/java/com/gitblit/IUserService.java3
-rw-r--r--src/main/java/com/gitblit/Launcher.java148
-rw-r--r--src/main/java/com/gitblit/MigrateTickets.java2
-rw-r--r--src/main/java/com/gitblit/ReindexTickets.java2
-rw-r--r--src/main/java/com/gitblit/StoredUserConfig.java231
-rw-r--r--src/main/java/com/gitblit/auth/AuthenticationProvider.java6
-rw-r--r--src/main/java/com/gitblit/auth/HtpasswdAuthProvider.java2
-rw-r--r--src/main/java/com/gitblit/auth/LdapAuthProvider.java278
-rw-r--r--src/main/java/com/gitblit/auth/PAMAuthProvider.java2
-rw-r--r--src/main/java/com/gitblit/auth/RedmineAuthProvider.java2
-rw-r--r--src/main/java/com/gitblit/auth/SalesforceAuthProvider.java2
-rw-r--r--src/main/java/com/gitblit/auth/WindowsAuthProvider.java2
-rw-r--r--src/main/java/com/gitblit/authority/GitblitAuthority.java19
-rw-r--r--src/main/java/com/gitblit/authority/Launcher.java166
-rw-r--r--src/main/java/com/gitblit/client/EditUserDialog.java30
-rw-r--r--src/main/java/com/gitblit/client/GitblitManagerLauncher.java165
-rw-r--r--src/main/java/com/gitblit/git/PatchsetReceivePack.java3
-rw-r--r--src/main/java/com/gitblit/ldap/LdapConnection.java338
-rw-r--r--src/main/java/com/gitblit/manager/AuthenticationManager.java127
-rw-r--r--src/main/java/com/gitblit/manager/RepositoryManager.java71
-rw-r--r--src/main/java/com/gitblit/manager/RuntimeManager.java2
-rw-r--r--src/main/java/com/gitblit/manager/UserManager.java24
-rw-r--r--src/main/java/com/gitblit/models/RefModel.java91
-rw-r--r--src/main/java/com/gitblit/models/RepositoryCommit.java49
-rw-r--r--src/main/java/com/gitblit/models/RepositoryModel.java3
-rw-r--r--src/main/java/com/gitblit/models/TicketModel.java6
-rw-r--r--src/main/java/com/gitblit/models/TreeNodeModel.java178
-rw-r--r--src/main/java/com/gitblit/models/UserModel.java21
-rw-r--r--src/main/java/com/gitblit/service/LuceneRepoIndexStore.java55
-rw-r--r--src/main/java/com/gitblit/service/LuceneService.java98
-rw-r--r--src/main/java/com/gitblit/service/MailService.java19
-rw-r--r--src/main/java/com/gitblit/service/MirrorService.java10
-rw-r--r--src/main/java/com/gitblit/servlet/AccessRestrictionFilter.java16
-rw-r--r--src/main/java/com/gitblit/servlet/GitFilter.java78
-rw-r--r--src/main/java/com/gitblit/servlet/GitblitContext.java12
-rw-r--r--src/main/java/com/gitblit/servlet/PagesServlet.java7
-rw-r--r--src/main/java/com/gitblit/servlet/RawServlet.java96
-rw-r--r--src/main/java/com/gitblit/servlet/RpcServlet.java4
-rw-r--r--src/main/java/com/gitblit/tickets/BranchTicketService.java3
-rw-r--r--src/main/java/com/gitblit/tickets/FileTicketService.java3
-rw-r--r--src/main/java/com/gitblit/tickets/ITicketService.java43
-rw-r--r--src/main/java/com/gitblit/tickets/NullTicketService.java3
-rw-r--r--src/main/java/com/gitblit/tickets/RedisTicketService.java3
-rw-r--r--src/main/java/com/gitblit/tickets/TicketIndexer.java86
-rw-r--r--src/main/java/com/gitblit/tickets/TicketNotifier.java4
-rw-r--r--src/main/java/com/gitblit/transport/ssh/IPublicKeyManager.java13
-rw-r--r--src/main/java/com/gitblit/transport/ssh/LdapKeyManager.java435
-rw-r--r--src/main/java/com/gitblit/transport/ssh/NonForwardingFilter.java2
-rw-r--r--src/main/java/com/gitblit/transport/ssh/SshDaemon.java46
-rw-r--r--src/main/java/com/gitblit/transport/ssh/SshDaemonClient.java2
-rw-r--r--src/main/java/com/gitblit/transport/ssh/SshServerSessionFactory.java10
-rw-r--r--src/main/java/com/gitblit/transport/ssh/WelcomeShell.java21
-rw-r--r--src/main/java/com/gitblit/transport/ssh/keys/KeysDispatcher.java17
-rw-r--r--src/main/java/com/gitblit/utils/ArrayUtils.java2
-rw-r--r--src/main/java/com/gitblit/utils/CommitCache.java117
-rw-r--r--src/main/java/com/gitblit/utils/ContainerDetector.java74
-rw-r--r--src/main/java/com/gitblit/utils/FileUtils.java23
-rw-r--r--src/main/java/com/gitblit/utils/HttpUtils.java4
-rw-r--r--src/main/java/com/gitblit/utils/JGitUtils.java349
-rw-r--r--src/main/java/com/gitblit/utils/LuceneIndexStore.java98
-rw-r--r--src/main/java/com/gitblit/utils/MarkdownUtils.java5
-rw-r--r--src/main/java/com/gitblit/utils/PasswordHash.java292
-rw-r--r--src/main/java/com/gitblit/utils/PasswordHashPbkdf2.java276
-rw-r--r--src/main/java/com/gitblit/utils/SecureRandom.java83
-rw-r--r--src/main/java/com/gitblit/utils/StringUtils.java29
-rw-r--r--src/main/java/com/gitblit/utils/TimeUtils.java91
-rw-r--r--src/main/java/com/gitblit/utils/X509Utils.java32
-rw-r--r--src/main/java/com/gitblit/wicket/GitBlitWebApp.properties19
-rw-r--r--src/main/java/com/gitblit/wicket/GitBlitWebApp_cs.properties791
-rw-r--r--src/main/java/com/gitblit/wicket/GitBlitWebApp_de.properties24
-rw-r--r--src/main/java/com/gitblit/wicket/GitBlitWebApp_es.properties48
-rw-r--r--src/main/java/com/gitblit/wicket/GitBlitWebApp_fr.properties18
-rw-r--r--src/main/java/com/gitblit/wicket/GitBlitWebApp_it.properties17
-rw-r--r--src/main/java/com/gitblit/wicket/GitBlitWebApp_ja.properties1331
-rw-r--r--src/main/java/com/gitblit/wicket/GitBlitWebApp_ko.properties1276
-rw-r--r--src/main/java/com/gitblit/wicket/GitBlitWebApp_nl.properties25
-rw-r--r--src/main/java/com/gitblit/wicket/GitBlitWebApp_no.properties78
-rw-r--r--src/main/java/com/gitblit/wicket/GitBlitWebApp_pl.properties17
-rw-r--r--src/main/java/com/gitblit/wicket/GitBlitWebApp_pt_BR.properties453
-rw-r--r--src/main/java/com/gitblit/wicket/GitBlitWebApp_ru.properties790
-rw-r--r--src/main/java/com/gitblit/wicket/GitBlitWebApp_zh_CN.properties601
-rw-r--r--src/main/java/com/gitblit/wicket/GitBlitWebApp_zh_TW.properties7
-rw-r--r--src/main/java/com/gitblit/wicket/MarkupProcessor.java66
-rw-r--r--src/main/java/com/gitblit/wicket/SessionlessForm.java14
-rw-r--r--src/main/java/com/gitblit/wicket/pages/BasePage.html4
-rw-r--r--src/main/java/com/gitblit/wicket/pages/BlamePage.java2
-rw-r--r--src/main/java/com/gitblit/wicket/pages/ChangePasswordPage.java20
-rw-r--r--src/main/java/com/gitblit/wicket/pages/DocPage.java5
-rw-r--r--src/main/java/com/gitblit/wicket/pages/DocsPage.java4
-rw-r--r--src/main/java/com/gitblit/wicket/pages/EditFilePage.java2
-rw-r--r--src/main/java/com/gitblit/wicket/pages/EditRepositoryPage.html3
-rw-r--r--src/main/java/com/gitblit/wicket/pages/EditRepositoryPage.java6
-rw-r--r--src/main/java/com/gitblit/wicket/pages/EditUserPage.html11
-rw-r--r--src/main/java/com/gitblit/wicket/pages/EditUserPage.java38
-rw-r--r--src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage.html2
-rw-r--r--src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_cs.html60
-rw-r--r--src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_de.html2
-rw-r--r--src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_es.html2
-rw-r--r--src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_it.html2
-rw-r--r--src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_ja.html60
-rw-r--r--src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_ko.html2
-rw-r--r--src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_nl.html2
-rw-r--r--src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_no.html2
-rw-r--r--src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_pl.html2
-rw-r--r--src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_pt_BR.html2
-rw-r--r--src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_ru.html60
-rw-r--r--src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_zh_CN.html2
-rw-r--r--src/main/java/com/gitblit/wicket/pages/FilestoreUsage_ja.html52
-rw-r--r--src/main/java/com/gitblit/wicket/pages/Language.java21
-rw-r--r--src/main/java/com/gitblit/wicket/pages/LuceneSearchPage.html4
-rw-r--r--src/main/java/com/gitblit/wicket/pages/LuceneSearchPage.java4
-rw-r--r--src/main/java/com/gitblit/wicket/pages/MetricsPage.java2
-rw-r--r--src/main/java/com/gitblit/wicket/pages/RawPage.java31
-rw-r--r--src/main/java/com/gitblit/wicket/pages/RepositoryPage.java10
-rw-r--r--src/main/java/com/gitblit/wicket/pages/SummaryPage.java2
-rw-r--r--src/main/java/com/gitblit/wicket/pages/TicketPage.java27
-rw-r--r--src/main/java/com/gitblit/wicket/pages/TicketsPage.java16
-rw-r--r--src/main/java/com/gitblit/wicket/pages/UserPage.java75
-rw-r--r--src/main/java/com/gitblit/wicket/panels/HistoryPanel.java39
-rw-r--r--src/main/java/com/gitblit/wicket/panels/LogPanel.java33
-rw-r--r--src/main/java/com/gitblit/wicket/panels/NestedRepositoryTreePanel.html106
-rw-r--r--src/main/java/com/gitblit/wicket/panels/NestedRepositoryTreePanel.java233
-rw-r--r--src/main/java/com/gitblit/wicket/panels/PagerPanel.java5
-rw-r--r--src/main/java/com/gitblit/wicket/panels/RepositoriesPanel.html21
-rw-r--r--src/main/java/com/gitblit/wicket/panels/RepositoriesPanel.java71
-rw-r--r--src/main/java/com/gitblit/wicket/panels/SearchPanel.java29
-rw-r--r--src/main/java/com/gitblit/wicket/panels/SshKeysPanel.java11
-rw-r--r--src/main/java/login_cs.mkd3
-rw-r--r--src/main/java/login_ja.mkd3
-rw-r--r--src/main/java/login_ru.mkd3
-rw-r--r--src/main/java/welcome_cs.mkd3
-rw-r--r--src/main/java/welcome_ja.mkd3
-rw-r--r--src/main/java/welcome_ru.mkd3
-rw-r--r--src/main/resources/bootstrap-fixes.css25
-rw-r--r--src/main/resources/gitblit.css2
-rw-r--r--src/main/resources/gitblit/js/collapsible-table.js71
-rw-r--r--src/site/administration.mkd2
-rw-r--r--src/site/design.mkd8
-rw-r--r--src/site/faq.mkd2
-rw-r--r--src/site/federation.mkd12
-rw-r--r--src/site/rpc.mkd7
-rw-r--r--src/site/setup_authentication.mkd2
-rw-r--r--src/site/setup_go.mkd20
-rw-r--r--src/site/setup_proxy.mkd1
-rw-r--r--src/site/siteindex.mkd39
-rwxr-xr-xsrc/site/templates/ghreleasenotes.awk66
-rw-r--r--src/site/tickets_overview.mkd2
-rw-r--r--src/site/upgrade_go.mkd22
-rw-r--r--src/test/config/test-users.conf3
-rw-r--r--src/test/data/.gitignore1
-rw-r--r--src/test/data/ambition.git.zipbin0 -> 199401 bytes
-rw-r--r--src/test/data/gitective.git.zipbin0 -> 2974989 bytes
-rw-r--r--src/test/data/hello-world.git.zipbin0 -> 76158 bytes
-rw-r--r--src/test/data/hello-world.properties14
-rw-r--r--src/test/data/ticgit.git.zipbin0 -> 364736 bytes
-rw-r--r--src/test/java/com/gitblit/StoredUserConfigTest.java207
-rw-r--r--src/test/java/com/gitblit/service/LuceneRepoIndexStoreTest.java267
-rw-r--r--src/test/java/com/gitblit/servlet/RawServletTest.java1426
-rw-r--r--src/test/java/com/gitblit/tests/.gitignore1
-rw-r--r--src/test/java/com/gitblit/tests/AuthenticationManagerTest.java124
-rw-r--r--src/test/java/com/gitblit/tests/BranchTicketServiceTest.java2
-rw-r--r--src/test/java/com/gitblit/tests/DiffUtilsTest.java22
-rw-r--r--src/test/java/com/gitblit/tests/FederationTests.java52
-rw-r--r--src/test/java/com/gitblit/tests/FileTicketServiceTest.java2
-rw-r--r--src/test/java/com/gitblit/tests/GitBlitSuite.java95
-rw-r--r--src/test/java/com/gitblit/tests/GitServletTest.java85
-rw-r--r--src/test/java/com/gitblit/tests/GroovyScriptTest.java46
-rw-r--r--src/test/java/com/gitblit/tests/HtpasswdAuthenticationTest.java4
-rw-r--r--src/test/java/com/gitblit/tests/JGitUtilsTest.java149
-rw-r--r--src/test/java/com/gitblit/tests/JsonUtilsTest.java9
-rw-r--r--src/test/java/com/gitblit/tests/LdapAuthenticationTest.java376
-rw-r--r--src/test/java/com/gitblit/tests/LdapBasedUnitTest.java410
-rw-r--r--src/test/java/com/gitblit/tests/LdapConnectionTest.java280
-rw-r--r--src/test/java/com/gitblit/tests/LdapPublicKeyManagerTest.java723
-rw-r--r--src/test/java/com/gitblit/tests/MarkdownUtilsTest.java74
-rw-r--r--src/test/java/com/gitblit/tests/MetricUtilsTest.java4
-rw-r--r--src/test/java/com/gitblit/tests/PushLogTest.java41
-rw-r--r--src/test/java/com/gitblit/tests/RedisTicketServiceTest.java5
-rw-r--r--src/test/java/com/gitblit/tests/RpcTests.java15
-rw-r--r--src/test/java/com/gitblit/tests/SshDaemonTest.java5
-rw-r--r--src/test/java/com/gitblit/tests/SshKeysDispatcherTest.java14
-rw-r--r--src/test/java/com/gitblit/tests/SshUnitTest.java81
-rw-r--r--src/test/java/com/gitblit/tests/StringUtilsTest.java49
-rw-r--r--src/test/java/com/gitblit/tests/TicketReferenceTest.java21
-rw-r--r--src/test/java/com/gitblit/tests/TicketServiceTest.java6
-rw-r--r--src/test/java/com/gitblit/tests/TimeUtilsTest.java108
-rw-r--r--src/test/java/com/gitblit/tests/UITicketTest.java37
-rw-r--r--src/test/java/com/gitblit/tests/UserModelTest.java49
-rw-r--r--src/test/java/com/gitblit/tests/UserServiceTest.java127
-rw-r--r--src/test/java/com/gitblit/tests/mock/MockGitblitContext.java12
-rw-r--r--src/test/java/com/gitblit/utils/LuceneIndexStoreTest.java245
-rw-r--r--src/test/java/com/gitblit/utils/PasswordHashTest.java666
-rw-r--r--src/test/java/com/gitblit/utils/SecureRandomTest.java33
-rw-r--r--src/test/java/com/gitblit/utils/TimeUtilsTest.java690
-rw-r--r--src/test/java/com/gitblit/wicket/GitBlitWebAppResourceBundleTest.java156
-rw-r--r--src/test/java/com/gitblit/wicket/MarkupProcessorTest.java734
-rw-r--r--src/test/java/com/gitblit/wicket/panels/TreeNodeModelTest.java49
-rw-r--r--src/test/resources/ldap/users.conf6
226 files changed, 16289 insertions, 3188 deletions
diff --git a/src/main/distrib/data/certs/instructions.tmpl_zh_TW b/src/main/distrib/data/certs/instructions.tmpl_zh_TW
new file mode 100644
index 00000000..72b99e0d
--- /dev/null
+++ b/src/main/distrib/data/certs/instructions.tmpl_zh_TW
@@ -0,0 +1,111 @@
+********************************************************************************
+ Gitblit 伺服器 $serverHostname 所需之 SSL 用戶端憑證
+********************************************************************************
+
+ $userDisplayname 您好,
+
+ 伺服器 $serverHostname 所需要的私鑰,公鑰以及Gitblit簽證檔案(CA)存放於 $username.p12, PKCS#12 certificate store[1] 以及 $username.pem.
+
+ 兩種證書皆受密碼保護.
+ 密碼提示: $storePasswordHint
+
+
+Git 憑證匯入步驟
+=============================================
+
+ 附件之 PEM 檔案可以直接匯入至您的git程式裡.
+
+ git config [--global] http.sslCert path/to/$username.pem
+
+ PEM檔案受密碼保護,因此匯入過程會提示多次. 如果您偏好不使用密碼,您需要另外匯出無密碼之私鑰後,再匯入git程式裡.
+
+ openssl rsa -in path/to/$username.pem -out path/to/$username.key
+ git config [--global] http.sslKey path/to/$username.key
+
+ 此外,您應該妥善保管已經解除密碼保護之私鑰.
+
+ 註:
+ 如果沒有匯出私鑰, 有些早期git版本將會發生匯入失敗問題,例如:Ubuntu 12.04 with git 1.7.9.5.
+
+
+Firefox 憑證匯入步驟
+=============================================
+
+ Firefox 有自己的證書管理介面.
+
+ 1. 點選 "選項->進階->憑證"
+ 2. 點選 "檢視憑證清單"
+ 3. 切換至 "您的憑證"
+ 4. 點選 "匯入(M)"
+ 5. 選擇電腦中的憑證檔案 $username.p12
+ 6. 輸入憑證檔案所需的密碼
+ 7. 切換至"憑證機構"
+ 8. 找到 "Gitblit Certificate Authority" 憑證
+ 9. 點選"編輯信任(E)"
+ 10.選擇信任網站.
+
+
+Chrome/IE (Windows) 憑證匯入步驟
+=============================================
+
+ 在Windows作業系統下, Chrome 與 IE 共用相同的憑證設定.
+
+ IE
+ ------------------------------------
+ 1. 選擇 "網際網路選項->內容"
+ 2. 點選"憑證"
+
+ Chrome
+ ------------------------------------
+ 1. 選擇 "設定->顯示進階設定->HTTP/SSL"
+ 2. 點選"管理憑證"
+
+ 共同步驟 (Windows)
+ ------------------------------------
+ 3. 切換至 "個人"
+ 4. 點選"匯入(I)"
+ 5. 依照指示匯入.
+ 請切換匯入檔案類型為p12並且找到 $username.p12 這個憑證檔案
+ 6. 輸入憑證檔案保護密碼
+ 7. 由於主要發放憑證單位(CA)與個人憑證檔案皆儲存於 $username.p12, 因此匯入時候,必須選擇 "自動根據憑證類型來選擇憑證存放區".
+ 如果選擇預設值匯入, 將不會安裝主要發放憑證單位(CA)
+
+
+Chrome (Linux) Installation Instructions
+=============================================
+
+ On Linux, Chrome maintains it's own certificate store.
+
+ 1. Navigate to Settings->Show Advanced Settings->HTTP/SSL
+ 2. Click the "Manage Certificates..." button
+ 3. Navigate your filesystem and select $username.p12
+ 4. At the password prompt enter the certificate store password
+ You have now imported your private key, public certificate, and the CA certificate
+ but now we must manually set the trust settings of the CA certificate.
+ 5. Switch to the "Authorities" tab
+ 6. Scroll down and find "Gitblit-> Gitblit Certificate Authority"
+ 7. Select it and click "Edit Trust..."
+ 8. Check "This certificate can identify websites" and click OK.
+
+
+Chrome/Safari (Mac OS X) Installation Instructions
+=============================================
+
+On Mac OS X, Chrome and Safari both use Keychain Access to store certificates
+so configuring one will automatically apply for both.
+
+ 1. Double-click $username.pem
+ 2. At the password prompt enter the certificate store password
+ You have now imported your private key, public certificate, and the CA certificate
+ but now we must manually set the trust settings of the CA certificate.
+ 3. Find the Gitblit Certificate Authority certificate, it should have a red
+ indicator meaning untrusted, and double-click it.
+ 4. Open the "Trust" disclosure triangle and change "When using this certificate"
+ to "Always Trust".
+ 5. Close the certificate view and enter your system password to save the changes
+ to your keychain.
+
+
+[1] PKCS#12 is one of the standard container formats for sharing private keys and
+ public certificates.
+[2] http://www.openssl.org
diff --git a/src/main/distrib/data/certs/mail.tmpl_zh_TW b/src/main/distrib/data/certs/mail.tmpl_zh_TW
new file mode 100644
index 00000000..20eb5183
--- /dev/null
+++ b/src/main/distrib/data/certs/mail.tmpl_zh_TW
@@ -0,0 +1,5 @@
+$userDisplayname 您好
+
+ 伺服器 $serverHostname 所需要的私鑰,公鑰以及Gitblit簽證檔案(CA)已經全部打包並且以zip壓縮檔方式寄給您.
+
+ 此外,檔案還附上各瀏覽器設定步驟供您參考. \ No newline at end of file
diff --git a/src/main/distrib/data/defaults.properties b/src/main/distrib/data/defaults.properties
index 852ea447..604caa8f 100644
--- a/src/main/distrib/data/defaults.properties
+++ b/src/main/distrib/data/defaults.properties
@@ -138,10 +138,25 @@ git.sshKeysManager = com.gitblit.transport.ssh.FileKeyManager
# SINCE 1.5.0
git.sshKeysFolder= ${baseFolder}/ssh
-# Use Kerberos5 (GSS) authentication
+
+# Authentication methods offered by the SSH server.
+# Space separated list of authentication method names that the
+# server shall offer. The default is "publickey password".
#
-# SINCE 1.7.0
-git.sshWithKrb5 = false
+# Valid authentication method names are:
+# publickey - authenticate with SSH public key
+# password - authenticate with username, password
+# keyboard-interactive - currently synonym to 'password'
+# gssapi-with-mic - GSS API Kerberos 5 authentication
+#
+# This setting obsoletes the "git.sshWithKrb5" setting. To enable
+# Kerberos5 (GSS) authentication, add 'gssapi-with-mic' to the list.
+#
+# SINCE 1.9.0
+# RESTART REQUIRED
+# SPACE-DELIMITED
+git.sshAuthenticationMethods = publickey password
+
# The path to a Kerberos 5 keytab.
#
@@ -301,7 +316,7 @@ git.defaultIncrementalPushTagPrefix = r
# the repository should be created with 'git init --shared' to make sure that
# it can be accessed e.g. via ssh (user git) and http (user www-data).
#
-# Valid values are the values available for the '--shared' option. The the manual
+# Valid values are the values available for the '--shared' option. See the manual
# page for 'git init' for more information on shared repositories.
#
# SINCE 1.4.0
@@ -567,6 +582,21 @@ tickets.acceptNewPatchsets = true
# SINCE 1.4.0
tickets.requireApproval = false
+# Default setting to control how patchsets are merged to the integration branch.
+# Valid values:
+# MERGE_ALWAYS - Always merge with a merge commit. Every ticket will show up as a branch,
+# even if it could have been fast-forward merged. This is the default.
+# MERGE_IF_NECESSARY - If possible, fast-forward the integration branch,
+# if not, merge with a merge commit.
+# FAST_FORWARD_ONLY - Only merge when a fast-forward is possible. This produces a strictly
+# linear history of the integration branch.
+#
+# This setting can be overriden per-repository.
+#
+# RESTART REQUIRED
+# SINCE 1.9.0
+tickets.mergeType = MERGE_ALWAYS
+
# The case-insensitive regular expression used to identify and close tickets on
# push to the integration branch for commits that are NOT already referenced as
# a patchset tip.
@@ -839,12 +869,14 @@ realm.userService = ${baseFolder}/users.conf
realm.authenticationProviders =
# How to store passwords.
-# Valid values are plain, md5, or combined-md5. md5 is the hash of password.
+# Valid values are plain, md5, combined-md5 or pbkdf2.
+# md5 is the hash of password.
# combined-md5 is the hash of username.toLowerCase()+password.
-# Default is md5.
+# pbkdf2 implements the PBKDF2 algorithm, which is a secure, salted password hashing scheme.
+# Default is pbkdf2. Using plain, md5 or combined-md5 is deprecated, as these are insecure schemes by now.
#
# SINCE 0.5.0
-realm.passwordStorage = md5
+realm.passwordStorage = pbkdf2
# Minimum valid length for a plain text password.
# Default value is 5. Absolute minimum is 4.
@@ -1233,6 +1265,7 @@ web.allowAppCloneLinks = true
# Choose how to present the repositories list.
# grouped = group nested/subfolder repositories together (no sorting)
# flat = flat list of repositories (sorting allowed)
+# tree = group nested/subfolder repositories in a tree structure with folding branches
#
# SINCE 0.5.0
web.repositoryListType = grouped
@@ -1250,6 +1283,20 @@ web.repositoryRootGroupName = main
# SINCE 0.8.0
web.repositoryListSwatches = true
+# Specify the behaviour of the Repository groups on the "Repositories"
+# page, specifically whether they can be collapsed and expanded, and
+# their default state on loading the page.
+# Only on repositoryListType grouped
+#
+# Values (case-insensitive):
+# disabled - Repository groups cannot collapsed; maintains behaviour
+# from previous versions of GitBlit.
+# expanded - On loading the page all repository groups are expanded.
+# collapsed - On loading the page all repository groups are collapsed.
+#
+# SINCE 1.9.0
+web.collapsibleRepositoryGroups = expanded
+
# Defines the default commit message renderer. This can be configured
# per-repository.
#
@@ -1797,6 +1844,10 @@ realm.salesforce.orgId = 0
realm.ldap.server = ldap://localhost
# Login username for LDAP searches.
+# This is usually a user with permissions to search LDAP users and groups.
+# It must have at least have the permission to search users. If it does not
+# have permission to search groups, the normal user logging in must have
+# the permission in LDAP to search groups.
# If this value is unspecified, anonymous LDAP login will be used.
#
# e.g. mydomain\\username
@@ -1809,8 +1860,14 @@ realm.ldap.username = cn=Directory Manager
# SINCE 1.0.0
realm.ldap.password = password
-# Bind pattern for Authentication.
-# Allow to directly authenticate an user without LDAP Searches.
+# Bind pattern for user authentication.
+# Allow to directly authenticate an user without searching for it in LDAP.
+# Use this if the LDAP server does not allow anonymous access and you don't
+# want to use a specific account to run searches. When set, it will override
+# the settings realm.ldap.username and realm.ldap.password.
+# This requires that all relevant user entries are children to the same DN,
+# and that logging users have permission to search for their groups in LDAP.
+# This will disable synchronization as a specific LDAP account is needed for that.
#
# e.g. CN=${username},OU=Users,OU=UserControl,OU=MyOrganization,DC=MyDomain
#
@@ -1925,7 +1982,26 @@ realm.ldap.email = email
# SINCE 1.0.0
realm.ldap.uid = uid
+# Attribute on the USER record that indicates their public SSH key.
+# Leave blank when public SSH keys shall not be retrieved from LDAP.
+#
+# This setting is only relevant when a public key manager is used that
+# retrieves SSH keys from LDAP (e.g. com.gitblit.transport.ssh.LdapKeyManager).
+#
+# The accepted format of the value is dependent on the public key manager used.
+# Examples:
+# sshPublicKey - Use the attribute 'sshPublicKey' on the user record.
+# altSecurityIdentities:SshKey - Use the attribute 'altSecurityIdentities'
+# on the user record, for which the record value
+# starts with 'SshKey:', followed by the SSH key entry.
+#
+# SINCE 1.9.0
+realm.ldap.sshPublicKey =
+
# Defines whether to synchronize all LDAP users and teams into the user service
+# This requires either anonymous LDAP access or that a specific account is set
+# in realm.ldap.username and realm.ldap.password, that has permission to read
+# users and groups in LDAP.
#
# Valid values: true, false
# If left blank, false is assumed
@@ -2084,6 +2160,13 @@ server.wantClientCertificates = false
# RESTART REQUIRED
server.shutdownPort = 8081
+# Http idle Timeout (in milliseconds) for http and https requests
+# Increase this value if you get java.util.concurrent.TimeoutException errors
+#
+# SINCE 1.9.0
+# RESTART REQUIRED
+server.httpIdleTimeout = 30000
+
#
# Gitblit Filestore Settings
#
diff --git a/src/main/distrib/data/projects_ja.conf b/src/main/distrib/data/projects_ja.conf
new file mode 100644
index 00000000..b9b8b197
--- /dev/null
+++ b/src/main/distrib/data/projects_ja.conf
@@ -0,0 +1,5 @@
+#このファイルは、他の翻訳ファイルと違いこのファイル名のままでは動作しない。
+#_ja は便宜上付けている名前で、projects.conf に改名し、既存の projects.conf と置き換えて初めて動作する。
+[project "main"]
+ title = メインリポジトリ
+ description = 共有リポジトリ群
diff --git a/src/main/distrib/linux/add-indexed-branch.sh b/src/main/distrib/linux/add-indexed-branch.sh
index 0d29e5d3..0285d1bb 100644
--- a/src/main/distrib/linux/add-indexed-branch.sh
+++ b/src/main/distrib/linux/add-indexed-branch.sh
@@ -16,6 +16,6 @@
# Set EXCLUSIONS for any repositories that you do not want to change
# --------------------------------------------------------------------------
FOLDER=data/git
-EXCLUSIONS=--skip test.git --skip group/test*
+EXCLUSIONS="--skip test.git --skip group/test*"
BRANCH=default
-java -cp gitblit.jar:"ext/*" com.gitblit.AddIndexedBranch --repositoriesFolder $FOLDER --branch $BRANCH $EXCLUSIONS
+java -cp gitblit.jar:"ext/*" com.gitblit.AddIndexedBranch --repositoriesFolder "$FOLDER" --branch "$BRANCH" "$EXCLUSIONS"
diff --git a/src/main/distrib/linux/authority.sh b/src/main/distrib/linux/authority.sh
index ce5c2377..c5c6c687 100644
--- a/src/main/distrib/linux/authority.sh
+++ b/src/main/distrib/linux/authority.sh
@@ -1,2 +1,2 @@
#!/bin/bash
-java -cp gitblit.jar com.gitblit.authority.Launcher --baseFolder data
+java -cp "gitblit.jar:ext/*" com.gitblit.authority.GitblitAuthority --baseFolder data
diff --git a/src/main/distrib/linux/gitblit-stop.sh b/src/main/distrib/linux/gitblit-stop.sh
index 2fef2034..2a774644 100644
--- a/src/main/distrib/linux/gitblit-stop.sh
+++ b/src/main/distrib/linux/gitblit-stop.sh
@@ -1,2 +1,2 @@
#!/bin/bash
-java -jar gitblit.jar --baseFolder data --stop
+java -cp "gitblit.jar:ext/*" com.gitblit.GitBlitServer --baseFolder data --stop
diff --git a/src/main/distrib/linux/gitblit.sh b/src/main/distrib/linux/gitblit.sh
index 7d631e72..2a52e24e 100644
--- a/src/main/distrib/linux/gitblit.sh
+++ b/src/main/distrib/linux/gitblit.sh
@@ -1,2 +1,2 @@
#!/bin/bash
-java -jar gitblit.jar --baseFolder data
+java -cp "gitblit.jar:ext/*" com.gitblit.GitBlitServer --baseFolder data
diff --git a/src/main/distrib/linux/install-service-fedora.sh b/src/main/distrib/linux/install-service-fedora.sh
index 4fb43c61..df17590f 100755
--- a/src/main/distrib/linux/install-service-fedora.sh
+++ b/src/main/distrib/linux/install-service-fedora.sh
@@ -18,16 +18,16 @@ After=network.target
[Service]
User=gitblit
Group=gitblit
-Environment="ARGS=-server -Xmx1024M -Djava.awt.headless=true -jar"
+Environment="ARGS=-server -Xmx1024M -Djava.awt.headless=true -cp"
EnvironmentFile=-/etc/sysconfig/gitblit
WorkingDirectory=/opt/gitblit
-ExecStart=/usr/bin/java \$ARGS gitblit.jar --httpsPort \$GITBLIT_HTTPS_PORT --httpPort \$GITBLIT_HTTP_PORT --baseFolder \$GITBLIT_BASE_FOLDER --dailyLogFile
-ExecStop=/usr/bin/java \$ARGS gitblit.jar --baseFolder \$GITBLIT_BASE_FOLDER --stop
+ExecStart=/usr/bin/java \$ARGS gitblit.jar:ext/* com.gitblit.GitBlitServer --httpsPort \$GITBLIT_HTTPS_PORT --httpPort \$GITBLIT_HTTP_PORT --baseFolder \$GITBLIT_BASE_FOLDER --dailyLogFile
+ExecStop=/usr/bin/java \$ARGS gitblit.jar:ext/* com.gitblit.GitBlitServer --baseFolder \$GITBLIT_BASE_FOLDER --stop
[Install]
WantedBy=multi-user.target
EOF
# Finally copy the files to the destination and register the systemd unit.
-sudo su -c "cp /tmp/gitblit.defaults /etc/sysconfig/gitblit && cp /tmp/gitblit.service /etc/systemd/system/"
-sudo su -c "systemctl daemon-reload && systemctl enable gitblit.service && systemctl start gitblit.service"
+sudo sh -c "cp /tmp/gitblit.defaults /etc/sysconfig/gitblit && cp /tmp/gitblit.service /etc/systemd/system/"
+sudo sh -c "systemctl daemon-reload && systemctl enable gitblit.service && systemctl start gitblit.service"
diff --git a/src/main/distrib/linux/install-service-freebsd.sh b/src/main/distrib/linux/install-service-freebsd.sh
new file mode 100644
index 00000000..727779cf
--- /dev/null
+++ b/src/main/distrib/linux/install-service-freebsd.sh
@@ -0,0 +1,2 @@
+#!/bin/bash
+sudo cp service-freebsd.sh /usr/local/etc/rc.d/gitblit
diff --git a/src/main/distrib/linux/migrate-tickets.sh b/src/main/distrib/linux/migrate-tickets.sh
index f521528e..0e2829a8 100644
--- a/src/main/distrib/linux/migrate-tickets.sh
+++ b/src/main/distrib/linux/migrate-tickets.sh
@@ -8,7 +8,7 @@
#
# --------------------------------------------------------------------------
-if [[ -z $1 || -z $2 ]]; then
+if [ -z $1 ] || [ -z $2 ]; then
echo "Please specify the output ticket service and your baseFolder!";
echo "";
echo "usage:";
@@ -17,5 +17,5 @@ if [[ -z $1 || -z $2 ]]; then
exit 1;
fi
-java -cp gitblit.jar:./ext/* com.gitblit.MigrateTickets $1 --baseFolder $2
+java -cp "gitblit.jar:ext/*" com.gitblit.MigrateTickets "$1" --baseFolder "$2"
diff --git a/src/main/distrib/linux/reindex-tickets.sh b/src/main/distrib/linux/reindex-tickets.sh
index 8261b819..ba808fa6 100644
--- a/src/main/distrib/linux/reindex-tickets.sh
+++ b/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:";
@@ -20,5 +20,5 @@ if [[ -z $1 ]]; then
exit 1;
fi
-java -cp gitblit.jar:./ext/* com.gitblit.ReindexTickets --baseFolder $1
+java -cp "gitblit.jar:ext/*" com.gitblit.ReindexTickets --baseFolder "$1"
diff --git a/src/main/distrib/linux/service-centos.sh b/src/main/distrib/linux/service-centos.sh
index 843f015a..a2645e7e 100644
--- a/src/main/distrib/linux/service-centos.sh
+++ b/src/main/distrib/linux/service-centos.sh
@@ -11,7 +11,7 @@ GITBLIT_HTTP_PORT=0
GITBLIT_HTTPS_PORT=8443
GITBLIT_LOG=/var/log/gitblit.log
source ${GITBLIT_PATH}/java-proxy-config.sh
-JAVA="java -server -Xmx1024M ${JAVA_PROXY_CONFIG} -Djava.awt.headless=true -jar"
+JAVA="java -server -Xmx1024M ${JAVA_PROXY_CONFIG} -Djava.awt.headless=true -cp"
RETVAL=0
@@ -21,7 +21,7 @@ case "$1" in
then
echo $"Starting gitblit server"
cd $GITBLIT_PATH
- $JAVA $GITBLIT_PATH/gitblit.jar --httpsPort $GITBLIT_HTTPS_PORT --httpPort $GITBLIT_HTTP_PORT --baseFolder $GITBLIT_BASE_FOLDER --dailyLogFile &
+ $JAVA "$GITBLIT_PATH/gitblit.jar:$GITBLIT_PATH/ext/*" com.gitblit.GitBlitServer --httpsPort $GITBLIT_HTTPS_PORT --httpPort $GITBLIT_HTTP_PORT --baseFolder $GITBLIT_BASE_FOLDER --dailyLogFile &
echo "."
exit $RETVAL
fi
@@ -32,7 +32,7 @@ case "$1" in
then
echo $"Stopping gitblit server"
cd $GITBLIT_PATH
- $JAVA $GITBLIT_PATH/gitblit.jar --baseFolder $GITBLIT_BASE_FOLDER --stop > /dev/null &
+ $JAVA "$GITBLIT_PATH/gitblit.jar:$GITBLIT_PATH/ext/*" com.gitblit.GitBlitServer --baseFolder $GITBLIT_BASE_FOLDER --stop > /dev/null &
echo "."
exit $RETVAL
fi
diff --git a/src/main/distrib/linux/service-freebsd.sh b/src/main/distrib/linux/service-freebsd.sh
new file mode 100644
index 00000000..513e64c4
--- /dev/null
+++ b/src/main/distrib/linux/service-freebsd.sh
@@ -0,0 +1,49 @@
+#!/bin/sh
+
+# PROVIDE: gitblit
+# BEFORE: LOGIN
+# KEYWORD: shutdown
+
+PATH="/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin"
+
+. /etc/rc.subr
+
+name="gitblit"
+rcvar="gitblit_enable"
+
+pidfile="/var/run/${name}.pid"
+
+start_cmd="${name}_start"
+stop_cmd="${name}_stop"
+restart_cmd="${name}_restart"
+
+
+# change theses values (default values)
+GITBLIT_PATH=/opt/gitblit
+GITBLIT_BASE_FOLDER=/opt/gitblit/data
+. ${GITBLIT_PATH}/java-proxy-config.sh
+COMMAND_LINE="java -server -Xmx1024M ${JAVA_PROXY_CONFIG} -Djava.awt.headless=true -cp gitblit.jar:ext/* com.gitblit.GitBlitServer --baseFolder $GITBLIT_BASE_FOLDER"
+
+gitblit_start()
+{
+ echo "Starting Gitblit Server..."
+ cd $GITBLIT_PATH
+ $COMMAND_LINE --dailyLogFile &
+}
+
+gitblit_stop()
+{
+ echo "Stopping Gitblit Server..."
+ cd $GITBLIT_PATH
+ $COMMAND_LINE --stop > /dev/null &
+}
+
+gitblit_restart()
+{
+ $0 stop
+ sleep 5
+ $0 start
+}
+
+load_rc_config $name
+run_rc_command "$1"
diff --git a/src/main/distrib/linux/service-ubuntu.sh b/src/main/distrib/linux/service-ubuntu.sh
index 769e3072..461a678c 100644
--- a/src/main/distrib/linux/service-ubuntu.sh
+++ b/src/main/distrib/linux/service-ubuntu.sh
@@ -19,7 +19,7 @@ GITBLIT_PATH=/opt/gitblit
GITBLIT_BASE_FOLDER=/opt/gitblit/data
GITBLIT_USER="gitblit"
source ${GITBLIT_PATH}/java-proxy-config.sh
-ARGS="-server -Xmx1024M ${JAVA_PROXY_CONFIG} -Djava.awt.headless=true -jar gitblit.jar --baseFolder $GITBLIT_BASE_FOLDER --dailyLogFile"
+ARGS="-server -Xmx1024M ${JAVA_PROXY_CONFIG} -Djava.awt.headless=true -cp gitblit.jar:ext/* com.gitblit.GitBlitServer --baseFolder $GITBLIT_BASE_FOLDER --dailyLogFile"
RETVAL=0
diff --git a/src/main/distrib/win/add-indexed-branch.cmd b/src/main/distrib/win/add-indexed-branch.cmd
index 37121ee6..25b46383 100644
--- a/src/main/distrib/win/add-indexed-branch.cmd
+++ b/src/main/distrib/win/add-indexed-branch.cmd
@@ -14,7 +14,11 @@
@REM Set BRANCH ("default" or fully qualified ref - i.e. refs/heads/master)
@REM Set EXCLUSIONS for any repositories that you do not want to change
@REM --------------------------------------------------------------------------
+@SETLOCAL
@SET FOLDER=data/git
@SET EXCLUSIONS=--skip test.git --skip group/test*
@SET BRANCH=default
+@PUSHD %~dp0
@java -cp gitblit.jar;"%CD%\ext\*" com.gitblit.AddIndexedBranch --repositoriesFolder %FOLDER% --branch %BRANCH% %EXCLUSIONS% %*
+@POPD
+@ENDLOCAL
diff --git a/src/main/distrib/win/authority.cmd b/src/main/distrib/win/authority.cmd
index f9a18640..a313cd73 100644
--- a/src/main/distrib/win/authority.cmd
+++ b/src/main/distrib/win/authority.cmd
@@ -1 +1,8 @@
-@java -cp gitblit.jar com.gitblit.authority.Launcher --baseFolder data %*
+@SETLOCAL
+
+@SET gbhome=%~dp0
+@SET gbhome=%gbhome:~0,-1%
+
+@java -cp "%gbhome%\gitblit.jar";"%gbhome%\ext\*" com.gitblit.authority.GitblitAuthority --baseFolder "%gbhome%\data" %*
+
+@ENDLOCAL
diff --git a/src/main/distrib/win/gitblit-stop.cmd b/src/main/distrib/win/gitblit-stop.cmd
index 34f0f4be..b2cdbc44 100644
--- a/src/main/distrib/win/gitblit-stop.cmd
+++ b/src/main/distrib/win/gitblit-stop.cmd
@@ -1 +1 @@
-@java -jar gitblit.jar --stop --baseFolder data %*
+@java -cp "%~dp0gitblit.jar";"%~dp0ext\*" com.gitblit.GitBlitServer --stop %*
diff --git a/src/main/distrib/win/gitblit.cmd b/src/main/distrib/win/gitblit.cmd
index 1a6d7e09..c02e3896 100644
--- a/src/main/distrib/win/gitblit.cmd
+++ b/src/main/distrib/win/gitblit.cmd
@@ -1 +1,8 @@
-@java -jar gitblit.jar --baseFolder data %*
+@SETLOCAL
+
+@SET gbhome=%~dp0
+@SET gbhome=%gbhome:~0,-1%
+
+@java -cp "%gbhome%\gitblit.jar";"%gbhome%\ext\*" com.gitblit.GitBlitServer --baseFolder "%gbhome%\data" %*
+
+@ENDLOCAL
diff --git a/src/main/distrib/win/installService.cmd b/src/main/distrib/win/installService.cmd
index a684ab21..23d45a46 100644
--- a/src/main/distrib/win/installService.cmd
+++ b/src/main/distrib/win/installService.cmd
@@ -8,31 +8,37 @@
@REM
@REM http://commons.apache.org/daemon/procrun.html
+@SETLOCAL
+
@REM arch = x86, amd64, or ia32
SET ARCH=amd64
+@SET gbhome=%~dp0
+@SET gbhome=%gbhome:~0,-1%
+
@REM Be careful not to introduce trailing whitespace after the ^ characters.
@REM Use ; or # to separate values in the --StartParams parameter.
-"%CD%\%ARCH%\gitblit.exe" //IS//gitblit ^
+"%gbhome%\%ARCH%\gitblit.exe" //IS//gitblit ^
--DisplayName="gitblit" ^
--Description="a pure Java Git solution" ^
--Startup=auto ^
- --LogPath="%CD%\logs" ^
+ --LogPath="%gbhome%\logs" ^
--LogLevel=INFO ^
--LogPrefix=gitblit ^
--StdOutput=auto ^
--StdError=auto ^
- --StartPath="%CD%" ^
- --StartClass=org.moxie.MxLauncher ^
+ --StartPath="%gbhome%" ^
+ --StartClass=com.gitblit.GitBlitServer ^
--StartMethod=main ^
- --StartParams="--storePassword;gitblit;--baseFolder;%CD%\data" ^
+ --StartParams="--storePassword;gitblit;--baseFolder;%gbhome%\data" ^
--StartMode=jvm ^
- --StopPath="%CD%" ^
- --StopClass=org.moxie.MxLauncher ^
+ --StopPath="%gbhome%" ^
+ --StopClass=com.gitblit.GitBlitServer ^
--StopMethod=main ^
- --StopParams="--stop;--baseFolder;%CD%\data" ^
+ --StopParams="--stop;--baseFolder;%gbhome%\data" ^
--StopMode=jvm ^
- --Classpath="%CD%\gitblit.jar" ^
+ --Classpath="%gbhome%\gitblit.jar;%gbhome%\ext\*" ^
--Jvm=auto ^
--JvmMx=1024
- \ No newline at end of file
+
+@ENDLOCAL
diff --git a/src/main/distrib/win/migrate-tickets.cmd b/src/main/distrib/win/migrate-tickets.cmd
index 5a26c8ed..e08c5a84 100644
--- a/src/main/distrib/win/migrate-tickets.cmd
+++ b/src/main/distrib/win/migrate-tickets.cmd
@@ -9,13 +9,13 @@
@if [%2]==[] goto help
-@java -cp gitblit.jar;"%CD%\ext\*" com.gitblit.MigrateTickets %1 --baseFolder %2
+@java -cp "%~dp0gitblit.jar";"%~dp0ext\*" 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
+@echo Please specify the output ticket service and your baseFolder!
+@echo.
+@echo e.g.: migrate-tickets com.gitblit.tickets.RedisTicketService "c:/gitblit data"
+@echo.
-:end \ No newline at end of file
+:end
diff --git a/src/main/distrib/win/reindex-tickets.cmd b/src/main/distrib/win/reindex-tickets.cmd
index c9116ca2..49eb122e 100644
--- a/src/main/distrib/win/reindex-tickets.cmd
+++ b/src/main/distrib/win/reindex-tickets.cmd
@@ -10,13 +10,13 @@
@REM --------------------------------------------------------------------------
@if [%1]==[] goto nobasefolder
-@java -cp gitblit.jar;"%CD%\ext\*" com.gitblit.ReindexTickets --baseFolder %1
+@java -cp "%~dp0gitblit.jar";"%~dp0ext\*" com.gitblit.ReindexTickets --baseFolder %1
@goto end
:nobasefolder
-@echo "Please specify your baseFolder!"
-@echo
-@echo " reindex-tickets c:/gitblit-data"
-@echo
+@echo Please specify your baseFolder!
+@echo.
+@echo reindex-tickets c:/gitblit-data
+@echo.
-:end \ No newline at end of file
+:end
diff --git a/src/main/distrib/win/uninstallService.cmd b/src/main/distrib/win/uninstallService.cmd
index e6c3b98c..6a574d19 100644
--- a/src/main/distrib/win/uninstallService.cmd
+++ b/src/main/distrib/win/uninstallService.cmd
@@ -1,5 +1,9 @@
+@SETLOCAL
+
@REM arch = x86, amd64, or ia32
SET ARCH=amd64
@REM Delete the gitblit service
-"%CD%\%ARCH%\gitblit.exe" //DS//gitblit \ No newline at end of file
+"%~dp0%ARCH%\gitblit.exe" //DS//gitblit
+
+@ENDLOCAL
diff --git a/src/main/java/WEB-INF/web.xml b/src/main/java/WEB-INF/web.xml
index db772153..c648dcde 100644
--- a/src/main/java/WEB-INF/web.xml
+++ b/src/main/java/WEB-INF/web.xml
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
-<web-app version="2.4"
- xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_3_0.xsd">
+<web-app version="3.0"
+ xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
<!-- The base folder is used to specify the root location of your Gitblit data.
@@ -49,4 +49,4 @@
<session-config>
<tracking-mode>COOKIE</tracking-mode>
</session-config>
-</web-app> \ No newline at end of file
+</web-app>
diff --git a/src/main/java/WEB-INF/weblogic.xml b/src/main/java/WEB-INF/weblogic.xml
index cd08111d..f41fa00d 100644
--- a/src/main/java/WEB-INF/weblogic.xml
+++ b/src/main/java/WEB-INF/weblogic.xml
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
-<wls:weblogic-web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:wls="http://www.bea.com/ns/weblogic/90" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd http://www.bea.com/ns/weblogic/90 http://www.bea.com/ns/weblogic/90/weblogic-web-app.xsd">
+<wls:weblogic-web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:wls="http://www.bea.com/ns/weblogic/90" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd http://www.bea.com/ns/weblogic/90 http://www.bea.com/ns/weblogic/90/weblogic-web-app.xsd">
<wls:weblogic-version>12.1.1</wls:weblogic-version>
<wls:context-root>gitblit</wls:context-root>
<wls:container-descriptor>
<wls:show-archived-real-path-enabled>true</wls:show-archived-real-path-enabled>
<wls:prefer-web-inf-classes>true</wls:prefer-web-inf-classes>
</wls:container-descriptor>
-</wls:weblogic-web-app> \ No newline at end of file
+</wls:weblogic-web-app>
diff --git a/src/main/java/com/gitblit/ConfigUserService.java b/src/main/java/com/gitblit/ConfigUserService.java
index 6d7230f7..22d1c190 100644
--- a/src/main/java/com/gitblit/ConfigUserService.java
+++ b/src/main/java/com/gitblit/ConfigUserService.java
@@ -676,7 +676,7 @@ public class ConfigUserService implements IUserService {
// Write a temporary copy of the users file
File realmFileCopy = new File(realmFile.getAbsolutePath() + ".tmp");
- StoredConfig config = new FileBasedConfig(realmFileCopy, FS.detect());
+ StoredUserConfig config = new StoredUserConfig(realmFileCopy);
// write users
for (UserModel model : users.values()) {
@@ -898,7 +898,7 @@ public class ConfigUserService implements IUserService {
user.countryCode = config.getString(USER, username, COUNTRYCODE);
user.cookie = config.getString(USER, username, COOKIE);
if (StringUtils.isEmpty(user.cookie) && !StringUtils.isEmpty(user.password)) {
- user.cookie = StringUtils.getSHA1(user.username + user.password);
+ user.cookie = user.createCookie();
}
// preferences
diff --git a/src/main/java/com/gitblit/Constants.java b/src/main/java/com/gitblit/Constants.java
index 6232552e..ab503bd3 100644
--- a/src/main/java/com/gitblit/Constants.java
+++ b/src/main/java/com/gitblit/Constants.java
@@ -62,6 +62,12 @@ public class Constants {
public static final String GIT_PATH = "/git/";
public static final String REGEX_SHA256 = "[a-fA-F0-9]{64}";
+
+ /**
+ * This regular expression is used when searching for "mentions" in tickets
+ * (when someone writes @thisOtherUser)
+ */
+ public static final String REGEX_TICKET_MENTION = "\\B@(?<user>[^\\s]+)\\b";
public static final String ZIP_PATH = "/zip/";
@@ -639,6 +645,37 @@ public class Constants {
}
}
+ /**
+ * The type of merge Gitblit will use when merging a ticket to the integration branch.
+ * <p>
+ * The default type is MERGE_ALWAYS.
+ * <p>
+ * This is modeled after the Gerrit SubmitType.
+ */
+ public static enum MergeType {
+ /** Allows a merge only if it can be fast-forward merged into the integration branch. */
+ FAST_FORWARD_ONLY,
+ /** Uses a fast-forward merge if possible, other wise a merge commit is created. */
+ MERGE_IF_NECESSARY,
+ // Future REBASE_IF_NECESSARY,
+ /** Always merge with a merge commit, even when a fast-forward would be possible. */
+ MERGE_ALWAYS,
+ // Future? CHERRY_PICK
+ ;
+
+ public static final MergeType DEFAULT_MERGE_TYPE = MERGE_ALWAYS;
+
+ public static MergeType fromName(String name) {
+ for (MergeType type : values()) {
+ if (type.name().equalsIgnoreCase(name)) {
+ return type;
+ }
+ }
+ return DEFAULT_MERGE_TYPE;
+ }
+ }
+
+
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface Unused {
diff --git a/src/main/java/com/gitblit/GitBlitServer.java b/src/main/java/com/gitblit/GitBlitServer.java
index 7b6cbbad..190cc5d2 100644
--- a/src/main/java/com/gitblit/GitBlitServer.java
+++ b/src/main/java/com/gitblit/GitBlitServer.java
@@ -143,7 +143,7 @@ public class GitBlitServer {
if (parser != null) {
parser.printUsage(System.out);
System.out
- .println("\nExample:\n java -server -Xmx1024M -jar gitblit.jar --repositoriesFolder c:\\git --httpPort 80 --httpsPort 443");
+ .println("\nExample:\n java -server -Xmx1024M -cp gitblit.jar:ext/* com.gitblit.GitBlitServer --repositoriesFolder /srv/git --httpPort 80 --httpsPort 443");
}
System.exit(0);
}
@@ -225,6 +225,10 @@ public class GitBlitServer {
String osversion = System.getProperty("os.version");
logger.info("Running on " + osname + " (" + osversion + ")");
+ String javaversion = System.getProperty("java.version");
+ String javavendor = System.getProperty("java.vendor");
+ logger.info("JVM version " + javaversion + " (" + javavendor + ")");
+
QueuedThreadPool threadPool = new QueuedThreadPool();
int maxThreads = settings.getInteger(Keys.server.threadPoolSize, 50);
if (maxThreads > 0) {
@@ -293,7 +297,7 @@ public class GitBlitServer {
ServerConnector connector = new ServerConnector(server, factory);
connector.setSoLingerTime(-1);
- connector.setIdleTimeout(30000);
+ connector.setIdleTimeout(settings.getLong(Keys.server.httpIdleTimeout, 30000L));
connector.setPort(params.securePort);
String bindInterface = settings.getString(Keys.server.httpsBindInterface, null);
if (!StringUtils.isEmpty(bindInterface)) {
@@ -330,7 +334,7 @@ public class GitBlitServer {
ServerConnector connector = new ServerConnector(server, new HttpConnectionFactory(httpConfig));
connector.setSoLingerTime(-1);
- connector.setIdleTimeout(30000);
+ connector.setIdleTimeout(settings.getLong(Keys.server.httpIdleTimeout, 30000L));
connector.setPort(params.port);
String bindInterface = settings.getString(Keys.server.httpBindInterface, null);
if (!StringUtils.isEmpty(bindInterface)) {
@@ -375,7 +379,8 @@ public class GitBlitServer {
HashSessionManager sessionManager = new HashSessionManager();
sessionManager.setHttpOnly(true);
// Use secure cookies if only serving https
- sessionManager.setSecureRequestOnly(params.port <= 0 && params.securePort > 0);
+ sessionManager.setSecureRequestOnly( (params.port <= 0 && params.securePort > 0) ||
+ (params.port > 0 && params.securePort > 0 && settings.getBoolean(Keys.server.redirectToHttpsPort, true)) );
rootContext.getSessionHandler().setSessionManager(sessionManager);
// Ensure there is a defined User Service
@@ -610,4 +615,4 @@ public class GitBlitServer {
public String ldapLdifFile;
}
-} \ No newline at end of file
+}
diff --git a/src/main/java/com/gitblit/IUserService.java b/src/main/java/com/gitblit/IUserService.java
index 6f3c5423..468f968f 100644
--- a/src/main/java/com/gitblit/IUserService.java
+++ b/src/main/java/com/gitblit/IUserService.java
@@ -26,6 +26,9 @@ import com.gitblit.models.UserModel;
* Implementations of IUserService control all aspects of UserModel objects and
* user authentication.
*
+ * Plugins implementing this interface (which are instantiated during {@link com.gitblit.manager.UserManager#start()}) can provide
+ * a default constructor or might also use {@link IRuntimeManager} as a constructor argument which will be passed automatically then.
+ *
* @author James Moger
*
*/
diff --git a/src/main/java/com/gitblit/Launcher.java b/src/main/java/com/gitblit/Launcher.java
deleted file mode 100644
index 68a9dbf9..00000000
--- a/src/main/java/com/gitblit/Launcher.java
+++ /dev/null
@@ -1,148 +0,0 @@
-/*
- * Copyright 2011 gitblit.com.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.gitblit;
-
-import java.io.File;
-import java.io.FileFilter;
-import java.io.IOException;
-import java.lang.reflect.Method;
-import java.net.URL;
-import java.net.URLClassLoader;
-import java.security.ProtectionDomain;
-import java.text.MessageFormat;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-
-/**
- * Launch helper class that adds all jars found in the local "lib" & "ext"
- * folders and then calls the application main. Using this technique we do not
- * have to specify a classpath and we can dynamically add jars to the
- * distribution.
- *
- * @author James Moger
- *
- */
-public class Launcher {
-
- public static final boolean DEBUG = false;
-
- /**
- * Parameters of the method to add an URL to the System classes.
- */
- private static final Class<?>[] PARAMETERS = new Class[] { URL.class };
-
- public static void main(String[] args) {
- if (DEBUG) {
- System.out.println("jcp=" + System.getProperty("java.class.path"));
- ProtectionDomain protectionDomain = Launcher.class.getProtectionDomain();
- System.out.println("launcher="
- + protectionDomain.getCodeSource().getLocation().toExternalForm());
- }
-
- // Load the JARs in the lib and ext folder
- String[] folders = new String[] { "lib", "ext" };
- List<File> jars = new ArrayList<File>();
- for (String folder : folders) {
- if (folder == null) {
- continue;
- }
- File libFolder = new File(folder);
- if (!libFolder.exists()) {
- continue;
- }
- List<File> found = findJars(libFolder.getAbsoluteFile());
- jars.addAll(found);
- }
- // sort the jars by name and then reverse the order so the newer version
- // of the library gets loaded in the event that this is an upgrade
- Collections.sort(jars);
- Collections.reverse(jars);
-
- if (jars.size() == 0) {
- for (String folder : folders) {
- File libFolder = new File(folder);
- // this is a test of adding a comment
- // more really interesting things
- System.err.println("Failed to find any JARs in " + libFolder.getPath());
- }
- System.exit(-1);
- } else {
- for (File jar : jars) {
- try {
- jar.canRead();
- addJarFile(jar);
- } catch (Throwable t) {
- t.printStackTrace();
- }
- }
- }
-
- // Start Server
- GitBlitServer.main(args);
- }
-
- public static List<File> findJars(File folder) {
- List<File> jars = new ArrayList<File>();
- if (folder.exists()) {
- File[] libs = folder.listFiles(new FileFilter() {
- @Override
- public boolean accept(File file) {
- return !file.isDirectory() && file.getName().toLowerCase().endsWith(".jar");
- }
- });
- if (libs != null && libs.length > 0) {
- jars.addAll(Arrays.asList(libs));
- if (DEBUG) {
- for (File jar : jars) {
- System.out.println("found " + jar);
- }
- }
- }
- }
-
- return jars;
- }
-
- /**
- * Adds a file to the classpath
- *
- * @param f
- * the file to be added
- * @throws IOException
- */
- public static void addJarFile(File f) throws IOException {
- if (f.getName().indexOf("-sources") > -1 || f.getName().indexOf("-javadoc") > -1) {
- // don't add source or javadoc jars to runtime classpath
- return;
- }
- URL u = f.toURI().toURL();
- if (DEBUG) {
- System.out.println("load=" + u.toExternalForm());
- }
- URLClassLoader sysloader = (URLClassLoader) ClassLoader.getSystemClassLoader();
- Class<?> sysclass = URLClassLoader.class;
- try {
- Method method = sysclass.getDeclaredMethod("addURL", PARAMETERS);
- method.setAccessible(true);
- method.invoke(sysloader, new Object[] { u });
- } catch (Throwable t) {
- throw new IOException(MessageFormat.format(
- "Error, could not add {0} to system classloader", f.getPath()), t);
- }
- }
-}
diff --git a/src/main/java/com/gitblit/MigrateTickets.java b/src/main/java/com/gitblit/MigrateTickets.java
index b08228ef..52287c45 100644
--- a/src/main/java/com/gitblit/MigrateTickets.java
+++ b/src/main/java/com/gitblit/MigrateTickets.java
@@ -116,7 +116,7 @@ public class MigrateTickets {
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");
+ .println("\nExample:\n java -cp gitblit.jar;\"%CD%/ext/*\" com.gitblit.MigrateTickets com.gitblit.tickets.RedisTicketService --baseFolder c:\\gitblit-data");
}
System.exit(0);
}
diff --git a/src/main/java/com/gitblit/ReindexTickets.java b/src/main/java/com/gitblit/ReindexTickets.java
index 858436af..12936db0 100644
--- a/src/main/java/com/gitblit/ReindexTickets.java
+++ b/src/main/java/com/gitblit/ReindexTickets.java
@@ -111,7 +111,7 @@ public class ReindexTickets {
if (parser != null) {
parser.printUsage(System.out);
System.out
- .println("\nExample:\n java -gitblit.jar com.gitblit.ReindexTickets --baseFolder c:\\gitblit-data");
+ .println("\nExample:\n java -cp gitblit.jar;\"%CD%/ext/*\" com.gitblit.ReindexTickets --baseFolder c:\\gitblit-data");
}
System.exit(0);
}
diff --git a/src/main/java/com/gitblit/StoredUserConfig.java b/src/main/java/com/gitblit/StoredUserConfig.java
new file mode 100644
index 00000000..c8f93b20
--- /dev/null
+++ b/src/main/java/com/gitblit/StoredUserConfig.java
@@ -0,0 +1,231 @@
+/*
+ * Copyright 2021 gitblit.com, Ingo Lafrenz
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.gitblit;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+/**
+ * Simple class with the only purpose to save the realm file (users.conf) in
+ * a fast efficient manner. The JGit Config classes used previously caused
+ * a massive CPU hog if the users file got bigger than about 30000 lines.
+ *
+ * @author Ingo Lafrenz
+ *
+ */
+public class StoredUserConfig {
+
+ private final File realmFileCopy;
+ private SortedMap<String, Section> sections = new TreeMap<>();
+
+ public StoredUserConfig(File realmFileCopy) {
+ this.realmFileCopy = realmFileCopy;
+ }
+
+ public void setString(final String section, final String subsection, String name, String value) {
+ String key = generateKey(section, subsection);
+ Section s = sections.get(key);
+ if (s == null) {
+ s = new Section(section, subsection);
+ sections.put(key, s);
+ }
+ s.addEntry(name, value);
+ }
+
+ public void setBoolean(String section, String subsection, String name, boolean value) {
+ setString(section, subsection, name, String.valueOf(value));
+ }
+
+ public void setStringList(String section, String subsection, String name, List<String> list) {
+ for (String value : list) {
+ setString(section, subsection, name, value);
+ }
+ }
+
+ public void save() throws IOException {
+ try (FileWriter fileWriter = new FileWriter(realmFileCopy);
+ PrintWriter printWriter = new PrintWriter(fileWriter);) {
+ for (Map.Entry<String,Section> entry : sections.entrySet()) {
+ writeSection(printWriter, entry.getKey(), entry.getValue());
+ }
+ }
+ }
+
+ private static void writeSection(PrintWriter printWriter, String key, Section section) {
+ if (section.getSubSection() == null) {
+ printWriter.printf("[%s]\n", section.getName());
+ }
+ else {
+ printWriter.printf("[%s \"%s\"]\n", section.getName(), section.getSubSection());
+ }
+ for (Entry entry : section.getEntries().values()) {
+ writeEntry(printWriter, entry.getKey(), entry.getValue());
+ }
+ }
+
+ private static void writeEntry(PrintWriter printWriter, String key, String value) {
+ printWriter.printf("\t%s = %s\n", key, escape(value));
+ }
+
+ private static String escape(String value) {
+ boolean quoteIt = false;
+ StringBuilder fixedValue = new StringBuilder(value.length() + 20);
+
+ for (char c : value.toCharArray()) {
+ switch (c) {
+ case '\n':
+ fixedValue.append("\\n");
+ break;
+
+ case '\t':
+ fixedValue.append("\\t");
+ break;
+
+ case '\b':
+ fixedValue.append("\\b");
+ break;
+
+ case '\\':
+ fixedValue.append("\\\\");
+ break;
+
+ case '"':
+ fixedValue.append("\\\"");
+ break;
+
+ case ';':
+ case '#':
+ quoteIt = true;
+ fixedValue.append(c);
+ break;
+
+ default:
+ fixedValue.append(c);
+ break;
+ }
+ }
+
+ if (quoteIt) {
+ fixedValue.insert(0,"\"");
+ fixedValue.append("\"");
+ }
+ return fixedValue.toString();
+ }
+
+ private static String generateKey(String key, String subKey) {
+ return "k:" + key + "s:" + (subKey == null ? "" : subKey);
+ }
+
+ private static class Section {
+ private final String name;
+ private final String subSection;
+ private final SortedMap<String, Entry> entries = new TreeMap<>();
+
+ public Section(String name, String subSection) {
+ this.name = name;
+ this.subSection = subSection;
+ }
+
+ public void addEntry(final String key, final String value) {
+ entries.put(generateKey(key, value), new Entry(key, value));
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getSubSection() {
+ return subSection;
+ }
+
+ public SortedMap<String, Entry> getEntries() {
+ return entries;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(name, subSection);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ Section other = (Section) obj;
+ return Objects.equals(name, other.name) && Objects.equals(subSection, other.subSection);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("Section [name=%s, subSection=%s]", name, subSection);
+ }
+
+ }
+
+ private static class Entry {
+ private final String key;
+ private final String value;
+
+ public Entry(String key, String value) {
+ this.key = key;
+ this.value = value;
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(key, value);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ Entry other = (Entry) obj;
+ return Objects.equals(key, other.key) && Objects.equals(value, other.value);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("Entry [key=%s, value=%s]", key, value);
+ }
+
+ }
+
+}
diff --git a/src/main/java/com/gitblit/auth/AuthenticationProvider.java b/src/main/java/com/gitblit/auth/AuthenticationProvider.java
index 0bfe2351..e359fd7e 100644
--- a/src/main/java/com/gitblit/auth/AuthenticationProvider.java
+++ b/src/main/java/com/gitblit/auth/AuthenticationProvider.java
@@ -78,10 +78,10 @@ public abstract class AuthenticationProvider {
public abstract AuthenticationType getAuthenticationType();
- protected void setCookie(UserModel user, char [] password) {
+ protected void setCookie(UserModel user) {
// create a user cookie
- if (StringUtils.isEmpty(user.cookie) && !ArrayUtils.isEmpty(password)) {
- user.cookie = StringUtils.getSHA1(user.username + new String(password));
+ if (StringUtils.isEmpty(user.cookie)) {
+ user.cookie = user.createCookie();
}
}
diff --git a/src/main/java/com/gitblit/auth/HtpasswdAuthProvider.java b/src/main/java/com/gitblit/auth/HtpasswdAuthProvider.java
index 2cdabf6f..3a6cb8ec 100644
--- a/src/main/java/com/gitblit/auth/HtpasswdAuthProvider.java
+++ b/src/main/java/com/gitblit/auth/HtpasswdAuthProvider.java
@@ -196,7 +196,7 @@ public class HtpasswdAuthProvider extends UsernamePasswordAuthenticationProvider
}
// create a user cookie
- setCookie(user, password);
+ setCookie(user);
// Set user attributes, hide password from backing user service.
user.password = Constants.EXTERNAL_ACCOUNT;
diff --git a/src/main/java/com/gitblit/auth/LdapAuthProvider.java b/src/main/java/com/gitblit/auth/LdapAuthProvider.java
index cc772e7b..6a2cbde2 100644
--- a/src/main/java/com/gitblit/auth/LdapAuthProvider.java
+++ b/src/main/java/com/gitblit/auth/LdapAuthProvider.java
@@ -16,9 +16,6 @@
*/
package com.gitblit.auth;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.security.GeneralSecurityException;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.HashMap;
@@ -33,26 +30,20 @@ import com.gitblit.Constants.AccountType;
import com.gitblit.Constants.Role;
import com.gitblit.Keys;
import com.gitblit.auth.AuthenticationProvider.UsernamePasswordAuthenticationProvider;
+import com.gitblit.ldap.LdapConnection;
import com.gitblit.models.TeamModel;
import com.gitblit.models.UserModel;
import com.gitblit.service.LdapSyncService;
import com.gitblit.utils.ArrayUtils;
import com.gitblit.utils.StringUtils;
import com.unboundid.ldap.sdk.Attribute;
-import com.unboundid.ldap.sdk.DereferencePolicy;
-import com.unboundid.ldap.sdk.ExtendedResult;
-import com.unboundid.ldap.sdk.LDAPConnection;
+import com.unboundid.ldap.sdk.BindResult;
import com.unboundid.ldap.sdk.LDAPException;
-import com.unboundid.ldap.sdk.LDAPSearchException;
import com.unboundid.ldap.sdk.ResultCode;
import com.unboundid.ldap.sdk.SearchRequest;
import com.unboundid.ldap.sdk.SearchResult;
import com.unboundid.ldap.sdk.SearchResultEntry;
import com.unboundid.ldap.sdk.SearchScope;
-import com.unboundid.ldap.sdk.SimpleBindRequest;
-import com.unboundid.ldap.sdk.extensions.StartTLSExtendedRequest;
-import com.unboundid.util.ssl.SSLUtil;
-import com.unboundid.util.ssl.TrustAllTrustManager;
/**
* Implementation of an LDAP user service.
@@ -107,12 +98,18 @@ public class LdapAuthProvider extends UsernamePasswordAuthenticationProvider {
if (enabled) {
logger.info("Synchronizing with LDAP @ " + settings.getRequiredString(Keys.realm.ldap.server));
final boolean deleteRemovedLdapUsers = settings.getBoolean(Keys.realm.ldap.removeDeletedUsers, true);
- LDAPConnection ldapConnection = getLdapConnection();
- if (ldapConnection != null) {
+ LdapConnection ldapConnection = new LdapConnection(settings);
+ if (ldapConnection.connect()) {
+ if (ldapConnection.bind() == null) {
+ ldapConnection.close();
+ logger.error("Cannot synchronize with LDAP.");
+ return;
+ }
+
try {
- String accountBase = settings.getString(Keys.realm.ldap.accountBase, "");
String uidAttribute = settings.getString(Keys.realm.ldap.uid, "uid");
- String accountPattern = settings.getString(Keys.realm.ldap.accountPattern, "(&(objectClass=person)(sAMAccountName=${username}))");
+ String accountBase = ldapConnection.getAccountBase();
+ String accountPattern = ldapConnection.getAccountPattern();
accountPattern = StringUtils.replace(accountPattern, "${username}", "*");
SearchResult result = doSearch(ldapConnection, accountBase, accountPattern);
@@ -163,6 +160,8 @@ public class LdapAuthProvider extends UsernamePasswordAuthenticationProvider {
final Map<String, TeamModel> userTeams = new HashMap<String, TeamModel>();
for (UserModel user : ldapUsers.values()) {
for (TeamModel userTeam : user.teams) {
+ // Is this an administrative team?
+ setAdminAttribute(userTeam);
userTeams.put(userTeam.name, userTeam);
}
}
@@ -179,66 +178,6 @@ public class LdapAuthProvider extends UsernamePasswordAuthenticationProvider {
}
}
- private LDAPConnection getLdapConnection() {
- try {
-
- URI ldapUrl = new URI(settings.getRequiredString(Keys.realm.ldap.server));
- String ldapHost = ldapUrl.getHost();
- int ldapPort = ldapUrl.getPort();
- String bindUserName = settings.getString(Keys.realm.ldap.username, "");
- String bindPassword = settings.getString(Keys.realm.ldap.password, "");
-
- LDAPConnection conn;
- if (ldapUrl.getScheme().equalsIgnoreCase("ldaps")) {
- // SSL
- SSLUtil sslUtil = new SSLUtil(new TrustAllTrustManager());
- conn = new LDAPConnection(sslUtil.createSSLSocketFactory());
- if (ldapPort == -1) {
- ldapPort = 636;
- }
- } else if (ldapUrl.getScheme().equalsIgnoreCase("ldap") || ldapUrl.getScheme().equalsIgnoreCase("ldap+tls")) {
- // no encryption or StartTLS
- conn = new LDAPConnection();
- if (ldapPort == -1) {
- ldapPort = 389;
- }
- } else {
- logger.error("Unsupported LDAP URL scheme: " + ldapUrl.getScheme());
- return null;
- }
-
- conn.connect(ldapHost, ldapPort);
-
- if (ldapUrl.getScheme().equalsIgnoreCase("ldap+tls")) {
- SSLUtil sslUtil = new SSLUtil(new TrustAllTrustManager());
- ExtendedResult extendedResult = conn.processExtendedOperation(
- new StartTLSExtendedRequest(sslUtil.createSSLContext()));
- if (extendedResult.getResultCode() != ResultCode.SUCCESS) {
- throw new LDAPException(extendedResult.getResultCode());
- }
- }
-
- if (StringUtils.isEmpty(bindUserName) && StringUtils.isEmpty(bindPassword)) {
- // anonymous bind
- conn.bind(new SimpleBindRequest());
- } else {
- // authenticated bind
- conn.bind(new SimpleBindRequest(bindUserName, bindPassword));
- }
-
- return conn;
-
- } catch (URISyntaxException e) {
- logger.error("Bad LDAP URL, should be in the form: ldap(s|+tls)://<server>:<port>", e);
- } catch (GeneralSecurityException e) {
- logger.error("Unable to create SSL Connection", e);
- } catch (LDAPException e) {
- logger.error("Error Connecting to LDAP", e);
- }
-
- return null;
- }
-
/**
* Credentials are defined in the LDAP server and can not be manipulated
* from Gitblit.
@@ -290,10 +229,7 @@ public class LdapAuthProvider extends UsernamePasswordAuthenticationProvider {
public boolean supportsRoleChanges(UserModel user, Role role) {
if (Role.ADMIN == role) {
if (!supportsTeamMembershipChanges()) {
- List<String> admins = settings.getStrings(Keys.realm.ldap.admins);
- if (admins.contains(user.username)) {
- return false;
- }
+ return false;
}
}
return true;
@@ -303,10 +239,7 @@ public class LdapAuthProvider extends UsernamePasswordAuthenticationProvider {
public boolean supportsRoleChanges(TeamModel team, Role role) {
if (Role.ADMIN == role) {
if (!supportsTeamMembershipChanges()) {
- List<String> admins = settings.getStrings(Keys.realm.ldap.admins);
- if (admins.contains("@" + team.name)) {
- return false;
- }
+ return false;
}
}
return true;
@@ -321,34 +254,33 @@ public class LdapAuthProvider extends UsernamePasswordAuthenticationProvider {
public UserModel authenticate(String username, char[] password) {
String simpleUsername = getSimpleUsername(username);
- LDAPConnection ldapConnection = getLdapConnection();
- if (ldapConnection != null) {
- try {
- boolean alreadyAuthenticated = false;
+ LdapConnection ldapConnection = new LdapConnection(settings);
+ if (ldapConnection.connect()) {
- String bindPattern = settings.getString(Keys.realm.ldap.bindpattern, "");
- if (!StringUtils.isEmpty(bindPattern)) {
- try {
- String bindUser = StringUtils.replace(bindPattern, "${username}", escapeLDAPSearchFilter(simpleUsername));
- ldapConnection.bind(bindUser, new String(password));
+ // Try to bind either to the "manager" account,
+ // or directly to the DN of the user logging in, if realm.ldap.bindpattern is configured.
+ String passwd = new String(password);
+ BindResult bindResult = null;
+ String bindPattern = settings.getString(Keys.realm.ldap.bindpattern, "");
+ if (! StringUtils.isEmpty(bindPattern)) {
+ bindResult = ldapConnection.bind(bindPattern, simpleUsername, passwd);
+ } else {
+ bindResult = ldapConnection.bind();
+ }
+ if (bindResult == null) {
+ ldapConnection.close();
+ return null;
+ }
- alreadyAuthenticated = true;
- } catch (LDAPException e) {
- return null;
- }
- }
+ try {
// Find the logging in user's DN
- String accountBase = settings.getString(Keys.realm.ldap.accountBase, "");
- String accountPattern = settings.getString(Keys.realm.ldap.accountPattern, "(&(objectClass=person)(sAMAccountName=${username}))");
- accountPattern = StringUtils.replace(accountPattern, "${username}", escapeLDAPSearchFilter(simpleUsername));
-
- SearchResult result = doSearch(ldapConnection, accountBase, accountPattern);
+ SearchResult result = ldapConnection.searchUser(simpleUsername);
if (result != null && result.getEntryCount() == 1) {
SearchResultEntry loggingInUser = result.getSearchEntries().get(0);
String loggingInUserDN = loggingInUser.getDN();
- if (alreadyAuthenticated || isAuthenticated(ldapConnection, loggingInUserDN, new String(password))) {
+ if (ldapConnection.isAuthenticated(loggingInUserDN, passwd)) {
logger.debug("LDAP authenticated: " + username);
UserModel user = null;
@@ -360,7 +292,7 @@ public class LdapAuthProvider extends UsernamePasswordAuthenticationProvider {
}
// create a user cookie
- setCookie(user, password);
+ setCookie(user);
if (!supportsTeamMembershipChanges()) {
getTeamsFromLdap(ldapConnection, simpleUsername, loggingInUser, user);
@@ -374,6 +306,8 @@ public class LdapAuthProvider extends UsernamePasswordAuthenticationProvider {
if (!supportsTeamMembershipChanges()) {
for (TeamModel userTeam : user.teams) {
+ // Is this an administrative team?
+ setAdminAttribute(userTeam);
updateTeam(userTeam);
}
}
@@ -404,10 +338,7 @@ public class LdapAuthProvider extends UsernamePasswordAuthenticationProvider {
if (!ArrayUtils.isEmpty(admins)) {
user.canAdmin = false;
for (String admin : admins) {
- if (admin.startsWith("@") && user.isTeamMember(admin.substring(1))) {
- // admin team
- user.canAdmin = true;
- } else if (user.getName().equalsIgnoreCase(admin)) {
+ if (user.getName().equalsIgnoreCase(admin)) {
// admin user
user.canAdmin = true;
}
@@ -416,6 +347,30 @@ public class LdapAuthProvider extends UsernamePasswordAuthenticationProvider {
}
}
+ /**
+ * Set the canAdmin attribute for team retrieved from LDAP.
+ * If we are not storing teams in LDAP and/or we have not defined any
+ * administrator teams, then do not change the admin flag.
+ *
+ * @param team
+ */
+ private void setAdminAttribute(TeamModel team) {
+ if (!supportsTeamMembershipChanges()) {
+ List<String> admins = settings.getStrings(Keys.realm.ldap.admins);
+ // if we have defined administrative teams, then set admin flag
+ // otherwise leave admin flag unchanged
+ if (!ArrayUtils.isEmpty(admins)) {
+ team.canAdmin = false;
+ for (String admin : admins) {
+ if (admin.startsWith("@") && team.name.equalsIgnoreCase(admin.substring(1))) {
+ // admin team
+ team.canAdmin = true;
+ }
+ }
+ }
+ }
+ }
+
private void setUserAttributes(UserModel user, SearchResultEntry userEntry) {
// Is this user an admin?
setAdminAttribute(user);
@@ -462,7 +417,7 @@ public class LdapAuthProvider extends UsernamePasswordAuthenticationProvider {
}
}
- private void getTeamsFromLdap(LDAPConnection ldapConnection, String simpleUsername, SearchResultEntry loggingInUser, UserModel user) {
+ private void getTeamsFromLdap(LdapConnection ldapConnection, String simpleUsername, SearchResultEntry loggingInUser, UserModel user) {
String loggingInUserDN = loggingInUser.getDN();
// Clear the users team memberships - we're going to get them from LDAP
@@ -471,15 +426,15 @@ public class LdapAuthProvider extends UsernamePasswordAuthenticationProvider {
String groupBase = settings.getString(Keys.realm.ldap.groupBase, "");
String groupMemberPattern = settings.getString(Keys.realm.ldap.groupMemberPattern, "(&(objectClass=group)(member=${dn}))");
- groupMemberPattern = StringUtils.replace(groupMemberPattern, "${dn}", escapeLDAPSearchFilter(loggingInUserDN));
- groupMemberPattern = StringUtils.replace(groupMemberPattern, "${username}", escapeLDAPSearchFilter(simpleUsername));
+ groupMemberPattern = StringUtils.replace(groupMemberPattern, "${dn}", LdapConnection.escapeLDAPSearchFilter(loggingInUserDN));
+ groupMemberPattern = StringUtils.replace(groupMemberPattern, "${username}", LdapConnection.escapeLDAPSearchFilter(simpleUsername));
// Fill in attributes into groupMemberPattern
for (Attribute userAttribute : loggingInUser.getAttributes()) {
- groupMemberPattern = StringUtils.replace(groupMemberPattern, "${" + userAttribute.getName() + "}", escapeLDAPSearchFilter(userAttribute.getValue()));
+ groupMemberPattern = StringUtils.replace(groupMemberPattern, "${" + userAttribute.getName() + "}", LdapConnection.escapeLDAPSearchFilter(userAttribute.getValue()));
}
- SearchResult teamMembershipResult = doSearch(ldapConnection, groupBase, true, groupMemberPattern, Arrays.asList("cn"));
+ SearchResult teamMembershipResult = searchTeamsInLdap(ldapConnection, groupBase, true, groupMemberPattern, Arrays.asList("cn"));
if (teamMembershipResult != null && teamMembershipResult.getEntryCount() > 0) {
for (int i = 0; i < teamMembershipResult.getEntryCount(); i++) {
SearchResultEntry teamEntry = teamMembershipResult.getSearchEntries().get(i);
@@ -496,12 +451,12 @@ public class LdapAuthProvider extends UsernamePasswordAuthenticationProvider {
}
}
- private void getEmptyTeamsFromLdap(LDAPConnection ldapConnection) {
+ private void getEmptyTeamsFromLdap(LdapConnection ldapConnection) {
logger.info("Start fetching empty teams from ldap.");
String groupBase = settings.getString(Keys.realm.ldap.groupBase, "");
String groupMemberPattern = settings.getString(Keys.realm.ldap.groupEmptyMemberPattern, "(&(objectClass=group)(!(member=*)))");
- SearchResult teamMembershipResult = doSearch(ldapConnection, groupBase, true, groupMemberPattern, null);
+ SearchResult teamMembershipResult = searchTeamsInLdap(ldapConnection, groupBase, true, groupMemberPattern, null);
if (teamMembershipResult != null && teamMembershipResult.getEntryCount() > 0) {
for (int i = 0; i < teamMembershipResult.getEntryCount(); i++) {
SearchResultEntry teamEntry = teamMembershipResult.getSearchEntries().get(i);
@@ -511,6 +466,7 @@ public class LdapAuthProvider extends UsernamePasswordAuthenticationProvider {
TeamModel teamModel = userManager.getTeamModel(teamName);
if (teamModel == null) {
teamModel = createTeamFromLdap(teamEntry);
+ setAdminAttribute(teamModel);
userManager.updateTeamModel(teamModel);
}
}
@@ -519,6 +475,30 @@ public class LdapAuthProvider extends UsernamePasswordAuthenticationProvider {
logger.info("Finished fetching empty teams from ldap.");
}
+
+ private SearchResult searchTeamsInLdap(LdapConnection ldapConnection, String base, boolean dereferenceAliases, String filter, List<String> attributes) {
+ SearchResult result = ldapConnection.search(base, dereferenceAliases, filter, attributes);
+ if (result == null) {
+ return null;
+ }
+
+ if (result.getResultCode() != ResultCode.SUCCESS) {
+ // Retry the search with user authorization in case we searched as a manager account that could not search for teams.
+ logger.debug("Rebinding as user to search for teams in LDAP");
+ result = null;
+ if (ldapConnection.rebindAsUser()) {
+ result = ldapConnection.search(base, dereferenceAliases, filter, attributes);
+ if (result.getResultCode() != ResultCode.SUCCESS) {
+ return null;
+ }
+ logger.info("Successful search after rebinding as user.");
+ }
+ }
+
+ return result;
+ }
+
+
private TeamModel createTeamFromLdap(SearchResultEntry teamEntry) {
TeamModel answer = new TeamModel(teamEntry.getAttributeValue("cn"));
answer.accountType = getAccountType();
@@ -527,47 +507,22 @@ public class LdapAuthProvider extends UsernamePasswordAuthenticationProvider {
return answer;
}
- private SearchResult doSearch(LDAPConnection ldapConnection, String base, String filter) {
- try {
- return ldapConnection.search(base, SearchScope.SUB, filter);
- } catch (LDAPSearchException e) {
- logger.error("Problem Searching LDAP", e);
-
- return null;
- }
- }
-
- private SearchResult doSearch(LDAPConnection ldapConnection, String base, boolean dereferenceAliases, String filter, List<String> attributes) {
+ private SearchResult doSearch(LdapConnection ldapConnection, String base, String filter) {
try {
SearchRequest searchRequest = new SearchRequest(base, SearchScope.SUB, filter);
- if (dereferenceAliases) {
- searchRequest.setDerefPolicy(DereferencePolicy.SEARCHING);
- }
- if (attributes != null) {
- searchRequest.setAttributes(attributes);
+ SearchResult result = ldapConnection.search(searchRequest);
+ if (result.getResultCode() != ResultCode.SUCCESS) {
+ return null;
}
- return ldapConnection.search(searchRequest);
-
- } catch (LDAPSearchException e) {
- logger.error("Problem Searching LDAP", e);
-
- return null;
+ return result;
} catch (LDAPException e) {
logger.error("Problem creating LDAP search", e);
return null;
}
}
- private boolean isAuthenticated(LDAPConnection ldapConnection, String userDn, String password) {
- try {
- // Binding will stop any LDAP-Injection Attacks since the searched-for user needs to bind to that DN
- ldapConnection.bind(userDn, password);
- return true;
- } catch (LDAPException e) {
- logger.error("Error authenticating user", e);
- return false;
- }
- }
+
+
/**
* Returns a simple username without any domain prefixes.
@@ -584,34 +539,6 @@ public class LdapAuthProvider extends UsernamePasswordAuthenticationProvider {
return username;
}
- // From: https://www.owasp.org/index.php/Preventing_LDAP_Injection_in_Java
- public static final String escapeLDAPSearchFilter(String filter) {
- StringBuilder sb = new StringBuilder();
- for (int i = 0; i < filter.length(); i++) {
- char curChar = filter.charAt(i);
- switch (curChar) {
- case '\\':
- sb.append("\\5c");
- break;
- case '*':
- sb.append("\\2a");
- break;
- case '(':
- sb.append("\\28");
- break;
- case ')':
- sb.append("\\29");
- break;
- case '\u0000':
- sb.append("\\00");
- break;
- default:
- sb.append(curChar);
- }
- }
- return sb.toString();
- }
-
private void configureSyncService() {
LdapSyncService ldapSyncService = new LdapSyncService(settings, this);
if (ldapSyncService.isReady()) {
@@ -624,5 +551,4 @@ public class LdapAuthProvider extends UsernamePasswordAuthenticationProvider {
logger.info("Ldap sync service is disabled.");
}
}
-
}
diff --git a/src/main/java/com/gitblit/auth/PAMAuthProvider.java b/src/main/java/com/gitblit/auth/PAMAuthProvider.java
index 46f4dd6a..b38d49df 100644
--- a/src/main/java/com/gitblit/auth/PAMAuthProvider.java
+++ b/src/main/java/com/gitblit/auth/PAMAuthProvider.java
@@ -122,7 +122,7 @@ public class PAMAuthProvider extends UsernamePasswordAuthenticationProvider {
}
// create a user cookie
- setCookie(user, password);
+ setCookie(user);
// update user attributes from UnixUser
user.accountType = getAccountType();
diff --git a/src/main/java/com/gitblit/auth/RedmineAuthProvider.java b/src/main/java/com/gitblit/auth/RedmineAuthProvider.java
index 27cece29..364aff04 100644
--- a/src/main/java/com/gitblit/auth/RedmineAuthProvider.java
+++ b/src/main/java/com/gitblit/auth/RedmineAuthProvider.java
@@ -139,7 +139,7 @@ public class RedmineAuthProvider extends UsernamePasswordAuthenticationProvider
}
// create a user cookie
- setCookie(user, password);
+ setCookie(user);
// update user attributes from Redmine
user.accountType = getAccountType();
diff --git a/src/main/java/com/gitblit/auth/SalesforceAuthProvider.java b/src/main/java/com/gitblit/auth/SalesforceAuthProvider.java
index df033c27..79c3a0c4 100644
--- a/src/main/java/com/gitblit/auth/SalesforceAuthProvider.java
+++ b/src/main/java/com/gitblit/auth/SalesforceAuthProvider.java
@@ -66,7 +66,7 @@ public class SalesforceAuthProvider extends UsernamePasswordAuthenticationProvid
user = new UserModel(simpleUsername);
}
- setCookie(user, password);
+ setCookie(user);
setUserAttributes(user, info);
updateUser(user);
diff --git a/src/main/java/com/gitblit/auth/WindowsAuthProvider.java b/src/main/java/com/gitblit/auth/WindowsAuthProvider.java
index aee51008..4c31fb15 100644
--- a/src/main/java/com/gitblit/auth/WindowsAuthProvider.java
+++ b/src/main/java/com/gitblit/auth/WindowsAuthProvider.java
@@ -153,7 +153,7 @@ public class WindowsAuthProvider extends UsernamePasswordAuthenticationProvider
}
// create a user cookie
- setCookie(user, password);
+ setCookie(user);
// update user attributes from Windows identity
user.accountType = getAccountType();
diff --git a/src/main/java/com/gitblit/authority/GitblitAuthority.java b/src/main/java/com/gitblit/authority/GitblitAuthority.java
index 5f4a7e7f..15c23a79 100644
--- a/src/main/java/com/gitblit/authority/GitblitAuthority.java
+++ b/src/main/java/com/gitblit/authority/GitblitAuthority.java
@@ -48,7 +48,9 @@ import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
+import java.util.ResourceBundle;
import javax.mail.Message;
import javax.swing.ImageIcon;
@@ -98,6 +100,7 @@ import com.gitblit.utils.X509Utils;
import com.gitblit.utils.X509Utils.RevocationReason;
import com.gitblit.utils.X509Utils.X509Log;
import com.gitblit.utils.X509Utils.X509Metadata;
+import com.gitblit.wicket.GitBlitWebSession;
/**
* Simple GUI tool for administering Gitblit client certificates.
@@ -447,7 +450,7 @@ public class GitblitAuthority extends JFrame implements X509Log {
}
File caKeystoreFile = new File(folder, X509Utils.CA_KEY_STORE);
- File zip = X509Utils.newClientBundle(metadata, caKeystoreFile, caKeystorePassword, GitblitAuthority.this);
+ File zip = X509Utils.newClientBundle(user,metadata, caKeystoreFile, caKeystorePassword, GitblitAuthority.this);
// save latest expiration date
if (ucm.expires == null || metadata.notAfter.before(ucm.expires)) {
@@ -850,9 +853,19 @@ public class GitblitAuthority extends JFrame implements X509Log {
try {
if (mail.isReady()) {
Mailing mailing = Mailing.newPlain();
- mailing.subject = "Your Gitblit client certificate for " + metadata.serverHostname;
+ if( user.getPreferences().getLocale()!=null )
+ mailing.subject = MessageFormat.format(ResourceBundle.getBundle("com.gitblit.wicket.GitBlitWebApp",user.getPreferences().getLocale()).getString("gb.emailClientCertificateSubject"), metadata.serverHostname);
+ else
+ mailing.subject = MessageFormat.format(ResourceBundle.getBundle("com.gitblit.wicket.GitBlitWebApp", Locale.ENGLISH).getString("gb.emailClientCertificateSubject") , metadata.serverHostname);
mailing.setRecipients(user.emailAddress);
- String body = X509Utils.processTemplate(new File(folder, X509Utils.CERTS + File.separator + "mail.tmpl"), metadata);
+ File fileMailTmp = null;
+ String body = null;
+ if( (fileMailTmp = new File(folder, X509Utils.CERTS + File.separator + "mail.tmpl"+"_"+user.getPreferences().getLocale())).exists())
+ body = X509Utils.processTemplate(fileMailTmp, metadata);
+ else{
+ fileMailTmp = new File(folder, X509Utils.CERTS + File.separator + "mail.tmpl");
+ body = X509Utils.processTemplate(fileMailTmp, metadata);
+ }
if (StringUtils.isEmpty(body)) {
body = MessageFormat.format("Hi {0}\n\nHere is your client certificate bundle.\nInside the zip file are installation instructions.", user.getDisplayName());
}
diff --git a/src/main/java/com/gitblit/authority/Launcher.java b/src/main/java/com/gitblit/authority/Launcher.java
deleted file mode 100644
index bffeb68e..00000000
--- a/src/main/java/com/gitblit/authority/Launcher.java
+++ /dev/null
@@ -1,166 +0,0 @@
-/*
- * Copyright 2012 gitblit.com.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.gitblit.authority;
-
-import java.awt.Color;
-import java.awt.EventQueue;
-import java.awt.FontMetrics;
-import java.awt.Graphics2D;
-import java.awt.SplashScreen;
-import java.io.File;
-import java.io.FileFilter;
-import java.io.IOException;
-import java.lang.reflect.Method;
-import java.net.URL;
-import java.net.URLClassLoader;
-import java.text.MessageFormat;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-
-import com.gitblit.Constants;
-import com.gitblit.client.Translation;
-
-/**
- * Downloads dependencies and launches Gitblit Authority.
- *
- * @author James Moger
- *
- */
-public class Launcher {
-
- public static final boolean DEBUG = false;
-
- /**
- * Parameters of the method to add an URL to the System classes.
- */
- private static final Class<?>[] PARAMETERS = new Class[] { URL.class };
-
-
- public static void main(String[] args) {
- final SplashScreen splash = SplashScreen.getSplashScreen();
-
- File libFolder = new File("ext");
- List<File> jars = findJars(libFolder.getAbsoluteFile());
-
- // sort the jars by name and then reverse the order so the newer version
- // of the library gets loaded in the event that this is an upgrade
- Collections.sort(jars);
- Collections.reverse(jars);
- for (File jar : jars) {
- try {
- updateSplash(splash, Translation.get("gb.loading") + " " + jar.getName() + "...");
- addJarFile(jar);
- } catch (IOException e) {
-
- }
- }
-
- updateSplash(splash, Translation.get("gb.starting") + " Gitblit Authority...");
- GitblitAuthority.main(args);
- }
-
- private static void updateSplash(final SplashScreen splash, final String string) {
- if (splash == null) {
- return;
- }
- try {
- EventQueue.invokeAndWait(new Runnable() {
- @Override
- public void run() {
- Graphics2D g = splash.createGraphics();
- if (g != null) {
- // Splash is 320x120
- FontMetrics fm = g.getFontMetrics();
-
- // paint startup status
- g.setColor(Color.darkGray);
- int h = fm.getHeight() + fm.getMaxDescent();
- int x = 5;
- int y = 115;
- int w = 320 - 2 * x;
- g.fillRect(x, y - h, w, h);
- g.setColor(Color.lightGray);
- g.drawRect(x, y - h, w, h);
- g.setColor(Color.WHITE);
- int xw = fm.stringWidth(string);
- g.drawString(string, x + ((w - xw) / 2), y - 5);
-
- // paint version
- String ver = "v" + Constants.getVersion();
- int vw = g.getFontMetrics().stringWidth(ver);
- g.drawString(ver, 320 - vw - 5, 34);
- g.dispose();
- splash.update();
- }
- }
- });
- } catch (Throwable t) {
- t.printStackTrace();
- }
- }
-
- public static List<File> findJars(File folder) {
- List<File> jars = new ArrayList<File>();
- if (folder.exists()) {
- File[] libs = folder.listFiles(new FileFilter() {
- @Override
- public boolean accept(File file) {
- return !file.isDirectory() && file.getName().toLowerCase().endsWith(".jar");
- }
- });
- if (libs != null && libs.length > 0) {
- jars.addAll(Arrays.asList(libs));
- if (DEBUG) {
- for (File jar : jars) {
- System.out.println("found " + jar);
- }
- }
- }
- }
-
- return jars;
- }
-
- /**
- * Adds a file to the classpath
- *
- * @param f
- * the file to be added
- * @throws IOException
- */
- public static void addJarFile(File f) throws IOException {
- if (f.getName().indexOf("-sources") > -1 || f.getName().indexOf("-javadoc") > -1) {
- // don't add source or javadoc jars to runtime classpath
- return;
- }
- URL u = f.toURI().toURL();
- if (DEBUG) {
- System.out.println("load=" + u.toExternalForm());
- }
- URLClassLoader sysloader = (URLClassLoader) ClassLoader.getSystemClassLoader();
- Class<?> sysclass = URLClassLoader.class;
- try {
- Method method = sysclass.getDeclaredMethod("addURL", PARAMETERS);
- method.setAccessible(true);
- method.invoke(sysloader, new Object[] { u });
- } catch (Throwable t) {
- throw new IOException(MessageFormat.format(
- "Error, could not add {0} to system classloader", f.getPath()), t);
- }
- }
-}
diff --git a/src/main/java/com/gitblit/client/EditUserDialog.java b/src/main/java/com/gitblit/client/EditUserDialog.java
index 676916b2..ecaed13d 100644
--- a/src/main/java/com/gitblit/client/EditUserDialog.java
+++ b/src/main/java/com/gitblit/client/EditUserDialog.java
@@ -58,8 +58,10 @@ import com.gitblit.models.RepositoryModel;
import com.gitblit.models.ServerSettings;
import com.gitblit.models.TeamModel;
import com.gitblit.models.UserModel;
+import com.gitblit.utils.PasswordHash;
import com.gitblit.utils.StringUtils;
+
public class EditUserDialog extends JDialog {
private static final long serialVersionUID = 1L;
@@ -317,8 +319,10 @@ public class EditUserDialog extends JDialog {
minLength));
return false;
}
- if (!password.toUpperCase().startsWith(StringUtils.MD5_TYPE)
- && !password.toUpperCase().startsWith(StringUtils.COMBINED_MD5_TYPE)) {
+ // What we actually test for here, is if the password has been changed. But this also catches if the password
+ // was not changed, but is stored in plain-text. Which is good because then editing the user will hash the
+ // password if by now the storage has been changed to a hashed variant.
+ if (!PasswordHash.isHashedEntry(password)) {
String cpw = new String(confirmPasswordField.getPassword());
if (cpw == null || cpw.length() != password.length()) {
error("Please confirm the password!");
@@ -330,21 +334,19 @@ public class EditUserDialog extends JDialog {
}
// change the cookie
- user.cookie = StringUtils.getSHA1(user.username + password);
-
- String type = settings.get(Keys.realm.passwordStorage).getString("md5");
- if (type.equalsIgnoreCase("md5")) {
- // store MD5 digest of password
- user.password = StringUtils.MD5_TYPE + StringUtils.getMD5(password);
- } else if (type.equalsIgnoreCase("combined-md5")) {
- // store MD5 digest of username+password
- user.password = StringUtils.COMBINED_MD5_TYPE
- + StringUtils.getMD5(user.username + password);
+ user.cookie = user.createCookie();
+
+ String type = settings.get(Keys.realm.passwordStorage).getString(PasswordHash.getDefaultType().name());
+ PasswordHash pwdHash = PasswordHash.instanceOf(type);
+ if (pwdHash != null) {
+ user.password = pwdHash.toHashedEntry(password, user.username);
} else {
- // plain-text password
+ // plain-text password.
+ // TODO: This is also used when the "realm.passwordStorage" configuration is not a valid type.
+ // This is a rather bad default, and should probably caught and changed to a secure default.
user.password = password;
}
- } else if (rename && password.toUpperCase().startsWith(StringUtils.COMBINED_MD5_TYPE)) {
+ } else if (rename && password.toUpperCase().startsWith(PasswordHash.Type.CMD5.name())) {
error("Gitblit is configured for combined-md5 password hashing. You must enter a new password on account rename.");
return false;
} else {
diff --git a/src/main/java/com/gitblit/client/GitblitManagerLauncher.java b/src/main/java/com/gitblit/client/GitblitManagerLauncher.java
deleted file mode 100644
index 8a43c472..00000000
--- a/src/main/java/com/gitblit/client/GitblitManagerLauncher.java
+++ /dev/null
@@ -1,165 +0,0 @@
-/*
- * Copyright 2011 gitblit.com.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.gitblit.client;
-
-import java.awt.Color;
-import java.awt.EventQueue;
-import java.awt.FontMetrics;
-import java.awt.Graphics2D;
-import java.awt.SplashScreen;
-import java.io.File;
-import java.io.FileFilter;
-import java.io.IOException;
-import java.lang.reflect.Method;
-import java.net.URL;
-import java.net.URLClassLoader;
-import java.text.MessageFormat;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-
-import com.gitblit.Constants;
-
-/**
- * Downloads dependencies and launches Gitblit Manager.
- *
- * @author James Moger
- *
- */
-public class GitblitManagerLauncher {
-
- public static final boolean DEBUG = false;
-
- /**
- * Parameters of the method to add an URL to the System classes.
- */
- private static final Class<?>[] PARAMETERS = new Class[] { URL.class };
-
- public static void main(String[] args) {
- final SplashScreen splash = SplashScreen.getSplashScreen();
-
- File libFolder = new File("ext");
- List<File> jars = findJars(libFolder.getAbsoluteFile());
-
- // sort the jars by name and then reverse the order so the newer version
- // of the library gets loaded in the event that this is an upgrade
- Collections.sort(jars);
- Collections.reverse(jars);
- for (File jar : jars) {
- try {
- updateSplash(splash, Translation.get("gb.loading") + " " + jar.getName() + "...");
- addJarFile(jar);
- } catch (IOException e) {
-
- }
- }
-
- updateSplash(splash, Translation.get("gb.starting") + " Gitblit Manager...");
- GitblitManager.main(args);
- }
-
- private static void updateSplash(final SplashScreen splash, final String string) {
- if (splash == null) {
- return;
- }
- try {
- EventQueue.invokeAndWait(new Runnable() {
- @Override
- public void run() {
- Graphics2D g = splash.createGraphics();
- if (g != null) {
- // Splash is 320x120
- FontMetrics fm = g.getFontMetrics();
-
- // paint startup status
- g.setColor(Color.darkGray);
- int h = fm.getHeight() + fm.getMaxDescent();
- int x = 5;
- int y = 115;
- int w = 320 - 2 * x;
- g.fillRect(x, y - h, w, h);
- g.setColor(Color.lightGray);
- g.drawRect(x, y - h, w, h);
- g.setColor(Color.WHITE);
- int xw = fm.stringWidth(string);
- g.drawString(string, x + ((w - xw) / 2), y - 5);
-
- // paint version
- String ver = "v" + Constants.getVersion();
- int vw = g.getFontMetrics().stringWidth(ver);
- g.drawString(ver, 320 - vw - 5, 34);
- g.dispose();
- splash.update();
- }
- }
- });
- } catch (Throwable t) {
- t.printStackTrace();
- }
- }
-
- public static List<File> findJars(File folder) {
- List<File> jars = new ArrayList<File>();
- if (folder.exists()) {
- File[] libs = folder.listFiles(new FileFilter() {
- @Override
- public boolean accept(File file) {
- return !file.isDirectory() && file.getName().toLowerCase().endsWith(".jar");
- }
- });
- if (libs != null && libs.length > 0) {
- jars.addAll(Arrays.asList(libs));
- if (DEBUG) {
- for (File jar : jars) {
- System.out.println("found " + jar);
- }
- }
- }
- }
-
- return jars;
- }
-
- /**
- * Adds a file to the classpath
- *
- * @param f
- * the file to be added
- * @throws IOException
- */
- public static void addJarFile(File f) throws IOException {
- if (f.getName().indexOf("-sources") > -1 || f.getName().indexOf("-javadoc") > -1) {
- // don't add source or javadoc jars to runtime classpath
- return;
- }
- URL u = f.toURI().toURL();
- if (DEBUG) {
- System.out.println("load=" + u.toExternalForm());
- }
- URLClassLoader sysloader = (URLClassLoader) ClassLoader.getSystemClassLoader();
- Class<?> sysclass = URLClassLoader.class;
- try {
- Method method = sysclass.getDeclaredMethod("addURL", PARAMETERS);
- method.setAccessible(true);
- method.invoke(sysloader, new Object[] { u });
- } catch (Throwable t) {
- throw new IOException(MessageFormat.format(
- "Error, could not add {0} to system classloader", f.getPath()), t);
- }
- }
-
-}
diff --git a/src/main/java/com/gitblit/git/PatchsetReceivePack.java b/src/main/java/com/gitblit/git/PatchsetReceivePack.java
index 33fa4705..4a09139a 100644
--- a/src/main/java/com/gitblit/git/PatchsetReceivePack.java
+++ b/src/main/java/com/gitblit/git/PatchsetReceivePack.java
@@ -599,7 +599,7 @@ public class PatchsetReceivePack extends GitblitReceivePack {
}
// ensure that the patchset can be cleanly merged right now
- MergeStatus status = JGitUtils.canMerge(getRepository(), tipCommit.getName(), forBranch);
+ MergeStatus status = JGitUtils.canMerge(getRepository(), tipCommit.getName(), forBranch, repository.mergeType);
switch (status) {
case ALREADY_MERGED:
sendError("");
@@ -1279,6 +1279,7 @@ public class PatchsetReceivePack extends GitblitReceivePack {
getRepository(),
patchset.tip,
ticket.mergeTo,
+ getRepositoryModel().mergeType,
committer,
message);
diff --git a/src/main/java/com/gitblit/ldap/LdapConnection.java b/src/main/java/com/gitblit/ldap/LdapConnection.java
new file mode 100644
index 00000000..14fedf10
--- /dev/null
+++ b/src/main/java/com/gitblit/ldap/LdapConnection.java
@@ -0,0 +1,338 @@
+/*
+ * Copyright 2016 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.gitblit.ldap;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.security.GeneralSecurityException;
+import java.util.List;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.IStoredSettings;
+import com.gitblit.Keys;
+import com.gitblit.utils.StringUtils;
+import com.unboundid.ldap.sdk.BindRequest;
+import com.unboundid.ldap.sdk.BindResult;
+import com.unboundid.ldap.sdk.DereferencePolicy;
+import com.unboundid.ldap.sdk.ExtendedResult;
+import com.unboundid.ldap.sdk.LDAPConnection;
+import com.unboundid.ldap.sdk.LDAPException;
+import com.unboundid.ldap.sdk.LDAPSearchException;
+import com.unboundid.ldap.sdk.ResultCode;
+import com.unboundid.ldap.sdk.SearchRequest;
+import com.unboundid.ldap.sdk.SearchResult;
+import com.unboundid.ldap.sdk.SearchScope;
+import com.unboundid.ldap.sdk.SimpleBindRequest;
+import com.unboundid.ldap.sdk.extensions.StartTLSExtendedRequest;
+import com.unboundid.util.ssl.SSLUtil;
+import com.unboundid.util.ssl.TrustAllTrustManager;
+
+public class LdapConnection implements AutoCloseable {
+
+ private final Logger logger = LoggerFactory.getLogger(getClass());
+
+ private IStoredSettings settings;
+
+ private LDAPConnection conn;
+ private SimpleBindRequest currentBindRequest;
+ private SimpleBindRequest managerBindRequest;
+ private SimpleBindRequest userBindRequest;
+
+
+ // From: https://www.owasp.org/index.php/Preventing_LDAP_Injection_in_Java
+ public static final String escapeLDAPSearchFilter(String filter) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < filter.length(); i++) {
+ char curChar = filter.charAt(i);
+ switch (curChar) {
+ case '\\':
+ sb.append("\\5c");
+ break;
+ case '*':
+ sb.append("\\2a");
+ break;
+ case '(':
+ sb.append("\\28");
+ break;
+ case ')':
+ sb.append("\\29");
+ break;
+ case '\u0000':
+ sb.append("\\00");
+ break;
+ default:
+ sb.append(curChar);
+ }
+ }
+ return sb.toString();
+ }
+
+
+
+ public static String getAccountBase(IStoredSettings settings) {
+ return settings.getString(Keys.realm.ldap.accountBase, "");
+ }
+
+ public static String getAccountPattern(IStoredSettings settings) {
+ return settings.getString(Keys.realm.ldap.accountPattern, "(&(objectClass=person)(sAMAccountName=${username}))");
+ }
+
+
+
+ public LdapConnection(IStoredSettings settings) {
+ this.settings = settings;
+
+ String bindUserName = settings.getString(Keys.realm.ldap.username, "");
+ String bindPassword = settings.getString(Keys.realm.ldap.password, "");
+ if (StringUtils.isEmpty(bindUserName) && StringUtils.isEmpty(bindPassword)) {
+ this.managerBindRequest = new SimpleBindRequest();
+ }
+ this.managerBindRequest = new SimpleBindRequest(bindUserName, bindPassword);
+ }
+
+
+
+ public String getAccountBase() {
+ return getAccountBase(settings);
+ }
+
+ public String getAccountPattern() {
+ return getAccountPattern(settings);
+ }
+
+
+
+ public boolean connect() {
+ try {
+ URI ldapUrl = new URI(settings.getRequiredString(Keys.realm.ldap.server));
+ String ldapHost = ldapUrl.getHost();
+ int ldapPort = ldapUrl.getPort();
+
+ if (ldapUrl.getScheme().equalsIgnoreCase("ldaps")) {
+ // SSL
+ SSLUtil sslUtil = new SSLUtil(new TrustAllTrustManager());
+ conn = new LDAPConnection(sslUtil.createSSLSocketFactory());
+ if (ldapPort == -1) {
+ ldapPort = 636;
+ }
+ } else if (ldapUrl.getScheme().equalsIgnoreCase("ldap") || ldapUrl.getScheme().equalsIgnoreCase("ldap+tls")) {
+ // no encryption or StartTLS
+ conn = new LDAPConnection();
+ if (ldapPort == -1) {
+ ldapPort = 389;
+ }
+ } else {
+ logger.error("Unsupported LDAP URL scheme: " + ldapUrl.getScheme());
+ return false;
+ }
+
+ conn.connect(ldapHost, ldapPort);
+
+ if (ldapUrl.getScheme().equalsIgnoreCase("ldap+tls")) {
+ SSLUtil sslUtil = new SSLUtil(new TrustAllTrustManager());
+ ExtendedResult extendedResult = conn.processExtendedOperation(
+ new StartTLSExtendedRequest(sslUtil.createSSLContext()));
+ if (extendedResult.getResultCode() != ResultCode.SUCCESS) {
+ throw new LDAPException(extendedResult.getResultCode());
+ }
+ }
+
+ return true;
+
+ } catch (URISyntaxException e) {
+ logger.error("Bad LDAP URL, should be in the form: ldap(s|+tls)://<server>:<port>", e);
+ } catch (GeneralSecurityException e) {
+ logger.error("Unable to create SSL Connection", e);
+ } catch (LDAPException e) {
+ logger.error("Error Connecting to LDAP", e);
+ }
+
+ return false;
+ }
+
+
+ public void close() {
+ if (conn != null) {
+ conn.close();
+ }
+ }
+
+
+
+ /**
+ * Bind using the manager credentials set in realm.ldap.username and ..password
+ * @return A bind result, or null if binding failed.
+ */
+ public BindResult bind() {
+ BindResult result = null;
+ try {
+ result = conn.bind(managerBindRequest);
+ currentBindRequest = managerBindRequest;
+ } catch (LDAPException e) {
+ logger.error("Error authenticating to LDAP with manager account to search the directory.");
+ logger.error(" Please check your settings for realm.ldap.username and realm.ldap.password.");
+ logger.debug(" Received exception when binding to LDAP", e);
+ return null;
+ }
+ return result;
+ }
+
+
+ /**
+ * Bind using the given credentials, by filling in the username in the given {@code bindPattern} to
+ * create the DN.
+ * @return A bind result, or null if binding failed.
+ */
+ public BindResult bind(String bindPattern, String simpleUsername, String password) {
+ BindResult result = null;
+ try {
+ String bindUser = StringUtils.replace(bindPattern, "${username}", escapeLDAPSearchFilter(simpleUsername));
+ SimpleBindRequest request = new SimpleBindRequest(bindUser, password);
+ result = conn.bind(request);
+ userBindRequest = request;
+ currentBindRequest = userBindRequest;
+ } catch (LDAPException e) {
+ logger.error("Error authenticating to LDAP with user account to search the directory.");
+ logger.error(" Please check your settings for realm.ldap.bindpattern.");
+ logger.debug(" Received exception when binding to LDAP", e);
+ return null;
+ }
+ return result;
+ }
+
+
+ public boolean rebindAsUser() {
+ if (userBindRequest == null || currentBindRequest == userBindRequest) {
+ return false;
+ }
+ try {
+ conn.bind(userBindRequest);
+ currentBindRequest = userBindRequest;
+ } catch (LDAPException e) {
+ conn.close();
+ logger.error("Error rebinding to LDAP with user account.", e);
+ return false;
+ }
+ return true;
+ }
+
+
+
+ public boolean isAuthenticated(String userDn, String password) {
+ verifyCurrentBinding();
+
+ // If the currently bound DN is already the DN of the logging in user, authentication has already happened
+ // during the previous bind operation. We accept this and return with the current bind left in place.
+ // This could also be changed to always retry binding as the logging in user, to make sure that the
+ // connection binding has not been tampered with in between. So far I see no way how this could happen
+ // and thus skip the repeated binding.
+ // This check also makes sure that the DN in realm.ldap.bindpattern actually matches the DN that was found
+ // when searching the user entry.
+ String boundDN = currentBindRequest.getBindDN();
+ if (boundDN != null && boundDN.equals(userDn)) {
+ return true;
+ }
+
+ // Bind a the logging in user to check for authentication.
+ // Afterwards, bind as the original bound DN again, to restore the previous authorization.
+ boolean isAuthenticated = false;
+ try {
+ // Binding will stop any LDAP-Injection Attacks since the searched-for user needs to bind to that DN
+ SimpleBindRequest ubr = new SimpleBindRequest(userDn, password);
+ conn.bind(ubr);
+ isAuthenticated = true;
+ userBindRequest = ubr;
+ } catch (LDAPException e) {
+ logger.error("Error authenticating user ({})", userDn, e);
+ }
+
+ try {
+ conn.bind(currentBindRequest);
+ } catch (LDAPException e) {
+ logger.error("Error reinstating original LDAP authorization (code {}). Team information may be inaccurate for this log in.",
+ e.getResultCode(), e);
+ }
+ return isAuthenticated;
+ }
+
+
+
+
+ public SearchResult search(SearchRequest request) {
+ try {
+ return conn.search(request);
+ } catch (LDAPSearchException e) {
+ logger.error("Problem Searching LDAP [{}]", e.getResultCode());
+ return e.getSearchResult();
+ }
+ }
+
+
+ public SearchResult search(String base, boolean dereferenceAliases, String filter, List<String> attributes) {
+ try {
+ SearchRequest searchRequest = new SearchRequest(base, SearchScope.SUB, filter);
+ if (dereferenceAliases) {
+ searchRequest.setDerefPolicy(DereferencePolicy.SEARCHING);
+ }
+ if (attributes != null) {
+ searchRequest.setAttributes(attributes);
+ }
+ SearchResult result = search(searchRequest);
+ return result;
+
+ } catch (LDAPException e) {
+ logger.error("Problem creating LDAP search", e);
+ return null;
+ }
+ }
+
+
+ public SearchResult searchUser(String username, List<String> attributes) {
+
+ String accountPattern = getAccountPattern();
+ accountPattern = StringUtils.replace(accountPattern, "${username}", escapeLDAPSearchFilter(username));
+
+ return search(getAccountBase(), false, accountPattern, attributes);
+ }
+
+
+ public SearchResult searchUser(String username) {
+ return searchUser(username, null);
+ }
+
+
+
+ private boolean verifyCurrentBinding() {
+ BindRequest lastBind = conn.getLastBindRequest();
+ if (lastBind == currentBindRequest) {
+ return true;
+ }
+ logger.debug("Unexpected binding in LdapConnection. {} != {}", lastBind, currentBindRequest);
+
+ String lastBoundDN = ((SimpleBindRequest)lastBind).getBindDN();
+ String boundDN = currentBindRequest.getBindDN();
+ logger.debug("Currently bound as '{}', check authentication for '{}'", lastBoundDN, boundDN);
+ if (boundDN != null && ! boundDN.equals(lastBoundDN)) {
+ logger.warn("Unexpected binding DN in LdapConnection. '{}' != '{}'.", lastBoundDN, boundDN);
+ logger.warn("Updated binding information in LDAP connection.");
+ currentBindRequest = (SimpleBindRequest)lastBind;
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/src/main/java/com/gitblit/manager/AuthenticationManager.java b/src/main/java/com/gitblit/manager/AuthenticationManager.java
index 49787631..68c83dae 100644
--- a/src/main/java/com/gitblit/manager/AuthenticationManager.java
+++ b/src/main/java/com/gitblit/manager/AuthenticationManager.java
@@ -18,10 +18,7 @@ package com.gitblit.manager;
import java.nio.charset.Charset;
import java.security.Principal;
import java.text.MessageFormat;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
import java.util.concurrent.TimeUnit;
import javax.servlet.http.Cookie;
@@ -52,6 +49,7 @@ import com.gitblit.models.UserModel;
import com.gitblit.transport.ssh.SshKey;
import com.gitblit.utils.Base64;
import com.gitblit.utils.HttpUtils;
+import com.gitblit.utils.PasswordHash;
import com.gitblit.utils.StringUtils;
import com.gitblit.utils.X509Utils.X509Metadata;
import com.google.inject.Inject;
@@ -454,7 +452,6 @@ public class AuthenticationManager implements IAuthenticationManager {
/**
* Authenticate a user based on a username and password.
*
- * @see IUserService.authenticate(String, char[])
* @param username
* @param password
* @return a user object or null
@@ -473,34 +470,39 @@ public class AuthenticationManager implements IAuthenticationManager {
}
String usernameDecoded = StringUtils.decodeUsername(username);
- String pw = new String(password);
- if (StringUtils.isEmpty(pw)) {
+ if (StringUtils.isEmpty(password)) {
// can not authenticate empty password
return null;
}
UserModel user = userManager.getUserModel(usernameDecoded);
- // try local authentication
- if (user != null && user.isLocalAccount()) {
- UserModel returnedUser = authenticateLocal(user, password);
- if (returnedUser != null) {
- // user authenticated
- return returnedUser;
- }
- } else {
- // try registered external authentication providers
- for (AuthenticationProvider provider : authenticationProviders) {
- if (provider instanceof UsernamePasswordAuthenticationProvider) {
- UserModel returnedUser = provider.authenticate(usernameDecoded, password);
- if (returnedUser != null) {
- // user authenticated
- returnedUser.accountType = provider.getAccountType();
- return validateAuthentication(returnedUser, AuthenticationType.CREDENTIALS);
+ try {
+ // try local authentication
+ if (user != null && user.isLocalAccount()) {
+ UserModel returnedUser = authenticateLocal(user, password);
+ if (returnedUser != null) {
+ // user authenticated
+ return returnedUser;
+ }
+ } else {
+ // try registered external authentication providers
+ for (AuthenticationProvider provider : authenticationProviders) {
+ if (provider instanceof UsernamePasswordAuthenticationProvider) {
+ UserModel returnedUser = provider.authenticate(usernameDecoded, password);
+ if (returnedUser != null) {
+ // user authenticated
+ returnedUser.accountType = provider.getAccountType();
+ return validateAuthentication(returnedUser, AuthenticationType.CREDENTIALS);
+ }
}
}
}
}
+ finally {
+ // Zero out password array to delete password from memory
+ Arrays.fill(password, Character.MIN_VALUE);
+ }
// could not authenticate locally or with a provider
logger.warn(MessageFormat.format("Failed login attempt for {0}, invalid credentials from {1}", username,
@@ -518,26 +520,63 @@ public class AuthenticationManager implements IAuthenticationManager {
*/
protected UserModel authenticateLocal(UserModel user, char [] password) {
UserModel returnedUser = null;
- if (user.password.startsWith(StringUtils.MD5_TYPE)) {
- // password digest
- String md5 = StringUtils.MD5_TYPE + StringUtils.getMD5(new String(password));
- if (user.password.equalsIgnoreCase(md5)) {
+
+ // Create a copy of the password that we can use to rehash to upgrade to a more secure hashing method.
+ // This is done to be independent from the implementation of the PasswordHash, which might already clear out
+ // the password it gets passed in. This looks a bit stupid, as we could simply clean up the mess, but this
+ // falls under "better safe than sorry".
+ char[] pwdToUpgrade = Arrays.copyOf(password, password.length);
+ try {
+ PasswordHash pwdHash = PasswordHash.instanceFor(user.password);
+ if (pwdHash != null) {
+ if (pwdHash.matches(user.password, password, user.username)) {
+ returnedUser = user;
+ }
+ } else if (user.password.equals(new String(password))) {
+ // plain-text password
returnedUser = user;
}
- } else if (user.password.startsWith(StringUtils.COMBINED_MD5_TYPE)) {
- // username+password digest
- String md5 = StringUtils.COMBINED_MD5_TYPE
- + StringUtils.getMD5(user.username.toLowerCase() + new String(password));
- if (user.password.equalsIgnoreCase(md5)) {
- returnedUser = user;
+
+ // validate user
+ returnedUser = validateAuthentication(returnedUser, AuthenticationType.CREDENTIALS);
+
+ // try to upgrade the stored password hash to a stronger hash, if necessary
+ upgradeStoredPassword(returnedUser, pwdToUpgrade, pwdHash);
+ }
+ finally {
+ // Now we make sure that the password is zeroed out in any case.
+ Arrays.fill(password, Character.MIN_VALUE);
+ Arrays.fill(pwdToUpgrade, Character.MIN_VALUE);
+ }
+
+ return returnedUser;
+ }
+
+ /**
+ * Upgrade stored password to a strong hash if configured.
+ *
+ * @param user the user to be updated
+ * @param password the password
+ * @param pwdHash
+ * Instance of PasswordHash for the stored password entry. If null, no current hashing is assumed.
+ */
+ private void upgradeStoredPassword(UserModel user, char[] password, PasswordHash pwdHash) {
+ // check if user has successfully authenticated i.e. is not null
+ if (user == null) return;
+
+ // check if strong hash algorithm is configured
+ String algorithm = settings.getString(Keys.realm.passwordStorage, PasswordHash.getDefaultType().name());
+ if (pwdHash == null || pwdHash.needsUpgradeTo(algorithm)) {
+ // rehash the provided correct password and update the user model
+ pwdHash = PasswordHash.instanceOf(algorithm);
+ if (pwdHash != null) { // necessary since the algorithm name could be something not supported.
+ user.password = pwdHash.toHashedEntry(password, user.username);
+ userManager.updateUserModel(user);
}
- } else if (user.password.equals(new String(password))) {
- // plain-text password
- returnedUser = user;
}
- return validateAuthentication(returnedUser, AuthenticationType.CREDENTIALS);
}
+
/**
* Returns the Gitlbit cookie in the request.
*
@@ -608,6 +647,11 @@ public class AuthenticationManager implements IAuthenticationManager {
userCookie = new Cookie(Constants.NAME, cookie);
// expire the cookie in 7 days
userCookie.setMaxAge((int) TimeUnit.DAYS.toSeconds(7));
+
+ // Set cookies HttpOnly so they are not accessible to JavaScript engines
+ userCookie.setHttpOnly(true);
+ // Set secure cookie if only HTTPS is used
+ userCookie.setSecure(httpsOnly());
}
}
String path = "/";
@@ -622,6 +666,15 @@ public class AuthenticationManager implements IAuthenticationManager {
}
}
+
+ private boolean httpsOnly() {
+ int port = settings.getInteger(Keys.server.httpPort, 0);
+ int tlsPort = settings.getInteger(Keys.server.httpsPort, 0);
+ return (port <= 0 && tlsPort > 0) ||
+ (port > 0 && tlsPort > 0 && settings.getBoolean(Keys.server.redirectToHttpsPort, true) );
+ }
+
+
/**
* Logout a user.
*
diff --git a/src/main/java/com/gitblit/manager/RepositoryManager.java b/src/main/java/com/gitblit/manager/RepositoryManager.java
index e9bf5b84..2be65873 100644
--- a/src/main/java/com/gitblit/manager/RepositoryManager.java
+++ b/src/main/java/com/gitblit/manager/RepositoryManager.java
@@ -63,6 +63,7 @@ import com.gitblit.Constants.AccessRestrictionType;
import com.gitblit.Constants.AuthorizationControl;
import com.gitblit.Constants.CommitMessageRenderer;
import com.gitblit.Constants.FederationStrategy;
+import com.gitblit.Constants.MergeType;
import com.gitblit.Constants.PermissionType;
import com.gitblit.Constants.RegistrantType;
import com.gitblit.GitBlitException;
@@ -899,6 +900,7 @@ public class RepositoryManager implements IRepositoryManager {
model.acceptNewTickets = getConfig(config, "acceptNewTickets", true);
model.requireApproval = getConfig(config, "requireApproval", settings.getBoolean(Keys.tickets.requireApproval, false));
model.mergeTo = getConfig(config, "mergeTo", null);
+ model.mergeType = MergeType.fromName(getConfig(config, "mergeType", settings.getString(Keys.tickets.mergeType, null)));
model.useIncrementalPushTags = getConfig(config, "useIncrementalPushTags", false);
model.incrementalPushTagPrefix = getConfig(config, "incrementalPushTagPrefix", null);
model.allowForks = getConfig(config, "allowForks", true);
@@ -1557,6 +1559,13 @@ public class RepositoryManager implements IRepositoryManager {
if (!StringUtils.isEmpty(repository.mergeTo)) {
config.setString(Constants.CONFIG_GITBLIT, null, "mergeTo", repository.mergeTo);
}
+ if (repository.mergeType == null || repository.mergeType == MergeType.fromName(settings.getString(Keys.tickets.mergeType, null))) {
+ // use default
+ config.unset(Constants.CONFIG_GITBLIT, null, "mergeType");
+ } else {
+ // override default
+ config.setString(Constants.CONFIG_GITBLIT, null, "mergeType", repository.mergeType.name());
+ }
config.setBoolean(Constants.CONFIG_GITBLIT, null, "useIncrementalPushTags", repository.useIncrementalPushTags);
if (StringUtils.isEmpty(repository.incrementalPushTagPrefix) ||
repository.incrementalPushTagPrefix.equals(settings.getString(Keys.git.defaultIncrementalPushTagPrefix, "r"))) {
@@ -1952,39 +1961,47 @@ public class RepositoryManager implements IRepositoryManager {
}
protected void configureCommitCache() {
- int daysToCache = settings.getInteger(Keys.web.activityCacheDays, 14);
+ final int daysToCache = settings.getInteger(Keys.web.activityCacheDays, 14);
if (daysToCache <= 0) {
logger.info("Commit cache is disabled");
- } else {
- long start = System.nanoTime();
- long repoCount = 0;
- long commitCount = 0;
- logger.info(MessageFormat.format("Preparing {0} day commit cache. please wait...", daysToCache));
- CommitCache.instance().setCacheDays(daysToCache);
- Date cutoff = CommitCache.instance().getCutoffDate();
- for (String repositoryName : getRepositoryList()) {
- RepositoryModel model = getRepositoryModel(repositoryName);
- if (model != null && model.hasCommits && model.lastChange.after(cutoff)) {
- repoCount++;
- Repository repository = getRepository(repositoryName);
- for (RefModel ref : JGitUtils.getLocalBranches(repository, true, -1)) {
- if (!ref.getDate().after(cutoff)) {
- // branch not recently updated
- continue;
- }
- List<?> commits = CommitCache.instance().getCommits(repositoryName, repository, ref.getName());
- if (commits.size() > 0) {
- logger.info(MessageFormat.format(" cached {0} commits for {1}:{2}",
- commits.size(), repositoryName, ref.getName()));
- commitCount += commits.size();
+ return;
+ }
+ logger.info(MessageFormat.format("Preparing {0} day commit cache...", daysToCache));
+ CommitCache.instance().setCacheDays(daysToCache);
+ Thread loader = new Thread() {
+ @Override
+ public void run() {
+ long start = System.nanoTime();
+ long repoCount = 0;
+ long commitCount = 0;
+ Date cutoff = CommitCache.instance().getCutoffDate();
+ for (String repositoryName : getRepositoryList()) {
+ RepositoryModel model = getRepositoryModel(repositoryName);
+ if (model != null && model.hasCommits && model.lastChange.after(cutoff)) {
+ repoCount++;
+ Repository repository = getRepository(repositoryName);
+ for (RefModel ref : JGitUtils.getLocalBranches(repository, true, -1)) {
+ if (!ref.getDate().after(cutoff)) {
+ // branch not recently updated
+ continue;
+ }
+ List<?> commits = CommitCache.instance().getCommits(repositoryName, repository, ref.getName());
+ if (commits.size() > 0) {
+ logger.info(MessageFormat.format(" cached {0} commits for {1}:{2}",
+ commits.size(), repositoryName, ref.getName()));
+ commitCount += commits.size();
+ }
}
+ repository.close();
}
- repository.close();
}
+ logger.info(MessageFormat.format("built {0} day commit cache of {1} commits across {2} repositories in {3} msecs",
+ daysToCache, commitCount, repoCount, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start)));
}
- logger.info(MessageFormat.format("built {0} day commit cache of {1} commits across {2} repositories in {3} msecs",
- daysToCache, commitCount, repoCount, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start)));
- }
+ };
+ loader.setName("CommitCacheLoader");
+ loader.setDaemon(true);
+ loader.start();
}
protected void confirmWriteAccess() {
diff --git a/src/main/java/com/gitblit/manager/RuntimeManager.java b/src/main/java/com/gitblit/manager/RuntimeManager.java
index 18d6b9c2..f0cca514 100644
--- a/src/main/java/com/gitblit/manager/RuntimeManager.java
+++ b/src/main/java/com/gitblit/manager/RuntimeManager.java
@@ -22,6 +22,7 @@ import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
+import com.gitblit.utils.ContainerDetector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -78,6 +79,7 @@ public class RuntimeManager implements IRuntimeManager {
logTimezone("App timezone: ", getTimezone());
logger.info("JVM locale : " + Locale.getDefault());
logger.info("App locale : " + (getLocale() == null ? "<client>" : getLocale()));
+ ContainerDetector.report(logger, true);
return this;
}
diff --git a/src/main/java/com/gitblit/manager/UserManager.java b/src/main/java/com/gitblit/manager/UserManager.java
index e88ac93c..d661c9b4 100644
--- a/src/main/java/com/gitblit/manager/UserManager.java
+++ b/src/main/java/com/gitblit/manager/UserManager.java
@@ -17,6 +17,8 @@ package com.gitblit.manager;
import java.io.File;
import java.io.IOException;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
@@ -119,8 +121,10 @@ public class UserManager implements IUserManager {
// typical file path configuration
File realmFile = runtimeManager.getFileOrFolder(Keys.realm.userService, "${baseFolder}/users.conf");
service = createUserService(realmFile);
- } catch (InstantiationException | IllegalAccessException e) {
- logger.error("failed to instantiate user service {}: {}", realm, e.getMessage());
+ } catch (InstantiationException | IllegalAccessException e) {
+ logger.error("failed to instantiate user service {}: {}. Trying once again with IRuntimeManager constructor", realm, e.getMessage());
+ //try once again with IRuntimeManager constructor. This adds support for subclasses of ConfigUserService and other custom IUserServices
+ service = createIRuntimeManagerAwareUserService(realm);
}
}
setUserService(service);
@@ -128,6 +132,22 @@ public class UserManager implements IUserManager {
return this;
}
+ /**
+ * Tries to create an {@link IUserService} with {@link #runtimeManager} as a constructor parameter
+ *
+ * @param realm the class name of the {@link IUserService} to be instantiated
+ * @return the {@link IUserService} or {@code null} if instantiation fails
+ */
+ private IUserService createIRuntimeManagerAwareUserService(String realm) {
+ try {
+ Constructor<?> constructor = Class.forName(realm).getConstructor(IRuntimeManager.class);
+ return (IUserService) constructor.newInstance(runtimeManager);
+ } catch (NoSuchMethodException | SecurityException | ClassNotFoundException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
+ logger.error("failed to instantiate user service {}: {}", realm, e.getMessage());
+ return null;
+ }
+ }
+
protected IUserService createUserService(File realmFile) {
IUserService service = null;
if (realmFile.getName().toLowerCase().endsWith(".conf")) {
diff --git a/src/main/java/com/gitblit/models/RefModel.java b/src/main/java/com/gitblit/models/RefModel.java
index d20c2dc6..43517180 100644
--- a/src/main/java/com/gitblit/models/RefModel.java
+++ b/src/main/java/com/gitblit/models/RefModel.java
@@ -36,18 +36,42 @@ import com.gitblit.utils.JGitUtils;
*/
public class RefModel implements Serializable, Comparable<RefModel> {
- private static final long serialVersionUID = 1L;
+ private static final long serialVersionUID = 876822269940583606L;
+
public final String displayName;
- public final RevObject referencedObject;
- public transient Ref reference;
+
+ private final Date date;
+ private final String name;
+ private final int type;
+ private final String id;
+ private final String referencedId;
+ private final boolean annotated;
+ private final PersonIdent person;
+ private final String shortMessage;
+ private final String fullMessage;
+
+ private transient ObjectId objectId;
+ private transient ObjectId referencedObjectId;
+
+ public transient Ref reference; // Used in too many places.
public RefModel(String displayName, Ref ref, RevObject refObject) {
- this.displayName = displayName;
this.reference = ref;
- this.referencedObject = refObject;
+ this.displayName = displayName;
+ this.date = internalGetDate(refObject);
+ this.name = ref != null ? ref.getName() : displayName;
+ this.type = internalGetReferencedObjectType(refObject);
+ this.objectId = ref != null ? ref.getObjectId() : ObjectId.zeroId();
+ this.id = this.objectId.getName();
+ this.referencedObjectId = internalGetReferencedObjectId(refObject);
+ this.referencedId = this.referencedObjectId.getName();
+ this.annotated = internalIsAnnotatedTag(ref, refObject);
+ this.person = internalGetAuthorIdent(refObject);
+ this.shortMessage = internalGetShortMessage(refObject);
+ this.fullMessage = internalGetFullMessage(refObject);
}
- public Date getDate() {
+ private Date internalGetDate(RevObject referencedObject) {
Date date = new Date(0);
if (referencedObject != null) {
if (referencedObject instanceof RevTag) {
@@ -64,14 +88,15 @@ public class RefModel implements Serializable, Comparable<RefModel> {
return date;
}
+ public Date getDate() {
+ return date;
+ }
+
public String getName() {
- if (reference == null) {
- return displayName;
- }
- return reference.getName();
+ return name;
}
- public int getReferencedObjectType() {
+ private int internalGetReferencedObjectType(RevObject referencedObject) {
int type = referencedObject.getType();
if (referencedObject instanceof RevTag) {
type = ((RevTag) referencedObject).getObject().getType();
@@ -79,14 +104,25 @@ public class RefModel implements Serializable, Comparable<RefModel> {
return type;
}
- public ObjectId getReferencedObjectId() {
+ public int getReferencedObjectType() {
+ return type;
+ }
+
+ private ObjectId internalGetReferencedObjectId(RevObject referencedObject) {
if (referencedObject instanceof RevTag) {
return ((RevTag) referencedObject).getObject().getId();
}
return referencedObject.getId();
}
- public String getShortMessage() {
+ public ObjectId getReferencedObjectId() {
+ if (referencedObjectId == null) {
+ referencedObjectId = ObjectId.fromString(referencedId);
+ }
+ return referencedObjectId;
+ }
+
+ private String internalGetShortMessage(RevObject referencedObject) {
String message = "";
if (referencedObject instanceof RevTag) {
message = ((RevTag) referencedObject).getShortMessage();
@@ -96,7 +132,11 @@ public class RefModel implements Serializable, Comparable<RefModel> {
return message;
}
- public String getFullMessage() {
+ public String getShortMessage() {
+ return shortMessage;
+ }
+
+ private String internalGetFullMessage(RevObject referencedObject) {
String message = "";
if (referencedObject instanceof RevTag) {
message = ((RevTag) referencedObject).getFullMessage();
@@ -106,7 +146,11 @@ public class RefModel implements Serializable, Comparable<RefModel> {
return message;
}
- public PersonIdent getAuthorIdent() {
+ public String getFullMessage() {
+ return fullMessage;
+ }
+
+ private PersonIdent internalGetAuthorIdent(RevObject referencedObject) {
if (referencedObject instanceof RevTag) {
return ((RevTag) referencedObject).getTaggerIdent();
} else if (referencedObject instanceof RevCommit) {
@@ -115,15 +159,26 @@ public class RefModel implements Serializable, Comparable<RefModel> {
return null;
}
+ public PersonIdent getAuthorIdent() {
+ return person;
+ }
+
public ObjectId getObjectId() {
- return reference.getObjectId();
+ if (objectId == null) {
+ objectId = ObjectId.fromString(id);
+ }
+ return objectId;
}
- public boolean isAnnotatedTag() {
+ private boolean internalIsAnnotatedTag(Ref reference, RevObject referencedObject) {
if (referencedObject instanceof RevTag) {
return !getReferencedObjectId().equals(getObjectId());
}
- return reference.getPeeledObjectId() != null;
+ return reference != null && reference.getPeeledObjectId() != null;
+ }
+
+ public boolean isAnnotatedTag() {
+ return annotated;
}
@Override
diff --git a/src/main/java/com/gitblit/models/RepositoryCommit.java b/src/main/java/com/gitblit/models/RepositoryCommit.java
index 765b4898..43f314a3 100644
--- a/src/main/java/com/gitblit/models/RepositoryCommit.java
+++ b/src/main/java/com/gitblit/models/RepositoryCommit.java
@@ -15,6 +15,8 @@
*/
package com.gitblit.models;
+import java.io.IOException;
+import java.io.ObjectInputStream;
import java.io.Serializable;
import java.text.MessageFormat;
import java.util.Date;
@@ -22,30 +24,36 @@ import java.util.List;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+import com.gitblit.wicket.GitBlitWebApp;
/**
- * Model class to represent a RevCommit, it's source repository, and the branch.
- * This class is used by the activity page.
+ * Model class to represent a RevCommit, it's source repository, and the branch. This class is used by the activity page.
*
* @author James Moger
*/
public class RepositoryCommit implements Serializable, Comparable<RepositoryCommit> {
- private static final long serialVersionUID = 1L;
+ private static final long serialVersionUID = -2214911650485772022L;
- public final String repository;
+ public String repository;
- public final String branch;
+ public String branch;
- private final RevCommit commit;
+ private final String commitId;
private List<RefModel> refs;
+ private transient RevCommit commit;
+
public RepositoryCommit(String repository, String branch, RevCommit commit) {
this.repository = repository;
this.branch = branch;
this.commit = commit;
+ this.commitId = commit.getName();
}
public void setRefs(List<RefModel> refs) {
@@ -80,7 +88,7 @@ public class RepositoryCommit implements Serializable, Comparable<RepositoryComm
return commit.getParentCount();
}
- public RevCommit [] getParents() {
+ public RevCommit[] getParents() {
return commit.getParents();
}
@@ -92,10 +100,14 @@ public class RepositoryCommit implements Serializable, Comparable<RepositoryComm
return commit.getCommitterIdent();
}
+ public RevCommit getCommit() {
+ return commit;
+ }
+
@Override
public boolean equals(Object o) {
if (o instanceof RepositoryCommit) {
- RepositoryCommit commit = (RepositoryCommit) o;
+ final RepositoryCommit commit = (RepositoryCommit) o;
return repository.equals(commit.repository) && getName().equals(commit.getName());
}
return false;
@@ -123,8 +135,23 @@ public class RepositoryCommit implements Serializable, Comparable<RepositoryComm
@Override
public String toString() {
- return MessageFormat.format("{0} {1} {2,date,yyyy-MM-dd HH:mm} {3} {4}",
- getShortName(), branch, getCommitterIdent().getWhen(), getAuthorIdent().getName(),
- getShortMessage());
+ return MessageFormat.format("{0} {1} {2,date,yyyy-MM-dd HH:mm} {3} {4}", getShortName(), branch, getCommitterIdent().getWhen(),
+ getAuthorIdent().getName(), getShortMessage());
+ }
+
+ // Serialization: restore the JGit RevCommit on reading
+
+ private void readObject(ObjectInputStream input) throws IOException, ClassNotFoundException {
+ // Read in fields and any hidden stuff
+ input.defaultReadObject();
+ // Go find the commit again.
+ final Repository repo = GitBlitWebApp.get().repositories().getRepository(repository);
+ if (repo == null) {
+ throw new IOException("Cannot find repositoy " + repository);
+ }
+ try (RevWalk walk = new RevWalk(repo)) {
+ commit = walk.parseCommit(repo.resolve(commitId));
+ }
}
+
} \ No newline at end of file
diff --git a/src/main/java/com/gitblit/models/RepositoryModel.java b/src/main/java/com/gitblit/models/RepositoryModel.java
index a81c622a..67ee1c7e 100644
--- a/src/main/java/com/gitblit/models/RepositoryModel.java
+++ b/src/main/java/com/gitblit/models/RepositoryModel.java
@@ -28,6 +28,7 @@ import com.gitblit.Constants.AccessRestrictionType;
import com.gitblit.Constants.AuthorizationControl;
import com.gitblit.Constants.CommitMessageRenderer;
import com.gitblit.Constants.FederationStrategy;
+import com.gitblit.Constants.MergeType;
import com.gitblit.utils.ArrayUtils;
import com.gitblit.utils.ModelUtils;
import com.gitblit.utils.StringUtils;
@@ -89,6 +90,7 @@ public class RepositoryModel implements Serializable, Comparable<RepositoryModel
public boolean acceptNewTickets;
public boolean requireApproval;
public String mergeTo;
+ public MergeType mergeType;
public transient boolean isCollectingGarbage;
public Date lastGC;
@@ -111,6 +113,7 @@ public class RepositoryModel implements Serializable, Comparable<RepositoryModel
this.isBare = true;
this.acceptNewTickets = true;
this.acceptNewPatchsets = true;
+ this.mergeType = MergeType.DEFAULT_MERGE_TYPE;
addOwner(owner);
}
diff --git a/src/main/java/com/gitblit/models/TicketModel.java b/src/main/java/com/gitblit/models/TicketModel.java
index d5345891..65e29dc0 100644
--- a/src/main/java/com/gitblit/models/TicketModel.java
+++ b/src/main/java/com/gitblit/models/TicketModel.java
@@ -43,6 +43,8 @@ import java.util.regex.Pattern;
import org.eclipse.jgit.util.RelativeDateFormatter;
+import com.gitblit.Constants;
+
/**
* The Gitblit Ticket model, its component classes, and enums.
*
@@ -773,10 +775,10 @@ public class TicketModel implements Serializable, Comparable<TicketModel> {
}
try {
- Pattern mentions = Pattern.compile("\\s@([A-Za-z0-9-_]+)");
+ Pattern mentions = Pattern.compile(Constants.REGEX_TICKET_MENTION);
Matcher m = mentions.matcher(text);
while (m.find()) {
- String username = m.group(1);
+ String username = m.group("user");
plusList(Field.mentions, username);
}
} catch (Exception e) {
diff --git a/src/main/java/com/gitblit/models/TreeNodeModel.java b/src/main/java/com/gitblit/models/TreeNodeModel.java
new file mode 100644
index 00000000..a69393e2
--- /dev/null
+++ b/src/main/java/com/gitblit/models/TreeNodeModel.java
@@ -0,0 +1,178 @@
+package com.gitblit.models;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import com.gitblit.utils.StringUtils;
+
+public class TreeNodeModel implements Serializable, Comparable<TreeNodeModel> {
+
+ private static final long serialVersionUID = 1L;
+ final TreeNodeModel parent;
+ final String name;
+ final List<TreeNodeModel> subFolders = new ArrayList<>();
+ final List<RepositoryModel> repositories = new ArrayList<>();
+
+ /**
+ * Create a new tree root
+ */
+ public TreeNodeModel() {
+ this.name = "/";
+ this.parent = null;
+ }
+
+ protected TreeNodeModel(String name, TreeNodeModel parent) {
+ this.name = name;
+ this.parent = parent;
+ }
+
+ public int getDepth() {
+ if(parent == null) {
+ return 0;
+ }else {
+ return parent.getDepth() +1;
+ }
+ }
+
+ /**
+ * Add a new sub folder to the current folder
+ *
+ * @param subFolder the subFolder to create
+ * @return the newly created folder to allow chaining
+ */
+ public TreeNodeModel add(String subFolder) {
+ TreeNodeModel n = new TreeNodeModel(subFolder, this);
+ subFolders.add(n);
+ Collections.sort(subFolders);
+ return n;
+ }
+
+ /**
+ * Add the given repo to the current folder
+ *
+ * @param repo
+ */
+ public void add(RepositoryModel repo) {
+ repositories.add(repo);
+ Collections.sort(repositories);
+ }
+
+ /**
+ * Adds the given repository model within the given path. Creates parent folders if they do not exist
+ *
+ * @param path
+ * @param model
+ */
+ public void add(String path, RepositoryModel model) {
+ TreeNodeModel folder = getSubTreeNode(this, path, true);
+ folder.add(model);
+ }
+
+ @Override
+ public String toString() {
+ String string = name + "\n";
+ for(TreeNodeModel n : subFolders) {
+ string += "f";
+ for(int i = 0; i < n.getDepth(); i++) {
+ string += "-";
+ }
+ string += n.toString();
+ }
+
+ for(RepositoryModel n : repositories) {
+ string += "r";
+ for(int i = 0; i < getDepth()+1; i++) {
+ string += "-";
+ }
+ string += n.toString() + "\n";
+ }
+
+ return string;
+ }
+
+ public boolean containsSubFolder(String path) {
+ return containsSubFolder(this, path);
+ }
+
+ public TreeNodeModel getSubFolder(String path) {
+ return getSubTreeNode(this, path, false);
+ }
+
+ public List<Serializable> getTreeAsListForFrontend(){
+ List<Serializable> l = new ArrayList<>();
+ getTreeAsListForFrontend(l, this);
+ return l;
+ }
+
+ private static void getTreeAsListForFrontend(List<Serializable> list, TreeNodeModel node) {
+ list.add(node);
+ for(TreeNodeModel t : node.subFolders) {
+ getTreeAsListForFrontend(list, t);
+ }
+ for(RepositoryModel r : node.repositories) {
+ list.add(r);
+ }
+ }
+
+ private static TreeNodeModel getSubTreeNode(TreeNodeModel node, String path, boolean create) {
+ if(!StringUtils.isEmpty(path)) {
+ boolean isPathInCurrentHierarchyLevel = path.lastIndexOf('/') < 0;
+ if(isPathInCurrentHierarchyLevel) {
+ for(TreeNodeModel t : node.subFolders) {
+ if(t.name.equals(path) ) {
+ return t;
+ }
+ }
+
+ if(create) {
+ node.add(path);
+ return getSubTreeNode(node, path, true);
+ }
+ }else {
+ //traverse into subFolder
+ String folderInCurrentHierarchyLevel = StringUtils.getFirstPathElement(path);
+
+ for(TreeNodeModel t : node.subFolders) {
+ if(t.name.equals(folderInCurrentHierarchyLevel) ) {
+ String folderInNextHierarchyLevel = path.substring(path.indexOf('/') + 1, path.length());
+ return getSubTreeNode(t, folderInNextHierarchyLevel, create);
+ }
+ }
+
+ if(create) {
+ String folderInNextHierarchyLevel = path.substring(path.indexOf('/') + 1, path.length());
+ return getSubTreeNode(node.add(folderInCurrentHierarchyLevel), folderInNextHierarchyLevel, true);
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private static boolean containsSubFolder(TreeNodeModel node, String path) {
+ return getSubTreeNode(node, path, false) != null;
+ }
+
+ @Override
+ public int compareTo(TreeNodeModel t) {
+ return StringUtils.compareRepositoryNames(name, t.name);
+ }
+
+ public TreeNodeModel getParent() {
+ return parent;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public List<TreeNodeModel> getSubFolders() {
+ return subFolders;
+ }
+
+ public List<RepositoryModel> getRepositories() {
+ return repositories;
+ }
+}
diff --git a/src/main/java/com/gitblit/models/UserModel.java b/src/main/java/com/gitblit/models/UserModel.java
index e1522748..b0933093 100644
--- a/src/main/java/com/gitblit/models/UserModel.java
+++ b/src/main/java/com/gitblit/models/UserModel.java
@@ -36,6 +36,7 @@ import com.gitblit.Constants.PermissionType;
import com.gitblit.Constants.RegistrantType;
import com.gitblit.utils.ArrayUtils;
import com.gitblit.utils.ModelUtils;
+import com.gitblit.utils.SecureRandom;
import com.gitblit.utils.StringUtils;
/**
@@ -52,6 +53,8 @@ public class UserModel implements Principal, Serializable, Comparable<UserModel>
public static final UserModel ANONYMOUS = new UserModel();
+ private static final SecureRandom RANDOM = new SecureRandom();
+
// field names are reflectively mapped in EditUser page
public String username;
public String password;
@@ -107,7 +110,7 @@ public class UserModel implements Principal, Serializable, Comparable<UserModel>
* @return the user's list of permissions
*/
public List<RegistrantAccessPermission> getRepositoryPermissions() {
- List<RegistrantAccessPermission> list = new ArrayList<RegistrantAccessPermission>();
+ List<RegistrantAccessPermission> list = new ArrayList<>();
if (canAdmin()) {
// user has REWIND access to all repositories
return list;
@@ -132,7 +135,6 @@ public class UserModel implements Principal, Serializable, Comparable<UserModel>
Collections.sort(list);
// include immutable team permissions, being careful to preserve order
- Set<RegistrantAccessPermission> set = new LinkedHashSet<RegistrantAccessPermission>(list);
for (TeamModel team : teams) {
for (RegistrantAccessPermission teamPermission : team.getRepositoryPermissions()) {
// we can not change an inherited team permission, though we can override
@@ -140,10 +142,17 @@ public class UserModel implements Principal, Serializable, Comparable<UserModel>
teamPermission.permissionType = PermissionType.TEAM;
teamPermission.source = team.name;
teamPermission.mutable = false;
- set.add(teamPermission);
+ int i = list.indexOf(teamPermission);
+ if (i < 0) list.add(teamPermission);
+ else {
+ RegistrantAccessPermission lp = list.get(i);
+ if (teamPermission.permission.exceeds(lp.permission)) {
+ list.set(i, teamPermission);
+ }
+ }
}
}
- return new ArrayList<RegistrantAccessPermission>(set);
+ return list;
}
/**
@@ -660,4 +669,8 @@ public class UserModel implements Principal, Serializable, Comparable<UserModel>
String projectPath = StringUtils.getFirstPathElement(repository);
return !StringUtils.isEmpty(projectPath) && projectPath.equalsIgnoreCase(getPersonalPath());
}
+
+ public String createCookie() {
+ return StringUtils.getSHA1(RANDOM.randomBytes(32));
+ }
}
diff --git a/src/main/java/com/gitblit/service/LuceneRepoIndexStore.java b/src/main/java/com/gitblit/service/LuceneRepoIndexStore.java
new file mode 100644
index 00000000..ff7d0885
--- /dev/null
+++ b/src/main/java/com/gitblit/service/LuceneRepoIndexStore.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2017 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.gitblit.service;
+
+import java.io.File;
+
+import com.gitblit.utils.LuceneIndexStore;
+
+/**
+ * @author Florian Zschocke
+ *
+ * @since 1.9.0
+ */
+class LuceneRepoIndexStore extends LuceneIndexStore
+{
+
+ private static final String LUCENE_DIR = "lucene";
+ private static final String CONF_FILE = "gb_lucene.conf";
+
+
+ /**
+ * @param repositoryFolder
+ * The directory of the repository for this index
+ * @param indexVersion
+ * Version of the index definition
+ */
+ public LuceneRepoIndexStore(File repositoryFolder, int indexVersion) {
+ super(new File(repositoryFolder, LUCENE_DIR), indexVersion);
+ }
+
+
+ /**
+ * Get the index config File.
+ *
+ * @return The index config File
+ */
+ public File getConfigFile() {
+ return new File(this.indexFolder, CONF_FILE);
+ }
+
+}
diff --git a/src/main/java/com/gitblit/service/LuceneService.java b/src/main/java/com/gitblit/service/LuceneService.java
index 097a39b2..906a0b5e 100644
--- a/src/main/java/com/gitblit/service/LuceneService.java
+++ b/src/main/java/com/gitblit/service/LuceneService.java
@@ -65,7 +65,6 @@ import org.apache.lucene.search.highlight.SimpleHTMLFormatter;
import org.apache.lucene.search.highlight.SimpleSpanFragmenter;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
-import org.apache.lucene.util.Version;
import org.eclipse.jgit.diff.DiffEntry.ChangeType;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.FileMode;
@@ -118,15 +117,9 @@ public class LuceneService implements Runnable {
private static final String FIELD_DATE = "date";
private static final String FIELD_TAG = "tag";
- private static final String CONF_FILE = "lucene.conf";
- private static final String LUCENE_DIR = "lucene";
- private static final String CONF_INDEX = "index";
- private static final String CONF_VERSION = "version";
private static final String CONF_ALIAS = "aliases";
private static final String CONF_BRANCH = "branches";
- private static final Version LUCENE_VERSION = Version.LUCENE_4_10_0;
-
private final Logger logger = LoggerFactory.getLogger(LuceneService.class);
private final IStoredSettings storedSettings;
@@ -267,7 +260,7 @@ public class LuceneService implements Runnable {
// close all writers
for (String writer : writers.keySet()) {
try {
- writers.get(writer).close(true);
+ writers.get(writer).close();
} catch (Throwable t) {
logger.error("Failed to close Lucene writer for " + writer, t);
}
@@ -293,26 +286,13 @@ public class LuceneService implements Runnable {
* @return true, if successful
*/
public boolean deleteIndex(String repositoryName) {
- try {
- // close any open writer/searcher
- close(repositoryName);
-
- // delete the index folder
- File repositoryFolder = FileKey.resolve(new File(repositoriesFolder, repositoryName), FS.DETECTED);
- File luceneIndex = new File(repositoryFolder, LUCENE_DIR);
- if (luceneIndex.exists()) {
- org.eclipse.jgit.util.FileUtils.delete(luceneIndex,
- org.eclipse.jgit.util.FileUtils.RECURSIVE);
- }
- // delete the config file
- File luceneConfig = new File(repositoryFolder, CONF_FILE);
- if (luceneConfig.exists()) {
- luceneConfig.delete();
- }
- return true;
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
+ // close any open writer/searcher
+ close(repositoryName);
+
+ // delete the index folder
+ File repositoryFolder = FileKey.resolve(new File(repositoriesFolder, repositoryName), FS.DETECTED);
+ LuceneRepoIndexStore luceneIndex = new LuceneRepoIndexStore(repositoryFolder, INDEX_VERSION);
+ return luceneIndex.delete();
}
/**
@@ -386,29 +366,20 @@ public class LuceneService implements Runnable {
* @return a config object
*/
private FileBasedConfig getConfig(Repository repository) {
- File file = new File(repository.getDirectory(), CONF_FILE);
- FileBasedConfig config = new FileBasedConfig(file, FS.detect());
+ LuceneRepoIndexStore luceneIndex = new LuceneRepoIndexStore(repository.getDirectory(), INDEX_VERSION);
+ FileBasedConfig config = new FileBasedConfig(luceneIndex.getConfigFile(), FS.detect());
return config;
}
/**
- * Reads the Lucene config file for the repository to check the index
- * version. If the index version is different, then rebuild the repository
- * index.
+ * Checks if an index exists for the repository, that is compatible with
+ * INDEX_VERSION and the Lucene version.
*
* @param repository
- * @return true of the on-disk index format is different than INDEX_VERSION
+ * @return true if no index is found for the repository, false otherwise.
*/
private boolean shouldReindex(Repository repository) {
- try {
- FileBasedConfig config = getConfig(repository);
- config.load();
- int indexVersion = config.getInt(CONF_INDEX, CONF_VERSION, 0);
- // reindex if versions do not match
- return indexVersion != INDEX_VERSION;
- } catch (Throwable t) {
- }
- return true;
+ return ! (new LuceneRepoIndexStore(repository.getDirectory(), INDEX_VERSION).hasIndex());
}
@@ -618,7 +589,6 @@ public class LuceneService implements Runnable {
reader.close();
// commit all changes and reset the searcher
- config.setInt(CONF_INDEX, null, CONF_VERSION, INDEX_VERSION);
config.save();
writer.commit();
resetIndexSearcher(model.name);
@@ -721,10 +691,9 @@ public class LuceneService implements Runnable {
String pattern = MessageFormat.format("{0}:'{'0} AND {1}:\"'{'1'}'\" AND {2}:\"'{'2'}'\"", FIELD_OBJECT_TYPE, FIELD_BRANCH, FIELD_PATH);
String q = MessageFormat.format(pattern, SearchObjectType.blob.name(), branch, path);
- BooleanQuery query = new BooleanQuery();
- StandardAnalyzer analyzer = new StandardAnalyzer(LUCENE_VERSION);
- QueryParser qp = new QueryParser(LUCENE_VERSION, FIELD_SUMMARY, analyzer);
- query.add(qp.parse(q), Occur.MUST);
+ StandardAnalyzer analyzer = new StandardAnalyzer();
+ QueryParser qp = new QueryParser(FIELD_SUMMARY, analyzer);
+ BooleanQuery query = new BooleanQuery.Builder().add(qp.parse(q), Occur.MUST).build();
IndexWriter writer = getIndexWriter(repositoryName);
int numDocsBefore = writer.numDocs();
@@ -848,7 +817,6 @@ public class LuceneService implements Runnable {
}
// update the config
- config.setInt(CONF_INDEX, null, CONF_VERSION, INDEX_VERSION);
config.setString(CONF_ALIAS, null, keyName, branchName);
config.setString(CONF_BRANCH, null, keyName, branch.getObjectId().getName());
config.save();
@@ -966,16 +934,13 @@ public class LuceneService implements Runnable {
*/
private IndexWriter getIndexWriter(String repository) throws IOException {
IndexWriter indexWriter = writers.get(repository);
- File repositoryFolder = FileKey.resolve(new File(repositoriesFolder, repository), FS.DETECTED);
- File indexFolder = new File(repositoryFolder, LUCENE_DIR);
- Directory directory = FSDirectory.open(indexFolder);
-
if (indexWriter == null) {
- if (!indexFolder.exists()) {
- indexFolder.mkdirs();
- }
- StandardAnalyzer analyzer = new StandardAnalyzer(LUCENE_VERSION);
- IndexWriterConfig config = new IndexWriterConfig(LUCENE_VERSION, analyzer);
+ File repositoryFolder = FileKey.resolve(new File(repositoriesFolder, repository), FS.DETECTED);
+ LuceneRepoIndexStore indexStore = new LuceneRepoIndexStore(repositoryFolder, INDEX_VERSION);
+ indexStore.create();
+ Directory directory = FSDirectory.open(indexStore.getPath());
+ StandardAnalyzer analyzer = new StandardAnalyzer();
+ IndexWriterConfig config = new IndexWriterConfig(analyzer);
config.setOpenMode(OpenMode.CREATE_OR_APPEND);
indexWriter = new IndexWriter(directory, config);
writers.put(repository, indexWriter);
@@ -1028,18 +993,18 @@ public class LuceneService implements Runnable {
return null;
}
Set<SearchResult> results = new LinkedHashSet<SearchResult>();
- StandardAnalyzer analyzer = new StandardAnalyzer(LUCENE_VERSION);
+ StandardAnalyzer analyzer = new StandardAnalyzer();
try {
// default search checks summary and content
- BooleanQuery query = new BooleanQuery();
+ BooleanQuery.Builder bldr = new BooleanQuery.Builder();
QueryParser qp;
- qp = new QueryParser(LUCENE_VERSION, FIELD_SUMMARY, analyzer);
+ qp = new QueryParser(FIELD_SUMMARY, analyzer);
qp.setAllowLeadingWildcard(true);
- query.add(qp.parse(text), Occur.SHOULD);
+ bldr.add(qp.parse(text), Occur.SHOULD);
- qp = new QueryParser(LUCENE_VERSION, FIELD_CONTENT, analyzer);
+ qp = new QueryParser(FIELD_CONTENT, analyzer);
qp.setAllowLeadingWildcard(true);
- query.add(qp.parse(text), Occur.SHOULD);
+ bldr.add(qp.parse(text), Occur.SHOULD);
IndexSearcher searcher;
if (repositories.length == 1) {
@@ -1057,10 +1022,11 @@ public class LuceneService implements Runnable {
searcher = new IndexSearcher(reader);
}
+ BooleanQuery query = bldr.build();
Query rewrittenQuery = searcher.rewrite(query);
logger.debug(rewrittenQuery.toString());
- TopScoreDocCollector collector = TopScoreDocCollector.create(5000, true);
+ TopScoreDocCollector collector = TopScoreDocCollector.create(5000);
searcher.search(rewrittenQuery, collector);
int offset = Math.max(0, (page - 1) * pageSize);
ScoreDoc[] hits = collector.topDocs(offset, pageSize).scoreDocs;
@@ -1225,7 +1191,7 @@ public class LuceneService implements Runnable {
*/
private class MultiSourceReader extends MultiReader {
- MultiSourceReader(IndexReader [] readers) {
+ MultiSourceReader(IndexReader [] readers) throws IOException {
super(readers, false);
}
diff --git a/src/main/java/com/gitblit/service/MailService.java b/src/main/java/com/gitblit/service/MailService.java
index ec3a84ca..a5cd46d4 100644
--- a/src/main/java/com/gitblit/service/MailService.java
+++ b/src/main/java/com/gitblit/service/MailService.java
@@ -17,6 +17,7 @@ package com.gitblit.service;
import java.io.File;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Properties;
@@ -31,6 +32,7 @@ import javax.mail.Authenticator;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.PasswordAuthentication;
+import javax.mail.SendFailedException;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
@@ -143,7 +145,7 @@ public class MailService implements Runnable {
if (StringUtils.isEmpty(fromAddress)) {
fromAddress = "gitblit@gitblit.com";
}
- InternetAddress from = new InternetAddress(fromAddress, mailing.from == null ? "Gitblit" : mailing.from);
+ InternetAddress from = new InternetAddress(fromAddress, mailing.from == null ? "Gitblit" : mailing.from,"utf-8");
message.setFrom(from);
Pattern validEmail = Pattern
@@ -272,9 +274,22 @@ public class MailService implements Runnable {
while ((message = queue.poll()) != null) {
try {
if (settings.getBoolean(Keys.mail.debug, false)) {
- logger.info("send: " + StringUtils.trimString(message.getSubject(), 60));
+ logger.info("send: '" + StringUtils.trimString(message.getSubject(), 60)
+ + "' to:" + StringUtils.trimString(Arrays.toString(message.getAllRecipients()), 300));
}
Transport.send(message);
+ } catch (SendFailedException sfe) {
+ if (settings.getBoolean(Keys.mail.debug, false)) {
+ logger.error("Failed to send message: {}", sfe.getMessage());
+ logger.info(" Invalid addresses: {}", Arrays.toString(sfe.getInvalidAddresses()));
+ logger.info(" Valid sent addresses: {}", Arrays.toString(sfe.getValidSentAddresses()));
+ logger.info(" Valid unset addresses: {}", Arrays.toString(sfe.getValidUnsentAddresses()));
+ logger.info("", sfe);
+ }
+ else {
+ logger.error("Failed to send message: {}", sfe.getMessage(), sfe.getNextException());
+ }
+ failures.add(message);
} catch (Throwable e) {
logger.error("Failed to send message", e);
failures.add(message);
diff --git a/src/main/java/com/gitblit/service/MirrorService.java b/src/main/java/com/gitblit/service/MirrorService.java
index cf9ccb55..eb247fc0 100644
--- a/src/main/java/com/gitblit/service/MirrorService.java
+++ b/src/main/java/com/gitblit/service/MirrorService.java
@@ -27,11 +27,14 @@ import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.lib.RefUpdate.Result;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.StoredConfig;
+import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.transport.FetchResult;
import org.eclipse.jgit.transport.ReceiveCommand;
import org.eclipse.jgit.transport.ReceiveCommand.Type;
import org.eclipse.jgit.transport.RemoteConfig;
import org.eclipse.jgit.transport.TrackingRefUpdate;
+import org.eclipse.jgit.transport.URIish;
+import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -146,7 +149,12 @@ public class MirrorService implements Runnable {
logger.debug("checking {} remote {} for ref updates", repositoryName, mirror.getName());
final boolean testing = false;
Git git = new Git(repository);
- FetchResult result = git.fetch().setRemote(mirror.getName()).setDryRun(testing).call();
+ CredentialsProvider creds = null;
+ URIish fetchUri = mirror.getURIs().get(0);
+ if (fetchUri.getUser() != null && fetchUri.getPass() != null) {
+ creds = new UsernamePasswordCredentialsProvider(fetchUri.getUser(), fetchUri.getPass());
+ }
+ FetchResult result = git.fetch().setCredentialsProvider(creds).setRemote(mirror.getName()).setDryRun(testing).call();
Collection<TrackingRefUpdate> refUpdates = result.getTrackingRefUpdates();
if (refUpdates.size() > 0) {
ReceiveCommand ticketBranchCmd = null;
diff --git a/src/main/java/com/gitblit/servlet/AccessRestrictionFilter.java b/src/main/java/com/gitblit/servlet/AccessRestrictionFilter.java
index e1d76db0..61c5eb1c 100644
--- a/src/main/java/com/gitblit/servlet/AccessRestrictionFilter.java
+++ b/src/main/java/com/gitblit/servlet/AccessRestrictionFilter.java
@@ -167,6 +167,7 @@ public abstract class AccessRestrictionFilter extends AuthenticationFilter {
String fullUrl = getFullUrl(httpRequest);
String repository = extractRepositoryName(fullUrl);
if (StringUtils.isEmpty(repository)) {
+ logger.info("ARF: Rejecting request, empty repository name in URL {}", fullUrl);
httpResponse.setStatus(HttpServletResponse.SC_BAD_REQUEST);
return;
}
@@ -181,6 +182,21 @@ public abstract class AccessRestrictionFilter extends AuthenticationFilter {
String fullSuffix = fullUrl.substring(repository.length());
String urlRequestType = getUrlRequestAction(fullSuffix);
+ if (StringUtils.isEmpty(urlRequestType)) {
+ logger.info("ARF: Rejecting request for {}, no supported action found in URL {}", repository, fullSuffix);
+ httpResponse.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+ return;
+ }
+
+ // TODO: Maybe checking for clone bundle should be done somewhere else? Like other stuff?
+ // In any way, the access to the constant from here is messed up an needs some cleaning up.
+ if (GitFilter.CLONE_BUNDLE.equalsIgnoreCase(urlRequestType)) {
+ logger.info(MessageFormat.format("ARF: Rejecting request for {0}, clone bundle is not implemented.", repository));
+ httpResponse.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, "The 'clone.bundle' command is currently not implemented. " +
+ "Please use a normal clone command.");
+ return;
+ }
+
UserModel user = getUser(httpRequest);
// Load the repository model
diff --git a/src/main/java/com/gitblit/servlet/GitFilter.java b/src/main/java/com/gitblit/servlet/GitFilter.java
index 9522893e..66933cbd 100644
--- a/src/main/java/com/gitblit/servlet/GitFilter.java
+++ b/src/main/java/com/gitblit/servlet/GitFilter.java
@@ -46,14 +46,16 @@ import com.gitblit.utils.StringUtils;
@Singleton
public class GitFilter extends AccessRestrictionFilter {
- protected static final String gitReceivePack = "/git-receive-pack";
+ static final String GIT_RECEIVE_PACK = "/git-receive-pack";
- protected static final String gitUploadPack = "/git-upload-pack";
+ static final String GIT_UPLOAD_PACK = "/git-upload-pack";
+
+ static final String CLONE_BUNDLE = "/clone.bundle";
- protected static final String gitLfs = "/info/lfs";
+ static final String GIT_LFS = "/info/lfs";
- protected static final String[] suffixes = { gitReceivePack, gitUploadPack, "/info/refs", "/HEAD",
- "/objects", gitLfs };
+ static final String[] SUFFIXES = {GIT_RECEIVE_PACK, GIT_UPLOAD_PACK, "/info/refs", "/HEAD",
+ "/objects", GIT_LFS, CLONE_BUNDLE};
private IStoredSettings settings;
@@ -76,13 +78,19 @@ public class GitFilter extends AccessRestrictionFilter {
/**
* Extract the repository name from the url.
*
- * @param cloneUrl
+ * @param url
+ * Request URL with repository name in it
* @return repository name
*/
- public static String getRepositoryName(String value) {
- String repository = value;
+ public static String getRepositoryName(String url) {
+ String repository = url;
+ // strip off parameters from the URL
+ if (repository.contains("?")) {
+ repository = repository.substring(0, repository.indexOf("?"));
+ }
+
// get the repository name from the url by finding a known url suffix
- for (String urlSuffix : suffixes) {
+ for (String urlSuffix : SUFFIXES) {
if (repository.indexOf(urlSuffix) > -1) {
repository = repository.substring(0, repository.indexOf(urlSuffix));
}
@@ -111,18 +119,20 @@ public class GitFilter extends AccessRestrictionFilter {
@Override
protected String getUrlRequestAction(String suffix) {
if (!StringUtils.isEmpty(suffix)) {
- if (suffix.startsWith(gitReceivePack)) {
- return gitReceivePack;
- } else if (suffix.startsWith(gitUploadPack)) {
- return gitUploadPack;
+ if (suffix.startsWith(GIT_RECEIVE_PACK)) {
+ return GIT_RECEIVE_PACK;
+ } else if (suffix.startsWith(GIT_UPLOAD_PACK)) {
+ return GIT_UPLOAD_PACK;
} else if (suffix.contains("?service=git-receive-pack")) {
- return gitReceivePack;
+ return GIT_RECEIVE_PACK;
} else if (suffix.contains("?service=git-upload-pack")) {
- return gitUploadPack;
- } else if (suffix.startsWith(gitLfs)) {
- return gitLfs;
+ return GIT_UPLOAD_PACK;
+ } else if (suffix.startsWith(GIT_LFS)) {
+ return GIT_LFS;
+ } else if (suffix.startsWith(CLONE_BUNDLE)) {
+ return CLONE_BUNDLE;
} else {
- return gitUploadPack;
+ return GIT_UPLOAD_PACK;
}
}
return null;
@@ -150,12 +160,18 @@ public class GitFilter extends AccessRestrictionFilter {
*/
@Override
protected boolean isCreationAllowed(String action) {
-
+ // Unknown action, don't allow creation
+ if (action == null) return false;
+
//Repository must already exist before large files can be deposited
- if (action.equals(gitLfs)) {
+ if (GIT_LFS.equals(action)) {
return false;
}
-
+ // Action is not implemened.
+ if (CLONE_BUNDLE.equals(action)) {
+ return false;
+ }
+
return settings.getBoolean(Keys.git.allowCreateOnPush, true);
}
@@ -170,7 +186,7 @@ public class GitFilter extends AccessRestrictionFilter {
protected boolean isActionAllowed(RepositoryModel repository, String action, String method) {
// the log here has been moved into ReceiveHook to provide clients with
// error messages
- if (gitLfs.equals(action)) {
+ if (GIT_LFS.equals(action)) {
if (!method.matches("GET|POST|PUT|HEAD")) {
return false;
}
@@ -194,13 +210,13 @@ public class GitFilter extends AccessRestrictionFilter {
*/
@Override
protected boolean requiresAuthentication(RepositoryModel repository, String action, String method) {
- if (gitUploadPack.equals(action)) {
+ if (GIT_UPLOAD_PACK.equals(action)) {
// send to client
return repository.accessRestriction.atLeast(AccessRestrictionType.CLONE);
- } else if (gitReceivePack.equals(action)) {
+ } else if (GIT_RECEIVE_PACK.equals(action)) {
// receive from client
return repository.accessRestriction.atLeast(AccessRestrictionType.PUSH);
- } else if (gitLfs.equals(action)) {
+ } else if (GIT_LFS.equals(action)) {
if (method.matches("GET|HEAD")) {
return repository.accessRestriction.atLeast(AccessRestrictionType.CLONE);
@@ -227,10 +243,10 @@ public class GitFilter extends AccessRestrictionFilter {
// Git Servlet disabled
return false;
}
- if (action.equals(gitReceivePack)) {
+ if (GIT_RECEIVE_PACK.equals(action)) {
// push permissions are enforced in the receive pack
return true;
- } else if (action.equals(gitUploadPack)) {
+ } else if (GIT_UPLOAD_PACK.equals(action)) {
// Clone request
if (user.canClone(repository)) {
return true;
@@ -255,9 +271,9 @@ public class GitFilter extends AccessRestrictionFilter {
*/
@Override
protected RepositoryModel createRepository(UserModel user, String repository, String action) {
- boolean isPush = !StringUtils.isEmpty(action) && gitReceivePack.equals(action);
+ boolean isPush = !StringUtils.isEmpty(action) && GIT_RECEIVE_PACK.equals(action);
- if (action.equals(gitLfs)) {
+ if (GIT_LFS.equals(action)) {
//Repository must already exist for any filestore actions
return null;
}
@@ -325,7 +341,7 @@ public class GitFilter extends AccessRestrictionFilter {
@Override
protected String getAuthenticationHeader(HttpServletRequest httpRequest, String action) {
- if (action.equals(gitLfs)) {
+ if (GIT_LFS.equals(action)) {
if (hasContentInRequestHeader(httpRequest, "Accept", FilestoreServlet.GIT_LFS_META_MIME)) {
return "LFS-Authenticate";
}
@@ -344,7 +360,7 @@ public class GitFilter extends AccessRestrictionFilter {
protected boolean hasValidRequestHeader(String action,
HttpServletRequest request) {
- if (action.equals(gitLfs) && request.getMethod().equals("POST")) {
+ if (GIT_LFS.equals(action) && request.getMethod().equals("POST")) {
if ( !hasContentInRequestHeader(request, "Accept", FilestoreServlet.GIT_LFS_META_MIME)
|| !hasContentInRequestHeader(request, "Content-Type", FilestoreServlet.GIT_LFS_META_MIME)) {
return false;
diff --git a/src/main/java/com/gitblit/servlet/GitblitContext.java b/src/main/java/com/gitblit/servlet/GitblitContext.java
index 750da796..cd8615a8 100644
--- a/src/main/java/com/gitblit/servlet/GitblitContext.java
+++ b/src/main/java/com/gitblit/servlet/GitblitContext.java
@@ -293,11 +293,13 @@ public class GitblitContext extends GuiceServletContextListener {
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);
+ if (pluginManager != null) {
+ for (LifeCycleListener listener : pluginManager.getExtensions(LifeCycleListener.class)) {
+ try {
+ listener.onShutdown();
+ } catch (Throwable t) {
+ logger.error(null, t);
+ }
}
}
diff --git a/src/main/java/com/gitblit/servlet/PagesServlet.java b/src/main/java/com/gitblit/servlet/PagesServlet.java
index 1473e966..a76a50b5 100644
--- a/src/main/java/com/gitblit/servlet/PagesServlet.java
+++ b/src/main/java/com/gitblit/servlet/PagesServlet.java
@@ -69,13 +69,14 @@ public class PagesServlet extends RawServlet {
}
@Override
- protected String getBranch(String repository, HttpServletRequest request) {
+ String getBranch(String repository, String pathInfo)
+ {
return "gh-pages";
}
@Override
- protected String getPath(String repository, String branch, HttpServletRequest request) {
- String pi = request.getPathInfo().substring(1);
+ String getPath(String repository, String branch, String pi)
+ {
if (pi.equals(repository)) {
return "";
}
diff --git a/src/main/java/com/gitblit/servlet/RawServlet.java b/src/main/java/com/gitblit/servlet/RawServlet.java
index dca57730..e2cd2881 100644
--- a/src/main/java/com/gitblit/servlet/RawServlet.java
+++ b/src/main/java/com/gitblit/servlet/RawServlet.java
@@ -36,6 +36,7 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.tika.Tika;
+import org.apache.wicket.protocol.http.WicketURLEncoder;
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.MutableObjectId;
import org.eclipse.jgit.lib.ObjectLoader;
@@ -69,6 +70,9 @@ import com.google.inject.Singleton;
@Singleton
public class RawServlet extends HttpServlet {
+ // Forward slash character
+ static final char FSC = '!';
+
private static final long serialVersionUID = 1L;
private transient Logger logger = LoggerFactory.getLogger(RawServlet.class);
@@ -99,23 +103,46 @@ public class RawServlet extends HttpServlet {
if (baseURL.length() > 0 && baseURL.charAt(baseURL.length() - 1) == '/') {
baseURL = baseURL.substring(0, baseURL.length() - 1);
}
+ if (repository.length() > 0 && repository.charAt(repository.length() - 1) == '/') {
+ repository = repository.substring(0, repository.length() - 1);
+ }
+ if (repository.length() > 0 && repository.charAt(0) == '/') {
+ repository = repository.substring(1);
+ }
- char fsc = '!';
- char c = GitblitContext.getManager(IRuntimeManager.class).getSettings().getChar(Keys.web.forwardSlashCharacter, '/');
- if (c != '/') {
- fsc = c;
+ char fsc = GitblitContext.getManager(IRuntimeManager.class).getSettings().getChar(Keys.web.forwardSlashCharacter, '/');
+ if (fsc == '/') {
+ fsc = FSC;
}
if (branch != null) {
branch = Repository.shortenRefName(branch).replace('/', fsc);
}
+ if (path != null && path.length() > 0 && path.charAt(0) == '/') {
+ path = path.substring(1);
+ }
String encodedPath = path == null ? "" : path.replace('/', fsc);
- return baseURL + Constants.RAW_PATH + repository + "/" + (branch == null ? "" : (branch + "/" + encodedPath));
+ String fullPath = repository + "/" + (branch == null ? "" : (branch + "/" + encodedPath));
+ return baseURL + Constants.RAW_PATH + WicketURLEncoder.FULL_PATH_INSTANCE.encode(fullPath);
}
- protected String getBranch(String repository, HttpServletRequest request) {
- String pi = request.getPathInfo();
- String branch = pi.substring(pi.indexOf(repository) + repository.length() + 1);
+
+ /**
+ * Find and return the name of a branch from a given repository in a HTTP request path info.
+ * The branch name returned is transformed to the form in the repository, i.e. a transformation
+ * of the forward slash character in the URL is reversed.
+ *
+ * @param repository
+ * Path of repository, no leading slash, no trailing slash
+ * @param pathInfo
+ * The sanitised path info from a HTTP request, i.e. without the leading slash.
+ *
+ * @return The name of the branch from the path info, unescaped.
+ */
+ String getBranch(String repository, String pathInfo)
+ {
+ if (pathInfo == null || pathInfo.isEmpty() || pathInfo.equals("/")) return "";
+ String branch = pathInfo.substring(pathInfo.indexOf(repository) + repository.length() + 1);
int fs = branch.indexOf('/');
if (fs > -1) {
branch = branch.substring(0, fs);
@@ -124,18 +151,53 @@ public class RawServlet extends HttpServlet {
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)) {
+ /**
+ * Find and return the path from a given repository and given branch in a HTTP request path info.
+ * The path string returned is transformed to the form in the repository, i.e. a transformation
+ * of the forward slash character in the URL is reversed.
+ *
+ * @param repository
+ * Path of repository, no leading slash, no trailing slash
+ * @param branch
+ * Branch name from the repository, i.e. with forward slash character, no leading slash, no trailing slash.
+ * @param pathInfo
+ * The sanitised path info from a HTTP request, i.e. without the leading slash.
+ *
+ * @return The file/folder path part from the path info, in unescaped form.
+ */
+ String getPath(String repository, String branch, String pathInfo)
+ {
+ if (pathInfo == null || pathInfo.isEmpty() || pathInfo.equals("/")) return "";
+
+ // Make the branch look like in the URL, or else it won't match later in the `indexOf`.
+ char c = runtimeManager.getSettings().getChar(Keys.web.forwardSlashCharacter, '/');
+ char fsc = (c == '/') ? FSC : c;
+ String base = repository + "/" + Repository.shortenRefName(branch).replace('/', fsc);
+
+ // 'repository/' or 'repository/branch' or 'repository/branch/'
+ if (pathInfo.equals(base)) {
return "";
}
- String path = pi.substring(pi.indexOf(base) + base.length() + 1);
+ // I have no idea why 'indexOf(base)' is used, which assumes something could come before 'base' in
+ // the pathInfo string. But since it is here, we handle it until we completly refactor the paths used
+ // in Gitblit to something sensible.
+ // 'leadin/repository/'
+ // 'leadin/repository/branch'
+ int pathStart = pathInfo.indexOf(base) + base.length();
+ // 'leadin/repository/branch/'
+ if (pathStart < pathInfo.length() && pathInfo.charAt(pathStart) == '/') pathStart++;
+ if (pathInfo.length() == pathStart) return "";
+ // 'leadin/repository/branch/path'
+ String path = pathInfo.substring(pathStart);
+
+ path = path.replace('!', '/').replace(c, '/');
+
+ // 'repository/branch/path/'
+ // 'leadin/repository/branch/path/'
if (path.endsWith("/")) {
path = path.substring(0, path.length() - 1);
}
- char c = runtimeManager.getSettings().getChar(Keys.web.forwardSlashCharacter, '/');
- return path.replace('!', '/').replace(c, '/');
+ return path;
}
protected boolean renderIndex() {
@@ -188,7 +250,7 @@ public class RawServlet extends HttpServlet {
}
// identify the branch
- String branch = getBranch(repository, request);
+ String branch = getBranch(repository, path);
if (StringUtils.isEmpty(branch)) {
branch = r.getBranch();
if (branch == null) {
@@ -207,7 +269,7 @@ public class RawServlet extends HttpServlet {
}
// identify the requested path
- String requestedPath = getPath(repository, branch, request);
+ String requestedPath = getPath(repository, branch, path);
// identify the commit
RevCommit commit = JGitUtils.getCommit(r, branch);
diff --git a/src/main/java/com/gitblit/servlet/RpcServlet.java b/src/main/java/com/gitblit/servlet/RpcServlet.java
index 9809a252..0081367f 100644
--- a/src/main/java/com/gitblit/servlet/RpcServlet.java
+++ b/src/main/java/com/gitblit/servlet/RpcServlet.java
@@ -133,6 +133,10 @@ public class RpcServlet extends JsonServlet {
model.name));
continue;
}
+ if(!StringUtils.isEmpty(objectName) && !objectName.equals(model.name)) {
+ // skip repository if a name was submitted and it doesn't match
+ continue;
+ }
// get local branches
Repository repository = gitblit.getRepository(model.name);
List<RefModel> refs = JGitUtils.getLocalBranches(repository, false, -1);
diff --git a/src/main/java/com/gitblit/tickets/BranchTicketService.java b/src/main/java/com/gitblit/tickets/BranchTicketService.java
index 7bef435a..8430c546 100644
--- a/src/main/java/com/gitblit/tickets/BranchTicketService.java
+++ b/src/main/java/com/gitblit/tickets/BranchTicketService.java
@@ -110,9 +110,8 @@ public class BranchTicketService extends ITicketService implements RefsChangedLi
}
@Override
- public BranchTicketService start() {
+ public void onStart() {
log.info("{} started", getClass().getSimpleName());
- return this;
}
@Override
diff --git a/src/main/java/com/gitblit/tickets/FileTicketService.java b/src/main/java/com/gitblit/tickets/FileTicketService.java
index 1e82f0de..05670468 100644
--- a/src/main/java/com/gitblit/tickets/FileTicketService.java
+++ b/src/main/java/com/gitblit/tickets/FileTicketService.java
@@ -80,9 +80,8 @@ public class FileTicketService extends ITicketService {
}
@Override
- public FileTicketService start() {
+ public void onStart() {
log.info("{} started", getClass().getSimpleName());
- return this;
}
@Override
diff --git a/src/main/java/com/gitblit/tickets/ITicketService.java b/src/main/java/com/gitblit/tickets/ITicketService.java
index 20b6505b..3252a603 100644
--- a/src/main/java/com/gitblit/tickets/ITicketService.java
+++ b/src/main/java/com/gitblit/tickets/ITicketService.java
@@ -181,7 +181,24 @@ public abstract class ITicketService implements IManager {
* @since 1.4.0
*/
@Override
- public abstract ITicketService start();
+ public final ITicketService start() {
+ onStart();
+ if (shouldReindex()) {
+ log.info("Re-indexing all tickets...");
+// long startTime = System.currentTimeMillis();
+ reindex();
+// float duration = (System.currentTimeMillis() - startTime) / 1000f;
+// log.info("Built Lucene index over all tickets in {} secs", duration);
+ }
+ return this;
+ }
+
+ /**
+ * Start the specific ticket service implementation.
+ *
+ * @since 1.9.0
+ */
+ public abstract void onStart();
/**
* Stop the service.
@@ -197,6 +214,12 @@ public abstract class ITicketService implements IManager {
}
/**
+ * Closes any open resources used by this service.
+ * @since 1.4.0
+ */
+ protected abstract void close();
+
+ /**
* Creates a ticket notifier. The ticket notifier is not thread-safe!
* @since 1.4.0
*/
@@ -274,12 +297,6 @@ public abstract class ITicketService implements IManager {
}
/**
- * Closes any open resources used by this service.
- * @since 1.4.0
- */
- protected abstract void close();
-
- /**
* Reset all caches in the service.
* @since 1.4.0
*/
@@ -1343,6 +1360,18 @@ public abstract class ITicketService implements IManager {
return indexer.queryFor(query, page, pageSize, sortBy, descending);
}
+
+ /**
+ * Checks tickets should get re-indexed.
+ *
+ * @return true if tickets should get re-indexed, false otherwise.
+ */
+ private boolean shouldReindex()
+ {
+ return indexer.shouldReindex();
+ }
+
+
/**
* Destroys an existing index and reindexes all tickets.
* This operation may be expensive and time-consuming.
diff --git a/src/main/java/com/gitblit/tickets/NullTicketService.java b/src/main/java/com/gitblit/tickets/NullTicketService.java
index 3947b945..050c6990 100644
--- a/src/main/java/com/gitblit/tickets/NullTicketService.java
+++ b/src/main/java/com/gitblit/tickets/NullTicketService.java
@@ -61,9 +61,8 @@ public class NullTicketService extends ITicketService {
}
@Override
- public NullTicketService start() {
+ public void onStart() {
log.info("{} started", getClass().getSimpleName());
- return this;
}
@Override
diff --git a/src/main/java/com/gitblit/tickets/RedisTicketService.java b/src/main/java/com/gitblit/tickets/RedisTicketService.java
index 0f9ad174..4e632317 100644
--- a/src/main/java/com/gitblit/tickets/RedisTicketService.java
+++ b/src/main/java/com/gitblit/tickets/RedisTicketService.java
@@ -83,12 +83,11 @@ public class RedisTicketService extends ITicketService {
}
@Override
- public RedisTicketService start() {
+ public void onStart() {
log.info("{} started", getClass().getSimpleName());
if (!isReady()) {
log.warn("{} is not ready!", getClass().getSimpleName());
}
- return this;
}
@Override
diff --git a/src/main/java/com/gitblit/tickets/TicketIndexer.java b/src/main/java/com/gitblit/tickets/TicketIndexer.java
index e2d53af7..7c164487 100644
--- a/src/main/java/com/gitblit/tickets/TicketIndexer.java
+++ b/src/main/java/com/gitblit/tickets/TicketIndexer.java
@@ -31,6 +31,8 @@ import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field.Store;
import org.apache.lucene.document.IntField;
import org.apache.lucene.document.LongField;
+import org.apache.lucene.document.NumericDocValuesField;
+import org.apache.lucene.document.SortedDocValuesField;
import org.apache.lucene.document.TextField;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexWriter;
@@ -49,7 +51,7 @@ import org.apache.lucene.search.TopFieldDocs;
import org.apache.lucene.search.TopScoreDocCollector;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
-import org.apache.lucene.util.Version;
+import org.apache.lucene.util.BytesRef;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -60,7 +62,7 @@ import com.gitblit.models.TicketModel;
import com.gitblit.models.TicketModel.Attachment;
import com.gitblit.models.TicketModel.Patchset;
import com.gitblit.models.TicketModel.Status;
-import com.gitblit.utils.FileUtils;
+import com.gitblit.utils.LuceneIndexStore;
import com.gitblit.utils.StringUtils;
/**
@@ -108,6 +110,8 @@ public class TicketIndexer {
priority(Type.INT),
severity(Type.INT);
+ final static int INDEX_VERSION = 2;
+
final Type fieldType;
Lucene(Type fieldType) {
@@ -167,16 +171,15 @@ public class TicketIndexer {
private final Logger log = LoggerFactory.getLogger(getClass());
- private final Version luceneVersion = Version.LUCENE_46;
-
- private final File luceneDir;
+ private final LuceneIndexStore indexStore;
private IndexWriter writer;
private IndexSearcher searcher;
public TicketIndexer(IRuntimeManager runtimeManager) {
- this.luceneDir = runtimeManager.getFileOrFolder(Keys.tickets.indexFolder, "${baseFolder}/tickets/lucene");
+ File luceneDir = runtimeManager.getFileOrFolder(Keys.tickets.indexFolder, "${baseFolder}/tickets/lucene");
+ this.indexStore = new LuceneIndexStore(luceneDir, Lucene.INDEX_VERSION);
}
/**
@@ -192,7 +195,7 @@ public class TicketIndexer {
*/
public void deleteAll() {
close();
- FileUtils.delete(luceneDir);
+ indexStore.delete();
}
/**
@@ -201,10 +204,9 @@ public class TicketIndexer {
public boolean deleteAll(RepositoryModel repository) {
try {
IndexWriter writer = getWriter();
- StandardAnalyzer analyzer = new StandardAnalyzer(luceneVersion);
- QueryParser qp = new QueryParser(luceneVersion, Lucene.rid.name(), analyzer);
- BooleanQuery query = new BooleanQuery();
- query.add(qp.parse(repository.getRID()), Occur.MUST);
+ StandardAnalyzer analyzer = new StandardAnalyzer();
+ QueryParser qp = new QueryParser(Lucene.rid.name(), analyzer);
+ BooleanQuery query = new BooleanQuery.Builder().add(qp.parse(repository.getRID()), Occur.MUST).build();
int numDocsBefore = writer.numDocs();
writer.deleteDocuments(query);
@@ -225,6 +227,18 @@ public class TicketIndexer {
}
/**
+ * Checks if a tickets index exists, that is compatible with Lucene.INDEX_VERSION
+ * and the Lucene codec version.
+ *
+ * @return true if no tickets index is found, false otherwise.
+ *
+ * @since 1.9.0
+ */
+ boolean shouldReindex() {
+ return ! this.indexStore.hasIndex();
+ }
+
+ /**
* Bulk Add/Update tickets in the Lucene index
*
* @param tickets
@@ -287,10 +301,9 @@ public class TicketIndexer {
* @return true, if deleted, false if no record was deleted
*/
private boolean delete(String repository, long ticketId, IndexWriter writer) throws Exception {
- StandardAnalyzer analyzer = new StandardAnalyzer(luceneVersion);
- QueryParser qp = new QueryParser(luceneVersion, Lucene.did.name(), analyzer);
- BooleanQuery query = new BooleanQuery();
- query.add(qp.parse(StringUtils.getSHA1(repository + ticketId)), Occur.MUST);
+ StandardAnalyzer analyzer = new StandardAnalyzer();
+ QueryParser qp = new QueryParser(Lucene.did.name(), analyzer);
+ BooleanQuery query = new BooleanQuery.Builder().add(qp.parse(StringUtils.getSHA1(repository + ticketId)), Occur.MUST).build();
int numDocsBefore = writer.numDocs();
writer.deleteDocuments(query);
@@ -331,30 +344,30 @@ public class TicketIndexer {
return Collections.emptyList();
}
Set<QueryResult> results = new LinkedHashSet<QueryResult>();
- StandardAnalyzer analyzer = new StandardAnalyzer(luceneVersion);
+ StandardAnalyzer analyzer = new StandardAnalyzer();
try {
// search the title, description and content
- BooleanQuery query = new BooleanQuery();
+ BooleanQuery.Builder bldr = new BooleanQuery.Builder();
QueryParser qp;
- qp = new QueryParser(luceneVersion, Lucene.title.name(), analyzer);
+ qp = new QueryParser(Lucene.title.name(), analyzer);
qp.setAllowLeadingWildcard(true);
- query.add(qp.parse(text), Occur.SHOULD);
+ bldr.add(qp.parse(text), Occur.SHOULD);
- qp = new QueryParser(luceneVersion, Lucene.body.name(), analyzer);
+ qp = new QueryParser(Lucene.body.name(), analyzer);
qp.setAllowLeadingWildcard(true);
- query.add(qp.parse(text), Occur.SHOULD);
+ bldr.add(qp.parse(text), Occur.SHOULD);
- qp = new QueryParser(luceneVersion, Lucene.content.name(), analyzer);
+ qp = new QueryParser(Lucene.content.name(), analyzer);
qp.setAllowLeadingWildcard(true);
- query.add(qp.parse(text), Occur.SHOULD);
+ bldr.add(qp.parse(text), Occur.SHOULD);
IndexSearcher searcher = getSearcher();
- Query rewrittenQuery = searcher.rewrite(query);
+ Query rewrittenQuery = searcher.rewrite(bldr.build());
log.debug(rewrittenQuery.toString());
- TopScoreDocCollector collector = TopScoreDocCollector.create(5000, true);
+ TopScoreDocCollector collector = TopScoreDocCollector.create(5000);
searcher.search(rewrittenQuery, collector);
int offset = Math.max(0, (page - 1) * pageSize);
ScoreDoc[] hits = collector.topDocs(offset, pageSize).scoreDocs;
@@ -392,9 +405,9 @@ public class TicketIndexer {
}
Set<QueryResult> results = new LinkedHashSet<QueryResult>();
- StandardAnalyzer analyzer = new StandardAnalyzer(luceneVersion);
+ StandardAnalyzer analyzer = new StandardAnalyzer();
try {
- QueryParser qp = new QueryParser(luceneVersion, Lucene.content.name(), analyzer);
+ QueryParser qp = new QueryParser(Lucene.content.name(), analyzer);
Query query = qp.parse(queryText);
IndexSearcher searcher = getSearcher();
@@ -409,7 +422,7 @@ public class TicketIndexer {
sort = new Sort(Lucene.fromString(sortBy).asSortField(desc));
}
int maxSize = 5000;
- TopFieldDocs docs = searcher.search(rewrittenQuery, null, maxSize, sort, false, false);
+ TopFieldDocs docs = searcher.search(rewrittenQuery, maxSize, sort, false, false);
int size = (pageSize <= 0) ? maxSize : pageSize;
int offset = Math.max(0, (page - 1) * size);
ScoreDoc[] hits = subset(docs.scoreDocs, offset, size);
@@ -443,14 +456,11 @@ public class TicketIndexer {
private IndexWriter getWriter() throws IOException {
if (writer == null) {
- Directory directory = FSDirectory.open(luceneDir);
-
- if (!luceneDir.exists()) {
- luceneDir.mkdirs();
- }
+ indexStore.create();
- StandardAnalyzer analyzer = new StandardAnalyzer(luceneVersion);
- IndexWriterConfig config = new IndexWriterConfig(luceneVersion, analyzer);
+ Directory directory = FSDirectory.open(indexStore.getPath());
+ StandardAnalyzer analyzer = new StandardAnalyzer();
+ IndexWriterConfig config = new IndexWriterConfig(analyzer);
config.setOpenMode(OpenMode.CREATE_OR_APPEND);
writer = new IndexWriter(directory, config);
}
@@ -554,14 +564,17 @@ public class TicketIndexer {
return;
}
doc.add(new LongField(lucene.name(), value.getTime(), Store.YES));
+ doc.add(new NumericDocValuesField(lucene.name(), value.getTime()));
}
private void toDocField(Document doc, Lucene lucene, long value) {
doc.add(new LongField(lucene.name(), value, Store.YES));
+ doc.add(new NumericDocValuesField(lucene.name(), value));
}
private void toDocField(Document doc, Lucene lucene, int value) {
doc.add(new IntField(lucene.name(), value, Store.YES));
+ doc.add(new NumericDocValuesField(lucene.name(), value));
}
private void toDocField(Document doc, Lucene lucene, String value) {
@@ -569,6 +582,7 @@ public class TicketIndexer {
return;
}
doc.add(new org.apache.lucene.document.Field(lucene.name(), value, TextField.TYPE_STORED));
+ doc.add(new SortedDocValuesField(lucene.name(), new BytesRef(value)));
}
/**
@@ -663,4 +677,4 @@ public class TicketIndexer {
int i = Integer.parseInt(val);
return i;
}
-} \ No newline at end of file
+}
diff --git a/src/main/java/com/gitblit/tickets/TicketNotifier.java b/src/main/java/com/gitblit/tickets/TicketNotifier.java
index 8c7fe6d4..b913db25 100644
--- a/src/main/java/com/gitblit/tickets/TicketNotifier.java
+++ b/src/main/java/com/gitblit/tickets/TicketNotifier.java
@@ -573,10 +573,10 @@ public class TicketNotifier {
// cc users mentioned in last comment
Change lastChange = ticket.changes.get(ticket.changes.size() - 1);
if (lastChange.hasComment()) {
- Pattern p = Pattern.compile("\\s@([A-Za-z0-9-_]+)");
+ Pattern p = Pattern.compile(Constants.REGEX_TICKET_MENTION);
Matcher m = p.matcher(lastChange.comment.text);
while (m.find()) {
- String username = m.group();
+ String username = m.group("user");
ccs.add(username);
}
}
diff --git a/src/main/java/com/gitblit/transport/ssh/IPublicKeyManager.java b/src/main/java/com/gitblit/transport/ssh/IPublicKeyManager.java
index 1e74b2f0..ffe64f59 100644
--- a/src/main/java/com/gitblit/transport/ssh/IPublicKeyManager.java
+++ b/src/main/java/com/gitblit/transport/ssh/IPublicKeyManager.java
@@ -25,6 +25,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.gitblit.manager.IManager;
+import com.gitblit.models.UserModel;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.CacheLoader.InvalidCacheLoadException;
@@ -99,4 +100,16 @@ public abstract class IPublicKeyManager implements IManager {
public abstract boolean removeKey(String username, SshKey key);
public abstract boolean removeAllKeys(String username);
+
+ public boolean supportsWritingKeys(UserModel user) {
+ return (user != null);
+ }
+
+ public boolean supportsCommentChanges(UserModel user) {
+ return (user != null);
+ }
+
+ public boolean supportsPermissionChanges(UserModel user) {
+ return (user != null);
+ }
}
diff --git a/src/main/java/com/gitblit/transport/ssh/LdapKeyManager.java b/src/main/java/com/gitblit/transport/ssh/LdapKeyManager.java
new file mode 100644
index 00000000..9b494027
--- /dev/null
+++ b/src/main/java/com/gitblit/transport/ssh/LdapKeyManager.java
@@ -0,0 +1,435 @@
+/*
+ * Copyright 2016 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.gitblit.transport.ssh;
+
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
+import org.apache.sshd.common.config.keys.KeyUtils;
+import org.apache.sshd.common.util.GenericUtils;
+
+import com.gitblit.IStoredSettings;
+import com.gitblit.Keys;
+import com.gitblit.Constants.AccessPermission;
+import com.gitblit.ldap.LdapConnection;
+import com.gitblit.models.UserModel;
+import com.gitblit.utils.StringUtils;
+import com.google.common.base.Joiner;
+import com.google.inject.Inject;
+import com.unboundid.ldap.sdk.BindResult;
+import com.unboundid.ldap.sdk.ResultCode;
+import com.unboundid.ldap.sdk.SearchResult;
+import com.unboundid.ldap.sdk.SearchResultEntry;
+
+/**
+ * LDAP-only public key manager
+ *
+ * Retrieves public keys from user's LDAP entries. Using this key manager,
+ * no SSH keys can be edited, i.e. added, removed, permissions changed, etc.
+ *
+ * This key manager supports SSH key entries in LDAP of the following form:
+ * [<prefix>:] [<options>] <type> <key> [<comment>]
+ * This follows the required form of entries in the authenticated_keys file,
+ * with an additional optional prefix. Key entries must have a key type
+ * (like "ssh-rsa") and a key, and may have a comment at the end.
+ *
+ * An entry may specify login options as specified for the authorized_keys file.
+ * The 'environment' option may be used to set the permissions for the key
+ * by setting a 'gbPerm' environment variable. The key manager will interpret
+ * such a environment variable option and use the set permission string to set
+ * the permission on the key in Gitblit. Example:
+ * environment="gbPerm=V",pty ssh-rsa AAAxjka.....dv= Clone only key
+ * Above entry would create a RSA key with the comment "Clone only key" and
+ * set the key permission to CLONE. All other options are ignored.
+ *
+ * In Active Directory SSH public keys are sometimes stored in the attribute
+ * 'altSecurityIdentity'. The attribute value is usually prefixed by a type
+ * identifier. LDAP entries could have the following attribute values:
+ * altSecurityIdentity: X.509: ADKEJBAKDBZUPABBD...
+ * altSecurityIdentity: SshKey: ssh-dsa AAAAknenazuzucbhda...
+ * This key manager supports this by allowing an optional prefix to identify
+ * SSH keys. The prefix to be used should be set in the 'realm.ldap.sshPublicKey'
+ * setting by separating it from the attribute name with a colon, e.g.:
+ * realm.ldap.sshPublicKey = altSecurityIdentity:SshKey
+ *
+ * @author Florian Zschocke
+ *
+ */
+public class LdapKeyManager extends IPublicKeyManager {
+
+ /**
+ * Pattern to find prefixes like 'SSHKey:' in key entries.
+ * These prefixes describe the type of an altSecurityIdentity.
+ * The pattern accepts anything but quote and colon up to the
+ * first colon at the start of a string.
+ */
+ private static final Pattern PREFIX_PATTERN = Pattern.compile("^([^\":]+):");
+ /**
+ * Pattern to find the string describing Gitblit permissions for a SSH key.
+ * The pattern matches on a string starting with 'gbPerm', matched case-insensitive,
+ * followed by '=' with optional whitespace around it, followed by a string of
+ * upper and lower case letters and '+' and '-' for the permission, which can optionally
+ * be enclosed in '"' or '\"' (only the leading quote is matched in the pattern).
+ * Only the group describing the permission is a capturing group.
+ */
+ private static final Pattern GB_PERM_PATTERN = Pattern.compile("(?i:gbPerm)\\s*=\\s*(?:\\\\\"|\")?\\s*([A-Za-z+-]+)");
+
+
+ private final IStoredSettings settings;
+
+
+
+ @Inject
+ public LdapKeyManager(IStoredSettings settings) {
+ this.settings = settings;
+ }
+
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName();
+ }
+
+ @Override
+ public LdapKeyManager start() {
+ log.info(toString());
+ return this;
+ }
+
+ @Override
+ public boolean isReady() {
+ return true;
+ }
+
+ @Override
+ public LdapKeyManager stop() {
+ return this;
+ }
+
+ @Override
+ protected boolean isStale(String username) {
+ // always return true so we gets keys from LDAP every time
+ return true;
+ }
+
+ @Override
+ protected List<SshKey> getKeysImpl(String username) {
+ try (LdapConnection conn = new LdapConnection(settings)) {
+ if (conn.connect()) {
+ log.info("loading ssh key for {} from LDAP directory", username);
+
+ BindResult bindResult = conn.bind();
+ if (bindResult == null) {
+ conn.close();
+ return null;
+ }
+
+ // Search the user entity
+
+ // Support prefixing the key data, e.g. when using altSecurityIdentities in AD.
+ String pubKeyAttribute = settings.getString(Keys.realm.ldap.sshPublicKey, "sshPublicKey");
+ String pkaPrefix = null;
+ int idx = pubKeyAttribute.indexOf(':');
+ if (idx > 0) {
+ pkaPrefix = pubKeyAttribute.substring(idx +1);
+ pubKeyAttribute = pubKeyAttribute.substring(0, idx);
+ }
+
+ SearchResult result = conn.searchUser(getSimpleUsername(username), Arrays.asList(pubKeyAttribute));
+ conn.close();
+
+ if (result != null && result.getResultCode() == ResultCode.SUCCESS) {
+ if ( result.getEntryCount() > 1) {
+ log.info("Found more than one entry for user {} in LDAP. Cannot retrieve SSH key.", username);
+ return null;
+ } else if ( result.getEntryCount() < 1) {
+ log.info("Found no entry for user {} in LDAP. Cannot retrieve SSH key.", username);
+ return null;
+ }
+
+ // Retrieve the SSH key attributes
+ SearchResultEntry foundUser = result.getSearchEntries().get(0);
+ String[] attrs = foundUser.getAttributeValues(pubKeyAttribute);
+ if (attrs == null ||attrs.length == 0) {
+ log.info("found no keys for user {} under attribute {} in directory", username, pubKeyAttribute);
+ return null;
+ }
+
+
+ // Filter resulting list to match with required special prefix in entry
+ List<GbAuthorizedKeyEntry> authorizedKeys = new ArrayList<>(attrs.length);
+ Matcher m = PREFIX_PATTERN.matcher("");
+ for (int i = 0; i < attrs.length; ++i) {
+ // strip out line breaks
+ String keyEntry = Joiner.on("").join(attrs[i].replace("\r\n", "\n").split("\n"));
+ m.reset(keyEntry);
+ try {
+ if (m.lookingAt()) { // Key is prefixed in LDAP
+ if (pkaPrefix == null) {
+ continue;
+ }
+ String prefix = m.group(1).trim();
+ if (! pkaPrefix.equalsIgnoreCase(prefix)) {
+ continue;
+ }
+ String s = keyEntry.substring(m.end()); // Strip prefix off
+ authorizedKeys.add(GbAuthorizedKeyEntry.parseAuthorizedKeyEntry(s));
+
+ } else { // Key is not prefixed in LDAP
+ if (pkaPrefix != null) {
+ continue;
+ }
+ String s = keyEntry; // Strip prefix off
+ authorizedKeys.add(GbAuthorizedKeyEntry.parseAuthorizedKeyEntry(s));
+ }
+ } catch (IllegalArgumentException e) {
+ log.info("Failed to parse key entry={}:", keyEntry, e.getMessage());
+ }
+ }
+
+ List<SshKey> keyList = new ArrayList<>(authorizedKeys.size());
+ for (GbAuthorizedKeyEntry keyEntry : authorizedKeys) {
+ try {
+ SshKey key = new SshKey(keyEntry.resolvePublicKey(null));
+ key.setComment(keyEntry.getComment());
+ setKeyPermissions(key, keyEntry);
+ keyList.add(key);
+ } catch (GeneralSecurityException | IOException e) {
+ log.warn("Error resolving key entry for user {}. Entry={}", username, keyEntry, e);
+ }
+ }
+ return keyList;
+ }
+ }
+ }
+
+ return null;
+ }
+
+
+ @Override
+ public boolean addKey(String username, SshKey key) {
+ return false;
+ }
+
+ @Override
+ public boolean removeKey(String username, SshKey key) {
+ return false;
+ }
+
+ @Override
+ public boolean removeAllKeys(String username) {
+ return false;
+ }
+
+
+ public boolean supportsWritingKeys(UserModel user) {
+ return false;
+ }
+
+ public boolean supportsCommentChanges(UserModel user) {
+ return false;
+ }
+
+ public boolean supportsPermissionChanges(UserModel user) {
+ return false;
+ }
+
+
+ private void setKeyPermissions(SshKey key, GbAuthorizedKeyEntry keyEntry) {
+ List<String> env = keyEntry.getLoginOptionValues("environment");
+ if (env != null && !env.isEmpty()) {
+ // Walk over all entries and find one that sets 'gbPerm'. The last one wins.
+ for (String envi : env) {
+ Matcher m = GB_PERM_PATTERN.matcher(envi);
+ if (m.find()) {
+ String perm = m.group(1).trim();
+ AccessPermission ap = AccessPermission.fromCode(perm);
+ if (ap == AccessPermission.NONE) {
+ ap = AccessPermission.valueOf(perm.toUpperCase());
+ }
+
+ if (ap != null && ap != AccessPermission.NONE) {
+ try {
+ key.setPermission(ap);
+ } catch (IllegalArgumentException e) {
+ log.warn("Incorrect permissions ({}) set for SSH key entry {}.", ap, envi, e);
+ }
+ }
+ }
+ }
+ }
+ }
+
+
+ /**
+ * Returns a simple username without any domain prefixes.
+ *
+ * @param username
+ * @return a simple username
+ */
+ private String getSimpleUsername(String username) {
+ int lastSlash = username.lastIndexOf('\\');
+ if (lastSlash > -1) {
+ username = username.substring(lastSlash + 1);
+ }
+
+ return username;
+ }
+
+
+ /**
+ * Extension of the AuthorizedKeyEntry from Mina SSHD with better option parsing.
+ *
+ * The class makes use of code from the two methods copied from the original
+ * Mina SSHD AuthorizedKeyEntry class. The code is rewritten to improve user login
+ * option support. Options are correctly parsed even if they have whitespace within
+ * double quotes. Options can occur multiple times, which is needed for example for
+ * the "environment" option. Thus for an option a list of strings is kept, holding
+ * multiple option values.
+ */
+ private static class GbAuthorizedKeyEntry extends AuthorizedKeyEntry {
+
+ private static final long serialVersionUID = 1L;
+ /**
+ * Pattern to extract the first part of the key entry without whitespace or only with quoted whitespace.
+ * The pattern essentially splits the line in two parts with two capturing groups. All other groups
+ * in the pattern are non-capturing. The first part is a continuous string that only includes double quoted
+ * whitespace and ends in whitespace. The second part is the rest of the line.
+ * The first part is at the beginning of the line, the lead-in. For a SSH key entry this can either be
+ * login options (see authorized keys file description) or the key type. Since options, other than the
+ * key type, can include whitespace and escaped double quotes within double quotes, the pattern takes
+ * care of that by searching for either "characters that are not whitespace and not double quotes"
+ * or "a double quote, followed by 'characters that are not a double quote or backslash, or a backslash
+ * and then a double quote, or a backslash', followed by a double quote".
+ */
+ private static final Pattern LEADIN_PATTERN = Pattern.compile("^((?:[^\\s\"]*|(?:\"(?:[^\"\\\\]|\\\\\"|\\\\)*\"))*\\s+)(.+)");
+ /**
+ * Pattern to split a comma separated list of options.
+ * Since an option could contain commas (as well as escaped double quotes) within double quotes
+ * in the option value, a simple split on comma is not enough. So the pattern searches for multiple
+ * occurrences of:
+ * characters that are not double quotes or a comma, or
+ * a double quote followed by: characters that are not a double quote or backslash, or
+ * a backslash and then a double quote, or
+ * a backslash,
+ * followed by a double quote.
+ */
+ private static final Pattern OPTION_PATTERN = Pattern.compile("([^\",]+|(?:\"(?:[^\"\\\\]|\\\\\"|\\\\)*\"))+");
+
+ // for options that have no value, "true" is used
+ private Map<String, List<String>> loginOptionsMulti = Collections.emptyMap();
+
+
+ List<String> getLoginOptionValues(String option) {
+ return loginOptionsMulti.get(option);
+ }
+
+
+
+ /**
+ * @param line Original line from an <code>authorized_keys</code> file
+ * @return {@link GbAuthorizedKeyEntry} or {@code null} if the line is
+ * {@code null}/empty or a comment line
+ * @throws IllegalArgumentException If failed to parse/decode the line
+ * @see #COMMENT_CHAR
+ */
+ public static GbAuthorizedKeyEntry parseAuthorizedKeyEntry(String line) throws IllegalArgumentException {
+ line = GenericUtils.trimToEmpty(line);
+ if (StringUtils.isEmpty(line) || (line.charAt(0) == COMMENT_CHAR) /* comment ? */) {
+ return null;
+ }
+
+ Matcher m = LEADIN_PATTERN.matcher(line);
+ if (! m.lookingAt()) {
+ throw new IllegalArgumentException("Bad format (no key data delimiter): " + line);
+ }
+
+ String keyType = m.group(1).trim();
+ final GbAuthorizedKeyEntry entry;
+ if (KeyUtils.getPublicKeyEntryDecoder(keyType) == null) { // assume this is due to the fact that it starts with login options
+ entry = parseAuthorizedKeyEntry(m.group(2));
+ if (entry == null) {
+ throw new IllegalArgumentException("Bad format (no key data after login options): " + line);
+ }
+
+ entry.parseAndSetLoginOptions(keyType);
+ } else {
+ int startPos = line.indexOf(' ');
+ if (startPos <= 0) {
+ throw new IllegalArgumentException("Bad format (no key data delimiter): " + line);
+ }
+
+ int endPos = line.indexOf(' ', startPos + 1);
+ if (endPos <= startPos) {
+ endPos = line.length();
+ }
+
+ String encData = (endPos < (line.length() - 1)) ? line.substring(0, endPos).trim() : line;
+ String comment = (endPos < (line.length() - 1)) ? line.substring(endPos + 1).trim() : null;
+ entry = parsePublicKeyEntry(new GbAuthorizedKeyEntry(), encData);
+ entry.setComment(comment);
+ }
+
+ return entry;
+ }
+
+ private void parseAndSetLoginOptions(String options) {
+ Matcher m = OPTION_PATTERN.matcher(options);
+ if (! m.find()) {
+ loginOptionsMulti = Collections.emptyMap();
+ }
+ Map<String, List<String>> optsMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
+
+ do {
+ String p = m.group();
+ p = GenericUtils.trimToEmpty(p);
+ if (StringUtils.isEmpty(p)) {
+ continue;
+ }
+
+ int pos = p.indexOf('=');
+ String name = (pos < 0) ? p : GenericUtils.trimToEmpty(p.substring(0, pos));
+ CharSequence value = (pos < 0) ? null : GenericUtils.trimToEmpty(p.substring(pos + 1));
+ value = GenericUtils.stripQuotes(value);
+
+ // For options without value the value is set to TRUE.
+ if (value == null) {
+ value = Boolean.TRUE.toString();
+ }
+
+ List<String> opts = optsMap.get(name);
+ if (opts == null) {
+ opts = new ArrayList<String>();
+ optsMap.put(name, opts);
+ }
+ opts.add(value.toString());
+ } while(m.find());
+
+ loginOptionsMulti = optsMap;
+ }
+ }
+
+}
diff --git a/src/main/java/com/gitblit/transport/ssh/NonForwardingFilter.java b/src/main/java/com/gitblit/transport/ssh/NonForwardingFilter.java
index 29f7750d..f2176cb0 100644
--- a/src/main/java/com/gitblit/transport/ssh/NonForwardingFilter.java
+++ b/src/main/java/com/gitblit/transport/ssh/NonForwardingFilter.java
@@ -15,8 +15,8 @@
*/
package com.gitblit.transport.ssh;
-import org.apache.sshd.common.SshdSocketAddress;
import org.apache.sshd.common.session.Session;
+import org.apache.sshd.common.util.net.SshdSocketAddress;
import org.apache.sshd.server.forward.ForwardingFilter;
public class NonForwardingFilter implements ForwardingFilter {
diff --git a/src/main/java/com/gitblit/transport/ssh/SshDaemon.java b/src/main/java/com/gitblit/transport/ssh/SshDaemon.java
index 5a94c9a3..63fa51dd 100644
--- a/src/main/java/com/gitblit/transport/ssh/SshDaemon.java
+++ b/src/main/java/com/gitblit/transport/ssh/SshDaemon.java
@@ -23,6 +23,7 @@ import java.net.InetSocketAddress;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.text.MessageFormat;
+import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.sshd.common.io.IoServiceFactoryFactory;
@@ -30,7 +31,7 @@ import org.apache.sshd.common.io.mina.MinaServiceFactoryFactory;
import org.apache.sshd.common.io.nio2.Nio2ServiceFactoryFactory;
import org.apache.sshd.common.util.SecurityUtils;
import org.apache.sshd.server.SshServer;
-import org.apache.sshd.server.auth.CachingPublicKeyAuthenticator;
+import org.apache.sshd.server.auth.pubkey.CachingPublicKeyAuthenticator;
import org.bouncycastle.openssl.PEMWriter;
import org.eclipse.jgit.internal.JGitText;
import org.slf4j.Logger;
@@ -55,6 +56,13 @@ public class SshDaemon {
private final Logger log = LoggerFactory.getLogger(SshDaemon.class);
+ private static final String AUTH_PUBLICKEY = "publickey";
+ private static final String AUTH_PASSWORD = "password";
+ private static final String AUTH_KBD_INTERACTIVE = "keyboard-interactive";
+ private static final String AUTH_GSSAPI = "gssapi-with-mic";
+
+
+
public static enum SshSessionBackend {
MINA, NIO2
}
@@ -97,9 +105,6 @@ public class SshDaemon {
FileKeyPairProvider hostKeyPairProvider = new FileKeyPairProvider();
hostKeyPairProvider.setFiles(new String [] { rsaKeyStore.getPath(), dsaKeyStore.getPath(), dsaKeyStore.getPath() });
- // Client public key authenticator
- SshKeyAuthenticator keyAuthenticator =
- new SshKeyAuthenticator(gitblit.getPublicKeyManager(), gitblit);
// Configure the preferred SSHD backend
String sshBackendStr = settings.getString(Keys.git.sshBackend,
@@ -125,16 +130,39 @@ public class SshDaemon {
sshd.setPort(addr.getPort());
sshd.setHost(addr.getHostName());
sshd.setKeyPairProvider(hostKeyPairProvider);
- sshd.setPublickeyAuthenticator(new CachingPublicKeyAuthenticator(keyAuthenticator));
- sshd.setPasswordAuthenticator(new UsernamePasswordAuthenticator(gitblit));
- if (settings.getBoolean(Keys.git.sshWithKrb5, false)) {
+
+ List<String> authMethods = settings.getStrings(Keys.git.sshAuthenticationMethods);
+ if (authMethods.isEmpty()) {
+ authMethods.add(AUTH_PUBLICKEY);
+ authMethods.add(AUTH_PASSWORD);
+ }
+ // Keep backward compatibility with old setting files that use the git.sshWithKrb5 setting.
+ if (settings.getBoolean("git.sshWithKrb5", false) && !authMethods.contains(AUTH_GSSAPI)) {
+ authMethods.add(AUTH_GSSAPI);
+ log.warn("git.sshWithKrb5 is obsolete!");
+ log.warn("Please add {} to {} in gitblit.properties!", AUTH_GSSAPI, Keys.git.sshAuthenticationMethods);
+ settings.overrideSetting(Keys.git.sshAuthenticationMethods,
+ settings.getString(Keys.git.sshAuthenticationMethods, AUTH_PUBLICKEY + " " + AUTH_PASSWORD) + " " + AUTH_GSSAPI);
+ }
+ if (authMethods.contains(AUTH_PUBLICKEY)) {
+ SshKeyAuthenticator keyAuthenticator = new SshKeyAuthenticator(gitblit.getPublicKeyManager(), gitblit);
+ sshd.setPublickeyAuthenticator(new CachingPublicKeyAuthenticator(keyAuthenticator));
+ log.info("SSH: adding public key authentication method.");
+ }
+ if (authMethods.contains(AUTH_PASSWORD) || authMethods.contains(AUTH_KBD_INTERACTIVE)) {
+ sshd.setPasswordAuthenticator(new UsernamePasswordAuthenticator(gitblit));
+ log.info("SSH: adding password authentication method.");
+ }
+ if (authMethods.contains(AUTH_GSSAPI)) {
sshd.setGSSAuthenticator(new SshKrbAuthenticator(settings, gitblit));
+ log.info("SSH: adding GSSAPI authentication method.");
}
- sshd.setSessionFactory(new SshServerSessionFactory());
+
+ sshd.setSessionFactory(new SshServerSessionFactory(sshd));
sshd.setFileSystemFactory(new DisabledFilesystemFactory());
sshd.setTcpipForwardingFilter(new NonForwardingFilter());
sshd.setCommandFactory(new SshCommandFactory(gitblit, workQueue));
- sshd.setShellFactory(new WelcomeShell(settings));
+ sshd.setShellFactory(new WelcomeShell(gitblit));
// Set the server id. This can be queried with:
// ssh-keyscan -t rsa,dsa -p 29418 localhost
diff --git a/src/main/java/com/gitblit/transport/ssh/SshDaemonClient.java b/src/main/java/com/gitblit/transport/ssh/SshDaemonClient.java
index af25251b..7024a9a9 100644
--- a/src/main/java/com/gitblit/transport/ssh/SshDaemonClient.java
+++ b/src/main/java/com/gitblit/transport/ssh/SshDaemonClient.java
@@ -17,7 +17,7 @@ package com.gitblit.transport.ssh;
import java.net.SocketAddress;
-import org.apache.sshd.common.session.Session.AttributeKey;
+import org.apache.sshd.common.AttributeStore.AttributeKey;
import com.gitblit.models.UserModel;
diff --git a/src/main/java/com/gitblit/transport/ssh/SshServerSessionFactory.java b/src/main/java/com/gitblit/transport/ssh/SshServerSessionFactory.java
index bc67cec0..fb85781a 100644
--- a/src/main/java/com/gitblit/transport/ssh/SshServerSessionFactory.java
+++ b/src/main/java/com/gitblit/transport/ssh/SshServerSessionFactory.java
@@ -22,7 +22,8 @@ import org.apache.sshd.common.future.CloseFuture;
import org.apache.sshd.common.future.SshFutureListener;
import org.apache.sshd.common.io.IoSession;
import org.apache.sshd.common.io.mina.MinaSession;
-import org.apache.sshd.common.session.AbstractSession;
+import org.apache.sshd.server.ServerFactoryManager;
+import org.apache.sshd.server.session.ServerSessionImpl;
import org.apache.sshd.server.session.SessionFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -36,11 +37,12 @@ public class SshServerSessionFactory extends SessionFactory {
private final Logger log = LoggerFactory.getLogger(getClass());
- public SshServerSessionFactory() {
+ public SshServerSessionFactory(ServerFactoryManager server) {
+ super(server);
}
@Override
- protected AbstractSession createSession(final IoSession io) throws Exception {
+ protected ServerSessionImpl createSession(final IoSession io) throws Exception {
log.info("creating ssh session from {}", io.getRemoteAddress());
if (io instanceof MinaSession) {
@@ -66,7 +68,7 @@ public class SshServerSessionFactory extends SessionFactory {
}
@Override
- protected AbstractSession doCreateSession(IoSession ioSession) throws Exception {
+ protected ServerSessionImpl doCreateSession(IoSession ioSession) throws Exception {
return new SshServerSession(getServer(), ioSession);
}
}
diff --git a/src/main/java/com/gitblit/transport/ssh/WelcomeShell.java b/src/main/java/com/gitblit/transport/ssh/WelcomeShell.java
index ec6f7291..7c407d36 100644
--- a/src/main/java/com/gitblit/transport/ssh/WelcomeShell.java
+++ b/src/main/java/com/gitblit/transport/ssh/WelcomeShell.java
@@ -34,6 +34,7 @@ import org.eclipse.jgit.util.SystemReader;
import com.gitblit.IStoredSettings;
import com.gitblit.Keys;
+import com.gitblit.manager.IGitblit;
import com.gitblit.models.UserModel;
import com.gitblit.transport.ssh.commands.DispatchCommand;
import com.gitblit.transport.ssh.commands.SshCommandFactory;
@@ -45,19 +46,20 @@ import com.gitblit.utils.StringUtils;
*/
public class WelcomeShell implements Factory<Command> {
- private final IStoredSettings settings;
+ private final IGitblit gitblit;
- public WelcomeShell(IStoredSettings settings) {
- this.settings = settings;
+ public WelcomeShell(IGitblit gitblit) {
+ this.gitblit = gitblit;
}
@Override
public Command create() {
- return new SendMessage(settings);
+ return new SendMessage(gitblit);
}
private static class SendMessage implements Command, SessionAware {
+ private final IPublicKeyManager km;
private final IStoredSettings settings;
private ServerSession session;
@@ -66,8 +68,9 @@ public class WelcomeShell implements Factory<Command> {
private OutputStream err;
private ExitCallback exit;
- SendMessage(IStoredSettings settings) {
- this.settings = settings;
+ SendMessage(IGitblit gitblit) {
+ this.km = gitblit.getPublicKeyManager();
+ this.settings = gitblit.getSettings();
}
@Override
@@ -116,6 +119,10 @@ public class WelcomeShell implements Factory<Command> {
UserModel user = client.getUser();
String hostname = getHostname();
int port = settings.getInteger(Keys.git.sshPort, 0);
+ boolean writeKeysIsSupported = true;
+ if (km != null) {
+ writeKeysIsSupported = km.supportsWritingKeys(user);
+ }
final String b1 = StringUtils.rightPad("", 72, '═');
final String b2 = StringUtils.rightPad("", 72, '─');
@@ -159,7 +166,7 @@ public class WelcomeShell implements Factory<Command> {
msg.append(nl);
msg.append(nl);
- if (client.getKey() == null) {
+ if (writeKeysIsSupported && client.getKey() == null) {
// user has authenticated with a password
// display add public key instructions
msg.append(" You may upload an SSH public key with the following syntax:");
diff --git a/src/main/java/com/gitblit/transport/ssh/keys/KeysDispatcher.java b/src/main/java/com/gitblit/transport/ssh/keys/KeysDispatcher.java
index da58584c..817a98ff 100644
--- a/src/main/java/com/gitblit/transport/ssh/keys/KeysDispatcher.java
+++ b/src/main/java/com/gitblit/transport/ssh/keys/KeysDispatcher.java
@@ -25,6 +25,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.gitblit.Constants.AccessPermission;
+import com.gitblit.models.UserModel;
import com.gitblit.transport.ssh.IPublicKeyManager;
import com.gitblit.transport.ssh.SshKey;
import com.gitblit.transport.ssh.commands.CommandMetaData;
@@ -47,12 +48,20 @@ public class KeysDispatcher extends DispatchCommand {
@Override
protected void setup() {
- register(AddKey.class);
- register(RemoveKey.class);
+ IPublicKeyManager km = getContext().getGitblit().getPublicKeyManager();
+ UserModel user = getContext().getClient().getUser();
+ if (km != null && km.supportsWritingKeys(user)) {
+ register(AddKey.class);
+ register(RemoveKey.class);
+ }
register(ListKeys.class);
register(WhichKey.class);
- register(CommentKey.class);
- register(PermissionKey.class);
+ if (km != null && km.supportsCommentChanges(user)) {
+ register(CommentKey.class);
+ }
+ if (km != null && km.supportsPermissionChanges(user)) {
+ register(PermissionKey.class);
+ }
}
@CommandMetaData(name = "add", description = "Add an SSH public key to your account")
diff --git a/src/main/java/com/gitblit/utils/ArrayUtils.java b/src/main/java/com/gitblit/utils/ArrayUtils.java
index 1402ad5e..b850ccc9 100644
--- a/src/main/java/com/gitblit/utils/ArrayUtils.java
+++ b/src/main/java/com/gitblit/utils/ArrayUtils.java
@@ -42,7 +42,7 @@ public class ArrayUtils {
}
public static boolean isEmpty(Collection<?> collection) {
- return collection == null || collection.size() == 0;
+ return collection == null || collection.isEmpty();
}
public static String toString(Collection<?> collection) {
diff --git a/src/main/java/com/gitblit/utils/CommitCache.java b/src/main/java/com/gitblit/utils/CommitCache.java
index a3963f50..53b8de19 100644
--- a/src/main/java/com/gitblit/utils/CommitCache.java
+++ b/src/main/java/com/gitblit/utils/CommitCache.java
@@ -19,9 +19,9 @@ import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import org.eclipse.jgit.lib.ObjectId;
@@ -58,7 +58,7 @@ public class CommitCache {
}
protected CommitCache() {
- cache = new ConcurrentHashMap<String, ObjectCache<List<RepositoryCommit>>>();
+ cache = new HashMap<>();
}
/**
@@ -93,7 +93,9 @@ public class CommitCache {
*
*/
public void clear() {
- cache.clear();
+ synchronized (cache) {
+ cache.clear();
+ }
}
/**
@@ -103,8 +105,11 @@ public class CommitCache {
*/
public void clear(String repositoryName) {
String repoKey = repositoryName.toLowerCase();
- ObjectCache<List<RepositoryCommit>> repoCache = cache.remove(repoKey);
- if (repoCache != null) {
+ boolean hadEntries = false;
+ synchronized (cache) {
+ hadEntries = cache.remove(repoKey) != null;
+ }
+ if (hadEntries) {
logger.info(MessageFormat.format("{0} commit cache cleared", repositoryName));
}
}
@@ -117,13 +122,17 @@ public class CommitCache {
*/
public void clear(String repositoryName, String branch) {
String repoKey = repositoryName.toLowerCase();
- ObjectCache<List<RepositoryCommit>> repoCache = cache.get(repoKey);
- if (repoCache != null) {
- List<RepositoryCommit> commits = repoCache.remove(branch.toLowerCase());
- if (!ArrayUtils.isEmpty(commits)) {
- logger.info(MessageFormat.format("{0}:{1} commit cache cleared", repositoryName, branch));
+ boolean hadEntries = false;
+ synchronized (cache) {
+ ObjectCache<List<RepositoryCommit>> repoCache = cache.get(repoKey);
+ if (repoCache != null) {
+ List<RepositoryCommit> commits = repoCache.remove(branch.toLowerCase());
+ hadEntries = !ArrayUtils.isEmpty(commits);
}
}
+ if (hadEntries) {
+ logger.info(MessageFormat.format("{0}:{1} commit cache cleared", repositoryName, branch));
+ }
}
/**
@@ -156,49 +165,55 @@ public class CommitCache {
if (cacheDays > 0 && (sinceDate.getTime() >= cacheCutoffDate.getTime())) {
// request fits within the cache window
String repoKey = repositoryName.toLowerCase();
- if (!cache.containsKey(repoKey)) {
- cache.put(repoKey, new ObjectCache<List<RepositoryCommit>>());
- }
-
- ObjectCache<List<RepositoryCommit>> repoCache = cache.get(repoKey);
String branchKey = branch.toLowerCase();
RevCommit tip = JGitUtils.getCommit(repository, branch);
Date tipDate = JGitUtils.getCommitDate(tip);
- List<RepositoryCommit> commits;
- if (!repoCache.hasCurrent(branchKey, tipDate)) {
- commits = repoCache.getObject(branchKey);
- if (ArrayUtils.isEmpty(commits)) {
- // we don't have any cached commits for this branch, reload
- commits = get(repositoryName, repository, branch, cacheCutoffDate);
- repoCache.updateObject(branchKey, tipDate, commits);
- logger.debug(MessageFormat.format("parsed {0} commits from {1}:{2} since {3,date,yyyy-MM-dd} in {4} msecs",
- commits.size(), repositoryName, branch, cacheCutoffDate, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start)));
- } else {
- // incrementally update cache since the last cached commit
- ObjectId sinceCommit = commits.get(0).getId();
- List<RepositoryCommit> incremental = get(repositoryName, repository, branch, sinceCommit);
- logger.info(MessageFormat.format("incrementally added {0} commits to cache for {1}:{2} in {3} msecs",
- incremental.size(), repositoryName, branch, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start)));
- incremental.addAll(commits);
- repoCache.updateObject(branchKey, tipDate, incremental);
- commits = incremental;
+ ObjectCache<List<RepositoryCommit>> repoCache;
+ synchronized (cache) {
+ repoCache = cache.get(repoKey);
+ if (repoCache == null) {
+ repoCache = new ObjectCache<>();
+ cache.put(repoKey, repoCache);
}
- } else {
- // cache is current
- commits = repoCache.getObject(branchKey);
- // evict older commits outside the cache window
- commits = reduce(commits, cacheCutoffDate);
- // update cache
- repoCache.updateObject(branchKey, tipDate, commits);
}
+ synchronized (repoCache) {
+ List<RepositoryCommit> commits;
+ if (!repoCache.hasCurrent(branchKey, tipDate)) {
+ commits = repoCache.getObject(branchKey);
+ if (ArrayUtils.isEmpty(commits)) {
+ // we don't have any cached commits for this branch, reload
+ commits = get(repositoryName, repository, branch, cacheCutoffDate);
+ repoCache.updateObject(branchKey, tipDate, commits);
+ logger.debug(MessageFormat.format("parsed {0} commits from {1}:{2} since {3,date,yyyy-MM-dd} in {4} msecs",
+ commits.size(), repositoryName, branch, cacheCutoffDate, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start)));
+ } else {
+ // incrementally update cache since the last cached commit
+ ObjectId sinceCommit = commits.get(0).getId();
+ List<RepositoryCommit> incremental = get(repositoryName, repository, branch, sinceCommit);
+ logger.info(MessageFormat.format("incrementally added {0} commits to cache for {1}:{2} in {3} msecs",
+ incremental.size(), repositoryName, branch, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start)));
+ incremental.addAll(commits);
+ repoCache.updateObject(branchKey, tipDate, incremental);
+ commits = incremental;
+ }
+ } else {
+ // cache is current
+ commits = repoCache.getObject(branchKey);
+ // evict older commits outside the cache window
+ commits = reduce(commits, cacheCutoffDate);
+ // update cache
+ repoCache.updateObject(branchKey, tipDate, commits);
+ }
- if (sinceDate.equals(cacheCutoffDate)) {
- list = commits;
- } else {
- // reduce the commits to those since the specified date
- list = reduce(commits, sinceDate);
+ if (sinceDate.equals(cacheCutoffDate)) {
+ // Mustn't hand out the cached list; that's not thread-safe
+ list = new ArrayList<>(commits);
+ } else {
+ // reduce the commits to those since the specified date
+ list = reduce(commits, sinceDate);
+ }
}
logger.debug(MessageFormat.format("retrieved {0} commits from cache of {1}:{2} since {3,date,yyyy-MM-dd} in {4} msecs",
list.size(), repositoryName, branch, sinceDate, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start)));
@@ -222,8 +237,9 @@ public class CommitCache {
*/
protected List<RepositoryCommit> get(String repositoryName, Repository repository, String branch, Date sinceDate) {
Map<ObjectId, List<RefModel>> allRefs = JGitUtils.getAllRefs(repository, false);
- List<RepositoryCommit> commits = new ArrayList<RepositoryCommit>();
- for (RevCommit commit : JGitUtils.getRevLog(repository, branch, sinceDate)) {
+ List<RevCommit> revLog = JGitUtils.getRevLog(repository, branch, sinceDate);
+ List<RepositoryCommit> commits = new ArrayList<RepositoryCommit>(revLog.size());
+ for (RevCommit commit : revLog) {
RepositoryCommit commitModel = new RepositoryCommit(repositoryName, branch, commit);
List<RefModel> commitRefs = allRefs.get(commitModel.getId());
commitModel.setRefs(commitRefs);
@@ -243,8 +259,9 @@ public class CommitCache {
*/
protected List<RepositoryCommit> get(String repositoryName, Repository repository, String branch, ObjectId sinceCommit) {
Map<ObjectId, List<RefModel>> allRefs = JGitUtils.getAllRefs(repository, false);
- List<RepositoryCommit> commits = new ArrayList<RepositoryCommit>();
- for (RevCommit commit : JGitUtils.getRevLog(repository, sinceCommit.getName(), branch)) {
+ List<RevCommit> revLog = JGitUtils.getRevLog(repository, sinceCommit.getName(), branch);
+ List<RepositoryCommit> commits = new ArrayList<RepositoryCommit>(revLog.size());
+ for (RevCommit commit : revLog) {
RepositoryCommit commitModel = new RepositoryCommit(repositoryName, branch, commit);
List<RefModel> commitRefs = allRefs.get(commitModel.getId());
commitModel.setRefs(commitRefs);
@@ -261,7 +278,7 @@ public class CommitCache {
* @return a list of commits
*/
protected List<RepositoryCommit> reduce(List<RepositoryCommit> commits, Date sinceDate) {
- List<RepositoryCommit> filtered = new ArrayList<RepositoryCommit>();
+ List<RepositoryCommit> filtered = new ArrayList<RepositoryCommit>(commits.size());
for (RepositoryCommit commit : commits) {
if (commit.getCommitDate().compareTo(sinceDate) >= 0) {
filtered.add(commit);
diff --git a/src/main/java/com/gitblit/utils/ContainerDetector.java b/src/main/java/com/gitblit/utils/ContainerDetector.java
new file mode 100644
index 00000000..8e4a071a
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/ContainerDetector.java
@@ -0,0 +1,74 @@
+package com.gitblit.utils;
+
+import org.slf4j.Logger;
+
+import java.io.File;
+
+/**
+ * The ContainerDetector tries to detect if the Gitblit instance
+ * is running in a container, or directly on the (virtualized) OS.
+ */
+public class ContainerDetector
+{
+ private static Boolean inContainer;
+ private static String detectedType = "";
+
+ /**
+ * Detect if this instance in running inside a container.
+ *
+ * @return true - if a container could be detected
+ * false - otherwise
+ */
+ public static boolean detect()
+ {
+ if (inContainer == null) {
+ File proc = new File("/proc/1/cgroup");
+ if (! proc.exists()) inContainer = Boolean.FALSE;
+ else {
+ String cgroups = FileUtils.readContent(proc, null);
+ if (cgroups.contains("/docker")) {
+ inContainer = Boolean.TRUE;
+ detectedType = "Docker container";
+ }
+ else if (cgroups.contains("/ecs")) {
+ inContainer = Boolean.TRUE;
+ detectedType = "ECS container";
+ }
+ else if (cgroups.contains("/kubepod") || cgroups.contains("/kubepods")) {
+ inContainer = Boolean.TRUE;
+ detectedType = "Kubernetes pod";
+ }
+ }
+
+ // Finally, if we still haven't found proof, it is probably not a container
+ if (inContainer == null) inContainer = Boolean.FALSE;
+ }
+
+ return inContainer;
+ }
+
+
+ /**
+ * Report to some output if a container was detected.
+ *
+ */
+ public static void report(Logger logger, boolean onlyIfInContainer)
+ {
+ if (detect()) {
+ String msg = "Running in a " + detectedType;
+ if (logger == null) {
+ System.out.println(msg);
+ }
+ else logger.info(msg);
+ }
+ else if (!onlyIfInContainer) {
+ String msg = "Not detected to be running in a container";
+ if (logger == null) {
+ System.out.println(msg);
+ }
+ else logger.info(msg);
+
+ }
+
+ }
+}
diff --git a/src/main/java/com/gitblit/utils/FileUtils.java b/src/main/java/com/gitblit/utils/FileUtils.java
index ad2509d0..0a12229c 100644
--- a/src/main/java/com/gitblit/utils/FileUtils.java
+++ b/src/main/java/com/gitblit/utils/FileUtils.java
@@ -26,6 +26,7 @@ import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.nio.charset.Charset;
+import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
@@ -302,8 +303,8 @@ public class FileUtils {
* @return a relative path from basePath to path
*/
public static String getRelativePath(File basePath, File path) {
- Path exactBase = Paths.get(getExactFile(basePath).toURI());
- Path exactPath = Paths.get(getExactFile(path).toURI());
+ Path exactBase = getExactPath(basePath);
+ Path exactPath = getExactPath(path);
if (exactPath.startsWith(exactBase)) {
return exactBase.relativize(exactPath).toString().replace('\\', '/');
}
@@ -312,20 +313,28 @@ public class FileUtils {
}
/**
- * Returns the exact path for a file. This path will be the canonical path
- * unless an exception is thrown in which case it will be the absolute path.
+ * Returns the exact path for a file. This path will be the real path
+ * with symbolic links unresolved. If that produces an IOException,
+ * the path will be the canonical path unless an exception is thrown
+ * in which case it will be the absolute path.
*
* @param path
* @return the exact file
*/
- public static File getExactFile(File path) {
+ private static Path getExactPath(File path) {
try {
- return path.getCanonicalFile();
+ return path.toPath().toRealPath(LinkOption.NOFOLLOW_LINKS);
} catch (IOException e) {
- return path.getAbsoluteFile();
+ // ignored, try next option
+ }
+ try {
+ return Paths.get(path.getCanonicalPath());
+ } catch (IOException e) {
+ return Paths.get(path.getAbsolutePath());
}
}
+
public static File resolveParameter(String parameter, File aFolder, String path) {
if (aFolder == null) {
// strip any parameter reference
diff --git a/src/main/java/com/gitblit/utils/HttpUtils.java b/src/main/java/com/gitblit/utils/HttpUtils.java
index 2fd8d898..27df5a52 100644
--- a/src/main/java/com/gitblit/utils/HttpUtils.java
+++ b/src/main/java/com/gitblit/utils/HttpUtils.java
@@ -110,7 +110,9 @@ public class HttpUtils {
sb.append(host);
if (("http".equals(scheme) && port != 80)
|| ("https".equals(scheme) && port != 443)) {
- sb.append(":").append(port);
+ if (!host.endsWith(":" + port)) {
+ sb.append(":").append(port);
+ }
}
sb.append(context);
return sb.toString();
diff --git a/src/main/java/com/gitblit/utils/JGitUtils.java b/src/main/java/com/gitblit/utils/JGitUtils.java
index a02fc3ff..e70b4f99 100644
--- a/src/main/java/com/gitblit/utils/JGitUtils.java
+++ b/src/main/java/com/gitblit/utils/JGitUtils.java
@@ -99,6 +99,7 @@ import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import com.gitblit.Constants.MergeType;
import com.gitblit.GitBlitException;
import com.gitblit.IStoredSettings;
import com.gitblit.Keys;
@@ -953,9 +954,9 @@ public class JGitUtils {
List<String> paths = new ArrayList<>();
while (tw.next()) {
- String child = isPathEmpty ? tw.getPathString()
- : tw.getPathString().replaceFirst(String.format("%s/", path), "");
- paths.add(child);
+ String pathString = tw.getPathString();
+ String child = isPathEmpty ? pathString : pathString.replaceFirst(Pattern.quote(String.format("%s/", path)), "");
+ paths.add(child);
}
for(String p: PathUtils.compressPaths(paths)) {
@@ -2453,44 +2454,13 @@ public class JGitUtils {
* @param repository
* @param src
* @param toBranch
+ * @param mergeType
+ * Defines the integration strategy to use for merging.
* @return true if we can merge without conflict
*/
- public static MergeStatus canMerge(Repository repository, String src, String toBranch) {
- RevWalk revWalk = null;
- try {
- revWalk = new RevWalk(repository);
- ObjectId branchId = repository.resolve(toBranch);
- if (branchId == null) {
- return MergeStatus.MISSING_INTEGRATION_BRANCH;
- }
- ObjectId srcId = repository.resolve(src);
- if (srcId == null) {
- return MergeStatus.MISSING_SRC_BRANCH;
- }
- RevCommit branchTip = revWalk.lookupCommit(branchId);
- RevCommit srcTip = revWalk.lookupCommit(srcId);
- if (revWalk.isMergedInto(srcTip, branchTip)) {
- // already merged
- return MergeStatus.ALREADY_MERGED;
- } else if (revWalk.isMergedInto(branchTip, srcTip)) {
- // fast-forward
- return MergeStatus.MERGEABLE;
- }
- RecursiveMerger merger = (RecursiveMerger) MergeStrategy.RECURSIVE.newMerger(repository, true);
- boolean canMerge = merger.merge(branchTip, srcTip);
- if (canMerge) {
- return MergeStatus.MERGEABLE;
- }
- } catch (NullPointerException e) {
- LOGGER.error("Failed to determine canMerge", e);
- } catch (IOException e) {
- LOGGER.error("Failed to determine canMerge", e);
- } finally {
- if (revWalk != null) {
- revWalk.close();
- }
- }
- return MergeStatus.NOT_MERGEABLE;
+ public static MergeStatus canMerge(Repository repository, String src, String toBranch, MergeType mergeType) {
+ IntegrationStrategy strategy = IntegrationStrategyFactory.create(mergeType, repository, src, toBranch);
+ return strategy.canMerge();
}
@@ -2511,11 +2481,13 @@ public class JGitUtils {
* @param repository
* @param src
* @param toBranch
+ * @param mergeType
+ * Defines the integration strategy to use for merging.
* @param committer
* @param message
* @return the merge result
*/
- public static MergeResult merge(Repository repository, String src, String toBranch,
+ public static MergeResult merge(Repository repository, String src, String toBranch, MergeType mergeType,
PersonIdent committer, String message) {
if (!toBranch.startsWith(Constants.R_REFS)) {
@@ -2523,15 +2495,202 @@ public class JGitUtils {
toBranch = Constants.R_HEADS + toBranch;
}
- RevWalk revWalk = null;
+ IntegrationStrategy strategy = IntegrationStrategyFactory.create(mergeType, repository, src, toBranch);
+ MergeResult mergeResult = strategy.merge(committer, message);
+
+ if (mergeResult.status != MergeStatus.MERGED) {
+ return mergeResult;
+ }
+
try {
- revWalk = new RevWalk(repository);
- RevCommit branchTip = revWalk.lookupCommit(repository.resolve(toBranch));
- RevCommit srcTip = revWalk.lookupCommit(repository.resolve(src));
- if (revWalk.isMergedInto(srcTip, branchTip)) {
- // already merged
- return new MergeResult(MergeStatus.ALREADY_MERGED, null);
+ // Update the integration branch ref
+ RefUpdate mergeRefUpdate = repository.updateRef(toBranch);
+ mergeRefUpdate.setNewObjectId(strategy.getMergeCommit());
+ mergeRefUpdate.setRefLogMessage(strategy.getRefLogMessage(), false);
+ mergeRefUpdate.setExpectedOldObjectId(strategy.branchTip);
+ RefUpdate.Result rc = mergeRefUpdate.update();
+ switch (rc) {
+ case FAST_FORWARD:
+ // successful, clean merge
+ break;
+ default:
+ mergeResult = new MergeResult(MergeStatus.FAILED, null);
+ throw new GitBlitException(MessageFormat.format("Unexpected result \"{0}\" when {1} in {2}",
+ rc.name(), strategy.getOperationMessage(), repository.getDirectory()));
+ }
+ } catch (IOException e) {
+ LOGGER.error("Failed to merge", e);
+ }
+
+ return mergeResult;
+ }
+
+
+ private static abstract class IntegrationStrategy {
+ Repository repository;
+ String src;
+ String toBranch;
+
+ RevWalk revWalk;
+ RevCommit branchTip;
+ RevCommit srcTip;
+
+ RevCommit mergeCommit;
+ String refLogMessage;
+ String operationMessage;
+
+ RevCommit getMergeCommit() {
+ return mergeCommit;
+ }
+
+ String getRefLogMessage() {
+ return refLogMessage;
+ }
+
+ String getOperationMessage() {
+ return operationMessage;
+ }
+
+ IntegrationStrategy(Repository repository, String src, String toBranch) {
+ this.repository = repository;
+ this.src = src;
+ this.toBranch = toBranch;
+ }
+
+ void prepare() throws IOException {
+ if (revWalk == null) revWalk = new RevWalk(repository);
+ ObjectId branchId = repository.resolve(toBranch);
+ if (branchId != null) {
+ branchTip = revWalk.lookupCommit(branchId);
+ }
+ ObjectId srcId = repository.resolve(src);
+ if (srcId != null) {
+ srcTip = revWalk.lookupCommit(srcId);
+ }
+ }
+
+
+ abstract MergeStatus _canMerge() throws IOException;
+
+
+ MergeStatus canMerge() {
+ try {
+ prepare();
+ if (branchTip == null) {
+ return MergeStatus.MISSING_INTEGRATION_BRANCH;
+ }
+ if (srcTip == null) {
+ return MergeStatus.MISSING_SRC_BRANCH;
+ }
+ if (revWalk.isMergedInto(srcTip, branchTip)) {
+ // already merged
+ return MergeStatus.ALREADY_MERGED;
+ }
+ // determined by specific integration strategy
+ return _canMerge();
+
+ } catch (NullPointerException e) {
+ LOGGER.error("Failed to determine canMerge", e);
+ } catch (IOException e) {
+ LOGGER.error("Failed to determine canMerge", e);
+ } finally {
+ if (revWalk != null) {
+ revWalk.close();
+ }
+ }
+
+ return MergeStatus.NOT_MERGEABLE;
+ }
+
+
+ abstract MergeResult _merge(PersonIdent committer, String message) throws IOException;
+
+
+ MergeResult merge(PersonIdent committer, String message) {
+ try {
+ prepare();
+ if (revWalk.isMergedInto(srcTip, branchTip)) {
+ // already merged
+ return new MergeResult(MergeStatus.ALREADY_MERGED, null);
+ }
+ // determined by specific integration strategy
+ return _merge(committer, message);
+
+ } catch (IOException e) {
+ LOGGER.error("Failed to merge", e);
+ } finally {
+ if (revWalk != null) {
+ revWalk.close();
+ }
+ }
+
+ return new MergeResult(MergeStatus.FAILED, null);
+ }
+ }
+
+
+ private static class FastForwardOnly extends IntegrationStrategy {
+ FastForwardOnly(Repository repository, String src, String toBranch) {
+ super(repository, src, toBranch);
+ }
+
+ @Override
+ MergeStatus _canMerge() throws IOException {
+ if (revWalk.isMergedInto(branchTip, srcTip)) {
+ // fast-forward
+ return MergeStatus.MERGEABLE;
+ }
+
+ return MergeStatus.NOT_MERGEABLE;
+ }
+
+ @Override
+ MergeResult _merge(PersonIdent committer, String message) throws IOException {
+ if (! revWalk.isMergedInto(branchTip, srcTip)) {
+ // is not fast-forward
+ return new MergeResult(MergeStatus.FAILED, null);
+ }
+
+ mergeCommit = srcTip;
+ refLogMessage = "merge " + src + ": Fast-forward";
+ operationMessage = MessageFormat.format("fast-forwarding {0} to commit {1}", srcTip.getName(), branchTip.getName());
+
+ return new MergeResult(MergeStatus.MERGED, srcTip.getName());
+ }
+ }
+
+ private static class MergeIfNecessary extends IntegrationStrategy {
+ MergeIfNecessary(Repository repository, String src, String toBranch) {
+ super(repository, src, toBranch);
+ }
+
+ @Override
+ MergeStatus _canMerge() throws IOException {
+ if (revWalk.isMergedInto(branchTip, srcTip)) {
+ // fast-forward
+ return MergeStatus.MERGEABLE;
+ }
+
+ RecursiveMerger merger = (RecursiveMerger) MergeStrategy.RECURSIVE.newMerger(repository, true);
+ boolean canMerge = merger.merge(branchTip, srcTip);
+ if (canMerge) {
+ return MergeStatus.MERGEABLE;
+ }
+
+ return MergeStatus.NOT_MERGEABLE;
+ }
+
+ @Override
+ MergeResult _merge(PersonIdent committer, String message) throws IOException {
+ if (revWalk.isMergedInto(branchTip, srcTip)) {
+ // fast-forward
+ mergeCommit = srcTip;
+ refLogMessage = "merge " + src + ": Fast-forward";
+ operationMessage = MessageFormat.format("fast-forwarding {0} to commit {1}", branchTip.getName(), srcTip.getName());
+
+ return new MergeResult(MergeStatus.MERGED, srcTip.getName());
}
+
RecursiveMerger merger = (RecursiveMerger) MergeStrategy.RECURSIVE.newMerger(repository, true);
boolean merged = merger.merge(branchTip, srcTip);
if (merged) {
@@ -2555,20 +2714,64 @@ public class JGitUtils {
ObjectId mergeCommitId = odi.insert(commitBuilder);
odi.flush();
- // set the merge ref to the merge commit
- RevCommit mergeCommit = revWalk.parseCommit(mergeCommitId);
- RefUpdate mergeRefUpdate = repository.updateRef(toBranch);
- mergeRefUpdate.setNewObjectId(mergeCommitId);
- mergeRefUpdate.setRefLogMessage("commit: " + mergeCommit.getShortMessage(), false);
- RefUpdate.Result rc = mergeRefUpdate.update();
- switch (rc) {
- case FAST_FORWARD:
- // successful, clean merge
- break;
- default:
- throw new GitBlitException(MessageFormat.format("Unexpected result \"{0}\" when merging commit {1} into {2} in {3}",
- rc.name(), srcTip.getName(), branchTip.getName(), repository.getDirectory()));
+ mergeCommit = revWalk.parseCommit(mergeCommitId);
+ refLogMessage = "commit: " + mergeCommit.getShortMessage();
+ operationMessage = MessageFormat.format("merging commit {0} into {1}", srcTip.getName(), branchTip.getName());
+
+ // return the merge commit id
+ return new MergeResult(MergeStatus.MERGED, mergeCommitId.getName());
+ } finally {
+ odi.close();
+ }
+ }
+ return new MergeResult(MergeStatus.FAILED, null);
+ }
+ }
+
+ private static class MergeAlways extends IntegrationStrategy {
+ MergeAlways(Repository repository, String src, String toBranch) {
+ super(repository, src, toBranch);
+ }
+
+ @Override
+ MergeStatus _canMerge() throws IOException {
+ RecursiveMerger merger = (RecursiveMerger) MergeStrategy.RECURSIVE.newMerger(repository, true);
+ boolean canMerge = merger.merge(branchTip, srcTip);
+ if (canMerge) {
+ return MergeStatus.MERGEABLE;
+ }
+
+ return MergeStatus.NOT_MERGEABLE;
+ }
+
+ @Override
+ MergeResult _merge(PersonIdent committer, String message) throws IOException {
+ RecursiveMerger merger = (RecursiveMerger) MergeStrategy.RECURSIVE.newMerger(repository, true);
+ boolean merged = merger.merge(branchTip, srcTip);
+ if (merged) {
+ // create a merge commit and a reference to track the merge commit
+ ObjectId treeId = merger.getResultTreeId();
+ ObjectInserter odi = repository.newObjectInserter();
+ try {
+ // Create a commit object
+ CommitBuilder commitBuilder = new CommitBuilder();
+ commitBuilder.setCommitter(committer);
+ commitBuilder.setAuthor(committer);
+ commitBuilder.setEncoding(Constants.CHARSET);
+ if (StringUtils.isEmpty(message)) {
+ message = MessageFormat.format("merge {0} into {1}", srcTip.getName(), branchTip.getName());
}
+ commitBuilder.setMessage(message);
+ commitBuilder.setParentIds(branchTip.getId(), srcTip.getId());
+ commitBuilder.setTreeId(treeId);
+
+ // Insert the merge commit into the repository
+ ObjectId mergeCommitId = odi.insert(commitBuilder);
+ odi.flush();
+
+ mergeCommit = revWalk.parseCommit(mergeCommitId);
+ refLogMessage = "commit: " + mergeCommit.getShortMessage();
+ operationMessage = MessageFormat.format("merging commit {0} into {1}", srcTip.getName(), branchTip.getName());
// return the merge commit id
return new MergeResult(MergeStatus.MERGED, mergeCommitId.getName());
@@ -2576,17 +2779,27 @@ public class JGitUtils {
odi.close();
}
}
- } catch (IOException e) {
- LOGGER.error("Failed to merge", e);
- } finally {
- if (revWalk != null) {
- revWalk.close();
+
+ return new MergeResult(MergeStatus.FAILED, null);
+ }
+ }
+
+
+ private static class IntegrationStrategyFactory {
+ static IntegrationStrategy create(MergeType mergeType, Repository repository, String src, String toBranch) {
+ switch(mergeType) {
+ case FAST_FORWARD_ONLY:
+ return new FastForwardOnly(repository, src, toBranch);
+ case MERGE_IF_NECESSARY:
+ return new MergeIfNecessary(repository, src, toBranch);
+ case MERGE_ALWAYS:
+ return new MergeAlways(repository, src, toBranch);
}
+ return null;
}
- return new MergeResult(MergeStatus.FAILED, null);
}
-
-
+
+
/**
* Returns the LFS URL for the given oid
* Currently assumes that the Gitblit Filestore is used
diff --git a/src/main/java/com/gitblit/utils/LuceneIndexStore.java b/src/main/java/com/gitblit/utils/LuceneIndexStore.java
new file mode 100644
index 00000000..c05e2019
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/LuceneIndexStore.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2017 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.gitblit.utils;
+
+import java.io.File;
+import java.nio.file.Path;
+
+/**
+ * @author Florian Zschocke
+ *
+ * @since 1.9.0
+ */
+public class LuceneIndexStore
+{
+
+ public static final int LUCENE_CODEC_VERSION = 54;
+
+ protected File indexFolder;
+
+ /**
+ * Constructor for a base folder that contains the version specific index folders
+ * and an index version.
+ *
+ * @param luceneFolder
+ * Path to the base folder for the Lucene indices, i.e. the common "lucene" directory.
+ * @param indexVersion
+ * Version of the index definition
+ */
+ public LuceneIndexStore(File luceneFolder, int indexVersion)
+ {
+ this.indexFolder = new File(luceneFolder, indexVersion + "_" + LUCENE_CODEC_VERSION);
+ }
+
+
+
+ /**
+ * Create the Lucene index directory for this index version and Lucene codec version
+ */
+ public void create()
+ {
+ if (! indexFolder.exists()) {
+ indexFolder.mkdirs();
+ }
+ }
+
+
+ /**
+ * Delete the Lucene index directory for this index version and Lucene codec version
+ *
+ * @return True if the directory could successfully be deleted.
+ */
+ public boolean delete()
+ {
+ if (indexFolder.exists()) {
+ return FileUtils.delete(indexFolder);
+ }
+ return true;
+ }
+
+
+
+ /**
+ * @return The Path to the index folder
+ */
+ public Path getPath()
+ {
+ return indexFolder.toPath();
+ }
+
+
+
+ /**
+ * Check if an index of the respective version, or compatible, already exists.
+ *
+ * @return True if an index exists, False otherwise
+ */
+ public boolean hasIndex()
+ {
+ return indexFolder.exists() &&
+ indexFolder.isDirectory() &&
+ (indexFolder.list().length > 1); // Must have more than 'write.lock'
+ }
+
+}
diff --git a/src/main/java/com/gitblit/utils/MarkdownUtils.java b/src/main/java/com/gitblit/utils/MarkdownUtils.java
index e0c9dd4e..8371b3c6 100644
--- a/src/main/java/com/gitblit/utils/MarkdownUtils.java
+++ b/src/main/java/com/gitblit/utils/MarkdownUtils.java
@@ -30,6 +30,7 @@ import org.pegdown.ParsingTimeoutException;
import org.pegdown.PegDownProcessor;
import org.pegdown.ast.RootNode;
+import com.gitblit.Constants;
import com.gitblit.IStoredSettings;
import com.gitblit.Keys;
import com.gitblit.wicket.MarkupProcessor.WorkaroundHtmlSerializer;
@@ -137,8 +138,8 @@ public class MarkdownUtils {
String canonicalUrl = settings.getString(Keys.web.canonicalUrl, "https://localhost:8443");
// emphasize and link mentions
- String mentionReplacement = String.format(" **[@$1](%1s/user/$1)**", canonicalUrl);
- text = text.replaceAll("\\s@([A-Za-z0-9-_]+)", mentionReplacement);
+ String mentionReplacement = String.format("**[@${user}](%1s/user/${user})**", canonicalUrl);
+ text = text.replaceAll(Constants.REGEX_TICKET_MENTION, mentionReplacement);
// link ticket refs
String ticketReplacement = MessageFormat.format("$1[#$2]({0}/tickets?r={1}&h=$2)$3", canonicalUrl, repositoryName);
diff --git a/src/main/java/com/gitblit/utils/PasswordHash.java b/src/main/java/com/gitblit/utils/PasswordHash.java
new file mode 100644
index 00000000..ee60dfb6
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/PasswordHash.java
@@ -0,0 +1,292 @@
+/*
+ * Copyright 2017 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.gitblit.utils;
+
+import java.util.Arrays;
+
+/**
+ * This is the superclass for classes responsible for handling password hashing.
+ *
+ * It provides a factory-like interface to create an instance of a class that
+ * is responsible for the mechanics of a specific password hashing method.
+ * It also provides the common interface, leaving implementation specifics
+ * to subclasses of itself, which are the factory products.
+ *
+ * @author Florian Zschocke
+ * @since 1.9.0
+ */
+public abstract class PasswordHash {
+
+ /**
+ * The types of implemented password hashing schemes.
+ */
+ public enum Type {
+ MD5,
+ CMD5,
+ PBKDF2;
+
+ static Type fromName(String name) {
+ if (name == null) return null;
+ for (Type type : Type.values()) {
+ if (type.name().equalsIgnoreCase(name)) return type;
+ }
+ // Compatibility with type id "PBKDF2WITHHMACSHA256", which is also handled by PBKDF2 type.
+ if (name.equalsIgnoreCase("PBKDF2WITHHMACSHA256")) return Type.PBKDF2;
+
+ // Recognise the name used for CMD5 in the settings file.
+ if (name.equalsIgnoreCase("combined-md5")) return Type.CMD5;
+
+ return null;
+ }
+ }
+
+ /**
+ * The hashing scheme type handled by an instance of a subclass
+ */
+ final Type type;
+
+
+ /**
+ * Constructor for subclasses to initialize the final type field.
+ * @param type
+ * Type of hashing scheme implemented by this instance.
+ */
+ PasswordHash(Type type) {
+ this.type = type;
+ }
+
+
+ /**
+ * When no hash type is specified, this determines the default that should be used.
+ */
+ public static Type getDefaultType() {
+ return Type.PBKDF2;
+ }
+
+
+ /**
+ * Create an instance of a password hashing class for the given hash type.
+ *
+ * @param type
+ * Type of hash to be used.
+ * @return A class that can calculate the given hash type and verify a user password,
+ * or null if the given hash type is not a valid one.
+ */
+ public static PasswordHash instanceOf(String type) {
+ Type hashType = Type.fromName(type);
+ if (hashType == null) return null;
+ switch (hashType) {
+ case MD5:
+ return new PasswordHashMD5();
+ case CMD5:
+ return new PasswordHashCombinedMD5();
+ case PBKDF2:
+ return new PasswordHashPbkdf2();
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * Create an instance of a password hashing class of the correct type for a given
+ * hashed password from the user password table. The stored hashed password needs
+ * to be prefixed with the hash type identifier.
+ *
+ * @param hashedEntry
+ * Hashed password string from the user table.
+ * @return
+ * A class that can calculate the given hash type and verify a user password,
+ * or null if no instance can be created for the hashed user password.
+ */
+ public static PasswordHash instanceFor(String hashedEntry) {
+ Type type = getEntryType(hashedEntry);
+ if (type != null) return instanceOf(type.name());
+ return null;
+ }
+
+ /**
+ * Test if a given string is a hashed password entry. This method simply checks if the
+ * given string is prefixed by a known hash type identifier.
+ *
+ * @param storedPassword
+ * A stored user password.
+ * @return True if the given string is detected to be hashed with a known hash type,
+ * false otherwise.
+ */
+ public static boolean isHashedEntry(String storedPassword) {
+ return null != getEntryType(storedPassword);
+ }
+
+
+ /**
+ * Some hash methods are considered more secure than others. This method determines for a certain type
+ * of password hash if it is inferior than a given other type and stored passwords should be
+ * upgraded to the given hashing method.
+ *
+ * @param algorithm
+ * Password hashing type to be checked if it is superior than the one of this instance.
+ * @return True, if the given type in parameter {@code algorithm} is better and stored passwords should be upgraded to it,
+ * false, otehrwise.
+ */
+ public boolean needsUpgradeTo(String algorithm) {
+ Type hashType = Type.fromName(algorithm);
+ if (hashType == null) return false;
+ if (this.type == hashType) return false;
+
+ // Right now we keep it really simple. With the existing types, only PBKDF2 is considered secure,
+ // everything else is inferior. This will need to be updated once more secure hashing algorithms
+ // are implemented, or the workload/parameters of the PBKDF2 are changed.
+ return hashType == Type.PBKDF2;
+ }
+
+
+ /**
+ * Convert the given password to a hashed password entry to be stored in the user table.
+ * The resulting string is prefixed by the hashing scheme type followed by a colon:
+ * TYPE:theactualhashinhex
+ *
+ * @param password
+ * Password to be hashed.
+ * @param username
+ * User name, only used for the Combined-MD5 (user+MD5) hashing type.
+ * @return
+ * Hashed password entry to be stored in the user table.
+ */
+ abstract public String toHashedEntry(char[] password, String username);
+
+ /**
+ * Convert the given password to a hashed password entry to be stored in the user table.
+ * The resulting string is prefixed by the hashing scheme type followed by a colon:
+ * TYPE:theactualhashinhex
+ *
+ * @param password
+ * Password to be hashed.
+ * @param username
+ * User name, only used for the Combined-MD5 (user+MD5) hashing type.
+ * @return
+ * Hashed password entry to be stored in the user table.
+ */
+ public String toHashedEntry(String password, String username) {
+ if (password == null) throw new IllegalArgumentException("The password argument may not be null when hashing a password.");
+ return toHashedEntry(password.toCharArray(), username);
+ }
+
+ /**
+ * Test if a given password (and user name) match a hashed password.
+ * The instance of the password hash class has to be created with
+ * {code instanceFor}, so that it matches the type of the hashed password
+ * entry to test against.
+ *
+ *
+ * @param hashedEntry
+ * The hashed password entry from the user password table.
+ * @param password
+ * Clear text password to test against the hashed one.
+ * @param username
+ * User name, needed for the MD5+USER hash type.
+ * @return True, if the password (and username) match the hashed password,
+ * false, otherwise.
+ */
+ public boolean matches(String hashedEntry, char[] password, String username) {
+ if (hashedEntry == null || type != PasswordHash.getEntryType(hashedEntry)) return false;
+ if (password == null) return false;
+
+ String hashed = toHashedEntry(password, username);
+ Arrays.fill(password, Character.MIN_VALUE);
+ return hashed.equalsIgnoreCase(hashedEntry);
+ }
+
+
+
+
+
+ static Type getEntryType(String hashedEntry) {
+ if (hashedEntry == null) return null;
+ int indexOfSeparator = hashedEntry.indexOf(':');
+ if (indexOfSeparator <= 0) return null;
+ String typeId = hashedEntry.substring(0, indexOfSeparator);
+ return Type.fromName(typeId);
+ }
+
+
+ static String getEntryValue(String hashedEntry) {
+ if (hashedEntry == null) return null;
+ int indexOfSeparator = hashedEntry.indexOf(':');
+ return hashedEntry.substring(indexOfSeparator +1);
+ }
+
+
+
+
+
+ /************************************** Implementations *************************************************/
+
+ private static class PasswordHashMD5 extends PasswordHash
+ {
+ PasswordHashMD5() {
+ super(Type.MD5);
+ }
+
+ // To keep the handling identical to how it was before, and therefore not risk invalidating stored passwords,
+ // for MD5 the (String,String) variant of the method is the one implementing the hashing.
+ @Override
+ public String toHashedEntry(char[] password, String username) {
+ if (password == null) throw new IllegalArgumentException("The password argument may not be null when hashing a password.");
+ return toHashedEntry(new String(password), username);
+ }
+
+ @Override
+ public String toHashedEntry(String password, String username) {
+ if (password == null) throw new IllegalArgumentException("The password argument may not be null when hashing a password.");
+ return type.name() + ":"
+ + StringUtils.getMD5(password);
+ }
+ }
+
+
+
+
+ private static class PasswordHashCombinedMD5 extends PasswordHash
+ {
+ PasswordHashCombinedMD5() {
+ super(Type.CMD5);
+ }
+
+ // To keep the handling identical to how it was before, and therefore not risk invalidating stored passwords,
+ // for Combined-MD5 the (String,String) variant of the method is the one implementing the hashing.
+ @Override
+ public String toHashedEntry(char[] password, String username) {
+ if (password == null) throw new IllegalArgumentException("The password argument may not be null when hashing a password.");
+ return toHashedEntry(new String(password), username);
+ }
+ @Override
+ public String toHashedEntry(String password, String username) {
+ if (password == null) throw new IllegalArgumentException("The password argument may not be null when hashing a password.");
+ if (username == null) throw new IllegalArgumentException("The username argument may not be null when hashing a password with Combined-MD5.");
+ if (StringUtils.isEmpty(username)) throw new IllegalArgumentException("The username argument may not be empty when hashing a password with Combined-MD5.");
+ return type.name() + ":"
+ + StringUtils.getMD5(username.toLowerCase() + password);
+ }
+
+ @Override
+ public boolean matches(String hashedEntry, char[] password, String username) {
+ if (username == null || StringUtils.isEmpty(username)) return false;
+ return super.matches(hashedEntry, password, username);
+ }
+
+ }
+}
diff --git a/src/main/java/com/gitblit/utils/PasswordHashPbkdf2.java b/src/main/java/com/gitblit/utils/PasswordHashPbkdf2.java
new file mode 100644
index 00000000..1bce1229
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/PasswordHashPbkdf2.java
@@ -0,0 +1,276 @@
+package com.gitblit.utils;
+
+import org.apache.commons.codec.DecoderException;
+import org.apache.commons.codec.binary.Hex;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.PBEKeySpec;
+import java.security.NoSuchAlgorithmException;
+import java.security.spec.InvalidKeySpecException;
+import java.util.Arrays;
+
+/**
+ * The class PasswordHashPbkdf2 implements password hashing and validation
+ * with PBKDF2
+ *
+ * It uses the concept proposed by OWASP - Hashing Java:
+ * https://www.owasp.org/index.php/Hashing_Java
+ */
+class PasswordHashPbkdf2 extends PasswordHash
+{
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(PasswordHashPbkdf2.class);
+
+ /**
+ * The PBKDF has some parameters that define security and workload.
+ * The Configuration class keeps these parameters.
+ */
+ private static class Configuration
+ {
+ private final String algorithm;
+ private final int iterations;
+ private final int keyLen;
+ private final int saltLen;
+
+ private Configuration(String algorithm, int iterations, int keyLen, int saltLen) {
+ this.algorithm = algorithm;
+ this.iterations = iterations;
+ this.keyLen = keyLen;
+ this.saltLen = saltLen;
+ }
+ }
+
+
+ private static final SecureRandom RANDOM = new SecureRandom();
+ /**
+ * A list of Configurations is created to list the configurations supported by
+ * this implementation. The configuration id is stored in the hashed entry,
+ * identifying the Configuration in this array.
+ * When adding a new variant with different values for these parameters, add
+ * it to this array.
+ * The code uses the last configuration in the array as the most secure, to be used
+ * when creating new hashes when no configuration is specified.
+ */
+ private static final Configuration[] configurations = {
+ // Configuration 0, also default when none is specified in the stored hashed entry.
+ new Configuration("PBKDF2WithHmacSHA256", 10000, 256, 32)
+ };
+
+
+ PasswordHashPbkdf2() {
+ super(Type.PBKDF2);
+ }
+
+
+ /*
+ * We return a hashed entry, where the hash part (salt+hash) itself is prefixed
+ * again by the configuration id of the configuration that was used for the PBKDF,
+ * enclosed in '$':
+ * PBKDF2:$0$thesaltThehash
+ */
+ @Override
+ public String toHashedEntry(char[] password, String username) {
+ if (password == null) {
+ LOGGER.warn("The password argument may not be null when hashing a password.");
+ throw new IllegalArgumentException("The password argument may not be null when hashing a password.");
+ }
+
+ int configId = getLatestConfigurationId();
+ Configuration config = configurations[configId];
+
+ byte[] salt = new byte[config.saltLen];
+ RANDOM.nextBytes(salt);
+ byte[] hash = hash(password, salt, config);
+
+ return type.name() + ":"
+ + "$" + configId + "$"
+ + StringUtils.toHex(salt)
+ + StringUtils.toHex(hash);
+ }
+
+ @Override
+ public boolean matches(String hashedEntry, char[] password, String username) {
+ if (hashedEntry == null || type != PasswordHash.getEntryType(hashedEntry)) return false;
+ if (password == null) return false;
+
+ String hashedPart = getEntryValue(hashedEntry);
+ int configId = getConfigIdFromStoredPassword(hashedPart);
+
+ return isPasswordCorrect(password, hashedPart, configurations[configId]);
+ }
+
+
+
+
+
+
+
+
+ /**
+ * Return the id of the most updated configuration of parameters for the PBKDF.
+ * New password hashes should be generated with this one.
+ *
+ * @return An index into the configurations array for the latest configuration.
+ */
+ private int getLatestConfigurationId() {
+ return configurations.length-1;
+ }
+
+
+ /**
+ * Get the configuration id from the stored hashed password, that was used when the
+ * hash was created. The configuration id is the index into the configuration array,
+ * and is stored in the format $Id$ after the type identifier: TYPE:$Id$....
+ * If there is no identifier in the stored entry, id 0 is used, to keep backward
+ * compatibility.
+ * If an id is found that is not in the range of the declared configurations,
+ * 0 is returned. This may fail password validation. As of now there is only one
+ * configuration and even if there were more, chances are slim that anything else
+ * was used. So we try at least the first one instead of failing with an exception
+ * as the probability of success is high enough to save the user from a bad experience
+ * and to risk some hassle for the admin finding out in the logs why a login failed,
+ * when it does.
+ *
+ * @param hashPart
+ * The hash part of the stored entry, i.e. the part after the TYPE:
+ * @return The configuration id, or
+ * 0 if none was found.
+ */
+ private static int getConfigIdFromStoredPassword(String hashPart) {
+ String[] parts = hashPart.split("\\$", 3);
+ // If there are not two parts, there is no '$'-enclosed part and we have no configuration information stored.
+ // Return default 0.
+ if (parts.length <= 2) return 0;
+
+ // The first string wil be empty. Even if it isn't we ignore it because it doesn't contain our information.
+ try {
+ int configId = Integer.parseInt(parts[1]);
+ if (configId < 0 || configId >= configurations.length) {
+ LOGGER.warn("A user table password entry contains a configuration id that is not valid: {}." +
+ "Assuming PBKDF configuration 0. This may fail to validate the password.", configId);
+ return 0;
+ }
+ return configId;
+ }
+ catch (NumberFormatException e) {
+ LOGGER.warn("A user table password entry contains a configuration id that is not a parsable number ({}${}$...)." +
+ "Assuming PBKDF configuration 0. This may fail to validate the password.", parts[0], parts[1], e);
+ return 0;
+ }
+ }
+
+
+
+
+
+ /**
+ * Hash.
+ *
+ * @param password
+ * the password
+ * @param salt
+ * the salt
+ * @param config
+ * Parameter configuration to use for the PBKDF
+ * @return Hashed result
+ */
+ private static byte[] hash(char[] password, byte[] salt, Configuration config) {
+ PBEKeySpec spec = new PBEKeySpec(password, salt, config.iterations, config.keyLen);
+ Arrays.fill(password, Character.MIN_VALUE);
+ try {
+ SecretKeyFactory skf = SecretKeyFactory.getInstance(config.algorithm);
+ return skf.generateSecret(spec).getEncoded();
+ } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
+ LOGGER.warn("Error while hashing password.", e);
+ throw new IllegalStateException("Error while hashing password", e);
+ } finally {
+ spec.clearPassword();
+ }
+ }
+
+ /**
+ * Checks if is password correct.
+ *
+ * @param passwordToCheck
+ * the password to check
+ * @param salt
+ * the salt
+ * @param expectedHash
+ * the expected hash
+ * @return true, if is password correct
+ */
+ private static boolean isPasswordCorrect(char[] passwordToCheck, byte[] salt, byte[] expectedHash, Configuration config) {
+ byte[] hashToCheck = hash(passwordToCheck, salt, config);
+ Arrays.fill(passwordToCheck, Character.MIN_VALUE);
+ if (hashToCheck.length != expectedHash.length) {
+ return false;
+ }
+ for (int i = 0; i < hashToCheck.length; i++) {
+ if (hashToCheck[i] != expectedHash[i]) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+
+ /**
+ * Gets the salt from stored password.
+ *
+ * @param storedPassword
+ * the stored password
+ * @return the salt from stored password
+ */
+ private static byte[] getSaltFromStoredPassword(String storedPassword, Configuration config) {
+ byte[] pw = getStoredHashWithStrippedPrefix(storedPassword);
+ return Arrays.copyOfRange(pw, 0, config.saltLen);
+ }
+
+ /**
+ * Gets the hash from stored password.
+ *
+ * @param storedPassword
+ * the stored password
+ * @return the hash from stored password
+ */
+ private static byte[] getHashFromStoredPassword(String storedPassword, Configuration config) {
+ byte[] pw = getStoredHashWithStrippedPrefix(storedPassword);
+ return Arrays.copyOfRange(pw, config.saltLen, pw.length);
+ }
+
+ /**
+ * Strips the configuration id prefix ($Id$) from a stored
+ * password and returns the decoded hash
+ *
+ * @param storedPassword
+ * the stored password
+ * @return the stored hash with stripped prefix
+ */
+ private static byte[] getStoredHashWithStrippedPrefix(String storedPassword) {
+ String[] strings = storedPassword.split("\\$", 3);
+ String saltAndHash = strings[strings.length -1];
+ try {
+ return Hex.decodeHex(saltAndHash.toCharArray());
+ } catch (DecoderException e) {
+ LOGGER.warn("Failed to decode stored password entry from hex to string.", e);
+ throw new IllegalStateException("Error while reading stored credentials", e);
+ }
+ }
+
+ /**
+ * Checks if password is correct.
+ *
+ * @param password
+ * the password to validate
+ * @param storedPassword
+ * the stored password, i.e. the password entry value, without the leading TYPE:
+ * @return true, if password is correct, false otherwise
+ */
+ private static boolean isPasswordCorrect(char[] password, String storedPassword, Configuration config) {
+ byte[] storedSalt = getSaltFromStoredPassword(storedPassword, config);
+ byte[] storedHash = getHashFromStoredPassword(storedPassword, config);
+ return isPasswordCorrect(password, storedSalt, storedHash, config);
+ }
+}
diff --git a/src/main/java/com/gitblit/utils/SecureRandom.java b/src/main/java/com/gitblit/utils/SecureRandom.java
new file mode 100644
index 00000000..119533d4
--- /dev/null
+++ b/src/main/java/com/gitblit/utils/SecureRandom.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2016 gitblit.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.utils;
+
+/**
+ * Wrapper class for java.security.SecureRandom, which will periodically reseed
+ * the PRNG in case an instance of the class has been running for a long time.
+ *
+ * @author Florian Zschocke
+ */
+public class SecureRandom {
+
+ /** Period (in ms) after which a new SecureRandom will be created in order to get a fresh random seed. */
+ private static final long RESEED_PERIOD = 24 * 60 * 60 * 1000; /* 24 hours */
+
+
+ private long last;
+ private java.security.SecureRandom random;
+
+
+
+ public SecureRandom() {
+ // Make sure the SecureRandom is seeded right from the start.
+ // This also lets any blocks during seeding occur at creation
+ // and prevents it from happening when getting next random bytes.
+ seed();
+ }
+
+
+
+ public byte[] randomBytes(int num) {
+ byte[] bytes = new byte[num];
+ nextBytes(bytes);
+ return bytes;
+ }
+
+
+ public void nextBytes(byte[] bytes) {
+ random.nextBytes(bytes);
+ reseed(false);
+ }
+
+
+ void reseed(boolean forced) {
+ long ts = System.currentTimeMillis();
+ if (forced || (ts - last) > RESEED_PERIOD) {
+ last = ts;
+ runReseed();
+ }
+ }
+
+
+
+ private void seed() {
+ random = new java.security.SecureRandom();
+ random.nextBytes(new byte[0]);
+ last = System.currentTimeMillis();
+ }
+
+
+ private void runReseed() {
+ // Have some other thread hit the penalty potentially incurred by reseeding,
+ // so that we can immediately return and not block the operation in progress.
+ new Thread() {
+ public void run() {
+ seed();
+ }
+ }.start();
+ }
+}
diff --git a/src/main/java/com/gitblit/utils/StringUtils.java b/src/main/java/com/gitblit/utils/StringUtils.java
index 643c52c3..0e23d637 100644
--- a/src/main/java/com/gitblit/utils/StringUtils.java
+++ b/src/main/java/com/gitblit/utils/StringUtils.java
@@ -46,10 +46,6 @@ import java.util.regex.PatternSyntaxException;
*/
public class StringUtils {
- public static final String MD5_TYPE = "MD5:";
-
- public static final String COMBINED_MD5_TYPE = "CMD5:";
-
/**
* Returns true if the string is null or empty.
*
@@ -61,6 +57,21 @@ public class StringUtils {
}
/**
+ * Returns true if the character array represents an empty String.
+ * An empty character sequence is defined as a sequence that
+ * either has no characters at all, or no characters above
+ * '\u0020' (space).
+ *
+ * @param value
+ * @return true if value is null or represents an empty String
+ */
+ public static boolean isEmpty(char[] value) {
+ if (value == null || value.length == 0) return true;
+ for ( char c : value) if (c > '\u0020') return false;
+ return true;
+ }
+
+ /**
* Replaces carriage returns and line feeds with html line breaks.
*
* @param string
@@ -94,6 +105,8 @@ public class StringUtils {
public static String escapeForHtml(String inStr, boolean changeSpace, int tabLength) {
StringBuilder retStr = new StringBuilder();
int i = 0;
+ int l = 0;
+
while (i < inStr.length()) {
if (inStr.charAt(i) == '&') {
retStr.append("&amp;");
@@ -106,12 +119,18 @@ public class StringUtils {
} else if (changeSpace && inStr.charAt(i) == ' ') {
retStr.append("&nbsp;");
} else if (changeSpace && inStr.charAt(i) == '\t') {
- for (int j = 0; j < tabLength; j++) {
+ for (int j = 0; j < tabLength - l; j++) {
retStr.append("&nbsp;");
}
+ l = -1;
} else {
retStr.append(inStr.charAt(i));
}
+
+ l = (l + 1) % tabLength;
+ if (inStr.charAt(i) == '\n') {
+ l = 0;
+ }
i++;
}
return retStr.toString();
diff --git a/src/main/java/com/gitblit/utils/TimeUtils.java b/src/main/java/com/gitblit/utils/TimeUtils.java
index c0e98e5d..6e0d3534 100644
--- a/src/main/java/com/gitblit/utils/TimeUtils.java
+++ b/src/main/java/com/gitblit/utils/TimeUtils.java
@@ -59,7 +59,11 @@ public class TimeUtils {
* @return true if date is today
*/
public static boolean isToday(Date date, TimeZone timezone) {
- Date now = new Date();
+ return isToday(date, timezone, new Date());
+ }
+
+
+ static boolean isToday(Date date, TimeZone timezone, Date now) {
SimpleDateFormat df = new SimpleDateFormat("yyyyMMdd");
if (timezone != null) {
df.setTimeZone(timezone);
@@ -74,8 +78,13 @@ public class TimeUtils {
* @return true if date is yesterday
*/
public static boolean isYesterday(Date date, TimeZone timezone) {
+ return isYesterday(date, timezone, new Date());
+ }
+
+
+ static boolean isYesterday(Date date, TimeZone timezone, Date now) {
Calendar cal = Calendar.getInstance();
- cal.setTime(new Date());
+ cal.setTime(now);
cal.add(Calendar.DATE, -1);
SimpleDateFormat df = new SimpleDateFormat("yyyyMMdd");
if (timezone != null) {
@@ -157,7 +166,12 @@ public class TimeUtils {
* @return hours ago
*/
public static int hoursAgo(Date date, boolean roundup) {
- long diff = System.currentTimeMillis() - date.getTime();
+ return hoursAgo(date, System.currentTimeMillis(), roundup);
+ }
+
+
+ static int hoursAgo(Date date, long now, boolean roundup) {
+ long diff = now - date.getTime();
int hours = (int) (diff / ONEHOUR);
if (roundup && (diff % ONEHOUR) >= HALFHOUR) {
hours++;
@@ -172,13 +186,68 @@ public class TimeUtils {
* @return days ago
*/
public static int daysAgo(Date date) {
- long today = ONEDAY * (System.currentTimeMillis()/ONEDAY);
+ return daysAgo(date, System.currentTimeMillis());
+ }
+
+ static int daysAgo(Date date, long now) {
+ long today = ONEDAY * (now/ONEDAY);
long day = ONEDAY * (date.getTime()/ONEDAY);
long diff = today - day;
int days = (int) (diff / ONEDAY);
return days;
}
+
+
+ /**
+ * Return the difference in calendar days between a given timestamp and the date.
+ * Calendar days means that the difference is calculated between
+ * calendar days, not 24 hour increments.
+ *
+ * This means the result is dependent on the timezone. Only the local
+ * time's time zone is used, i.e. both time stamps are interpreted in
+ * the given time zone.
+ *
+ * E.g. if now is 10:00 on 20.10.2020 GMT and the date given is for
+ * either 6:00 or 20:00 on 18.10.2020 GMT then the result is two days
+ * in both cases.
+ *
+ *
+ * @param date
+ * Date in the past
+ * @param now
+ * Timestamp representing current time (used for unit tests)
+ * @return calendar days ago
+ */
+ static int calendarDaysAgo(Date date, TimeZone timezone, long now) {
+ Calendar cal;
+ if (timezone == null) {
+ cal = Calendar.getInstance();
+ } else {
+ cal = Calendar.getInstance(timezone);
+ }
+
+ cal.setTimeInMillis(now);
+ cal.set(Calendar.MILLISECOND, 0);
+ cal.set(Calendar.SECOND, 0);
+ cal.set(Calendar.MINUTE, 0);
+ cal.set(Calendar.HOUR_OF_DAY, 12);
+ long today = cal.getTime().getTime();
+
+ cal.clear();
+ cal.setTime(date);
+ cal.set(Calendar.MILLISECOND, 0);
+ cal.set(Calendar.SECOND, 0);
+ cal.set(Calendar.MINUTE, 0);
+ cal.set(Calendar.HOUR_OF_DAY, 12);
+ long day = cal.getTime().getTime();
+
+ long diff = today - day;
+ int days = (int) (diff / ONEDAY);
+ return days;
+ }
+
+
public String today() {
return translate("gb.time.today", "today");
}
@@ -217,13 +286,19 @@ public class TimeUtils {
* @return the string representation of the duration OR the css class
*/
private String timeAgo(Date date, boolean css) {
- if (isToday(date, timezone) || isYesterday(date, timezone)) {
- int mins = minutesAgo(date, true);
+ return timeAgo(date, css, System.currentTimeMillis());
+ }
+
+
+ String timeAgo(Date date, boolean css, long now) {
+ Date dNow = new Date(now);
+ if (isToday(date, timezone, dNow) || isYesterday(date, timezone, dNow)) {
+ int mins = minutesAgo(date, now, true);
if (mins >= 120) {
if (css) {
return "age1";
}
- int hours = hoursAgo(date, true);
+ int hours = hoursAgo(date, now, true);
if (hours > 23) {
return yesterday();
} else {
@@ -238,7 +313,7 @@ public class TimeUtils {
}
return translate("gb.time.justNow", "just now");
} else {
- int days = daysAgo(date);
+ int days = calendarDaysAgo(date, timezone, now);
if (css) {
if (days <= 7) {
return "age2";
diff --git a/src/main/java/com/gitblit/utils/X509Utils.java b/src/main/java/com/gitblit/utils/X509Utils.java
index a2650be4..b661922d 100644
--- a/src/main/java/com/gitblit/utils/X509Utils.java
+++ b/src/main/java/com/gitblit/utils/X509Utils.java
@@ -743,6 +743,25 @@ public class X509Utils {
*/
public static File newClientBundle(X509Metadata clientMetadata, File caKeystoreFile,
String caKeystorePassword, X509Log x509log) {
+ return newClientBundle(null,clientMetadata,caKeystoreFile,caKeystorePassword,x509log);
+ }
+
+ /**
+ * Creates a new client certificate PKCS#12 and PEM store. Any existing
+ * stores are destroyed. After generation, the certificates are bundled
+ * into a zip file with a personalized README file.
+ *
+ * The zip file reference is returned.
+ *
+ * @param user
+ * @param clientMetadata a container for dynamic parameters needed for generation
+ * @param caKeystoreFile
+ * @param caKeystorePassword
+ * @param x509log
+ * @return a zip file containing the P12, PEM, and personalized README
+ */
+ public static File newClientBundle(com.gitblit.models.UserModel user,X509Metadata clientMetadata, File caKeystoreFile,
+ String caKeystorePassword, X509Log x509log) {
try {
// read the Gitblit CA key and certificate
KeyStore store = openKeyStore(caKeystoreFile, caKeystorePassword);
@@ -755,8 +774,17 @@ public class X509Utils {
x509log.log(MessageFormat.format("New client certificate {0,number,0} [{1}]", cert.getSerialNumber(), cert.getSubjectDN().getName()));
// process template message
- String readme = processTemplate(new File(caKeystoreFile.getParentFile(), "instructions.tmpl"), clientMetadata);
-
+ String readme = null;
+ String sInstructionsFileName = "instructions.tmpl";
+ if( user == null )
+ readme = processTemplate(new File(caKeystoreFile.getParentFile(),sInstructionsFileName), clientMetadata);
+ else{
+ File fileInstructionsTmp = null;
+ if( (fileInstructionsTmp = new File(caKeystoreFile.getParentFile(),sInstructionsFileName+"_"+user.getPreferences().getLocale())).exists() )
+ readme = processTemplate(fileInstructionsTmp,clientMetadata);
+ else
+ readme = processTemplate(new File(caKeystoreFile.getParentFile(),sInstructionsFileName),clientMetadata);
+ }
// Create a zip bundle with the p12, pem, and a personalized readme
File zipFile = new File(targetFolder, clientMetadata.commonName + ".zip");
if (zipFile.exists()) {
diff --git a/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties b/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties
index a215b4d6..9c643d92 100644
--- a/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties
+++ b/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties
@@ -170,7 +170,6 @@ gb.accessLevel = access level
gb.default = default
gb.setDefault = set default
gb.since = since
-gb.status = status
gb.bootDate = boot date
gb.servletContainer = servlet container
gb.heapMaximum = maximum heap
@@ -218,7 +217,8 @@ gb.pages = pages
gb.workingCopy = working copy
gb.workingCopyWarning = this repository has a working copy and can not receive pushes
gb.query = query
-gb.queryHelp = Standard query syntax is supported.<p/><p/>Please see <a target="_new" href="http://lucene.apache.org/core/old_versioned_docs/versions/3_5_0/queryparsersyntax.html">Lucene Query Parser Syntax</a> for details.
+gb.queryHelp = Standard query syntax is supported.<p/><p/>Please see ${querySyntax} for details.
+gb.querySyntax = Lucene Query Parser Syntax
gb.queryResults = results {0} - {1} ({2} hits)
gb.noHits = no hits
gb.authored = authored
@@ -357,7 +357,7 @@ gb.pushPermission = {0} (push)
gb.createPermission = {0} (push, ref creation)
gb.deletePermission = {0} (push, ref creation+deletion)
gb.rewindPermission = {0} (push, ref creation+deletion+rewind)
-gb.permission = permission
+gb.permission = Permission
gb.regexPermission = this permission is set from regular expression \"{0}\"
gb.accessDenied = access denied
gb.busyCollectingGarbage = sorry, Gitblit is busy collecting garbage in {0}
@@ -643,7 +643,7 @@ gb.patchsetN = patchset {0}
gb.reviewedPatchsetRev = reviewed patchset {0} revision {1}: {2}
gb.review = review
gb.reviews = reviews
-gb.veto = veto
+gb.veto = veto
gb.needsImprovement = needs improvement
gb.looksGood = looks good
gb.approve = approve
@@ -660,6 +660,7 @@ gb.nTotalTickets = {0} total
gb.body = body
gb.mergeSha = mergeSha
gb.mergeTo = merge to
+gb.mergeType = merge type
gb.labels = labels
gb.reviewers = reviewers
gb.voters = voters
@@ -671,6 +672,7 @@ gb.repositoryDoesNotAcceptPatchsets = This repository does not accept patchsets.
gb.serverDoesNotAcceptPatchsets = This server does not accept patchsets.
gb.ticketIsClosed = This ticket is closed.
gb.mergeToDescription = default integration branch for merging ticket patchsets
+gb.mergeTypeDescription = merge a ticket fast-forward only, if necessary, or always with a merge commit to the integration branch
gb.anonymousCanNotPropose = Anonymous users can not propose patchsets.
gb.youDoNotHaveClonePermission = You are not permitted to clone this repository.
gb.myTickets = my tickets
@@ -720,7 +722,7 @@ gb.federationStrategyDescription = Control if and how to federate this repositor
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.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.
@@ -737,9 +739,8 @@ 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.sshKeyComment = 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
@@ -781,3 +782,7 @@ gb.deletePatchsetSuccess = Deleted Patchset {0}.
gb.deletePatchsetFailure = Error deleting Patchset {0}.
gb.referencedByCommit = Referenced by commit.
gb.referencedByTicket = Referenced by ticket.
+gb.emailClientCertificateSubject = Your Gitblit client certificate for {0}
+
+# This last property for unit tests to test successful loading of the resource file
+gb.loadLang = default
diff --git a/src/main/java/com/gitblit/wicket/GitBlitWebApp_cs.properties b/src/main/java/com/gitblit/wicket/GitBlitWebApp_cs.properties
new file mode 100644
index 00000000..cc01a51c
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/GitBlitWebApp_cs.properties
@@ -0,0 +1,791 @@
+
+gb.zip=zip
+gb.repository=repozit\u00e1\u0159
+gb.owner=vlastn\u00edk
+gb.description=popis
+gb.lastChange=posledn\u00ed zm\u011bna
+gb.author=autor
+gb.age=st\u00e1\u0159\u00ed
+gb.tree=strom
+gb.parent=rodi\u010d
+gb.url=URL
+gb.history=historie
+gb.object=objekt
+gb.ticketAssigned=p\u0159i\u0159azen
+gb.ticketStatus=status
+gb.ticketComments=koment\u00e1\u0159e
+gb.local=lok\u00e1ln\u00ed
+gb.branches=v\u011btve
+gb.patch=patch
+gb.diff=diff
+gb.allTags=v\u0161echny tagy...
+gb.allBranches=v\u0161echny v\u011btve...
+gb.moreLogs=v\u0161echny commity...
+gb.summary=souhrn
+gb.newRepository=nov\u00fd repozit\u00e1\u0159
+gb.newUser=nov\u00fd u\u017eivatel
+gb.pageFirst=prvn\u00ed
+gb.pagePrevious=p\u0159edchoz\u00ed
+gb.pageNext=dal\u0161\u00ed
+gb.head=HEAD
+gb.login=p\u0159ihl\u00e1sit
+gb.logout=odhl\u00e1sit
+gb.username=u\u017eivatelsk\u00e9 jm\u00e9no
+gb.password=heslo
+gb.search=hledat
+gb.modification=zmena
+gb.rename=p\u0159ejmenov\u00e1n\u00ed
+gb.stats=statistiky
+gb.tagger=taguj\u00edc\u00ed
+gb.moreHistory=v\u00edce historie...
+gb.changedFiles=zm\u011bn\u011bn\u00e9 soubory
+gb.filesAdded={0} soubr\u016f p\u0159id\u00e1no
+gb.filesModified={0} soubr\u016f zm\u011bn\u011bno
+gb.filesDeleted={0} soubr\u016f smaz\u00e1no
+gb.filesCopied={0} soubr\u016f zkopirov\u00e1no
+gb.filesRenamed={0} soubr\u016f p\u0159ejmenov\u00e1no
+gb.edit=upravit
+gb.searchTooltip=Hledej {0}
+gb.delete=smazat
+gb.docs=dokumentace
+gb.accessRestriction=Omezen\u00ed p\u0159\u00edstupu
+gb.name=n\u00e1zev
+gb.enableDocs=Povolit dokumentaci
+gb.save=ulo\u017eit
+gb.isFrozen=je zmra\u017een\u00fd
+gb.cancel=zru\u0161it
+gb.changePassword=zm\u011bnit heslo
+gb.repositories=repozit\u00e1\u0159e
+gb.frequency=frekvence
+gb.folder=slo\u017eka
+gb.lastPull=posledn\u00ed pull
+gb.nextPull=n\u00e1sleduj\u00edc\u00ed pull
+gb.registration=registrace
+gb.origin=origin
+gb.headRef=v\u00fdchoz\u00ed v\u011btev (HEAD)
+gb.oid=id objektu
+gb.editFile=upravit soubor
+gb.continueEditing=Pokra\u010dovat v uprav\u00e1ch
+gb.allRepositories=V\u0161echny repozit\u00e1\u0159e
+gb.ignore_whitespace=ignorovat b\u00edle znaky
+gb.show_whitespace=uk\u00e1zat b\u00edl\u00e9 znaky
+gb.diffDeletedFileSkipped=(smazan\u00fd)
+gb.comment=Koment\u00e1\u0159
+gb.tag=tag
+gb.tags=tagy
+gb.commit=commit
+gb.committer=commituj\u00edc\u00ed
+gb.log=z\u00e1znam
+gb.ticket=\u00fakol
+gb.commitdiff=commitdiff
+gb.tickets=\u00fakoly
+gb.difftocurrent=porovnej se sou\u010dasn\u00fdm
+gb.metrics=metriky
+gb.markdown=markdown
+gb.missingUsername=Chyb\u00fd u\u017eivatelsk\u00e9 jm\u00e9no
+gb.searchTypeTooltip=Vyberte typ hled\u00e1n\u00ed
+gb.enableTickets=povolit \u00fakoly
+gb.showRemoteBranches=uk\u00e1zat vzd\u00e1len\u00e9 v\u011btve
+gb.editUsers=upravit u\u017eivatele
+gb.confirmPassword=potvrdit heslo
+gb.restrictedRepositories=omezen\u00fd repozit\u00e1\u0159
+gb.canAdmin=m\u016f\u017ee administrovat
+gb.notRestricted=anonymn\u00ed prohl\u00ed\u017een\u00ed, clone a push
+gb.view=prohl\u00ed\u017een\u00ed
+gb.remote=vzd\u00e1len\u00fd
+gb.deletion=vym\u00e1z\u00e1n\u00ed
+gb.pushRestricted=ov\u011b\u0159en\u00fd push
+gb.cloneRestricted=ov\u011b\u0159en\u00fd clone a push
+gb.viewRestricted=ov\u011b\u0159en\u00e9 prohl\u00ed\u017een\u00ed, clone a push
+gb.useDocsDescription=vyp\u00ed\u0161e Markdown dokumentaci v repozit\u00e1\u0159i
+gb.federatedRepositoryDefinitions=definice repozit\u00e1\u0159\u016f
+gb.federatedUserDefinitions=definice u\u017eivatel\u016f
+gb.federatedSettingDefinitions=definice nastaven\u00ed
+gb.type=typ
+gb.inclusions=zahrnut\u00e9
+gb.exclusions=vylou\u010den\u00e9
+gb.sendProposal=navrhnout
+gb.message=zpr\u00e1va
+gb.destinationUrl=odeslat do
+gb.destinationUrlDescription=URL Gitblit instance kam poslat n\u00e1vrh
+gb.users=u\u017eivatel\u00e9
+gb.federation=federace
+gb.error=chyba
+gb.refresh=obnovit
+gb.browse=proch\u00e1zet
+gb.clone=klonovat
+gb.filter=filtrovat
+gb.create=vytvo\u0159it
+gb.recent=ned\u00e1vn\u00e9
+gb.available=dostupn\u00e9
+gb.selected=vybran\u00e9
+gb.size=velikost
+gb.downloading=stahov\u00e1n\u00ed
+gb.loading=nahr\u00e1v\u00e1n\u00ed
+gb.starting=spou\u0161t\u011bn\u00ed
+gb.general=obecn\u00e9
+gb.settings=nastaven\u00ed
+gb.manage=spravovat
+gb.lastLogin=posledn\u00ed p\u0159ihl\u00e1\u0161en\u00ed
+gb.skipSizeCalculation=p\u0159esko\u010dit v\u00fdpo\u010det velikosti
+gb.default=v\u00fdchoz\u00ed
+gb.setDefault=nastavit v\u00fdchoz\u00ed
+gb.since=od
+gb.status=status
+gb.servletContainer=kontejner servletu
+gb.version=verze
+gb.releaseDate=datum vyd\u00e1n\u00ed
+gb.date=datum
+gb.activity=aktivita
+gb.subscribe=p\u0159ihl\u00e1sit
+gb.branch=v\u011btev
+gb.recentActivity=posledn\u00ed aktivita
+gb.commits=zm\u011bny
+gb.teams=t\u00fdmy
+gb.teamName=jm\u00e9no t\u00fdmu
+gb.teamMembers=\u010dlenov\u00e9 t\u00fdmu
+gb.teamMemberships=\u010dlenstv\u00ed v t\u00fdmu
+gb.newTeam=nov\u00fd t\u00fdm
+gb.permittedTeams=povolen\u00e9 t\u00fdmy
+gb.emptyRepository=pr\u00e1zdn\u00fd repozit\u00e1\u0159
+gb.repositoryUrl=URL repozit\u00e1\u0159e
+gb.filters=filtry
+gb.generalDescription=obecn\u00e1 nastaven\u00ed
+gb.pages=str\u00e1nky
+gb.workingCopy=pracovn\u00ed kopie
+gb.empty=pr\u00e1zdn\u00fd
+gb.deleteRepository=Smazat repozit\u00e1\u0159 "{0}"?
+gb.repositoryDeleted=Repozit\u00e1\u0159 ''{0}'' smaz\u00e1no.
+gb.repositoryDeleteFailed=Smaz\u00e1n\u00ed repozit\u00e1\u0159e ''{0}'' selhalo!
+gb.deleteUser=Smazat u\u017eivatele "{0}"?
+gb.userDeleted=U\u017eivatel ''{0}'' smaz\u00e1n.
+gb.userDeleteFailed=Chyba pri maz\u00e1n\u00ed u\u017eivatele ''{0}''!
+gb.time.justNow=nyn\u00ed
+gb.time.today=dnes
+gb.time.yesterday=v\u010dera
+gb.time.hoursAgo=p\u0159ed {0} hodinami
+gb.time.minsAgo=p\u0159ed {0} minutami
+gb.time.daysAgo=p\u0159ed {0} dny
+gb.time.weeksAgo=p\u0159ed {0} t\u00fddny
+gb.time.monthsAgo=p\u0159ed {0} m\u011bs\u00edci
+gb.time.oneYearAgo=p\u0159ed jedn\u00edm rokem
+gb.time.yearsAgo=p\u0159ed {0} roky
+gb.duration.oneDay=1 den
+gb.duration.days={0} dn\u00ed
+gb.duration.oneMonth=1 m\u011bs\u00edc
+gb.duration.months={0} m\u011bs\u00edc\u016f
+gb.duration.oneYear=1 rok
+gb.duration.years={0} let
+gb.projects=projekty
+gb.project=projekt
+gb.allProjects=v\u0161echny projetky
+gb.copyToClipboard=zkop\u00edrovat do schr\u00e1nky
+gb.add=p\u0159idat
+gb.noPermission=SMAZAT TOTO OPR\u00c1VN\u011aN\u00cd
+gb.viewPermission={0} (zobrazit)
+gb.clonePermission={0} (klonovat)
+gb.pushPermission={0} (push)
+gb.createPermission={0} (push, vytv\u00e1\u0159en\u00ed ref)
+gb.deletePermission={0} (push, vytv\u00e1\u0159en\u00ed+maz\u00e1n\u00ed ref)
+gb.rewindPermission={0} (push, vytv\u00e1\u0159en\u00ed+maz\u00e1n\u00ed+rewind ref)
+gb.regexPermission=toto opr\u00e1vn\u011bn\u00ed je nastaveno z regul\u00e1rn\u00edho v\u00fdrazu "{0}"
+gb.accessDenied=p\u0159\u00edstup odep\u0159en
+gb.administrator=admin
+gb.administratorPermission=administr\u00e1tor Gitblitu
+gb.team=t\u00fdm
+gb.missing=chyb\u00ed!
+gb.effective=efektivn\u00ed
+gb.properties=vlastnosti
+gb.serialNumber=s\u00e9riov\u00e9 \u010d\u00edslo
+gb.certificates=certifik\u00e1ty
+gb.newCertificate=nov\u00fd certifik\u00e1t
+gb.revokeCertificate=zru\u0161it certifik\u00e1t
+gb.sendEmail=odeslat e-mail
+gb.passwordHint=n\u00e1pov\u011bda hesla
+gb.ok=ok
+gb.subject=p\u0159edm\u011bt
+gb.issuer=vydavatel
+gb.validFrom=platn\u00fd od
+gb.validUntil=platn\u00fd do
+gb.publicKey=ve\u0159ejn\u00fd kl\u00ed\u010d
+gb.unspecified=nespecifikov\u00e1no
+gb.oneCommitTo=1 commit do
+gb.noMilestoneSelected=mezn\u00edk nevybr\u00e1n
+gb.veto=vetovat
+gb.illegalCharacterRepositoryName=Neplatn\u00fd znak ''{0}'' ve jm\u00e9n\u011b repozit\u00e1\u0159e.
+gb.errorAdminLoginRequired=Administrace vy\u017eaduje p\u0159ihl\u00e1\u0161en\u00ed
+gb.compare=porovnat
+gb.nClosedTickets={0} zav\u0159eno
+gb.canCreate=m\u016f\u017ee vytv\u00e1\u0159et
+gb.leaveComment=zanechat koment\u00e1\u0159...
+gb.sort=se\u0159adit
+gb.deletedBranch=smazat v\u011btev
+gb.export=exportovat
+gb.myRepositories=moje repozit\u00e1\u0159e
+gb.md5FingerPrint=MD5 otisk
+gb.watching=sledovat
+gb.sha1FingerPrint=SHA1 otisk
+gb.clearCache=vymazat cache
+gb.warning=varov\u00e1n\u00ed
+gb.discussion=diskuze
+gb.oneCommit=jeden commit
+gb.duration=doba trv\u00e1n\u00ed
+gb.watchers=sleduj\u00edc\u00ed
+gb.addedNCommits=p\u0159id\u00e1no {0} commit\u016f
+gb.expires=vypr\u0161\u00ed
+gb.diffNewFile=Nov\u00fd soubor
+gb.revisionHistory=historie reviz\u00ed
+gb.initWithReadme=P\u0159idat README
+gb.editMilestone=upravit mezn\u00edk
+gb.signatureAlgorithm=algoritmus podpisu
+gb.overview=p\u0159ehled
+gb.expired=vypr\u0161el
+gb.dailyActivity=denn\u00ed aktivita
+gb.validity=platnost
+gb.query=dotaz
+gb.to=do
+gb.newTicket=nov\u00fd \u00fakol
+gb.download=st\u00e1hnout
+gb.issued=vydan\u00fd
+gb.owners=vlastn\u00edci
+gb.closed=uzav\u0159en\u00fd
+gb.bugTickets=chyba
+gb.write=ps\u00e1t
+gb.receive=p\u0159ijmout
+gb.updated=aktualizov\u00e1no
+gb.voters=hlasuj\u00edc\u00ed
+gb.at=v
+gb.noSelectedRepositoriesWarning=pros\u00edm vyberte jeden nebo v\u00edce repozit\u00e1\u0159\u016f!
+gb.ticketN=\u00fakol #{0
+gb.time.inHours=b\u011bhem {0} hodin
+gb.sortNewest=nejnov\u011bj\u0161\u00ed
+gb.deleteMilestone=Smazat mezn\u00edk "{0}"?
+gb.emailAddressDescription=Prim\u00e1rn\u00ed e-mailov\u00e1 adresa pro p\u0159\u00edjem upozorn\u011bn\u00ed
+gb.line=linka
+gb.lastNDays=posledn\u00edch {0} dn\u00ed
+gb.sortMostRecentlyUpdated=ned\u00e1vno aktualizov\u00e1no
+gb.displayName=zobrazovan\u00fd n\u00e1zev
+gb.administration=administrace
+gb.priority=priorita
+gb.overdue=zpo\u017ed\u011bn\u00fd
+gb.any=libovoln\u00fd
+gb.indexedBranches=Indexovan\u00e9 v\u011btve
+gb.diffDeletedFile=Soubor byl smaz\u00e1n
+gb.userCreated=Nov\u00fd u\u017eivatel ''{0}'' \u00fasp\u011b\u0161n\u011b vytvo\u0159en.
+gb.teamCreated=Nov\u00fd t\u00fdm ''{0}'' \u00fasp\u011b\u0161n\u011b vytvo\u0159en.
+gb.isSparkleshared=repozit\u00e1\u0159 je Sparkleshare
+gb.nTotalTickets={0} celkem
+gb.all=v\u0161echny
+gb.new=nov\u00fd
+gb.excludeFromActivity=odebrat z aktivn\u00ed str\u00e1nky
+gb.referencedByTicket=Odkazovan\u00fd \u00fakolem.
+gb.ticketId=id \u00fakolu
+gb.forkNotAuthorized=Promint\u011b, nejste opr\u00e1vn\u011bni k vytvo\u0159en\u00ed forku {0}
+gb.servers=servry
+gb.nParticipants={0} \u00fa\u010dastn\u00edk\u016f
+gb.oneParticipant={0} \u00fa\u010dastn\u00edk
+gb.reset=reset
+gb.oneComment={0} koment\u00e1\u0159
+gb.oneAttachment={0} p\u0159\u00edloha
+gb.from=od
+gb.blob=blob
+gb.severity=z\u00e1va\u017enost
+gb.accessLevel=\u00farove\u0148 p\u0159\u00edstupu
+gb.yourCreatedTickets=vytvo\u0159eno v\u00e1mi
+gb.editTicket=upravit \u00fakol
+gb.milestones=mezn\u00edky
+gb.sortLowestSeverity=nejni\u017e\u0161\u00ed z\u00e1va\u017enost
+gb.permittedUsers=opr\u00e1vn\u011bn\u00fd u\u017eivatel
+gb.sortMostVotes=nejv\u00edce hlas\u016f
+gb.plugins=pluginy
+gb.specified=specifikovan\u00e9
+gb.sortHighestPriority=nejvy\u0161\u0161\u00ed priorita
+gb.content=obsah
+gb.organization=organizace
+gb.passwordTooShort=Heslo je p\u0159\u00edli\u0161 kr\u00e1tke. minimum je {0} znak\u016f.
+gb.notSpecified=nen\u00ed specifikov\u00e1no
+gb.superseded=nahrazeno
+gb.keyCompromise=kl\u00ed\u010d kompromitov\u00e1n
+gb.locality=lokalita
+gb.addSshKey=P\u0159idat SSH kl\u00ed\u010d
+gb.myFork=zobrazit m\u016fj fork
+gb.mergeType=typ slou\u010den\u00ed
+gb.commitsTo={0} commit\u016f do
+gb.indexedBranchesDescription=Vyberte v\u011btev kter\u00e1 bude indexov\u00e1na pomoc\u00ed Lucene
+gb.couldNotCreateFederationProposal=Nelze vytvo\u0159it n\u00e1vrh federace!
+gb.completeGravatarProfile=Dokon\u010dete profil na Gravatar.com
+gb.errorOnlyAdminOrOwnerMayEditRepository=Pouze administr\u00e1tor nebo vlastn\u00edk m\u016f\u017ee upravovat repozit\u00e1\u0159
+gb.diffCopiedFile=Soubor byl zkop\u00edrov\u00e1n z {0}
+gb.failedToReadMessage=Nelze p\u0159e\u010d\u00edst v\u00fdchoz\u00ed zpr\u00e1vu z {0}!
+gb.organizationalUnit=organiza\u010dn\u00ed jednotka
+gb.newSSLCertificate=nov\u00fd serverov\u00fd SSL certifik\u00e1t
+gb.anonymousPolicyDescription=Kdokoli m\u016f\u017ee zobrazit repozit\u00e1\u0159 a prov\u00e1d\u011bt clone a push.
+gb.deleteRepositoryDescription=Smazan\u00e9 repozit\u00e1\u0159e budou neobnoviteln\u00e1.
+gb.allowForks=povolit forky
+gb.newMilestone=nov\u00fd mezn\u00edk
+gb.nFederationProposalsToReview={0} n\u00e1vrh\u016f federac\u00ed \u010dek\u00e1 na posouzen\u00ed.
+gb.anonymousCanNotPropose=Anonymn\u00ed u\u017eivatel nem\u016f\u017ee navrhovat patchsety.
+gb.patchsetN=sada zm\u011bn {0}
+gb.failedToUpdateUser=Aktualizace u\u017eivatelsk\u00e9ho \u00fa\u010dtu selhala!
+gb.commitIsNull=Commit je null
+gb.certificate=certifik\u00e1t
+gb.isMirror=tento repozit\u00e1\u0159 je zrcadlo
+gb.accessPolicy=Opr\u00e1vn\u011bn\u00ed p\u0159\u00edstupu
+gb.todaysActivityNone=dnes / nic
+gb.transportPreference=Preference transportu
+gb.reviews=posouzen\u00ed
+gb.open=otev\u0159\u00edt
+gb.oneMoreCommit=1 dal\u0161\u00ed commit \u00bb
+gb.patchsetVetoedMore=Posuzovatel volil tento patchset.
+gb.about=o
+gb.action=akce
+gb.acceptNewPatchsets=p\u0159ijmout sady zm\u011bn
+gb.accessPermissions=p\u0159\u00edstupov\u00e1 opr\u00e1vn\u011bn\u00ed
+gb.accountPreferences=Preference \u00fa\u010dtu
+gb.active=aktivn\u00ed
+gb.activeAuthors=aktivn\u00ed u\u017eivatel\u00e9
+gb.activeRepositories=aktivn\u00ed repozit\u00e1\u0159e
+gb.addComment=p\u0159idat koment\u00e1\u0159
+gb.addedOneCommit=p\u0159id\u00e1n 1 commit
+gb.opacityAdjust=Nastavit nepr\u016fhlednost
+gb.errorAdministrationDisabled=Administrace je zak\u00e1z\u00e1na
+gb.affiliationChanged=p\u0159\u00edslu\u0161nost se zm\u011bnila
+gb.moreChanges=v\u0161echny zm\u011bny...
+gb.verifyCommitterNote=v\u0161echna slou\u010den\u00ed vy\u017eaduj\u00ed "--no-ff" aby se vynutila identita v\u00fdvoj\u00e1\u0159e
+gb.tokenJurDescription=v\u0161echny repozit\u00e1\u0159e
+gb.tokenUnrDescription=v\u0161echny repozit\u00e1\u0159e a u\u017eivatel\u00e9
+gb.tokenAllDescription=v\u0161echny repozit\u00e1\u0159e, u\u017eivatel\u00e9 a nastaven\u00ed
+gb.heapAllocated=alokovan\u00e1 halda
+gb.allowForksDescription=povolit opr\u00e1vn\u011bn\u00fdm u\u017eivatel\u016fm vytv\u00e1\u0159et fork tohoto repozit\u00e1\u0159e
+gb.acceptNewTicketsDescription=povolit vytv\u00e1\u0159en\u00ed hl\u00e1\u0161en\u00ed o chyb\u00e1ch, vylep\u0161en\u00ed, ulohy a dal\u0161\u00edch \u00fakol\u016f
+gb.acceptNewTickets=povolit nov\u00e9 \u00fakoly
+gb.anonymousUser=anonym
+gb.anonymousPolicy=Anonymn\u00ed prohl\u00ed\u017een\u00ed, klonov\u00e1n\u00ed a push
+gb.approve=schv\u00e1lit
+gb.yourAssignedTickets=p\u0159i\u0159azeno v\u00e1m
+gb.attributes=atributy
+gb.body=t\u011blo
+gb.caCompromise=CA kompromitov\u00e1na
+gb.canAdminDescription=m\u016f\u017ee spravovat Gitblit server
+gb.canCreateDescription=m\u016f\u017ee vytv\u00e1\u0159et osobn\u00ed repozit\u00e1\u0159
+gb.canFork=m\u016f\u017ee prov\u00e1d\u011bt fork
+gb.canForkDescription=m\u016f\u017ee prov\u00e1d\u011bt fork opr\u00e1vn\u011bn\u00fdch repozit\u00e1\u0159\u016f do osobn\u00edch repozit\u00e1\u0159\u016f
+gb.canNotLoadRepository=Nelze na\u010d\u00edst repozit\u00e1\u0159
+gb.canNotProposePatchset=nelze navrhnout sadu zm\u011bn
+gb.acceptNewPatchsetsDescription=p\u0159imout sady zm\u011bn p\u0159edan\u00fdch pomoc\u00ed push do tohoto repozit\u00e1\u0159e
+gb.checkoutStep2=P\u0159ekontrolovat sadu zm\u011bn
+gb.checkoutViaCommandLine=P\u0159ekontrolovat pomoc\u00ed p\u0159\u00edkazov\u00e9 \u0159\u00e1dky
+gb.checkout=checkout
+gb.clientCertificateBundleSent=Bal\u00edk klientsk\u00e9ho certifik\u00e1tu {0} odesl\u00e1n
+gb.closedMilestones=uzav\u0159en\u00e9 mezn\u00edky
+gb.comments=koment\u00e1\u0159e
+gb.commented=komentov\u00e1no
+gb.compareToMergeBase=porovnat k z\u00e1kladu slu\u010dov\u00e1n\u00ed
+gb.requireApprovalDescription=sady zm\u011bn mus\u00ed b\u00fdt odsouhlaseny p\u0159ed t\u00edm, ne\u017e je povoleno tla\u010d\u00edtko slou\u010den\u00ed
+gb.nComments={0} koment\u00e1\u0159\u016f
+gb.milestoneProgress={0} otev\u0159eno, {1} uzav\u0159eno
+gb.nOpenTickets={0} otev\u0159eno
+gb.nMoreCommits={0} v\u00edce commit\u016f \u00bb
+gb.diffStat={0} p\u0159id\u00e1n\u00ed a {1} odebr\u00e1n\u00ed
+gb.noForks={0} nem\u00e1 \u017e\u00e1dn\u00e9 forky
+gb.repositoryForked={0} byl forknut
+gb.userServiceDoesNotPermitPasswordChanges={0} nepovoluje zm\u011bnu hesla!
+gb.userServiceDoesNotPermitAddUser={0} nedovoluje p\u0159id\u00e1n\u00ed u\u017eivatelsk\u00e9ho \u00fa\u010dtu!
+gb.doesNotExistInTree={0} nen\u00ed obsa\u017een ve stromu {1}
+gb.branchStats={0} commit\u016f a {1} tag\u016f b\u011bhem {2}
+gb.nCommits={0} commit\u016f
+gb.nAttachments={0} p\u0159\u00edloh
+gb.externalPermissions={0} p\u0159\u00edstupov\u00e1 pr\u00e1va jsou spravov\u00e1na vzd\u00e1len\u011b
+gb.excludePermission={0} (vyjmout)
+gb.yourWatchedTickets=sledov\u00e1no v\u00e1mi
+gb.viewCertificate=zobrazit certifik\u00e1t
+gb.viewComparison=zobrazit porovn\u00e1n\u00ed t\u011bchto {0} commit\u016f \u00bb
+gb.verifyCommitter=ov\u011b\u0159it v\u00fdvoj\u00e1\u0159e
+gb.usernameUnavailable=U\u017eivatelsk\u00e9 jm\u00e9no ''{0}'' nen\u00ed dostupn\u00e9.
+gb.userPermissions=opr\u00e1vn\u011bn\u00ed u\u017eivatele
+gb.nameDescription=pou\u017e\u00edvat '/' k seskupen\u00ed repozit\u00e1\u0159\u016f, nap\u0159. libraries/mycoollib.git
+gb.heapUsed=pou\u017eit\u00e1 halda
+gb.uploadedPatchsetNRevisionN=nahr\u00e1na sada zm\u011bn {0} revize {1}
+gb.uploadedPatchsetN=nahr\u00e1na sada zm\u011bn {0}
+gb.unstar=odzna\u010d
+gb.title=titulek
+gb.ticketSettings=Nastaven\u00ed \u00fakolu
+gb.repositoryIsFrozen=Repozit\u00e1\u0159 je zmra\u017een\u00e9.
+gb.repositoryIsMirror=Repozit\u00e1\u0159 je zrcadlo pouze pro \u010dten\u00ed.
+gb.serverDoesNotAcceptPatchsets=Tento server neakceptuje sady zm\u011bn.
+gb.ticketIsClosed=Tento \u00fakol je uzav\u0159en\u00fd.
+gb.initWithReadmeDescription=Toto vytvo\u0159\u00ed jednoduch\u00fd README dokument ve va\u0161em repozit\u00e1\u0159i.
+gb.ticketPatchset=\u00fakol {0}, psada zm\u011bn {1}
+gb.vote=hlasovat pro toto {0}
+gb.votes=hlasy
+gb.watch=sledovat toto {0}
+gb.updatedBy=aktualizoval
+gb.unauthorizedAccessForRepository=Neautorizovan\u00fd p\u0159\u00edstup do repozit\u00e1\u0159e.
+gb.fileNotMergeable=Nelze prov\u00e9st commit {0}. Tento soubor nelze automaticky slou\u010dit.
+gb.patchsetNotApprovedMore=Posuzovatel mus\u00ed schv\u00e1lit tuto sadu zm\u011bn.
+gb.teamPermission=opr\u00e1vn\u011bn\u00ed nastavil "{0}" \u010dlenstv\u00ed v t\u00fdmu
+gb.ticketsWelcome=M\u016f\u017eete pou\u017e\u00edt \u00fakoly k organizaci va\u0161eho seznamu \u00fakol\u016f, diskutovat chyby a spolupracovat na sad\u00e1ch zm\u011bn.
+gb.sshKeyComment=Koment\u00e1\u0159
+gb.sshKeyCommentDescription=Vlo\u017ete voliteln\u00fd koment\u00e1\u0159. Pokud bude pr\u00e1zdn\u00fd, bude automaticky vyta\u017een z dat kl\u00ed\u010de.
+gb.feed=kan\u00e1l
+gb.recentActivityNone=posledn\u00edch {0} dn\u00ed / nic
+gb.selectFederationStrategy=Pros\u00edm zvolte strategii federace!
+gb.ptDescription=n\u00e1stroj sad zm\u011bn Gitblitu
+gb.milestone=mezn\u00edk
+gb.federationSets=sady federac\u00ed
+gb.diffRenamedFile=Soubor byl p\u0159ejmenov\u00e1n z {0}
+gb.noActivityToday=dnes tu nebyla \u017e\u00e1dn\u00e1 aktivita
+gb.noActivity=nebyla tu \u017e\u00e1dn\u00e1 aktivita b\u011bhem posledn\u00edch {0} dn\u00ed
+gb.dashboard=hlavn\u00ed panel
+gb.myDashboard=m\u016fj hlavn\u00ed panel
+gb.myProfile=m\u016fj profil
+gb.myTickets=moje \u00fakoly
+gb.merged=slou\u010deno
+gb.mergeTo=slou\u010dit do
+gb.mergedPatchset=slou\u010dit sadu zm\u011bn
+gb.mergedPullRequest=slou\u010dit \u017e\u00e1dost o p\u0159eta\u017een\u00ed
+gb.mergingViaCommandLine=Slou\u010dit pomoc\u00ed p\u0159\u00edkazov\u00e9 \u0159\u00e1dky
+gb.merge=slou\u010dit
+gb.heapMaximum=maxim\u00e1ln\u00ed halda
+gb.sortLowestPriority=nejni\u017e\u010d\u00ed priorita
+gb.luceneDisabled=indexov\u00e1n\u00ed pomoc\u00ed Lucene je zak\u00e1zan\u00e9
+gb.maintenanceTickets=\u00fadr\u017eba
+gb.isFork=je fork
+gb.isNotValidFile=nen\u00ed platn\u00fd soubor
+gb.key=Kl\u00ed\u010d
+gb.initialCommit=Prvn\u00ed commit
+gb.invalidExpirationDate=deplatn\u00e9 datum vypr\u0161en\u00ed!
+gb.invalidUsernameOrPassword=Neplatn\u00e9 u\u017eivatelsk\u00e9 jm\u00e9no nebo heslo!
+gb.filestoreHelp=Jak pou\u017e\u00edvat \u00dalo\u017ei\u010dt\u011b velk\u00fdch soubor\u016f?
+gb.filestore=velk\u00e9 soubory
+gb.filestoreStats=\u00dalo\u017ei\u0161t\u011b velk\u00fdch soubor\u016f obsahuje {0} soubor\u016f o celkov\u00e9 velikosti {1}. ({2} zb\u00fdv\u00e1)
+gb.findSomeRepositories=naj\u00edt n\u011bjak\u00e9 repozit\u00e1\u0159e
+gb.repositoryForkFailed=fork selhal
+gb.forkInProgress=fork prob\u00edh\u00e1
+gb.forkRepository=prov\u00e9st fork {0}?
+gb.forkedFrom=fork z
+gb.forks=forky
+gb.forksProhibited=vytvo\u0159en\u00ed forku zak\u00e1z\u00e1no
+gb.free=uvolnit
+gb.expiring=exportov\u00e1n\u00ed
+gb.excludeFromFederation=vyjmout z federace
+gb.deletePatchsetFailure=Chyba p\u0159i odstranov\u00e1n\u00ed sady zm\u011bn {0}.
+gb.enhancementTickets=vylep\u0161en\u00ed
+gb.requestTickets=vylep\u0161en\u00ed a \u00falohy
+gb.emailAddress=e-mailov\u00e1 adresa
+gb.gcPeriodDescription=doba mezi \u00faklidy
+gb.garbageCollection=\u00daklid
+gb.gcPeriod=GC perioda
+gb.gcThreshold=GC pr\u00e1h
+gb.gc=GC
+gb.sortLeastVotes=posledn\u00ed hlasy
+gb.sortLeastComments=posledn\u00ed koment\u00e1\u0159e
+gb.looksGood=vypad\u00e1 dob\u0159e
+gb.mergeBase=z\u00e1klad slou\u010den\u00ed
+gb.mergeStep3=Slou\u010dit navrhovan\u00e9 zm\u011bny a aktualizuj server
+gb.mentionsMeTickets=zmi\u0148uj\u00edc\u00ed v\u00e1s
+gb.mirrorOf=zrcadlo pro {0}
+gb.miscellaneous=r\u016fzn\u00e9
+gb.sortMostComments=nejv\u00edce koment\u00e1\u0159\u016f
+gb.mutable=prom\u011bnliv\u00fd
+gb.needsImprovement=pot\u0159ebuje zlep\u0161en\u00ed
+gb.newCertificateDefaults=v\u00fdchoz\u00ed hodnoty nov\u00e9ho certifik\u00e1tu
+gb.noComments=\u017e\u00e1dn\u00e9 koment\u00e1\u0159e
+gb.none=\u017e\u00e1dn\u00e9
+gb.noIndexedRepositoriesWarning=\u017e\u00e1dn\u00fd z va\u0161ich repozit\u00e1\u0159\u016f nen\u00ed nakonfigurov\u00e1n pro indexov\u00e1n\u00ed pomoc\u00ed Lucene
+gb.sortOldest=nejstar\u0161\u00ed
+gb.owned=vlastn\u00ed
+gb.starred=ozna\u010den\u00e9
+gb.starredAndOwned=ozna\u010den\u00e9 a vlastn\u00ed
+gb.starredRepositories=ozna\u010den\u00e9 repozit\u00e1\u0159e
+gb.star=ozna\u010dit
+gb.initWithGitignore=P\u0159idat soubor .gitignore
+gb.time.inMinutes=b\u011bhem {0} minut
+gb.time.inDays=b\u011bhem {0} dn\u00ed
+gb.languagePreference=Nastaven\u00ed jazyka
+gb.recentActivityStats=posledn\u00edch {0} dn\u00ed / {1} commit\u016f od {2} autor\u016f
+gb.incrementalPushTagMessage=Automaticky otagovan\u00e1 [{0}] v\u011btev p\u0159i push
+gb.undefinedQueryWarning=dotaz je nedefinovan\u00fd!
+gb.sessionEnded=Sezen\u00ed bylo zav\u0159eno
+gb.myUrlDescription=ve\u0159ejn\u011b dostupn\u00e1 URL va\u0161\u00ed Gitblit instance
+gb.addition=p\u0159\u00eddavek
+gb.accessPermissionsForUserDescription=nastavte \u010dlenstv\u00ed v t\u00fdmech nebo povolte p\u0159\u00edstup do zvolen\u00fdch omezen\u00fdch repozit\u00e1\u0159\u016f
+gb.accessPermissionsForTeamDescription=nastavte \u010dleny t\u00fdmu a povolte p\u0159\u00edstup do zvolen\u00fdch omezen\u00fdch repozit\u00e1\u0159\u016f
+gb.headRefDescription=V\u00fdchoz\u00ed v\u011btev kter\u00e1 bude klonov\u00e1na a bude zobrazena na souhrnn\u00e9 str\u00e1nce.
+gb.passwordChangeAborted=Zm\u011bna hesla p\u0159eru\u0161ena.
+gb.monthlyActivity=m\u011bs\u00ed\u010dn\u00ed aktivita
+gb.excludeFromFederationDescription=blokuj prov\u00e1den\u00ed pull tohoto \u00fa\u010dtu instanc\u00edm Gitblitu ve federaci
+gb.siteNameDescription=kr\u00e1tk\u00e9 popisn\u00e9 jm\u00e9no va\u0161eho serveru
+gb.federationRegistration=registrace federace
+gb.registrations=registrace federace
+gb.customFields=vlastn\u00ed pole
+gb.customFieldsDescription=vlastn\u00ed pole dostupn\u00e1 pro h\u00e1\u010dky Groovy
+gb.certificateRevoked=Certifik\u00e1t {0,\u010d\u00edslo,0} byl zam\u00edtnut
+gb.cessationOfOperation=zastaven\u00ed provozu
+gb.committed=commitov\u00e1no
+gb.commitActivityDOW=aktivita commit\u016f podle dne v t\u00fddnu
+gb.federationRepositoryDescription=sd\u00edlet tento repozit\u00e1\u0159 s ostatn\u00edmi Gitblit servery
+gb.allowAuthenticatedDescription=ud\u011blit pr\u00e1vo RW+ v\u0161em p\u0159ihl\u00e1\u0161en\u00fdm u\u017eivatel\u016fm
+gb.allowNamedDescription=ud\u011blit p\u0159esn\u00e1 opr\u00e1vn\u011bn\u00ed pojmenovan\u00fdm u\u017eivatel\u016fm a t\u00fdm\u016fm
+gb.authorizationControl=kontrola autorizace
+gb.countryCode=k\u00f3d zem\u011b
+gb.ticketOpenDate=datum otev\u0159en\u00ed
+gb.bootDate=datum startu
+gb.of=z
+gb.busyCollectingGarbage=Promint\u011b, Gitblit je zanepr\u00e1zdn\u011bn \u00faklidem v {0}
+gb.noProposals=Promint\u011b, {0} nyn\u00ed neakceptuje n\u00e1vrhy.
+gb.noFederation=Promint\u011b, {0} nen\u00ed nakonfigurov\u00e1n k federaci s jin\u00fdmi Gitblit instancemi.
+gb.noGitblitFound=Promint\u011b, {0} nelze nal\u00e9zt Gitblit instanci {1}
+gb.proposalFailed=Promint\u011b, {0} nep\u0159ijala \u017e\u00e1dn\u00e1 data n\u00e1vrhu!
+gb.proposalError=Promint\u011b, {0} hl\u00e1s\u00ed, \u017ee do\u0161lo k neo\u010dek\u00e1van\u00e9 chyb\u011b.
+gb.passwordHintRequired=n\u00e1pov\u011bda hesla je po\u017eadov\u00e1na!
+gb.in=v
+gb.stateProvince=st\u00e1t nebo provincie
+gb.forksProhibitedWarning=repozit\u00e1\u0159 zakazuje forky
+gb.workingCopyWarning=tento repozit\u00e1\u0159 m\u00e1 pracovn\u00ed kopii a nem\u016f\u017ee p\u0159ij\u00edmat push
+gb.federationStrategy=strategie federace
+gb.isFederated=je ve federaci
+gb.metricAuthorExclusions=vylou\u010dit autora z metrik
+gb.failedToFindAccount=chyba p\u0159i hled\u00e1n\u00ed u\u017eivatelsk\u00e9ho \u00fa\u010dtu ''{0}''
+gb.failedToFindGravatarProfile=Chyba p\u0159i hled\u00e1n\u00ed profilu Gravatar pro {0}
+gb.federateThis=tento repozit\u00e1\u0159 ve federaci
+gb.federateOrigin=origin ve federaci
+gb.fork=fork
+gb.combinedMd5Rename=Gitblit je nakonfigurov\u00e1n pro kombinovan\u00e9 MD5 he\u0161ov\u00e1n\u00ed hesla. Mus\u00edte zadat nov\u00e9 heslo p\u0159i p\u0159ejmenov\u00e1n\u00ed \u00fa\u010dtu.
+gb.inherited=zd\u011bd\u011bn\u00fd
+gb.todaysActivityStats=dnes / {1} commit\u016f od {2} autor\u016f
+gb.hostname=hostname
+gb.noMaximum=\u017e\u00e1dn\u00e9 maximum
+gb.failedtoRead=Chyba \u010dten\u00ed
+gb.showRemoteBranchesDescription=uk\u00e1zat vzd\u00e1len\u00e9 v\u011btve
+gb.showReadme=uk\u00e1zat readme
+gb.showReadmeDescription=uk\u00e1zat "readme" Markdown soubor na souhrnn\u00e9 str\u00e1nce
+gb.siteName=jm\u00e9no str\u00e1nky
+gb.clientCertificateGenerated=\u00dasp\u011b\u0161n\u011b vygenerov\u00e1n klientsk\u00fd certifik\u00e1t pro {0}
+gb.sslCertificateGeneratedRestart=\u00dasp\u011b\u0161n\u011b vygenerov\u00e1n serverov\u00fd SSL certifik\u00e1t pro {0}.\nMus\u00edte restartovat Gitblit pro pou\u017eit\u00ed nov\u00e9ho certifik\u00e1tu.\n\nPokud spou\u0161t\u00edte s parametrem '--alias', budete jej muset nastavit na ''--alias {0}''.
+gb.sslCertificateGenerated=\u00dasp\u011b\u0161n\u011b vygenerov\u00e1n serverov\u00fd SSL certifik\u00e1t pro {0}
+gb.viewAccess=V Gitblitu nem\u00e1te pr\u00e1vo pro p\u0159\u00edstup ke \u010dten\u00ed nebo z\u00e1pisu
+gb.createdNewTag=vytvo\u0159it nov\u00fd tag
+gb.enableIncrementalPushTags=povolit inkrement\u00e1ln\u00ed push tags
+gb.OneProposalToReview=Je tu jeden n\u00e1vrh na federaci \u010dekaj\u00edc\u00ed na vy\u0159\u00edzen\u00ed.
+gb.pushedOneCommitTo=proveden push 1 commitu do
+gb.pushedNewBranch=proveden push do nov\u00e9 v\u011btve
+gb.pushedNewTag=proveden push nov\u00e9ho tagu
+gb.pushedNCommitsTo=proveden push {0} commit\u016f do
+gb.authored=napsan\u00fd
+gb.skipSummaryMetrics=p\u0159esko\u010dit souhrnn\u00e9 metriky
+gb.maxActivityCommits=maxim\u00e1ln\u00ed aktivita commit\u016f
+gb.skipSummaryMetricsDescription=nepo\u010d\u00edtat metriky na souhrnn\u00e9 str\u00e1nce (redukuje \u010das na\u010d\u00edt\u00e1n\u00ed str\u00e1nky)
+gb.skipSizeCalculationDescription=nepo\u010d\u00edtat velikost repozit\u00e1\u0159e (redukuje \u010das na\u010d\u00edt\u00e1n\u00ed str\u00e1nky)
+gb.failedToFindCommit=Nelze nal\u00e9zt commit "{0}" v {1}!
+gb.markdownFailure=Chyba p\u0159i zpracov\u00e1n\u00ed Markdown obsahu!
+gb.failedToSendProposal=Selhalo odesl\u00e1n\u00ed n\u00e1vrhu!
+gb.couldNotFindTag=Nelze nal\u00e9zt tag {0}
+gb.couldNotFindFederationRegistration=Nelze nal\u00e9zt registraci federace!
+gb.couldNotFindFederationProposal=Nelze nal\u00e9zt n\u00e1vrh k federaci!
+gb.teamNameUnavailable=T\u00fdm se jm\u00e9nem ''{0}'' nen\u00ed dostupn\u00fd.
+gb.ownerDescription=vlastn\u00edk m\u016f\u017ee upravovat nastaven\u00ed repozit\u00e1\u0159e
+gb.newClientCertificateMessage=POZN\u00c1MKA:\n'Heslo' nen\u00ed u\u017eivatelsk\u00e9 heslo. Je to heslo k ochran\u011b \u00falo\u017ei\u0161t\u011b kl\u00ed\u010d\u016f. Toto heslo nebude nikam ulo\u017eeno tak\u017ee mus\u00edte zadat tak\u00e9 n\u00e1pov\u011bdu, kter\u00e1 bude p\u0159ilo\u017eena k instrukc\u00edm u\u017eivatelova README.
+gb.emailCertificateBundle=bal\u00edk klientsk\u00e9ho certifik\u00e1tu e-mailu
+gb.passwordChanged=Heslo \u00fasp\u011b\u0161n\u011b zm\u011bn\u011bno.
+gb.passwordsDoNotMatch=Hesla se neshoduj\u00ed!
+gb.permission=Opr\u00e1vn\u011bn\u00ed
+gb.teamPermissions=opr\u00e1vn\u011bn\u00ed t\u00fdmu
+gb.repositoryPermissions=opr\u00e1vn\u011bn\u00ed repozit\u00e1\u0159e
+gb.createdNewBranch=vytvo\u0159it novou v\u011bt\u011bv
+gb.teamMustSpecifyRepository=T\u00fdm mus\u00ed nastavit nejm\u00e9n\u011b jeden repozit\u00e1\u0159.
+gb.emailMeOnMyTicketChanges=Poslat e-mail p\u0159i zm\u011bn\u011b m\u00e9ho \u00fakolu
+gb.patchsetMergeableMore=Tato sada zm\u011bn m\u016f\u017ee b\u00fdt slou\u010dena do {0} tak\u00e9 pomoc\u00ed p\u0159\u00edkazov\u00e9 \u0159\u00e1dky.
+gb.createdThisTicket=\u00fakol vytvo\u0159en
+gb.showHideDetails=zobrazit/skr\u00fdt detaily
+gb.labels=popisky
+gb.topicsAndLabels=t\u00e9mata a popisky
+gb.topic=t\u00e9ma
+gb.preview=n\u00e1hled
+gb.commitsInPatchsetN=commity v sad\u011b zm\u011bn {0}
+gb.ptSimplifiedMerge=zjednodu\u0161en\u00e1 syntaxe slu\u010dov\u00e1n\u00ed
+gb.compareToN=porovnat s {0}
+gb.authenticatedPushPolicy=Omezit Push (P\u0159ihl\u00e1\u0161en\u00fd)
+gb.authenticatedPushPolicyDescription=Kdokoli m\u016f\u017ee prohl\u00ed\u017eet a klonovat tento repozit\u00e1\u0159. V\u0161ichni p\u0159ihl\u00e1\u0161en\u00ed u\u017eivatel\u00e9 budou m\u00edt pr\u00e1va RW+.
+gb.clonePolicyDescription=Kdokoli m\u016f\u017ee vid\u011bt tento repozit\u00e1\u0159. M\u016f\u017eete vybrat, kdo m\u016f\u017ee klonovat a prov\u00e1d\u011bt push.
+gb.clonePolicy=Omezit klonov\u00e1n\u00ed a Push
+gb.closeBrowser=Pros\u00edm zav\u0159ete prohl\u00ed\u017ee\u010d ke spr\u00e1vn\u00e9mu ukon\u010den\u00ed sezen\u00ed.
+gb.commitMessageRendererDescription=Zpr\u00e1va commitu m\u016f\u017ee b\u00fdt zobrazena jako prost\u00fd text nebo vykreslena jako markup.
+gb.commitMessageRenderer=vykreslen\u00ed zpr\u00e1vy commitu
+gb.commitChanges=Zmeny commitu
+gb.commitActivityTrend=Trend aktivity commit\u016f
+gb.commitActivityAuthors=prim\u00e1rn\u00ed auto\u0159i podle aktivity commit\u016f
+gb.createdNewPullRequest=vytvo\u0159ena \u017e\u00e1dost o p\u0159eta\u017een\u00ed
+gb.createFirstTicket=vytvo\u0159te v\u00e1\u0161 prvn\u00ed \u00fakol
+gb.createReadme=vytvo\u0159it README
+gb.deletedTag=smazat tag
+gb.deletePatchset=Smazat sadu zm\u011bn {0}
+gb.deletePatchsetSuccess=Smazat sadu zm\u011bn {0}.
+gb.deleteRepositoryHeader=Smazat repozit\u00e1\u0159
+gb.disableUser=zak\u00e1zat u\u017eivatele
+gb.disableUserDescription=zamezit tomuto \u00fa\u010dtu v p\u0159ihl\u00e1\u0161en\u00ed
+gb.displayNameDescription=Preferovan\u00e9 jm\u00e9no prozobrazen\u00ed
+gb.docsWelcome1=M\u016f\u017eete pou\u017e\u00edt dokumentaci k dokumentov\u00e1n\u00ed va\u0161eho repozit\u00e1\u0159e.
+gb.docsWelcome2=Za\u010dn\u011bte commitem souboru README.md nebo HOME.md.
+gb.emailClientCertificateSubject=V\u00e1\u0161 Gitblit klientsk\u00fd certifik\u00e1t pro {0}
+gb.manual=ru\u010dn\u011b
+gb.refs=refs
+gb.removeVote=vzd\u00e1len\u00e9 hlasov\u00e1n\u00ed
+gb.review=posoudit
+gb.revoked=zru\u0161it
+gb.rewind=REWIND
+gb.permissions=opr\u00e1vn\u011bn\u00ed
+gb.personalRepositories=osobn\u00ed repozit\u00e1\u0159e
+gb.pleaseSetDestinationUrl=Pros\u00edm zadejte cilovou URL pro v\u00e1\u0161 n\u00e1vrh!
+gb.patchset=sada zm\u011bn
+gb.proposal=n\u00e1vrh
+gb.proposalReceived=N\u00e1vrh \u00fasp\u011b\u0161n\u011b p\u0159ijat od {0}.
+gb.proposePatchset=navrhnout sadu zm\u011bn
+gb.proposeWith=navrhnout sadu zm\u011bn s {0}
+gb.proposedThisChange=navrhnout tuto zm\u011bnu
+gb.proposalTickets=navrhnout zm\u011bny
+gb.queries=dotazy
+gb.questionTickets=ot\u00e1zky
+gb.reason=d\u016fvod
+gb.received=p\u0159ijat
+gb.receiveSettings=Nastaven\u00ed p\u0159\u00edjmu
+gb.referencedByCommit=Odkazovan\u00fd commitem.
+gb.reflog=reflog
+gb.illegalRelativeSlash=RElativn\u00ed reference na adres\u00e1\u0159 (../) jsou zak\u00e1z\u00e1ny.
+gb.useTicketsDescription=pouze pro \u010dten\u00ed, idstribuovan\u00e9 Ticgit \u00fakoly
+gb.privilegeWithdrawn=sta\u017een\u00ed privilegi\u00ed
+gb.repositoryNotSpecifiedFor=Repozit\u00e1\u0159 nen\u00ed nastaven pro {0}!
+gb.ownerPermission=vlastn\u00edk repozit\u00e1\u0159e
+gb.requireApproval=vy\u017eaduje schv\u00e1len\u00ed
+gb.verifyCommitterDescription=vy\u017eaduje aby se identita v\u00fdvoj\u00e1\u0159e shodovala s Gitblit \u00fa\u010dtem pou\u017eit\u00fdm pro push
+gb.responsible=zodpov\u011bdn\u00fd
+gb.accessPermissionsDescription=omezit p\u0159\u00edstup na u\u017eivatel\u00e9 a t\u00fdmy
+gb.namedPushPolicy=Omezit Push (Pojmenovan\u00fd)
+gb.viewPolicy=Omezit prohl\u00ed\u017een\u00ed, klonov\u00e1n\u00ed a Push
+gb.raw=raw
+gb.searchForAuthor=Vyhledat commity podle autora
+gb.searchForCommitter=Vyhledat commity podle v\u00fdvoj\u00e1\u0159e
+gb.isFrozenDescription=zak\u00e1zat operace push
+gb.tokens=token federace
+gb.proposals=n\u00e1vrh federace
+gb.token=token
+gb.federationResults=v\u00fdsledek p\u0159eta\u017een\u00ed federace
+gb.maxHits=maximum v\u00fdsledk\u016f
+gb.preReceiveScripts=skript p\u0159ed p\u0159\u00edjmem
+gb.postReceiveScripts=skript po p\u0159\u00edjmu
+gb.hookScripts=k\u00e1\u010dkovac\u00ed skripty
+gb.hookScriptsDescription=spustit Groovy skript po push na tento Gitblit server
+gb.noHits=\u017e\u00e1dn\u00e9 v\u00fdsledky
+gb.queryResults=v\u00fdsledek {0} - {1} ({2} v\u00fdsledk\u016f)
+gb.pleaseSetRepositoryName=Pros\u00edm, nastavte jm\u00e9no repozit\u00e1\u0159e!
+gb.illegalLeadingSlash=Po\u010d\u00e1te\u010dn\u00ed reference na ko\u0159enov\u00fd adres\u00e1\u0159 (/) jsou zak\u00e1z\u00e1ny.
+gb.selectAccessRestriction=Pros\u00edm, vyberte omezen\u00ed p\u0159\u00edstupu!
+gb.pleaseSetTeamName=Pros\u00edm, zadejte jm\u00e9no t\u00fdmu!
+gb.pleaseSetUsername=Pros\u00edm, zadejte u\u017eivatelsk\u00e9 jm\u00e9no!
+gb.repositoryNotSpecified=Repozit\u00e1\u0159 nen\u00ed vybr\u00e1n!
+gb.pleaseSetGitblitUrl=Pros\u00edm, zadejte URL va\u0161eho Gitblitu!
+gb.errorOnlyAdminMayCreateRepository=Pouze administr\u00e1tor m\u016f\u017ee vytv\u00e1\u0159et repozit\u00e1\u0159
+gb.preparingFork=vytv\u00e1\u0159en\u00ed va\u0161eho forku...
+gb.illegalPersonalRepositoryLocation=v\u00e1\u0161 soukrom\u00fd repozit\u00e1\u0159 mus\u00ed b\u00fdt um\u00edst\u011bn v "{0}"
+gb.gcThresholdDescription=minim\u00e1ln\u00ed celkov\u00e1 velikost uvoln\u011bn\u00fdch objekt\u016f pro vyvol\u00e1n\u00ed \u00faklidu
+gb.missingPermission=repozit\u00e1\u0159 pro toto opr\u00e1vn\u011bn\u00ed chyb\u00ed!
+gb.revokeCertificateReason=Pros\u00edm, vyberte d\u016fvod zam\u00edtnut\u00ed certifik\u00e1tu
+gb.hostnameRequired=Pros\u00edm, zadejte hostname
+gb.pleaseGenerateClientCertificate=Pros\u00edm, vytvo\u0159te klientsk\u00fd certifik\u00e1t pro {0}
+gb.enterKeystorePassword=Pros\u00edm, zadejte heslo k \u00falo\u017ei\u0161ti kl\u00ed\u010d\u016f Gitblitu
+gb.maxActivityCommitsDescription=maxim\u00e1ln\u00ed po\u010det commit\u016f pro p\u0159isp\u011bn\u00ed na str\u00e1nku aktivit
+gb.serveCertificate=provozovat https s t\u00edmto certifik\u00e1tem
+gb.useIncrementalPushTagsDescription=p\u0159i akci push, automaticky tagovat ka\u017edou v\u011btev s inkrement\u00e1ln\u00edm \u010d\u00edslem revize
+gb.stargazers=pozorovatel\u00e9
+gb.reviewPatchset=posoudit {0} sadu zm\u011bn {1}
+gb.home=dom\u016f
+gb.mirrorWarning=tento repozit\u00e1\u0159 je zrcadlo a nem\u016f\u017ee p\u0159ij\u00edmat push
+gb.noDescriptionGiven=popis nen\u00ed zad\u00e1n
+gb.toBranch=do {0}
+gb.createdBy=vytvo\u0159il
+gb.patchsetMergeable=Tato sada zm\u011bn m\u016f\u017ee b\u00fdt automaticky slou\u010dena do {0}.
+gb.patchsetAlreadyMerged=Tato sada zm\u011bn byla slou\u010dena do {0}.
+gb.patchsetNotMergeable=Tato sada zm\u011bn nem\u016f\u017ee b\u00fdt automaticky slou\u010dena do {0}.
+gb.patchsetNotMergeableMore=Tato sada zm\u011bn mus\u00ed b\u00fdt p\u0159evedena na nov\u00fd z\u00e1klad nebo slou\u010dena manu\u00e1ln\u011b do {0}, aby se vy\u0159e\u0161ily konflikty.
+gb.patchsetNotApproved=Tato sada zm\u011bn nebyla schv\u00e1lena pro slou\u010den\u00ed do {0}.
+gb.taskTickets=\u00falohy
+gb.sortLeastRecentlyUpdated=naposledy aktualizovan\u00e9
+gb.searchTickets=prohledat \u00fakol
+gb.searchTicketsTooltip=prohledat {0} \u00fakol\u016f
+gb.changedStatus=zm\u011bnit status
+gb.proposePatchsetNote=Jste v\u00fdt\u00e1n\u00ed k navr\u017een\u00ed sady zm\u011bn pro tento \u00fakol.
+gb.proposeInstructions=Pro za\u010d\u00e1tek, vytvo\u0159te sadu zm\u011bn a nahrajte ji pomoc\u00ed Gitu. Gitblit propoj\u00ed va\u0161i sadu zm\u011bn s t\u00edmto \u00fakolem pomoc\u00ed id.
+gb.jceWarning=Va\u0161e Java Runtime Environment nem\u00e1 soubory "JCE Unlimited Strength Jurisdiction Policy".\nToto zmen\u0161\u00ed d\u00e9lku hesla, kterou m\u016f\u017eete za\u0161ifrovat \u00falo\u017ei\u0161t\u011b kl\u00ed\u010d\u016f na 7 znak\u016f.\nSoubory politik jsou dodate\u010dn\u00e1 mo\u017enost ke sta\u017een\u00ed od Oracle.\n\nP\u0159ejete si pokra\u010dovat a vygenerovat i p\u0159es to certifika\u010dn\u00ed infrastrukturu?\n\nOdpov\u011b\u010f Ne v\u00e1s p\u0159esm\u011bruje na str\u00e1nky Oracle, kde sy m\u016f\u017eete tyto soubory politik st\u00e1hnout.
+gb.byNAuthors=od {0} autor\u016f
+gb.byOneAuthor=od {0}
+gb.sortMostPatchsetRevisions=nejv\u00edce reviz\u00ed sad zm\u011bn
+gb.sortLeastPatchsetRevisions=nejm\u00e9n\u011b reviz\u00ed sad zm\u011bn
+gb.stepN=Krok {0}
+gb.stopWatching=p\u0159estat sledovat
+gb.mergeSha=slou\u010dit SHA
+gb.reviewers=recenzenti
+gb.mentions=zminuje
+gb.repositoryDoesNotAcceptPatchsets=Tento repozit\u00e1\u0159 nep\u0159ij\u00edm\u00e1 sady zm\u011bn.
+gb.openMilestones=otev\u0159en\u00e9 mezn\u00edky
+gb.extensions=roz\u0161\u00ed\u0159en\u00ed
+gb.pleaseSelectProject=Pros\u00edm, vyberte projekt!
+gb.checkoutViaCommandLineNote=M\u016f\u017eete p\u0159ekontrolovat a otestovat tyto zm\u011bny lok\u00e1ln\u011b v klonu tohoto repozit\u00e1\u0159e.
+gb.checkoutStep1=Z\u00edskat aktu\u00e1ln\u00ed sadu zm\u011bn \u2014 spus\u0165te toto z va\u0161eho projektov\u00e9ho adres\u00e1\u0159e
+gb.mergingViaCommandLineNote=Pokud si nep\u0159ejete pou\u017e\u00edt tla\u010d\u00edtko pro slou\u010den\u00ed, nebo nelze prov\u00e9st automatick\u00e9 slou\u010den\u00ed, m\u016f\u017eete prov\u00e9st manu\u00e1ln\u00ed slou\u010den\u00ed pomoc\u00ed p\u0159\u00edkazov\u00e9 \u0159\u00e1dky.
+gb.mergeStep1=Vytvo\u0159it novou v\u011btev pro prohl\u00e9dnut\u00ed zm\u011bn \u2014 spus\u0165te tot z va\u0161eho projektov\u00e9ho adres\u00e1\u0159e
+gb.mergeStep2=P\u0159evz\u00edt navrhovan\u00e9 zm\u011bny a prozkoumat
+gb.ptCheckout=St\u00e1hnout a p\u0159ekontrolovat aktu\u00e1ln\u00ed sadu zm\u011bn do posuzovac\u00ed v\u011btve
+gb.ptMerge=St\u00e1hnout a slou\u010dit aktu\u00e1ln\u00ed sadu zm\u011bn do va\u0161\u00ed lok\u00e1ln\u00ed v\u011btve
+gb.ptDescription1=Barnum je pomocn\u00edk v p\u0159\u00edkazov\u00e9 \u0159\u00e1dce pro Git, kter\u00fd zjednodu\u0161uje syntaxi pro pr\u00e1ci s \u00fakly a sadami zm\u011bn v Gitblitu.
+gb.ptSimplifiedCollaboration=zjednodu\u0161en\u00e1 syntaxe pro spolupr\u00e1ci
+gb.ptDescription2=Barnum vy\u017eaduje Python 3 a nativn\u00ed Git. B\u011b\u017e\u00ed na Windows, Linux a Mac OS X.
+gb.reviewedPatchsetRev=posouzen\u00e9 sady zm\u011bn {0} revize {1}: {2}
+gb.hasNotReviewed=neposoudil
+gb.mergeToDescription=v\u00fdchoz\u00ed integra\u010dn\u00ed v\u011btev pro slu\u010dov\u00e1n\u00ed sad zm\u011bn z \u00fakol\u016f
+gb.mergeTypeDescription=slou\u010dit \u00fakol s p\u0159esunem vp\u0159ed, pokud je pot\u0159eba, nebo v\u017edy s commitem o slou\u010den\u00ed do integra\u010dn\u00ed v\u011btve
+gb.youDoNotHaveClonePermission=Nem\u00e1te opr\u00e1vn\u011bn\u00ed pro klonov\u00e1n\u00ed tohoto repozit\u00e1\u0159e.
+gb.milestoneDeleteFailed=Selhalo odstran\u011bn\u00ed mezn\u00edku ''{0}''!
+gb.notifyChangedOpenTickets=pos\u00edlat upozorn\u011bn\u00ed na zm\u011bn\u011bn\u00e9 otev\u0159en\u00e9 \u00fakoly
+gb.accessPolicyDescription=Vyberte politiky p\u0159\u00edstupu pro ovl\u00e1d\u00e1n\u00ed viditelnosti a pr\u00e1vech gitu.
+gb.namedPushPolicyDescription=Kdokoli m\u016f\u017ee vid\u011bt a klonovat tento repozit\u00e1\u0159. M\u016f\u017eete vybrat, kdo m\u016f\u017ee prov\u00e1d\u011bt push.
+gb.viewPolicyDescription=M\u016f\u017eete vybrat, kdo m\u016f\u017ee vid\u011bt, klonovat a prov\u00e1d\u011bt push do tohoto repozit\u00e1\u0159e.
+gb.initialCommitDescription=Umo\u017en\u00ed v\u00e1m okam\u017eit\u00fd <code>git clone</code> tohoto repozit\u00e1\u0159e. P\u0159esko\u010dte tento krok, pokud jste ji\u017e spustili lok\u00e1ln\u011b <code>git init</code>.
+gb.initWithGitignoreDescription=Vlo\u017e\u00ed konfigura\u010dn\u00ed soubor, kter\u00fd bude \u0159\u00edkat klient\u016fm Git, jak\u00e9 soubory, adres\u00e1\u0159e nebo vzory ignorovat.
+gb.pleaseSelectGitIgnore=Pros\u00edm, vyberte soubor .gitignore
+gb.ownersDescription=Vlastn\u00edci mohou spravovat v\u0161echna nastaven\u00ed repozit\u00e1\u0159e, ale nemohou p\u0159ejmenovat repozit\u00e1\u0159, pokud to nen\u00ed jejich osobn\u00ed repozit\u00e1\u0159.
+gb.userPermissionsDescription=M\u016f\u017eete nastavit individu\u00e1ln\u00ed u\u017eivatelsk\u00e1 opr\u00e1vn\u011bn\u00ed. Toto nastaven\u00ed potla\u010d\u00ed pr\u00e1va t\u00fdmu nebo regex.
+gb.teamPermissionsDescription=M\u016f\u017eete nastavit individu\u00e1ln\u00ed opr\u00e1vn\u011bn\u00ed t\u00fdmu. Toto nastaven\u00ed potla\u010d\u00ed opr\u00e1vn\u011bn\u00ed regex.
+gb.receiveSettingsDescription=Nastaven\u00ed p\u0159\u00edjmu ovl\u00e1d\u00e1 prov\u00e1d\u011bn\u00ed operace push s repozit\u00e1\u0159em.
+gb.preReceiveDescription=H\u00e1\u010dky p\u0159ed p\u0159ijet\u00edm budou vykon\u00e1ny po p\u0159ijet\u00ed commitu, ale <em>P\u0158EDT\u00cdM</em>, ne\u017e budou aktualizov\u00e1ny refs.<p>Toto je vhodn\u00fd h\u00e1\u010dek pro zam\u00edtnut\u00ed operace push.</p>
+gb.postReceiveDescription=H\u00e1\u010dky po p\u0159ijet\u00ed budou vykon\u00e1ny po p\u0159ijet\u00ed commitu, ale <em>POTOM</em>, co budou aktualizov\u00e1ny refs.<p>Toto je vhodn\u00fd h\u00e1\u010dek pro upozorn\u011bn\u00ed, spu\u0161t\u011bn\u00ed sestaven\u00ed, atd.</p>
+gb.federationStrategyDescription=Ovl\u00e1dat jestli a jak federovat tento repozit\u00e1\u0159 s jim\u00fdn Gitblit.
+gb.federationSetsDescription=Tento repozit\u00e1\u0159 bude obsa\u017een ve vybran\u00fdch federac\u00edch.
+gb.originDescription=URL, ze kter\u00e9ho byl tento repozit\u00e1\u0159 klonov\u00e1no.
+gb.garbageCollectionDescription=\u00daklid zabal\u00ed voln\u00e9 objekty, kter\u00e9 byly nahr\u00e1ny klienty pomoc\u00ed push a odstran\u00ed obj\u011bkty, na kter\u00e9 v repozit\u00e1\u0159i nevede reference.
+gb.preferences=preference
+gb.accountPreferencesDescription=Stanovte preference va\u0161eho \u00fa\u010dtu
+gb.languagePreferenceDescription=Vyberte v\u00e1mi preferovan\u00fd p\u0159eklad pro Gitblit
+gb.emailMeOnMyTicketChangesDescription=Pos\u00edlat mi e-malov\u00e9 notifikace o zm\u011bn\u00e1ch, kter\u00e9 jsem provedl na \u00fakolech
+gb.sshKeys=SSH kl\u00ed\u010de
+gb.sshKeysDescription=SSH ve\u0159ejn\u00fd kl\u00ed\u010d je bezpe\u010dn\u00e1 alternativa k autentifikaci pomoc\u00ed hesla
+gb.sshKeyPermissionDescription=Ur\u010dete p\u0159\u00edstupov\u00e1 pr\u00e1va pro SSH kl\u00ed\u010d
+gb.transportPreferenceDescription=Vyberte transport, kter\u00fd preferujete pro klonov\u00e1n\u00ed
+gb.sortHighestSeverity=nejvy\u0161\u0161\u00ed z\u00e1va\u017enost
+gb.diffFileDiffTooLarge=Rozd\u00edl je p\u0159\u00edli\u0161 velk\u00fd
+gb.missingIntegrationBranchMore=C\u00edlov\u00e1 integra\u010dn\u00ed v\u011btev v repozit\u00e1\u0159i neexistuje!
+gb.queryHelp=Standardn\u00ed syntaxe dotaz\u016f je podporov\u00e1na.<p/><p/>Pod\u00edvejte se pros\u00edm na ${querySyntax} pro detaily.
+gb.querySyntax=Lucene Query Parser Syntax
+gb.diffTruncated=Diff o\u0159iznut po p\u0159edchoz\u00edm souboru
+gb.blame=blame
+gb.imgdiffSubtract=Od\u010d\u00edtat (\u010dern\u00e1 = stejn\u00e9l)
+gb.statusChangedOn=status zm\u011bn\u011bn na
+gb.statusChangedBy=status zm\u011bnil
+gb.fileCommitted=\u00dasp\u011b\u0161n\u011b commitov\u00e1no {0}.
+gb.due=z d\u016fvodu
+gb.mailingLists=e-mailov\u00e1 konference
+gb.blinkComparator=Blink kompar\u00e1tor
+palette.available=Dostupn\u00ed
+palette.selected=Vybran\u00ed
+
+# This last property for unit tests to test successful loading of the resource file
+gb.loadLang = \u010de\u0161tina
diff --git a/src/main/java/com/gitblit/wicket/GitBlitWebApp_de.properties b/src/main/java/com/gitblit/wicket/GitBlitWebApp_de.properties
index eca3fd2a..677144eb 100644
--- a/src/main/java/com/gitblit/wicket/GitBlitWebApp_de.properties
+++ b/src/main/java/com/gitblit/wicket/GitBlitWebApp_de.properties
@@ -140,7 +140,7 @@ 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.destinationUrl = Senden an
gb.destinationUrlDescription = Die URL der Gitblit-Instanz, an welche die Anfrage gestellt werden soll
gb.users = Benutzer
gb.federation = Verbindungen (Federation)
@@ -170,7 +170,6 @@ 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
@@ -218,7 +217,8 @@ 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.queryHelp = Standard Abfragesyntax wird unterst\u00fctzt.<p/><p/>Unter ${querySyntax} finden Sie weitere Details.
+gb.querySyntax = Lucene Query Parser Syntax
gb.queryResults = Ergebnisse {0} - {1} ({2} Treffer)
gb.noHits = Keine Treffer
gb.authored = ist Autor von
@@ -313,7 +313,7 @@ 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.allowNamedDescription = Benutzern und Teams feingranulare Rechte gew\u00e4hren
gb.markdownFailure = Markdown Inhalt konnte nicht geparst werden!
gb.clearCache = Cache leeren
gb.projects = Projekte
@@ -431,7 +431,7 @@ gb.pleaseGenerateClientCertificate = Bitte generieren Sie ein Client Zertifikat
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.jceWarning = 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
@@ -521,7 +521,7 @@ gb.mergedPatchset = Merge des Patchset durchgef\u00fchrt
gb.commented = kommentierte
gb.noDescriptionGiven = keine Beschreibung hinterlegt
gb.toBranch = nach {0}
-gb.createdBy = angelegt durch
+gb.createdBy = angelegt durch
gb.oneParticipant = {0} Teilnehmer
gb.nParticipants = {0} Teilnehmer
gb.noComments = keine Kommentare
@@ -591,7 +591,7 @@ 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.changedStatus = hat Status ge\u00e4ndert
gb.discussion = Diskussion
gb.updated = aktualisiert
gb.proposePatchset = Patchset Vorschlagen
@@ -719,7 +719,7 @@ gb.federationStrategyDescription = Bestimmen Sie, ob und wie dieses Repository m
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.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.
@@ -736,9 +736,8 @@ 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.sshKeyComment = 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
@@ -752,4 +751,7 @@ gb.diffCopiedFile = Datei kopiert von {0}
gb.diffTruncated = Diff nach obiger Datei abgeschnitten
gb.opacityAdjust = Transparenz
gb.blinkComparator = Blinkkomparator
-gb.imgdiffSubtract = Pixeldifferenz (schwarz = identisch) \ No newline at end of file
+gb.imgdiffSubtract = Pixeldifferenz (schwarz = identisch)
+
+# This last property for unit tests to test successful loading of the resource file
+gb.loadLang = Deutsch
diff --git a/src/main/java/com/gitblit/wicket/GitBlitWebApp_es.properties b/src/main/java/com/gitblit/wicket/GitBlitWebApp_es.properties
index 791b286e..2865aa91 100644
--- a/src/main/java/com/gitblit/wicket/GitBlitWebApp_es.properties
+++ b/src/main/java/com/gitblit/wicket/GitBlitWebApp_es.properties
@@ -18,7 +18,7 @@ gb.object = Objeto
gb.ticketId = Id Ticket
gb.ticketAssigned = Asignado
gb.ticketOpenDate = Fecha de apertura
-gb.ticketState = Estado
+gb.ticketStatus = Estado
gb.ticketComments = Comentarios
gb.view = Ver
gb.local = Local
@@ -58,12 +58,12 @@ gb.rename = Renombrar
gb.metrics = Movimientos
gb.stats = Estad&iacute;sticas
gb.markdown = Markdown
-gb.changedFiles = Archivos cambiados
+gb.changedFiles = Archivos cambiados
gb.filesAdded = {0} Archivos a\u00F1adidos
-gb.filesModified = {0} Archivos modificados
-gb.filesDeleted = {0} Archivos eliminados
-gb.filesCopied = {0} Archivos copiados
-gb.filesRenamed = {0} Archivos renombrados
+gb.filesModified = {0} Archivos modificados
+gb.filesDeleted = {0} Archivos eliminados
+gb.filesCopied = {0} Archivos copiados
+gb.filesRenamed = {0} Archivos renombrados
gb.missingUsername = Usuario omitido
gb.edit = Editar
gb.searchTypeTooltip = Seleccionar tipo de b\u00FAsqueda
@@ -170,7 +170,6 @@ gb.accessLevel = Nivel de acceso
gb.default = Predeterminado
gb.setDefault = Poner predeterminado
gb.since = Desde
-gb.status = Estado
gb.bootDate = Fecha de inicio
gb.servletContainer = Contenedor ServLet
gb.heapMaximum = Pila m\u00E1xima
@@ -218,7 +217,8 @@ gb.pages = P\u00E1ginas
gb.workingCopy = Copia de trabajo
gb.workingCopyWarning = Este repositorio tiene una copia de trabajo y no se le puede empujar
gb.query = Consulta
-gb.queryHelp = Se admite la sintaxis de consulta est\u00E1ndar.<p/>Por favor, lee el: <a target="_new" href="http://lucene.apache.org/core/old_versioned_docs/versions/3_5_0/queryparsersyntax.html">Analizador sint\u00E1ctico de consultas de Lucene</a> para m\u00E1s detalles.
+gb.queryHelp = Se admite la sintaxis de consulta est\u00E1ndar.<p/>Por favor, lee el: ${querySyntax} para m\u00E1s detalles.
+gb.querySyntax = Analizador sint\u00E1ctico de consultas de Lucene
gb.queryResults = Resultados {0} - {1} ({2} coincidencias)
gb.noHits = Sin coincidencias
gb.authored = Autor
@@ -237,7 +237,7 @@ gb.passwordTooShort = La contrase\u00F1a es muy corta. La longitud m\u00EDnima e
gb.passwordChanged = Contrase\u00F1a cambiada satisfactoriamente.
gb.passwordChangeAborted = Cambio de contrase\u00F1a abortado.
gb.pleaseSetRepositoryName = \u00A1Por favor introduce un nombre para el repositorio!
-gb.illegalLeadingSlash = Referencias a la carpeta ra\i00EDz (/) estu00E1n prohibidas.
+gb.illegalLeadingSlash = Referencias a la carpeta ra\u00EDz (/) estu00E1n prohibidas.
gb.illegalRelativeSlash = Referencias relativas a la carpeta (../) est\u00E1n prohibidas.
gb.illegalCharacterRepositoryName = \u00A1Caracter ilegal ''{0}'' en el nombre del repositorio!
gb.selectAccessRestriction = \u00A1Por favor selecciona la restricci\u00F3n de acceso!
@@ -253,8 +253,8 @@ gb.userCreated = Nuevo usuario ''{0}'' creado satisfactoriamente.
gb.couldNotFindFederationRegistration = \u00A1No se pudo encontrar el registro de federaci\u00F3n!
gb.failedToFindGravatarProfile = Fallo al buscar el perfil Gravatar de {0}
gb.branchStats = {0} consigna(s) y {1} etiqueta(s) en {2}
-gb.repositoryNotSpecified = /u00A1Repositorio no especificado!
-gb.repositoryNotSpecifiedFor = /u00A1Repositorio no especificado para {0}!
+gb.repositoryNotSpecified = \u00A1Repositorio no especificado!
+gb.repositoryNotSpecifiedFor = \u00A1Repositorio no especificado para {0}!
gb.canNotLoadRepository = No se puede cargar el repositorio
gb.commitIsNull = La consigna es nula
gb.unauthorizedAccessForRepository = Acceso no autorizado al repositorio
@@ -271,9 +271,9 @@ gb.proposalReceived = Propuesta recibida satisfactoriamente por {0}.
gb.noGitblitFound = Lo siento, {0} no puede encontrar una instancia de GitBlit en {1}.
gb.noProposals = Lo siento, {0} no acepta propuestas en este momento.
gb.noFederation = Lo siento, {0} no est\u00E1 configurado para Federar con ninguna instancia de GitBlit.
-gb.proposalFailed = /u00A1Lo siento, {0} no ha recibido ning\u00FAn dato de propuesta!
-gb.proposalError = /u00A1Lo siento, {0} informa de que ha ocurrido un error inesperado!
-gb.failedToSendProposal = /u00A1Fallo al enviar la propuesta!
+gb.proposalFailed = \u00A1Lo siento, {0} no ha recibido ning\u00FAn dato de propuesta!
+gb.proposalError = \u00A1Lo siento, {0} informa de que ha ocurrido un error inesperado!
+gb.failedToSendProposal = \u00A1Fallo al enviar la propuesta!
gb.userServiceDoesNotPermitAddUser = \u00A1{0} no permite a\u00F1adir una cuenta de usuario!
gb.userServiceDoesNotPermitPasswordChanges = \u00A1{0} no permite cambio de contrase\u00F1a!
gb.displayName = Nombre
@@ -411,7 +411,7 @@ gb.keyCompromise = Clave de compromiso
gb.caCompromise = Compromiso CA
gb.affiliationChanged = Afiliaci\u00F3n cambiada
gb.superseded = Sustituida
-gb.cessationOfOperation = Cese de operaci\u00F3n
+gb.cessationOfOperation = Cese de operaci\u00F3n
gb.privilegeWithdrawn = Privilegios retirados
gb.time.inMinutes = en {0} mints
gb.time.inHours = en {0} horas
@@ -420,7 +420,7 @@ gb.hostname = Nombre de host
gb.hostnameRequired = Por favor, introduzca un nombre de host
gb.newSSLCertificate = Nuevo certificado SSL del servidor
gb.newCertificateDefaults = Nuevo certificado predeterminado
-gb.duration = Duraci\u00F3n
+gb.duration = Duraci\u00F3n
gb.certificateRevoked = El cretificado {0,n\u00FAmero,0} ha sido revocado
gb.clientCertificateGenerated = Nuevo certificado de cliente generado correctamente para {0}
gb.sslCertificateGenerated = Nuevo certificado de SSL generado correctamente para {0}
@@ -440,15 +440,15 @@ gb.serveCertificate = Servidor https con este certificado
gb.sslCertificateGeneratedRestart = Certificado SSL generado correctamente para {0}.\nDebes reiniciar Gitblit para usar el nuevo certificado.\n\nSi lo has iniciado con la opci\u00F3n '--alias' deber\u00E1s ajustar dicha opci\u00F3n a ''--alias {0}''.
gb.validity = Vigencia
gb.siteName = Nombre del sitio
-gb.siteNameDescription = Nombre corto y descriptivo de tu servidor
+gb.siteNameDescription = Nombre corto y descriptivo de tu servidor
gb.excludeFromActivity = Excluir de la p\u00E1gina de actividad
gb.isSparkleshared = compartido mediante Sparkleshared
gb.owners = Propiet\u00E1rios
gb.sessionEnded = La sesi\u00F3n ha sido cerrada
gb.closeBrowser = Porfavor cierre el navegador para terminar correctamente la sesi\u00F3n.
-b.doesNotExistInTree = {0} no existe en el \u00E1rbol {1}
+gb.doesNotExistInTree = {0} no existe en el \u00E1rbol {1}
gb.enableIncrementalPushTags = Etiquetar incrementalmente al empujar
-gb.useIncrementalPushTagsDescription = Al empujar, etiquetar autom\u00E1ticamente cada punta de la rama con un n\u00FAmero incremental de revisi\u00F3n
+gb.useIncrementalPushTagsDescription = Al empujar, etiquetar autom\u00E1ticamente cada punta de la rama con un n\u00FAmero incremental de revisi\u00F3n
gb.incrementalPushTagMessage = Auto-etiquetado [{0}] rama al empujar
gb.externalPermissions = {0} los permisos de acceso son mantenidos externamente
gb.viewAccess = No tienes acceso de lectura o escritura a Gitblit
@@ -505,7 +505,7 @@ gb.noActivityToday = hoy no ha habido actividad
gb.anonymousUser= an\u00F3nimo
gb.commitMessageRenderer = Procesador de mensaje en consignas
gb.diffStat = {0} inserciones | {1} supresiones
-b.home = Home
+gb.home = Home
gb.isMirror = Este repositorio es un espejo
gb.mirrorOf = Espejo de {0}
gb.mirrorWarning = Este repositorio es un espejo y no se le puede empujar
@@ -642,7 +642,7 @@ gb.patchsetN = parche {0}
gb.reviewedPatchsetRev = parche revisado {0} revisi\u00F3n {1}: {2}
gb.review = revista
gb.reviews = revisiones
-gb.veto = veto
+gb.veto = veto
gb.needsImprovement = necesita mejorar
gb.looksGood = parece bueno
gb.approve = aprobar
@@ -669,4 +669,8 @@ gb.repositoryIsFrozen = Este Repositorio est\u00E1 congelado.
gb.repositoryDoesNotAcceptPatchsets = Este Repositorio no acepta parches.
gb.serverDoesNotAcceptPatchsets = Este Servidor-Git no acepta parches.
gb.ticketIsClosed = Este ticket est\u00E1 cerrado.
-gb.mergeToDescription = Rama de integraciu00F3n predeterminada para combinar ticket y parches \ No newline at end of file
+gb.mergeToDescription = Rama de integraciu00F3n predeterminada para combinar ticket y parches
+gb.sshKeyComment = Comentar
+
+# This last property for unit tests to test successful loading of the resource file
+gb.loadLang = Espa\u00f1ol
diff --git a/src/main/java/com/gitblit/wicket/GitBlitWebApp_fr.properties b/src/main/java/com/gitblit/wicket/GitBlitWebApp_fr.properties
index d479b3d6..f02748c0 100644
--- a/src/main/java/com/gitblit/wicket/GitBlitWebApp_fr.properties
+++ b/src/main/java/com/gitblit/wicket/GitBlitWebApp_fr.properties
@@ -49,7 +49,7 @@ gb.tagger = tagger
gb.moreHistory = plus d'historique...
gb.difftocurrent = diff to current
gb.search = recherche
-gb.searchForAuthor = Recherche des livraisons dont l'auteur est
+gb.searchForAuthor = Recherche des livraisons dont l'auteur est
gb.searchForCommitter = Recherche des livraisons livr\u00e9es par
gb.addition = ajout
gb.modification = modification
@@ -108,7 +108,7 @@ gb.federateThis = f\u00e9d\u00e9rer ce d\u00e9p\u00f4t
gb.federateOrigin = f\u00e9d\u00e9rer "origin"
gb.excludeFromFederation = exclure de la f\u00e9d\u00e9ration
gb.excludeFromFederationDescription = emp\u00eacher les instances Gitblit f\u00e9d\u00e9r\u00e9e de "puller" ce compte
-gb.tokens = jettons de la f\u00e9d\u00e9ration
+gb.tokens = jettons de la f\u00e9d\u00e9ration
gb.tokenAllDescription = tous les d\u00e9p\u00f4ts, utilisateurs & param\u00e8tres
gb.tokenUnrDescription = tous les d\u00e9p\u00f4ts & utilisateurs
gb.tokenJurDescription = tous les d\u00e9p\u00f4ts
@@ -170,7 +170,6 @@ gb.accessLevel = niveau d'acc\u00e8s
gb.default = d\u00e9faut
gb.setDefault = d\u00e9finir par d\u00e9faut
gb.since = depuis
-gb.status = \u00e9tat
gb.bootDate = date de d\u00e9marrage
gb.servletContainer = conteneur de servlet
gb.heapMaximum = pile maximum
@@ -218,7 +217,8 @@ gb.pages = pages
gb.workingCopy = d\u00e9p\u00f4t de travail
gb.workingCopyWarning = ce d\u00e9p\u00f4t poss\u00e8de un d\u00e9p\u00f4t de travail et ne peut donc pas recevoir de pushes
gb.query = recherche
-gb.queryHelp = La syntaxe Lucene standard est support\u00e9e.<p/><p/>Se r\u00e9f\u00e9rer \u00e0 la <a target="_new" href="http://lucene.apache.org/core/old_versioned_docs/versions/3_5_0/queryparsersyntax.html">Syntaxe de recherche Lucene</a> pour plus de d\u00e9tails.
+gb.queryHelp = La syntaxe Lucene standard est support\u00e9e.<p/><p/>Se r\u00e9f\u00e9rer \u00e0 la ${querySyntax} pour plus de d\u00e9tails.
+gb.querySyntax = Syntaxe de recherche Lucene
gb.queryResults = r\u00e9sultats {0} - {1} ({2} hits)
gb.noHits = aucun r\u00e9sultats
gb.authored = authored
@@ -437,7 +437,7 @@ gb.maxActivityCommitsDescription = nombre maximum de livraisons pour contribuer
gb.noMaximum = illimit\u00e9
gb.attributes = attributes
gb.serveCertificate = servir https avec ce certificat
-gb.sslCertificateGeneratedRestart = Un nouveau certificat serveur SSL a \u00e9t\u00e9 g\u00e9n\u00e9r\u00e9 avec succ\u00e8s pour {0}.\Gitblit doit \u00eatre red\u00e9marrer pour prendre en compte le nouveau certificat.\n\nSi vous utilisez l'option de demarrage '--alias', v\u00e9rifiez qu'elle est d\u00e9finie comme suit : ''--alias {0}''.
+gb.sslCertificateGeneratedRestart = Un nouveau certificat serveur SSL a \u00e9t\u00e9 g\u00e9n\u00e9r\u00e9 avec succ\u00e8s pour {0}.\nGitblit doit \u00eatre red\u00e9marrer pour prendre en compte le nouveau certificat.\n\nSi vous utilisez l'option de demarrage '--alias', v\u00e9rifiez qu'elle est d\u00e9finie comme suit : ''--alias {0}''.
gb.validity = validit\u00e9
gb.siteName = nom du site
gb.siteNameDescription = Nom court et descriptif du serveur
@@ -642,7 +642,7 @@ gb.patchsetN = correctif {0}
gb.reviewedPatchsetRev = correctif approuv\u00e9 {0} revision {1}: {2}
gb.review = v\u00e9rification
gb.reviews = v\u00e9rifications
-gb.veto = v\u00e9to
+gb.veto = v\u00e9to
gb.needsImprovement = demande am\u00e9lioration
gb.looksGood = semble bon
gb.approve = approuver
@@ -681,4 +681,8 @@ gb.diffCopiedFile = Fichier copi\u00e9 de {0}
gb.diffTruncated = Affichage de diff\u00e9rences supprim\u00e9e apr\u00e8s le fichier ci-dessus
gb.opacityAdjust = ajuster l'opacit\u00e9
gb.blinkComparator = Comparateur \u00e0 clignotement
-gb.imgdiffSubtract = Diff\u00e9rence (noir = identique) \ No newline at end of file
+gb.imgdiffSubtract = Diff\u00e9rence (noir = identique)
+gb.sshKeyComment = Commentaire
+
+# This last property for unit tests to test successful loading of the resource file
+gb.loadLang = fran\u00e7ais
diff --git a/src/main/java/com/gitblit/wicket/GitBlitWebApp_it.properties b/src/main/java/com/gitblit/wicket/GitBlitWebApp_it.properties
index 7230ec83..e0c406fe 100644
--- a/src/main/java/com/gitblit/wicket/GitBlitWebApp_it.properties
+++ b/src/main/java/com/gitblit/wicket/GitBlitWebApp_it.properties
@@ -170,7 +170,6 @@ 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
@@ -218,7 +217,8 @@ 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.queryHelp = La sintassi standard supportata.<p/><p/>Si vedi ${querySyntax} per ulteriori dettagli.
+gb.querySyntax = Lucene Query Parser Syntax
gb.queryResults = risultati {0} - {1} ({2} corrispondenze)
gb.noHits = nessuna corrispondenza
gb.authored = creato da
@@ -323,7 +323,7 @@ gb.copyToClipboard = copia negli appunti
gb.fork = ramificazione
gb.forks = ramificazioni
gb.forkRepository = crea una ramificazione {0}?
-gb.repositoryForked = ramificazione creata per {0}
+gb.repositoryForked = ramificazione creata per {0}
gb.repositoryForkFailed= ramificazione fallita
gb.personalRepositories = repository personali
gb.allowForks = consenti ramificazioni
@@ -596,7 +596,7 @@ 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.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
@@ -642,7 +642,7 @@ gb.patchsetN = patch {0}
gb.reviewedPatchsetRev = patch analizzata {0} revisione {1}: {2}
gb.review = revisione
gb.reviews = revisioni
-gb.veto = veto
+gb.veto = veto
gb.needsImprovement = da migliorare
gb.looksGood = sembra buona
gb.approve = approvata
@@ -719,7 +719,7 @@ gb.federationStrategyDescription = Stabilisce se e come federare questo reposito
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.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
@@ -736,10 +736,11 @@ 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.sshKeyComment = 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
+# This last property for unit tests to test successful loading of the resource file
+gb.loadLang = Italiano
diff --git a/src/main/java/com/gitblit/wicket/GitBlitWebApp_ja.properties b/src/main/java/com/gitblit/wicket/GitBlitWebApp_ja.properties
index c8a2449f..880fb897 100644
--- a/src/main/java/com/gitblit/wicket/GitBlitWebApp_ja.properties
+++ b/src/main/java/com/gitblit/wicket/GitBlitWebApp_ja.properties
@@ -1,352 +1,709 @@
+
+# リポジトリ
gb.repository = \u30ea\u30dd\u30b8\u30c8\u30ea
+# 所有者
gb.owner = \u6240\u6709\u8005
+# 説明
gb.description = \u8aac\u660e
+# 最後の変更
gb.lastChange = \u6700\u5f8c\u306e\u5909\u66f4
-gb.refs = refs
+# 参照
+gb.refs = \u53c2\u7167
+# タグ
gb.tag = \u30bf\u30b0
+# タグ
gb.tags = \u30bf\u30b0
+# 作者
gb.author = \u4f5c\u8005
-gb.committer = \u30b3\u30df\u30c3\u30bf\u30fc
+# コミット者
+gb.committer = \u30b3\u30df\u30c3\u30c8\u8005
+# コミット
gb.commit = \u30b3\u30df\u30c3\u30c8
-gb.tree = tree
+# #リポジトリトップ→「ツリー」→ [ファイル]の「注釈履歴」で、いまいち機能の分からないページを出すと現れる。
+# #これの機能自体もよく分からない。
+gb.age = age
+# ツリー
+gb.tree = \u30c4\u30ea\u30fc
+# 親
gb.parent = \u89aa
gb.url = URL
+# 履歴
gb.history = \u5c65\u6b74
-gb.raw = raw
-gb.object = object
+# 直
+gb.raw = \u76f4
+# タグの対象
+# #タグ詳細ページのターゲットコミットに、(何故か) "object [コミットID] コミット" という表記がある。
+# #この "object" の部分。オブジェクトはおかしいので、こう訳した。
+gb.object = \u30bf\u30b0\u306e\u5bfe\u8c61
+# チケットID
gb.ticketId = \u30c1\u30b1\u30c3\u30c8ID
+# 割り当て済み
gb.ticketAssigned = \u5272\u308a\u5f53\u3066\u6e08\u307f
+# オープン日
gb.ticketOpenDate = \u30aa\u30fc\u30d7\u30f3\u65e5
-gb.ticketState = \u72b6\u614b
+# 状態
+gb.ticketStatus= \u72b6\u614b
+# コメント
gb.ticketComments = \u30b3\u30e1\u30f3\u30c8
+# 見る
gb.view = \u898b\u308b
+# ローカル
gb.local = \u30ed\u30fc\u30ab\u30eb
+# リモート
gb.remote = \u30ea\u30e2\u30fc\u30c8
+# ブランチ
gb.branches = \u30d6\u30e9\u30f3\u30c1
+# パッチ
gb.patch = \u30d1\u30c3\u30c1
-gb.diff = diff
+# 差分
+gb.diff = \u5dee\u5206
+# ログ
gb.log = \u30ed\u30b0
-gb.moreLogs = more commits...
-gb.allTags = all tags...
-gb.allBranches = all branches...
+# コミット一覧
+gb.moreLogs = \u30b3\u30df\u30c3\u30c8\u4e00\u89a7
+# タグ一覧
+gb.allTags = \u30bf\u30b0\u4e00\u89a7
+# ブランチ一覧
+gb.allBranches = \u30d6\u30e9\u30f3\u30c1\u4e00\u89a7
+# 概要
gb.summary = \u6982\u8981
+# チケット
gb.ticket = \u30c1\u30b1\u30c3\u30c8
+# リポジトリ作成
gb.newRepository = \u30ea\u30dd\u30b8\u30c8\u30ea\u4f5c\u6210
+# ユーザー作成
gb.newUser = \u30e6\u30fc\u30b6\u30fc\u4f5c\u6210
-gb.commitdiff = commitdiff
+# コミットの差分
+gb.commitdiff = \u30b3\u30df\u30c3\u30c8\u306e\u5dee\u5206
+# チケット
gb.tickets = \u30c1\u30b1\u30c3\u30c8
-gb.pageFirst = first
-gb.pagePrevious = prev
-gb.pageNext = next
+# 最初のページ
+gb.pageFirst = \u6700\u521d\u306e\u30da\u30fc\u30b8
+# 前へ
+gb.pagePrevious = \u524d\u3078
+# 次へ
+gb.pageNext = \u6b21\u3078
gb.head = HEAD
+# 注釈履歴
gb.blame = \u6ce8\u91c8\u5c65\u6b74
+# ログイン
gb.login = \u30ed\u30b0\u30a4\u30f3
+# ログアウト
gb.logout = \u30ed\u30b0\u30a2\u30a6\u30c8
+# ユーザー名
gb.username = \u30e6\u30fc\u30b6\u30fc\u540d
+# パスワード
gb.password = \u30d1\u30b9\u30ef\u30fc\u30c9
-gb.tagger = tagger
-gb.moreHistory = more history...
-gb.difftocurrent = diff to current
+# タグ添付者
+# #タグ詳細ページにある
+gb.tagger = \u30bf\u30b0\u6dfb\u4ed8\u8005
+# 続き...
+gb.moreHistory = \u7d9a\u304d...
+# 現在との差分
+# #リポジトリトップ→「ツリー」→ [ファイル]の「履歴」で、ファイル単位の履歴ページを出すと現れる。
+gb.difftocurrent = \u73fe\u5728\u3068\u306e\u5dee\u5206
+# 検索
gb.search = \u691c\u7d22
-gb.searchForAuthor = Search for commits authored by
-gb.searchForCommitter = Search for commits committed by
+# 作者で検索
+gb.searchForAuthor = \u4f5c\u8005\u3067\u691c\u7d22
+# コミット者で検索
+gb.searchForCommitter = \u30b3\u30df\u30c3\u30c8\u8005\u3067\u691c\u7d22
+# 追加
gb.addition = \u8ffd\u52a0
+# 修正
gb.modification = \u4fee\u6b63
+# 削除
gb.deletion = \u524a\u9664
+# リネーム
gb.rename = \u30ea\u30cd\u30fc\u30e0
+# 指標
gb.metrics = \u6307\u6a19
+# 統計
gb.stats = \u7d71\u8a08
-gb.markdown = markdown
-gb.changedFiles = \u5909\u66f4\u3055\u308c\u305f\u30d5\u30a1\u30a4\u30eb
+# マークダウン
+# #おそらく未使用
+gb.markdown = \u30de\u30fc\u30af\u30c0\u30a6\u30f3
+# 変更されたファイル
+gb.changedFiles = \u5909\u66f4\u3055\u308c\u305f\u30d5\u30a1\u30a4\u30eb
+# ファイルが追加
gb.filesAdded = {0} \u30d5\u30a1\u30a4\u30eb\u304c\u8ffd\u52a0
+# ファイルが変更
gb.filesModified = {0} \u30d5\u30a1\u30a4\u30eb\u304c\u5909\u66f4
+# ファイルが削除
gb.filesDeleted = {0} \u30d5\u30a1\u30a4\u30eb\u304c\u524a\u9664
+# ファイルがコピー
gb.filesCopied = {0} \u30d5\u30a1\u30a4\u30eb\u304c\u30b3\u30d4\u30fc
+# ファイルがリネーム
gb.filesRenamed = {0} \u30d5\u30a1\u30a4\u30eb\u304c\u30ea\u30cd\u30fc\u30e0
+# ユーザー名がありません
gb.missingUsername = \u30e6\u30fc\u30b6\u30fc\u540d\u304c\u3042\u308a\u307e\u305b\u3093
+# 編集
gb.edit = \u7de8\u96c6
-gb.searchTypeTooltip = Select Search Type
+# 検索方法を選択
+gb.searchTypeTooltip = \u691c\u7d22\u65b9\u6cd5\u3092\u9078\u629e
+# {0}から検索
gb.searchTooltip = {0}\u304b\u3089\u691c\u7d22
+# 削除
gb.delete = \u524a\u9664
-gb.docs = docs
+# ドキュメント
+gb.docs = \u30c9\u30ad\u30e5\u30e1\u30f3\u30c8
+# アクセス制限
gb.accessRestriction = \u30a2\u30af\u30bb\u30b9\u5236\u9650
+# 名前
gb.name = \u540d\u524d
+# チケットを有効化
gb.enableTickets = \u30c1\u30b1\u30c3\u30c8\u3092\u6709\u52b9\u5316
+# ドキュメントを有効化
gb.enableDocs = \u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u3092\u6709\u52b9\u5316
+# 保存
gb.save = \u4fdd\u5b58
+# リモートブランチを表示
gb.showRemoteBranches = \u30ea\u30e2\u30fc\u30c8\u30d6\u30e9\u30f3\u30c1\u3092\u8868\u793a
+# ユーザー編集
gb.editUsers = \u30e6\u30fc\u30b6\u30fc\u7de8\u96c6
+# パスワード(確認)
gb.confirmPassword = \u30d1\u30b9\u30ef\u30fc\u30c9(\u78ba\u8a8d)
+# 制限リポジトリ
gb.restrictedRepositories = \u5236\u9650\u30ea\u30dd\u30b8\u30c8\u30ea
+# 管理者
gb.canAdmin = \u7ba1\u7406\u8005
+# 匿名 view, clone, & push
gb.notRestricted = \u533f\u540d view, clone, & push
+# 認証 push
gb.pushRestricted = \u8a8d\u8a3c push
+# 認証 clone & push
gb.cloneRestricted = \u8a8d\u8a3c clone & push
+# 認証 view, clone, & push
gb.viewRestricted = \u8a8d\u8a3c view, clone, & push
+# 分散イシュー管理システム Ticgit を利用する
gb.useTicketsDescription = \u5206\u6563\u30a4\u30b7\u30e5\u30fc\u7ba1\u7406\u30b7\u30b9\u30c6\u30e0 Ticgit \u3092\u5229\u7528\u3059\u308b
+# リポジトリ内の Markdown ドキュメントを列挙する
gb.useDocsDescription = \u30ea\u30dd\u30b8\u30c8\u30ea\u5185\u306e Markdown \u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u3092\u5217\u6319\u3059\u308b
+# リポジトリ内の Markdown ドキュメントを列挙する
gb.showRemoteBranchesDescription = \u30ea\u30e2\u30fc\u30c8\u30d6\u30e9\u30f3\u30c1\u3092\u8868\u793a\u3059\u308b
-gb.canAdminDescription = Gitblit\u30b5\u30fc\u30d0\u30fc\u306e\u7ba1\u7406\u8005
+# Gitblitサーバーの管理者
+gb.canAdminDescription = Gitblit \u30b5\u30fc\u30d0\u30fc\u306e\u7ba1\u7406\u8005
+# 許可されたユーザー
gb.permittedUsers = \u8a31\u53ef\u3055\u308c\u305f\u30e6\u30fc\u30b6\u30fc
+# 凍結
gb.isFrozen = \u51cd\u7d50
-gb.isFrozenDescription = push\u64cd\u4f5c\u3092\u62d2\u5426\u3059\u308b
-gb.zip = zip
+# プッシュ操作を拒否します
+gb.isFrozenDescription = \u30d7\u30c3\u30b7\u30e5\u64cd\u4f5c\u3092\u62d2\u5426\u3057\u307e\u3059
+gb.zip = Zip
+# readme表示
gb.showReadme = readme\u8868\u793a
+# "readme" Markdownファイルを概要ページに表示する
gb.showReadmeDescription = \"readme\" Markdown\u30d5\u30a1\u30a4\u30eb\u3092\u6982\u8981\u30da\u30fc\u30b8\u306b\u8868\u793a\u3059\u308b
-gb.nameDescription = \u30ea\u30dd\u30b8\u30c8\u30ea\u3092\u30b0\u30eb\u30fc\u30d7\u5316\u3059\u308b\u306b\u306f '/' \u3092\u4f7f\u3046\u3002 e.g. libraries/mycoollib.git
+# リポジトリをグループ化するには '/' を使います。 例: libraries/mycoollib.git
+gb.nameDescription = \u30ea\u30dd\u30b8\u30c8\u30ea\u3092\u30b0\u30eb\u30fc\u30d7\u5316\u3059\u308b\u306b\u306f '\/' \u3092\u4f7f\u3044\u307e\u3059\u3002 \u4f8b: libraries\/mycoollib.git
+# 所有者はリポジトリの設定を変更できる
gb.ownerDescription = \u6240\u6709\u8005\u306f\u30ea\u30dd\u30b8\u30c8\u30ea\u306e\u8a2d\u5b9a\u3092\u5909\u66f4\u3067\u304d\u308b
+# #使われてはいる様子だが、どうやったら出てくるのかサッパリ分からん。
gb.blob = blob
-gb.commitActivityTrend = commit activity trend
-gb.commitActivityDOW = commit activity by day of week
-gb.commitActivityAuthors = primary authors by commit activity
+# commit activity trend
+# コミット活動の傾向
+gb.commitActivityTrend = \u30b3\u30df\u30c3\u30c8\u6d3b\u52d5\u306e\u50be\u5411
+# commit activity by day of week
+# 曜日別コミット活動
+gb.commitActivityDOW = \u66dc\u65e5\u5225 \u30b3\u30df\u30c3\u30c8\u6d3b\u52d5
+# primary authors by commit activity
+# コミット活動の主な作者
+gb.commitActivityAuthors = \u30b3\u30df\u30c3\u30c8\u6d3b\u52d5\u306e\u4e3b\u306a\u4f5c\u8005
+# フィード
gb.feed = \u30d5\u30a3\u30fc\u30c9
+# キャンセル
gb.cancel = \u30ad\u30e3\u30f3\u30bb\u30eb
+# パスワード変更
gb.changePassword = \u30d1\u30b9\u30ef\u30fc\u30c9\u5909\u66f4
-gb.isFederated = is federated
-gb.federateThis = federate this repository
+# 連結
+# #と連結中
+gb.isFederated = \u3068\u9023\u7d50\u4e2d
+# このリポジトリを連結する
+gb.federateThis = \u3053\u306e\u30ea\u30dd\u30b8\u30c8\u30ea\u3092\u9023\u7d50\u3059\u308b
gb.federateOrigin = federate the origin
-gb.excludeFromFederation = \u30d5\u30a7\u30c7\u30ec\u30fc\u30b7\u30e7\u30f3\u304b\u3089\u9664\u5916\u3059\u308b
-gb.excludeFromFederationDescription = block federated Gitblit instances from pulling this account
-gb.tokens = federation tokens
-gb.tokenAllDescription = all repositories, users, & settings
-gb.tokenUnrDescription = all repositories & users
-gb.tokenJurDescription = all repositories
-gb.federatedRepositoryDefinitions = repository definitions
-gb.federatedUserDefinitions = user definitions
-gb.federatedSettingDefinitions = setting definitions
-gb.proposals = federation proposals
-gb.received = received
-gb.type = type
-gb.token = token
+# 連結除外
+gb.excludeFromFederation = \u9023\u7d50\u9664\u5916
+# このアカウントで Gitblit と連結したツールからのプルを許可しません
+gb.excludeFromFederationDescription = \u3053\u306e\u30a2\u30ab\u30a6\u30f3\u30c8\u3067 Gitblit \u3068\u9023\u7d50\u3057\u305f\u30c4\u30fc\u30eb\u304b\u3089\u306e\u30d7\u30eb\u3092\u8a31\u53ef\u3057\u307e\u305b\u30933093
+# 連結キー
+gb.tokens = \u9023\u7d50\u30ad\u30fc
+# 全てのリポジトリ、ユーザー、設定
+gb.tokenAllDescription = \u5168\u3066\u306e\u30ea\u30dd\u30b8\u30c8\u30ea\u3001\u30e6\u30fc\u30b6\u30fc\u3001\u8a2d\u5b9a
+# 全てのリポジトリとユーザー
+gb.tokenUnrDescription = \u5168\u3066\u306e\u30ea\u30dd\u30b8\u30c8\u30ea\u3068\u30e6\u30fc\u30b6\u30fc
+# 全てのリポジトリ
+gb.tokenJurDescription = \u5168\u3066\u306e\u30ea\u30dd\u30b8\u30c8\u30ea
+# リポジトリの定義
+gb.federatedRepositoryDefinitions = \u5168\u3066\u306e\u30ea\u30dd\u30b8\u30c8\u30ea
+# ユーザーの定義
+gb.federatedUserDefinitions = \u30e6\u30fc\u30b6\u30fc\u306e\u5b9a\u7fa9
+# 設定値
+gb.federatedSettingDefinitions = \u8a2d\u5b9a\u5024
+# 連結申し込み
+# #連結申し込みを受け付ける設定をしている GitBlit にてこれを受け付けると gb.federation(連結) 画面内の gb.token(連結キー) 一覧の下に、gb.proposals(連結申し込み) 一覧が現れる。
+gb.proposals = \u9023\u7d50\u7533\u3057\u8fbc\u307f
+# 前述の gb.proposals(連結申し込み) 一覧の行をクリックすると、gb.proposals(連結申し込み) 画面に遷移する。
+# 受信
+gb.received = \u53d7\u4fe1
+# タイプ
+# #ユーザーリストだったりチームリストだったり、複数箇所に現れるのでこうしておくしか...
+gb.type = \u30bf\u30a4\u30d7
+# 連結キー
+gb.token = \u9023\u7d50\u30ad\u30fc
+# リポジトリ
gb.repositories = \u30ea\u30dd\u30b8\u30c8\u30ea
-gb.proposal = proposal
-gb.frequency = frequency
-gb.folder = \u30d5\u30a9\u30eb\u30c0\u30fc
-gb.lastPull = last pull
-gb.nextPull = next pull
-gb.inclusions = inclusions
-gb.exclusions = exclusions
-gb.registration = registration
-gb.registrations = federation registrations
-gb.sendProposal = propose
-gb.status = status
-gb.origin = origin
-gb.headRef = \u30c7\u30d5\u30a9\u30eb\u30c8\u30d6\u30e9\u30f3\u30c1 (HEAD)
+# 申し込み
+gb.proposal = \u7533\u3057\u8fbc\u307f
+# #gb.federationRegistration(登録された連結)画面
+# プル周期
+gb.frequency = \u30d7\u30eb\u5468\u671f
+# 参照先
+gb.folder = \u53c2\u7167\u5148
+# 最終プル
+gb.lastPull = \u6700\u7d42\u30d7\u30eb
+# 次回プル予定
+gb.nextPull = \u6b21\u56de\u30d7\u30eb\u4e88\u5b9a
+# プルするリポジトリ
+gb.inclusions = \u30d7\u30eb\u3059\u308b\u30ea\u30dd\u30b8\u30c8\u30ea
+# プルしないリポジトリ
+gb.exclusions = \u30d7\u30eb\u3057\u306a\u3044\u30ea\u30dd\u30b8\u30c8\u30ea
+# 登録
+gb.registration = \u767b\u9332
+# 登録された連結
+# #gb.proposals(連結申し込み) 画面にて表示された連結設定の雛形を gitblit.properties (もしくは default.properties)
+# #に貼り付けて GitBlit を更に再起動すると、gb.federation(連結) 画面の gb.token(連結キー) 一覧の下に、更に
+# ##gb.registrations(登録された連結)一覧が現れる。
+gb.registrations = \u767b\u9332\u3055\u308c\u305f\u9023\u7d50
+# 連結申し込み
+gb.sendProposal = \u7533\u3057\u8fbc\u307f
+# 状態
+gb.status = \u72b6\u614b
+# フォーク元 URL
+gb.origin = \u30d5\u30a9\u30fc\u30af\u5143 URL
+# デフォルトブランチ (HEAD)
+gb.headRef = \u30c7\u30d5\u30a9\u30eb\u30c8\u30d6\u30e9\u30f3\u30c1 (HEAD)
+# HEAD のリンク先 ref を変更する e.g. refs/heads/master
gb.headRefDescription = HEAD \u306e\u30ea\u30f3\u30af\u5148 ref \u3092\u5909\u66f4\u3059\u308b e.g. refs/heads/master
-gb.federationStrategy = \u30d5\u30a7\u30c7\u30ec\u30fc\u30b7\u30e7\u30f3\u6226\u7565
-gb.federationRegistration = federation registration
-gb.federationResults = federation pull results
-gb.federationSets = federation sets
+# 連結設定
+gb.federationStrategy = \u9023\u7d50\u8a2d\u5b9a
+# #gb.registrations(登録された連結)一覧が現れ、この行をクリックすると、gb.registrations(登録された連結)画面に遷移する。
+# #ここにある文言群
+# 登録された連結
+gb.federationRegistration = \u767b\u9332\u3055\u308c\u305f\u9023\u7d50
+# 連結元からのプルの結果
+gb.federationResults = \u9023\u7d50\u5143\u304b\u3089\u306e\u30d7\u30eb\u306e\u7d50\u679c
+# 連結セット
+gb.federationSets = \u9023\u7d50\u30bb\u30c3\u30c8
+# メッセージ
gb.message = \u30e1\u30c3\u30bb\u30fc\u30b8
-gb.myUrlDescription = the publicly accessible url for your Gitblit instance
-gb.destinationUrl = send to
-gb.destinationUrlDescription = the url of the Gitblit instance to send your proposal
+# #gb.federation から gb.tokens (連結一覧) に入り、何れかの行の gb.sendProposal をクリックすると遷移する gb.sendProposal 画面にある。
+# この Gitblit 環境の URL
+# gb.myUrlDescription = the publicly accessible url for your Gitblit instance
+gb.myUrlDescription = \u3053\u306e Gitblit \u74b0\u5883\u306e URL
+# 送信先
+# gb.destinationUrl = send to
+gb.destinationUrl = \u9001\u4fe1\u5148
+# 申し込み先 URL
+gb.destinationUrlDescription = \u7533\u3057\u8fbc\u307f\u5148 URL
+# ユーザー
gb.users = \u30e6\u30fc\u30b6\u30fc
-gb.federation = \u30d5\u30a7\u30c7\u30ec\u30fc\u30b7\u30e7\u30f3
+# 連結
+gb.federation = \u9023\u7d50
+# エラー
gb.error = \u30a8\u30e9\u30fc
+# 更新
gb.refresh = \u66f4\u65b0
+# 閲覧
gb.browse = \u95b2\u89a7
-gb.clone = clone
+# クローン
+gb.clone = \u30af\u30ed\u30fc\u30f3
+# フィルター
gb.filter = \u30d5\u30a3\u30eb\u30bf\u30fc
+# 作成
gb.create = \u4f5c\u6210
+# サーバー
gb.servers = \u30b5\u30fc\u30d0\u30fc
+# 最近のアクセス
gb.recent = \u6700\u8fd1\u306e\u30a2\u30af\u30bb\u30b9
-gb.available = available
-gb.selected = selected
+# 利用可能
+gb.available = \u5229\u7528\u53ef\u80fd
+# 選択中
+gb.selected = \u9078\u629e\u4e2d
+# サイズ
gb.size = \u30b5\u30a4\u30ba
+# ダウンロード中
gb.downloading = \u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u4e2d
+# ロード中
gb.loading = \u30ed\u30fc\u30c9\u4e2d
+# 起動中
gb.starting = \u8d77\u52d5\u4e2d
+# 一般
gb.general = \u4e00\u822c
+# 設定
gb.settings = \u8a2d\u5b9a
+# 管理
gb.manage = \u7ba1\u7406
+# 最終ログイン
gb.lastLogin = \u6700\u7d42\u30ed\u30b0\u30a4\u30f3
+# サイズ計算をスキップ
gb.skipSizeCalculation = \u30b5\u30a4\u30ba\u8a08\u7b97\u3092\u30b9\u30ad\u30c3\u30d7
+# リポジトリのサイズを計算しない (ページのロード時間を削減)
gb.skipSizeCalculationDescription = \u30ea\u30dd\u30b8\u30c8\u30ea\u306e\u30b5\u30a4\u30ba\u3092\u8a08\u7b97\u3057\u306a\u3044 (\u30da\u30fc\u30b8\u306e\u30ed\u30fc\u30c9\u6642\u9593\u3092\u524a\u6e1b)
+# 概要での指標をスキップ
gb.skipSummaryMetrics = \u6982\u8981\u3067\u306e\u6307\u6a19\u3092\u30b9\u30ad\u30c3\u30d7
+# 概要ページで指標を計算しない (ページのロード時間を削減)
gb.skipSummaryMetricsDescription = \u6982\u8981\u30da\u30fc\u30b8\u3067\u6307\u6a19\u3092\u8a08\u7b97\u3057\u306a\u3044 (\u30da\u30fc\u30b8\u306e\u30ed\u30fc\u30c9\u6642\u9593\u3092\u524a\u6e1b)
-gb.accessLevel = \u30a2\u30af\u30bb\u30b9\u30ec\u30d9\u30eb
+# アクセスレベル
+gb.accessLevel = \u30a2\u30af\u30bb\u30b9\u30ec\u30d9\u30eb
+# デフォルト
gb.default = \u30c7\u30d5\u30a9\u30eb\u30c8
+# デフォルトに設定
gb.setDefault = \u30c7\u30d5\u30a9\u30eb\u30c8\u306b\u8a2d\u5b9a
gb.since = since
-gb.status = status
-gb.bootDate = boot date
+# 起動日時
+# #GitBlit Manager の「状態」タブで使用
+gb.bootDate = \u8d77\u52d5\u65e5\u6642
+# サーブレットコンテナ
gb.servletContainer = \u30b5\u30fc\u30d6\u30ec\u30c3\u30c8\u30b3\u30f3\u30c6\u30ca
+# 最大ヒープ
gb.heapMaximum = \u6700\u5927\u30d2\u30fc\u30d7
+# 確保済みヒープ
gb.heapAllocated = \u78ba\u4fdd\u6e08\u307f\u30d2\u30fc\u30d7
+# 使用ヒープ
gb.heapUsed = \u4f7f\u7528\u30d2\u30fc\u30d7
-gb.free = free
+# 利用可能
+gb.free = \u5229\u7528\u53ef\u80fd
+# バージョン
gb.version = \u30d0\u30fc\u30b8\u30e7\u30f3
+# リリース日
gb.releaseDate = \u30ea\u30ea\u30fc\u30b9\u65e5
+# 日時
gb.date = \u65e5\u6642
+# 活動
gb.activity = \u6d3b\u52d5
-gb.subscribe = \u8cfc\u8aad
+# 登録
+gb.subscribe = \u767b\u9332
+# ブランチ
gb.branch = \u30d6\u30e9\u30f3\u30c1
+# 最大ヒット数
gb.maxHits = \u6700\u5927\u30d2\u30c3\u30c8\u6570
+# 最近の活動
gb.recentActivity = \u6700\u8fd1\u306e\u6d3b\u52d5
+# ここ{0}日間 / {2}人の作者から {1}コミット
gb.recentActivityStats = \u3053\u3053{0}\u65e5\u9593 / {2}\u4eba\u306e\u4f5c\u8005\u304b\u3089 {1}\u30b3\u30df\u30c3\u30c8
+# ここ{0}日間 / なし
gb.recentActivityNone = \u3053\u3053{0}\u65e5\u9593 / \u306a\u3057
+# 毎日の活動
gb.dailyActivity = \u6bce\u65e5\u306e\u6d3b\u52d5
+# 活発なリポジトリ
gb.activeRepositories = \u6d3b\u767a\u306a\u30ea\u30dd\u30b8\u30c8\u30ea
+# 活発な作者
gb.activeAuthors = \u6d3b\u767a\u306a\u4f5c\u8005
+# コミット
gb.commits = \u30b3\u30df\u30c3\u30c8
+# チーム
gb.teams = \u30c1\u30fc\u30e0
+# チーム名
gb.teamName = \u30c1\u30fc\u30e0\u540d
+# チームメンバー
gb.teamMembers = \u30c1\u30fc\u30e0\u30e1\u30f3\u30d0\u30fc
+# チーム
gb.teamMemberships = \u30c1\u30fc\u30e0
+# チーム作成
gb.newTeam = \u30c1\u30fc\u30e0\u4f5c\u6210
+# 許可されたチーム
gb.permittedTeams = \u8a31\u53ef\u3055\u308c\u305f\u30c1\u30fc\u30e0
+# 空のリポジトリ
gb.emptyRepository = \u7a7a\u306e\u30ea\u30dd\u30b8\u30c8\u30ea
+# リポジトリのURL
gb.repositoryUrl = \u30ea\u30dd\u30b8\u30c8\u30ea\u306eURL
+# メーリングリスト
gb.mailingLists = \u30e1\u30fc\u30ea\u30f3\u30b0\u30ea\u30b9\u30c8
-gb.preReceiveScripts = pre-receive \u30b9\u30af\u30ea\u30d7\u30c8
-gb.postReceiveScripts = post-receive \u30b9\u30af\u30ea\u30d7\u30c8
+# 受信前実行スクリプト
+gb.preReceiveScripts = \u53d7\u4fe1\u524d\u5b9f\u884c\u30b9\u30af\u30ea\u30d7\u30c8
+# 受信後実行スクリプト
+gb.postReceiveScripts = \u53d7\u4fe1\u5f8c\u5b9f\u884c\u30b9\u30af\u30ea\u30d7\u30c8
+# フックスクリプト
gb.hookScripts = \u30d5\u30c3\u30af\u30b9\u30af\u30ea\u30d7\u30c8
-gb.customFields = custom fields
+# カスタムフィールド
+gb.customFields = \u30ab\u30b9\u30bf\u30e0\u30d5\u30a3\u30fc\u30eb\u30c9
gb.customFieldsDescription = custom fields available to Groovy hooks
+# アクセス権限
gb.accessPermissions = \u30a2\u30af\u30bb\u30b9\u6a29\u9650
+# フィルター
gb.filters = \u30d5\u30a3\u30eb\u30bf\u30fc
+# 一般的な設定
gb.generalDescription = \u4e00\u822c\u7684\u306a\u8a2d\u5b9a
+# ユーザーとチームでアクセスを制限する
gb.accessPermissionsDescription = \u30e6\u30fc\u30b6\u30fc\u3068\u30c1\u30fc\u30e0\u3067\u30a2\u30af\u30bb\u30b9\u3092\u5236\u9650\u3059\u308b
+# チームを設定する、特定の制限リポジトリへのアクセスを許可する
gb.accessPermissionsForUserDescription = \u30c1\u30fc\u30e0\u3092\u8a2d\u5b9a\u3059\u308b\u3001\u7279\u5b9a\u306e\u5236\u9650\u30ea\u30dd\u30b8\u30c8\u30ea\u3078\u306e\u30a2\u30af\u30bb\u30b9\u3092\u8a31\u53ef\u3059\u308b
+# チームメンバーを設定する、特定の制限リポジトリへのアクセスを許可する
gb.accessPermissionsForTeamDescription = \u30c1\u30fc\u30e0\u30e1\u30f3\u30d0\u30fc\u3092\u8a2d\u5b9a\u3059\u308b\u3001\u7279\u5b9a\u306e\u5236\u9650\u30ea\u30dd\u30b8\u30c8\u30ea\u3078\u306e\u30a2\u30af\u30bb\u30b9\u3092\u8a31\u53ef\u3059\u308b
-gb.federationRepositoryDescription = \u3053\u306e\u30ea\u30dd\u30b8\u30c8\u30ea\u3092\u4ed6\u306e Gitblit \u30b5\u30fc\u30d0\u30fc\u3068\u5171\u6709\u3059\u308b
+# このリポジトリを他の Gitblit サーバーと共有します
+gb.federationRepositoryDescription = \u3053\u306e\u30ea\u30dd\u30b8\u30c8\u30ea\u3092\u4ed6\u306e Gitblit \u30b5\u30fc\u30d0\u30fc\u3068\u5171\u6709\u3057\u307e\u3059
+# この Gitblit サーバーに push された時に Groovy スクリプトを実行する
gb.hookScriptsDescription = \u3053\u306e Gitblit \u30b5\u30fc\u30d0\u30fc\u306b push \u3055\u308c\u305f\u6642\u306b Groovy \u30b9\u30af\u30ea\u30d7\u30c8\u3092\u5b9f\u884c\u3059\u308b
+# リセット
gb.reset = \u30ea\u30bb\u30c3\u30c8
+# ページ
gb.pages = \u30da\u30fc\u30b8
+# 作業コピー
gb.workingCopy = \u4f5c\u696d\u30b3\u30d4\u30fc
+# このリポジトリには作業コピーがあるため push できません
gb.workingCopyWarning = \u3053\u306e\u30ea\u30dd\u30b8\u30c8\u30ea\u306b\u306f\u4f5c\u696d\u30b3\u30d4\u30fc\u304c\u3042\u308b\u305f\u3081 push \u3067\u304d\u307e\u305b\u3093
+# クエリー
gb.query = \u30af\u30a8\u30ea\u30fc
-gb.queryHelp = \u6a19\u6e96\u7684\u306a\u30af\u30a8\u30ea\u30fc\u66f8\u5f0f\u3092\u30b5\u30dd\u30fc\u30c8\u3057\u3066\u3044\u307e\u3059\u3002<p/><p/>\u8a73\u7d30\u306f <a target="_new" href="http://lucene.apache.org/core/old_versioned_docs/versions/3_5_0/queryparsersyntax.html">Lucene Query Parser Syntax</a> \u3092\u53c2\u7167\u3057\u3066\u4e0b\u3055\u3044\u3002
-gb.queryResults = \u691c\u7d22\u7d50\u679c {0} - {1} ({2} \u30d2\u30c3\u30c8)
+# 標準的なクエリー書式をサポートしています。<p/><p/>詳細は ${querySyntax} を参照して下さい。
+gb.queryHelp = \u6a19\u6e96\u7684\u306a\u30af\u30a8\u30ea\u30fc\u66f8\u5f0f\u3092\u30b5\u30dd\u30fc\u30c8\u3057\u3066\u3044\u307e\u3059\u3002<p\/><p\/>\u8a73\u7d30\u306f ${querySyntax} \u3092\u53c2\u7167\u3057\u3066\u4e0b\u3055\u3044\u3002
+# Lucene 構文解析の文法リファレンス
+gb.querySyntax = Lucene \u69cb\u6587\u89e3\u6790\u306e\u6587\u6cd5\u30ea\u30d5\u30a1\u30ec\u30f3\u30b9
+# 検索結果 {0} - {1} ({2} 該当)
+gb.queryResults = \u691c\u7d22\u7d50\u679c {0} - {1} ({2} \u8a72\u5f53)
+# 見つかりませんでした。
gb.noHits = \u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f\u3002
-gb.authored = authored
-gb.committed = committed
+# 著
+gb.authored = \u8457
+# コミット
+gb.committed = \u30b3\u30df\u30c3\u30c8
+# インデックスするブランチ
gb.indexedBranches = \u30a4\u30f3\u30c7\u30c3\u30af\u30b9\u3059\u308b\u30d6\u30e9\u30f3\u30c1
-gb.indexedBranchesDescription = Lucene \u3067\u30a4\u30f3\u30c7\u30c3\u30af\u30b9\u3059\u308b\u30d6\u30e9\u30f3\u30c1\u3092\u9078\u629e
+# Lucene でインデックスするブランチを選択してください
+gb.indexedBranchesDescription = Lucene \u3067\u30a4\u30f3\u30c7\u30c3\u30af\u30b9\u3059\u308b\u30d6\u30e9\u30f3\u30c1\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044
+# Lucene でインデックスするように設定されているリポジトリがありません
gb.noIndexedRepositoriesWarning = Lucene \u3067\u30a4\u30f3\u30c7\u30c3\u30af\u30b9\u3059\u308b\u3088\u3046\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u308b\u30ea\u30dd\u30b8\u30c8\u30ea\u304c\u3042\u308a\u307e\u305b\u3093
+# クエリーが未定義です!
gb.undefinedQueryWarning = \u30af\u30a8\u30ea\u30fc\u304c\u672a\u5b9a\u7fa9\u3067\u3059!
+# クエリーが未定義です!
gb.noSelectedRepositoriesWarning = \u3072\u3068\u3064\u4ee5\u4e0a\u306e\u30ea\u30dd\u30b8\u30c8\u30ea\u3092\u9078\u629e\u3057\u3066\u4e0b\u3055\u3044!
+# Lucene インデックスは無効化されています
gb.luceneDisabled = Lucene \u30a4\u30f3\u30c7\u30c3\u30af\u30b9\u306f\u7121\u52b9\u5316\u3055\u308c\u3066\u3044\u307e\u3059
+# 読み込み失敗
gb.failedtoRead = \u8aad\u307f\u8fbc\u307f\u5931\u6557
-gb.isNotValidFile = is not a valid file
+# は無効なファイルです
+gb.isNotValidFile = \u306f\u7121\u52b9\u306a\u30d5\u30a1\u30a4\u30eb\u3067\u3059
+# {0}からのデフォルトメッセージの読み込みに失敗しました!
gb.failedToReadMessage = {0}\u304b\u3089\u306e\u30c7\u30d5\u30a9\u30eb\u30c8\u30e1\u30c3\u30bb\u30fc\u30b8\u306e\u8aad\u307f\u8fbc\u307f\u306b\u5931\u6557\u3057\u307e\u3057\u305f!
+# パスワードが一致しません!
gb.passwordsDoNotMatch = \u30d1\u30b9\u30ef\u30fc\u30c9\u304c\u4e00\u81f4\u3057\u307e\u305b\u3093!
+# パスワードが短すぎます。最低で{0}文字必要です。
gb.passwordTooShort = \u30d1\u30b9\u30ef\u30fc\u30c9\u304c\u77ed\u3059\u304e\u307e\u3059\u3002\u6700\u4f4e\u3067{0}\u6587\u5b57\u5fc5\u8981\u3067\u3059\u3002
+# パスワードを変更しました。
gb.passwordChanged = \u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5909\u66f4\u3057\u307e\u3057\u305f\u3002
+# パスワードの変更を中止しました。
gb.passwordChangeAborted = \u30d1\u30b9\u30ef\u30fc\u30c9\u306e\u5909\u66f4\u3092\u4e2d\u6b62\u3057\u307e\u3057\u305f\u3002
+# リポジトリ名を設定して下さい。
gb.pleaseSetRepositoryName = \u30ea\u30dd\u30b8\u30c8\u30ea\u540d\u3092\u8a2d\u5b9a\u3057\u3066\u4e0b\u3055\u3044\u3002
+# 先頭のルートフォルダ参照(/)は禁止です。
gb.illegalLeadingSlash = \u5148\u982d\u306e\u30eb\u30fc\u30c8\u30d5\u30a9\u30eb\u30c0\u53c2\u7167(/)\u306f\u7981\u6b62\u3067\u3059\u3002
+# 相対フォルダ参照(../)は禁止です。
gb.illegalRelativeSlash = \u76f8\u5bfe\u30d5\u30a9\u30eb\u30c0\u53c2\u7167(../)\u306f\u7981\u6b62\u3067\u3059\u3002
+# リポジトリ名に''{0}''不正な文字が含まれています!
gb.illegalCharacterRepositoryName = \u30ea\u30dd\u30b8\u30c8\u30ea\u540d\u306b''{0}''\u4e0d\u6b63\u306a\u6587\u5b57\u304c\u542b\u307e\u308c\u3066\u3044\u307e\u3059!
+# アクセス制限を選択して下さい!
gb.selectAccessRestriction = \u30a2\u30af\u30bb\u30b9\u5236\u9650\u3092\u9078\u629e\u3057\u3066\u4e0b\u3055\u3044!
+# フェデレーション戦略を選択して下さい!
gb.selectFederationStrategy = \u30d5\u30a7\u30c7\u30ec\u30fc\u30b7\u30e7\u30f3\u6226\u7565\u3092\u9078\u629e\u3057\u3066\u4e0b\u3055\u3044!
+# チーム名を入力して下さい!
gb.pleaseSetTeamName = \u30c1\u30fc\u30e0\u540d\u3092\u5165\u529b\u3057\u3066\u4e0b\u3055\u3044!
+# チーム名''{0}''は利用できません。.
gb.teamNameUnavailable = \u30c1\u30fc\u30e0\u540d''{0}''\u306f\u5229\u7528\u3067\u304d\u307e\u305b\u3093\u3002.
+# 最低でもリポジトリをチームに一つ指定して下さい。
gb.teamMustSpecifyRepository = \u6700\u4f4e\u3067\u3082\u30ea\u30dd\u30b8\u30c8\u30ea\u3092\u30c1\u30fc\u30e0\u306b\u4e00\u3064\u6307\u5b9a\u3057\u3066\u4e0b\u3055\u3044\u3002
+# 新しいチーム''{0}''を作成しました。
gb.teamCreated = \u65b0\u3057\u3044\u30c1\u30fc\u30e0''{0}''\u3092\u4f5c\u6210\u3057\u307e\u3057\u305f\u3002
+# ユーザー名を入力して下さい!
gb.pleaseSetUsername = \u30e6\u30fc\u30b6\u30fc\u540d\u3092\u5165\u529b\u3057\u3066\u4e0b\u3055\u3044!
+# ユーザー名''{0}''は利用できません。
gb.usernameUnavailable = \u30e6\u30fc\u30b6\u30fc\u540d''{0}''\u306f\u5229\u7528\u3067\u304d\u307e\u305b\u3093\u3002
-gb.combinedMd5Rename = Gitblit\u306fcombined-md5\u30d1\u30b9\u30ef\u30fc\u30c9\u30cf\u30c3\u30b7\u30e5\u304c\u6709\u52b9\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u30a2\u30ab\u30a6\u30f3\u30c8\u540d\u306e\u5909\u66f4\u3067\u306f\u65b0\u3057\u3044\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u4e0b\u3055\u3044\u3002
+# Gitblit は combined-md5 パスワードハッシュが有効に設定されています。アカウント名の変更では新しいパスワードを入力して下さい。
+gb.combinedMd5Rename = Gitblit \u306f combined-md5 \u30d1\u30b9\u30ef\u30fc\u30c9\u30cf\u30c3\u30b7\u30e5\u304c\u6709\u52b9\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u30a2\u30ab\u30a6\u30f3\u30c8\u540d\u306e\u5909\u66f4\u3067\u306f\u65b0\u3057\u3044\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u4e0b\u3055\u3044\u3002
+# 新しいユーザー''{0}''を作成しました。
gb.userCreated = \u65b0\u3057\u3044\u30e6\u30fc\u30b6\u30fc''{0}''\u3092\u4f5c\u6210\u3057\u307e\u3057\u305f\u3002
+# フェデレーション登録が見つかりませんでした!
gb.couldNotFindFederationRegistration = \u30d5\u30a7\u30c7\u30ec\u30fc\u30b7\u30e7\u30f3\u767b\u9332\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f!
-gb.failedToFindGravatarProfile = {0} \u306eGravatar\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f\u3002
+# {0} の Gravatar プロファイルが見つかりませんでした。
+gb.failedToFindGravatarProfile = {0} \u306e Gravatar \u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f\u3002
+# {2}で{0}コミット、{1}タグ
gb.branchStats = {2}\u3067{0}\u30b3\u30df\u30c3\u30c8\u3001{1}\u30bf\u30b0
+# リポジトリが指定されていません。
gb.repositoryNotSpecified = \u30ea\u30dd\u30b8\u30c8\u30ea\u304c\u6307\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002
+# {0}にリポジトリが指定されていません!
gb.repositoryNotSpecifiedFor = {0}\u306b\u30ea\u30dd\u30b8\u30c8\u30ea\u304c\u6307\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093!
+# リポジトリをロードできません
gb.canNotLoadRepository = \u30ea\u30dd\u30b8\u30c8\u30ea\u3092\u30ed\u30fc\u30c9\u3067\u304d\u307e\u305b\u3093
+# コミットが空です
gb.commitIsNull = \u30b3\u30df\u30c3\u30c8\u304c\u7a7a\u3067\u3059
+# リポジトリへのアクセス権がありません
gb.unauthorizedAccessForRepository = \u30ea\u30dd\u30b8\u30c8\u30ea\u3078\u306e\u30a2\u30af\u30bb\u30b9\u6a29\u304c\u3042\u308a\u307e\u305b\u3093
gb.failedToFindCommit = Failed to find commit \"{0}\" in {1} for {2} page!
gb.couldNotFindFederationProposal = Could not find federation proposal!
+# ユーザー名またはパスワードが無効です!
gb.invalidUsernameOrPassword = \u30e6\u30fc\u30b6\u30fc\u540d\u307e\u305f\u306f\u30d1\u30b9\u30ef\u30fc\u30c9\u304c\u7121\u52b9\u3067\u3059!
-gb.OneProposalToReview = There is 1 federation proposal awaiting review.
-gb.nFederationProposalsToReview = There are {0} federation proposals awaiting review.
-gb.couldNotFindTag = {0} \u30bf\u30b0\u3092\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f
-gb.couldNotCreateFederationProposal = Could not create federation proposal!
-gb.pleaseSetGitblitUrl = Please enter your Gitblit url!
+# #連結の申し込みがあった際に、メニューの下辺りに表示されるポップアップの文言群
+# 連結の申し込みがありました。確認してください。
+gb.OneProposalToReview = \u9023\u7d50\u7533\u3057\u8fbc\u307f\u304c\u3042\u308a\u307e\u3057\u305f\u3002\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002
+# {0}つの連結申し込みがありました。確認してください。
+gb.nFederationProposalsToReview = {0}\u3064\u306e\u9023\u7d50\u7533\u3057\u8fbc\u307f\u304c\u3042\u308a\u307e\u3057\u305f\u3002\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002
+# {0} タグが見つかりませんでした
+gb.couldNotFindTag = #{0} \u30bf\u30b0\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f
+# #連結設定が出来ませんでした。
+gb.couldNotCreateFederationProposal = \u9023\u7d50\u8a2d\u5b9a\u304c\u51fa\u6765\u307e\u305b\u3093\u3067\u3057\u305f\u3002
+# GitBlit の URL を入力してください
+gb.pleaseSetGitblitUrl = GitBlit \u306e URL \u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044
gb.pleaseSetDestinationUrl = Please enter a destination url for your proposal!
gb.proposalReceived = Proposal successfully received by {0}.
gb.noGitblitFound = Sorry, {0} could not find a Gitblit instance at {1}.
-gb.noProposals = Sorry, {0} is not accepting proposals at this time.
-gb.noFederation = Sorry, {0} is not configured to federate with any Gitblit instances.
+# {0} は現在 申し込みを受け付けていません
+gb.noProposals = {0} \u306f\u73fe\u5728 \u7533\u3057\u8fbc\u307f\u3092\u53d7\u3051\u4ed8\u3051\u3066\u3044\u307e\u305b\u3093
+# {0} は他の GitBlit と連結するように設定されていません。
+gb.noFederation = {0} \u306f\u4ed6\u306e GitBlit \u3068\u9023\u7d50\u3059\u308b\u3088\u3046\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002
gb.proposalFailed = Sorry, {0} did not receive any proposal data!
gb.proposalError = Sorry, {0} reports that an unexpected error occurred!
gb.failedToSendProposal = Failed to send proposal!
gb.userServiceDoesNotPermitAddUser = {0} does not permit adding a user account!
gb.userServiceDoesNotPermitPasswordChanges = {0} does not permit password changes!
+# 表示名
gb.displayName = \u8868\u793a\u540d
+# メールアドレス
gb.emailAddress = \u30e1\u30fc\u30eb\u30a2\u30c9\u30ec\u30b9
gb.errorAdminLoginRequired = Administration requires a login
gb.errorOnlyAdminMayCreateRepository = Only an administrator may create a repository
gb.errorOnlyAdminOrOwnerMayEditRepository = Only an administrator or the owner may edit a repository
gb.errorAdministrationDisabled = Administration is disabled
+# ここ{0}日間
gb.lastNDays = \u3053\u3053{0}\u65e5\u9593
gb.completeGravatarProfile = Complete profile on Gravatar.com
-gb.none = none
-gb.line = line
-gb.content = content
-gb.empty = empty
-gb.inherited = inherited
+# #ここら辺、多分未使用
+# 無し
+gb.none = \u7121\u3057
+# 行
+gb.line = \u884c
+# 内容
+gb.content = \u5185\u5bb9
+# 空
+gb.empty = \u7a7a
+# 継承
+gb.inherited = \u7d99\u627f
+# リポジトリ"{0}"を削除しますか?
gb.deleteRepository = \u30ea\u30dd\u30b8\u30c8\u30ea\"{0}\"\u3092\u524a\u9664\u3057\u307e\u3059\u304b?
+# リポジトリ''{0}''を削除しました。
gb.repositoryDeleted = \u30ea\u30dd\u30b8\u30c8\u30ea''{0}''\u3092\u524a\u9664\u3057\u307e\u3057\u305f\u3002
+# リポジトリ''{0}''の削除に失敗しました!
gb.repositoryDeleteFailed = \u30ea\u30dd\u30b8\u30c8\u30ea''{0}''\u306e\u524a\u9664\u306b\u5931\u6557\u3057\u307e\u3057\u305f!
+# ユーザー"{0}"を削除しますか?
gb.deleteUser = \u30e6\u30fc\u30b6\u30fc\"{0}\"\u3092\u524a\u9664\u3057\u307e\u3059\u304b?
+# ユーザー''{0}''を削除しました。
gb.userDeleted = \u30e6\u30fc\u30b6\u30fc''{0}''\u3092\u524a\u9664\u3057\u307e\u3057\u305f\u3002
+# ユーザー''{0}''の削除に失敗しました!
gb.userDeleteFailed = \u30e6\u30fc\u30b6\u30fc''{0}''\u306e\u524a\u9664\u306b\u5931\u6557\u3057\u307e\u3057\u305f!
+# たった今
gb.time.justNow = \u305f\u3063\u305f\u4eca
+# 今日
gb.time.today = \u4eca\u65e5
+# 昨日
gb.time.yesterday = \u6628\u65e5
+# {0}分前
gb.time.minsAgo = {0}\u5206\u524d
+# {0}時間前
gb.time.hoursAgo = {0}\u6642\u9593\u524d
+# {0}日前
gb.time.daysAgo = {0}\u65e5\u524d
+# {0}週前
gb.time.weeksAgo = {0}\u9031\u524d
+# {0}ヶ月前
gb.time.monthsAgo = {0}\u30f6\u6708\u524d
+# 1年前
gb.time.oneYearAgo = 1\u5e74\u524d
+# {0}年前
gb.time.yearsAgo = {0}\u5e74\u524d
+# 1日
gb.duration.oneDay = 1\u65e5
+# {0}日
gb.duration.days = {0}\u65e5
+# 1ヶ月
gb.duration.oneMonth = 1\u30f6\u6708
+# {0}ヶ月
gb.duration.months = {0}\u30f6\u6708
+# 1年
gb.duration.oneYear = 1\u5e74
+# {0}年
gb.duration.years = {0}\u5e74
+# 権限制御
gb.authorizationControl = \u6a29\u9650\u5236\u5fa1
+# 全ての認証済みユーザーへアクセスを許可する
gb.allowAuthenticatedDescription = \u5168\u3066\u306e\u8a8d\u8a3c\u6e08\u307f\u30e6\u30fc\u30b6\u30fc\u3078\u30a2\u30af\u30bb\u30b9\u3092\u8a31\u53ef\u3059\u308b
+# 指定した名前のユーザー/チームへアクセスを許可する
gb.allowNamedDescription = \u6307\u5b9a\u3057\u305f\u540d\u524d\u306e\u30e6\u30fc\u30b6\u30fc/\u30c1\u30fc\u30e0\u3078\u30a2\u30af\u30bb\u30b9\u3092\u8a31\u53ef\u3059\u308b
+# Markdown のパースに失敗しました!
gb.markdownFailure = Markdown \u306e\u30d1\u30fc\u30b9\u306b\u5931\u6557\u3057\u307e\u3057\u305f!
+# キャッシュをクリア
gb.clearCache = \u30ad\u30e3\u30c3\u30b7\u30e5\u3092\u30af\u30ea\u30a2
+# プロジェクト
gb.projects = \u30d7\u30ed\u30b8\u30a7\u30af\u30c8
+# プロジェクト
gb.project = \u30d7\u30ed\u30b8\u30a7\u30af\u30c8
-gb.allProjects = \u30b9\u307a\u624b\u306e\u30d7\u30ed\u30b8\u30a7\u30af\u30c8
+# 全てのプロジェクト
+gb.allProjects = \u5168\u3066\u306e\u30d7\u30ed\u30b8\u30a7\u30af\u30c8
+# クリップボードにコピー
gb.copyToClipboard = \u30af\u30ea\u30c3\u30d7\u30dc\u30fc\u30c9\u306b\u30b3\u30d4\u30fc
-gb.fork = fork
-gb.forks = forks
-gb.forkRepository = fork {0}?
+# フォーク
+gb.fork = \u30d5\u30a9\u30fc\u30af
+# フォーク一覧
+gb.forks = \u30d5\u30a9\u30fc\u30af\u4e00\u89a7
+# {0} をフォークしますか?
+gb.forkRepository = {0} \u3092\u30d5\u30a9\u30fc\u30af\u3057\u307e\u3059\u304b?
+# {0} をフォークしました
gb.repositoryForked = {0} \u3092\u30d5\u30a9\u30fc\u30af\u3057\u307e\u3057\u305f
+# フォークに失敗しました
gb.repositoryForkFailed= \u30d5\u30a9\u30fc\u30af\u306b\u5931\u6557\u3057\u307e\u3057\u305f
+# 個人リポジトリ
gb.personalRepositories = \u500b\u4eba\u30ea\u30dd\u30b8\u30c8\u30ea
+# 全てのフォーク
gb.allowForks = \u5168\u3066\u306e\u30d5\u30a9\u30fc\u30af
-gb.allowForksDescription = \u8a8d\u8a3c\u6e08\u307f\u30e6\u30fc\u30b6\u30fc\u306b\u3053\u306e\u30ea\u30dd\u30b8\u30c8\u30ea\u306e\u30d5\u30a9\u30fc\u30af\u3092\u8a31\u53ef\u3059\u308b
+# 認証済みユーザーにこのリポジトリのフォークを許可します
+gb.allowForksDescription = \u8a8d\u8a3c\u6e08\u307f\u30e6\u30fc\u30b6\u30fc\u306b\u3053\u306e\u30ea\u30dd\u30b8\u30c8\u30ea\u306e\u30d5\u30a9\u30fc\u30af\u3092\u8a31\u53ef\u3057\u307e\u3059
+# 次から分岐:
gb.forkedFrom = forked from
+# フォーク可能
gb.canFork = \u30d5\u30a9\u30fc\u30af\u53ef\u80fd
-gb.canForkDescription = \u6a29\u9650\u3092\u6301\u3064\u30ea\u30dd\u30b8\u30c8\u30ea\u304b\u3089\u500b\u4eba\u30ea\u30dd\u30b8\u30c8\u30ea\u3078\u306e\u30d5\u30a9\u30fc\u30af\u3067\u304d\u308b
+# 権限を持つリポジトリから個人リポジトリへのフォークができます
+gb.canForkDescription = \u6a29\u9650\u3092\u6301\u3064\u30ea\u30dd\u30b8\u30c8\u30ea\u304b\u3089\u500b\u4eba\u30ea\u30dd\u30b8\u30c8\u30ea\u3078\u306e\u30d5\u30a9\u30fc\u30af\u304c\u3067\u304d\u307e\u3059
+# 自分のフォークを見る
gb.myFork = \u81ea\u5206\u306e\u30d5\u30a9\u30fc\u30af\u3092\u898b\u308b
+# フォーク禁止
gb.forksProhibited = \u30d5\u30a9\u30fc\u30af\u7981\u6b62
+# このリポジトリはフォークできません
gb.forksProhibitedWarning = \u3053\u306e\u30ea\u30dd\u30b8\u30c8\u30ea\u306f\u30d5\u30a9\u30fc\u30af\u3067\u304d\u307e\u305b\u3093
+# {0}にはフォークがありません
gb.noForks = {0}\u306b\u306f\u30d5\u30a9\u30fc\u30af\u304c\u3042\u308a\u307e\u305b\u3093
-gb.forkNotAuthorized = \u3059\u307f\u307e\u305b\u3093\u3001\u3042\u306a\u305f\u306f{0}\u3092\u30d5\u30a9\u30fc\u30af\u3067\u304d\u307e\u305b\u3093
+# {0}のフォーク権限がありません
+gb.forkNotAuthorized = {0}\u306e\u30d5\u30a9\u30fc\u30af\u6a29\u9650\u304c\u3042\u308a\u307e\u305b\u3093
+# フォーク中です
gb.forkInProgress = \u30d5\u30a9\u30fc\u30af\u4e2d\u3067\u3059
+# フォークの準備中...
gb.preparingFork = \u30d5\u30a9\u30fc\u30af\u306e\u6e96\u5099\u4e2d...
-gb.isFork = is fork
+# はフォークされました
+gb.isFork = \u306f\u30d5\u30a9\u30fc\u30af\u3055\u308c\u307e\u3057\u305f
+# 作成可能
gb.canCreate = \u4f5c\u6210\u53ef\u80fd
-gb.canCreateDescription = \u500b\u4eba\u30ea\u30dd\u30b8\u30c8\u30ea\u3092\u4f5c\u6210\u3067\u304d\u308b
+# 個人リポジトリを作成できます
+gb.canCreateDescription = \u500b\u4eba\u30ea\u30dd\u30b8\u30c8\u30ea\u3092\u4f5c\u6210\u3067\u304d\u307e\u3059
+# あなたの個人リポジトリは"{0}"に存在しなければなりません
gb.illegalPersonalRepositoryLocation = \u3042\u306a\u305f\u306e\u500b\u4eba\u30ea\u30dd\u30b8\u30c8\u30ea\u306f\"{0}\"\u306b\u5b58\u5728\u3057\u306a\u3051\u308c\u3070\u306a\u308a\u307e\u305b\u3093
-gb.verifyCommitter = verify committer
-gb.verifyCommitterDescription = require committer identity to match pushing Gitblt user account
-gb.verifyCommitterNote = all merges require "--no-ff" to enforce committer identity
-gb.repositoryPermissions = repository permissions
-gb.userPermissions = \u30e6\u30fc\u30b6\u30fc\u30d1\u30fc\u30df\u30c3\u30b7\u30e7\u30f3
-gb.teamPermissions = \u30c1\u30fc\u30e0\u30d1\u30fc\u30df\u30c3\u30b7\u30e7\u30f3
+# コミット者の確認
+gb.verifyCommitter = \u30b3\u30df\u30c3\u30c8\u8005\u306e\u78ba\u8a8d
+# コミット者が、プッシュしようとしている Gitblit ユーザーのアカウントと一致することを要求します。
+gb.verifyCommitterDescription = \u30b3\u30df\u30c3\u30c8\u8005\u304c\u3001\u30d7\u30c3\u30b7\u30e5\u3057\u3088\u3046\u3068\u3057\u3066\u3044\u308b Gitblit \u30e6\u30fc\u30b6\u30fc\u306e\u30a2\u30ab\u30a6\u30f3\u30c8\u3068\u4e00\u81f4\u3059\u308b\u3053\u3068\u3092\u8981\u6c42\u3057\u307e\u3059\u3002
+# コミットを強要するには、マージの際に "--no-ff" オプションが必要です。
+gb.verifyCommitterNote = \u30b3\u30df\u30c3\u30c8\u3092\u5f37\u8981\u3059\u308b\u306b\u306f\u3001\u30de\u30fc\u30b8\u306e\u969b\u306b \"--no-ff\" \u30aa\u30d7\u30b7\u30e7\u30f3\u304c\u5fc5\u8981\u3067\u3059\u3002
+# 与える権限
+gb.repositoryPermissions = \u4e0e\u3048\u308b\u6a29\u9650
+# ユーザーごとの権限設定
+gb.userPermissions = \u30e6\u30fc\u30b6\u30fc\u3054\u3068\u306e\u6a29\u9650\u8a2d\u5b9a
+# チーム単位の権限設定
+gb.teamPermissions = \u30c1\u30fc\u30e0\u5358\u4f4d\u306e\u6a29\u9650\u8a2d\u5b9a
+# 追加
gb.add = \u8ffd\u52a0
gb.noPermission = DELETE THIS PERMISSION
gb.excludePermission = {0} (exclude)
@@ -356,45 +713,76 @@ gb.pushPermission = {0} (push)
gb.createPermission = {0} (push, ref creation)
gb.deletePermission = {0} (push, ref creation+deletion)
gb.rewindPermission = {0} (push, ref creation+deletion+rewind)
-gb.permission = \u30d1\u30fc\u30df\u30c3\u30b7\u30e7\u30f3
+# 権限
+gb.permission = \u6a29\u9650
gb.regexPermission = this permission is set from regular expression \"{0}\"
+# アクセスが拒否されました
gb.accessDenied = \u30a2\u30af\u30bb\u30b9\u304c\u62d2\u5426\u3055\u308c\u307e\u3057\u305f
+# {0} は整理中です...
gb.busyCollectingGarbage = sorry, Gitblit is busy collecting garbage in {0}
-gb.gcPeriod = GC\u5468\u671f
-gb.gcPeriodDescription = GC\u306e\u5b9f\u884c\u9593\u9694
-gb.gcThreshold = GC\u95be\u5024
+# GC 周期
+gb.gcPeriod = GC \u5468\u671f
+# GC の実行間隔
+gb.gcPeriodDescription = GC \u306e\u5b9f\u884c\u9593\u9694
+# GC 閾値
+gb.gcThreshold = GC \u95be\u5024
+# 緩いオブジェクトの合計サイズの最小値
gb.gcThresholdDescription = \u7de9\u3044\u30aa\u30d6\u30b8\u30a7\u30af\u30c8\u306e\u5408\u8a08\u30b5\u30a4\u30ba\u306e\u6700\u5c0f\u5024
+# リポジトリの所有者
gb.ownerPermission = \u30ea\u30dd\u30b8\u30c8\u30ea\u306e\u6240\u6709\u8005
+# 管理者
gb.administrator = \u7ba1\u7406\u8005
-gb.administratorPermission = Gitblit\u7ba1\u7406\u8005
+# Gitblit 管理者
+gb.administratorPermission = Gitblit \u7ba1\u7406\u8005
+# チーム
gb.team = \u30c1\u30fc\u30e0
gb.teamPermission = permission set by \"{0}\" team membership
gb.missing = missing!
gb.missingPermission = the repository for this permission is missing!
+# 変更可能
gb.mutable = \u5909\u66f4\u53ef\u80fd
+# 指定
gb.specified = \u6307\u5b9a
+# 有効
gb.effective = \u6709\u52b9
-gb.organizationalUnit = organizational unit
-gb.organization = organization
-gb.locality = locality
-gb.stateProvince = state or province
-gb.countryCode = country code
-gb.properties = properties
+# 所属部署
+gb.organizationalUnit = \u6240\u5c5e\u90e8\u7f72
+# 所属組織
+gb.organization = \u6240\u5c5e\u7d44\u7e54
+# 市区町村
+gb.locality = \u5e02\u533a\u753a\u6751
+# 都道府県
+gb.stateProvince = \u90fd\u9053\u5e9c\u770c
+# 国名コード
+gb.countryCode = \u56fd\u540d\u30b3\u30fc\u30c9
+# 設定
+gb.properties = \u8a2d\u5b9a
gb.issued = issued
-gb.expires = expires
-gb.expired = expired
+# 有効期限
+gb.expires = \u6709\u52b9\u671f\u9650
+# 期限切れ
+gb.expired = \u671f\u9650\u5207\u308c
gb.expiring = expiring
-gb.revoked = revoked
-gb.serialNumber = serial number
-gb.certificates = certificates
-gb.newCertificate = new certificate
-gb.revokeCertificate = revoke certificate
-gb.sendEmail = send email
-gb.passwordHint = password hint
+# 失効
+gb.revoked = \u5931\u52b9
+# シリアルNo.
+gb.serialNumber = \u30b7\u30ea\u30a2\u30ebNo.
+# 証明書リスト
+gb.certificates = \u8a3c\u660e\u66f8\u30ea\u30b9\u30c8
+# 証明書の作成
+gb.newCertificate = \u8a3c\u660e\u66f8\u306e\u4f5c\u6210
+# 証明書の無効化
+gb.revokeCertificate = \u8a3c\u660e\u66f8\u306e\u7121\u52b9\u5316
+# メール送信
+gb.sendEmail = \u30e1\u30fc\u30eb\u9001\u4fe1
+# パスワードのヒント
+gb.passwordHint = \u30d1\u30b9\u30ef\u30fc\u30c9\u306e\u30d2\u30f3\u30c8
gb.ok = ok
-gb.invalidExpirationDate = invalid expiration date!
+# 不正な有効期限です!
+gb.invalidExpirationDate = \u4e0d\u6b63\u306a\u6709\u52b9\u671f\u9650\u3067\u3059!
gb.passwordHintRequired = password hint required!
-gb.viewCertificate = view certificate
+# 証明書の表示
+gb.viewCertificate = \u8a3c\u660e\u66f8\u306e\u8868\u793a
gb.subject = subject
gb.issuer = issuer
gb.validFrom = valid from
@@ -404,48 +792,641 @@ gb.signatureAlgorithm = signature algorithm
gb.sha1FingerPrint = SHA-1 Fingerprint
gb.md5FingerPrint = MD5 Fingerprint
gb.reason = reason
-gb.revokeCertificateReason = Please select a reason for certificate revocation
-gb.unspecified = unspecified
-gb.keyCompromise = key compromise
-gb.caCompromise = CA compromise
-gb.affiliationChanged = affiliation changed
-gb.superseded = superseded
-gb.cessationOfOperation = cessation of operation
-gb.privilegeWithdrawn = privilege withdrawn
-gb.time.inMinutes = in {0} mins
-gb.time.inHours = in {0} hours
-gb.time.inDays = in {0} days
-gb.hostname = hostname
-gb.hostnameRequired = Please enter a hostname
-gb.newSSLCertificate = new server SSL certificate
-gb.newCertificateDefaults = new certificate defaults
-gb.duration = duration
-gb.certificateRevoked = Certificate {0,number,0} has been revoked
-gb.clientCertificateGenerated = Successfully generated new client certificate for {0}
+# 証明書の無効化理由を選択してください
+gb.revokeCertificateReason = \u8a3c\u660e\u66f8\u306e\u7121\u52b9\u5316\u7406\u7531\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044
+# 詳細不明
+gb.unspecified = \u8a73\u7d30\u4e0d\u660e
+# キー漏洩
+gb.keyCompromise = \u30ad\u30fc\u6f0f\u6d29
+# 証明書漏洩
+gb.caCompromise = \u8a3c\u660e\u66f8\u6f0f\u6d29
+# 所属変更
+gb.affiliationChanged = \u6240\u5c5e\u5909\u66f4
+# 交替
+gb.superseded = \u4ea4\u66ff
+# 操作中止
+gb.cessationOfOperation = \u64cd\u4f5c\u4e2d\u6b62
+# 特権剥奪
+gb.privilegeWithdrawn = \u7279\u6a29\u5265\u596a
+# あと {0} 分
+gb.time.inMinutes = \u3042\u3068 {0} \u5206
+# あと {0} 時間
+gb.time.inHours = \u3042\u3068 {0} \u6642\u9593
+# あと {0} 日
+gb.time.inDays = \u3042\u3068 {0} \u65e5
+# ホスト名
+gb.hostname = \u30db\u30b9\u30c8\u540d
+# ホスト名を入力してください
+gb.hostnameRequired = \u30db\u30b9\u30c8\u540d\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044
+# SSL証明書新規作成
+gb.newSSLCertificate = SSL\u8a3c\u660e\u66f8\u65b0\u898f\u4f5c\u6210
+# 証明書の既定値
+gb.newCertificateDefaults = \u8a3c\u660e\u66f8\u306e\u65e2\u5b9a\u5024
+# 期間
+# #現在未使用な様子だが、一応...
+gb.duration = \u671f\u9593
+# 証明書 {0,number,0} は失効しました
+gb.certificateRevoked = \u8a3c\u660e\u66f8 {0,number,0} \u306f\u5931\u52b9\u3057\u307e\u3057\u305f
+# {0}向けクライアント証明書を作成しました
+gb.clientCertificateGenerated = {0}\u5411\u3051\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u8a3c\u660e\u66f8\u3092\u4f5c\u6210\u3057\u307e\u3057\u305f
gb.sslCertificateGenerated = Successfully generated new server SSL certificate for {0}
-gb.newClientCertificateMessage = NOTE:\nThe 'password' is not the user's password, it is the password to protect the user's keystore. This password is not saved so you must also enter a 'hint' which will be included in the user's README instructions.
-gb.certificate = certificate
-gb.emailCertificateBundle = email client certificate bundle
-gb.pleaseGenerateClientCertificate = Please generate a client certificate for {0}
-gb.clientCertificateBundleSent = Client certificate bundle for {0} sent
-gb.enterKeystorePassword = Please enter the Gitblit keystore password
-gb.warning = warning
+# 注意:\nここでの「パスワード」とは、ユーザー向けパスワードではなく、ユーザーのキーストアを保護するためのパスワードです。このパスワードは保存されないので、ユーザー向けREADMEファイルで説明している様に、「ヒント」も入力しなければなりません。
+gb.newClientCertificateMessage = \u6ce8\u610f:\n\u3053\u3053\u3067\u306e\u300c\u30d1\u30b9\u30ef\u30fc\u30c9\u300d\u3068\u306f\u3001\u30e6\u30fc\u30b6\u30fc\u5411\u3051\u30d1\u30b9\u30ef\u30fc\u30c9\u3067\u306f\u306a\u304f\u3001\u30e6\u30fc\u30b6\u30fc\u306e\u30ad\u30fc\u30b9\u30c8\u30a2\u3092\u4fdd\u8b77\u3059\u308b\u305f\u3081\u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u3067\u3059\u3002\u3053\u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u306f\u4fdd\u5b58\u3055\u308c\u306a\u3044\u306e\u3067\u3001\u30e6\u30fc\u30b6\u30fc\u5411\u3051README\u30d5\u30a1\u30a4\u30eb\u3067\u8aac\u660e\u3057\u3066\u3044\u308b\u69d8\u306b\u3001\u300c\u30d2\u30f3\u30c8\u300d\u3082\u5165\u529b\u3057\u306a\u3051\u308c\u3070\u306a\u308a\u307e\u305b\u3093\u3002
+# 証明書
+gb.certificate = \u8a3c\u660e\u66f8
+# クライアント証明書を添付してメール送信
+gb.emailCertificateBundle = \u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u8a3c\u660e\u66f8\u3092\u6dfb\u4ed8\u3057\u3066\u30e1\u30fc\u30eb\u9001\u4fe1
+# {0} 向けのクライアント証明書を作成してください
+gb.pleaseGenerateClientCertificate = {0} \u5411\u3051\u306e\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u8a3c\u660e\u66f8\u3092\u4f5c\u6210\u3057\u3066\u304f\u3060\u3055\u3044
+# {0} 向けのクライアント証明書を送付しました
+gb.clientCertificateBundleSent = {0} \u5411\u3051\u306e\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u8a3c\u660e\u66f8\u3092\u9001\u4ed8\u3057\u307e\u3057\u305f
+# Gitblit のシステムパスワードを入力してください
+gb.enterKeystorePassword = Gitblit \u306e\u30b7\u30b9\u30c6\u30e0\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044
+# 警告
+gb.warning = \u8b66\u544a
gb.jceWarning = Your Java Runtime Environment does not have the \"JCE Unlimited Strength Jurisdiction Policy\" files.\nThis will limit the length of passwords you may use to encrypt your keystores to 7 characters.\nThese policy files are an optional download from Oracle.\n\nWould you like to continue and generate the certificate infrastructure anyway?\n\nAnswering No will direct your browser to Oracle's download page so that you may download the policy files.
-gb.maxActivityCommits = max activity commits
+# 最大活動数
+gb.maxActivityCommits = \u6700\u5927\u6d3b\u52d5\u6570
+# 活動ページに寄与するコミット数の上限
gb.maxActivityCommitsDescription = \u6d3b\u52d5\u30da\u30fc\u30b8\u306b\u5bc4\u4e0e\u3059\u308b\u30b3\u30df\u30c3\u30c8\u6570\u306e\u4e0a\u9650
+# 上限なし
gb.noMaximum = \u4e0a\u9650\u306a\u3057
+# 属性
gb.attributes = \u5c5e\u6027
-gb.serveCertificate = serve https with this certificate
-gb.sslCertificateGeneratedRestart = Successfully generated new server SSL certificate for {0}.\nYou must restart Gitblit to use the new certificate.\n\nIf you are launching with the '--alias' parameter you will have to set that to ''--alias {0}''.
-gb.validity = validity
-gb.siteName = site name
-gb.siteNameDescription = short, descriptive name of your server
-gb.excludeFromActivity = \u6d3b\u52d5\u30da\u30fc\u30b8\u304b\u3089\u9664\u5916\u3059\u308b
-gb.isSparkleshared = repository is Sparkleshared
-gb.owners = owners
-gb.sessionEnded = Session has been closed
-gb.closeBrowser = Please close the browser to properly end the session.
-gb.doesNotExistInTree = {0} does not exist in tree {1}
-gb.enableIncrementalPushTags = enable incremental push tags
-gb.useIncrementalPushTagsDescription = on push, automatically tag each branch tip with an incremental revision number
-gb.incrementalPushTagMessage = Auto-tagged [{0}] branch on push \ No newline at end of file
+# この証明書で https を提供します
+gb.serveCertificate = \u3053\u306e\u8a3c\u660e\u66f8\u3067 https \u3092\u63d0\u4f9b\u3057\u307e\u3059
+# {0}の証明書を新規に作成しました\n新しい証明書を使うために Gitblit を再起動してください\n\n--alias オプションを使用する場合、''--alias {0}'' を指定しなければなりません。
+gb.sslCertificateGeneratedRestart = {0}\u306e\u8a3c\u660e\u66f8\u3092\u65b0\u898f\u306b\u4f5c\u6210\u3057\u307e\u3057\u305f\n\u65b0\u3057\u3044\u8a3c\u660e\u66f8\u3092\u4f7f\u3046\u305f\u3081\u306b Gitblit \u3092\u518d\u8d77\u52d5\u3057\u3066\u304f\u3060\u3055\u3044\n\n--alias \u30aa\u30d7\u30b7\u30e7\u30f3\u3092\u4f7f\u7528\u3059\u308b\u5834\u5408\u3001''--alias {0}'' \u3092\u6307\u5b9a\u3057\u306a\u3051\u308c\u3070\u306a\u308a\u307e\u305b\u3093\u3002
+# 有効期間
+gb.validity = \u6709\u52b9\u671f\u9593
+# サイト名
+gb.siteName = \u30b5\u30a4\u30c8\u540d
+# このサーバーを簡潔によく表した名前を指定します
+gb.siteNameDescription = \u3053\u306e\u30b5\u30fc\u30d0\u30fc\u3092\u7c21\u6f54\u306b\u3088\u304f\u8868\u3057\u305f\u540d\u524d\u3092\u6307\u5b9a\u3057\u307e\u3059
+# 活動ページから除外します
+gb.excludeFromActivity = \u6d3b\u52d5\u30da\u30fc\u30b8\u304b\u3089\u9664\u5916\u3057\u307e\u3059
+# リポジトリは Sparkleshare に公開されます
+gb.isSparkleshared = \u30ea\u30dd\u30b8\u30c8\u30ea\u306f Sparkleshare \u306b\u516c\u958b\u3055\u308c\u307e\u3059
+# 所有者
+gb.owners = \u6240\u6709\u8005
+# セッションは閉じられました
+gb.sessionEnded = \u30bb\u30c3\u30b7\u30e7\u30f3\u306f\u9589\u3058\u3089\u308c\u307e\u3057\u305f
+# セッションを正しく終了するため、ブラウザを閉じてください
+gb.closeBrowser = \u30bb\u30c3\u30b7\u30e7\u30f3\u3092\u6b63\u3057\u304f\u7d42\u4e86\u3059\u308b\u305f\u3081\u3001\u30d6\u30e9\u30a6\u30b6\u3092\u9589\u3058\u3066\u304f\u3060\u3055\u3044
+# {0} は{1}ツリーにはありません
+gb.doesNotExistInTree = {0} \u306f{1}\u30c4\u30ea\u30fc\u306b\u306f\u3042\u308a\u307e\u305b\u3093
+# インクリメンタルプッシュタグ
+gb.enableIncrementalPushTags = \u30a4\u30f3\u30af\u30ea\u30e1\u30f3\u30bf\u30eb\u30d7\u30c3\u30b7\u30e5\u30bf\u30b0
+# プッシュの際、各ブランチにリビジョン番号を連番で採番し、自動的にタグ付けします。
+gb.useIncrementalPushTagsDescription = \u30d7\u30c3\u30b7\u30e5\u306e\u969b\u3001\u5404\u30d6\u30e9\u30f3\u30c1\u306b\u30ea\u30d3\u30b8\u30e7\u30f3\u756a\u53f7\u3092\u9023\u756a\u3067\u63a1\u756a\u3057\u3001\u81ea\u52d5\u7684\u306b\u30bf\u30b0\u4ed8\u3051\u3057\u307e\u3059\u3002
+gb.incrementalPushTagMessage = Auto-tagged [{0}] branch on push
+gb.externalPermissions = {0} access permissions are externally maintained
+gb.viewAccess = You do not have Gitblit read or write access
+gb.overview = overview
+gb.dashboard = dashboard
+gb.monthlyActivity = monthly activity
+# プロフィール
+gb.myProfile = \u30d7\u30ed\u30d5\u30a3\u30fc\u30eb
+# 比較
+gb.compare = \u6bd4\u8f03
+# 手動
+gb.manual = \u624b\u52d5
+gb.from = from
+gb.to = to
+gb.at = at
+gb.of = of
+gb.in = in
+gb.moreChanges = all changes...
+gb.pushedNCommitsTo = pushed {0} commits to
+gb.pushedOneCommitTo = pushed 1 commit to
+gb.commitsTo = {0} commits to
+gb.oneCommitTo = 1 commit to
+gb.byNAuthors = by {0} authors
+gb.byOneAuthor = by {0}
+# 変更点参照
+gb.viewComparison = \u5909\u66f4\u70b9\u53c2\u7167 \u00bb
+# {0} more commits »
+gb.nMoreCommits = {0} more commits \u00bb
+gb.oneMoreCommit = 1 more commit \u00bb
+gb.pushedNewTag = pushed new tag
+gb.createdNewTag = created new tag
+gb.deletedTag = deleted tag
+gb.pushedNewBranch = pushed new branch
+gb.createdNewBranch = created new branch
+gb.deletedBranch = deleted branch
+gb.createdNewPullRequest = created pull request
+gb.mergedPullRequest = merged pull request
+gb.rewind = REWIND
+# お気に入りに追加
+gb.star = \u304a\u6c17\u306b\u5165\u308a\u306b\u8ffd\u52a0
+# お気に入りから削除
+gb.unstar = \u304a\u6c17\u306b\u5165\u308a\u304b\u3089\u524a\u9664
+gb.stargazers = stargazers
+# お気に入りのリポジトリ
+gb.starredRepositories = \u304a\u6c17\u306b\u5165\u308a\u306e\u30ea\u30dd\u30b8\u30c8\u30ea
+gb.failedToUpdateUser = Failed to update user account!
+# 所有しているリポジトリ
+gb.myRepositories = \u6240\u6709\u3057\u3066\u3044\u308b\u30ea\u30dd\u30b8\u30c8\u30ea
+# ここ{0}日は活動がありません
+gb.noActivity = \u3053\u3053{0}\u65e5\u306f\u6d3b\u52d5\u304c\u3042\u308a\u307e\u305b\u3093
+gb.findSomeRepositories = find some repositories
+# 作者指標例外
+gb.metricAuthorExclusions = \u4f5c\u8005\u6307\u6a19\u4f8b\u5916
+# ダッシュボード
+gb.myDashboard = \u30c0\u30c3\u30b7\u30e5\u30dc\u30fc\u30c9
+gb.failedToFindAccount = failed to find user account ''{0}''
+# ログ参照
+gb.reflog = \u30ed\u30b0\u53c2\u7167
+# 最近の更新
+gb.active = \u6700\u8fd1\u306e\u66f4\u65b0
+# お気に入り
+gb.starred = \u304a\u6c17\u306b\u5165\u308a
+# 所有
+gb.owned = \u6240\u6709
+# お気に入りと所有中
+gb.starredAndOwned = \u304a\u6c17\u306b\u5165\u308a\u6240\u6709\u4e2d
+gb.reviewPatchset = review {0} patchset {1}
+gb.todaysActivityStats = today / {1} commits by {2} authors
+gb.todaysActivityNone = today / none
+gb.noActivityToday = there has been no activity today
+gb.anonymousUser= anonymous
+# コミットメッセージ表示方法
+gb.commitMessageRenderer = \u30b3\u30df\u30c3\u30c8\u30e1\u30c3\u30bb\u30fc\u30b8\u8868\u793a\u65b9\u6cd5
+gb.diffStat = {0} insertions & {1} deletions
+gb.home = home
+gb.isMirror = this repository is a mirror
+gb.mirrorOf = mirror of {0}
+gb.mirrorWarning = this repository is a mirror and can not receive pushes
+# リポジトリの解説資料として、ドキュメントを付けることが出来ます。
+gb.docsWelcome1 = \u30ea\u30dd\u30b8\u30c8\u30ea\u306e\u89e3\u8aac\u8cc7\u6599\u3068\u3057\u3066\u3001\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u3092\u4ed8\u3051\u308b\u3053\u3068\u304c\u51fa\u6765\u307e\u3059\u3002
+# README.md もしくは HOME.md ファイルをリポジトリルートにコミットしてください。<br/>
+# リポジトリ作成時に、README.md のサンプルを自動生成することも出来ます。「README ファイルを含める」にチェックを入れてリポジトリを生成してみてください。
+gb.docsWelcome2 = README.md \u3082\u3057\u304f\u306f HOME.md \u30d5\u30a1\u30a4\u30eb\u3092\u30ea\u30dd\u30b8\u30c8\u30ea\u30eb\u30fc\u30c8\u306b\u30b3\u30df\u30c3\u30c8\u3057\u3066\u304f\u3060\u3055\u3044\u3002<br/>\u30ea\u30dd\u30b8\u30c8\u30ea\u4f5c\u6210\u6642\u306b\u3001README.md \u306e\u30b5\u30f3\u30d7\u30eb\u3092\u81ea\u52d5\u751f\u6210\u3059\u308b\u3053\u3068\u3082\u51fa\u6765\u307e\u3059\u3002\u300cREADME \u30d5\u30a1\u30a4\u30eb\u3092\u542b\u3081\u308b\u300d\u306b\u30c1\u30a7\u30c3\u30af\u3092\u5165\u308c\u3066\u30ea\u30dd\u30b8\u30c8\u30ea\u3092\u751f\u6210\u3057\u3066\u307f\u3066\u304f\u3060\u3055\u3044\u3002
+gb.createReadme = create a README
+# 担当者
+gb.responsible = \u62c5\u5f53\u8005
+# チケット作成者
+gb.createdThisTicket = \u30c1\u30b1\u30c3\u30c8\u4f5c\u6210\u8005
+gb.proposedThisChange = proposed this change
+gb.uploadedPatchsetN = uploaded patchset {0}
+gb.uploadedPatchsetNRevisionN = uploaded patchset {0} revision {1}
+gb.mergedPatchset = merged patchset
+# コメント付与
+gb.commented = \u30b3\u30e1\u30f3\u30c8\u4ed8\u4e0e
+# 解説無し
+gb.noDescriptionGiven = \u89e3\u8aac\u7121\u3057
+gb.toBranch = to {0}
+# 作成者
+gb.createdBy = \u4f5c\u6210\u8005
+# 参加者 {0} 人
+gb.oneParticipant = \u53c2\u52a0\u8005 {0} \u4eba
+# 参加者 {0} 人
+gb.nParticipants = \u53c2\u52a0\u8005 {0} \u4eba
+# コメント無し
+gb.noComments = \u30b3\u30e1\u30f3\u30c8\u7121\u3057
+# {0} コメント
+gb.oneComment = {0} \u30b3\u30e1\u30f3\u30c8
+# {0} コメント
+gb.nComments = {0} \u30b3\u30e1\u30f3\u30c8
+gb.oneAttachment = {0} attachment
+gb.nAttachments = {0} attachments
+# マイルストーン
+gb.milestone = \u30de\u30a4\u30eb\u30b9\u30c8\u30fc\u30f3
+gb.compareToMergeBase = compare to merge base
+gb.compareToN = compare to {0}
+# 未完了
+gb.open = \u672a\u5b8c\u4e86
+# 完了
+gb.closed = \u5b8c\u4e86
+gb.merged = merged
+# {0}チケット {1} patchset
+gb.ticketPatchset = ticket {0}, patchset {1}
+gb.patchsetMergeable = This patchset can be automatically merged into {0}.
+gb.patchsetMergeableMore = This patchset may also be merged into {0} from the command line.
+gb.patchsetAlreadyMerged = This patchset has been merged into {0}.
+gb.patchsetNotMergeable = This patchset can not be automatically merged into {0}.
+gb.patchsetNotMergeableMore = This patchset must be rebased or manually merged into {0} to resolve conflicts.
+gb.patchsetNotApproved = This patchset revision has not been approved for merging into {0}.
+gb.patchsetNotApprovedMore = A reviewer must approve this patchset.
+gb.patchsetVetoedMore = A reviewer has vetoed this patchset.
+# 書き込み
+gb.write = \u66f8\u304d\u8fbc\u307f
+# コメント
+gb.comment = \u30b3\u30e1\u30f3\u30c8
+# プレビュー
+gb.preview = \u30d7\u30ec\u30d3\u30e5\u30fc
+# コメントを記述してください...
+gb.leaveComment = \u30b3\u30e1\u30f3\u30c8\u3092\u8a18\u8ff0\u3057\u3066\u304f\u3060\u3055\u3044...
+# 詳細の表示/非表示
+gb.showHideDetails = \u8a73\u7d30\u306e\u8868\u793a/\u975e\u8868\u793a
+# patchsets を受け付ける
+gb.acceptNewPatchsets = patchsets \u3092\u53d7\u3051\u4ed8\u3051\u308b
+# このリポジトリにプッシュされてきた patchsets を受け付けます。
+gb.acceptNewPatchsetsDescription = \u3053\u306e\u30ea\u30dd\u30b8\u30c8\u30ea\u306b\u30d7\u30c3\u30b7\u30e5\u3055\u308c\u3066\u304d\u305f patchsets \u3092\u53d7\u3051\u4ed8\u3051\u307e\u3059\u3002
+# 新規チケットを許可
+gb.acceptNewTickets = \u65b0\u898f\u30c1\u30b1\u30c3\u30c8\u3092\u8a31\u53ef
+# 不具合・機能強化・タスクなどのチケット生成を許可します。
+gb.acceptNewTicketsDescription = \u4e0d\u5177\u5408\u30fb\u6a5f\u80fd\u5f37\u5316\u30fb\u30bf\u30b9\u30af\u306a\u3069\u306e\u30c1\u30b1\u30c3\u30c8\u751f\u6210\u3092\u8a31\u53ef\u3057\u307e\u3059\u3002
+# 承認要求
+gb.requireApproval = \u627f\u8a8d\u8981\u6c42
+# patchsets は、マージボタンが有効になる前に承認されなければなりません。
+gb.requireApprovalDescription = patchsets \u306f\u3001\u30de\u30fc\u30b8\u30dc\u30bf\u30f3\u304c\u6709\u52b9\u306b\u306a\u308b\u524d\u306b\u627f\u8a8d\u3055\u308c\u306a\u3051\u308c\u3070\u306a\u308a\u307e\u305b\u3093\u3002
+# ラベル
+gb.topic = \u30e9\u30d9\u30eb
+# 変更の提案
+gb.proposalTickets = \u5909\u66f4\u306e\u63d0\u6848
+# 不具合
+gb.bugTickets = \u4e0d\u5177\u5408
+# 機能強化
+gb.enhancementTickets = \u6a5f\u80fd\u5f37\u5316
+# タスク
+gb.taskTickets = \u30bf\u30b9\u30af
+# 質問
+gb.questionTickets = \u8cea\u554f
+# 保守
+gb.maintenanceTickets = \u4fdd\u5b88
+gb.requestTickets = enhancements & tasks
+# あなたが発信したチケット
+gb.yourCreatedTickets = \u3042\u306a\u305f\u304c\u767a\u4fe1\u3057\u305f\u30c1\u30b1\u30c3\u30c8
+# 監視中のチケット
+gb.yourWatchedTickets = \u76e3\u8996\u4e2d\u306e\u30c1\u30b1\u30c3\u30c8
+# あなたが意見したチケット
+gb.mentionsMeTickets = \u3042\u306a\u305f\u304c\u610f\u898b\u3057\u305f\u30c1\u30b1\u30c3\u30c8
+# 更新者
+gb.updatedBy = \u66f4\u65b0\u8005
+# 並び替え
+gb.sort = \u4e26\u3073\u66ff\u3048
+# 新しい順
+gb.sortNewest = \u65b0\u3057\u3044\u9806
+# 古い順
+gb.sortOldest = \u53e4\u3044\u9806
+# 最近更新されたものから
+gb.sortMostRecentlyUpdated = \u6700\u8fd1\u66f4\u65b0\u3055\u308c\u305f\u3082\u306e\u304b\u3089
+# 以前に更新されたものから
+gb.sortLeastRecentlyUpdated = \u4ee5\u524d\u306b\u66f4\u65b0\u3055\u308c\u305f\u3082\u306e\u304b\u3089
+# コメントの多い順
+gb.sortMostComments = \u30b3\u30e1\u30f3\u30c8\u306e\u591a\u3044\u9806
+# コメントの少ない順
+gb.sortLeastComments = \u30b3\u30e1\u30f3\u30c8\u306e\u5c11\u306a\u3044\u9806
+# patchset 版の多い順
+gb.sortMostPatchsetRevisions = patchset \u7248\u306e\u591a\u3044\u9806
+# patchset 版の少ない順
+gb.sortLeastPatchsetRevisions = patchset \u7248\u306e\u5c11\u306a\u3044\u9806
+# いいねの多い順
+gb.sortMostVotes = \u3044\u3044\u306d\u306e\u591a\u3044\u9806
+# いいねの少ない順
+gb.sortLeastVotes = \u3044\u3044\u306d\u306e\u5c11\u306a\u3044\u9806
+# ラベル
+gb.topicsAndLabels = \u30e9\u30d9\u30eb
+# マイルストーン
+gb.milestones = \u30de\u30a4\u30eb\u30b9\u30c8\u30fc\u30f3
+# マイルストーンが選択されていません
+gb.noMilestoneSelected = \u30de\u30a4\u30eb\u30b9\u30c8\u30fc\u30f3\u304c\u9078\u629e\u3055\u308c\u3066\u3044\u307e\u305b\u3093
+gb.notSpecified = not specified
+# 期限
+gb.due = \u671f\u9650
+# 問い合わせ内容
+gb.queries = \u554f\u3044\u5408\u308f\u305b\u5185\u5bb9
+gb.searchTicketsTooltip = search {0} tickets
+# チケットを検索
+gb.searchTickets = \u30c1\u30b1\u30c3\u30c8\u3092\u691c\u7d22
+# 新規
+gb.new = \u65b0\u898f
+# 新規チケット
+gb.newTicket = \u65b0\u898f\u30c1\u30b1\u30c3\u30c8
+# チケットの編集
+gb.editTicket = \u30c1\u30b1\u30c3\u30c8\u306e\u7de8\u96c6
+# チケットは、TODO リストを体系付け、不具合について検討し、patchsets と協力するのに利用できます。
+gb.ticketsWelcome = \u30c1\u30b1\u30c3\u30c8\u306f\u3001TODO \u30ea\u30b9\u30c8\u3092\u4f53\u7cfb\u4ed8\u3051\u3001\u4e0d\u5177\u5408\u306b\u3064\u3044\u3066\u691c\u8a0e\u3057\u3001patchsets \u3068\u5354\u529b\u3059\u308b\u306e\u306b\u5229\u7528\u3067\u304d\u307e\u3059\u3002
+# チケット登録
+gb.createFirstTicket = \u30c1\u30b1\u30c3\u30c8\u3092\u4f5c\u6210
+# 表題
+gb.title = \u8868\u984c
+gb.changedStatus = changed the status
+# 会議室
+gb.discussion = \u4f1a\u8b70\u5ba4
+gb.updated = updated
+# patchset の提案
+gb.proposePatchset = patchset \u306e\u63d0\u6848
+# patchset 提案へようこそ。
+gb.proposePatchsetNote = patchset \u63d0\u6848\u3078\u3088\u3046\u3053\u305d\u3002
+# 最初に、patchset を作成して git に上げます。Gitblit はその patchset を、ID でチケットと紐付けます。
+gb.proposeInstructions = \u6700\u521d\u306b\u3001patchset \u3092\u4f5c\u6210\u3057\u3066 git \u306b\u4e0a\u3052\u307e\u3059\u3002Gitblit \u306f\u305d\u306e patchset \u3092\u3001ID \u3067\u30c1\u30b1\u30c3\u30c8\u3068\u7d10\u4ed8\u3051\u307e\u3059\u3002
+gb.proposeWith = propose a patchset with {0}
+gb.revisionHistory = revision history
+gb.merge = merge
+# 行動
+gb.action = \u884c\u52d5
+gb.patchset = patchset
+# 全て
+gb.all = \u5168\u3066
+gb.mergeBase = merge base
+gb.checkout = checkout
+gb.checkoutViaCommandLine = Checkout via command line
+gb.checkoutViaCommandLineNote = You can checkout and test these changes locally from your clone of this repository.
+gb.checkoutStep1 = Fetch the current patchset \u2014 run this from your project directory
+gb.checkoutStep2 = Checkout the patchset to a new branch and review
+gb.mergingViaCommandLine = Merging via command line
+gb.mergingViaCommandLineNote = If you do not want to use the merge button or an automatic merge cannot be performed, you can perform a manual merge on the command line.
+gb.mergeStep1 = Check out a new branch to review the changes \u2014 run this from your project directory
+gb.mergeStep2 = Bring in the proposed changes and review
+gb.mergeStep3 = Merge the proposed changes and update the server
+gb.download = download
+gb.ptDescription = the Gitblit patchset tool
+gb.ptCheckout = Fetch & checkout the current patchset to a review branch
+gb.ptMerge = Fetch & merge the current patchset into your local branch
+gb.ptDescription1 = Barnum is a command-line companion for Git that simplifies the syntax for working with Gitblit Tickets and Patchsets.
+gb.ptSimplifiedCollaboration = simplified collaboration syntax
+gb.ptSimplifiedMerge = simplified merge syntax
+gb.ptDescription2 = Barnum requires Python 3 and native Git. It runs on Windows, Linux, and Mac OS X.
+gb.stepN = Step {0}
+# 観察者
+gb.watchers = \u89b3\u5bdf\u8005
+# いいね!
+gb.votes = \u3044\u3044\u306d
+# この {0} へいいね! を付ける
+gb.vote = \u3053\u306e {0} \u3078\u3044\u3044\u306d! \u3092\u4ed8\u3051\u308b
+# この {0} を観察する
+gb.watch = \u3053\u306e {0} \u3092\u89b3\u5bdf\u3059\u308b
+# いいねを取り消す
+gb.removeVote = \u3044\u3044\u306d\u3092\u53d6\u308a\u6d88\u3059
+# 観察を止める
+gb.stopWatching = \u89b3\u5bdf\u3092\u6b62\u3081\u308b
+# 観察中
+gb.watching = \u89b3\u5bdf\u4e2d
+# コメント一覧
+gb.comments = \u30b3\u30e1\u30f3\u30c8\u4e00\u89a7
+# コメント追加
+gb.addComment = \u30b3\u30e1\u30f3\u30c8\u8ffd\u52a0
+# エクスポート
+gb.export = \u30a8\u30af\u30b9\u30dd\u30fc\u30c8
+# 1 コミット
+gb.oneCommit = 1 \u30b3\u30df\u30c3\u30c8
+# {0} コミット
+gb.nCommits = {0} \u30b3\u30df\u30c3\u30c8
+# 1 コミットを追加
+gb.addedOneCommit = 1 \u30b3\u30df\u30c3\u30c8\u3092\u8ffd\u52a0
+# {0} コミットを追加
+gb.addedNCommits = {0} \u30b3\u30df\u30c3\u30c8\u3092\u8ffd\u52a0
+gb.commitsInPatchsetN = commits in patchset {0}
+gb.patchsetN = patchset {0}
+gb.reviewedPatchsetRev = reviewed patchset {0} revision {1}: {2}
+gb.review = review
+gb.reviews = reviews
+gb.veto = veto
+gb.needsImprovement = needs improvement
+gb.looksGood = looks good
+gb.approve = approve
+gb.hasNotReviewed = has not reviewed
+gb.about = about
+gb.ticketN = ticket #{0}
+# 無効なユーザー
+gb.disableUser = \u7121\u52b9\u306a\u30e6\u30fc\u30b6\u30fc
+# このユーザーを許可しません
+gb.disableUserDescription = \u3053\u306e\u30e6\u30fc\u30b6\u30fc\u3092\u8a31\u53ef\u3057\u307e\u305b\u3093
+gb.any = any
+# 未完了:{0} 件 完了:{1} 件
+gb.milestoneProgress = \u672a\u5b8c\u4e86:{0} \u4ef6 \u5b8c\u4e86:{1} \u4ef6
+# 未完了:{0} 件
+gb.nOpenTickets = \u672a\u5b8c\u4e86:{0} \u4ef6
+# 完了:{0} 件
+gb.nClosedTickets = \u5b8c\u4e86:{0} \u4ef6
+# 全 {0} 件
+gb.nTotalTickets = \u5168 {0} \u4ef6
+# 本文
+gb.body = \u672c\u6587
+# マージ
+gb.mergeTo = \u30de\u30fc\u30b8
+# マージ方法
+gb.mergeType = \u30de\u30fc\u30b8\u65b9\u6cd5
+gb.labels = labels
+gb.reviewers = reviewers
+# 賛同者
+gb.voters = \u8cdb\u540c\u8005
+gb.mentions = mentions
+gb.canNotProposePatchset = can not propose a patchset
+# このリポジトリは読取専用のミラーです
+gb.repositoryIsMirror = \u3053\u306e\u30ea\u30dd\u30b8\u30c8\u30ea\u306f\u8aad\u53d6\u5c02\u7528\u306e\u30df\u30e9\u30fc\u3067\u3059
+# このリポジトリは凍結中です。
+gb.repositoryIsFrozen = \u3053\u306e\u30ea\u30dd\u30b8\u30c8\u30ea\u306f\u51cd\u7d50\u4e2d\u3067\u3059\u3002
+# このリポジトリは patchsets を受け付けません。
+gb.repositoryDoesNotAcceptPatchsets = \u3053\u306e\u30ea\u30dd\u30b8\u30c8\u30ea\u306f patchsets \u3092\u53d7\u3051\u4ed8\u3051\u307e\u305b\u3093\u3002
+# このサーバーは patchsets を受け付けません。
+gb.serverDoesNotAcceptPatchsets = \u3053\u306e\u30b5\u30fc\u30d0\u30fc\u306f patchsets \u3092\u53d7\u3051\u4ed8\u3051\u307e\u305b\u3093\u3002
+# このチケットを完了にします。
+gb.ticketIsClosed = \u3053\u306e\u30c1\u30b1\u30c3\u30c8\u3092\u5b8c\u4e86\u306b\u3057\u307e\u3059\u3002
+# チケット patchsets をマージする際の既定の統合ブランチを指定します。
+gb.mergeToDescription = \u30c1\u30b1\u30c3\u30c8 patchsets \u3092\u30de\u30fc\u30b8\u3059\u308b\u969b\u306e\u65e2\u5b9a\u306e\u7d71\u5408\u30d6\u30e9\u30f3\u30c1\u3092\u6307\u5b9a\u3057\u307e\u3059\u3002
+# 統合ブランチに対して、常に早送りマージするか、必要ならばマージコミットを作成するか、常にマージコミットを作成するか、選択します。
+gb.mergeTypeDescription = \u7d71\u5408\u30d6\u30e9\u30f3\u30c1\u306b\u7d71\u5408\u30d6\u30e9\u30f3\u30c1\u306b\u5bfe\u3057\u3066\u3001\u5e38\u306b\u65e9\u9001\u308a\u30de\u30fc\u30b8\u3059\u308b\u304b\u3001\u5fc5\u8981\u306a\u3089\u3070\u30de\u30fc\u30b8\u30b3\u30df\u30c3\u30c8\u3092\u4f5c\u6210\u3059\u308b\u304b\u3001\u5e38\u306b\u30de\u30fc\u30b8\u30b3\u30df\u30c3\u30c8\u3092\u4f5c\u6210\u3059\u308b\u304b\u3001\u9078\u629e\u3057\u307e\u3059\u3002
+gb.anonymousCanNotPropose = Anonymous users can not propose patchsets.
+# このリポジトリをクローンする権限がありません
+gb.youDoNotHaveClonePermission = \u3053\u306e\u30ea\u30dd\u30b8\u30c8\u30ea\u3092\u30af\u30ed\u30fc\u30f3\u3059\u308b\u6a29\u9650\u304c\u3042\u308a\u307e\u305b\u3093
+# チケット
+gb.myTickets = \u30c1\u30b1\u30c3\u30c8
+# あなたが割り当てたチケット
+gb.yourAssignedTickets = \u3042\u306a\u305f\u304c\u5272\u308a\u5f53\u3066\u305f\u30c1\u30b1\u30c3\u30c8
+# 新規マイルストーン
+gb.newMilestone = \u65b0\u898f\u30de\u30a4\u30eb\u30b9\u30c8\u30fc\u30f3
+# マイルストーンの編集
+gb.editMilestone = \u30de\u30a4\u30eb\u30b9\u30c8\u30fc\u30f3\u306e\u7de8\u96c6
+# \"{0}\" を削除しますか?
+gb.deleteMilestone = \"{0}\" \u3092\u524a\u9664\u3057\u307e\u3059\u304b?
+# マイルストーン ''{0}'' を削除できませんでした。
+gb.milestoneDeleteFailed = \u30de\u30a4\u30eb\u30b9\u30c8\u30fc\u30f3 ''{0}'' \u3092\u524a\u9664\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002
+# 未完了のチケットの変更を通知しました。
+gb.notifyChangedOpenTickets = \u672a\u5b8c\u4e86\u306e\u30c1\u30b1\u30c3\u30c8\u306e\u5909\u66f4\u3092\u901a\u77e5\u3057\u307e\u3057\u305f\u3002
+gb.overdue = overdue
+# 未完了のマイルストーン
+gb.openMilestones = \u672a\u5b8c\u4e86\u306e\u30de\u30a4\u30eb\u30b9\u30c8\u30fc\u30f3
+# 完了したマイルストーン
+gb.closedMilestones = \u5b8c\u4e86\u3057\u305f\u30de\u30a4\u30eb\u30b9\u30c8\u30fc\u30f3
+# 管理
+gb.administration = \u7ba1\u7406
+# プラグイン
+gb.plugins = \u30d7\u30e9\u30b0\u30a4\u30f3
+# 拡張
+gb.extensions = \u62e1\u5f35
+# プロジェクトを選択してください
+gb.pleaseSelectProject = \u30d7\u30ed\u30b8\u30a7\u30af\u30c8\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044
+# TODO : この辺、コメントの訳が怪しい。
+# アクセス制限
+gb.accessPolicy = \u30a2\u30af\u30bb\u30b9\u5236\u9650
+# このリポジトリへのアクセス制限に関する設定を選択してください。下の選択肢ほど、制限が厳しくなります。
+gb.accessPolicyDescription = \u3053\u306e\u30ea\u30dd\u30b8\u30c8\u30ea\u3078\u306e\u30a2\u30af\u30bb\u30b9\u5236\u9650\u306b\u95a2\u3059\u308b\u8a2d\u5b9a\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044\u3002\u4e0b\u306e\u9078\u629e\u80a2\u307b\u3069\u3001\u5236\u9650\u304c\u53b3\u3057\u304f\u306a\u308a\u307e\u3059\u3002
+# 匿名ユーザーを許可
+gb.anonymousPolicy = \u533f\u540d\u30e6\u30fc\u30b6\u30fc\u3092\u8a31\u53ef
+# 全ユーザーに、参照・クローン・プッシュを許可します。
+gb.anonymousPolicyDescription = \u5168\u30e6\u30fc\u30b6\u30fc\u306b\u3001\u53c2\u7167\u30fb\u30af\u30ed\u30fc\u30f3\u30fb\u30d7\u30c3\u30b7\u30e5\u3092\u8a31\u53ef\u3057\u307e\u3059\u3002
+# プッシュ禁止 (許可制)
+gb.authenticatedPushPolicy = \u30d7\u30c3\u30b7\u30e5\u7981\u6b62 (\u8a31\u53ef\u5236)
+# 誰でも参照とクローンが可能です。許可されたユーザーには、プッシュ権限が与えられます。
+gb.authenticatedPushPolicyDescription = \u8ab0\u3067\u3082\u53c2\u7167\u3068\u30af\u30ed\u30fc\u30f3\u304c\u53ef\u80fd\u3067\u3059\u3002\u8a31\u53ef\u3055\u308c\u305f\u30e6\u30fc\u30b6\u30fc\u306b\u306f\u3001\u30d7\u30c3\u30b7\u30e5\u6a29\u9650\u304c\u4e0e\u3048\u3089\u308c\u307e\u3059\u3002
+# プッシュ禁止 (名前制)
+gb.namedPushPolicy = \u30d7\u30c3\u30b7\u30e5\u7981\u6b62 (\u540d\u524d\u5236)
+# 誰でも参照とクローンが可能です。プッシュ権限はユーザーごとに与えることが出来ます。
+gb.namedPushPolicyDescription = \u8ab0\u3067\u3082\u53c2\u7167\u3068\u30af\u30ed\u30fc\u30f3\u304c\u53ef\u80fd\u3067\u3059\u3002\u30d7\u30c3\u30b7\u30e5\u6a29\u9650\u306f\u30e6\u30fc\u30b6\u30fc\u3054\u3068\u306b\u4e0e\u3048\u308b\u3053\u3068\u304c\u51fa\u6765\u307e\u3059\u3002
+# クローン・プッシュ禁止
+gb.clonePolicy = \u30af\u30ed\u30fc\u30f3\u30fb\u30d7\u30c3\u30b7\u30e5\u7981\u6b62
+# 誰でも参照が可能です。クローンとプッシュはユーザーごとに設定可能です。
+gb.clonePolicyDescription = \u8ab0\u3067\u3082\u53c2\u7167\u304c\u53ef\u80fd\u3067\u3059\u3002\u30af\u30ed\u30fc\u30f3\u3068\u30d7\u30c3\u30b7\u30e5\u306f\u30e6\u30fc\u30b6\u30fc\u3054\u3068\u306b\u8a2d\u5b9a\u53ef\u80fd\u3067\u3059\u3002
+# 参照・クローン・プッシュ禁止
+gb.viewPolicy = \u53c2\u7167\u30fb\u30af\u30ed\u30fc\u30f3\u30fb\u30d7\u30c3\u30b7\u30e5\u7981\u6b62
+# 参照・クローン・プッシュの許可を、ユーザーごとに設定します。
+gb.viewPolicyDescription = \u53c2\u7167\u30fb\u30af\u30ed\u30fc\u30f3\u30fb\u30d7\u30c3\u30b7\u30e5\u306e\u8a31\u53ef\u3092\u3001\u30e6\u30fc\u30b6\u30fc\u3054\u3068\u306b\u8a2d\u5b9a\u3057\u307e\u3059\u3002
+# 初期コミット
+gb.initialCommit = \u521d\u671f\u30b3\u30df\u30c3\u30c8
+# これにより、即 <code>git clone</code> コマンドが使用可能になります。既に作業環境にて <code>git clone</code> コマンドを実行済みならば、この操作は省略出来ます。
+gb.initialCommitDescription = \u3053\u308c\u306b\u3088\u308a\u3001\u5373 <code> git clone</code> \u30b3\u30de\u30f3\u30c9\u304c\u4f7f\u7528\u53ef\u80fd\u306b\u306a\u308a\u307e\u3059\u3002\u65e2\u306b\u4f5c\u696d\u74b0\u5883\u306b\u3066 <code>git clone</code> \u30b3\u30de\u30f3\u30c9\u3092\u5b9f\u884c\u6e08\u307f\u306a\u3089\u3070\u3001\u3053\u306e\u64cd\u4f5c\u306f\u7701\u7565\u51fa\u6765\u307e\u3059\u3002
+# TODO : ここら辺、実際にチェックを入れて、動作を見ておきたい
+# README ファイルを含める
+gb.initWithReadme = README \u30d5\u30a1\u30a4\u30eb\u3092\u542b\u3081\u308b
+# チェックを入れると、単純な README を生成してリポジトリに含めます。
+gb.initWithReadmeDescription = \u30c1\u30a7\u30c3\u30af\u3092\u5165\u308c\u308b\u3068\u3001\u5358\u7d14\u306a README \u3092\u751f\u6210\u3057\u3066\u30ea\u30dd\u30b8\u30c8\u30ea\u306b\u542b\u3081\u307e\u3059\u3002
+# .gitignore ファイルを含める
+gb.initWithGitignore = .gitignore \u30d5\u30a1\u30a4\u30eb\u3092\u542b\u3081\u308b
+# 無視設定ファイルを含めます。
+gb.initWithGitignoreDescription = \u7121\u8996\u8a2d\u5b9a\u30d5\u30a1\u30a4\u30eb\u3092\u542b\u3081\u307e\u3059\u3002
+gb.pleaseSelectGitIgnore = Please select a .gitignore file
+# 受信設定
+gb.receive = \u53d7\u4fe1\u8a2d\u5b9a
+# 権限
+gb.permissions = \u6a29\u9650
+# 所有者は全員リポジトリの設定を変更することが出来ます。ただし、個人所有のリポジトリを除いて、リポジトリを改名することは出来ません。
+gb.ownersDescription = \u6240\u6709\u8005\u306f\u5168\u54e1\u30ea\u30dd\u30b8\u30c8\u30ea\u306e\u8a2d\u5b9a\u3092\u5909\u66f4\u3059\u308b\u3053\u3068\u304c\u51fa\u6765\u307e\u3059\u3002\u305f\u3060\u3057\u3001\u500b\u4eba\u6240\u6709\u306e\u30ea\u30dd\u30b8\u30c8\u30ea\u3092\u9664\u3044\u3066\u3001\u30ea\u30dd\u30b8\u30c8\u30ea\u3092\u6539\u540d\u3059\u308b\u3053\u3068\u306f\u51fa\u6765\u307e\u305b\u3093\u3002
+# ユーザーの権限は個別に設定することが出来ます。これらの設定は、チーム単位や正規表現による権限の設定を上書きします。
+gb.userPermissionsDescription = \u30e6\u30fc\u30b6\u30fc\u306e\u6a29\u9650\u306f\u500b\u5225\u306b\u8a2d\u5b9a\u3059\u308b\u3053\u3068\u304c\u51fa\u6765\u307e\u3059\u3002\u3053\u308c\u3089\u306e\u8a2d\u5b9a\u306f\u3001\u30c1\u30fc\u30e0\u5358\u4f4d\u3084\u6b63\u898f\u8868\u73fe\u306b\u3088\u308b\u6a29\u9650\u306e\u8a2d\u5b9a\u3092\u4e0a\u66f8\u304d\u3057\u307e\u3059\u3002
+# チームの権限は個別に設定することが出来ます。これらの設定は、正規表現による権限の設定を上書きします。
+gb.teamPermissionsDescription = \u30c1\u30fc\u30e0\u306e\u6a29\u9650\u306f\u500b\u5225\u306b\u8a2d\u5b9a\u3059\u308b\u3053\u3068\u304c\u51fa\u6765\u307e\u3059\u3002\u3053\u308c\u3089\u306e\u8a2d\u5b9a\u306f\u3001\u6b63\u898f\u8868\u73fe\u306b\u3088\u308b\u6a29\u9650\u306e\u8a2d\u5b9a\u3092\u4e0a\u66f8\u304d\u3057\u307e\u3059\u3002
+# チケット設定
+gb.ticketSettings = \u30c1\u30b1\u30c3\u30c8\u8a2d\u5b9a
+# 受信設定
+gb.receiveSettings = \u53d7\u4fe1\u8a2d\u5b9a
+# 受信設定は、リポジトリへのプッシュを制御します。
+gb.receiveSettingsDescription = \u53d7\u4fe1\u8a2d\u5b9a\u306f\u3001\u30ea\u30dd\u30b8\u30c8\u30ea\u3078\u306e\u30d7\u30c3\u30b7\u30e5\u3092\u5236\u5fa1\u3057\u307e\u3059\u3002
+# 受信前イベントは、コミットを受信し、且つそれを更新する「前に」実行されます。<p>プッシュを拒否するのに利用できます。</p>
+gb.preReceiveDescription = \u53d7\u4fe1\u524d\u30a4\u30d9\u30f3\u30c8\u306f\u3001\u30b3\u30df\u30c3\u30c8\u3092\u53d7\u4fe1\u3057\u3001\u4e14\u3064\u305d\u308c\u3092\u66f4\u65b0\u3059\u308b\u300c\u524d\u306b\u300d\u5b9f\u884c\u3055\u308c\u307e\u3059\u3002<p>\u30d7\u30c3\u30b7\u30e5\u3092\u62d2\u5426\u3059\u308b\u306e\u306b\u5229\u7528\u3067\u304d\u307e\u3059\u3002<\/p>
+# 受信後イベントは、、且つそれが更新される「後に」実行されます。<p>ビルド対象などに通知を行うのに利用できます。</p>
+gb.postReceiveDescription = \u53d7\u4fe1\u5f8c\u30a4\u30d9\u30f3\u30c8\u306f\u3001\u3001\u4e14\u3064\u305d\u308c\u304c\u66f4\u65b0\u3055\u308c\u308b\u300c\u5f8c\u306b\u300d\u5b9f\u884c\u3055\u308c\u307e\u3059\u3002<p>\u30d3\u30eb\u30c9\u5bfe\u8c61\u306a\u3069\u306b\u901a\u77e5\u3092\u884c\u3046\u306e\u306b\u5229\u7528\u3067\u304d\u307e\u3059\u3002<\/p>
+# このリポジトリを、他の Gitblit と連結させるか否か、するならばその方法を設定します。
+gb.federationStrategyDescription = \u30c1\u30b1\u30c3\u30c8\u306f\u3001TODO \u30ea\u30b9\u30c8\u3092\u4f53\u7cfb\u4ed8\u3051\u3001\u4e0d\u5177\u5408\u306b\u3064\u3044\u3066\u691c\u8a0e\u3057\u3001patchsets \u3068\u5f37\u529b\u3059\u308b\u306e\u306b\u5229\u7528\u3067\u304d\u307e\u3059\u3002
+# このリポジトリに、選択された連結セットを含めます。
+gb.federationSetsDescription = \u3053\u306e\u30ea\u30dd\u30b8\u30c8\u30ea\u306b\u3001\u9078\u629e\u3055\u308c\u305f\u9023\u7d50\u30bb\u30c3\u30c8\u3092\u542b\u3081\u307e\u3059\u3002
+# その他
+gb.miscellaneous = \u305d\u306e\u4ed6
+# このリポジトリがフォークされた際の、元の URL です
+gb.originDescription = \u3053\u306e\u30ea\u30dd\u30b8\u30c8\u30ea\u304c\u30d5\u30a9\u30fc\u30af\u3055\u308c\u305f\u969b\u306e\u3001\u5143\u306e URL \u3067\u3059
+gb.gc = GC
+# ガベージコレクション
+gb.garbageCollection = \u30ac\u30d9\u30fc\u30b8\u30b3\u30ec\u30af\u30b7\u30e7\u30f3
+# ガベージコレクターはクライアントからプッシュされた失われたオブジェクトを纏め、リポジトリから参照されていないオブジェクトを削除します。
+# TODO : 酷い翻訳...
+gb.garbageCollectionDescription = \u30ac\u30d9\u30fc\u30b8\u30b3\u30ec\u30af\u30bf\u30fc\u306f\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u304b\u3089\u30d7\u30c3\u30b7\u30e5\u3055\u308c\u305f\u5931\u308f\u308c\u305f\u30aa\u30d6\u30b8\u30a7\u30af\u30c8\u3092\u7e8f\u3081\u3001\u30ea\u30dd\u30b8\u30c8\u30ea\u304b\u3089\u53c2\u7167\u3055\u308c\u3066\u3044\u306a\u3044\u30aa\u30d6\u30b8\u30a7\u30af\u30c8\u3092\u524a\u9664\u3057\u307e\u3059\u3002
+# コミットメッセージは、テキストか HTML 形式かで表示することが出来ます。
+gb.commitMessageRendererDescription = \u30b3\u30df\u30c3\u30c8\u30e1\u30c3\u30bb\u30fc\u30b8\u306f\u3001\u30c6\u30ad\u30b9\u30c8\u304b HTML \u5f62\u5f0f\u304b\u3067\u8868\u793a\u3059\u308b\u3053\u3068\u304c\u51fa\u6765\u307e\u3059\u3002
+# 環境設定
+gb.preferences = \u74b0\u5883\u8a2d\u5b9a
+# アカウント設定
+gb.accountPreferences = \u30a2\u30ab\u30a6\u30f3\u30c8\u8a2d\u5b9a
+# アカウントを設定します。
+gb.accountPreferencesDescription = \u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u8a2d\u5b9a\u3057\u307e\u3059\u3002
+# 言語設定
+gb.languagePreference = \u8a00\u8a9e\u8a2d\u5b9a
+# 「言語設定」の解説。不要と思われる。
+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
+# Gitblit からの通知を受け取るメールアドレス
+gb.emailAddressDescription = Gitblit \u304b\u3089\u306e\u901a\u77e5\u3092\u53d7\u3051\u53d6\u308b\u30e1\u30fc\u30eb\u30a2\u30c9\u30ec\u30b9
+# SSH キー
+gb.sshKeys = SSH \u30ad\u30fc
+# SSH 公開キーによる認証は、パスワードによる認証に代わる安全な方法です。
+gb.sshKeysDescription = SSH \u516c\u958b\u30ad\u30fc\u306b\u3088\u308b\u8a8d\u8a3c\u306f\u3001\u30d1\u30b9\u30ef\u30fc\u30c9\u306b\u3088\u308b\u8a8d\u8a3c\u306b\u4ee3\u308f\u308b\u5b89\u5168\u306a\u65b9\u6cd5\u3067\u3059\u3002
+# SSH キーの追加
+gb.addSshKey = SSH \u30ad\u30fc\u306e\u8ffd\u52a0
+# キー
+gb.key = \u30ad\u30fc
+# 追加のコメントを入力します。省略した場合、コメントはキーより抽出されます
+gb.sshKeyCommentDescription = \u8ffd\u52a0\u306e\u30b3\u30e1\u30f3\u30c8\u3092\u5165\u529b\u3057\u307e\u3059\u3002\u7701\u7565\u3057\u305f\u5834\u5408\u3001\u30b3\u30e1\u30f3\u30c8\u306f\u30ad\u30fc\u3088\u308a\u62bd\u51fa\u3055\u308c\u307e\u3059
+# この SSH キーに与える権限を指定します
+gb.sshKeyPermissionDescription = \u3053\u306e SSH \u30ad\u30fc\u306b\u4e0e\u3048\u308b\u6a29\u9650\u3092\u6307\u5b9a\u3057\u307e\u3059
+# 通信設定
+gb.transportPreference = \u901a\u4fe1\u8a2d\u5b9a
+# クローン作成時に使用する通信方法を選択します。
+gb.transportPreferenceDescription = \u30af\u30ed\u30fc\u30f3\u4f5c\u6210\u6642\u306b\u4f7f\u7528\u3059\u308b\u901a\u4fe1\u65b9\u6cd5\u3092\u9078\u629e\u3057\u307e\u3059\u3002
+# プロパティ
+#優先度
+gb.priority = \u512a\u5148\u5ea6
+# 危険度
+gb.severity = \u5371\u967a\u5ea6
+# 優先度の高い順
+gb.sortHighestPriority = \u512a\u5148\u5ea6\u306e\u9ad8\u3044\u9806
+# 優先度の低い順
+gb.sortLowestPriority = \u512a\u5148\u5ea6\u306e\u4f4e\u3044\u9806
+# 危険度の高い順
+gb.sortHighestSeverity = \u5371\u967a\u5ea6\u306e\u9ad8\u3044\u9806
+# 危険度の低い順
+gb.sortLowestSeverity = \u5371\u967a\u5ea6\u306e\u4f4e\u3044\u9806
+gb.missingIntegrationBranchMore = The target integration branch does not exist in the repository!
+gb.diffDeletedFileSkipped = (deleted)
+gb.diffFileDiffTooLarge = Diff too large
+gb.diffNewFile = New file
+gb.diffDeletedFile = File was deleted
+gb.diffRenamedFile = File was renamed from {0}
+gb.diffCopiedFile = File was copied from {0}
+gb.diffTruncated = Diff truncated after the above file
+gb.opacityAdjust = Adjust opacity
+gb.blinkComparator = Blink comparator
+gb.imgdiffSubtract = Subtract (black = identical)
+# リポジトリの削除
+gb.deleteRepositoryHeader = \u30ea\u30dd\u30b8\u30c8\u30ea\u306e\u524a\u9664
+# リポジトリを削除すると、元に戻せません。
+gb.deleteRepositoryDescription = \u30ea\u30dd\u30b8\u30c8\u30ea\u3092\u524a\u9664\u3059\u308b\u3068\u3001\u5143\u306b\u623b\u305b\u307e\u305b\u3093\u3002
+# 空白の違いを表示
+# #差分ページで、一度「空白の違いを無視」をクリックすると、代わりに表示される。
+gb.show_whitespace = \u7a7a\u767d\u306e\u9055\u3044\u3092\u8868\u793a
+# 空白の違いを無視
+gb.ignore_whitespace = \u7a7a\u767d\u306e\u9055\u3044\u3092\u7121\u8996
+# リポジトリリスト
+gb.allRepositories = \u30ea\u30dd\u30b8\u30c8\u30ea\u30ea\u30b9\u30c8
+# オブジェクトID
+gb.oid = \u30aa\u30d6\u30b8\u30a7\u30af\u30c8ID
+# ファイルストア
+gb.filestore = \u30d5\u30a1\u30a4\u30eb\u30b9\u30c8\u30a2
+# #ファイルストアは、サイズが{1} の {0} ファイルを含んでいます。({2} 残)
+gb.filestoreStats = \u30d5\u30a1\u30a4\u30eb\u30b9\u30c8\u30a2\u306f\u3001\u30b5\u30a4\u30ba\u304c{1} \u306e {0} \u30d5\u30a1\u30a4\u30eb\u3092\u542b\u3093\u3067\u3044\u307e\u3059\u3002({2} \u6b8b)
+# 状態変更場所
+gb.statusChangedOn = \u72b6\u614b\u5909\u66f4\u5834\u6240
+# 状態変更者
+gb.statusChangedBy = \u72b6\u614b\u5909\u66f4\u8005
+# ファイルストアの使用方法
+gb.filestoreHelp = \u30d5\u30a1\u30a4\u30eb\u30b9\u30c8\u30a2\u306e\u4f7f\u7528\u65b9\u6cd5
+# ファイルの編集
+gb.editFile = \u30d5\u30a1\u30a4\u30eb\u306e\u7de8\u96c6
+# #以下2つ、リポジトリメニューのドキュメントタブで、ドキュメントを編集して保存しようとすると現れるダイアログのボタンのキャプション
+# 編集続行
+gb.continueEditing = \u7de8\u96c6\u7d9a\u884c
+# 変更点
+gb.commitChanges = \u5909\u66f4\u70b9
+# {0}をコミットできませんでした。このファイルは自動マージ出来ません
+gb.fileNotMergeable = Unable to commit {0}. This file can not be automatically merged.
+# {0}をコミットしました
+gb.fileCommitted = {0}\u3092\u30b3\u30df\u30c3\u30c8\u3057\u307e\u3057\u305f
+# パッチセット {0} を削除しました
+gb.deletePatchset = \u30d1\u30c3\u30c1\u30bb\u30c3\u30c8 {0} \u3092\u524a\u9664\u3057\u307e\u3057\u305f
+# パッチセット {0} を削除しました。
+gb.deletePatchsetSuccess = \u30d1\u30c3\u30c1\u30bb\u30c3\u30c8 {0} \u3092\u524a\u9664\u3057\u307e\u3057\u305f\u3002
+# パッチセット {0} の削除中にエラーが起きました。
+gb.deletePatchsetFailure = \u30d1\u30c3\u30c1\u30bb\u30c3\u30c8 {0} \u306e\u524a\u9664\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u8d77\u304d\u307e\u3057\u305f\u3002
+# コミットに参照されています。
+gb.referencedByCommit = \u30b3\u30df\u30c3\u30c8\u306b\u53c2\u7167\u3055\u308c\u3066\u3044\u307e\u3059\u3002
+# チケットに参照されています。
+gb.referencedByTicket = \u30c1\u30b1\u30c3\u30c8\u306b\u53c2\u7167\u3055\u308c\u3066\u3044\u307e\u3059\u3002
+# {0} にアクセスするのに必要な、あなたのGitblitクライアント証明書です。
+gb.emailClientCertificateSubject = {0} \u306b\u30a2\u30af\u30bb\u30b9\u3059\u308b\u306e\u306b\u5fc5\u8981\u306a\u3001\u3042\u306a\u305f\u306eGitblit\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u8a3c\u660e\u66f8\u3067\u3059\u3002
+
+
+# This last property for unit tests to test successful loading of the resource file
+gb.loadLang = \u306b\u307b\u3093\u3054
diff --git a/src/main/java/com/gitblit/wicket/GitBlitWebApp_ko.properties b/src/main/java/com/gitblit/wicket/GitBlitWebApp_ko.properties
index 404f0d28..31f3b05b 100644
--- a/src/main/java/com/gitblit/wicket/GitBlitWebApp_ko.properties
+++ b/src/main/java/com/gitblit/wicket/GitBlitWebApp_ko.properties
@@ -1,184 +1,183 @@
-gb.repository = \uc800\uc7a5\uc18c
-gb.owner = \uc18c\uc720\uc790
-gb.description = \uc124\uba85
-gb.lastChange = \ucd5c\uadfc \ubcc0\uacbd
+gb.repository = \uC800\uC7A5\uC18C
+gb.owner = \uC18C\uC720\uC790
+gb.description = \uC124\uBA85
+gb.lastChange = \uCD5C\uADFC \uBCC0\uACBD
gb.refs = refs
-gb.tag = \ud0dc\uadf8
-gb.tags = \ud0dc\uadf8\ub4e4
-gb.author = \uc791\uc131\uc790
-gb.committer = \ucee4\ubbf8\ud130
-gb.commit = \ucee4\ubc0b
-gb.age = \ub098\uc774
-gb.tree = \ud2b8\ub9ac
-gb.parent = \ubd80\ubaa8
+gb.tag = \uD0DC\uADF8
+gb.tags = \uD0DC\uADF8\uB4E4
+gb.author = \uC791\uC131\uC790
+gb.committer = \uCEE4\uBBF8\uD130
+gb.commit = \uCEE4\uBC0B
+gb.age = \uB098\uC774
+gb.tree = \uD2B8\uB9AC
+gb.parent = \uBD80\uBAA8
gb.url = URL
-gb.history = \ud788\uc2a4\ud1a0\ub9ac
+gb.history = \uD788\uC2A4\uD1A0\uB9AC
gb.raw = raw
-gb.object = \uc624\ube0c\uc81d\ud2b8
-gb.ticketId = \ud2f0\ucf13 id
-gb.ticketAssigned = \ud560\ub2f9
-gb.ticketOpenDate = \uc5f4\ub9b0 \ub0a0\uc790
-gb.ticketState = \uc0c1\ud0dc
-gb.ticketComments = \ucf54\uba58\ud2b8
-gb.view = \ubcf4\uae30
-gb.local = \ub85c\uceec
-gb.remote = \ub9ac\ubaa8\ud2b8
-gb.branches = \ube0c\ub79c\uce58
-gb.patch = \ud328\uce58
-gb.diff = \ube44\uad50
-gb.log = \ub85c\uadf8
-gb.moreLogs = \ucee4\ubc0b \ub354 \ubcf4\uae30...
-gb.allTags = \ubaa8\ub4e0 \ud0dc\uadf8...
-gb.allBranches = \ubaa8\ub4e0 \ube0c\ub79c\uce58...
-gb.summary = \uc694\uc57d
-gb.ticket = \ud2f0\ucf13
-gb.newRepository = \uc0c8 \uc800\uc7a5\uc18c
-gb.newUser = \uc0c8 \uc0ac\uc6a9\uc790
-gb.commitdiff = \ucee4\ubc0b\ube44\uad50
-gb.tickets = \ud2f0\ucf13
-gb.pageFirst = \ucc98\uc74c
-gb.pagePrevious = \uc774\uc804
-gb.pageNext = \ub2e4\uc74c
+gb.object = \uC624\uBE0C\uC81D\uD2B8
+gb.ticketId = \uD2F0\uCF13 id
+gb.ticketAssigned = \uD560\uB2F9
+gb.ticketOpenDate = \uC5F4\uB9B0 \uB0A0\uC790
+gb.ticketStatus = \uC0C1\uD0DC
+gb.ticketComments = \uCF54\uBA58\uD2B8
+gb.view = \uBCF4\uAE30
+gb.local = \uB85C\uCEEC
+gb.remote = \uB9AC\uBAA8\uD2B8
+gb.branches = \uBE0C\uB79C\uCE58
+gb.patch = \uD328\uCE58
+gb.diff = \uBE44\uAD50
+gb.log = \uB85C\uADF8
+gb.moreLogs = \uCEE4\uBC0B \uB354 \uBCF4\uAE30...
+gb.allTags = \uBAA8\uB4E0 \uD0DC\uADF8...
+gb.allBranches = \uBAA8\uB4E0 \uBE0C\uB79C\uCE58...
+gb.summary = \uC694\uC57D
+gb.ticket = \uD2F0\uCF13
+gb.newRepository = \uC0C8 \uC800\uC7A5\uC18C
+gb.newUser = \uC0C8 \uC0AC\uC6A9\uC790
+gb.commitdiff = \uCEE4\uBC0B\uBE44\uAD50
+gb.tickets = \uD2F0\uCF13
+gb.pageFirst = \uCC98\uC74C
+gb.pagePrevious = \uC774\uC804
+gb.pageNext = \uB2E4\uC74C
gb.head = HEAD
gb.blame = blame
-gb.login = \ub85c\uadf8\uc778
-gb.logout = \ub85c\uadf8\uc544\uc6c3
-gb.username = \uc720\uc800\ub124\uc784
-gb.password = \ud328\uc2a4\uc6cc\ub4dc
-gb.tagger = \ud0dc\uac70
-gb.moreHistory = \ud788\uc2a4\ud1a0\ub9ac \ub354 \ubcf4\uae30...
-gb.difftocurrent = \ud604\uc7ac\uc640 \ube44\uad50
-gb.search = \uac80\uc0c9
-gb.searchForAuthor = \ucee4\ubc0b\uc744 \uc791\uc131\uc790\ub85c \uac80\uc0c9
-gb.searchForCommitter = \ucee4\ubc0b\uc744 \ucee4\ubc0b\ud130\ub85c \uac80\uc0c9
-gb.addition = \ucd94\uac00
-gb.modification = \ubcc0\uacbd
-gb.deletion = \uc0ad\uc81c
-gb.rename = \uc774\ub984\ubcc0\uacbd
-gb.metrics = \uba54\ud2b8\ub9ad
-gb.stats = \uc0c1\ud0dc
-gb.markdown = \ub9c8\ud06c\ub2e4\uc6b4
-gb.changedFiles = \ud30c\uc77c \ubcc0\uacbd\ub428
-gb.filesAdded = {0}\uac1c \ud30c\uc77c \ucd94\uac00\ub428
-gb.filesModified = {0}\uac1c \ud30c\uc77c \ubcc0\uacbd\ub428
-gb.filesDeleted = {0}\uac1c \ud30c\uc77c \uc0ad\uc81c\ub428
-gb.filesCopied = {0}\uac1c \ud30c\uc77c \ubcf5\uc0ac\ub428
-gb.filesRenamed = {0}\uac1c \ud30c\uc77c \uc774\ub984 \ubcc0\uacbd\ub428
-gb.missingUsername = \uc720\uc800\ub124\uc784 \ub204\ub77d
-gb.edit = \uc218\uc815
-gb.searchTypeTooltip = \uac80\uc0c9 \ud0c0\uc785 \uc120\ud0dd
-gb.searchTooltip = {0} \uac80\uc0c9
-gb.delete = \uc0ad\uc81c
-gb.docs = \ubb38\uc11c
-gb.accessRestriction = \uc811\uc18d \uc81c\ud55c
-gb.name = \uc774\ub984
-gb.enableTickets = \ud2f0\ucf13 \uc0ac\uc6a9
-gb.enableDocs = \ubb38\uc11c \uc0ac\uc6a9
-gb.save = \uc800\uc7a5
-gb.showRemoteBranches = \ub9ac\ubaa8\ud2b8 \ube0c\ub79c\uce58 \ubcf4\uae30
-gb.editUsers = \uc720\uc800 \uc218\uc815
-gb.confirmPassword = \ud328\uc2a4\uc6cc\ub4dc \ud655\uc778
-gb.restrictedRepositories = \uc81c\ud55c\ub41c \uc800\uc7a5\uc18c
-gb.canAdmin = \uad00\ub9ac \uac00\ub2a5
-gb.notRestricted = \uc775\uba85 \ubdf0, \ud074\ub860, & \ud478\uc2dc
-gb.pushRestricted = \uc778\uc99d\ub41c \uc720\uc800\ub9cc \ud478\uc2dc
-gb.cloneRestricted = \uc778\uc99d\ub41c \uc720\uc800\ub9cc \ud074\ub860 & \ud478\uc2dc
-gb.viewRestricted = \uc778\uc99d\ub41c \uc720\uc800\ub9cc \ubdf0, \ud074\ub860, & \ud478\uc2dc
-gb.useTicketsDescription = Ticgit(\ubd84\uc0b0 \ud2f0\ucf13 \uc2dc\uc2a4\ud15c) \uc774\uc288 \uc0ac\uc6a9
-gb.useDocsDescription = \uc800\uc7a5\uc18c \uc788\ub294 \ub9c8\ud06c\ub2e4\uc6b4 \ubb38\uc11c \uc0ac\uc6a9
-gb.showRemoteBranchesDescription = \ub9ac\ubaa8\ud2b8 \ube0c\ub79c\uce58 \ubcf4\uae30
-gb.canAdminDescription = Gitblit \uad00\ub9ac \uad8c\ud55c \ubd80\uc5ec
-gb.permittedUsers = \ud5c8\uc6a9\ub41c \uc0ac\uc6a9\uc790
-gb.isFrozen = \ud504\ub9ac\uc9d5\ub428
-gb.isFrozenDescription = \ud478\uc2dc \ucc28\ub2e8
+gb.login = \uB85C\uADF8\uC778
+gb.logout = \uB85C\uADF8\uC544\uC6C3
+gb.username = \uC720\uC800\uB124\uC784
+gb.password = \uD328\uC2A4\uC6CC\uB4DC
+gb.tagger = \uD0DC\uAC70
+gb.moreHistory = \uD788\uC2A4\uD1A0\uB9AC \uB354 \uBCF4\uAE30...
+gb.difftocurrent = \uD604\uC7AC\uC640 \uBE44\uAD50
+gb.search = \uAC80\uC0C9
+gb.searchForAuthor = \uCEE4\uBC0B\uC744 \uC791\uC131\uC790\uB85C \uAC80\uC0C9
+gb.searchForCommitter = \uCEE4\uBC0B\uC744 \uCEE4\uBC0B\uD130\uB85C \uAC80\uC0C9
+gb.addition = \uCD94\uAC00
+gb.modification = \uBCC0\uACBD
+gb.deletion = \uC0AD\uC81C
+gb.rename = \uC774\uB984\uBCC0\uACBD
+gb.metrics = \uBA54\uD2B8\uB9AD
+gb.stats = \uC0C1\uD0DC
+gb.markdown = \uB9C8\uD06C\uB2E4\uC6B4
+gb.changedFiles = \uD30C\uC77C \uBCC0\uACBD\uB428
+gb.filesAdded = {0}\uAC1C \uD30C\uC77C \uCD94\uAC00\uB428
+gb.filesModified = {0}\uAC1C \uD30C\uC77C \uBCC0\uACBD\uB428
+gb.filesDeleted = {0}\uAC1C \uD30C\uC77C \uC0AD\uC81C\uB428
+gb.filesCopied = {0}\uAC1C \uD30C\uC77C \uBCF5\uC0AC\uB428
+gb.filesRenamed = {0}\uAC1C \uD30C\uC77C \uC774\uB984 \uBCC0\uACBD\uB428
+gb.missingUsername = \uC720\uC800\uB124\uC784 \uB204\uB77D
+gb.edit = \uC218\uC815
+gb.searchTypeTooltip = \uAC80\uC0C9 \uD0C0\uC785 \uC120\uD0DD
+gb.searchTooltip = {0} \uAC80\uC0C9
+gb.delete = \uC0AD\uC81C
+gb.docs = \uBB38\uC11C
+gb.accessRestriction = \uC811\uC18D \uC81C\uD55C
+gb.name = \uC774\uB984
+gb.enableTickets = \uD2F0\uCF13 \uC0AC\uC6A9
+gb.enableDocs = \uBB38\uC11C \uC0AC\uC6A9
+gb.save = \uC800\uC7A5
+gb.showRemoteBranches = \uB9AC\uBAA8\uD2B8 \uBE0C\uB79C\uCE58 \uBCF4\uAE30
+gb.editUsers = \uC720\uC800 \uC218\uC815
+gb.confirmPassword = \uD328\uC2A4\uC6CC\uB4DC \uD655\uC778
+gb.restrictedRepositories = \uC81C\uD55C\uB41C \uC800\uC7A5\uC18C
+gb.canAdmin = \uAD00\uB9AC \uAC00\uB2A5
+gb.notRestricted = \uC775\uBA85 \uBDF0, \uD074\uB860, & \uD478\uC2DC
+gb.pushRestricted = \uC778\uC99D\uB41C \uC720\uC800\uB9CC \uD478\uC2DC
+gb.cloneRestricted = \uC778\uC99D\uB41C \uC720\uC800\uB9CC \uD074\uB860 & \uD478\uC2DC
+gb.viewRestricted = \uC778\uC99D\uB41C \uC720\uC800\uB9CC \uBDF0, \uD074\uB860, & \uD478\uC2DC
+gb.useTicketsDescription = Ticgit(\uBD84\uC0B0 \uD2F0\uCF13 \uC2DC\uC2A4\uD15C) \uC774\uC288 \uC0AC\uC6A9
+gb.useDocsDescription = \uC800\uC7A5\uC18C \uC788\uB294 \uB9C8\uD06C\uB2E4\uC6B4 \uBB38\uC11C \uC0AC\uC6A9
+gb.showRemoteBranchesDescription = \uB9AC\uBAA8\uD2B8 \uBE0C\uB79C\uCE58 \uBCF4\uAE30
+gb.canAdminDescription = Gitblit \uAD00\uB9AC \uAD8C\uD55C \uBD80\uC5EC
+gb.permittedUsers = \uD5C8\uC6A9\uB41C \uC0AC\uC6A9\uC790
+gb.isFrozen = \uD504\uB9AC\uC9D5\uB428
+gb.isFrozenDescription = \uD478\uC2DC \uCC28\uB2E8
gb.zip = zip
-gb.showReadme = \ub9ac\ub4dc\ubbf8(readme) \ubcf4\uae30
-gb.showReadmeDescription = \uc694\uc57d\ud398\uc774\uc9c0\uc5d0\uc11c \"readme\" \ub9c8\ud06c\ub2e4\uc6b4 \ud30c\uc77c \ubcf4\uae30
-gb.nameDescription = \uc800\uc7a5\uc18c\ub97c \uadf8\ub8f9\uc73c\ub85c \ubb36\uc73c\ub824\uba74 '/' \ub97c \uc0ac\uc6a9. \uc608) libraries/reponame.git
-gb.ownerDescription = \uc18c\uc720\uc790\ub294 \uc800\uc7a5\uc18c \uc124\uc815\uc744 \ubcc0\uacbd\ud560 \uc218 \uc788\uc74c
+gb.showReadme = \uB9AC\uB4DC\uBBF8(readme) \uBCF4\uAE30
+gb.showReadmeDescription = \uC694\uC57D\uD398\uC774\uC9C0\uC5D0\uC11C \"readme\" \uB9C8\uD06C\uB2E4\uC6B4 \uD30C\uC77C \uBCF4\uAE30
+gb.nameDescription = \uC800\uC7A5\uC18C\uB97C \uADF8\uB8F9\uC73C\uB85C \uBB36\uC73C\uB824\uBA74 '/' \uB97C \uC0AC\uC6A9. \uC608) libraries/reponame.git
+gb.ownerDescription = \uC18C\uC720\uC790\uB294 \uC800\uC7A5\uC18C \uC124\uC815\uC744 \uBCC0\uACBD\uD560 \uC218 \uC788\uC74C
gb.blob = blob
-gb.commitActivityTrend = \ucee4\ubc0b \ud65c\ub3d9 \ud2b8\ub79c\ub4dc
-gb.commitActivityDOW = 1\uc8fc\uc77c\uc758 \uc77c\ub2e8\uc704 \ucee4\ubc0b \ud65c\ub3d9
-gb.commitActivityAuthors = \ucee4\ubc0b \ud65c\ub3d9\uc758 \uc8fc \uc791\uc131\uc790
-gb.feed = \ud53c\ub4dc
-gb.cancel = \ucde8\uc18c
-gb.changePassword = \ud328\uc2a4\uc6cc\ub4dc \ubcc0\uacbd
-gb.isFederated = \ud398\ub354\ub808\uc774\uc158\ub428
-gb.federateThis = \uc774 \uc800\uc7a5\uc18c\ub97c \ud398\ub354\ub808\uc774\uc158\ud568
-gb.federateOrigin = origin \uc5d0 \ud398\ub354\ub808\uc774\uc158
-gb.excludeFromFederation = \ud398\ub354\ub808\uc774\uc158 \uc81c\uc678
-gb.excludeFromFederationDescription = \uc774 \uacc4\uc815\uc73c\ub85c \ud480\ub9c1\ub418\ub294 \ud398\ub7ec\ub808\uc774\uc158 \ub41c Gitblit \uc778\uc2a4\ud134\uc2a4 \ucc28\ub2e8
-gb.tokens = \ud398\ub354\ub808\uc774\uc158 \ud1a0\ud070
-gb.tokenAllDescription = \ubaa8\ub4e0 \uc800\uc7a5\uc18c, \ud1a0\ud070, \uc0ac\uc6a9\uc790 & \uc124\uc815
-gb.tokenUnrDescription = \ubaa8\ub4e0 \uc800\uc7a5\uc18c & \uc0ac\uc6a9\uc790
-gb.tokenJurDescription = \ubaa8\ub4e0 \uc800\uc7a5\uc18c
-gb.federatedRepositoryDefinitions = \uc800\uc7a5\uc18c \uc815\uc758
-gb.federatedUserDefinitions = \uc0ac\uc6a9\uc790 \uc815\uc758
-gb.federatedSettingDefinitions = \uc124\uc815 \uc815\uc758
-gb.proposals = \ud398\ub354\ub808\uc774\uc158 \uc81c\uc548
-gb.received = \uc218\uc2e0\ud568
-gb.type = \ud0c0\uc785
-gb.token = \ud1a0\ud070
-gb.repositories = \uc800\uc7a5\uc18c
-gb.proposal = \uc81c\uc548
-gb.frequency = \ube48\ub3c4
-gb.folder = \ud3f4\ub354
-gb.lastPull = \ub9c8\uc9c0\ub9c9 \ud480
-gb.nextPull = \ub2e4\uc74c \ud480
-gb.inclusions = \ud3ec\ud568
-gb.exclusions = \uc81c\uc678
-gb.registration = \ub4f1\ub85d
-gb.registrations = \ud398\ub354\ub808\uc774\uc158 \ub4f1\ub85d
-gb.sendProposal = \uc81c\uc548\ud558\uae30
-gb.status = \uc0c1\ud0dc
+gb.commitActivityTrend = \uCEE4\uBC0B \uD65C\uB3D9 \uD2B8\uB79C\uB4DC
+gb.commitActivityDOW = 1\uC8FC\uC77C\uC758 \uC77C\uB2E8\uC704 \uCEE4\uBC0B \uD65C\uB3D9
+gb.commitActivityAuthors = \uCEE4\uBC0B \uD65C\uB3D9\uC758 \uC8FC \uC791\uC131\uC790
+gb.feed = \uD53C\uB4DC
+gb.cancel = \uCDE8\uC18C
+gb.changePassword = \uD328\uC2A4\uC6CC\uB4DC \uBCC0\uACBD
+gb.isFederated = \uD398\uB354\uB808\uC774\uC158\uB428
+gb.federateThis = \uC774 \uC800\uC7A5\uC18C\uB97C \uD398\uB354\uB808\uC774\uC158\uD568
+gb.federateOrigin = origin \uC5D0 \uD398\uB354\uB808\uC774\uC158
+gb.excludeFromFederation = \uD398\uB354\uB808\uC774\uC158 \uC81C\uC678
+gb.excludeFromFederationDescription = \uC774 \uACC4\uC815\uC73C\uB85C \uD480\uB9C1\uB418\uB294 \uD398\uB7EC\uB808\uC774\uC158 \uB41C Gitblit \uC778\uC2A4\uD134\uC2A4 \uCC28\uB2E8
+gb.tokens = \uD398\uB354\uB808\uC774\uC158 \uD1A0\uD070
+gb.tokenAllDescription = \uBAA8\uB4E0 \uC800\uC7A5\uC18C, \uD1A0\uD070, \uC0AC\uC6A9\uC790 & \uC124\uC815
+gb.tokenUnrDescription = \uBAA8\uB4E0 \uC800\uC7A5\uC18C & \uC0AC\uC6A9\uC790
+gb.tokenJurDescription = \uBAA8\uB4E0 \uC800\uC7A5\uC18C
+gb.federatedRepositoryDefinitions = \uC800\uC7A5\uC18C \uC815\uC758
+gb.federatedUserDefinitions = \uC0AC\uC6A9\uC790 \uC815\uC758
+gb.federatedSettingDefinitions = \uC124\uC815 \uC815\uC758
+gb.proposals = \uD398\uB354\uB808\uC774\uC158 \uC81C\uC548
+gb.received = \uC218\uC2E0\uD568
+gb.type = \uD0C0\uC785
+gb.token = \uD1A0\uD070
+gb.repositories = \uC800\uC7A5\uC18C
+gb.proposal = \uC81C\uC548
+gb.frequency = \uBE48\uB3C4
+gb.folder = \uD3F4\uB354
+gb.lastPull = \uB9C8\uC9C0\uB9C9 \uD480
+gb.nextPull = \uB2E4\uC74C \uD480
+gb.inclusions = \uD3EC\uD568
+gb.exclusions = \uC81C\uC678
+gb.registration = \uB4F1\uB85D
+gb.registrations = \uD398\uB354\uB808\uC774\uC158 \uB4F1\uB85D
+gb.sendProposal = \uC81C\uC548\uD558\uAE30
+gb.status = \uC0C1\uD0DC
gb.origin = origin
-gb.headRef = \ub514\ud3f4\ud2b8 \ube0c\ub79c\uce58(HEAD)
-gb.headRefDescription = \ub514\ud3f4\ud2b8 \ube0c\ub79c\uce58\ub97c \uc785\ub825. \uc608) refs/heads/master
-gb.federationStrategy = \ud398\ub354\ub808\uc774\uc158 \uc815\ucc45
-gb.federationRegistration = \ud398\ub354\ub808\uc774\uc158 \ub4f1\ub85d
-gb.federationResults = \ud398\ub354\ub808\uc774\uc158 \ud480 \uacb0\uacfc
-gb.federationSets = \ud398\ub354\ub808\uc774\uc158 \uc14b
-gb.message = \uba54\uc2dc\uc9c0
-gb.myUrlDescription = \uacf5\uac1c\ub418\uc5b4 \uc811\uc18d\ud560 \uc218 \uc788\ub294 Gitblit \uc778\uc2a4\ud134\uc2a4 url
-gb.destinationUrl = \ub85c \ubcf4\ub0c4
-gb.destinationUrlDescription = \uc81c\uc548\uc744 \uc804\uc1a1\ud560 \ub300\uc0c1 Gitblit \uc778\uc2a4\ud134\uc2a4\uc758 url
-gb.users = \uc720\uc800
-gb.federation = \ud398\ub354\ub808\uc774\uc158
-gb.error = \uc5d0\ub7ec
-gb.refresh = \uc0c8\ub85c\uace0\uce68
-gb.browse = \ube0c\ub77c\uc6b0\uc988
+gb.headRef = \uB514\uD3F4\uD2B8 \uBE0C\uB79C\uCE58(HEAD)
+gb.headRefDescription = \uB514\uD3F4\uD2B8 \uBE0C\uB79C\uCE58\uB97C \uC785\uB825. \uC608) refs/heads/master
+gb.federationStrategy = \uD398\uB354\uB808\uC774\uC158 \uC815\uCC45
+gb.federationRegistration = \uD398\uB354\uB808\uC774\uC158 \uB4F1\uB85D
+gb.federationResults = \uD398\uB354\uB808\uC774\uC158 \uD480 \uACB0\uACFC
+gb.federationSets = \uD398\uB354\uB808\uC774\uC158 \uC14B
+gb.message = \uBA54\uC2DC\uC9C0
+gb.myUrlDescription = \uACF5\uAC1C\uB418\uC5B4 \uC811\uC18D\uD560 \uC218 \uC788\uB294 Gitblit \uC778\uC2A4\uD134\uC2A4 url
+gb.destinationUrl = \uB85C \uBCF4\uB0C4
+gb.destinationUrlDescription = \uC81C\uC548\uC744 \uC804\uC1A1\uD560 \uB300\uC0C1 Gitblit \uC778\uC2A4\uD134\uC2A4\uC758 url
+gb.users = \uC720\uC800
+gb.federation = \uD398\uB354\uB808\uC774\uC158
+gb.error = \uC5D0\uB7EC
+gb.refresh = \uC0C8\uB85C\uACE0\uCE68
+gb.browse = \uBE0C\uB77C\uC6B0\uC988
gb.clone = clone
-gb.filter = \ud544\ud130
-gb.create = \uc0dd\uc131
-gb.servers = \uc11c\ubc84
+gb.filter = \uD544\uD130
+gb.create = \uC0DD\uC131
+gb.servers = \uC11C\uBC84
gb.recent = recent
-gb.available = \uac00\ub2a5\ud55c
-gb.selected = \uc120\ud0dd\ub41c
-gb.size = \ud06c\uae30
-gb.downloading = \ub2e4\uc6b4\ub85c\ub4dc\uc911
-gb.loading = \ub85c\ub529\uc911
-gb.starting = \uc2dc\uc791\uc911
-gb.general = \uc77c\ubc18
-gb.settings = \uc138\ud305
-gb.manage = \uad00\ub9ac
-gb.lastLogin = \ub9c8\uc9c0\ub9c9 \ub85c\uadf8\uc778
-gb.skipSizeCalculation = \ud06c\uae30 \uacc4\uc0b0 \ubb34\uc2dc
-gb.skipSizeCalculationDescription = \uc800\uc7a5\uc18c \ud06c\uae30 \uacc4\uc0b0\ud558\uc9c0 \uc54a\uc74c (\ud398\uc774\uc9c0 \ub85c\ub529 \uc2dc\uac04 \ub2e8\ucd95\ub428)
-gb.skipSummaryMetrics = \uba54\ud2b8\ub9ad \uc694\uc57d \ubb34\uc2dc
-gb.skipSummaryMetricsDescription = \uc694\uc57d \ud398\uc9c0\uc774\uc5d0\uc11c \uba54\ud2b8\ub9ad \uacc4\uc0b0\ud558\uc9c0 \uc54a\uc74c (\ud398\uc774\uc9c0 \ub85c\ub529 \uc2dc\uac04 \ub2e8\ucd95\ub428)
-gb.accessLevel = \uc811\uc18d \ub808\ubca8
-gb.default = \ub514\ud3f4\ud2b8
-gb.setDefault = \ub514\ud3f4\ud2b8 \uc124\uc815
+gb.available = \uAC00\uB2A5\uD55C
+gb.selected = \uC120\uD0DD\uB41C
+gb.size = \uD06C\uAE30
+gb.downloading = \uB2E4\uC6B4\uB85C\uB4DC\uC911
+gb.loading = \uB85C\uB529\uC911
+gb.starting = \uC2DC\uC791\uC911
+gb.general = \uC77C\uBC18
+gb.settings = \uC138\uD305
+gb.manage = \uAD00\uB9AC
+gb.lastLogin = \uB9C8\uC9C0\uB9C9 \uB85C\uADF8\uC778
+gb.skipSizeCalculation = \uD06C\uAE30 \uACC4\uC0B0 \uBB34\uC2DC
+gb.skipSizeCalculationDescription = \uC800\uC7A5\uC18C \uD06C\uAE30 \uACC4\uC0B0\uD558\uC9C0 \uC54A\uC74C (\uD398\uC774\uC9C0 \uB85C\uB529 \uC2DC\uAC04 \uB2E8\uCD95\uB428)
+gb.skipSummaryMetrics = \uBA54\uD2B8\uB9AD \uC694\uC57D \uBB34\uC2DC
+gb.skipSummaryMetricsDescription = \uC694\uC57D \uD398\uC9C0\uC774\uC5D0\uC11C \uBA54\uD2B8\uB9AD \uACC4\uC0B0\uD558\uC9C0 \uC54A\uC74C (\uD398\uC774\uC9C0 \uB85C\uB529 \uC2DC\uAC04 \uB2E8\uCD95\uB428)
+gb.accessLevel = \uC811\uC18D \uB808\uBCA8
+gb.default = \uB514\uD3F4\uD2B8
+gb.setDefault = \uB514\uD3F4\uD2B8 \uC124\uC815
gb.since = since
-gb.status = \uc0c1\ud0dc
-gb.bootDate = \ubd80\ud305 \uc77c\uc790
-gb.servletContainer = \uc11c\ube14\ub9bf \ucee8\ud14c\uc774\ub108
-gb.heapMaximum = \ucd5c\ub300 \ud799
-gb.heapAllocated = \ud560\ub2f9\ub41c \ud799
-gb.heapUsed = \uc0ac\uc6a9\ub41c \ud799
-gb.free = \ud504\ub9ac
-gb.version = \ubc84\uc804
-gb.releaseDate = \ub9b4\ub9ac\uc988 \ub0a0\uc9dc
+gb.bootDate = \uBD80\uD305 \uC77C\uC790
+gb.servletContainer = \uC11C\uBE14\uB9BF \uCEE8\uD14C\uC774\uB108
+gb.heapMaximum = \uCD5C\uB300 \uD799
+gb.heapAllocated = \uD560\uB2F9\uB41C \uD799
+gb.heapUsed = \uC0AC\uC6A9\uB41C \uD799
+gb.free = \uD504\uB9AC
+gb.version = \uBC84\uC804
+gb.releaseDate = \uB9B4\uB9AC\uC988 \uB0A0\uC9DC
gb.date = date
gb.activity = \uc561\ud2f0\ube44\ud2f0
gb.subscribe = \uad6c\ub3c5
@@ -218,7 +217,8 @@ gb.pages = \ud398\uc774\uc9c0
gb.workingCopy = \uc6cc\ud0b9 \uce74\ud53c
gb.workingCopyWarning = \uc774 \uc800\uc7a5\uc18c\ub294 \uc6cc\ud0b9\uce74\ud53c\ub97c \uac00\uc9c0\uace0 \uc788\uace0 \ud478\uc2dc\ub97c \ubc1b\uc744 \uc218 \uc5c6\uc74c
gb.query = \ucffc\ub9ac
-gb.queryHelp = \ud45c\uc900 \ucffc\ub9ac \ubb38\ubc95\uc744 \uc9c0\uc6d0.<p/><p/>\uc790\uc138\ud55c \uac83\uc744 \uc6d0\ud55c\ub2e4\uba74 <a target="_new" href="http://lucene.apache.org/core/old_versioned_docs/versions/3_5_0/queryparsersyntax.html">\ub8e8\uc2e0 \ucffc\ub9ac \ud30c\uc11c \ubb38\ubc95</a> \uc744 \ubc29\ubb38\ud574 \uc8fc\uc138\uc694.
+gb.queryHelp = \ud45c\uc900 \ucffc\ub9ac \ubb38\ubc95\uc744 \uc9c0\uc6d0.<p/><p/>\uc790\uc138\ud55c \uac83\uc744 \uc6d0\ud55c\ub2e4\uba74 ${querySyntax} \uc744 \ubc29\ubb38\ud574 \uc8fc\uc138\uc694.
+gb.querySyntax = \ub8e8\uc2e0 \ucffc\ub9ac \ud30c\uc11c \ubb38\ubc95
gb.queryResults = \uac80\uc0c9\uacb0\uacfc {0} - {1} ({2}\uac1c \uac80\uc0c9\ub428)
gb.noHits = \uac80\uc0c9 \uacb0\uacfc \uc5c6\uc74c
gb.authored = \uac00 \uc791\uc131\ud568.
@@ -285,460 +285,504 @@ gb.errorAdministrationDisabled = \uad00\ub9ac\uae30\ub2a5 \ube44\ud65c\uc131\ud6
gb.lastNDays = {0} \uc77c\uc804
gb.completeGravatarProfile = Gravatar.com \uc5d0 \ud504\ub85c\ud30c\uc77c \uc0dd\uc131\ub428
gb.none = none
-gb.line = \ub77c\uc778
-gb.content = \ub0b4\uc6a9
+gb.line = \uB77C\uC778
+gb.content = \uB0B4\uC6A9
gb.empty = empty
-gb.inherited = \uc0c1\uc18d
-gb.deleteRepository = \uc800\uc7a5\uc18c \"{0}\" \ub97c \uc0ad\uc81c\ud560\uae4c\uc694?
-gb.repositoryDeleted = \uc800\uc7a5\uc18c ''{0}'' \uc0ad\uc81c\ub428.
-gb.repositoryDeleteFailed = \uc800\uc7a5\uc18c ''{0}'' \uc0ad\uc81c \uc2e4\ud328!
-gb.deleteUser = \uc0ac\uc6a9\uc790 \"{0}\"\ub97c \uc0ad\uc81c\ud560\uae4c\uc694?
-gb.userDeleted = \uc0ac\uc6a9\uc790 ''{0}'' \uc0ad\uc81c\ub428.
-gb.userDeleteFailed = \uc0ac\uc6a9\uc790 ''{0}'' \uc0ad\uc81c \uc2e4\ud328!
-gb.time.justNow = \uc9c0\uae08
-gb.time.today = \uc624\ub298
-gb.time.yesterday = \uc5b4\uc81c
-gb.time.minsAgo = {0}\ubd84 \uc804
-gb.time.hoursAgo = {0}\uc2dc\uac04 \uc804
-gb.time.daysAgo = {0}\uc77c \uc804
-gb.time.weeksAgo = {0}\uc8fc \uc804
-gb.time.monthsAgo = {0}\ub2ec \uc804
-gb.time.oneYearAgo = 1\ub144 \uc804
-gb.time.yearsAgo = {0}\ub144 \uc804
-gb.duration.oneDay = 1\uc77c
-gb.duration.days = {0}\uc77c
-gb.duration.oneMonth = 1\uac1c\uc6d4
-gb.duration.months = {0}\uac1c\uc6d4
-gb.duration.oneYear = 1\ub144
-gb.duration.years = {0}\ub144
-gb.authorizationControl = \uc778\uc99d \uc81c\uc5b4
-gb.allowAuthenticatedDescription = \ubaa8\ub4e0 \uc778\uc99d\ub41c \uc720\uc800\uc5d0\uac8c \uad8c\ud55c \ubd80\uc5ec
-gb.allowNamedDescription = \uc774\ub984\uc73c\ub85c \uc720\uc800\ub098 \ud300\uc5d0\uac8c \uad8c\ud55c \ubd80\uc5ec
-gb.markdownFailure = \ub9c8\ud06c\ub2e4\uc6b4 \ucee8\ud150\uce20 \ud30c\uc2f1 \uc624\ub958!
-gb.clearCache = \uce90\uc2dc \uc9c0\uc6b0\uae30
-gb.projects = \ud504\ub85c\uc81d\ud2b8
-gb.project = \ud504\ub85c\uc81d\ud2b8
-gb.allProjects = \ubaa8\ub4e0 \ud504\ub85c\uc81d\ud2b8
-gb.copyToClipboard = \ud074\ub9bd\ubcf4\ub4dc\uc5d0 \ubcf5\uc0ac
-gb.fork = \ud3ec\ud06c
-gb.forks = \ud3ec\ud06c
-gb.forkRepository = {0}\ub97c \ud3ec\ud06c\ud560\uae4c\uc694?
-gb.repositoryForked = {0} \ud3ec\ud06c\ub428
-gb.repositoryForkFailed= \ud3ec\ud06c\uc2e4\ud328
-gb.personalRepositories = \uac1c\uc778 \uc800\uc7a5\uc18c
-gb.allowForks = \ud3ec\ud06c \ud5c8\uc6a9
-gb.allowForksDescription = \uc774 \uc800\uc7a5\uc18c\ub97c \uc778\uc99d\ub41c \uc720\uc800\uc5d0\uac70 \ud3ec\ud06c \ud5c8\uc6a9
-gb.forkedFrom = \ub85c\ubd80\ud130 \ud3ec\ud06c\ub428
-gb.canFork = \ud3ec\ud06c \uac00\ub2a5
-gb.canForkDescription = \ud5c8\uc6a9\ub41c \uc800\uc7a5\uc18c\ub97c \uac1c\uc778 \uc800\uc7a5\uc18c\uc5d0 \ud3ec\ud06c\ud560 \uc218 \uc788\uc74c
-gb.myFork = \ub0b4 \ud3ec\ud06c \ubcf4\uae30
-gb.forksProhibited = \ud3ec\ud06c \ucc28\ub2e8\ub428
-gb.forksProhibitedWarning = \uc774 \uc800\uc7a5\uc18c\ub294 \ud3ec\ud06c \ucc28\ub2e8\ub418\uc5b4 \uc788\uc74c
-gb.noForks = {0} \ub294 \ud3ec\ud06c \uc5c6\uc74c
-gb.forkNotAuthorized = \uc8c4\uc1a1\ud569\ub2c8\ub2e4. {0} \ud3ec\ud06c\uc5d0 \uc811\uc18d\uc774 \uc778\uc99d\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.
-gb.forkInProgress = \ud504\ud06c \uc9c4\ud589 \uc911
-gb.preparingFork = \ud3ec\ud06c \uc900\ube44 \uc911...
-gb.isFork = \ud3ec\ud06c\ud55c
-gb.canCreate = \uc0dd\uc131 \uac00\ub2a5
-gb.canCreateDescription = \uac1c\uc778 \uc800\uc7a5\uc18c\ub97c \ub9cc\ub4e4 \uc218 \uc788\uc74c
-gb.illegalPersonalRepositoryLocation = \uac1c\uc778 \uc800\uc7a5\uc18c\ub294 \ubc18\ub4dc\uc2dc \"{0}\" \uc5d0 \uc704\uce58\ud574\uc57c \ud569\ub2c8\ub2e4.
-gb.verifyCommitter = \ucee4\ubbf8\ud130 \ud655\uc778
-gb.verifyCommitterDescription = \ucee4\ubbf8\ud130 ID \ub294 Gitblit ID \uc640 \ub9e4\uce58\ub418\uc5b4\uc57c \ud568
-gb.verifyCommitterNote = \ubaa8\ub4e0 \uba38\uc9c0\ub294 \ucee4\ubbf8\ud130 ID \ub97c \uc801\uc6a9\ud558\uae30 \uc704\ud574 "--no-ff" \uc635\uc158 \ud544\uc694
-gb.repositoryPermissions = \uc800\uc7a5\uc18c \uad8c\ud55c
-gb.userPermissions = \uc720\uc800 \uad8c\ud55c
-gb.teamPermissions = \ud300 \uad8c\ud55c
-gb.add = \ucd94\uac00
-gb.noPermission = \uc774 \uad8c\ud55c \uc0ad\uc81c
-gb.excludePermission = {0} (\uc81c\uc678)
-gb.viewPermission = {0} (\ubcf4\uae30)
-gb.clonePermission = {0} (\ud074\ub860)
-gb.pushPermission = {0} (\ud478\uc2dc)
-gb.createPermission = {0} (\ud478\uc2dc, ref \uc0dd\uc131)
-gb.deletePermission = {0} (\ud478\uc2dc, ref \uc0dd\uc131+\uc0ad\uc81c)
-gb.rewindPermission = {0} (\ud478\uc2dc, ref \uc0dd\uc131+\uc0ad\uc81c+\ub418\ub3cc\ub9ac\uae30)
-gb.permission = \uad8c\ud55c
-gb.regexPermission = \uc774 \uad8c\ud55c\uc740 \uc815\uaddc\uc2dd \"{0}\" \ub85c\ubd80\ud130 \uc124\uc815\ub428
-gb.accessDenied = \uc811\uc18d \uac70\ubd80
-gb.busyCollectingGarbage = \uc8c4\uc1a1\ud569\ub2c8\ub2e4. Gitblit \uc740 \uac00\ube44\uc9c0 \uceec\ub809\uc158 \uc911\uc785\ub2c8\ub2e4. {0}
-gb.gcPeriod = GC \uc8fc\uae30
-gb.gcPeriodDescription = \uac00\ube44\uc9c0 \ud074\ub809\uc158\uac04\uc758 \uc2dc\uac04 \uac04\uaca9
-gb.gcThreshold = GC \uae30\uc900\uc810
-gb.gcThresholdDescription = \uc870\uae30 \uac00\ube44\uc9c0 \uceec\ub809\uc158\uc744 \ubc1c\uc0dd\uc2dc\ud0a4\uae30 \uc704\ud55c \uc624\ube0c\uc81d\ud2b8\ub4e4\uc758 \ucd5c\uc18c \uc804\uccb4 \ud06c\uae30
-gb.ownerPermission = \uc800\uc7a5\uc18c \uc624\ub108
-gb.administrator = \uad00\ub9ac\uc790
-gb.administratorPermission = Gitblit \uad00\ub9ac\uc790
-gb.team = \ud300
-gb.teamPermission = \"{0}\" \ud300 \uba64\ubc84\uc5d0 \uad8c\ud55c \uc124\uc815\ub428
-gb.missing = \ub204\ub77d!
-gb.missingPermission = \uc774 \uad8c\ud55c\uc744 \uc704\ud55c \uc800\uc7a5\uc18c \ub204\ub77d!
-gb.mutable = \uac00\ubcc0
-gb.specified = \uc9c0\uc815\ub41c
-gb.effective = \ud6a8\uacfc\uc801
-gb.organizationalUnit = \uc870\uc9c1
-gb.organization = \uae30\uad00
-gb.locality = \uc704\uce58
-gb.stateProvince = \ub3c4 \ub610\ub294 \uc8fc
-gb.countryCode = \uad6d\uac00\ucf54\ub4dc
-gb.properties = \uc18d\uc131
-gb.issued = \ubc1c\uae09\ub428
-gb.expires = \ub9cc\ub8cc
-gb.expired = \ub9cc\ub8cc\ub428
-gb.expiring = \ub9cc\ub8cc\uc911
-gb.revoked = \ud3d0\uae30\ub428
-gb.serialNumber = \uc77c\ub828\ubc88\ud638
-gb.certificates = \uc778\uc99d\uc11c
-gb.newCertificate = \uc0c8 \uc778\uc99d\uc11c
-gb.revokeCertificate = \uc778\uc99d\uc11c \ud3d0\uae30
-gb.sendEmail = \uba54\uc77c \ubcf4\ub0b4\uae30
-gb.passwordHint = \ud328\uc2a4\uc6cc\ub4dc \ud78c\ud2b8
+gb.inherited = \uC0C1\uC18D
+gb.deleteRepository = \uC800\uC7A5\uC18C \"{0}\" \uB97C \uC0AD\uC81C\uD560\uAE4C\uC694?
+gb.repositoryDeleted = \uC800\uC7A5\uC18C ''{0}'' \uC0AD\uC81C\uB428.
+gb.repositoryDeleteFailed = \uC800\uC7A5\uC18C ''{0}'' \uC0AD\uC81C \uC2E4\uD328!
+gb.deleteUser = \uC0AC\uC6A9\uC790 \"{0}\"\uB97C \uC0AD\uC81C\uD560\uAE4C\uC694?
+gb.userDeleted = \uC0AC\uC6A9\uC790 ''{0}'' \uC0AD\uC81C\uB428.
+gb.userDeleteFailed = \uC0AC\uC6A9\uC790 ''{0}'' \uC0AD\uC81C \uC2E4\uD328!
+gb.time.justNow = \uC9C0\uAE08
+gb.time.today = \uC624\uB298
+gb.time.yesterday = \uC5B4\uC81C
+gb.time.minsAgo = {0}\uBD84 \uC804
+gb.time.hoursAgo = {0}\uC2DC\uAC04 \uC804
+gb.time.daysAgo = {0}\uC77C \uC804
+gb.time.weeksAgo = {0}\uC8FC \uC804
+gb.time.monthsAgo = {0}\uB2EC \uC804
+gb.time.oneYearAgo = 1\uB144 \uC804
+gb.time.yearsAgo = {0}\uB144 \uC804
+gb.duration.oneDay = 1\uC77C
+gb.duration.days = {0}\uC77C
+gb.duration.oneMonth = 1\uAC1C\uC6D4
+gb.duration.months = {0}\uAC1C\uC6D4
+gb.duration.oneYear = 1\uB144
+gb.duration.years = {0}\uB144
+gb.authorizationControl = \uC778\uC99D \uC81C\uC5B4
+gb.allowAuthenticatedDescription = \uBAA8\uB4E0 \uC778\uC99D\uB41C \uC720\uC800\uC5D0\uAC8C \uAD8C\uD55C \uBD80\uC5EC
+gb.allowNamedDescription = \uC774\uB984\uC73C\uB85C \uC720\uC800\uB098 \uD300\uC5D0\uAC8C \uAD8C\uD55C \uBD80\uC5EC
+gb.markdownFailure = \uB9C8\uD06C\uB2E4\uC6B4 \uCEE8\uD150\uCE20 \uD30C\uC2F1 \uC624\uB958!
+gb.clearCache = \uCE90\uC2DC \uC9C0\uC6B0\uAE30
+gb.projects = \uD504\uB85C\uC81D\uD2B8
+gb.project = \uD504\uB85C\uC81D\uD2B8
+gb.allProjects = \uBAA8\uB4E0 \uD504\uB85C\uC81D\uD2B8
+gb.copyToClipboard = \uD074\uB9BD\uBCF4\uB4DC\uC5D0 \uBCF5\uC0AC
+gb.fork = \uD3EC\uD06C
+gb.forks = \uD3EC\uD06C
+gb.forkRepository = {0}\uB97C \uD3EC\uD06C\uD560\uAE4C\uC694?
+gb.repositoryForked = {0} \uD3EC\uD06C\uB428
+gb.repositoryForkFailed= \uD3EC\uD06C\uC2E4\uD328
+gb.personalRepositories = \uAC1C\uC778 \uC800\uC7A5\uC18C
+gb.allowForks = \uD3EC\uD06C \uD5C8\uC6A9
+gb.allowForksDescription = \uC774 \uC800\uC7A5\uC18C\uB97C \uC778\uC99D\uB41C \uC720\uC800\uC5D0\uAC70 \uD3EC\uD06C \uD5C8\uC6A9
+gb.forkedFrom = \uB85C\uBD80\uD130 \uD3EC\uD06C\uB428
+gb.canFork = \uD3EC\uD06C \uAC00\uB2A5
+gb.canForkDescription = \uD5C8\uC6A9\uB41C \uC800\uC7A5\uC18C\uB97C \uAC1C\uC778 \uC800\uC7A5\uC18C\uC5D0 \uD3EC\uD06C\uD560 \uC218 \uC788\uC74C
+gb.myFork = \uB0B4 \uD3EC\uD06C \uBCF4\uAE30
+gb.forksProhibited = \uD3EC\uD06C \uCC28\uB2E8\uB428
+gb.forksProhibitedWarning = \uC774 \uC800\uC7A5\uC18C\uB294 \uD3EC\uD06C \uCC28\uB2E8\uB418\uC5B4 \uC788\uC74C
+gb.noForks = {0} \uB294 \uD3EC\uD06C \uC5C6\uC74C
+gb.forkNotAuthorized = \uC8C4\uC1A1\uD569\uB2C8\uB2E4. {0} \uD3EC\uD06C\uC5D0 \uC811\uC18D\uC774 \uC778\uC99D\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.
+gb.forkInProgress = \uD504\uD06C \uC9C4\uD589 \uC911
+gb.preparingFork = \uD3EC\uD06C \uC900\uBE44 \uC911...
+gb.isFork = \uD3EC\uD06C\uD55C
+gb.canCreate = \uC0DD\uC131 \uAC00\uB2A5
+gb.canCreateDescription = \uAC1C\uC778 \uC800\uC7A5\uC18C\uB97C \uB9CC\uB4E4 \uC218 \uC788\uC74C
+gb.illegalPersonalRepositoryLocation = \uAC1C\uC778 \uC800\uC7A5\uC18C\uB294 \uBC18\uB4DC\uC2DC \"{0}\" \uC5D0 \uC704\uCE58\uD574\uC57C \uD569\uB2C8\uB2E4.
+gb.verifyCommitter = \uCEE4\uBBF8\uD130 \uD655\uC778
+gb.verifyCommitterDescription = \uCEE4\uBBF8\uD130 ID \uB294 Gitblit ID \uC640 \uB9E4\uCE58\uB418\uC5B4\uC57C \uD568
+gb.verifyCommitterNote = \uBAA8\uB4E0 \uBA38\uC9C0\uB294 \uCEE4\uBBF8\uD130 ID \uB97C \uC801\uC6A9\uD558\uAE30 \uC704\uD574 "--no-ff" \uC635\uC158 \uD544\uC694
+gb.repositoryPermissions = \uC800\uC7A5\uC18C \uAD8C\uD55C
+gb.userPermissions = \uC720\uC800 \uAD8C\uD55C
+gb.teamPermissions = \uD300 \uAD8C\uD55C
+gb.add = \uCD94\uAC00
+gb.noPermission = \uC774 \uAD8C\uD55C \uC0AD\uC81C
+gb.excludePermission = {0} (\uC81C\uC678)
+gb.viewPermission = {0} (\uBCF4\uAE30)
+gb.clonePermission = {0} (\uD074\uB860)
+gb.pushPermission = {0} (\uD478\uC2DC)
+gb.createPermission = {0} (\uD478\uC2DC, ref \uC0DD\uC131)
+gb.deletePermission = {0} (\uD478\uC2DC, ref \uC0DD\uC131+\uC0AD\uC81C)
+gb.rewindPermission = {0} (\uD478\uC2DC, ref \uC0DD\uC131+\uC0AD\uC81C+\uB418\uB3CC\uB9AC\uAE30)
+gb.permission = \uAD8C\uD55C
+gb.regexPermission = \uC774 \uAD8C\uD55C\uC740 \uC815\uADDC\uC2DD \"{0}\" \uB85C\uBD80\uD130 \uC124\uC815\uB428
+gb.accessDenied = \uC811\uC18D \uAC70\uBD80
+gb.busyCollectingGarbage = \uC8C4\uC1A1\uD569\uB2C8\uB2E4. Gitblit \uC740 \uAC00\uBE44\uC9C0 \uCEEC\uB809\uC158 \uC911\uC785\uB2C8\uB2E4. {0}
+gb.gcPeriod = GC \uC8FC\uAE30
+gb.gcPeriodDescription = \uAC00\uBE44\uC9C0 \uD074\uB809\uC158\uAC04\uC758 \uC2DC\uAC04 \uAC04\uACA9
+gb.gcThreshold = GC \uAE30\uC900\uC810
+gb.gcThresholdDescription = \uC870\uAE30 \uAC00\uBE44\uC9C0 \uCEEC\uB809\uC158\uC744 \uBC1C\uC0DD\uC2DC\uD0A4\uAE30 \uC704\uD55C \uC624\uBE0C\uC81D\uD2B8\uB4E4\uC758 \uCD5C\uC18C \uC804\uCCB4 \uD06C\uAE30
+gb.ownerPermission = \uC800\uC7A5\uC18C \uC624\uB108
+gb.administrator = \uAD00\uB9AC\uC790
+gb.administratorPermission = Gitblit \uAD00\uB9AC\uC790
+gb.team = \uD300
+gb.teamPermission = \"{0}\" \uD300 \uBA64\uBC84\uC5D0 \uAD8C\uD55C \uC124\uC815\uB428
+gb.missing = \uB204\uB77D!
+gb.missingPermission = \uC774 \uAD8C\uD55C\uC744 \uC704\uD55C \uC800\uC7A5\uC18C \uB204\uB77D!
+gb.mutable = \uAC00\uBCC0
+gb.specified = \uC9C0\uC815\uB41C
+gb.effective = \uD6A8\uACFC\uC801
+gb.organizationalUnit = \uC870\uC9C1
+gb.organization = \uAE30\uAD00
+gb.locality = \uC704\uCE58
+gb.stateProvince = \uB3C4 \uB610\uB294 \uC8FC
+gb.countryCode = \uAD6D\uAC00\uCF54\uB4DC
+gb.properties = \uC18D\uC131
+gb.issued = \uBC1C\uAE09\uB428
+gb.expires = \uB9CC\uB8CC
+gb.expired = \uB9CC\uB8CC\uB428
+gb.expiring = \uB9CC\uB8CC\uC911
+gb.revoked = \uD3D0\uAE30\uB428
+gb.serialNumber = \uC77C\uB828\uBC88\uD638
+gb.certificates = \uC778\uC99D\uC11C
+gb.newCertificate = \uC0C8 \uC778\uC99D\uC11C
+gb.revokeCertificate = \uC778\uC99D\uC11C \uD3D0\uAE30
+gb.sendEmail = \uBA54\uC77C \uBCF4\uB0B4\uAE30
+gb.passwordHint = \uD328\uC2A4\uC6CC\uB4DC \uD78C\uD2B8
gb.ok = ok
-gb.invalidExpirationDate = \ub9d0\ub8cc\uc77c\uc790 \uc624\ub958!
-gb.passwordHintRequired = \ud328\uc2a4\uc6cc\ub4dc \ud78c\ud2b8 \ud544\uc218!
-gb.viewCertificate = \uc778\uc99d\uc11c \ubcf4\uae30
-gb.subject = \uc774\ub984
-gb.issuer = \ubc1c\uae09\uc790
-gb.validFrom = \uc720\ud6a8\uae30\uac04 (\uc2dc\uc791)
-gb.validUntil = \uc720\ud6a8\uae30\uac04 (\ub05d)
-gb.publicKey = \uacf5\uac1c\ud0a4
-gb.signatureAlgorithm = \uc11c\uba85 \uc54c\uace0\ub9ac\uc998
-gb.sha1FingerPrint = SHA-1 \uc9c0\ubb38 \uc54c\uace0\ub9ac\uc998
-gb.md5FingerPrint = MD5 \uc9c0\ubb38 \uc54c\uace0\ub9ac\uc998
-gb.reason = \uc774\uc720
-gb.revokeCertificateReason = \uc778\uc99d\uc11c \ud574\uc9c0\uc774\uc720\ub97c \uc120\ud0dd\ud558\uc138\uc694
-gb.unspecified = \ud45c\uc2dc\ud558\uc9c0 \uc54a\uc74c
-gb.keyCompromise = \ud0a4 \uc190\uc0c1
-gb.caCompromise = CA \uc190\uc0c1
-gb.affiliationChanged = \uad00\uacc4 \ubcc0\uacbd\ub428
-gb.superseded = \ub300\uccb4\ub428
-gb.cessationOfOperation = \uc6b4\uc601 \uc911\uc9c0
-gb.privilegeWithdrawn = \uad8c\ud55c \ucca0\ud68c\ub428
-gb.time.inMinutes = {0} \ubd84
-gb.time.inHours = {0} \uc2dc\uac04
-gb.time.inDays = {0} \uc77c
-gb.hostname = \ud638\uc2a4\ud2b8\uba85
-gb.hostnameRequired = \ud638\uc2a4\ud2b8\uba85\uc744 \uc785\ub825\ud558\uc138\uc694
-gb.newSSLCertificate = \uc0c8 \uc11c\ubc84 SSL \uc778\uc99d\uc11c
-gb.newCertificateDefaults = \uc0c8 \uc778\uc99d\uc11c \uae30\ubcf8
-gb.duration = \uae30\uac04
-gb.certificateRevoked = \uc778\uc99d\uc11c {0,number,0} \ub294 \ud3d0\uae30\ub418\uc5c8\uc2b5\ub2c8\ub2e4
-gb.clientCertificateGenerated = {0} \uc744(\ub97c) \uc704\ud55c \uc0c8\ub85c\uc6b4 \ud074\ub77c\uc774\uc5b8\ud2b8 \uc778\uc99d\uc11c \uc0dd\uc131 \uc131\uacf5
-gb.sslCertificateGenerated = {0} \uc744(\ub97c) \uc704\ud55c \uc0c8\ub85c\uc6b4 \uc11c\ubc84 SSL \uc778\uc99d\uc11c \uc0dd\uc131 \uc131\uacf5
-gb.newClientCertificateMessage = \ub178\ud2b8:\n'\ud328\uc2a4\uc6cc\ub4dc' \ub294 \uc720\uc800\uc758 \ud328\uc2a4\uc6cc\ub4dc\uac00 \uc544\ub2c8\ub77c \uc720\uc800\uc758 \ud0a4\uc2a4\ud1a0\uc5b4 \ub97c \ubcf4\ud638\ud558\uae30 \uc704\ud55c \uac83\uc785\ub2c8\ub2e4. \uc774 \ud328\uc2a4\uc6cc\ub4dc\ub294 \uc800\uc7a5\ub418\uc9c0 \uc54a\uc73c\ubbc0\ub85c \uc0ac\uc6a9\uc790 README \uc9c0\uce68\uc5d0 \ud3ec\ud568\ub420 '\ud78c\ud2b8' \ub97c \ubc18\ub4dc\uc2dc \uc785\ub825\ud574\uc57c \ud569\ub2c8\ub2e4.
-gb.certificate = \uc778\uc99d\uc11c
-gb.emailCertificateBundle = \uc774\uba54\uc77c \ud074\ub77c\uc774\uc5b8\ud2b8 \uc778\uc99d\uc11c \ubc88\ub4e4
-gb.pleaseGenerateClientCertificate = {0} \uc744(\ub97c) \uc704\ud55c \ud074\ub77c\uc774\uc5b8\ud2b8 \uc778\uc99d\uc11c\ub97c \uc0dd\uc131\ud558\uc138\uc694
-gb.clientCertificateBundleSent = {0} \uc744(\ub97c) \uc704\ud55c \ud074\ub77c\uc774\uc5b8\ud2b8 \uc778\uc99d\uc11c \ubc88\ub4e4 \ubc1c\uc1a1\ub428
-gb.enterKeystorePassword = Gitblit \ud0a4\uc2a4\ud1a0\uc5b4 \ud328\uc2a4\uc6cc\ub4dc\ub97c \uc785\ub825\ud558\uc138\uc694
-gb.warning = \uacbd\uace0
-gb.jceWarning = \uc790\ubc14 \uc2e4\ud589\ud658\uacbd\uc5d0 \"JCE Unlimited Strength Jurisdiction Policy\" \ud30c\uc77c\uc774 \uc5c6\uc2b5\ub2c8\ub2e4.\n\uc774\uac83\uc740 \ud0a4\uc800\uc7a5\uc18c \uc554\ud638\ud654\uc5d0 \uc0ac\uc6a9\ub418\ub294 \ud328\uc2a4\uc6cc\ub4dc\uc758 \uae38\uc774\ub294 7\uc790\ub85c \uc81c\ud55c\ud569\ub2c8\ub2e4.\n\uc774 \uc815\ucc45 \ud30c\uc77c\uc740 Oracle \uc5d0\uc11c \uc120\ud0dd\uc801\uc73c\ub85c \ub2e4\uc6b4\ub85c\ub4dc\ud574\uc57c \ud569\ub2c8\ub2e4.\n\n\ubb34\uc2dc\ud558\uace0 \uc778\uc99d\uc11c \uc778\ud504\ub77c\ub97c \uc0dd\uc131\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?\n\n\uc544\ub2c8\uc624(No) \ub77c\uace0 \ub2f5\ud558\uba74 \uc815\ucc45\ud30c\uc77c\uc744 \ub2e4\uc6b4\ubc1b\uc744 \uc218 \uc788\ub294 Oracle \ub2e4\uc6b4\ub85c\ub4dc \ud398\uc774\uc9c0\ub97c \ube0c\ub77c\uc6b0\uc800\ub85c \uc548\ub0b4\ud560 \uac83\uc785\ub2c8\ub2e4.
-gb.maxActivityCommits = \ucd5c\ub300 \uc561\ud2f0\ube44\ud2f0 \ucee4\ubc0b
-gb.maxActivityCommitsDescription = \uc561\ud2f0\ube44\ud2f0 \ud398\uc774\uc9c0\uc5d0 \ud45c\uc2dc\ud560 \ucd5c\ub300 \ucee4\ubc0b \uc218
-gb.noMaximum = \ubb34\uc81c\ud55c
-gb.attributes = \uc18d\uc131
-gb.serveCertificate = \uc774 \uc778\uc99d\uc11c\ub85c https \uc81c\uacf5
-gb.sslCertificateGeneratedRestart = {0} \uc744(\ub97c) \uc704\ud55c \uc0c8 \uc11c\ubc84 SSL \uc778\uc99d\uc11c\ub97c \uc131\uacf5\uc801\uc73c\ub85c \uc0dd\uc131\ud558\uc600\uc2b5\ub2c8\ub2e4. \n\uc0c8 \uc778\uc99d\uc11c\ub97c \uc0ac\uc6a9\ud558\ub824\uba74 Gitblit \uc744 \uc7ac\uc2dc\uc791 \ud574\uc57c \ud569\ub2c8\ub2e4.\n\n'--alias' \ud30c\ub77c\ubbf8\ud130\ub85c \uc2e4\ud589\ud55c\ub2e4\uba74 ''--alias {0}'' \ub85c \uc124\uc815\ud574\uc57c \ud569\ub2c8\ub2e4.
-gb.validity = \uc720\ud6a8\uc131
-gb.siteName = \uc0ac\uc774\ud2b8 \uc774\ub984
-gb.siteNameDescription = \uc11c\ubc84\uc758 \uc9e6\uc740 \uc124\uba85\uc774 \ud3ec\ud568\ub41c \uc774\ub984
-gb.excludeFromActivity = \uc561\ud2f0\ube44\ud2f0 \ud398\uc774\uc9c0\uc5d0\uc11c \uc81c\uc678
-gb.isSparkleshared = \uc800\uc7a5\uc18c\ub294 Sparkleshare \ub428
-gb.owners = \uc624\ub108
-gb.sessionEnded = \uc138\uc158\uc774 \uc885\ub8cc\ub428
-gb.closeBrowser = \uc815\ud655\ud788 \uc138\uc158\uc744 \uc885\ub8cc\ud558\ub824\uba74 \ube0c\ub77c\uc6b0\uc800\ub97c \ub2eb\uc544 \uc8fc\uc138\uc694.
-gb.doesNotExistInTree = {1} \ud2b8\ub9ac\uc5d0 {0} \uac00 \uc5c6\uc74c
-gb.enableIncrementalPushTags = \uc99d\uac00\ud558\ub294 \ud478\uc2dc \ud0dc\uadf8 \uac00\ub2a5
-gb.useIncrementalPushTagsDescription = \ud478\uc2dc\ud560 \ub54c, \uc99d\uac00\ud558\ub294 \ub9ac\ube44\uc804 \ubc88\ud638\uac00 \uc790\ub3d9\uc73c\ub85c \ud0dc\uadf8\ub428
-gb.incrementalPushTagMessage = \ud478\uc2dc\ud560 \ub54c [{0}] \ube0c\ub79c\uce58\uc5d0 \uc790\ub3d9\uc73c\ub85c \ud0dc\uadf8\ub428
-gb.externalPermissions = {0} \uc811\uc18d \uad8c\ud55c\uc740 \uc678\ubd80\uc5d0\uc11c \uad00\ub9ac\ub428
-gb.viewAccess = Gitblit \uc5d0 \uc77d\uae30 \ub610\ub294 \uc4f0\uae30 \uad8c\ud55c\uc774 \uc5c6\uc74c
-gb.overview = \uac1c\uc694
-gb.dashboard = \ub300\uc2dc\ubcf4\ub4dc
-gb.monthlyActivity = \uc6d4\ubcc4 \uc561\ud2f0\ube44
-gb.myProfile = \ub0b4 \ud504\ub85c\ud544
-gb.compare = \ube44\uad50
-gb.manual = \uc124\uba85\uc11c
+gb.invalidExpirationDate = \uB9D0\uB8CC\uC77C\uC790 \uC624\uB958!
+gb.passwordHintRequired = \uD328\uC2A4\uC6CC\uB4DC \uD78C\uD2B8 \uD544\uC218!
+gb.viewCertificate = \uC778\uC99D\uC11C \uBCF4\uAE30
+gb.subject = \uC774\uB984
+gb.issuer = \uBC1C\uAE09\uC790
+gb.validFrom = \uC720\uD6A8\uAE30\uAC04 (\uC2DC\uC791)
+gb.validUntil = \uC720\uD6A8\uAE30\uAC04 (\uB05D)
+gb.publicKey = \uACF5\uAC1C\uD0A4
+gb.signatureAlgorithm = \uC11C\uBA85 \uC54C\uACE0\uB9AC\uC998
+gb.sha1FingerPrint = SHA-1 \uC9C0\uBB38 \uC54C\uACE0\uB9AC\uC998
+gb.md5FingerPrint = MD5 \uC9C0\uBB38 \uC54C\uACE0\uB9AC\uC998
+gb.reason = \uC774\uC720
+gb.revokeCertificateReason = \uC778\uC99D\uC11C \uD574\uC9C0\uC774\uC720\uB97C \uC120\uD0DD\uD558\uC138\uC694
+gb.unspecified = \uD45C\uC2DC\uD558\uC9C0 \uC54A\uC74C
+gb.keyCompromise = \uD0A4 \uC190\uC0C1
+gb.caCompromise = CA \uC190\uC0C1
+gb.affiliationChanged = \uAD00\uACC4 \uBCC0\uACBD\uB428
+gb.superseded = \uB300\uCCB4\uB428
+gb.cessationOfOperation = \uC6B4\uC601 \uC911\uC9C0
+gb.privilegeWithdrawn = \uAD8C\uD55C \uCCA0\uD68C\uB428
+gb.time.inMinutes = {0} \uBD84
+gb.time.inHours = {0} \uC2DC\uAC04
+gb.time.inDays = {0} \uC77C
+gb.hostname = \uD638\uC2A4\uD2B8\uBA85
+gb.hostnameRequired = \uD638\uC2A4\uD2B8\uBA85\uC744 \uC785\uB825\uD558\uC138\uC694
+gb.newSSLCertificate = \uC0C8 \uC11C\uBC84 SSL \uC778\uC99D\uC11C
+gb.newCertificateDefaults = \uC0C8 \uC778\uC99D\uC11C \uAE30\uBCF8
+gb.duration = \uAE30\uAC04
+gb.certificateRevoked = \uC778\uC99D\uC11C {0,number,0} \uB294 \uD3D0\uAE30\uB418\uC5C8\uC2B5\uB2C8\uB2E4
+gb.clientCertificateGenerated = {0} \uC744(\uB97C) \uC704\uD55C \uC0C8\uB85C\uC6B4 \uD074\uB77C\uC774\uC5B8\uD2B8 \uC778\uC99D\uC11C \uC0DD\uC131 \uC131\uACF5
+gb.sslCertificateGenerated = {0} \uC744(\uB97C) \uC704\uD55C \uC0C8\uB85C\uC6B4 \uC11C\uBC84 SSL \uC778\uC99D\uC11C \uC0DD\uC131 \uC131\uACF5
+gb.newClientCertificateMessage = \uB178\uD2B8:\n'\uD328\uC2A4\uC6CC\uB4DC' \uB294 \uC720\uC800\uC758 \uD328\uC2A4\uC6CC\uB4DC\uAC00 \uC544\uB2C8\uB77C \uC720\uC800\uC758 \uD0A4\uC2A4\uD1A0\uC5B4 \uB97C \uBCF4\uD638\uD558\uAE30 \uC704\uD55C \uAC83\uC785\uB2C8\uB2E4. \uC774 \uD328\uC2A4\uC6CC\uB4DC\uB294 \uC800\uC7A5\uB418\uC9C0 \uC54A\uC73C\uBBC0\uB85C \uC0AC\uC6A9\uC790 README \uC9C0\uCE68\uC5D0 \uD3EC\uD568\uB420 '\uD78C\uD2B8' \uB97C \uBC18\uB4DC\uC2DC \uC785\uB825\uD574\uC57C \uD569\uB2C8\uB2E4.
+gb.certificate = \uC778\uC99D\uC11C
+gb.emailCertificateBundle = \uC774\uBA54\uC77C \uD074\uB77C\uC774\uC5B8\uD2B8 \uC778\uC99D\uC11C \uBC88\uB4E4
+gb.pleaseGenerateClientCertificate = {0} \uC744(\uB97C) \uC704\uD55C \uD074\uB77C\uC774\uC5B8\uD2B8 \uC778\uC99D\uC11C\uB97C \uC0DD\uC131\uD558\uC138\uC694
+gb.clientCertificateBundleSent = {0} \uC744(\uB97C) \uC704\uD55C \uD074\uB77C\uC774\uC5B8\uD2B8 \uC778\uC99D\uC11C \uBC88\uB4E4 \uBC1C\uC1A1\uB428
+gb.enterKeystorePassword = Gitblit \uD0A4\uC2A4\uD1A0\uC5B4 \uD328\uC2A4\uC6CC\uB4DC\uB97C \uC785\uB825\uD558\uC138\uC694
+gb.warning = \uACBD\uACE0
+gb.jceWarning = \uC790\uBC14 \uC2E4\uD589\uD658\uACBD\uC5D0 \"JCE Unlimited Strength Jurisdiction Policy\" \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.\n\uC774\uAC83\uC740 \uD0A4\uC800\uC7A5\uC18C \uC554\uD638\uD654\uC5D0 \uC0AC\uC6A9\uB418\uB294 \uD328\uC2A4\uC6CC\uB4DC\uC758 \uAE38\uC774\uB294 7\uC790\uB85C \uC81C\uD55C\uD569\uB2C8\uB2E4.\n\uC774 \uC815\uCC45 \uD30C\uC77C\uC740 Oracle \uC5D0\uC11C \uC120\uD0DD\uC801\uC73C\uB85C \uB2E4\uC6B4\uB85C\uB4DC\uD574\uC57C \uD569\uB2C8\uB2E4.\n\n\uBB34\uC2DC\uD558\uACE0 \uC778\uC99D\uC11C \uC778\uD504\uB77C\uB97C \uC0DD\uC131\uD558\uC2DC\uACA0\uC2B5\uB2C8\uAE4C?\n\n\uC544\uB2C8\uC624(No) \uB77C\uACE0 \uB2F5\uD558\uBA74 \uC815\uCC45\uD30C\uC77C\uC744 \uB2E4\uC6B4\uBC1B\uC744 \uC218 \uC788\uB294 Oracle \uB2E4\uC6B4\uB85C\uB4DC \uD398\uC774\uC9C0\uB97C \uBE0C\uB77C\uC6B0\uC800\uB85C \uC548\uB0B4\uD560 \uAC83\uC785\uB2C8\uB2E4.
+gb.maxActivityCommits = \uCD5C\uB300 \uC561\uD2F0\uBE44\uD2F0 \uCEE4\uBC0B
+gb.maxActivityCommitsDescription = \uC561\uD2F0\uBE44\uD2F0 \uD398\uC774\uC9C0\uC5D0 \uD45C\uC2DC\uD560 \uCD5C\uB300 \uCEE4\uBC0B \uC218
+gb.noMaximum = \uBB34\uC81C\uD55C
+gb.attributes = \uC18D\uC131
+gb.serveCertificate = \uC774 \uC778\uC99D\uC11C\uB85C https \uC81C\uACF5
+gb.sslCertificateGeneratedRestart = {0} \uC744(\uB97C) \uC704\uD55C \uC0C8 \uC11C\uBC84 SSL \uC778\uC99D\uC11C\uB97C \uC131\uACF5\uC801\uC73C\uB85C \uC0DD\uC131\uD558\uC600\uC2B5\uB2C8\uB2E4. \n\uC0C8 \uC778\uC99D\uC11C\uB97C \uC0AC\uC6A9\uD558\uB824\uBA74 Gitblit \uC744 \uC7AC\uC2DC\uC791 \uD574\uC57C \uD569\uB2C8\uB2E4.\n\n'--alias' \uD30C\uB77C\uBBF8\uD130\uB85C \uC2E4\uD589\uD55C\uB2E4\uBA74 ''--alias {0}'' \uB85C \uC124\uC815\uD574\uC57C \uD569\uB2C8\uB2E4.
+gb.validity = \uC720\uD6A8\uC131
+gb.siteName = \uC0AC\uC774\uD2B8 \uC774\uB984
+gb.siteNameDescription = \uC11C\uBC84\uC758 \uC9E6\uC740 \uC124\uBA85\uC774 \uD3EC\uD568\uB41C \uC774\uB984
+gb.excludeFromActivity = \uC561\uD2F0\uBE44\uD2F0 \uD398\uC774\uC9C0\uC5D0\uC11C \uC81C\uC678
+gb.isSparkleshared = \uC800\uC7A5\uC18C\uB294 Sparkleshare \uB428
+gb.owners = \uC624\uB108
+gb.sessionEnded = \uC138\uC158\uC774 \uC885\uB8CC\uB428
+gb.closeBrowser = \uC815\uD655\uD788 \uC138\uC158\uC744 \uC885\uB8CC\uD558\uB824\uBA74 \uBE0C\uB77C\uC6B0\uC800\uB97C \uB2EB\uC544 \uC8FC\uC138\uC694.
+gb.doesNotExistInTree = {1} \uD2B8\uB9AC\uC5D0 {0} \uAC00 \uC5C6\uC74C
+gb.enableIncrementalPushTags = \uC99D\uAC00\uD558\uB294 \uD478\uC2DC \uD0DC\uADF8 \uAC00\uB2A5
+gb.useIncrementalPushTagsDescription = \uD478\uC2DC\uD560 \uB54C, \uC99D\uAC00\uD558\uB294 \uB9AC\uBE44\uC804 \uBC88\uD638\uAC00 \uC790\uB3D9\uC73C\uB85C \uD0DC\uADF8\uB428
+gb.incrementalPushTagMessage = \uD478\uC2DC\uD560 \uB54C [{0}] \uBE0C\uB79C\uCE58\uC5D0 \uC790\uB3D9\uC73C\uB85C \uD0DC\uADF8\uB428
+gb.externalPermissions = {0} \uC811\uC18D \uAD8C\uD55C\uC740 \uC678\uBD80\uC5D0\uC11C \uAD00\uB9AC\uB428
+gb.viewAccess = Gitblit \uC5D0 \uC77D\uAE30 \uB610\uB294 \uC4F0\uAE30 \uAD8C\uD55C\uC774 \uC5C6\uC74C
+gb.overview = \uAC1C\uC694
+gb.dashboard = \uB300\uC2DC\uBCF4\uB4DC
+gb.monthlyActivity = \uC6D4\uBCC4 \uC561\uD2F0\uBE44
+gb.myProfile = \uB0B4 \uD504\uB85C\uD544
+gb.compare = \uBE44\uAD50
+gb.manual = \uC124\uBA85\uC11C
gb.from = from
gb.to = to
gb.at = at
gb.of = of
gb.in = in
-gb.moreChanges = \ubaa8\ub4e0 \ubcc0\uacbd...
-gb.pushedNCommitsTo = {0} \uac1c \ucee4\ubc0b\uc774 \ud478\uc2dc\ub428
-gb.pushedOneCommitTo = 1 \uac1c \ucee4\ubc0b\uc774 \ud478\uc2dc\ub428
-gb.commitsTo = {0} \uac1c \ucee4\ubc0b
-gb.oneCommitTo = 1 \uac1c \ucee4\ubc0b
-gb.byNAuthors = {0} \uba85\uc758 \uc791\uc131\uc790
-gb.byOneAuthor = {0} \uc5d0 \uc758\ud574
-gb.viewComparison = {0} \ucee4\ubc0b\uc758 \ube44\uad50 \ubcf4\uae30 \u00bb
-gb.nMoreCommits = {0} \uac1c \ub354 \u00bb
-gb.oneMoreCommit = 1 \uac1c \ub354 \u00bb
-gb.pushedNewTag = \uc0c8 \ud0dc\uadf8\uac00 \ud478\uc2dc\ub428
-gb.createdNewTag = \uc0c8 \ud0dc\uadf8\uac00 \uc0dd\uc131\ub428
-gb.deletedTag = \ud0dc\uadf8\uac00 \uc0ad\uc81c\ub428
-gb.pushedNewBranch = \uc0c8 \ube0c\ub79c\uce58\uac00 \ud478\uc2dc\ub428
-gb.createdNewBranch = \uc0c8 \ube0c\ub79c\uce58\uac00 \uc0dd\uc131\ub428
-gb.deletedBranch = \ube0c\ub79c\uce58\uac00 \uc0ad\uc81c\ub428
-gb.createdNewPullRequest = \ud480 \ub9ac\ud018\uc2a4\ud2b8\uac00 \uc0dd\uc131\ub428
-gb.mergedPullRequest = \ud480 \ub9ac\ud018\uc2a4\ud2b8\uac00 \uba38\uc9c0\ub428
+gb.moreChanges = \uBAA8\uB4E0 \uBCC0\uACBD...
+gb.pushedNCommitsTo = {0} \uAC1C \uCEE4\uBC0B\uC774 \uD478\uC2DC\uB428
+gb.pushedOneCommitTo = 1 \uAC1C \uCEE4\uBC0B\uC774 \uD478\uC2DC\uB428
+gb.commitsTo = {0} \uAC1C \uCEE4\uBC0B
+gb.oneCommitTo = 1 \uAC1C \uCEE4\uBC0B
+gb.byNAuthors = {0} \uBA85\uC758 \uC791\uC131\uC790
+gb.byOneAuthor = {0} \uC5D0 \uC758\uD574
+gb.viewComparison = {0} \uCEE4\uBC0B\uC758 \uBE44\uAD50 \uBCF4\uAE30 \u00BB
+gb.nMoreCommits = {0} \uAC1C \uB354 \u00BB
+gb.oneMoreCommit = 1 \uAC1C \uB354 \u00BB
+gb.pushedNewTag = \uC0C8 \uD0DC\uADF8\uAC00 \uD478\uC2DC\uB428
+gb.createdNewTag = \uC0C8 \uD0DC\uADF8\uAC00 \uC0DD\uC131\uB428
+gb.deletedTag = \uD0DC\uADF8\uAC00 \uC0AD\uC81C\uB428
+gb.pushedNewBranch = \uC0C8 \uBE0C\uB79C\uCE58\uAC00 \uD478\uC2DC\uB428
+gb.createdNewBranch = \uC0C8 \uBE0C\uB79C\uCE58\uAC00 \uC0DD\uC131\uB428
+gb.deletedBranch = \uBE0C\uB79C\uCE58\uAC00 \uC0AD\uC81C\uB428
+gb.createdNewPullRequest = \uD480 \uB9AC\uD018\uC2A4\uD2B8\uAC00 \uC0DD\uC131\uB428
+gb.mergedPullRequest = \uD480 \uB9AC\uD018\uC2A4\uD2B8\uAC00 \uBA38\uC9C0\uB428
gb.rewind = REWIND
-gb.star = \ubcc4
-gb.unstar = \ubcc4\uc81c\uac70
-gb.stargazers = \uad00\uce21\uc790
-gb.starredRepositories = \ubcc4 \ud45c\uc2dc\ub41c \uc800\uc7a5\uc18c
-gb.failedToUpdateUser = \uacc4\uc815 \uc5c5\ub370\uc774\ud2b8 \uc2e4\ud328!
-gb.myRepositories = \ub0b4 \uc800\uc7a5\uc18c
-gb.noActivity = \uc9c0\ub09c {0} \uc77c\uac04 \uc561\ud2f0\ube44\ud2f0 \uc5c6\uc74c
-gb.findSomeRepositories = \uc800\uc7a5\uc18c \ucc3e\uae30
-gb.metricAuthorExclusions = \uc791\uc131\uc790 \uba54\ud2b8\ub9ad \uc81c\uc678
-gb.myDashboard = \ub0b4 \ub300\uc2dc\ubcf4\ub4dc
-gb.failedToFindAccount = ''{0}'' \uacc4\uc815 \ucc3e\uae30 \uc2e4\ud328
+gb.star = \uBCC4
+gb.unstar = \uBCC4\uC81C\uAC70
+gb.stargazers = \uAD00\uCE21\uC790
+gb.starredRepositories = \uBCC4 \uD45C\uC2DC\uB41C \uC800\uC7A5\uC18C
+gb.failedToUpdateUser = \uACC4\uC815 \uC5C5\uB370\uC774\uD2B8 \uC2E4\uD328!
+gb.myRepositories = \uB0B4 \uC800\uC7A5\uC18C
+gb.noActivity = \uC9C0\uB09C {0} \uC77C\uAC04 \uC561\uD2F0\uBE44\uD2F0 \uC5C6\uC74C
+gb.findSomeRepositories = \uC800\uC7A5\uC18C \uCC3E\uAE30
+gb.metricAuthorExclusions = \uC791\uC131\uC790 \uBA54\uD2B8\uB9AD \uC81C\uC678
+gb.myDashboard = \uB0B4 \uB300\uC2DC\uBCF4\uB4DC
+gb.failedToFindAccount = ''{0}'' \uACC4\uC815 \uCC3E\uAE30 \uC2E4\uD328
gb.reflog = reflog
-gb.active = \ud65c\uc131
-gb.starred = \ubcc4\ud45c
-gb.owned = \uc18c\uc720\ud568
-gb.starredAndOwned = \ubcc4\ud45c & \uc18c\uc720\ud568
-gb.reviewPatchset = \ub9ac\ubdf0 {0} \ud328\uce58\uc14b {1}
-gb.todaysActivityStats = \uc624\ub298 / {2} \uc791\uc131\uc790\uac00 {1} \ucee4\ubc0b\uc0dd\uc131
-gb.todaysActivityNone = \uc624\ub298 / \uc5c6\uc74c
-gb.noActivityToday = \uc624\ub298\uc740 \uc561\ud2f0\ube44\ud2f0\uac00 \uc5c6\uc74c
-gb.anonymousUser= \uc775\uba85
-gb.commitMessageRenderer = \ucee4\ubc0b \uba54\uc2dc\uc9c0 \ub79c\ub354\ub7ec
-gb.diffStat = {0}\uac1c \ucd94\uac00 & {1}\uac1c \uc0ad\uc81c
-gb.home = \ud648
-gb.isMirror = \ubbf8\ub7ec \uc800\uc7a5\uc18c
-gb.mirrorOf = {0} \uc758 \ubbf8\ub7ec
-gb.mirrorWarning = \uc774 \uc800\uc7a5\uc18c\ub294 \ubbf8\ub7ec\uc774\uace0 \ud478\uc2dc\ub97c \ubc1b\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.
-gb.docsWelcome1 = \uc800\uc7a5\uc18c\ub97c \ubb38\uc11c\ud654\ud558\uae30 \uc704\ud574 \ubb38\uc11c\ub4e4\uc744 \uc0ac\uc6a9\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.
-gb.docsWelcome2 = \uc2dc\uc791\ud558\uae30 \uc704\ud574 README.md \ub610\ub294 HOME.md \ud30c\uc77c\uc744 \ucee4\ubc0b\ud558\uc138\uc694.
-gb.createReadme = README \uc0dd\uc131
-gb.responsible = \ub2f4\ub2f9\uc790
-gb.createdThisTicket = \uc774 \ud2f0\ucf13 \uc0dd\uc131
-gb.proposedThisChange = \uc774 \ubcc0\uacbd \uc81c\uc548
-gb.uploadedPatchsetN = {0} \ud328\uce58\uc14b \uc5c5\ub85c\ub4dc
-gb.uploadedPatchsetNRevisionN = \ub9ac\ube44\uc804 {1} \ud328\uce58\uc14b {0} \uc5c5\ub85c\ub4dc
-gb.mergedPatchset = \ud328\uce58\uc14b \uba38\uc9c0
-gb.commented = \ucf54\uba58\ud2b8
-gb.noDescriptionGiven = \uc124\uba85 \uc5c6\uc74c
-gb.toBranch = {0}\uc5d0\uac8c
-gb.createdBy = \uc0dd\uc131\uc790
-gb.oneParticipant = \ucc38\uac00\uc790 {0}
-gb.nParticipants = \ucc38\uac00\uc790 {0}
-gb.noComments = \ucf54\uba58\ud2b8 \uc5c6\uc74c
-gb.oneComment = \ucf54\uba58\ud2b8 {0}
-gb.nComments = \ucf54\uba58\ud2b8 {0}
-gb.oneAttachment = \ucca8\ubd80\ud30c\uc77c {0}
-gb.nAttachments = \ucca8\ubd80\ud30c\uc77c {0}
-gb.milestone = \ub9c8\uc77c\uc2a4\ud1a4
-gb.compareToMergeBase = \uba38\uc9c0\ubca0\uc774\uc2a4\uc640 \ube44\uad50
-gb.compareToN = {0}\uc640 \ube44\uad50
-gb.open = \uc5f4\ub9bc
-gb.closed = \ub2eb\ud798
-gb.merged = \uba38\uc9c0\ud568
-gb.ticketPatchset = {0} \ud2f0\ucf13, {1} \ud328\uce58\uc14b
-gb.patchsetMergeable = \uc774 \ud328\uce58\uc14b\uc740 {0}\uc5d0 \uc790\ub3d9\uc73c\ub85c \uba38\uc9c0\ub420 \uc218 \uc788\uc2b5\ub2c8\ub2e4.
-gb.patchsetMergeableMore = \uc774 \ud328\uce58\uc14b\uc740 \ucee4\ub9e8\ub4dc \ub77c\uc778\uc5d0\uc11c {0}\uc5d0 \uba38\uc9c0\ub420 \uc218 \uc788\uc2b5\ub2c8\ub2e4.
-gb.patchsetAlreadyMerged = \uc774 \ud328\uce58\uc14b\uc740 {0}\uc5d0 \uba38\uc9c0\ub428.
-gb.patchsetNotMergeable = \uc774 \ud328\uce58\uc14b\uc740 {0}\uc5d0 \uc790\ub3d9\uc73c\ub85c \uba38\uc9c0\ub420 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.
-gb.patchsetNotMergeableMore = \ucda9\ub3cc\uc744 \ud574\uacb0\ud558\uae30 \uc704\ud574 \uc774 \ud328\uce58\uc14b\uc740 \ub9ac\ubca0\uc774\uc2a4 \ud558\uac70\ub098 {0}\uc5d0 \uba38\uc9c0\ub418\uc5b4\uc57c \ud569\ub2c8\ub2e4.
-gb.patchsetNotApproved = \uc774 \ud328\uce58\uc14b \ub9ac\ube44\uc804\uc740 {0}\uc5d0 \uba38\uc9c0\ub418\ub294\uac83\uc774 \uc2b9\uc778\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.
-gb.patchsetNotApprovedMore = \ub9ac\ubdf0\uc5b4\uac00 \uc774 \ud328\uce58\uc14b\uc744 \uc2b9\uc778\ud574\uc57c \ud569\ub2c8\ub2e4.
-gb.patchsetVetoedMore = \ub9ac\ubdf0\uc5b4\uac00 \uc774 \ud328\uce58\uc14b\uc744 \uac70\ubd80\ud558\uc600\uc2b5\ub2c8\ub2e4.
-gb.write = \uc4f0\uae30
-gb.comment = \ucf54\uba58\ud2b8
-gb.preview = \ubbf8\ub9ac\ubcf4\uae30
-gb.leaveComment = \ucf54\uba58\ud2b8 \ub0a8\uae30\uae30...
-gb.showHideDetails = \uc0c1\uc138 \ubcf4\uae30/\uc228\uae30\uae30
-gb.acceptNewPatchsets = \ud328\uce58\uc14b \uc2b9\uc778
-gb.acceptNewPatchsetsDescription = \uc774 \uc800\uc7a5\uc18c\uc5d0 \ud328\uce58\uc14b\uc744 \ud478\uc2dc\ud558\ub294\uac83\uc744 \uc2b9\uc778
-gb.acceptNewTickets = \uc0c8 \ud2f0\ucf13 \ud5c8\uc6a9
-gb.acceptNewTicketsDescription = \ubc84\uadf8, \uac1c\uc120, \ud0c0\uc2a4\ud06c, \ub4f1\uc758 \ud2f0\ucf13 \uc0dd\uc131 \ud5c8\uc6a9
-gb.requireApproval = \uc2b9\uc778 \ud544\uc694
-gb.requireApprovalDescription = \uba38\uc9c0\ubc84\ud2bc \ud65c\uc131\ud654 \uc804 \ud328\uce58\uc14b\uc774 \uc2b9\uc778\ub418\uc5b4\uc57c \ud568
-gb.topic = \ud1a0\ud53d
-gb.proposalTickets = \ubcc0\uacbd \uc81c\uc548
-gb.bugTickets = \ubc84\uadf8
-gb.enhancementTickets = \uac1c\uc120
-gb.taskTickets = \ud0c0\uc2a4\ud06c
-gb.questionTickets = \uc9c8\ubb38
-gb.requestTickets = \uac1c\uc120 & \ud0c0\uc2a4\ud06c
-gb.yourCreatedTickets = \ub0b4\uac00 \uc0dd\uc131\ud568
-gb.yourWatchedTickets = \ub0b4\uac00 \uc9c0\ucf1c\ubd04
-gb.mentionsMeTickets = \ub098\ub97c \ub9e8\uc158 \uc911
-gb.updatedBy = \uc5c5\ub370\uc774\ud2b8
-gb.sort = \uc815\ub82c
-gb.sortNewest = \ucd5c\uc2e0 \uc21c
-gb.sortOldest = \uc624\ub798\ub41c \uc21c
-gb.sortMostRecentlyUpdated = \ucd5c\uadfc \uc5c5\ub370\uc774\ud2b8 \uc21c
-gb.sortLeastRecentlyUpdated = \uc624\ub798\ub41c \uc5c5\ub370\uc774\ud2b8\ub41c \uc21c
-gb.sortMostComments = \ucf54\uba58\ud2b8 \ub9ce\uc740 \uc21c
-gb.sortLeastComments = \ucf54\uba58\ud2b8 \uc801\uc740 \uc21c
-gb.sortMostPatchsetRevisions = \ud328\uce58\uc14b \ub9ac\ube44\uc804 \ub9ce\uc740 \uc21c
-gb.sortLeastPatchsetRevisions = \ud328\uce58\uc14b \ub9ac\ube44\uc804 \uc801\uc740 \uc21c
-gb.sortMostVotes = \uac70\ubd80 \ub9ce\uc740 \uc21c
-gb.sortLeastVotes = \uac70\ubd80 \uc801\uc740 \uc21c
-gb.topicsAndLabels = \ud1a0\ud53d & \ub77c\ubca8
-gb.milestones = \ub9c8\uc77c\uc2a4\ud1a4
-gb.noMilestoneSelected = \uc120\ud0dd\ub41c \ub9c8\uc77c\uc2a4\ud1a4 \uc5c6\uc74c
-gb.notSpecified = \uc9c0\uc815\ub418\uc9c0 \uc54a\uc74c
+gb.active = \uD65C\uC131
+gb.starred = \uBCC4\uD45C
+gb.owned = \uC18C\uC720\uD568
+gb.starredAndOwned = \uBCC4\uD45C & \uC18C\uC720\uD568
+gb.reviewPatchset = \uB9AC\uBDF0 {0} \uD328\uCE58\uC14B {1}
+gb.todaysActivityStats = \uC624\uB298 / {2} \uC791\uC131\uC790\uAC00 {1} \uCEE4\uBC0B\uC0DD\uC131
+gb.todaysActivityNone = \uC624\uB298 / \uC5C6\uC74C
+gb.noActivityToday = \uC624\uB298\uC740 \uC561\uD2F0\uBE44\uD2F0\uAC00 \uC5C6\uC74C
+gb.anonymousUser= \uC775\uBA85
+gb.commitMessageRenderer = \uCEE4\uBC0B \uBA54\uC2DC\uC9C0 \uB79C\uB354\uB7EC
+gb.diffStat = {0}\uAC1C \uCD94\uAC00 & {1}\uAC1C \uC0AD\uC81C
+gb.home = \uD648
+gb.isMirror = \uBBF8\uB7EC \uC800\uC7A5\uC18C
+gb.mirrorOf = {0} \uC758 \uBBF8\uB7EC
+gb.mirrorWarning = \uC774 \uC800\uC7A5\uC18C\uB294 \uBBF8\uB7EC\uC774\uACE0 \uD478\uC2DC\uB97C \uBC1B\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.
+gb.docsWelcome1 = \uC800\uC7A5\uC18C\uB97C \uBB38\uC11C\uD654\uD558\uAE30 \uC704\uD574 \uBB38\uC11C\uB4E4\uC744 \uC0AC\uC6A9\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.
+gb.docsWelcome2 = \uC2DC\uC791\uD558\uAE30 \uC704\uD574 README.md \uB610\uB294 HOME.md \uD30C\uC77C\uC744 \uCEE4\uBC0B\uD558\uC138\uC694.
+gb.createReadme = README \uC0DD\uC131
+gb.responsible = \uB2F4\uB2F9\uC790
+gb.createdThisTicket = \uC774 \uD2F0\uCF13 \uC0DD\uC131
+gb.proposedThisChange = \uC774 \uBCC0\uACBD \uC81C\uC548
+gb.uploadedPatchsetN = {0} \uD328\uCE58\uC14B \uC5C5\uB85C\uB4DC
+gb.uploadedPatchsetNRevisionN = \uB9AC\uBE44\uC804 {1} \uD328\uCE58\uC14B {0} \uC5C5\uB85C\uB4DC
+gb.mergedPatchset = \uD328\uCE58\uC14B \uBA38\uC9C0
+gb.commented = \uCF54\uBA58\uD2B8
+gb.noDescriptionGiven = \uC124\uBA85 \uC5C6\uC74C
+gb.toBranch = {0}\uC5D0\uAC8C
+gb.createdBy = \uC0DD\uC131\uC790
+gb.oneParticipant = \uCC38\uAC00\uC790 {0}
+gb.nParticipants = \uCC38\uAC00\uC790 {0}
+gb.noComments = \uCF54\uBA58\uD2B8 \uC5C6\uC74C
+gb.oneComment = \uCF54\uBA58\uD2B8 {0}
+gb.nComments = \uCF54\uBA58\uD2B8 {0}
+gb.oneAttachment = \uCCA8\uBD80\uD30C\uC77C {0}
+gb.nAttachments = \uCCA8\uBD80\uD30C\uC77C {0}
+gb.milestone = \uB9C8\uC77C\uC2A4\uD1A4
+gb.compareToMergeBase = \uBA38\uC9C0\uBCA0\uC774\uC2A4\uC640 \uBE44\uAD50
+gb.compareToN = {0}\uC640 \uBE44\uAD50
+gb.open = \uC5F4\uB9BC
+gb.closed = \uB2EB\uD798
+gb.merged = \uBA38\uC9C0\uD568
+gb.ticketPatchset = {0} \uD2F0\uCF13, {1} \uD328\uCE58\uC14B
+gb.patchsetMergeable = \uC774 \uD328\uCE58\uC14B\uC740 {0}\uC5D0 \uC790\uB3D9\uC73C\uB85C \uBA38\uC9C0\uB420 \uC218 \uC788\uC2B5\uB2C8\uB2E4.
+gb.patchsetMergeableMore = \uC774 \uD328\uCE58\uC14B\uC740 \uCEE4\uB9E8\uB4DC \uB77C\uC778\uC5D0\uC11C {0}\uC5D0 \uBA38\uC9C0\uB420 \uC218 \uC788\uC2B5\uB2C8\uB2E4.
+gb.patchsetAlreadyMerged = \uC774 \uD328\uCE58\uC14B\uC740 {0}\uC5D0 \uBA38\uC9C0\uB428.
+gb.patchsetNotMergeable = \uC774 \uD328\uCE58\uC14B\uC740 {0}\uC5D0 \uC790\uB3D9\uC73C\uB85C \uBA38\uC9C0\uB420 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.
+gb.patchsetNotMergeableMore = \uCDA9\uB3CC\uC744 \uD574\uACB0\uD558\uAE30 \uC704\uD574 \uC774 \uD328\uCE58\uC14B\uC740 \uB9AC\uBCA0\uC774\uC2A4 \uD558\uAC70\uB098 {0}\uC5D0 \uBA38\uC9C0\uB418\uC5B4\uC57C \uD569\uB2C8\uB2E4.
+gb.patchsetNotApproved = \uC774 \uD328\uCE58\uC14B \uB9AC\uBE44\uC804\uC740 {0}\uC5D0 \uBA38\uC9C0\uB418\uB294\uAC83\uC774 \uC2B9\uC778\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.
+gb.patchsetNotApprovedMore = \uB9AC\uBDF0\uC5B4\uAC00 \uC774 \uD328\uCE58\uC14B\uC744 \uC2B9\uC778\uD574\uC57C \uD569\uB2C8\uB2E4.
+gb.patchsetVetoedMore = \uB9AC\uBDF0\uC5B4\uAC00 \uC774 \uD328\uCE58\uC14B\uC744 \uAC70\uBD80\uD558\uC600\uC2B5\uB2C8\uB2E4.
+gb.write = \uC4F0\uAE30
+gb.comment = \uCF54\uBA58\uD2B8
+gb.preview = \uBBF8\uB9AC\uBCF4\uAE30
+gb.leaveComment = \uCF54\uBA58\uD2B8 \uB0A8\uAE30\uAE30...
+gb.showHideDetails = \uC0C1\uC138 \uBCF4\uAE30/\uC228\uAE30\uAE30
+gb.acceptNewPatchsets = \uD328\uCE58\uC14B \uC2B9\uC778
+gb.acceptNewPatchsetsDescription = \uC774 \uC800\uC7A5\uC18C\uC5D0 \uD328\uCE58\uC14B\uC744 \uD478\uC2DC\uD558\uB294\uAC83\uC744 \uC2B9\uC778
+gb.acceptNewTickets = \uC0C8 \uD2F0\uCF13 \uD5C8\uC6A9
+gb.acceptNewTicketsDescription = \uBC84\uADF8, \uAC1C\uC120, \uD0C0\uC2A4\uD06C, \uB4F1\uC758 \uD2F0\uCF13 \uC0DD\uC131 \uD5C8\uC6A9
+gb.requireApproval = \uC2B9\uC778 \uD544\uC694
+gb.requireApprovalDescription = \uBA38\uC9C0\uBC84\uD2BC \uD65C\uC131\uD654 \uC804 \uD328\uCE58\uC14B\uC774 \uC2B9\uC778\uB418\uC5B4\uC57C \uD568
+gb.topic = \uD1A0\uD53D
+gb.proposalTickets = \uBCC0\uACBD \uC81C\uC548
+gb.bugTickets = \uBC84\uADF8
+gb.enhancementTickets = \uAC1C\uC120
+gb.taskTickets = \uD0C0\uC2A4\uD06C
+gb.questionTickets = \uC9C8\uBB38
+gb.maintenanceTickets = \uC720\uC9C0\uBCF4\uC218
+gb.requestTickets = \uAC1C\uC120 & \uD0C0\uC2A4\uD06C
+gb.yourCreatedTickets = \uB0B4\uAC00 \uC0DD\uC131\uD568
+gb.yourWatchedTickets = \uB0B4\uAC00 \uC9C0\uCF1C\uBD04
+gb.mentionsMeTickets = \uB098\uB97C \uB9E8\uC158 \uC911
+gb.updatedBy = \uC5C5\uB370\uC774\uD2B8
+gb.sort = \uC815\uB82C
+gb.sortNewest = \uCD5C\uC2E0 \uC21C
+gb.sortOldest = \uC624\uB798\uB41C \uC21C
+gb.sortMostRecentlyUpdated = \uCD5C\uADFC \uC5C5\uB370\uC774\uD2B8 \uC21C
+gb.sortLeastRecentlyUpdated = \uC624\uB798\uB41C \uC5C5\uB370\uC774\uD2B8\uB41C \uC21C
+gb.sortMostComments = \uCF54\uBA58\uD2B8 \uB9CE\uC740 \uC21C
+gb.sortLeastComments = \uCF54\uBA58\uD2B8 \uC801\uC740 \uC21C
+gb.sortMostPatchsetRevisions = \uD328\uCE58\uC14B \uB9AC\uBE44\uC804 \uB9CE\uC740 \uC21C
+gb.sortLeastPatchsetRevisions = \uD328\uCE58\uC14B \uB9AC\uBE44\uC804 \uC801\uC740 \uC21C
+gb.sortMostVotes = \uAC70\uBD80 \uB9CE\uC740 \uC21C
+gb.sortLeastVotes = \uAC70\uBD80 \uC801\uC740 \uC21C
+gb.topicsAndLabels = \uD1A0\uD53D & \uB77C\uBCA8
+gb.milestones = \uB9C8\uC77C\uC2A4\uD1A4
+gb.noMilestoneSelected = \uC120\uD0DD\uB41C \uB9C8\uC77C\uC2A4\uD1A4 \uC5C6\uC74C
+gb.notSpecified = \uC9C0\uC815\uB418\uC9C0 \uC54A\uC74C
gb.due = due
-gb.queries = \ucffc\ub9ac
-gb.searchTicketsTooltip = {0} \ud2f0\ucf13 \uac80\uc0c9
-gb.searchTickets = \ud2f0\ucf13 \uac80\uc0c9
-gb.new = \uc0c8
-gb.newTicket = \uc0c8 \ud2f0\ucf13
-gb.editTicket = \ud2f0\ucf13 \uc218\uc815
-gb.ticketsWelcome = \ud560\uc77c \ubaa9\ub85d, \ubc84\uadf8 \ud1a0\ub860\uc744 \uc815\ub9ac\ud558\uace0 \ud328\uce58\uc14b\uc73c\ub85c \ud611\uc5c5\ud558\uae30 \uc704\ud574 \ud2f0\ucf13\uc744 \uc0ac\uc6a9\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.
-gb.createFirstTicket = \uccab\ubc88\uc9f8 \ud2f0\ucf13\uc744 \ub9cc\ub4dc\uc138\uc694.
-gb.title = \uc81c\ubaa9
-gb.changedStatus = \uc0c1\ud0dc \ubcc0\uacbd\ub428
-gb.discussion = \ud1a0\ub860
-gb.updated = \uc5c5\ub370\uc774\ud2b8\ub428
-gb.proposePatchset = \ud328\uce58\uc14b\uc758 \uc81c\uc548
-gb.proposePatchsetNote = \uc774 \ud2f0\ucf13\uc5d0 \ub300\ud55c \ud328\uce58\uc14b \uc81c\uc548\uc744 \ud658\uc601\ud569\ub2c8\ub2e4.
-gb.proposeInstructions = \ub9e8 \uba3c\uc800, \ud328\uce58\uc14b\uc744 \ub9cc\ub4e4\uace0 Git\uc73c\ub85c \uc5c5\ub85c\ub4dc \ud558\uc138\uc694. Gitblit \uc774 id \ub85c \uc774 \ud2f0\ucf13\uacfc \uc5f0\uacb0\ud560 \uac83\uc785\ub2c8\ub2e4.
-gb.proposeWith = \uc774\uac83\uc73c\ub85c \ud328\uce58\uc14b \uc81c\uc548 - {0}
-gb.revisionHistory = \ub9ac\ube44\uc804 \ud788\uc2a4\ud1a0\ub9ac
-gb.merge = \uba38\uc9c0
-gb.action = \uc561\uc158
-gb.patchset = \ud328\uce58\uc14b
-gb.all = \ubaa8\ub450
-gb.mergeBase = \uba38\uc9c0 \ubca0\uc774\uc2a4
-gb.checkout = \uccb4\ud06c\uc544\uc6c3
-gb.checkoutViaCommandLine = \ucee4\ub9e8\ub4dc \ub77c\uc778\uc73c\ub85c \uccb4\ud06c\uc544\uc6c3
-gb.checkoutViaCommandLineNote = \uc774 \uc800\uc7a5\uc18c\uc758 \ud074\ub860\uc5d0\uc11c \ub85c\uceec \ubcc0\uacbd\uc0ac\ud56d\uc744 \uccb4\ud06c\uc544\uc6c3\ud558\uace0 \ud14c\uc2a4\ud2b8\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.
-gb.checkoutStep1 = \ud604\uc7ac \ud328\uce58\uc14b\uc744 \uac00\uc838\uc624\uc138\uc694. - \ud504\ub85c\uc81d\ud2b8 \ub514\ub809\ud1a0\ub9ac\uc5d0\uc11c \uc774 \uc791\uc5c5\uc744 \uc2e4\ud589\ud558\uc138\uc694.
-gb.checkoutStep2 = \uc0c8 \ube0c\ub79c\uce58\uc640 \ub9ac\ubdf0\uc5d0 \ud328\uce58\uc14b\uc744 \uccb4\ud06c\uc544\uc6c3 \ud558\uc138\uc694.
-gb.mergingViaCommandLine = \ucee4\ub9e8\ub4dc \ub77c\uc778\uc5d0\uc11c \uba38\uc9d5
-gb.mergingViaCommandLineNote = \uba38\uc9c0 \ubc84\ud2bc \uc0ac\uc6a9\uc744 \uc6d0\ud558\uc9c0 \uc54a\uac70\ub098 \uc790\ub3d9 \uba38\uc9c0\uac00 \ub3d9\uc791\ud558\uc9c0 \ub418\uc9c0 \uc54a\ub294\ub2e4\uba74, \ucee4\ub9e8\ub4dc \ub77c\uc778\uc5d0\uc11c \uc218\ub3d9\uc73c\ub85c \uba38\uc9c0\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.
-gb.mergeStep1 = \ubcc0\uacbd\uc0ac\ud56d\uc744 \ub9ac\ubdf0\ud558\uae30\uc704\ud574 \uc0c8 \ube0c\ub79c\uce58\ub97c \uccb4\ud06c\uc544\uc6c3 \ud558\uc138\uc694. \u2014 \ud504\ub85c\uc81d\ud2b8 \ub514\ub809\ud1a0\ub9ac\uc5d0\uc11c \uc774 \uc791\uc5c5\uc744 \uc2e4\ud589\ud558\uc138\uc694.
-gb.mergeStep2 = \uc81c\uc548\ub41c \ubcc0\uacbd\uc0ac\ud56d\uacfc \ub9ac\ubdf0\ub97c \uac00\uc838\uc624\uc138\uc694.
-gb.mergeStep3 = \uc81c\uc548\ub41c \ubcc0\uacbd\uc0ac\ud56d\uc744 \uba38\uc9c0\ud558\uace0 \uc11c\ubc84\uc5d0 \uc5c5\ub370\uc774\ud2b8 \ud558\uc138\uc694.
-gb.download = \ub2e4\uc6b4\ub85c\ub4dc
-gb.ptDescription = Gitblit \ud328\uce58\uc14b \ub3c4\uad6c
-gb.ptCheckout = \ub9ac\ubdf0 \ube0c\ub79c\uce58\uc5d0 \ud604\uc7ac \ud328\uce58\uc14b\uc744 \uac00\uc838\uc624\uace0 \uccb4\ud06c\uc544\uc6c3\ud558\uc138\uc694.
-gb.ptMerge = \ub85c\uceec \ube0c\ub79c\uce58\uc5d0 \ud604\uc7ac \ud328\uce58\uc14b\uc744 \uac00\uc838\uc624\uace0 \uba38\uc9c0\ud558\uc138\uc694.
-gb.ptDescription1 = Barnum \uc740 Gitblit \ud2f0\ucf13\uacfc \ud328\uce58\uc14b\uc744 \uc0ac\uc6a9\ud558\uae30 \uc704\ud55c \uad6c\ubb38\uc744 \ub2e8\uc21c\ud558\uac8c \ud574\uc904 Git \ucee4\ub9e8\ub4dc \ub77c\uc778 \ub3c4\uad6c \uc785\ub2c8\ub2e4.
-gb.ptSimplifiedCollaboration = \ub2e8\uc21c\ud654\ub41c \uacf5\ub3d9\uc791\uc5c5 \uad6c\ubb38
-gb.ptSimplifiedMerge = \ub2e8\uc21c\ud654\ub41c \uba38\uc9c0 \uad6c\ubb38
-gb.ptDescription2 = Barnum \uc740 \ud30c\uc774\uc36c 3 \uc640 \ub124\uc774\ud2f0\ube0c Git \uc774 \ud544\uc694\ud569\ub2c8\ub2e4. Windows, Linux, and Mac OS X \uc5d0\uc11c \ub3d9\uc791\ud569\ub2c8\ub2e4.
-gb.stepN = {0} \ub2e8\uacc4
-gb.watchers = \uc9c0\ucf1c\ubcf4\ub294 \uc774
-gb.votes = \ud22c\ud45c
-gb.vote = {0} \uc5d0 \ud22c\ud45c\ud558\uae30
-gb.watch = {0} \uc9c0\ucf1c\ubcf4\uae30
-gb.removeVote = \ud22c\ud45c \uc81c\uac70
-gb.stopWatching = \uc9c0\ucf1c\ubcf4\uae30 \uc911\uc9c0
-gb.watching = \uc9c0\ucf1c\ubcf4\ub294 \uc911
-gb.comments = \ucf54\uba58\ud2b8
-gb.addComment = \ucf54\uba58\ud2b8 \ucd94\uac00
-gb.export = \ub0b4\ubcf4\ub0b4\uae30
-gb.oneCommit = 1\uac1c \ucee4\ubc0b
-gb.nCommits = {0}\uac1c \ucee4\ubc0b
-gb.addedOneCommit = 1\uac1c \ucee4\ubc0b \ucd94\uac00
-gb.addedNCommits = {0}\uac1c \ucee4\ubc0b \ucd94\uac00
-gb.commitsInPatchsetN = {0} \ud328\uce58\uc14b\uc758 \ucee4\ubc0b
-gb.patchsetN = \ud328\uce58\uc14b {0}
-gb.reviewedPatchsetRev = \ud328\uce58\uc14b {0} \ub9ac\ube44\uc804 {1}: {2} \ub9ac\ubdf0
-gb.review = \ub9ac\ubdf0
-gb.reviews = \ub9ac\ubdf0
-gb.veto = \uac70\ubd80
-gb.needsImprovement = \uac1c\uc120 \ud544\uc694
-gb.looksGood = \uc88b\uc544 \ubcf4\uc784
-gb.approve = \uc2b9\uc778
-gb.hasNotReviewed = \ub9ac\ubdf0\ub418\uc9c0 \uc54a\uc74c
+gb.queries = \uCFFC\uB9AC
+gb.searchTicketsTooltip = {0} \uD2F0\uCF13 \uAC80\uC0C9
+gb.searchTickets = \uD2F0\uCF13 \uAC80\uC0C9
+gb.new = \uC0C8
+gb.newTicket = \uC0C8 \uD2F0\uCF13
+gb.editTicket = \uD2F0\uCF13 \uC218\uC815
+gb.ticketsWelcome = \uD560\uC77C \uBAA9\uB85D, \uBC84\uADF8 \uD1A0\uB860\uC744 \uC815\uB9AC\uD558\uACE0 \uD328\uCE58\uC14B\uC73C\uB85C \uD611\uC5C5\uD558\uAE30 \uC704\uD574 \uD2F0\uCF13\uC744 \uC0AC\uC6A9\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.
+gb.createFirstTicket = \uCCAB\uBC88\uC9F8 \uD2F0\uCF13\uC744 \uB9CC\uB4DC\uC138\uC694.
+gb.title = \uC81C\uBAA9
+gb.changedStatus = \uC0C1\uD0DC \uBCC0\uACBD\uB428
+gb.discussion = \uD1A0\uB860
+gb.updated = \uC5C5\uB370\uC774\uD2B8\uB428
+gb.proposePatchset = \uD328\uCE58\uC14B\uC758 \uC81C\uC548
+gb.proposePatchsetNote = \uC774 \uD2F0\uCF13\uC5D0 \uB300\uD55C \uD328\uCE58\uC14B \uC81C\uC548\uC744 \uD658\uC601\uD569\uB2C8\uB2E4.
+gb.proposeInstructions = \uB9E8 \uBA3C\uC800, \uD328\uCE58\uC14B\uC744 \uB9CC\uB4E4\uACE0 Git\uC73C\uB85C \uC5C5\uB85C\uB4DC \uD558\uC138\uC694. Gitblit \uC774 id \uB85C \uC774 \uD2F0\uCF13\uACFC \uC5F0\uACB0\uD560 \uAC83\uC785\uB2C8\uB2E4.
+gb.proposeWith = \uC774\uAC83\uC73C\uB85C \uD328\uCE58\uC14B \uC81C\uC548 - {0}
+gb.revisionHistory = \uB9AC\uBE44\uC804 \uD788\uC2A4\uD1A0\uB9AC
+gb.merge = \uBA38\uC9C0
+gb.action = \uC561\uC158
+gb.patchset = \uD328\uCE58\uC14B
+gb.all = \uBAA8\uB450
+gb.mergeBase = \uBA38\uC9C0 \uBCA0\uC774\uC2A4
+gb.checkout = \uCCB4\uD06C\uC544\uC6C3
+gb.checkoutViaCommandLine = \uCEE4\uB9E8\uB4DC \uB77C\uC778\uC73C\uB85C \uCCB4\uD06C\uC544\uC6C3
+gb.checkoutViaCommandLineNote = \uC774 \uC800\uC7A5\uC18C\uC758 \uD074\uB860\uC5D0\uC11C \uB85C\uCEEC \uBCC0\uACBD\uC0AC\uD56D\uC744 \uCCB4\uD06C\uC544\uC6C3\uD558\uACE0 \uD14C\uC2A4\uD2B8\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.
+gb.checkoutStep1 = \uD604\uC7AC \uD328\uCE58\uC14B\uC744 \uAC00\uC838\uC624\uC138\uC694. - \uD504\uB85C\uC81D\uD2B8 \uB514\uB809\uD1A0\uB9AC\uC5D0\uC11C \uC774 \uC791\uC5C5\uC744 \uC2E4\uD589\uD558\uC138\uC694.
+gb.checkoutStep2 = \uC0C8 \uBE0C\uB79C\uCE58\uC640 \uB9AC\uBDF0\uC5D0 \uD328\uCE58\uC14B\uC744 \uCCB4\uD06C\uC544\uC6C3 \uD558\uC138\uC694.
+gb.mergingViaCommandLine = \uCEE4\uB9E8\uB4DC \uB77C\uC778\uC5D0\uC11C \uBA38\uC9D5
+gb.mergingViaCommandLineNote = \uBA38\uC9C0 \uBC84\uD2BC \uC0AC\uC6A9\uC744 \uC6D0\uD558\uC9C0 \uC54A\uAC70\uB098 \uC790\uB3D9 \uBA38\uC9C0\uAC00 \uB3D9\uC791\uD558\uC9C0 \uB418\uC9C0 \uC54A\uB294\uB2E4\uBA74, \uCEE4\uB9E8\uB4DC \uB77C\uC778\uC5D0\uC11C \uC218\uB3D9\uC73C\uB85C \uBA38\uC9C0\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.
+gb.mergeStep1 = \uBCC0\uACBD\uC0AC\uD56D\uC744 \uB9AC\uBDF0\uD558\uAE30\uC704\uD574 \uC0C8 \uBE0C\uB79C\uCE58\uB97C \uCCB4\uD06C\uC544\uC6C3 \uD558\uC138\uC694. \u2014 \uD504\uB85C\uC81D\uD2B8 \uB514\uB809\uD1A0\uB9AC\uC5D0\uC11C \uC774 \uC791\uC5C5\uC744 \uC2E4\uD589\uD558\uC138\uC694.
+gb.mergeStep2 = \uC81C\uC548\uB41C \uBCC0\uACBD\uC0AC\uD56D\uACFC \uB9AC\uBDF0\uB97C \uAC00\uC838\uC624\uC138\uC694.
+gb.mergeStep3 = \uC81C\uC548\uB41C \uBCC0\uACBD\uC0AC\uD56D\uC744 \uBA38\uC9C0\uD558\uACE0 \uC11C\uBC84\uC5D0 \uC5C5\uB370\uC774\uD2B8 \uD558\uC138\uC694.
+gb.download = \uB2E4\uC6B4\uB85C\uB4DC
+gb.ptDescription = Gitblit \uD328\uCE58\uC14B \uB3C4\uAD6C
+gb.ptCheckout = \uB9AC\uBDF0 \uBE0C\uB79C\uCE58\uC5D0 \uD604\uC7AC \uD328\uCE58\uC14B\uC744 \uAC00\uC838\uC624\uACE0 \uCCB4\uD06C\uC544\uC6C3\uD558\uC138\uC694.
+gb.ptMerge = \uB85C\uCEEC \uBE0C\uB79C\uCE58\uC5D0 \uD604\uC7AC \uD328\uCE58\uC14B\uC744 \uAC00\uC838\uC624\uACE0 \uBA38\uC9C0\uD558\uC138\uC694.
+gb.ptDescription1 = Barnum \uC740 Gitblit \uD2F0\uCF13\uACFC \uD328\uCE58\uC14B\uC744 \uC0AC\uC6A9\uD558\uAE30 \uC704\uD55C \uAD6C\uBB38\uC744 \uB2E8\uC21C\uD558\uAC8C \uD574\uC904 Git \uCEE4\uB9E8\uB4DC \uB77C\uC778 \uB3C4\uAD6C \uC785\uB2C8\uB2E4.
+gb.ptSimplifiedCollaboration = \uB2E8\uC21C\uD654\uB41C \uACF5\uB3D9\uC791\uC5C5 \uAD6C\uBB38
+gb.ptSimplifiedMerge = \uB2E8\uC21C\uD654\uB41C \uBA38\uC9C0 \uAD6C\uBB38
+gb.ptDescription2 = Barnum \uC740 \uD30C\uC774\uC36C 3 \uC640 \uB124\uC774\uD2F0\uBE0C Git \uC774 \uD544\uC694\uD569\uB2C8\uB2E4. Windows, Linux, and Mac OS X \uC5D0\uC11C \uB3D9\uC791\uD569\uB2C8\uB2E4.
+gb.stepN = {0} \uB2E8\uACC4
+gb.watchers = \uC9C0\uCF1C\uBCF4\uB294 \uC774
+gb.votes = \uD22C\uD45C
+gb.vote = {0} \uC5D0 \uD22C\uD45C\uD558\uAE30
+gb.watch = {0} \uC9C0\uCF1C\uBCF4\uAE30
+gb.removeVote = \uD22C\uD45C \uC81C\uAC70
+gb.stopWatching = \uC9C0\uCF1C\uBCF4\uAE30 \uC911\uC9C0
+gb.watching = \uC9C0\uCF1C\uBCF4\uB294 \uC911
+gb.comments = \uCF54\uBA58\uD2B8
+gb.addComment = \uCF54\uBA58\uD2B8 \uCD94\uAC00
+gb.export = \uB0B4\uBCF4\uB0B4\uAE30
+gb.oneCommit = 1\uAC1C \uCEE4\uBC0B
+gb.nCommits = {0}\uAC1C \uCEE4\uBC0B
+gb.addedOneCommit = 1\uAC1C \uCEE4\uBC0B \uCD94\uAC00
+gb.addedNCommits = {0}\uAC1C \uCEE4\uBC0B \uCD94\uAC00
+gb.commitsInPatchsetN = {0} \uD328\uCE58\uC14B\uC758 \uCEE4\uBC0B
+gb.patchsetN = \uD328\uCE58\uC14B {0}
+gb.reviewedPatchsetRev = \uD328\uCE58\uC14B {0} \uB9AC\uBE44\uC804 {1}: {2} \uB9AC\uBDF0
+gb.review = \uB9AC\uBDF0
+gb.reviews = \uB9AC\uBDF0
+gb.veto = \uAC70\uBD80
+gb.needsImprovement = \uAC1C\uC120 \uD544\uC694
+gb.looksGood = \uC88B\uC544 \uBCF4\uC784
+gb.approve = \uC2B9\uC778
+gb.hasNotReviewed = \uB9AC\uBDF0\uB418\uC9C0 \uC54A\uC74C
gb.about = about
-gb.ticketN = \ud2f0\ucf13 #{0}
-gb.disableUser = \uc0ac\uc6a9\uc790 \ube44\ud65c\uc131\ud654
-gb.disableUserDescription = \uc778\uc99d\uc5d0\uc11c \uc774 \uacc4\uc815 \ucc28\ub2e8
-gb.any = \ubaa8\ub450
-gb.milestoneProgress = {0}\uac1c \uc5f4\ub9bc, {1}\uac1c \ub2eb\ud798
-gb.nOpenTickets = {0}\uac1c \uc5f4\ub9bc
-gb.nClosedTickets = {0}\uac1c \ub2eb\ud798
-gb.nTotalTickets = \ubaa8\ub450 {0}\uac1c
-gb.body = \ub0b4\uc6a9
+gb.ticketN = \uD2F0\uCF13 #{0}
+gb.disableUser = \uC0AC\uC6A9\uC790 \uBE44\uD65C\uC131\uD654
+gb.disableUserDescription = \uC778\uC99D\uC5D0\uC11C \uC774 \uACC4\uC815 \uCC28\uB2E8
+gb.any = \uBAA8\uB450
+gb.milestoneProgress = {0}\uAC1C \uC5F4\uB9BC, {1}\uAC1C \uB2EB\uD798
+gb.nOpenTickets = {0}\uAC1C \uC5F4\uB9BC
+gb.nClosedTickets = {0}\uAC1C \uB2EB\uD798
+gb.nTotalTickets = \uBAA8\uB450 {0}\uAC1C
+gb.body = \uB0B4\uC6A9
gb.mergeSha = mergeSha
-gb.mergeTo = \uba38\uc9c0\ub300\uc0c1
-gb.labels = \ub77c\ubca8
-gb.reviewers = \uac80\ud1a0\uc790
-gb.voters = \ud22c\ud45c\uc790
-gb.mentions = \ub9e8\uc158
-gb.canNotProposePatchset = \ud328\uce58\uc14b\uc744 \uc81c\uc548\ud560 \uc218 \uc5c6\uc74c
-gb.repositoryIsMirror = \uc774 \uc800\uc7a5\uc18c\ub294 \uc77d\uae30\uc804\uc6a9 \ubbf8\ub7ec\uc785\ub2c8\ub2e4.
-gb.repositoryIsFrozen = \uc774 \uc800\uc7a5\uc18c\ub294 \ud504\ub85c\uc98c \uc0c1\ud0dc\uc785\ub2c8\ub2e4.
-gb.repositoryDoesNotAcceptPatchsets = \uc774 \uc800\uc7a5\uc18c\ub294 \ud328\uce58\uc14b\uc744 \uc218\uc6a9\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.
-gb.serverDoesNotAcceptPatchsets = \uc774 \uc11c\ubc84\ub294 \ud328\uce58\uc14b\uc744 \uc218\uc6a9\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.
-gb.ticketIsClosed = \uc774 \ud2f0\ucf13\uc740 \ub2eb\ud600 \uc788\uc2b5\ub2c8\ub2e4.
-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.
+gb.mergeTo = \uBA38\uC9C0\uB300\uC0C1
+gb.mergeType = \uBA38\uC9C0\uD0C0\uC785
+gb.labels = \uB77C\uBCA8
+gb.reviewers = \uAC80\uD1A0\uC790
+gb.voters = \uD22C\uD45C\uC790
+gb.mentions = \uB9E8\uC158
+gb.canNotProposePatchset = \uD328\uCE58\uC14B\uC744 \uC81C\uC548\uD560 \uC218 \uC5C6\uC74C
+gb.repositoryIsMirror = \uC774 \uC800\uC7A5\uC18C\uB294 \uC77D\uAE30\uC804\uC6A9 \uBBF8\uB7EC\uC785\uB2C8\uB2E4.
+gb.repositoryIsFrozen = \uC774 \uC800\uC7A5\uC18C\uB294 \uD504\uB85C\uC98C \uC0C1\uD0DC\uC785\uB2C8\uB2E4.
+gb.repositoryDoesNotAcceptPatchsets = \uC774 \uC800\uC7A5\uC18C\uB294 \uD328\uCE58\uC14B\uC744 \uC218\uC6A9\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.
+gb.serverDoesNotAcceptPatchsets = \uC774 \uC11C\uBC84\uB294 \uD328\uCE58\uC14B\uC744 \uC218\uC6A9\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.
+gb.ticketIsClosed = \uC774 \uD2F0\uCF13\uC740 \uB2EB\uD600 \uC788\uC2B5\uB2C8\uB2E4.
+gb.mergeToDescription = \uD2F0\uCF13 \uD328\uCE58\uC14B\uC744 \uBA38\uC9C0\uD560 \uAE30\uBCF8 \uD1B5\uD569 \uBE0C\uB79C\uCE58
+gb.mergeTypeDescription = fast-forward \uB9CC \uBA38\uC9C0, \uD544\uC694\uD558\uBA74, \uB610\uB294 \uD56D\uC0C1 \uD1B5\uD569\uBE0C\uB79C\uCE58\uC5D0 \uCEE4\uBC0B \uBA38\uC9C0
+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.sshKeyComment = \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.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.
+gb.priority = \uC6B0\uC120\uC21C\uC704
+gb.severity = \uC911\uC694\uB3C4
+gb.sortHighestPriority = \uCD5C\uACE0 \uC6B0\uC120\uC21C\uC704
+gb.sortLowestPriority = \uCD5C\uC800 \uC6B0\uC120\uC21C\uC704
+gb.sortHighestSeverity = \uCD5C\uACE0 \uC911\uC694\uB3C4
+gb.sortLowestSeverity = \uCD5C\uC800 \uC911\uC694\uB3C4
+gb.missingIntegrationBranchMore = \uD1B5\uD569\uB300\uC0C1 \uBE0C\uB79C\uCE58\uAC00 \uC800\uC7A5\uC18C\uC5D0 \uC5C6\uC5B4\uC694!
+gb.diffDeletedFileSkipped = (\uC0AD\uC81C\uB428)
+gb.diffFileDiffTooLarge = \uBE44\uAD50\uD558\uAE30\uC5D0 \uB108\uBB34 \uD07C
+gb.diffNewFile = \uC0C8 \uD30C\uC77C
+gb.diffDeletedFile = \uD30C\uC77C\uC774 \uC0AD\uC81C\uB428
+gb.diffRenamedFile = {0} \uC5D0\uC11C \uC774\uB984\uC774 \uBCC0\uACBD\uB428
+gb.diffCopiedFile = {0} \uC5D0\uC11C \uBCF5\uC0AC\uB428
+gb.diffTruncated = \uC704 \uD30C\uC77C\uC774\uD6C4 \uCC28\uC774 \uC81C\uAC70\uB428
+gb.opacityAdjust = \uBD88\uD22C\uBA85\uB3C4 \uC870\uC815
+gb.blinkComparator = \uC810\uBA78 \uBE44\uAD50\uAE30
+gb.imgdiffSubtract = Subtract (black = identical)
+gb.deleteRepositoryHeader = \uC800\uC7A5\uC18C \uC0AD\uC81C
+gb.deleteRepositoryDescription = \uC0AD\uC81C\uB41C \uC800\uC7A5\uC18C\uB294 \uBCF5\uAD6C\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.
+gb.show_whitespace = \uACF5\uBC31 \uBCF4\uAE30
+gb.ignore_whitespace = \uACF5\uBC31 \uBB34\uC2DC
+gb.allRepositories = \uBAA8\uB4E0 \uC800\uC7A5\uC18C
+gb.oid = \uC624\uBE0C\uC81D\uD2B8 id
+gb.filestore = \uD30C\uC77C\uC2A4\uD1A0\uC5B4
+gb.filestoreStats = \uD30C\uC77C\uC2A4\uD1A0\uC5B4\uC5D0 {1} \uC6A9\uB7C9\uC73C\uB85C {0} \uAC1C \uD30C\uC77C\uC774 \uC788\uC74C. ({2} \uB0A8\uC74C)
+gb.statusChangedOn = \uC0C1\uD0DC \uBCC0\uACBD\uC77C
+gb.statusChangedBy = \uC0C1\uD0DC \uBCC0\uACBD\uC790
+gb.filestoreHelp = \uD30C\uC77C\uC2A4\uD1A0\uC5B4 \uC0AC\uC6A9\uBC95?
+gb.editFile = \uD30C\uC77C \uC218\uC815
+gb.continueEditing = \uACC4\uC18D \uC218\uC815
+gb.commitChanges = \uCEE4\uBC0B \uBCC0\uD654
+gb.fileNotMergeable = {0} \uC744(\uB97C) \uCEE4\uBC0B\uD560 \uC218 \uC5C6\uC74C. \uC774 \uD30C\uC77C\uC740 \uC790\uB3D9\uC73C\uB85C \uBA38\uC9C0 \uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.
+gb.fileCommitted = {0} \uAC00 \uC131\uACF5\uC801\uC73C\uB85C \uCEE4\uBC0B\uB418\uC5C8\uC5B4\uC694.
+gb.deletePatchset = {0} \uD328\uCE58\uC14B \uC0AD\uC81C
+gb.deletePatchsetSuccess = {0} \uD328\uCE58\uC14B\uC774 \uC0AD\uC81C\uB418\uC5C8\uC5B4\uC694.
+gb.deletePatchsetFailure = {0} \uD328\uCE58\uC14B \uC0AD\uC81C \uC624\uB958.
+gb.referencedByCommit = \uCEE4\uBC0B\uC5D0 \uCC38\uC870\uB428.
+gb.referencedByTicket = \uD2F0\uCF13\uC5D0 \uCC38\uC870\uB428.
+
+# This last property for unit tests to test successful loading of the resource file
+gb.loadLang = \ud55c\uad6d\uc5b4
+
diff --git a/src/main/java/com/gitblit/wicket/GitBlitWebApp_nl.properties b/src/main/java/com/gitblit/wicket/GitBlitWebApp_nl.properties
index f71d67d7..a869e96b 100644
--- a/src/main/java/com/gitblit/wicket/GitBlitWebApp_nl.properties
+++ b/src/main/java/com/gitblit/wicket/GitBlitWebApp_nl.properties
@@ -18,7 +18,7 @@ gb.object = object
gb.ticketId = ticket id
gb.ticketAssigned = toegewezen
gb.ticketOpenDate = open datum
-gb.ticketState = status
+gb.ticketStatus = status
gb.ticketComments = commentaar
gb.view = view
gb.local = local
@@ -170,7 +170,6 @@ gb.accessLevel = toegangsniveau
gb.default = standaard
gb.setDefault = instellen als standaard
gb.since = sinds
-gb.status = status
gb.bootDate = boot datum
gb.servletContainer = servlet container
gb.heapMaximum = maximum heap
@@ -210,7 +209,7 @@ gb.filters = filters
gb.generalDescription = algemene instellingen
gb.accessPermissionsDescription = beperk toegang voor gebruikers en teams
gb.accessPermissionsForUserDescription = stel teamlidmaatschappen in of geef toegang tot specifieke besloten repositories
-gb.accessPermissionsForTeamDescription = stel teamlidmaatschappen in en geef toegang tot specifieke besloten repositories
+gb.accessPermissionsForTeamDescription = stel teamlidmaatschappen in en geef toegang tot specifieke besloten repositories
gb.federationRepositoryDescription = deel deze repositorie met andere Gitblit servers
gb.hookScriptsDescription = run Groovy scripts bij pushes naar deze Gitblit server
gb.reset = reset
@@ -218,7 +217,8 @@ gb.pages = paginas
gb.workingCopy = werkkopie
gb.workingCopyWarning = deze repositorie heeft een werkkopie en kan geen pushes ontvangen
gb.query = query
-gb.queryHelp = Standaard query syntax wordt ondersteund.<p/><p/>Zie aub <a target="_new" href="http://lucene.apache.org/core/old_versioned_docs/versions/3_5_0/queryparsersyntax.html">Lucene Query Parser Syntax</a> voor informatie.
+gb.queryHelp = Standaard query syntax wordt ondersteund.<p/><p/>Zie aub ${querySyntax} voor informatie.
+gb.querySyntax = Lucene Query Parser Syntax
gb.queryResults = resultaten {0} - {1} ({2} hits)
gb.noHits = geen hits
gb.authored = geschreven
@@ -261,7 +261,7 @@ gb.unauthorizedAccessForRepository = Niet toegestane toegang tot repositorie
gb.failedToFindCommit = Het vinden van commit \"{0}\" in {1} voor {2} pagina is mislukt!
gb.couldNotFindFederationProposal = Kon federatievoorstel niet vinden!
gb.invalidUsernameOrPassword = Onjuiste gebruikersnaam of wachtwoord!
-gb.OneProposalToReview = Er is 1 federatie voorstel dat wacht op review.
+gb.OneProposalToReview = Er is 1 federatie voorstel dat wacht op review.
gb.nFederationProposalsToReview = Er zijn {0} federatie verzoeken die wachten op review.
gb.couldNotFindTag = Kon tag {0} niet vinden
gb.couldNotCreateFederationProposal = Kon geen federatie voorstel maken!
@@ -291,7 +291,7 @@ gb.empty = leeg
gb.inherited = geërfd
gb.deleteRepository = Verwijder repositorie \"{0}\"?
gb.repositoryDeleted = Repositorie ''{0}'' verwijderd.
-gb.repositoryDeleteFailed = Verwijdering van repositorie ''{0}'' mislukt!
+gb.repositoryDeleteFailed = Verwijdering van repositorie ''{0}'' mislukt!
gb.deleteUser = Verwijder gebruiker \"{0}\"?
gb.userDeleted = Gebruiker ''{0}'' verwijderd.
gb.userDeleteFailed = Verwijdering van gebruiker ''{0}'' mislukt!
@@ -607,7 +607,7 @@ gb.mergeBase = merge base
gb.checkout = checkout
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.checkoutStep1 = Download 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 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.
@@ -642,7 +642,7 @@ gb.patchsetN = patchset {0}
gb.reviewedPatchsetRev = reviewed patchset {0} revisie {1}: {2}
gb.review = review
gb.reviews = reviews
-gb.veto = veto
+gb.veto = veto
gb.needsImprovement = kan beter
gb.looksGood = ziet er goed uit
gb.approve = goedkeuren
@@ -719,7 +719,7 @@ gb.federationStrategyDescription = Bepaal of en hoe deze repositorie te federere
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.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.
@@ -736,9 +736,12 @@ 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.sshKeyComment = 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
+
+# This last property for unit tests to test successful loading of the resource file
+gb.loadLang = Nederlands
+
diff --git a/src/main/java/com/gitblit/wicket/GitBlitWebApp_no.properties b/src/main/java/com/gitblit/wicket/GitBlitWebApp_no.properties
index efc13a1e..96522ec6 100644
--- a/src/main/java/com/gitblit/wicket/GitBlitWebApp_no.properties
+++ b/src/main/java/com/gitblit/wicket/GitBlitWebApp_no.properties
@@ -49,8 +49,8 @@ gb.tagger = tagger
gb.moreHistory = mer historikk...
gb.difftocurrent = diff med gjeldende
gb.search = s\u00F8k
-gb.searchForAuthor = S\u00F8k etter commits skrever av
-gb.searchForCommitter = S\u00F8k etter commits committet av
+gb.searchForAuthor = S\u00F8k etter commits skrever av
+gb.searchForCommitter = S\u00F8k etter commits committet av
gb.addition = lagt til
gb.modification = endring
gb.deletion = sletting
@@ -88,14 +88,14 @@ gb.useTicketsDescription = skrivebeskyttede, distribuerte Ticgit issues
gb.useDocsDescription = eumererer Markdown dokumentasjon i repositoriet
gb.showRemoteBranchesDescription = vis remote branches
gb.canAdminDescription = kan administere Gitblit server
-gb.permittedUsers = tillatte brukere
+gb.permittedUsers = tillatte brukere
gb.isFrozen = er frosset
gb.isFrozenDescription = avvis push operasjoner
gb.zip = zip
gb.showReadme = vis readme
gb.showReadmeDescription = vis en \"readme\" Markdown fil p\u00e5 oppsummeringssiden
gb.nameDescription = bruk '/' for \u00e5 gruppere repositorier. F.eks libraries/mycoollib.git
-gb.ownerDescription = eieren kan redigere repository-innstillinge
+gb.ownerDescription = eieren kan redigere repository-innstillinge
gb.blob = blob
gb.commitActivityTrend = aktivitetstrend for commits
gb.commitActivityDOW = commit aktivitet gruppert p\u00e5 ukedag
@@ -136,7 +136,7 @@ gb.headRef = standard branch (HEAD)
gb.headRefDescription = endre ref'en HEAD peker mot, f.eks refs/heads/master
gb.federationStrategy = federeringsstrategi
gb.federationRegistration = federeringsregistrering
-gb.federationResults = resultat av federerings pull
+gb.federationResults = resultat av federerings pull
gb.federationSets = federeringssett
gb.message = melding
gb.myUrlDescription = den offentlige url'en for din Gitblit instans
@@ -156,8 +156,8 @@ gb.available = tilgjengelig
gb.selected = valgt
gb.size = st\u00F8rrelse
gb.downloading = laster ned
-gb.loading = laster
-gb.starting = starter
+gb.loading = laster
+gb.starting = starter
gb.general = generelt
gb.settings = innstillinger
gb.manage = administrere
@@ -170,7 +170,6 @@ gb.accessLevel = tilgangsniv\u00e5
gb.default = standardverdi
gb.setDefault = sett standardverdi
gb.since = siden
-gb.status = status
gb.bootDate = oppstartstidspunkt
gb.servletContainer = servlet container
gb.heapMaximum = maks minne (heap)
@@ -193,7 +192,7 @@ gb.activeAuthors = aktive forfattere
gb.commits = commits
gb.teams = team
gb.teamName = teamnavn
-gb.teamMembers = teammedlemmer
+gb.teamMembers = teammedlemmer
gb.teamMemberships = teammedlemskap
gb.newTeam = nytt team
gb.permittedTeams = tillatte team
@@ -207,24 +206,25 @@ gb.customFields = custom fields
gb.customFieldsDescription = custom fields tilgjengelige for Groovy hooks
gb.accessPermissions = tilgangsprivileger
gb.filters = filtre
-gb.generalDescription = vanlige innstillinger
+gb.generalDescription = vanlige innstillinger
gb.accessPermissionsDescription = begrens tilgang vha brukere og team
gb.accessPermissionsForUserDescription = sett teammedlemskap eller gi tilgang til spesifikke repositorier
gb.accessPermissionsForTeamDescription = sett teammedlemskap eller gi tilgang til spesifikke repositorier
-gb.federationRepositoryDescription = del dette repositoriett med andre Gitblit servere
+gb.federationRepositoryDescription = del dette repositoriett med andre Gitblit servere
gb.hookScriptsDescription = kj\u00F8r Groovy scripts ved pushes til denne Gitblit serveren
gb.reset = reset
gb.pages = sider
gb.workingCopy = arbeidskopi
gb.workingCopyWarning = dette repositoriet har en arbeidskopi, og kan ikke pushes til.
gb.query = sp\u00F8rring
-gb.queryHelp = Standard lucene sp\u00F8rringssyntaks st\u00F8ttes.<p/><p/>Se <a target="_new" href="http://lucene.apache.org/core/old_versioned_docs/versions/3_5_0/queryparsersyntax.html">Lucene Query Parser Syntax</a> for mer info.
+gb.queryHelp = Standard lucene sp\u00F8rringssyntaks st\u00F8ttes.<p/><p/>Se ${querySyntax} for mer info.
+gb.querySyntax = Lucene Query Parser Syntax
gb.queryResults = resultater {0} - {1} ({2} treff)
gb.noHits = ingen treff
gb.authored = forfattet
gb.committed = committet
gb.indexedBranches = indekserte brancher
-gb.indexedBranchesDescription = velg branchene som skal inkluderes i din Lucene indeks.
+gb.indexedBranchesDescription = velg branchene som skal inkluderes i din Lucene indeks.
gb.noIndexedRepositoriesWarning = ingen av dine repositorier er satt opp med Lucene indeksering
gb.undefinedQueryWarning = sp\u00F8rringen er udefinert
gb.noSelectedRepositoriesWarning = vennligst velg en eller flere epositorier!
@@ -237,7 +237,7 @@ gb.passwordTooShort = Passorder er for kort. Minimum lengde er {0} tegn.
gb.passwordChanged = Passorder er endret.
gb.passwordChangeAborted = Passordendring ble avbrutt.
gb.pleaseSetRepositoryName = Vennligst sett repositorynavnet!
-gb.illegalLeadingSlash = Katalognavn kan ikke begynne med '/'
+gb.illegalLeadingSlash = Katalognavn kan ikke begynne med '/'
gb.illegalRelativeSlash = Relative katalognavn (../) kan ikke benyttes.
gb.illegalCharacterRepositoryName = Ugyldig tegn ''{0}'' i repositorynavnet!
gb.selectAccessRestriction = Vennligst velg tilgangskontrollniv\u00e5!
@@ -262,7 +262,7 @@ gb.failedToFindCommit = Fant ikke commit \"{0}\" i {1}!
gb.couldNotFindFederationProposal = Fant ikke fefereringsforslag!
gb.invalidUsernameOrPassword = Ugyldig brukernavn eller passord!
gb.OneProposalToReview = 1 federeringsforslag venter p\u00e5 behandling.
-gb.nFederationProposalsToReview = {0} federeingsforslag venter p\u00e5 behandling.
+gb.nFederationProposalsToReview = {0} federeingsforslag venter p\u00e5 behandling.
gb.couldNotFindTag = Fant ikke tag {0}
gb.couldNotCreateFederationProposal = Kunne ikke opprette federeringsforslag!
gb.pleaseSetGitblitUrl = Vennligst angi din Gitblit url!
@@ -332,7 +332,7 @@ gb.forkedFrom = forket fra
gb.canFork = kan forke
gb.canForkDescription = kan forke autoriserte repositorier til personlige repositorier
gb.myFork = se min fork
-gb.forksProhibited = forkig er ikke tillattt
+gb.forksProhibited = forkig er ikke tillattt
gb.forksProhibitedWarning = dette repositoriet tillater ikke forking
gb.noForks = {0} har ingen forks
gb.forkNotAuthorized = beklager, du har ikke tillatelse til \u00e5 forke {0}
@@ -340,16 +340,16 @@ gb.forkInProgress = forking p\u00e5g\u00e5r
gb.preparingFork = forbereder din fork...
gb.isFork = er en fork
gb.canCreate = kan opprette
-gb.canCreateDescription = kan opprette personlige repositorier
+gb.canCreateDescription = kan opprette personlige repositorier
gb.illegalPersonalRepositoryLocation = ditt personlige repository m\u00e5 v\u00e6re plassert p\u00e5 \"{0}\"
gb.verifyCommitter = verifiser committer
-gb.verifyCommitterDescription = krev at committer identitet matcher pushing Gitblit brukerkonto
+gb.verifyCommitterDescription = krev at committer identitet matcher pushing Gitblit brukerkonto
gb.verifyCommitterNote = alle merger krever "--no-ff" for \u00e5 beolde kjent committer identitet
gb.repositoryPermissions = repository tilganger
gb.userPermissions = brukertilganger
gb.teamPermissions = teamtilganger
gb.add = legg til
-gb.noPermission = SLETT DENNE TILGANGEN
+gb.noPermission = SLETT DENNE TILGANGEN
gb.excludePermission = {0} (ekskluder)
gb.viewPermission = {0} (vis)
gb.clonePermission = {0} (clone)
@@ -357,7 +357,7 @@ gb.pushPermission = {0} (push)
gb.createPermission = {0} (push, ref creation)
gb.deletePermission = {0} (push, ref creation+deletion)
gb.rewindPermission = {0} (push, ref creation+deletion+rewind)
-gb.permission = tilgang
+gb.permission = Tilgang
gb.regexPermission = denne tilgangen er gitt av regul\u00e6ruttrykket \"{0}\"
gb.accessDenied = ingen tilgang
gb.busyCollectingGarbage = beklager, Gitblit er opptatt med \u00e5 samle s\u00F8ppel {0}
@@ -398,21 +398,21 @@ gb.passwordHintRequired = passord hint er p\u00e5krevd!
gb.viewCertificate = vis sertifikat
gb.subject = subject
gb.issuer = utsteder
-gb.validFrom = gyldig fra
+gb.validFrom = gyldig fra
gb.validUntil = gyldig til
gb.publicKey = public key
gb.signatureAlgorithm = signaturalgoritme
gb.sha1FingerPrint = SHA-1 fingeravtrykk
gb.md5FingerPrint = MD5 fingeravtrykk
gb.reason = begrunnelse
-gb.revokeCertificateReason = Vennligst angi en grunn for tilbakekalling av sertifikatet.
+gb.revokeCertificateReason = Vennligst angi en grunn for tilbakekalling av sertifikatet.
gb.unspecified = uspesifisert
gb.keyCompromise = n\u00F8kkelen er komprommitert
gb.caCompromise = CA er komprommitert
gb.affiliationChanged = tilknytning endret
gb.superseded = erstattet
gb.cessationOfOperation = opph\u00F8rt
-gb.privilegeWithdrawn = privileget er trukket tilbake
+gb.privilegeWithdrawn = privileget er trukket tilbake
gb.time.inMinutes = om {0} minutter
gb.time.inHours = om {0} timer
gb.time.inDays = om {0} dager
@@ -445,12 +445,12 @@ gb.excludeFromActivity = ekskluder fra aktivitetssiden
gb.isSparkleshared = repositoriet er Sparkleshared
gb.owners = eiere
gb.sessionEnded = Sesjonen har blitt lukket
-gb.closeBrowser = Vennligst lukk nettleseren for \u00e5 avslutte sesjonen fullstendig.
+gb.closeBrowser = Vennligst lukk nettleseren for \u00e5 avslutte sesjonen fullstendig.
gb.doesNotExistInTree = {0} finnes ikke i treet {1}
gb.enableIncrementalPushTags = sl\u00e5 p\u00e5 inkrementelle push tags
gb.useIncrementalPushTagsDescription = ved pushing til repositoriet, tag hver branch med et inkrementelt revisjonsnummer
gb.incrementalPushTagMessage = Auto-tagget [{0}] branch ved push
-gb.externalPermissions = {0} tilgangsrettigheter blir vedlikeholdt eksternt.
+gb.externalPermissions = {0} tilgangsrettigheter blir vedlikeholdt eksternt.
gb.viewAccess = Du har hverken lese- eller skrivetilgang til Gitblit.
gb.overview = oversikt
gb.dashboard = dashboard
@@ -500,7 +500,7 @@ gb.owned = eid
gb.starredAndOwned = fulgt og eid
gb.reviewPatchset = gjennomgang {0} patchset {1}
gb.todaysActivityStats = i dag / {1} commits av {2} forfattere
-gb.todaysActivityNone = i dag / ingen
+gb.todaysActivityNone = i dag / ingen
gb.noActivityToday = det har ikke v\u00e6rt noen aktivitet i dag
gb.anonymousUser= anonym
gb.commitMessageRenderer = commit melding renderer
@@ -509,7 +509,7 @@ gb.home = hjem
gb.isMirror = dette repositoriet er et speil
gb.mirrorOf = speiling av {0}
gb.mirrorWarning = dette repositoriet er et speil, og kan ikke pushes til
-gb.docsWelcome1 = Du kan bruke dokumentasjon til \u00e5 dokumentrere ditt repository
+gb.docsWelcome1 = Du kan bruke dokumentasjon til \u00e5 dokumentrere ditt repository
gb.docsWelcome2 = Commit en README.md eller en HOME.md fil for \u00e5 komme i gang
gb.createReadme = lag en README
gb.responsible = ansvarlig
@@ -585,7 +585,7 @@ gb.due = frist
gb.queries = sp\u00F8rringer
gb.searchTicketsTooltip = s\u00F8k {0} tickets
gb.searchTickets = s\u00F8k tickets
-gb.new = ny
+gb.new = ny
gb.newTicket = ny ticket
gb.editTicket = endre ticket
gb.ticketsWelcome = Du kan bruke tickets til \u00e5 organisere din todo-liste, diskutere bugs og \u00e5 samarbeide om patchsets.
@@ -608,7 +608,7 @@ gb.checkout = checkout
gb.checkoutViaCommandLine = Checkout via kommandolinjen
gb.checkoutViaCommandLineNote = Du kan sjekke ut og teste disse endringene lokalt fra din klone av dette repositoriet.
gb.checkoutStep1 = Hent det gjeldende patchsettet \u2014 kj\u00F8r dette fra din prosjekt-katalog
-gb.checkoutStep2 = Checkout patchsettet til en ny branch og g\u00e5 igjennom koden
+gb.checkoutStep2 = Checkout patchsettet til en ny branch og g\u00e5 igjennom koden
gb.mergingViaCommandLine = Merging vha kommandolinjen
gb.mergingViaCommandLineNote = Hvis du ikke \u00F8nsker \u00e5 bruke merge-knappen, eller hvis en automatisk merge ikke kan bli utf\u00F8rt, kan du gj\u00F8re en manuell merge p\u00e5 kommandolinjen.
gb.mergeStep1 = Checkout en ny branch for \u00e5 g\u00e5 igjennom endringene \u2014 kj\u00F8r dette fra din prosjektkatalog
@@ -642,7 +642,7 @@ gb.patchsetN = patchsett {0}
gb.reviewedPatchsetRev = gjennomgikk patchset {0} revisjon {1}: {2}
gb.review = gjennomgang
gb.reviews = gjennomganger
-gb.veto = veto
+gb.veto = veto
gb.needsImprovement = m\u00e5 forbedres
gb.looksGood = ser bra ut
gb.approve = godkjenne
@@ -693,13 +693,13 @@ gb.anonymousPolicyDescription = Alle kan se, clone ofg pushe til dette repositor
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.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.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.initialCommitDescription = Dette vil la deg kunne <code>git clone</code> repositoriet med en gang. Hopp over dette skrittet hvis du allerede har kj\u00F8rt 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
@@ -719,7 +719,7 @@ gb.federationStrategyDescription = Kontroller om og hvordan dette repositoriet s
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.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.
@@ -736,9 +736,11 @@ 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.sshKeyComment = 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 \ No newline at end of file
+gb.transportPreference = Foretrukket transport
+gb.transportPreferenceDescription = Sett transportmetoden du foretrekker for cloning
+
+# This last property for unit tests to test successful loading of the resource file
+gb.loadLang = Norsk
diff --git a/src/main/java/com/gitblit/wicket/GitBlitWebApp_pl.properties b/src/main/java/com/gitblit/wicket/GitBlitWebApp_pl.properties
index b00d1ff8..a4753e72 100644
--- a/src/main/java/com/gitblit/wicket/GitBlitWebApp_pl.properties
+++ b/src/main/java/com/gitblit/wicket/GitBlitWebApp_pl.properties
@@ -17,7 +17,7 @@ gb.object = Obiekt
gb.ticketId = Id ticketu
gb.ticketAssigned = Przydzielony
gb.ticketOpenDate = Data otwarcia
-gb.ticketState = Status
+gb.ticketStatus = Status
gb.ticketComments = Komentarze
gb.view = Widok
gb.local = Lokalne
@@ -83,7 +83,7 @@ gb.notRestricted = Anonimowy podgl\u0105d, klonowanie i zapis
gb.pushRestricted = Uwierzytelniony zapis
gb.cloneRestricted = Uwierzytelnione klonowanie i zapis
gb.viewRestricted = Uwierzytelniony podgl\u0105d, klonowanie i zapis
-gb.useTicketsDescription = Rozproszone zg\u0142oszenia Ticgit
+gb.useTicketsDescription = Rozproszone zg\u0142oszenia Ticgit
gb.useDocsDescription = Parsuj znaczniki Markdown w repozytorium
gb.showRemoteBranchesDescription = Poka\u017C zdalne ga\u0142\u0119zie
gb.canAdminDescription = Mo\u017Ce administrowa\u0107 serwerem Gitblit
@@ -108,7 +108,7 @@ gb.federateOrigin = Federuj origin
gb.excludeFromFederation = Wy\u0142\u0105cz z federacji
gb.excludeFromFederationDescription = Blokuj sfederowane instancje Gitblit od pobierania tego konta
gb.tokens = Tokeny federacji
-gb.tokenAllDescription = Wszystkie repozytoria, u\u017Cytkownicy i ustawienia
+gb.tokenAllDescription = Wszystkie repozytoria, u\u017Cytkownicy i ustawienia
gb.tokenUnrDescription = Wszystkie repozytoria i u\u017Cytkownicy
gb.tokenJurDescription = Wszystkie repozytoria
gb.federatedRepositoryDefinitions = Definicje repozytori\u00F3w
@@ -169,7 +169,6 @@ gb.accessLevel = Poziom dost\u0119pu
gb.default = Domy\u015Blny
gb.setDefault = Ustaw domy\u015Blny
gb.since = Od
-gb.status = Status
gb.bootDate = Data uruchomienia
gb.servletContainer = Kontener serwlet\u00F3w
gb.heapMaximum = Maksymalny stos
@@ -217,7 +216,8 @@ gb.pages = Strony
gb.workingCopy = Kopia robocza
gb.workingCopyWarning = To repozytorium ma kopi\u0119 robocz\u0105 i nie mo\u017Ce otrzymywa\u0107 zmian
gb.query = Szukaj
-gb.queryHelp = Standardowa sk\u0142adnia wyszukiwa\u0144 jest wspierana.<p/><p/>Na stronie <a target="_new" href="http://lucene.apache.org/core/old_versioned_docs/versions/3_5_0/queryparsersyntax.html">Lucene Query Parser Syntax</a> dost\u0119pne s\u0105 dalsze szczeg\u00F3\u0142y.
+gb.queryHelp = Standardowa sk\u0142adnia wyszukiwa\u0144 jest wspierana.<p/><p/>Na stronie ${querySyntax} dost\u0119pne s\u0105 dalsze szczeg\u00F3\u0142y.
+gb.querySyntax = Lucene Query Parser Syntax
gb.queryResults = Wyniki {0} - {1} ({2} wynik\u00F3w)
gb.noHits = Brak wynik\u00F3w
gb.authored = utworzy\u0142
@@ -360,7 +360,7 @@ gb.permission = uprawnienie
gb.regexPermission = to uprawnienie jest nadane z wyra\u017Cenia regularnego \"{0}\"
gb.accessDenied = dost\u0119p zabroniony
gb.busyCollectingGarbage = Przepraszamy, Gitblit jest w trakcie od\u015Bmiecania zasob\u00F3w w {0}
-gb.gcPeriod = okres GC
+gb.gcPeriod = okres GC
gb.gcPeriodDescription = okres pomi\u0119dzy kolejnymi od\u015Bmieceniami
gb.gcThreshold = pr\u00F3g GC
gb.gcThresholdDescription = minimalna liczba wolnych obiekt\u00F3w jaka uruchomi wczesne od\u015Bmiecanie pami\u0119ci
@@ -460,7 +460,7 @@ gb.manual = r\u0119cznie
gb.from = od
gb.to = do
gb.at = w
-gb.of = z
+gb.of = z
gb.in = w
gb.moreChanges = wszystkie zmiany...
gb.pushedNCommitsTo = wgra\u0142(a) {0} zmian do
@@ -502,3 +502,6 @@ gb.todaysActivityStats = dzisiaj / {1} zmian przez {2} autor\u00F3w
gb.todaysActivityNone = dzisiaj / brak
gb.noActivityToday = brak aktywno\u015Bci w dniu dzisiejszym
gb.anonymousUser= anonimowy
+
+# This last property for unit tests to test successful loading of the resource file
+gb.loadLang = polszczyzna
diff --git a/src/main/java/com/gitblit/wicket/GitBlitWebApp_pt_BR.properties b/src/main/java/com/gitblit/wicket/GitBlitWebApp_pt_BR.properties
index 4d2ea553..26b6838d 100644
--- a/src/main/java/com/gitblit/wicket/GitBlitWebApp_pt_BR.properties
+++ b/src/main/java/com/gitblit/wicket/GitBlitWebApp_pt_BR.properties
@@ -1,24 +1,24 @@
-gb.repository = repositrio
-gb.owner = proprietrio
-gb.description = descrio
-gb.lastChange = ltima alterao
+gb.repository = reposit\u00f3rio
+gb.owner = propriet\u00e1rio
+gb.description = descri\u00e7\u00e3o
+gb.lastChange = \u00faltima altera\u00e7\u00e3o
gb.refs = refs
gb.tag = tag
gb.tags = tags
gb.author = autor
gb.committer = committer
gb.commit = commit
-gb.tree = rvore
+gb.tree = \u00e1rvore
gb.parent = parent
gb.url = URL
-gb.history = histrico
+gb.history = hist\u00f3rico
gb.raw = raw
gb.object = object
gb.ticketId = ticket id
-gb.ticketAssigned = atribudo
+gb.ticketAssigned = atribu\u00eddo
gb.ticketOpenDate = data da abertura
-gb.ticketState = estado
-gb.ticketComments = comentrios
+gb.ticketStatus = estado
+gb.ticketComments = coment\u00e1rios
gb.view = visualizar
gb.local = local
gb.remote = remote
@@ -31,13 +31,13 @@ gb.allTags = todas as tags...
gb.allBranches = todos os branches...
gb.summary = resumo
gb.ticket = ticket
-gb.newRepository = novo repositrio
-gb.newUser = novo usurio
+gb.newRepository = novo reposit\u00f3rio
+gb.newUser = novo usu\u00e1rio
gb.commitdiff = commitdiff
gb.tickets = tickets
gb.pageFirst = primeira
-gb.pagePrevious anterior
-gb.pageNext = prxima
+gb.pagePrevious = anterior
+gb.pageNext = pr\u00f3xima
gb.head = HEAD
gb.blame = blame
gb.login = login
@@ -45,19 +45,19 @@ gb.logout = logout
gb.username = username
gb.password = password
gb.tagger = tagger
-gb.moreHistory = mais histrico...
+gb.moreHistory = mais hist\u00f3rico...
gb.difftocurrent = diff para a atual
gb.search = pesquisar
-gb.searchForAuthor = Procurar por commits cujo autor
+gb.searchForAuthor = Procurar por commits cujo autor \u00e9
gb.searchForCommitter = Procurar por commits commitados por
gb.addition = adicionados
gb.modification = modificados
gb.deletion = apagados
gb.rename = renomear
-gb.metrics = mtricas
-gb.stats = estatsticas
+gb.metrics = m\u00e9tricas
+gb.stats = estat\u00edsticas
gb.markdown = markdown
-gb.changedFiles = arquivos alterados
+gb.changedFiles = arquivos alterados
gb.filesAdded = {0} arquivos adicionados
gb.filesModified = {0} arquivos modificados
gb.filesDeleted = {0} arquivos deletados
@@ -69,80 +69,80 @@ gb.searchTypeTooltip = Selecione o Tipo de Pesquisa
gb.searchTooltip = Pesquisar {0}
gb.delete = deletar
gb.docs = docs
-gb.accessRestriction = restrio de acesso
+gb.accessRestriction = restri\u00e7\u00e3o de acesso
gb.name = nome
gb.enableTickets = ativar tickets
-gb.enableDocs = ativar documentao
+gb.enableDocs = ativar documenta\u00e7\u00e3o
gb.save = salvar
gb.showRemoteBranches = mostrar branches remotos
-gb.editUsers = editar usurios
+gb.editUsers = editar usu\u00e1rios
gb.confirmPassword = confirmar password
-gb.restrictedRepositories = repositrios restritos
+gb.restrictedRepositories = reposit\u00f3rios restritos
gb.canAdmin = pode administrar
-gb.notRestricted = visualizao annima, clone, & push
-gb.pushRestricted = push autnticado
-gb.cloneRestricted = clone & push autnticados
-gb.viewRestricted = view, clone, & push autnticados
-gb.useTicketsDescription = somente leitura, issues do Ticgit distribudos
-gb.useDocsDescription = enumerar documentao Markdown no repositrio
+gb.notRestricted = visualiza\u00e7\u00e3o an\u00f4nima, clone, & push
+gb.pushRestricted = push aut\u00eanticado
+gb.cloneRestricted = clone & push aut\u00eanticados
+gb.viewRestricted = view, clone, & push aut\u00eanticados
+gb.useTicketsDescription = somente leitura, issues do Ticgit distribu\u00eddos
+gb.useDocsDescription = enumerar documenta\u00e7\u00e3o Markdown no reposit\u00f3rio
gb.showRemoteBranchesDescription = mostrar branches remotos
gb.canAdminDescription = pode administrar o server Gitblit
-gb.permittedUsers = usurios autorizados
+gb.permittedUsers = usu\u00e1rios autorizados
gb.isFrozen = congelar
gb.isFrozenDescription = proibir fazer push
gb.zip = zip
gb.showReadme = mostrar readme
-gb.showReadmeDescription = mostrar um arquivo \"leia-me\" na pgina de apresentao do projeto
-gb.nameDescription = utilizer a '/' para agrupar repositrios. e.g. libraries/mycoollib.git
-gb.ownerDescription = o proprietrio pode editar configuraes do repositrio
+gb.showReadmeDescription = mostrar um arquivo \"leia-me\" na p\u00e1gina de apresenta\u00e7\u00e3o do projeto
+gb.nameDescription = utilizer a '/' para agrupar reposit\u00f3rios. e.g. libraries/mycoollib.git
+gb.ownerDescription = o propriet\u00e1rio pode editar configura\u00e7\u00f5es do reposit\u00f3rio
gb.blob = blob
-gb.commitActivityTrend = tendncia dos commits
+gb.commitActivityTrend = tend\u00eancia dos commits
gb.commitActivityDOW = commits por dia da semana
gb.commitActivityAuthors = principais committers
gb.feed = feed
gb.cancel = cancelar
gb.changePassword = alterar password
-gb.isFederated = est federado
-gb.federateThis = federar este repositrio
+gb.isFederated = est\u00e1 federado
+gb.federateThis = federar este reposit\u00f3rio
gb.federateOrigin = federar o origin
-gb.excludeFromFederation = excluir da federao
-gb.excludeFromFederationDescription = bloquear instncias federadas do GitBlit de fazer pull desta conta
-gb.tokens = tokens de federao
-gb.tokenAllDescription = todos repositrios, usurios & configuraes
-gb.tokenUnrDescription = todos repositrios & usurios
-gb.tokenJurDescription = todos repositrios
-gb.federatedRepositoryDefinitions = definies de repositrio
-gb.federatedUserDefinitions = definies de usurios
-gb.federatedSettingDefinitions = definies de configuraes
-gb.proposals = propostas de federaes
+gb.excludeFromFederation = excluir da federa\u00e7\u00e3o
+gb.excludeFromFederationDescription = bloquear inst\u00e2ncias federadas do GitBlit de fazer pull desta conta
+gb.tokens = tokens de federa\u00e7\u00e3o
+gb.tokenAllDescription = todos reposit\u00f3rios, usu\u00e1rios & configura\u00e7\u00f5es
+gb.tokenUnrDescription = todos reposit\u00f3rios & usu\u00e1rios
+gb.tokenJurDescription = todos reposit\u00f3rios
+gb.federatedRepositoryDefinitions = defini\u00e7\u00f5es de reposit\u00f3rio
+gb.federatedUserDefinitions = defini\u00e7\u00f5es de usu\u00e1rios
+gb.federatedSettingDefinitions = defini\u00e7\u00f5es de configura\u00e7\u00f5es
+gb.proposals = propostas de federa\u00e7\u00f5es
gb.received = recebidos
gb.type = tipo
gb.token = token
-gb.repositories = repositrios
+gb.repositories = reposit\u00f3rios
gb.proposal = propostas
-gb.frequency = frequncia
+gb.frequency = frequ\u00eancia
gb.folder = pasta
-gb.lastPull = ltimo pull
-gb.nextPull = prximo pull
-gb.inclusions = incluses
-gb.exclusions = exclues
+gb.lastPull = \u00faltimo pull
+gb.nextPull = pr\u00f3ximo pull
+gb.inclusions = inclus\u00f5es
+gb.exclusions = exclu\u00f5es
gb.registration = cadastro
-gb.registrations = cadastro de federaes
+gb.registrations = cadastro de federa\u00e7\u00f5es
gb.sendProposal = enviar proposta
gb.status = status
gb.origin = origin
gb.headRef = default branch (HEAD)
gb.headRefDescription = alterar a ref a qual a HEAD aponta. e.g. refs/heads/master
-gb.federationStrategy = estratgia de federao
-gb.federationRegistration = cadastro de federaes
-gb.federationResults = resultados dos pulls de federaes
-gb.federationSets = ajustes de federaes
+gb.federationStrategy = estrat\u00e9gia de federa\u00e7\u00e3o
+gb.federationRegistration = cadastro de federa\u00e7\u00f5es
+gb.federationResults = resultados dos pulls de federa\u00e7\u00f5es
+gb.federationSets = ajustes de federa\u00e7\u00f5es
gb.message = mensagem
-gb.myUrlDescription = a url de acesso pblico para a instncia Gitblit
+gb.myUrlDescription = a url de acesso p\u00fablico para a inst\u00e2ncia Gitblit
gb.destinationUrl = enviar para
-gb.destinationUrlDescription = a url da intncia do Gitblit para enviar sua proposta
-gb.users = usurios
-gb.federation = federao
+gb.destinationUrlDescription = a url da int\u00e2ncia do Gitblit para enviar sua proposta
+gb.users = usu\u00e1rios
+gb.federation = federa\u00e7\u00e3o
gb.error = erro
gb.refresh = atualizar
gb.browse = navegar
@@ -151,169 +151,169 @@ gb.filter = filtrar
gb.create = criar
gb.servers = servidores
gb.recent = recente
-gb.available = disponvel
+gb.available = dispon\u00edvel
gb.selected = selecionado
gb.size = tamanho
gb.downloading = downloading
gb.loading = loading
gb.starting = inciando
gb.general = geral
-gb.settings = configuraes
+gb.settings = configura\u00e7\u00f5es
gb.manage = administrar
-gb.lastLogin = ltimo login
-gb.skipSizeCalculation = ignorar clculo do tamanho
-gb.skipSizeCalculationDescription = no calcular o tamanho do repositrio (reduz o tempo de load da pgina)
-gb.skipSummaryMetrics = ignorar resumo das mtricas
-gb.skipSummaryMetricsDescription = no calcular mtricas na pgina de resumo
+gb.lastLogin = \u00faltimo login
+gb.skipSizeCalculation = ignorar c\u00e1lculo do tamanho
+gb.skipSizeCalculationDescription = n\u00e3o calcular o tamanho do reposit\u00f3rio (reduz o tempo de load da p\u00e1gina)
+gb.skipSummaryMetrics = ignorar resumo das m\u00e9tricas
+gb.skipSummaryMetricsDescription = n\u00e3o calcular m\u00e9tricas na p\u00e1gina de resumo
gb.accessLevel = acesso
gb.default = default
gb.setDefault = tornar default
gb.since = desde
-gb.status = status
gb.bootDate = data do boot
gb.servletContainer = servlet container
-gb.heapMaximum = heap mximo
+gb.heapMaximum = heap m\u00e1ximo
gb.heapAllocated = alocar heap
gb.heapUsed = usar heap
gb.free = free
-gb.version = verso
+gb.version = vers\u00e3o
gb.releaseDate = data de release
gb.date = data
gb.activity = atividade
gb.subscribe = inscrever
gb.branch = branch
-gb.maxHits = hits mximos
+gb.maxHits = hits m\u00e1ximos
gb.recentActivity = atividade recente
-gb.recentActivityStats = ltimos {0} dias / {1} commits por {2} autores
-gb.recentActivityNone = ltimos {0} dias / nenhum
-gb.dailyActivity = atividade diria
-gb.activeRepositories = repositrios ativos
+gb.recentActivityStats = \u00faltimos {0} dias / {1} commits por {2} autores
+gb.recentActivityNone = \u00faltimos {0} dias / nenhum
+gb.dailyActivity = atividade di\u00e1ria
+gb.activeRepositories = reposit\u00f3rios ativos
gb.activeAuthors = autores ativos
gb.commits = commits
gb.teams = equipes
gb.teamName = nome da equipe
gb.teamMembers = membros
-gb.teamMemberships = filiaes em equipes
+gb.teamMemberships = filia\u00e7\u00f5es em equipes
gb.newTeam = nova equipe
gb.permittedTeams = equipes permitidas
-gb.emptyRepository = repositrio vazio
-gb.repositoryUrl = url do repositrio
+gb.emptyRepository = reposit\u00f3rio vazio
+gb.repositoryUrl = url do reposit\u00f3rio
gb.mailingLists = listas de e-mails
gb.preReceiveScripts = pre-receive scripts
gb.postReceiveScripts = post-receive scripts
gb.hookScripts = hook scripts
gb.customFields = campos customizados
-gb.customFieldsDescription = campos customizados disponveis para Groovy hooks
-gb.accessPermissions = permisses de acesso
+gb.customFieldsDescription = campos customizados dispon\u00edveis para Groovy hooks
+gb.accessPermissions = permiss\u00f5es de acesso
gb.filters = filtros
-gb.generalDescription = configuraes comuns
-gb.accessPermissionsDescription = restringir acesso por usurios e equipes
-gb.accessPermissionsForUserDescription = ajustar filiaes em equipes ou garantir acesso a repositrios especficos
-gb.accessPermissionsForTeamDescription = ajustar membros da equipe e garantir acesso a repositrios especficos
-gb.federationRepositoryDescription = compartilhar este repositrio com outros servidores Gitblit
+gb.generalDescription = configura\u00e7\u00f5es comuns
+gb.accessPermissionsDescription = restringir acesso por usu\u00e1rios e equipes
+gb.accessPermissionsForUserDescription = ajustar filia\u00e7\u00f5es em equipes ou garantir acesso a reposit\u00f3rios espec\u00edficos
+gb.accessPermissionsForTeamDescription = ajustar membros da equipe e garantir acesso a reposit\u00f3rios espec\u00edficos
+gb.federationRepositoryDescription = compartilhar este reposit\u00f3rio com outros servidores Gitblit
gb.hookScriptsDescription = rodar scripts Groovy nos pushes para este servidor Gitblit
gb.reset = reset
-gb.pages = pginas
+gb.pages = p\u00e1ginas
gb.workingCopy = working copy
-gb.workingCopyWarning = este repositrio tem uma working copy e no pode receber pushes
+gb.workingCopyWarning = este reposit\u00f3rio tem uma working copy e n\u00e3o pode receber pushes
gb.query = query
-gb.queryHelp = Standard query syntax suportada.<p/><p/>Por favor veja <a target="_new" href="http://lucene.apache.org/core/old_versioned_docs/versions/3_5_0/queryparsersyntax.html">Lucene Query Parser Syntax</a> para mais detalhes.
+gb.queryHelp = Standard query syntax \u00e9 suportada.<p/><p/>Por favor veja ${querySyntax} para mais detalhes.
+gb.querySyntax = Lucene Query Parser Syntax
gb.queryResults = resultados {0} - {1} ({2} hits)
gb.noHits = sem hits
gb.authored = foi autor de
gb.committed = committed
gb.indexedBranches = branches indexados
-gb.indexedBranchesDescription = selecione os branches para incluir nos seus ndices Lucene
-gb.noIndexedRepositoriesWarning = nenhum dos seus repositrios foram configurados para indexao do Lucene
-gb.undefinedQueryWarning = a query no foi definida!
-gb.noSelectedRepositoriesWarning = por favor selecione um ou mais repositrios!
-gb.luceneDisabled = indexao do Lucene est desabilitada
+gb.indexedBranchesDescription = selecione os branches para incluir nos seus \u00edndices Lucene
+gb.noIndexedRepositoriesWarning = nenhum dos seus reposit\u00f3rios foram configurados para indexa\u00e7\u00e3o do Lucene
+gb.undefinedQueryWarning = a query n\u00e3o foi definida!
+gb.noSelectedRepositoriesWarning = por favor selecione um ou mais reposit\u00f3rios!
+gb.luceneDisabled = indexa\u00e7\u00e3o do Lucene est\u00e1 desabilitada
gb.failedtoRead = leitura falhou
-gb.isNotValidFile = no um arquivo vlido
+gb.isNotValidFile = n\u00e3o \u00e9 um arquivo v\u00e1lido
gb.failedToReadMessage = falha ao ler mensagens default de {0}!
-gb.passwordsDoNotMatch = Passwords no conferem!
-gb.passwordTooShort = Password muito curto. Tamanho mnimo so {0} caracteres.
+gb.passwordsDoNotMatch = Passwords n\u00e3o conferem!
+gb.passwordTooShort = Password \u00e9 muito curto. Tamanho m\u00ednimo s\u00e3o {0} caracteres.
gb.passwordChanged = Password alterado com sucesso.
-gb.passwordChangeAborted = alterao do password foi abortada.
-gb.pleaseSetRepositoryName = Por favor ajuste o nome do repositrio!
-gb.illegalLeadingSlash = Referncias a diretrios raiz comeando com (/) so proibidas.
-gb.illegalRelativeSlash = Referncias a diretrios relativos (../) so proibidas.
-gb.illegalCharacterRepositoryName = Caractere ilegal ''{0}'' no nome do repositrio!
-gb.selectAccessRestriction = Por favor selecione a restrio de acesso!
-gb.selectFederationStrategy = Por favor selecione a estratgia de federao!
+gb.passwordChangeAborted = altera\u00e7\u00e3o do password foi abortada.
+gb.pleaseSetRepositoryName = Por favor ajuste o nome do reposit\u00f3rio!
+gb.illegalLeadingSlash = Refer\u00eancias a diret\u00f3rios raiz come\u00e7ando com (/) s\u00e3o proibidas.
+gb.illegalRelativeSlash = Refer\u00eancias a diret\u00f3rios relativos (../) s\u00e3o proibidas.
+gb.illegalCharacterRepositoryName = Caractere ilegal ''{0}'' no nome do reposit\u00f3rio!
+gb.selectAccessRestriction = Por favor selecione a restri\u00e7\u00e3o de acesso!
+gb.selectFederationStrategy = Por favor selecione a estrat\u00e9gia de federa\u00e7\u00e3o!
gb.pleaseSetTeamName = Por favor insira um nome de equipe!
-gb.teamNameUnavailable = O nome de equipe ''{0}'' est indisponvel.
-gb.teamMustSpecifyRepository = Uma equipe deve especificar pelo menos um repositrio.
+gb.teamNameUnavailable = O nome de equipe ''{0}'' est\u00e1 indispon\u00edvel.
+gb.teamMustSpecifyRepository = Uma equipe deve especificar pelo menos um reposit\u00f3rio.
gb.teamCreated = Nova equipe ''{0}'' criada com sucesso.
gb.pleaseSetUsername = Por favor entre com um username!
-gb.usernameUnavailable = Username ''{0}'' est indisponvel.
-gb.combinedMd5Rename = Gitblit est configurado para usar um hash combinado-md5. Voc deve inserir um novo password ao renamear a conta.
-gb.userCreated = Novo usurio ''{0}'' criado com sucesso.
-gb.couldNotFindFederationRegistration = No foi possvel localizar o registro da federao!
+gb.usernameUnavailable = Username ''{0}'' est\u00e1 indispon\u00edvel.
+gb.combinedMd5Rename = Gitblit est\u00e1 configurado para usar um hash combinado-md5. Voc\u00ea deve inserir um novo password ao renamear a conta.
+gb.userCreated = Novo usu\u00e1rio ''{0}'' criado com sucesso.
+gb.couldNotFindFederationRegistration = N\u00e3o foi poss\u00edvel localizar o registro da federa\u00e7\u00e3o!
gb.failedToFindGravatarProfile = Falha ao localizar um perfil Gravatar para {0}
gb.branchStats = {0} commits e {1} tags em {2}
-gb.repositoryNotSpecified = Repositrio no especficado!
-gb.repositoryNotSpecifiedFor = Repositrio no especficado para {0}!
-gb.canNotLoadRepository = No foi possvel carregar o repositrio
-gb.commitIsNull = Commit est nulo
-gb.unauthorizedAccessForRepository = Acesso no autorizado para o repositrio
-gb.failedToFindCommit = No foi possvel achar o commit \"{0}\" em {1} para {2} pgina!
-gb.couldNotFindFederationProposal = No foi possvel localizar propostas de federao!
-gb.invalidUsernameOrPassword = username ou password invlido!
-gb.OneProposalToReview = Existe uma proposta de federao aguardando reviso.
-gb.nFederationProposalsToReview = Existem {0} propostas de federao aguardando reviso.
-gb.couldNotFindTag = No foi possvel localizar a tag {0}
-gb.couldNotCreateFederationProposal = No foi possvel criar uma proposta de federation!
+gb.repositoryNotSpecified = Reposit\u00f3rio n\u00e3o espec\u00edficado!
+gb.repositoryNotSpecifiedFor = Reposit\u00f3rio n\u00e3o espec\u00edficado para {0}!
+gb.canNotLoadRepository = N\u00e3o foi poss\u00edvel carregar o reposit\u00f3rio
+gb.commitIsNull = Commit est\u00e1 nulo
+gb.unauthorizedAccessForRepository = Acesso n\u00e3o autorizado para o reposit\u00f3rio
+gb.failedToFindCommit = N\u00e3o foi poss\u00edvel achar o commit \"{0}\" em {1} para {2} p\u00e1gina!
+gb.couldNotFindFederationProposal = N\u00e3o foi poss\u00edvel localizar propostas de federa\u00e7\u00e3o!
+gb.invalidUsernameOrPassword = username ou password inv\u00e1lido!
+gb.OneProposalToReview = Existe uma proposta de federa\u00e7\u00e3o aguardando revis\u00e3o.
+gb.nFederationProposalsToReview = Existem {0} propostas de federa\u00e7\u00e3o aguardando revis\u00e3o.
+gb.couldNotFindTag = N\u00e3o foi poss\u00edvel localizar a tag {0}
+gb.couldNotCreateFederationProposal = N\u00e3o foi poss\u00edvel criar uma proposta de federation!
gb.pleaseSetGitblitUrl = Por favor insira sua url do Gitblit!
gb.pleaseSetDestinationUrl = Por favor insira a url de destino para sua proposta!
gb.proposalReceived = Proposta recebida com sucesso por {0}.
-gb.noGitblitFound = Desculpe, {0} no localizou uma instncia do Gitblit em {1}.
-gb.noProposals = Desculpe, {0} no est aceitando propostas agora.
-gb.noFederation = Desculpe, {0} no est configurado com nenhuma intncia do Gitblit.
-gb.proposalFailed = Desculpe, {0} no recebeu nenhum dado de proposta!
+gb.noGitblitFound = Desculpe, {0} n\u00e3o localizou uma inst\u00e2ncia do Gitblit em {1}.
+gb.noProposals = Desculpe, {0} n\u00e3o est\u00e1 aceitando propostas agora.
+gb.noFederation = Desculpe, {0} n\u00e3o est\u00e1 configurado com nenhuma int\u00e2ncia do Gitblit.
+gb.proposalFailed = Desculpe, {0} n\u00e3o recebeu nenhum dado de proposta!
gb.proposalError = Desculpe, {0} reportou que um erro inesperado ocorreu!
-gb.failedToSendProposal = No foi possvel enviar a proposta!
-gb.userServiceDoesNotPermitAddUser = {0} no permite adicionar uma conta de usurio!
-gb.userServiceDoesNotPermitPasswordChanges = {0} no permite alteraes no password!
+gb.failedToSendProposal = N\u00e3o foi poss\u00edvel enviar a proposta!
+gb.userServiceDoesNotPermitAddUser = {0} n\u00e3o permite adicionar uma conta de usu\u00e1rio!
+gb.userServiceDoesNotPermitPasswordChanges = {0} n\u00e3o permite altera\u00e7\u00f5es no password!
gb.displayName = nome
gb.emailAddress = e-mail
-gb.errorAdminLoginRequired = Administrao requer um login
-gb.errorOnlyAdminMayCreateRepository = Somente umadministrador pode criar um repositrio
-gb.errorOnlyAdminOrOwnerMayEditRepository = Somente umadministrador pode editar um repositrio
-gb.errorAdministrationDisabled = Administrao est desabilitada
-gb.lastNDays = ltimos {0} dias
+gb.errorAdminLoginRequired = Administra\u00e7\u00e3o requer um login
+gb.errorOnlyAdminMayCreateRepository = Somente umadministrador pode criar um reposit\u00f3rio
+gb.errorOnlyAdminOrOwnerMayEditRepository = Somente umadministrador pode editar um reposit\u00f3rio
+gb.errorAdministrationDisabled = Administra\u00e7\u00e3o est\u00e1 desabilitada
+gb.lastNDays = \u00faltimos {0} dias
gb.completeGravatarProfile = Profile completo em Gravatar.com
gb.none = nenhum
gb.line = linha
-gb.content = contedo
+gb.content = conte\u00fado
gb.empty = vazio
gb.inherited = herdado
-gb.deleteRepository = Deletar repositrio \"{0}\"?
-gb.repositoryDeleted = Repositrio ''{0}'' deletado.
-gb.repositoryDeleteFailed = No foi possvel apagar o repositrio ''{0}''!
-gb.deleteUser = Deletar usurio \"{0}\"?
-gb.userDeleted = Usurio ''{0}'' deletado.
-gb.userDeleteFailed = No foi possvel apagar o usurio ''{0}''!
+gb.deleteRepository = Deletar reposit\u00f3rio \"{0}\"?
+gb.repositoryDeleted = Reposit\u00f3rio ''{0}'' deletado.
+gb.repositoryDeleteFailed = N\u00e3o foi poss\u00edvel apagar o reposit\u00f3rio ''{0}''!
+gb.deleteUser = Deletar usu\u00e1rio \"{0}\"?
+gb.userDeleted = Usu\u00e1rio ''{0}'' deletado.
+gb.userDeleteFailed = N\u00e3o foi poss\u00edvel apagar o usu\u00e1rio ''{0}''!
gb.time.justNow = agora mesmo
gb.time.today = hoje
gb.time.yesterday = ontem
-gb.time.minsAgo = h {0} minutos
-gb.time.hoursAgo = h {0} horas
-gb.time.daysAgo = h {0} dias
-gb.time.weeksAgo = h {0} semanas
-gb.time.monthsAgo = h {0} meses
-gb.time.oneYearAgo = h 1 ano
-gb.time.yearsAgo = h {0} anos
+gb.time.minsAgo = h\u00e1 {0} minutos
+gb.time.hoursAgo = h\u00e1 {0} horas
+gb.time.daysAgo = h\u00e1 {0} dias
+gb.time.weeksAgo = h\u00e1 {0} semanas
+gb.time.monthsAgo = h\u00e1 {0} meses
+gb.time.oneYearAgo = h\u00e1 1 ano
+gb.time.yearsAgo = h\u00e1 {0} anos
gb.duration.oneDay = 1 dia
gb.duration.days = {0} dias
-gb.duration.oneMonth = 1 ms
+gb.duration.oneMonth = 1 m\u00eas
gb.duration.months = {0} meses
gb.duration.oneYear = 1 ano
gb.duration.years = {0} anos
-gb.authorizationControl = controle de autorizao
-gb.allowAuthenticatedDescription = conceder permisso RW+ para todos os usurios autnticados
-gb.allowNamedDescription = conceder permisses refinadas para usurios escolhidos ou equipes
-gb.markdownFailure = No foi possvel converter contedo Markdown!
+gb.authorizationControl = controle de autoriza\u00e7\u00e3o
+gb.allowAuthenticatedDescription = conceder permiss\u00e3o RW+ para todos os usu\u00e1rios aut\u00eanticados
+gb.allowNamedDescription = conceder permiss\u00f5es refinadas para usu\u00e1rios escolhidos ou equipes
+gb.markdownFailure = N\u00e3o foi poss\u00edvel converter conte\u00fado Markdown!
gb.clearCache = limpar o cache
gb.projects = projetos
gb.project = projeto
@@ -323,32 +323,32 @@ gb.fork = fork
gb.forks = forks
gb.forkRepository = fork {0}?
gb.repositoryForked = fork feito em {0}
-gb.repositoryForkFailed= no foi possvel fazer fork
-gb.personalRepositories = repositrios pessoais
+gb.repositoryForkFailed= n\u00e3o foi poss\u00edvel fazer fork
+gb.personalRepositories = reposit\u00f3rios pessoais
gb.allowForks = permitir forks
-gb.allowForksDescription = permitir usurios autorizados a fazer fork deste repositrio
+gb.allowForksDescription = permitir usu\u00e1rios autorizados a fazer fork deste reposit\u00f3rio
gb.forkedFrom = forked de
gb.canFork = pode fazer fork
-gb.canForkDescription = pode fazer fork de repositrios autorizados para repositrios pessoais
+gb.canForkDescription = pode fazer fork de reposit\u00f3rios autorizados para reposit\u00f3rios pessoais
gb.myFork = visualizar meu fork
gb.forksProhibited = forks proibidos
-gb.forksProhibitedWarning = este repositrio probe forks
-gb.noForks = {0} no possui forks
-gb.forkNotAuthorized = desculpe, voc no est autorizado a fazer fork de {0}
+gb.forksProhibitedWarning = este reposit\u00f3rio pro\u00edbe forks
+gb.noForks = {0} n\u00e3o possui forks
+gb.forkNotAuthorized = desculpe, voc\u00ea n\u00e3o est\u00e1 autorizado a fazer fork de {0}
gb.forkInProgress = fork em progresso
gb.preparingFork = preparando seu fork...
-gb.isFork = fork
+gb.isFork = \u00e9 fork
gb.canCreate = pode criar
-gb.canCreateDescription = pode criar repositrios pessoais
-gb.illegalPersonalRepositoryLocation = seu repositrio pessoal deve estar localizado em \"{0}\"
+gb.canCreateDescription = pode criar reposit\u00f3rios pessoais
+gb.illegalPersonalRepositoryLocation = seu reposit\u00f3rio pessoal deve estar localizado em \"{0}\"
gb.verifyCommitter = verificar committer
gb.verifyCommitterDescription = requer a identidade do committer para combinar com uma conta do Gitblt
gb.verifyCommitterNote = todos os merges requerem "--no-ff" para impor a identidade do committer
-gb.repositoryPermissions = permisses de repositrio
-gb.userPermissions = permisses de usurio
-gb.teamPermissions = permisses de equipe
+gb.repositoryPermissions = permiss\u00f5es de reposit\u00f3rio
+gb.userPermissions = permiss\u00f5es de usu\u00e1rio
+gb.teamPermissions = permiss\u00f5es de equipe
gb.add = add
-gb.noPermission = APAGAR ESTA PERMISSO
+gb.noPermission = APAGAR ESTA PERMISS\u00c3O
gb.excludePermission = {0} (excluir)
gb.viewPermission = {0} (visualizar)
gb.clonePermission = {0} (clonar)
@@ -356,102 +356,102 @@ gb.pushPermission = {0} (push)
gb.createPermission = {0} (push, ref creation)
gb.deletePermission = {0} (push, ref creation+deletion)
gb.rewindPermission = {0} (push, ref creation+deletion+rewind)
-gb.permission = permisso
-gb.regexPermission = esta permisso foi configurada atravs da expresso regular \"{0}\"
+gb.permission = permiss\u00e3o
+gb.regexPermission = esta permiss\u00e3o foi configurada atrav\u00e9s da express\u00e3o regular \"{0}\"
gb.accessDenied = acesso negado
-gb.busyCollectingGarbage = desculpe, o Gitblit est ocupado coletando lixo em {0}
-gb.gcPeriod = perodo do GC
-gb.gcPeriodDescription = durao entre as coletas de lixo
-gb.gcThreshold = limite do GC
-gb.gcThresholdDescription = tamanho total mnimo de objetos \"abandonados\" que ativam a coleta de lixo
-gb.ownerPermission = proprietrio do repositrio
+gb.busyCollectingGarbage = desculpe, o Gitblit est\u00e1 ocupado coletando lixo em {0}
+gb.gcPeriod = per\u00edodo do GC
+gb.gcPeriodDescription = dura\u00e7\u00e3o entre as coletas de lixo
+gb.gcThreshold = limite do GC
+gb.gcThresholdDescription = tamanho total m\u00ednimo de objetos \"abandonados\" que ativam a coleta de lixo
+gb.ownerPermission = propriet\u00e1rio do reposit\u00f3rio
gb.administrator = administrador
gb.administratorPermission = administrador do Gitblit
gb.team = equipe
-gb.teamPermission = permisso concedida pela filiao a equipe \"{0}\"
+gb.teamPermission = permiss\u00e3o concedida pela filia\u00e7\u00e3o a equipe \"{0}\"
gb.missing = faltando!
-gb.missingPermission = o repositrio para esta permisso est faltando!
-gb.mutable = mutvel
-gb.specified = especfico
+gb.missingPermission = o reposit\u00f3rio para esta permiss\u00e3o est\u00e1 faltando!
+gb.mutable = mut\u00e1vel
+gb.specified = espec\u00edfico
gb.effective = efetivo
gb.organizationalUnit = unidade organizacional
-gb.organization = organizao
+gb.organization = organiza\u00e7\u00e3o
gb.locality = localidade
-gb.stateProvince = estado ou provncia
-gb.countryCode = cdigo do pas
+gb.stateProvince = estado ou prov\u00edncia
+gb.countryCode = c\u00f3digo do pa\u00eds
gb.properties = propriedades
gb.issued = emitido
gb.expires = expira
gb.expired = expirado
gb.expiring = expirando
gb.revoked = revogado
-gb.serialNumber = nmero serial
+gb.serialNumber = n\u00famero serial
gb.certificates = certificados
gb.newCertificate = novo certificado
gb.revokeCertificate = revogar certificado
gb.sendEmail = enviar email
gb.passwordHint = dica de password
gb.ok = ok
-gb.invalidExpirationDate = data de expirao invlida!
+gb.invalidExpirationDate = data de expira\u00e7\u00e3o inv\u00e1lida!
gb.passwordHintRequired = dica de password requerida!
gb.viewCertificate = visualizar certificado
gb.subject = assunto
gb.issuer = emissor
-gb.validFrom = vlido a partir de
-gb.validUntil = vlido at
-gb.publicKey = chave pblica
+gb.validFrom = v\u00e1lido a partir de
+gb.validUntil = v\u00e1lido at\u00e9
+gb.publicKey = chave p\u00fablica
gb.signatureAlgorithm = algoritmo de assinatura
-gb.sha1FingerPrint = digital SHA-1
+gb.sha1FingerPrint = digital SHA-1
gb.md5FingerPrint = digital MD5
-gb.reason = razo
-gb.revokeCertificateReason = Por selecione a razo da revogao do certificado
-gb.unspecified = no especfico
+gb.reason = raz\u00e3o
+gb.revokeCertificateReason = Por selecione a raz\u00e3o da revoga\u00e7\u00e3o do certificado
+gb.unspecified = n\u00e3o espec\u00edfico
gb.keyCompromise = comprometimento de chave
gb.caCompromise = compromisso CA
-gb.affiliationChanged = afiliao foi alterada
-gb.superseded = substitudas
-gb.cessationOfOperation = cessao de funcionamento
-gb.privilegeWithdrawn = privilgio retirado
+gb.affiliationChanged = afilia\u00e7\u00e3o foi alterada
+gb.superseded = substitu\u00eddas
+gb.cessationOfOperation = cessa\u00e7\u00e3o de funcionamento
+gb.privilegeWithdrawn = privil\u00e9gio retirado
gb.time.inMinutes = em {0} minutos
gb.time.inHours = em {0} horas
gb.time.inDays = em {0} dias
gb.hostname = hostname
gb.hostnameRequired = Por favor insira um hostname
gb.newSSLCertificate = novo servidor de certificado SSL
-gb.newCertificateDefaults = novos padres de certificao
-gb.duration = durao
-gb.certificateRevoked = Certificado {0, nmero, 0} foi revogado
+gb.newCertificateDefaults = novos padr\u00f5es de certifica\u00e7\u00e3o
+gb.duration = dura\u00e7\u00e3o
+gb.certificateRevoked = Certificado {0, n\u00famero, 0} foi revogado
gb.clientCertificateGenerated = Novo certificado cliente para {0} foi gerado com sucesso
gb.sslCertificateGenerated = Novo servidor de certificado SSL gerado com sucesso para {0}
-gb.newClientCertificateMessage = OBSERVAO:\nO 'password' no o password do usurio mas sim o password usado para proteger a keystore. Este password no ser salvo ento voc tambm inserir uma dica que ser includa nas instrues de LEIA-ME do usurio.
+gb.newClientCertificateMessage = OBSERVA\u00c7\u00c3O:\nO 'password' n\u00e3o \u00e9 o password do usu\u00e1rio mas sim o password usado para proteger a keystore. Este password n\u00e3o ser\u00e1 salvo ent\u00e3o voc\u00ea tamb\u00e9m inserir uma dica que ser\u00e1 inclu\u00edda nas instru\u00e7\u00f5es de LEIA-ME do usu\u00e1rio.
gb.certificate = certificado
gb.emailCertificateBundle = pacote certificado de cliente de email
gb.pleaseGenerateClientCertificate = Por favor gere um certificado cliente para {0}
gb.clientCertificateBundleSent = Pacote de certificado de cliente para {0} enviada
gb.enterKeystorePassword = Por favor insira uma chave para keystore do Gitblit
gb.warning = warning
-gb.jceWarning = Seu Java Runtime Environment no tem os arquivos \"JCE Unlimited Strength Jurisdiction Policy\".\nIsto ir limitar o tamanho dos passwords que voc usar para encriptar suas keystores para 7 caracteres.\nEstes arquivos de polticas so um download opcional da Oracle.\n\nVoc gostaria de continuar e gerar os certificados de infraestrutura de qualquer forma?\n\nRespondendo "No" ir redirecionar o seu browser para a pgina de downloads da Oracle, de onde voc poder fazer download desses arquivos.
-gb.maxActivityCommits = limitar exibio de commits
-gb.maxActivityCommitsDescription = quantidade mxima de commits para contribuir para a pgina de atividade
+gb.jceWarning = Seu Java Runtime Environment n\u00e3o tem os arquivos \"JCE Unlimited Strength Jurisdiction Policy\".\nIsto ir\u00e1 limitar o tamanho dos passwords que voc\u00ea usar\u00e1 para encriptar suas keystores para 7 caracteres.\nEstes arquivos de pol\u00edticas s\u00e3o um download opcional da Oracle.\n\nVoc\u00ea gostaria de continuar e gerar os certificados de infraestrutura de qualquer forma?\n\nRespondendo "N\u00e3o" ir\u00e1 redirecionar o seu browser para a p\u00e1gina de downloads da Oracle, de onde voc\u00ea poder\u00e1 fazer download desses arquivos.
+gb.maxActivityCommits = limitar exibi\u00e7\u00e3o de commits
+gb.maxActivityCommitsDescription = quantidade m\u00e1xima de commits para contribuir para a p\u00e1gina de atividade
gb.noMaximum = ilimitado
gb.attributes = atributos
gb.serveCertificate = servir https com este certificado
-gb.sslCertificateGeneratedRestart = Novo certificado SSL de servidor gerado com sucesso para {0}.\nVoc deve reiniciar o Gitblit para usar o novo certificado.\n\nSe voc estiver executando com o parmetro '--alias', voc precisar alter-lo para ''--alias {0}''.
+gb.sslCertificateGeneratedRestart = Novo certificado SSL de servidor gerado com sucesso para {0}.\nVoc\u00ea deve reiniciar o Gitblit para usar o novo certificado.\n\nSe voc\u00ea estiver executando com o par\u00e2metro '--alias', voc\u00ea precisar\u00e1 alter\u00e1-lo para ''--alias {0}''.
gb.validity = validade
gb.siteName = nome do site
gb.siteNameDescription = breve, mas ainda assim um nome descritivo para seu servidor
-gb.excludeFromActivity = excluir da pgina de atividades
-gb.isSparkleshared = repositrio Sparkleshared
-gb.owners = proprietrios
-gb.sessionEnded = A Sesso foi encerrada
-gb.closeBrowser = Por favor feche a tela do browser para encerrar a sesso apropriadamente.
-gb.doesNotExistInTree = {0} no existe na rvore {1}
+gb.excludeFromActivity = excluir da p\u00e1gina de atividades
+gb.isSparkleshared = reposit\u00f3rio \u00e9 Sparkleshared
+gb.owners = propriet\u00e1rios
+gb.sessionEnded = A Sess\u00e3o foi encerrada
+gb.closeBrowser = Por favor feche a tela do browser para encerrar a sess\u00e3o apropriadamente.
+gb.doesNotExistInTree = {0} n\u00e3o existe na \u00e1rvore {1}
gb.enableIncrementalPushTags = ativar push tags incrementais
-gb.useIncrementalPushTagsDescription = quando fizer push, automaticamente taguear cada branch com um nmero de reviso incremental
-gb.incrementalPushTagMessage = [{0}] branch foi tagueado automticamente depois do push
-gb.externalPermissions = {0} permisses de acesso so externalizadas
-gb.viewAccess = Voc no tem permisso para ler ou escrever o Gitblit
-gb.overview = viso geral
+gb.useIncrementalPushTagsDescription = quando fizer push, automaticamente taguear cada branch com um n\u00famero de revis\u00e3o incremental
+gb.incrementalPushTagMessage = [{0}] branch foi tagueado autom\u00e1ticamente depois do push
+gb.externalPermissions = {0} permiss\u00f5es de acesso s\u00e3o externalizadas
+gb.viewAccess = Voc\u00ea n\u00e3o tem permiss\u00e3o para ler ou escrever o Gitblit
+gb.overview = vis\u00e3o geral
gb.dashboard = dashboard
gb.monthlyActivity = atividade mensal
gb.myProfile = meu perfil
@@ -462,14 +462,14 @@ gb.to = para
gb.at = em
gb.of = de
gb.in = em
-gb.moreChanges = todas mudanas...
+gb.moreChanges = todas mudan\u00e7as...
gb.pushedNCommitsTo = fez push de {0} commits para
gb.pushedOneCommitTo = fez push de 1 commit para
gb.commitsTo = {0} commits para
gb.oneCommitTo = 1 commit para
gb.byNAuthors = por {0} autores
gb.byOneAuthor = por {0}
-gb.viewComparison = veja uma comparao desses {0} commits \u00bb
+gb.viewComparison = veja uma compara\u00e7\u00e3o desses {0} commits \u00bb
gb.nMoreCommits = {0} mais commits \u00bb
gb.oneMoreCommit = mais 1 commit \u00bb
gb.pushedNewTag = fez push de uma nova tag
@@ -478,20 +478,20 @@ gb.deletedTag = tag deletada
gb.pushedNewBranch = fez push de um novo branch
gb.createdNewBranch = um novo branch foi criado
gb.deletedBranch = branch deletado
-gb.createdNewPullRequest = requisies de pull foram criadas
+gb.createdNewPullRequest = requisi\u00e7\u00f5es de pull foram criadas
gb.mergedPullRequest = pull request mergeado
gb.rewind = REBOBINAR
gb.star = favoritar
gb.unstar = desfavoritar
gb.stargazers = favoritos
-gb.starredRepositories = repositries favoritos
-gb.failedToUpdateUser = Falha ao atualizar a conta de usurio!
-gb.myRepositories = meus repositrios
-gb.noActivity = no houve atividade no ltimos {0} dias
-gb.findSomeRepositories = localizar alguns repositrios
-gb.metricAuthorExclusions = excluses de mtricas de autor
+gb.starredRepositories = reposit\u00f3ries favoritos
+gb.failedToUpdateUser = Falha ao atualizar a conta de usu\u00e1rio!
+gb.myRepositories = meus reposit\u00f3rios
+gb.noActivity = n\u00e3o houve atividade no \u00faltimos {0} dias
+gb.findSomeRepositories = localizar alguns reposit\u00f3rios
+gb.metricAuthorExclusions = exclus\u00f5es de m\u00e9tricas de autor
gb.myDashboard = meu dashboard
-gb.failedToFindAccount = falha ao localizar a conta do usurio ''{0}''
+gb.failedToFindAccount = falha ao localizar a conta do usu\u00e1rio ''{0}''
gb.reflog = reflog
gb.active = ativo
gb.starred = favoritado
@@ -500,5 +500,8 @@ gb.starredAndOwned = favoritado & apropriado
gb.reviewPatchset = rever {0} patchset {1}
gb.todaysActivityStats = hoje / {1} commits por {2} autores
gb.todaysActivityNone = hoje / nenhum
-gb.noActivityToday = no houve atividade hoje
-gb.anonymousUser= annimo \ No newline at end of file
+gb.noActivityToday = n\u00e3o houve atividade hoje
+gb.anonymousUser= an\u00f4nimo
+
+# This last property for unit tests to test successful loading of the resource file
+gb.loadLang = Portugu\u00eas
diff --git a/src/main/java/com/gitblit/wicket/GitBlitWebApp_ru.properties b/src/main/java/com/gitblit/wicket/GitBlitWebApp_ru.properties
new file mode 100644
index 00000000..b3af7e00
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/GitBlitWebApp_ru.properties
@@ -0,0 +1,790 @@
+gb.repository = \u0440\u0435\u043F\u043E
+gb.owner = \u0432\u043B\u0430\u0434\u0435\u043B\u0435\u0446
+gb.description = \u043E\u043F\u0438\u0441\u0430\u043D\u0438\u0435
+gb.lastChange = \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0435\u0435 \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u0435
+gb.refs = \u0441\u0441\u044B\u043B\u043A\u0438
+gb.tag = \u0442\u0435\u0433
+gb.tags = \u0442\u0435\u0433\u0438
+gb.author = \u0430\u0432\u0442\u043E\u0440
+gb.committer = \u043A\u043E\u043C\u043C\u0438\u0442\u0442\u0435\u0440
+gb.commit = \u043A\u043E\u043C\u043C\u0438\u0442
+gb.age = \u0432\u043E\u0437\u0440\u0430\u0441\u0442
+gb.tree = \u0434\u0435\u0440\u0435\u0432\u043E
+gb.parent = parent
+gb.url = URL
+gb.history = \u0438\u0441\u0442\u043E\u0440\u0438\u044F
+gb.raw = raw
+gb.object = \u043E\u0431\u044A\u0435\u043A\u0442
+gb.ticketId = \u0438\u0434\u0435\u043D\u0442\u0438\u0444\u0438\u043A\u0430\u0442\u043E\u0440 \u0437\u0430\u044F\u0432\u043A\u0438
+gb.ticketAssigned = \u043D\u0430\u0437\u043D\u0430\u0447\u0435\u043D\u043E
+gb.ticketOpenDate = \u0434\u0430\u0442\u0430 \u043E\u0442\u043A\u0440\u044B\u0442\u0438\u044F
+gb.ticketStatus = \u0441\u0442\u0430\u0442\u0443\u0441
+gb.ticketComments = \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438
+gb.view = view
+gb.local = \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0439
+gb.remote = \u0443\u0434\u0430\u043B\u0435\u043D\u043D\u044B\u0439
+gb.branches = \u0432\u0435\u0442\u043A\u0438
+gb.patch = \u043F\u0430\u0442\u0447
+gb.diff = diff
+gb.log = log
+gb.moreLogs = \u0431\u043E\u043B\u044C\u0448\u0435 \u043A\u043E\u043C\u043C\u0438\u0442\u043E\u0432 ...
+gb.allTags = \u0432\u0441\u0435 \u0442\u0435\u0433\u0438 ...
+gb.allBranches = \u0432\u0441\u0435 \u0432\u0435\u0442\u0432\u0438 ...
+gb.summary = \u0441\u0432\u043E\u0434\u043A\u0430
+gb.ticket = \u0437\u0430\u044F\u0432\u043A\u0430
+gb.newRepository = \u043D\u043E\u0432\u044B\u0439 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439
+gb.newUser = \u043D\u043E\u0432\u044B\u0439 \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044C
+gb.commitdiff = commitdiff
+gb.tickets = \u0437\u0430\u044F\u0432\u043A\u0438
+gb.pageFirst = \u043F\u0435\u0440\u0432\u0430\u044F
+gb.pagePrevious = \u043F\u0440\u0435\u0434\u044B\u0434\u0443\u0449\u0430\u044F
+gb.pageNext = \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0430\u044F
+gb.head = HEAD
+gb.blame = blame
+gb.login = \u0432\u0445\u043E\u0434
+gb.logout = \u0432\u044B\u0445\u043E\u0434
+gb.username = \u0438\u043C\u044F \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F
+gb.password = \u043F\u0430\u0440\u043E\u043B\u044C
+gb.tagger = tagger
+gb.moreHistory = \u0431\u043E\u043B\u044C\u0448\u0435 \u0438\u0441\u0442\u043E\u0440\u0438\u0438 ...
+gb.difftocurrent = diff to current
+gb.search = \u043F\u043E\u0438\u0441\u043A
+gb.searchForAuthor = \u041F\u043E\u0438\u0441\u043A \u043A\u043E\u043C\u043C\u0438\u0442\u043E\u0432, \u043F\u043E \u0430\u0432\u0442\u043E\u0440\u0443
+gb.searchForCommitter = \u041F\u043E\u0438\u0441\u043A \u043A\u043E\u043C\u043C\u0438\u0442\u043E\u0432, \u0441\u043E\u0432\u0435\u0440\u0448\u0435\u043D\u043D\u044B\u0445
+gb.addition = \u0441\u043B\u043E\u0436\u0435\u043D\u0438\u0435
+gb.modification = \u043C\u043E\u0434\u0438\u0444\u0438\u043A\u0430\u0446\u0438\u044F
+gb.deletion = \u0443\u0434\u0430\u043B\u0435\u043D\u0438\u0435
+gb.rename = \u043F\u0435\u0440\u0435\u0438\u043C\u0435\u043D\u043E\u0432\u0430\u043D\u0438\u0435
+gb.metrics = \u043C\u0435\u0442\u0440\u0438\u043A\u0438
+gb.stats = \u0441\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u043A\u0430
+gb.markdown = markdown
+gb.changedFiles = \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u043D\u044B\u0435 \u0444\u0430\u0439\u043B\u044B
+gb.filesAdded = {0} \u0444\u0430\u0439\u043B\u043E\u0432 \u0434\u043E\u0431\u0430\u0432\u043B\u0435\u043D\u043E
+gb.filesModified = {0} \u0444\u0430\u0439\u043B\u043E\u0432 \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u043E
+gb.filesDeleted = {0} \u0444\u0430\u0439\u043B\u043E\u0432 \u0443\u0434\u0430\u043B\u0435\u043D\u043E
+gb.filesCopied = {0} \u0441\u043A\u043E\u043F\u0438\u0440\u043E\u0432\u0430\u043D\u043D\u044B\u0445 \u0444\u0430\u0439\u043B\u043E\u0432
+gb.filesRenamed = {0} \u0444\u0430\u0439\u043B\u044B \u043F\u0435\u0440\u0435\u0438\u043C\u0435\u043D\u043E\u0432\u0430\u043D\u044B
+gb.missingUsername = \u041E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0438\u043C\u044F \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F
+gb.edit = \u043F\u0440\u0430\u0432\u0438\u0442\u044C
+gb.searchTypeTooltip = \u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0438\u043F \u043F\u043E\u0438\u0441\u043A\u0430
+gb.searchTooltip = \u041F\u043E\u0438\u0441\u043A {0}
+gb.delete = \u0443\u0434\u0430\u043B\u0438\u0442\u044C
+gb.docs = \u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u044B
+gb.accessRestriction = \u043E\u0433\u0440\u0430\u043D\u0438\u0447\u0435\u043D\u0438\u0435 \u0434\u043E\u0441\u0442\u0443\u043F\u0430
+gb.name = \u0438\u043C\u044F
+gb.enableTickets = \u0432\u043A\u043B\u044E\u0447\u0438\u0442\u044C \u0437\u0430\u044F\u0432\u043A\u0438
+gb.enableDocs = \u0432\u043A\u043B\u044E\u0447\u0438\u0442\u044C \u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u044B
+gb.save = \u0441\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C
+gb.showRemoteBranches = \u043F\u043E\u043A\u0430\u0437\u0430\u0442\u044C \u0443\u0434\u0430\u043B\u0435\u043D\u043D\u044B\u0435 \u0432\u0435\u0442\u0432\u0438
+gb.editUsers = \u0440\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u0435\u0439
+gb.confirmPassword = \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044C \u043F\u0430\u0440\u043E\u043B\u044C
+gb.restrictedRepositories = \u043E\u0433\u0440\u0430\u043D\u0438\u0447\u0435\u043D\u043D\u044B\u0435 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438
+gb.canAdmin = \u043C\u043E\u0436\u043D\u043E \u0430\u0434\u043C\u0438\u043D\u0438\u0442\u044C
+gb.notRestricted = \u0430\u043D\u043E\u043D\u0438\u043C\u043D\u044B\u0439 \u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440, \u043A\u043B\u043E\u043D\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u0435 \u0438 push
+gb.pushRestricted = \u0430\u0443\u0442\u0435\u043D\u0442\u0438\u0444\u0438\u0446\u0438\u0440\u043E\u0432\u0430\u043D\u043D\u044B\u0439 push
+gb.cloneRestricted = \u0430\u0443\u0442\u0435\u043D\u0442\u0438\u0444\u0438\u0446\u0438\u0440\u043E\u0432\u0430\u043D\u043D\u044B\u0439 \u043A\u043B\u043E\u043D \u0438 push
+gb.viewRestricted = \u0430\u0443\u0442\u0435\u043D\u0442\u0438\u0444\u0438\u0446\u0438\u0440\u043E\u0432\u0430\u043D\u043D\u043E\u0435 \u043F\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043B\u0435\u043D\u0438\u0435, \u043A\u043B\u043E\u043D\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u0435 \u0438 push
+gb.useTicketsDescription = \u0442\u043E\u043B\u044C\u043A\u043E \u0434\u043B\u044F \u0447\u0442\u0435\u043D\u0438\u044F, \u0440\u0430\u0441\u043F\u0440\u043E\u0441\u0442\u0440\u0430\u043D\u0435\u043D\u043D\u044B\u0435 \u043F\u0440\u043E\u0431\u043B\u0435\u043C\u044B Ticgit
+gb.useDocsDescription = \u043F\u0435\u0440\u0435\u0447\u0438\u0441\u043B\u044F\u0435\u0442 \u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u0430\u0446\u0438\u044E \u043F\u043E Markdown \u0432 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438
+gb.showRemoteBranchesDescription = \u043F\u043E\u043A\u0430\u0437\u0430\u0442\u044C \u0443\u0434\u0430\u043B\u0435\u043D\u043D\u044B\u0435 \u0432\u0435\u0442\u0432\u0438
+gb.canAdminDescription = \u043C\u043E\u0436\u0435\u0442 \u0430\u0434\u043C\u0438\u043D\u0438\u0441\u0442\u0440\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0441\u0435\u0440\u0432\u0435\u0440 Gitblit
+gb.permittedUsers = \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043D\u043D\u044B\u0435 \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u0438
+gb.isFrozen = \u0437\u0430\u043C\u043E\u0440\u043E\u0436\u0435\u043D
+gb.isFrozenDescription = \u0437\u0430\u043F\u0440\u0435\u0442\u0438\u0442\u044C \u043E\u043F\u0435\u0440\u0430\u0446\u0438\u0438 push
+gb.zip = zip
+gb.showReadme = \u043F\u043E\u043A\u0430\u0437\u0430\u0442\u044C readme
+gb.showReadmeDescription = \u043F\u043E\u043A\u0430\u0437\u0430\u0442\u044C \u0444\u0430\u0439\u043B \"readme\" Markdown \u043D\u0430 \u0441\u0442\u0440\u0430\u043D\u0438\u0446\u0435 \u0441\u0432\u043E\u0434\u043A\u0438
+gb.nameDescription = \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u044C '/' \u0434\u043B\u044F \u0433\u0440\u0443\u043F\u043F\u0438\u0440\u043E\u0432\u043A\u0438 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0435\u0432. \u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440 libraries/mycoollib.git
+gb.ownerDescription = \u0432\u043B\u0430\u0434\u0435\u043B\u0435\u0446 \u043C\u043E\u0436\u0435\u0442 \u0440\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F
+gb.blob = blob
+gb.commitActivityTrend = \u0437\u0430\u0444\u0438\u043A\u0441\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0442\u0440\u0435\u043D\u0434 \u0430\u043A\u0442\u0438\u0432\u043D\u043E\u0441\u0442\u0438
+gb.commitActivityDOW = \u0437\u0430\u0444\u0438\u043A\u0441\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0430\u043A\u0442\u0438\u0432\u043D\u043E\u0441\u0442\u044C \u043F\u043E \u0434\u043D\u044F\u043C \u043D\u0435\u0434\u0435\u043B\u0438
+gb.commitActivityAuthors = \u043F\u0435\u0440\u0432\u0438\u0447\u043D\u044B\u0435 \u0430\u0432\u0442\u043E\u0440\u044B \u043F\u043E \u0430\u043A\u0442\u0438\u0432\u043D\u043E\u0441\u0442\u0438 \u0444\u0438\u043A\u0441\u0430\u0446\u0438\u0438
+gb.feed = feed
+gb.cancel = \u043E\u0442\u043C\u0435\u043D\u0438\u0442\u044C
+gb.changePassword = \u0438\u0437\u043C\u0435\u043D\u0438\u0442\u044C \u043F\u0430\u0440\u043E\u043B\u044C
+gb.isFederated = \u044F\u0432\u043B\u044F\u0435\u0442\u0441\u044F \u043E\u0431\u044A\u0435\u0434\u0438\u043D\u0435\u043D\u0438\u0435\u043C
+gb.federateThis = \u043E\u0431\u044A\u0435\u0434\u0438\u043D\u0438\u0442\u044C \u044D\u0442\u043E\u0442 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439
+gb.federateOrigin = \u043E\u0431\u044A\u0435\u0434\u0438\u043D\u0438\u0442\u044C \u0438\u0441\u0442\u043E\u0447\u043D\u0438\u043A
+gb.excludeFromFederation = \u0438\u0441\u043A\u043B\u044E\u0447\u0438\u0442\u044C \u0438\u0437 \u043E\u0431\u044A\u0435\u0434\u0438\u043D\u0435\u043D\u0438\u044F
+gb.excludeFromFederationDescription = \u0437\u0430\u0431\u043B\u043E\u043A\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u043E\u0431\u044A\u0435\u0434\u0438\u043D\u0435\u043D\u043D\u044B\u0435 \u044D\u043A\u0437\u0435\u043C\u043F\u043B\u044F\u0440\u044B Gitblit \u043E\u0442 pulling \u044D\u0442\u043E\u0439 \u0443\u0447\u0435\u0442\u043D\u043E\u0439 \u0437\u0430\u043F\u0438\u0441\u0438
+gb.tokens = \u0442\u043E\u043A\u0435\u043D\u044B \u043E\u0431\u044A\u0435\u0434\u0438\u043D\u0435\u043D\u0438\u044F
+gb.tokenAllDescription = \u0432\u0441\u0435 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438, \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u0438 \u0438 \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438
+gb.tokenUnrDescription = \u0432\u0441\u0435 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u0438 \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u0438
+gb.tokenJurDescription = \u0432\u0441\u0435 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438
+gb.federatedRepositoryDefinitions = \u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0435\u043D\u0438\u044F \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F
+gb.federatedUserDefinitions = \u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0435\u043D\u0438\u044F \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F
+gb.federatedSettingDefinitions = \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438 \u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0435\u043D\u0438\u044F
+gb.proposals = \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u044F \u043E\u0431\u044A\u0435\u0434\u0438\u043D\u0435\u043D\u0438\u044F
+gb.received = \u043F\u043E\u043B\u0443\u0447\u0435\u043D\u043E
+gb.type = \u0442\u0438\u043F
+gb.token = \u0442\u043E\u043A\u0435\u043D
+gb.repositories = \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438
+gb.proposal = \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0435
+gb.frequency = \u0447\u0430\u0441\u0442\u043E\u0442\u0430
+gb.folder = \u043F\u0430\u043F\u043A\u0430
+gb.lastPull = \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0435\u0435 \u043D\u0430\u0436\u0430\u0442\u0438\u0435
+gb.nextPull = \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0435\u0435 \u043D\u0430\u0436\u0430\u0442\u0438\u0435
+gb.inclusion = \u0432\u043A\u043B\u044E\u0447\u0435\u043D\u0438\u044F
+gb.exclusion = \u0438\u0441\u043A\u043B\u044E\u0447\u0435\u043D\u0438\u044F
+gb.registration = \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u044F
+gb.registrations = \u043E\u0431\u044A\u0435\u0434\u0438\u043D\u0435\u043D\u0438\u0435 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0439
+gb.sendProposal = \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0438\u0442\u044C
+gb.status = \u0441\u0442\u0430\u0442\u0443\u0441
+gb.origin = \u043F\u0440\u043E\u0438\u0441\u0445\u043E\u0436\u0434\u0435\u043D\u0438\u0435
+gb.headRef = \u0432\u0435\u0442\u0432\u044C \u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E (HEAD)
+gb.headRefDescription = \u0412\u0435\u0442\u0432\u044C \u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E, \u043A\u043E\u0442\u043E\u0440\u0430\u044F \u0431\u0443\u0434\u0435\u0442 \u043A\u043B\u043E\u043D\u0438\u0440\u043E\u0432\u0430\u043D\u0430 \u0438 \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0430 \u043D\u0430 \u0441\u0442\u0440\u0430\u043D\u0438\u0446\u0435 \u0421\u0432\u043E\u0434\u043A\u0430.
+gb.federationStrategy = \u0441\u0442\u0440\u0430\u0442\u0435\u0433\u0438\u044F \u043E\u0431\u044C\u0435\u0434\u0438\u043D\u0435\u043D\u0438\u044F
+gb.federationRegistration = \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u044F \u043E\u0431\u044C\u0435\u0434\u0438\u043D\u0435\u043D\u0438\u044F
+gb.federationResults = \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442\u044B \u0438\u0437\u0432\u043B\u0435\u0447\u0435\u043D\u0438\u044F \u043E\u0431\u044C\u0435\u0434\u0438\u043D\u0435\u043D\u0438\u044F
+gb.federationSets = \u043D\u0430\u0431\u043E\u0440\u044B \u043E\u0431\u044C\u0435\u0434\u0438\u043D\u0435\u043D\u0438\u044F
+gb.message = \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0435
+gb.myUrlDescription = \u043E\u0431\u0449\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u044B\u0439 URL \u0434\u043B\u044F \u0432\u0430\u0448\u0435\u0433\u043E \u044D\u043A\u0437\u0435\u043C\u043F\u043B\u044F\u0440\u0430 Gitblit
+gb.destinationUrl = \u043E\u0442\u043F\u0440\u0430\u0432\u0438\u0442\u044C
+gb.destinationUrlDescription = URL-\u0430\u0434\u0440\u0435\u0441 \u044D\u043A\u0437\u0435\u043C\u043F\u043B\u044F\u0440\u0430 Gitblit \u0434\u043B\u044F \u043E\u0442\u043F\u0440\u0430\u0432\u043A\u0438 \u0432\u0430\u0448\u0435\u0433\u043E \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u044F
+gb.users = \u044E\u0437\u0435\u0440\u044B
+gb.federation = \u043E\u0431\u044C\u0435\u0434\u0438\u043D\u0435\u043D\u0438\u0435
+gb.error = \u043E\u0448\u0438\u0431\u043A\u0430
+gb.refresh = \u043E\u0431\u043D\u043E\u0432\u0438\u0442\u044C
+gb.browse = \u043E\u0431\u043E\u0437\u0440\u0435\u0432\u0430\u0442\u044C
+gb.clone = clone
+gb.filter = \u0444\u0438\u043B\u044C\u0442\u0440
+gb.create = \u0441\u043E\u0437\u0434\u0430\u0442\u044C
+gb.servers = \u0441\u0435\u0440\u0432\u0435\u0440\u043E\u0432
+gb.recent = \u043D\u0435\u0434\u0430\u0432\u043D\u043E
+gb.available = \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u043E
+gb.selected = \u0432\u044B\u0431\u0440\u0430\u043D\u043D\u044B\u0439
+gb.size = \u0440\u0430\u0437\u043C\u0435\u0440
+gb.downloading = \u0437\u0430\u0433\u0440\u0443\u0437\u043A\u0430
+gb.loading = \u0437\u0430\u0433\u0440\u0443\u0437\u043A\u0430
+gb.starting = \u0437\u0430\u043F\u0443\u0441\u043A
+gb.general = \u043E\u0431\u0449\u0438\u0435
+gb.settings = \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438
+gb.manage = \u0443\u043F\u0440\u0430\u0432\u043B\u044F\u0442\u044C
+gb.lastLogin = \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0438\u0439 \u0432\u0445\u043E\u0434
+gb.skipSizeCalculation = \u0440\u0430\u0441\u0447\u0435\u0442 \u0440\u0430\u0437\u043C\u0435\u0440\u0430 \u043F\u0440\u043E\u043F\u0443\u0441\u043A\u0430
+gb.skipSizeCalculationDescription = \u043D\u0435 \u0440\u0430\u0441\u0441\u0447\u0438\u0442\u044B\u0432\u0430\u0442\u044C \u0440\u0430\u0437\u043C\u0435\u0440 \u0445\u0440\u0430\u043D\u0438\u043B\u0438\u0449\u0430 (\u0441\u043E\u043A\u0440\u0430\u0449\u0430\u0435\u0442 \u0432\u0440\u0435\u043C\u044F \u0437\u0430\u0433\u0440\u0443\u0437\u043A\u0438 \u0441\u0442\u0440\u0430\u043D\u0438\u0446\u044B)
+gb.skipSummaryMetrics = \u043F\u0440\u043E\u043F\u0443\u0441\u0442\u0438\u0442\u044C \u0441\u0432\u043E\u0434\u043D\u044B\u0435 \u043F\u043E\u043A\u0430\u0437\u0430\u0442\u0435\u043B\u0438
+gb.skipSummaryMetricsDescription = \u043D\u0435 \u0440\u0430\u0441\u0441\u0447\u0438\u0442\u044B\u0432\u0430\u0442\u044C \u043F\u043E\u043A\u0430\u0437\u0430\u0442\u0435\u043B\u0438 \u043D\u0430 \u0441\u0442\u0440\u0430\u043D\u0438\u0446\u0435 \u0441\u0432\u043E\u0434\u043A\u0438 (\u0441\u043E\u043A\u0440\u0430\u0449\u0430\u0435\u0442 \u0432\u0440\u0435\u043C\u044F \u0437\u0430\u0433\u0440\u0443\u0437\u043A\u0438 \u0441\u0442\u0440\u0430\u043D\u0438\u0446\u044B)
+gb.accessLevel = \u0443\u0440\u043E\u0432\u0435\u043D\u044C \u0434\u043E\u0441\u0442\u0443\u043F\u0430
+gb.default = \u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E
+gb.setDefault = \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442\u044C \u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E
+gb.since = \u0441 \u0442\u0435\u0445 \u043F\u043E\u0440
+gb.status = \u0441\u0442\u0430\u0442\u0443\u0441
+gb.bootDate = \u0434\u0430\u0442\u0430 \u0437\u0430\u0433\u0440\u0443\u0437\u043A\u0438
+gb.servletContainer = \u043A\u043E\u043D\u0442\u0435\u0439\u043D\u0435\u0440 \u0441\u0435\u0440\u0432\u043B\u0435\u0442\u0430
+gb.heapMaximum = \u043C\u0430\u043A\u0441\u0438\u043C\u0430\u043B\u044C\u043D\u0430\u044F \u043A\u0443\u0447\u0430
+gb.heapAllocated = \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u043D\u0430\u044F \u043A\u0443\u0447\u0430
+gb.heapUsed = \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0435\u0442\u0441\u044F \u043A\u0443\u0447\u0430
+gb.free = \u0441\u0432\u043E\u0431\u043E\u0434\u043D\u043E
+gb.version = \u0432\u0435\u0440\u0441\u0438\u044F
+gb.releaseDate = \u0434\u0430\u0442\u0430 \u0432\u044B\u043F\u0443\u0441\u043A\u0430
+gb.date = \u0434\u0430\u0442\u0430
+gb.activity = \u0430\u043A\u0442\u0438\u0432\u043D\u043E\u0441\u0442\u044C
+gb.subscribe = \u043F\u043E\u0434\u043F\u0438\u0441\u0430\u0442\u044C\u0441\u044F
+gb.branch = \u0432\u0435\u0442\u0432\u044C
+gb.maxHits = \u043C\u0430\u043A\u0441\u0438\u043C\u0443\u043C \u0445\u0438\u0442\u043E\u0432
+gb.recentActivity = \u0430\u043A\u0442\u0438\u0432\u043D\u043E\u0441\u0442\u044C
+gb.recentActivityStats = \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0438\u0435 {0} \u0434\u043D\u0435\u0439 / {1} \u043A\u043E\u043C\u043C\u0438\u0442\u043E\u0432 \u043E\u0442 {2} \u0430\u0432\u0442\u043E\u0440\u043E\u0432
+gb.recentActivityNone = \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0438\u0435 {0} \u0434\u043D\u0435\u0439 / \u043D\u0435\u0442
+gb.dailyActivity = \u0435\u0436\u0435\u0434\u043D\u0435\u0432\u043D\u0430\u044F \u0430\u043A\u0442\u0438\u0432\u043D\u043E\u0441\u0442\u044C
+gb.activeRepositories = \u0430\u043A\u0442\u0438\u0432\u043D\u044B\u0435 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438
+gb.activeAuthors = \u0430\u043A\u0442\u0438\u0432\u043D\u044B\u0435 \u0430\u0432\u0442\u043E\u0440\u044B
+gb.commits = \u043A\u043E\u043C\u043C\u0438\u0442\u044B
+gb.teams = \u043A\u043E\u043C\u0430\u043D\u0434\u044B
+gb.teamName = \u043D\u0430\u0437\u0432\u0430\u043D\u0438\u0435 \u043A\u043E\u043C\u0430\u043D\u0434\u044B
+gb.teamMembers = \u0447\u043B\u0435\u043D\u044B \u043A\u043E\u043C\u0430\u043D\u0434\u044B
+gb.teamMemberships = \u0447\u043B\u0435\u043D\u0441\u0442\u0432\u043E \u0432 \u043A\u043E\u043C\u0430\u043D\u0434\u0435
+gb.newTeam = \u043D\u043E\u0432\u0430\u044F \u043A\u043E\u043C\u0430\u043D\u0434\u0430
+gb.permittedTeams = \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043D\u043D\u044B\u0435 \u043A\u043E\u043C\u0430\u043D\u0434\u044B
+gb.emptyRepository = \u043F\u0443\u0441\u0442\u043E\u0439 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439
+gb.repositoryUrl = URL \u0445\u0440\u0430\u043D\u0438\u043B\u0438\u0449\u0430
+gb.mailingLists = \u0441\u043F\u0438\u0441\u043A\u0438 \u0440\u0430\u0441\u0441\u044B\u043B\u043A\u0438
+gb.preReceiveScripts = \u043F\u0440\u0435\u0434\u0432\u0430\u0440\u0438\u0442\u0435\u043B\u044C\u043D\u043E \u043F\u043E\u043B\u0443\u0447\u0438\u0442\u044C \u0441\u0446\u0435\u043D\u0430\u0440\u0438\u0438
+gb.postReceiveScripts = \u043F\u043E\u0441\u0442-\u043F\u043E\u043B\u0443\u0447\u0435\u043D\u0438\u0435 \u0441\u0446\u0435\u043D\u0430\u0440\u0438\u0435\u0432
+gb.hookScripts = \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0438\u0442\u044C \u0441\u043A\u0440\u0438\u043F\u0442\u044B
+gb.customFields = \u043D\u0430\u0441\u0442\u0440\u0430\u0438\u0432\u0430\u0435\u043C\u044B\u0435 \u043F\u043E\u043B\u044F
+gb.customFieldsDescription = \u043D\u0430\u0441\u0442\u0440\u0430\u0438\u0432\u0430\u0435\u043C\u044B\u0435 \u043F\u043E\u043B\u044F, \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u044B\u0435 \u0434\u043B\u044F \u0445\u0443\u043A\u043E\u0432 Groovy
+gb.accessPermissions = \u043F\u0440\u0430\u0432\u0430 \u0434\u043E\u0441\u0442\u0443\u043F\u0430
+gb.filters = \u0444\u0438\u043B\u044C\u0442\u0440\u044B
+gb.generalDescription = \u043E\u0431\u0449\u0438\u0435 \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438
+gb.accessPermissionsDescription = \u043E\u0433\u0440\u0430\u043D\u0438\u0447\u0438\u0442\u044C \u0434\u043E\u0441\u0442\u0443\u043F \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F\u043C \u0438 \u043A\u043E\u043C\u0430\u043D\u0434\u0430\u043C
+gb.accessPermissionsForUserDescription = \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442\u044C \u0447\u043B\u0435\u043D\u0441\u0442\u0432\u043E \u0432 \u043A\u043E\u043C\u0430\u043D\u0434\u0435 \u0438\u043B\u0438 \u043F\u0440\u0435\u0434\u043E\u0441\u0442\u0430\u0432\u0438\u0442\u044C \u0434\u043E\u0441\u0442\u0443\u043F \u043A \u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0435\u043D\u043D\u044B\u043C \u0437\u0430\u043A\u0440\u044B\u0442\u044B\u043C \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F\u043C
+gb.accessPermissionsForTeamDescription = \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442\u044C \u0447\u043B\u0435\u043D\u043E\u0432 \u043A\u043E\u043C\u0430\u043D\u0434\u044B \u0438 \u043F\u0440\u0435\u0434\u043E\u0441\u0442\u0430\u0432\u0438\u0442\u044C \u0434\u043E\u0441\u0442\u0443\u043F \u043A \u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0435\u043D\u043D\u044B\u043C \u0437\u0430\u043A\u0440\u044B\u0442\u044B\u043C \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F\u043C
+gb.federationRepositoryDescription = \u043F\u043E\u0434\u0435\u043B\u0438\u0442\u044C\u0441\u044F \u044D\u0442\u0438\u043C \u0445\u0440\u0430\u043D\u0438\u043B\u0438\u0449\u0435\u043C \u0441 \u0434\u0440\u0443\u0433\u0438\u043C\u0438 \u0441\u0435\u0440\u0432\u0435\u0440\u0430\u043C\u0438 Gitblit
+gb.hookScriptsDescription = \u0437\u0430\u043F\u0443\u0441\u043A\u0430\u0442\u044C \u0441\u043A\u0440\u0438\u043F\u0442\u044B Groovy \u043F\u0440\u0438 \u043E\u0442\u043F\u0440\u0430\u0432\u043A\u0435 \u043D\u0430 \u044D\u0442\u043E\u0442 \u0441\u0435\u0440\u0432\u0435\u0440 Gitblit
+gb.reset = \u0441\u0431\u0440\u043E\u0441
+gb.pages = \u0441\u0442\u0440\u0430\u043D\u0438\u0446
+gb.workingCopy = \u0440\u0430\u0431\u043E\u0447\u0430\u044F \u043A\u043E\u043F\u0438\u044F
+gb.workingCopyWarning = \u044D\u0442\u043E\u0442 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439 \u0438\u043C\u0435\u0435\u0442 \u0440\u0430\u0431\u043E\u0447\u0443\u044E \u043A\u043E\u043F\u0438\u044E \u0438 \u043D\u0435 \u043C\u043E\u0436\u0435\u0442 \u043F\u043E\u043B\u0443\u0447\u0430\u0442\u044C \u043D\u0430\u0436\u0430\u0442\u0438\u044F
+gb.query = \u0437\u0430\u043F\u0440\u043E\u0441
+gb.queryHelp = \u0421\u0442\u0430\u043D\u0434\u0430\u0440\u0442\u043D\u044B\u0439 \u0441\u0438\u043D\u0442\u0430\u043A\u0441\u0438\u0441 \u0437\u0430\u043F\u0440\u043E\u0441\u0430 \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044F. <p/><p/> \u041F\u043E\u0434\u0440\u043E\u0431\u043D\u043E\u0441\u0442\u0438 \u0441\u043C\u043E\u0442\u0440\u0438\u0442\u0435 \u0432 ${querySyntax}.
+gb.querySyntax = \u0441\u0438\u043D\u0442\u0430\u043A\u0441\u0438\u0447\u0435\u0441\u043A\u0438\u0439 \u0430\u043D\u0430\u043B\u0438\u0437\u0430\u0442\u043E\u0440 \u0437\u0430\u043F\u0440\u043E\u0441\u043E\u0432 Lucene
+gb.queryResults = \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442\u044B {0} - {1} ({2} \u0441\u043E\u0432\u043F\u0430\u0434\u0435\u043D\u0438\u0439)
+gb.noHits = \u043D\u0435\u0442 \u0441\u043E\u0432\u043F\u0430\u0434\u0435\u043D\u0438\u0439
+gb.authored = \u0430\u0432\u0442\u043E\u0440
+gb.committed = \u0441\u043E\u0432\u0435\u0440\u0448\u0435\u043D\u043E
+gb.indexedBranches = \u0418\u043D\u0434\u0435\u043A\u0441\u0438\u0440\u043E\u0432\u0430\u043D\u043D\u044B\u0435 \u0432\u0435\u0442\u0432\u0438
+gb.indexedBranchesDescription = \u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u0432\u0435\u0442\u0432\u0438, \u043A\u043E\u0442\u043E\u0440\u044B\u0435 \u0431\u0443\u0434\u0443\u0442 \u043F\u0440\u043E\u0438\u043D\u0434\u0435\u043A\u0441\u0438\u0440\u043E\u0432\u0430\u043D\u044B Lucene
+gb.noIndexedRepositoriesWarning = \u043D\u0438 \u043E\u0434\u0438\u043D \u0438\u0437 \u0432\u0430\u0448\u0438\u0445 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0435\u0432 \u043D\u0435 \u043D\u0430\u0441\u0442\u0440\u043E\u0435\u043D \u0434\u043B\u044F \u0438\u043D\u0434\u0435\u043A\u0441\u0430\u0446\u0438\u0438 Lucene
+gb.undefinedQueryWarning = \u0437\u0430\u043F\u0440\u043E\u0441 \u043D\u0435 \u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0435\u043D!
+gb.noSelectedRepositoriesWarning = \u0432\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u043E\u0434\u0438\u043D \u0438\u043B\u0438 \u043D\u0435\u0441\u043A\u043E\u043B\u044C\u043A\u043E \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0435\u0432!
+gb.luceneDisabled = \u0438\u043D\u0434\u0435\u043A\u0441\u0430\u0446\u0438\u044F Lucene \u043E\u0442\u043A\u043B\u044E\u0447\u0435\u043D\u0430
+gb.failedtoRead = \u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u0442\u044C
+gb.isNotValidFile = \u043D\u0435 \u044F\u0432\u043B\u044F\u0435\u0442\u0441\u044F \u0434\u043E\u043F\u0443\u0441\u0442\u0438\u043C\u044B\u043C \u0444\u0430\u0439\u043B\u043E\u043C
+gb.failedToReadMessage = \u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u0442\u044C \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0435 \u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E \u0438\u0437 {0}!
+gb.passwordsDoNotMatch = \u041F\u0430\u0440\u043E\u043B\u0438 \u043D\u0435 \u0441\u043E\u0432\u043F\u0430\u0434\u0430\u044E\u0442!
+gb.passwordTooShort = \u041F\u0430\u0440\u043E\u043B\u044C \u0441\u043B\u0438\u0448\u043A\u043E\u043C \u043A\u043E\u0440\u043E\u0442\u043A\u0438\u0439. \u041C\u0438\u043D\u0438\u043C\u0430\u043B\u044C\u043D\u0430\u044F \u0434\u043B\u0438\u043D\u0430: {0} \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432.
+gb.passwordChanged = \u041F\u0430\u0440\u043E\u043B\u044C \u0443\u0441\u043F\u0435\u0448\u043D\u043E \u0438\u0437\u043C\u0435\u043D\u0435\u043D.
+gb.passwordChangeAborted = \u0418\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u0435 \u043F\u0430\u0440\u043E\u043B\u044F \u043E\u0442\u043C\u0435\u043D\u0435\u043D\u043E.
+gb.pleaseSetRepositoryName = \u041F\u043E\u0436\u0430\u043B\u0443\u0439\u0441\u0442\u0430, \u0443\u043A\u0430\u0436\u0438\u0442\u0435 \u0438\u043C\u044F \u0445\u0440\u0430\u043D\u0438\u043B\u0438\u0449\u0430!
+gb.illegalLeadingSlash = \u0421\u0441\u044B\u043B\u043A\u0438 \u043D\u0430 \u0432\u0435\u0434\u0443\u0449\u0438\u0435 \u043A\u043E\u0440\u043D\u0435\u0432\u044B\u0435 \u043F\u0430\u043F\u043A\u0438 (/) \u0437\u0430\u043F\u0440\u0435\u0449\u0435\u043D\u044B.
+gb.illegalRelativeSlash = \u041E\u0442\u043D\u043E\u0441\u0438\u0442\u0435\u043B\u044C\u043D\u044B\u0435 \u0441\u0441\u044B\u043B\u043A\u0438 \u043D\u0430 \u043F\u0430\u043F\u043A\u0438 (../) \u0437\u0430\u043F\u0440\u0435\u0449\u0435\u043D\u044B.
+gb.illegalCharacterRepositoryName = \u041D\u0435\u0434\u043E\u043F\u0443\u0441\u0442\u0438\u043C\u044B\u0439 \u0441\u0438\u043C\u0432\u043E\u043B ''{0}'' \u0432 \u0438\u043C\u0435\u043D\u0438 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F!
+gb.selectAccessRestriction = \u041F\u043E\u0436\u0430\u043B\u0443\u0439\u0441\u0442\u0430, \u0432\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u043E\u0433\u0440\u0430\u043D\u0438\u0447\u0435\u043D\u0438\u0435 \u0434\u043E\u0441\u0442\u0443\u043F\u0430!
+gb.selectFederationStrategy = \u041F\u043E\u0436\u0430\u043B\u0443\u0439\u0441\u0442\u0430, \u0432\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u0442\u0440\u0430\u0442\u0435\u0433\u0438\u044E \u0444\u0435\u0434\u0435\u0440\u0430\u0446\u0438\u0438!
+gb.pleaseSetTeamName = \u041F\u043E\u0436\u0430\u043B\u0443\u0439\u0441\u0442\u0430, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043C\u044F \u043A\u043E\u043C\u0430\u043D\u0434\u044B!
+gb.teamNameUnavailable = \u041D\u0430\u0437\u0432\u0430\u043D\u0438\u0435 \u043A\u043E\u043C\u0430\u043D\u0434\u044B ''{0}'' \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u043E.
+gb.teamMustSpecifyRepository = \u041A\u043E\u043C\u0430\u043D\u0434\u0430 \u0434\u043E\u043B\u0436\u043D\u0430 \u0443\u043A\u0430\u0437\u0430\u0442\u044C \u0445\u043E\u0442\u044F \u0431\u044B \u043E\u0434\u0438\u043D \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439.
+gb.teamCreated = \u041D\u043E\u0432\u0430\u044F \u043A\u043E\u043C\u0430\u043D\u0434\u0430 ''{0}'' \u0443\u0441\u043F\u0435\u0448\u043D\u043E \u0441\u043E\u0437\u0434\u0430\u043D\u0430.
+gb.pleaseSetUsername = \u041F\u043E\u0436\u0430\u043B\u0443\u0439\u0441\u0442\u0430, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043C\u044F \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F!
+gb.usernameUnavailable = \u0418\u043C\u044F \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F ''{0}'' \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u043E.
+gb.combinedMd5Rename = Gitblit \u043D\u0430\u0441\u0442\u0440\u043E\u0435\u043D \u0434\u043B\u044F \u043A\u043E\u043C\u0431\u0438\u043D\u0438\u0440\u043E\u0432\u0430\u043D\u043D\u043E\u0433\u043E \u0445\u0435\u0448\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u044F \u043F\u0430\u0440\u043E\u043B\u044F md5. \u0412\u044B \u0434\u043E\u043B\u0436\u043D\u044B \u0432\u0432\u0435\u0441\u0442\u0438 \u043D\u043E\u0432\u044B\u0439 \u043F\u0430\u0440\u043E\u043B\u044C \u043F\u0440\u0438 \u043F\u0435\u0440\u0435\u0438\u043C\u0435\u043D\u043E\u0432\u0430\u043D\u0438\u0438 \u0430\u043A\u043A\u0430\u0443\u043D\u0442\u0430.
+gb.userCreated = \u041D\u043E\u0432\u044B\u0439 \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044C ''{0}'' \u0443\u0441\u043F\u0435\u0448\u043D\u043E \u0441\u043E\u0437\u0434\u0430\u043D.
+gb.couldNotFindFederationRegistration = \u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043D\u0430\u0439\u0442\u0438 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u044E \u043E\u0431\u044C\u0435\u0434\u0438\u043D\u0435\u043D\u0438\u044F!
+gb.failedToFindGravatarProfile = \u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043D\u0430\u0439\u0442\u0438 \u043F\u0440\u043E\u0444\u0438\u043B\u044C Gravatar \u0434\u043B\u044F {0}
+gb.branchStats = {0} \u043A\u043E\u043C\u043C\u0438\u0442\u043E\u0432 \u0438 {1} \u0442\u0435\u0433\u043E\u0432 \u0432 {2}
+gb.repositoryNotSpecified = \u0420\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439 \u043D\u0435 \u0443\u043A\u0430\u0437\u0430\u043D!
+gb.repositoryNotSpecifiedFor = \u0420\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439 \u043D\u0435 \u0443\u043A\u0430\u0437\u0430\u043D \u0434\u043B\u044F {0}!
+gb.canNotLoadRepository = \u041D\u0435\u0432\u043E\u0437\u043C\u043E\u0436\u043D\u043E \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439
+gb.commitIsNull = \u041F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u0435 \u0440\u0430\u0432\u043D\u043E \u043D\u0443\u043B\u044E
+gb.unauthorizedAccessForRepository = \u041D\u0435\u0441\u0430\u043D\u043A\u0446\u0438\u043E\u043D\u0438\u0440\u043E\u0432\u0430\u043D\u043D\u044B\u0439 \u0434\u043E\u0441\u0442\u0443\u043F \u043A \u0445\u0440\u0430\u043D\u0438\u043B\u0438\u0449\u0443
+gb.failedToFindCommit = \u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043D\u0430\u0439\u0442\u0438 \u043A\u043E\u043C\u043C\u0438\u0442 \"{0}\" \u0432 {1}!
+gb.couldNotFindFederationProposal = \u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043D\u0430\u0439\u0442\u0438 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0435 \u0444\u0435\u0434\u0435\u0440\u0430\u0446\u0438\u0438!
+gb.invalidUsernameOrPassword = \u041D\u0435\u0432\u0435\u0440\u043D\u043E\u0435 \u0438\u043C\u044F \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F \u0438\u043B\u0438 \u043F\u0430\u0440\u043E\u043B\u044C!
+gb.OneProposalToReview = \u0415\u0441\u0442\u044C 1 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0435 \u0444\u0435\u0434\u0435\u0440\u0430\u0446\u0438\u0438, \u043E\u0436\u0438\u0434\u0430\u044E\u0449\u0435\u0435 \u0440\u0430\u0441\u0441\u043C\u043E\u0442\u0440\u0435\u043D\u0438\u044F.
+gb.nFederationProposalsToReview = \u0415\u0441\u0442\u044C {0} \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0439 \u0444\u0435\u0434\u0435\u0440\u0430\u0446\u0438\u0438, \u043E\u0436\u0438\u0434\u0430\u044E\u0449\u0438\u0445 \u0440\u0430\u0441\u0441\u043C\u043E\u0442\u0440\u0435\u043D\u0438\u044F.
+gb.couldNotFindTag = \u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043D\u0430\u0439\u0442\u0438 \u0442\u0435\u0433 {0}
+gb.couldNotCreateFederationProposal = \u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0441\u043E\u0437\u0434\u0430\u0442\u044C \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0435 \u0444\u0435\u0434\u0435\u0440\u0430\u0446\u0438\u0438!
+gb.pleaseSetGitblitUrl = \u041F\u043E\u0436\u0430\u043B\u0443\u0439\u0441\u0442\u0430, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0432\u0430\u0448 URL Gitblit!
+gb.pleaseSetDestinationUrl = \u041F\u043E\u0436\u0430\u043B\u0443\u0439\u0441\u0442\u0430, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0446\u0435\u043B\u0435\u0432\u043E\u0439 URL \u0434\u043B\u044F \u0432\u0430\u0448\u0435\u0433\u043E \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u044F!
+gb.proposalReceived = \u041F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0435 \u0443\u0441\u043F\u0435\u0448\u043D\u043E \u043F\u043E\u043B\u0443\u0447\u0435\u043D\u043E {0}.
+gb.noGitblitFound = \u0418\u0437\u0432\u0438\u043D\u0438\u0442\u0435, {0} \u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043D\u0430\u0439\u0442\u0438 \u044D\u043A\u0437\u0435\u043C\u043F\u043B\u044F\u0440 Gitblit \u0432 {1}.
+gb.noProposals = \u0418\u0437\u0432\u0438\u043D\u0438\u0442\u0435, \u0432 \u043D\u0430\u0441\u0442\u043E\u044F\u0449\u0435\u0435 \u0432\u0440\u0435\u043C\u044F {0} \u043D\u0435 \u043F\u0440\u0438\u043D\u0438\u043C\u0430\u0435\u0442 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u044F.
+gb.noFederation = \u0418\u0437\u0432\u0438\u043D\u0438\u0442\u0435, {0} \u043D\u0435 \u043D\u0430\u0441\u0442\u0440\u043E\u0435\u043D \u0434\u043B\u044F \u043E\u0431\u044A\u0435\u0434\u0438\u043D\u0435\u043D\u0438\u044F \u0441 \u043B\u044E\u0431\u044B\u043C\u0438 \u044D\u043A\u0437\u0435\u043C\u043F\u043B\u044F\u0440\u0430\u043C\u0438 Gitblit.
+gb.proposalFailed = \u0418\u0437\u0432\u0438\u043D\u0438\u0442\u0435, {0} \u043D\u0435 \u043F\u043E\u043B\u0443\u0447\u0438\u043B \u043D\u0438\u043A\u0430\u043A\u0438\u0445 \u0434\u0430\u043D\u043D\u044B\u0445 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u044F!
+gb.proposalError = \u0418\u0437\u0432\u0438\u043D\u0438\u0442\u0435, {0} \u0441\u043E\u043E\u0431\u0449\u0430\u0435\u0442, \u0447\u0442\u043E \u043F\u0440\u043E\u0438\u0437\u043E\u0448\u043B\u0430 \u043D\u0435\u043F\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043D\u043D\u0430\u044F \u043E\u0448\u0438\u0431\u043A\u0430!
+gb.failedToSendProposal = \u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043E\u0442\u043F\u0440\u0430\u0432\u0438\u0442\u044C \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0435!
+gb.userServiceDoesNotPermitAddUser = {0} \u043D\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0430\u0435\u0442 \u0434\u043E\u0431\u0430\u0432\u043B\u044F\u0442\u044C \u0443\u0447\u0435\u0442\u043D\u0443\u044E \u0437\u0430\u043F\u0438\u0441\u044C \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F!
+gb.userServiceDoesNotPermitPasswordChanges = {0} \u043D\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0430\u0435\u0442 \u0441\u043C\u0435\u043D\u0443 \u043F\u0430\u0440\u043E\u043B\u044F!
+gb.displayName = \u0424\u0418\u041E
+gb.emailAddress = e-mail
+gb.errorAdminLoginRequired = \u0410\u0434\u043C\u0438\u043D\u0438\u0441\u0442\u0440\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u0435 \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u043B\u043E\u0433\u0438\u043D
+gb.errorOnlyAdminMayCreateRepository = \u0422\u043E\u043B\u044C\u043A\u043E \u0430\u0434\u043C\u0438\u043D\u0438\u0441\u0442\u0440\u0430\u0442\u043E\u0440 \u043C\u043E\u0436\u0435\u0442 \u0441\u043E\u0437\u0434\u0430\u0432\u0430\u0442\u044C \u0445\u0440\u0430\u043D\u0438\u043B\u0438\u0449\u0435
+gb.errorOnlyAdminOrOwnerMayEditRepository = \u0420\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439 \u043C\u043E\u0436\u0435\u0442 \u0442\u043E\u043B\u044C\u043A\u043E \u0430\u0434\u043C\u0438\u043D\u0438\u0441\u0442\u0440\u0430\u0442\u043E\u0440 \u0438\u043B\u0438 \u0432\u043B\u0430\u0434\u0435\u043B\u0435\u0446
+gb.errorAdministrationDisabled = \u0410\u0434\u043C\u0438\u043D\u0438\u0441\u0442\u0440\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u0435 \u043E\u0442\u043A\u043B\u044E\u0447\u0435\u043D\u043E
+gb.lastNDays = \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0438\u0435 {0} \u0434\u043D\u0435\u0439
+gb.completeGravatarProfile = \u041F\u043E\u043B\u043D\u044B\u0439 \u043F\u0440\u043E\u0444\u0438\u043B\u044C \u043D\u0430 Gravatar.com
+gb.none = \u043D\u0435\u0442
+gb.line = \u043B\u0438\u043D\u0438\u044F
+gb.content = \u0441\u043E\u0434\u0435\u0440\u0436\u0430\u043D\u0438\u0435
+gb.empty = \u043F\u0443\u0441\u0442\u043E
+gb.inherited = \u043D\u0430\u0441\u043B\u0435\u0434\u0443\u0435\u0442\u0441\u044F
+gb.deleteRepository = \u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439 \"{0}\"?
+gb.repositoryDeleted = \u0420\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439 ''{0}'' \u0443\u0434\u0430\u043B\u0435\u043D.
+gb.repositoryDeleteFailed = \u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0443\u0434\u0430\u043B\u0438\u0442\u044C \u0445\u0440\u0430\u043D\u0438\u043B\u0438\u0449\u0435 ''{0}''!
+gb.deleteUser = \u0423\u0434\u0430\u043B\u0438\u0442\u044C \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F \"{0}\"?
+gb.userDeleted = \u041F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044C ''{0}'' \u0443\u0434\u0430\u043B\u0435\u043D.
+gb.userDeleteFailed = \u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0443\u0434\u0430\u043B\u0438\u0442\u044C \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F ''{0}''!
+gb.time.justNow = \u0442\u043E\u043B\u044C\u043A\u043E \u0441\u0435\u0439\u0447\u0430\u0441
+gb.time.today = \u0441\u0435\u0433\u043E\u0434\u043D\u044F
+gb.time.yesterday = \u0432\u0447\u0435\u0440\u0430
+gb.time.minsAgo = {0} \u043C\u0438\u043D\u0443\u0442 \u043D\u0430\u0437\u0430\u0434
+gb.time.hoursAgo = {0} \u0447\u0430\u0441\u043E\u0432 \u043D\u0430\u0437\u0430\u0434
+gb.time.daysAgo = {0} \u0434\u043D\u0435\u0439 \u043D\u0430\u0437\u0430\u0434
+gb.time.weeksAgo = {0} \u043D\u0435\u0434\u0435\u043B\u044C \u043D\u0430\u0437\u0430\u0434
+gb.time.monthsAgo = {0} \u043C\u0435\u0441\u044F\u0446\u0435\u0432 \u043D\u0430\u0437\u0430\u0434
+gb.time.oneYearAgo = 1 \u0433\u043E\u0434 \u043D\u0430\u0437\u0430\u0434
+gb.time.yearsAgo = {0} \u043B\u0435\u0442 \u043D\u0430\u0437\u0430\u0434
+gb.duration.oneDay = 1 \u0434\u0435\u043D\u044C
+gb.duration.days = {0} \u0434\u043D\u0435\u0439
+gb.duration.oneMonth = 1 \u043C\u0435\u0441\u044F\u0446
+gb.duration.months = {0} \u043C\u0435\u0441\u044F\u0446\u0435\u0432
+gb.duration.oneYear = 1 \u0433\u043E\u0434
+gb.duration.years = {0} \u043B\u0435\u0442
+gb.authorizationControl = \u043A\u043E\u043D\u0442\u0440\u043E\u043B\u044C \u0430\u0432\u0442\u043E\u0440\u0438\u0437\u0430\u0446\u0438\u0438
+gb.allowAuthenticatedDescription = \u043F\u0440\u0435\u0434\u043E\u0441\u0442\u0430\u0432\u0438\u0442\u044C \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043D\u0438\u0435 RW + \u0432\u0441\u0435\u043C \u0430\u0443\u0442\u0435\u043D\u0442\u0438\u0444\u0438\u0446\u0438\u0440\u043E\u0432\u0430\u043D\u043D\u044B\u043C \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F\u043C
+gb.allowNamedDescription = \u043F\u0440\u0435\u0434\u043E\u0441\u0442\u0430\u0432\u0438\u0442\u044C \u0434\u0435\u0442\u0430\u043B\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u043D\u043D\u044B\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043D\u0438\u044F \u0438\u043C\u0435\u043D\u043D\u044B\u043C \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F\u043C \u0438\u043B\u0438 \u043A\u043E\u043C\u0430\u043D\u0434\u0430\u043C
+gb.markdownFailure = \u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043F\u0440\u043E\u0430\u043D\u0430\u043B\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u043A\u043E\u043D\u0442\u0435\u043D\u0442 Markdown!
+gb.clearCache = \u043E\u0447\u0438\u0441\u0442\u0438\u0442\u044C \u043A\u0435\u0448
+gb.projects = \u043F\u0440\u043E\u0435\u043A\u0442\u044B
+gb.project = \u043F\u0440\u043E\u0435\u043A\u0442
+gb.allProjects = \u0432\u0441\u0435 \u043F\u0440\u043E\u0435\u043A\u0442\u044B
+gb.copyToClipboard = \u0441\u043A\u043E\u043F\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0432 \u0431\u0443\u0444\u0435\u0440
+gb.fork = \u0444\u043E\u0440\u043A
+gb.forks = \u0444\u043E\u0440\u043A\u0438
+gb.forkRepository = fork {0}?
+gb.repositoryForked = {0} \u0431\u044B\u043B \u0444\u043E\u0440\u043A\u043D\u0443\u0442
+gb.repositoryForkFailed = fork \u043D\u0435 \u0443\u0434\u0430\u043B\u0441\u044F
+gb.personalRepositories = \u043B\u0438\u0447\u043D\u044B\u0435 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438
+gb.allowForks = \u0440\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044C \u0444\u043E\u0440\u043A\u0438
+gb.allowForksDescription = \u0440\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044C \u0430\u0432\u0442\u043E\u0440\u0438\u0437\u043E\u0432\u0430\u043D\u043D\u044B\u043C \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F\u043C \u0444\u043E\u0440\u043A\u0430\u0442\u044C \u044D\u0442\u043E\u0442 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439
+gb.forkedFrom = \u0444\u043E\u0440\u043A \u043E\u0442
+gb.canFork = \u043C\u043E\u0436\u043D\u043E \u0444\u043E\u0440\u043A\u0430\u0442\u044C
+gb.canForkDescription = \u043C\u043E\u0436\u0435\u0442 \u0444\u043E\u0440\u043A\u0430\u0442\u044C \u0430\u0432\u0442\u043E\u0440\u0438\u0437\u043E\u0432\u0430\u043D\u043D\u044B\u0435 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u0432 \u043B\u0438\u0447\u043D\u044B\u0435 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438
+gb.myFork = \u043F\u043E\u0441\u043C\u043E\u0442\u0440\u0435\u0442\u044C \u043C\u043E\u0439 \u0444\u043E\u0440\u043A
+gb.forksProjected = \u0444\u043E\u0440\u043A\u0438 \u0437\u0430\u043F\u0440\u0435\u0449\u0435\u043D\u044B
+gb.forksProjectedWarning = \u044D\u0442\u043E\u0442 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439 \u0437\u0430\u043F\u0440\u0435\u0449\u0430\u0435\u0442 \u0444\u043E\u0440\u043A\u0438
+gb.noForks = {0} \u043D\u0435 \u0438\u043C\u0435\u0435\u0442 \u0444\u043E\u0440\u043A\u043E\u0432
+gb.forkNotAuthorized = \u0438\u0437\u0432\u0438\u043D\u0438\u0442\u0435, \u0443 \u0432\u0430\u0441 \u043D\u0435\u0442 \u043F\u0440\u0430\u0432 \u043D\u0430 \u0444\u043E\u0440\u043A {0}
+gb.forkInProgress = \u0444\u043E\u0440\u043A \u0432 \u043F\u0440\u043E\u0446\u0435\u0441\u0441\u0435
+gb.preparingFork = \u043F\u043E\u0434\u0433\u043E\u0442\u043E\u0432\u043A\u0430 \u0432\u0430\u0448\u0435\u0433\u043E \u0444\u043E\u0440\u043A\u0430 ...
+gb.isFork = \u044F\u0432\u043B\u044F\u0435\u0442\u0441\u044F \u0444\u043E\u0440\u043A\u043E\u043C
+gb.canCreate = \u043C\u043E\u0436\u043D\u043E \u0441\u043E\u0437\u0434\u0430\u0442\u044C
+gb.canCreateDescription = \u043C\u043E\u0436\u0435\u0442 \u0441\u043E\u0437\u0434\u0430\u0432\u0430\u0442\u044C \u043B\u0438\u0447\u043D\u044B\u0435 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438
+gb.illegalPersonalRepositoryLocation = \u0432\u0430\u0448 \u043B\u0438\u0447\u043D\u044B\u0439 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439 \u0434\u043E\u043B\u0436\u0435\u043D \u043D\u0430\u0445\u043E\u0434\u0438\u0442\u044C\u0441\u044F \u0432 \"{0}\"
+gb.verifyCommitter = \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C \u043A\u043E\u043C\u043C\u0438\u0442\u0442\u0435\u0440\u0430
+gb.verifyCommitterDescription = \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044F \u0438\u0434\u0435\u043D\u0442\u0438\u0444\u0438\u043A\u0430\u0446\u0438\u044F \u043A\u043E\u043C\u043C\u0438\u0442\u0442\u0435\u0440\u0430 \u0434\u043B\u044F \u0441\u043E\u0432\u043F\u0430\u0434\u0435\u043D\u0438\u044F \u0441 \u043E\u0442\u043F\u0440\u0430\u0432\u043A\u043E\u0439 \u0443\u0447\u0435\u0442\u043D\u043E\u0439 \u0437\u0430\u043F\u0438\u0441\u0438 \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F Gitblit
+gb.verifyCommitterNote = \u0432\u0441\u0435 \u0441\u043B\u0438\u044F\u043D\u0438\u044F \u0442\u0440\u0435\u0431\u0443\u044E\u0442 "--no-ff" \u0434\u043B\u044F \u043E\u0431\u0435\u0441\u043F\u0435\u0447\u0435\u043D\u0438\u044F \u0438\u0434\u0435\u043D\u0442\u0438\u0444\u0438\u043A\u0430\u0446\u0438\u0438 \u043A\u043E\u043C\u043C\u0438\u0442\u0442\u0435\u0440\u0430
+gb.repositoryPermissions = \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043D\u0438\u044F \u0445\u0440\u0430\u043D\u0438\u043B\u0438\u0449\u0430
+gb.userPermissions = \u043F\u0440\u0430\u0432\u0430 \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F
+gb.teamPermissions = \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043D\u0438\u044F \u043A\u043E\u043C\u0430\u043D\u0434\u044B
+gb.add = \u0434\u043E\u0431\u0430\u0432\u0438\u0442\u044C
+gb.noPermission = \u0423\u0414\u0410\u041B\u0418\u0422\u042C \u042D\u0422\u041E \u0420\u0410\u0417\u0420\u0415\u0428\u0415\u041D\u0418\u0415
+gb.excludePermission = {0} (\u0438\u0441\u043A\u043B\u044E\u0447\u0438\u0442\u044C)
+gb.viewPermission = {0} (\u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440)
+gb.clonePermission = {0} (clone)
+gb.pushPermission = {0} (push)
+gb.createPermission = {0} (push, \u0441\u043E\u0437\u0434\u0430\u043D\u0438\u0435 \u0441\u0441\u044B\u043B\u043A\u0438 ref)
+gb.deletePermission = {0} (push, \u0441\u043E\u0437\u0434\u0430\u043D\u0438\u0435 \u0441\u0441\u044B\u043B\u043A\u0438 ref + \u0443\u0434\u0430\u043B\u0435\u043D\u0438\u0435)
+gb.rewindPermission = {0} (push, \u0441\u043E\u0437\u0434\u0430\u043D\u0438\u0435 \u0441\u0441\u044B\u043B\u043A\u0438 ref + \u0443\u0434\u0430\u043B\u0435\u043D\u0438\u0435 + rewind)
+gb.permission = \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043D\u0438\u0435
+gb.regexPermission = \u044D\u0442\u043E \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043D\u0438\u0435 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u043E \u0438\u0437 \u0440\u0435\u0433\u0443\u043B\u044F\u0440\u043D\u043E\u0433\u043E \u0432\u044B\u0440\u0430\u0436\u0435\u043D\u0438\u044F \"{0}\"
+gb.accessDenied = \u0434\u043E\u0441\u0442\u0443\u043F \u0437\u0430\u043F\u0440\u0435\u0449\u0435\u043D
+gb.busyCollectingGarbage = \u0438\u0437\u0432\u0438\u043D\u0438\u0442\u0435, Gitblit \u0437\u0430\u043D\u044F\u0442 \u0441\u0431\u043E\u0440\u043E\u043C \u043C\u0443\u0441\u043E\u0440\u0430 \u0432 {0}
+gb.gcPeriod = \u043F\u0435\u0440\u0438\u043E\u0434 GC
+gb.gcPeriodDescription = \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0438\u0442\u0435\u043B\u044C\u043D\u043E\u0441\u0442\u044C \u043C\u0435\u0436\u0434\u0443 \u0441\u0431\u043E\u0440\u043A\u0430\u043C\u0438 \u043C\u0443\u0441\u043E\u0440\u0430
+gb.gcThreshold = \u043F\u043E\u0440\u043E\u0433 GC
+gb.gcThresholdDescription = \u043C\u0438\u043D\u0438\u043C\u0430\u043B\u044C\u043D\u044B\u0439 \u043E\u0431\u0449\u0438\u0439 \u0440\u0430\u0437\u043C\u0435\u0440 \u043D\u0435\u0437\u0430\u043A\u0440\u0435\u043F\u043B\u0435\u043D\u043D\u044B\u0445 \u043E\u0431\u044A\u0435\u043A\u0442\u043E\u0432 \u0434\u043B\u044F \u0437\u0430\u043F\u0443\u0441\u043A\u0430 \u0440\u0430\u043D\u043D\u0435\u0439 \u0441\u0431\u043E\u0440\u043A\u0438 \u043C\u0443\u0441\u043E\u0440\u0430
+gb.ownerPermission = \u0432\u043B\u0430\u0434\u0435\u043B\u0435\u0446 \u0445\u0440\u0430\u043D\u0438\u043B\u0438\u0449\u0430
+gb.administrator = admin
+gb.administratorPermission = \u0430\u0434\u043C\u0438\u043D\u0438\u0441\u0442\u0440\u0430\u0442\u043E\u0440 Gitblit
+gb.team = \u043A\u043E\u043C\u0430\u043D\u0434\u0430
+gb.teamPermission = \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043D\u0438\u0435, \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u043D\u043E\u0435 \"{0}\" \u0447\u043B\u0435\u043D\u0441\u0442\u0432\u043E\u043C \u0432 \u043A\u043E\u043C\u0430\u043D\u0434\u0435
+gb.missing = \u043E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442!
+gb.missingPermission = \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439 \u0434\u043B\u044F \u044D\u0442\u043E\u0433\u043E \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043D\u0438\u044F \u043E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442!
+gb.mutable = \u0438\u0437\u043C\u0435\u043D\u044F\u0435\u043C\u043E\u0435
+gb.specified = \u0443\u043A\u0430\u0437\u0430\u043D\u043E\u0435
+gb.effective = \u044D\u0444\u0444\u0435\u043A\u0442\u0438\u0432\u043D\u043E\u0435
+gb.organizationalUnit = \u043E\u0440\u0433\u0430\u043D\u0438\u0437\u0430\u0446\u0438\u043E\u043D\u043D\u0430\u044F \u0435\u0434\u0438\u043D\u0438\u0446\u0430
+gb.organization = \u043E\u0440\u0433\u0430\u043D\u0438\u0437\u0430\u0446\u0438\u044F
+gb.locality = locality
+gb.stateProvince = \u0448\u0442\u0430\u0442 \u0438\u043B\u0438 \u043F\u0440\u043E\u0432\u0438\u043D\u0446\u0438\u044F
+gb.countryCode = \u043A\u043E\u0434 \u0441\u0442\u0440\u0430\u043D\u044B
+gb.properties = \u0441\u0432\u043E\u0439\u0441\u0442\u0432\u0430
+gb.issued = \u0432\u044B\u0434\u0430\u043D
+gb.expires = \u0438\u0441\u0442\u0435\u043A\u0430\u0435\u0442
+gb.expired = \u0438\u0441\u0442\u0435\u043A
+gb.expiring = \u0438\u0441\u0442\u0435\u043A\u0430\u044E\u0449\u0438\u0439
+gb.revoked = \u043E\u0442\u043E\u0437\u0432\u0430\u043D
+gb.serialNumber = \u0441\u0435\u0440\u0438\u0439\u043D\u044B\u0439 \u043D\u043E\u043C\u0435\u0440
+gb.certificates = \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043A\u0430\u0442\u044B
+gb.newCertificate = \u043D\u043E\u0432\u044B\u0439 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043A\u0430\u0442
+gb.revokeCertificate = \u043E\u0442\u043E\u0437\u0432\u0430\u0442\u044C \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043A\u0430\u0442
+gb.sendEmail = \u043E\u0442\u043F\u0440\u0430\u0432\u0438\u0442\u044C \u043F\u0438\u0441\u044C\u043C\u043E
+gb.passwordHint = \u043F\u043E\u0434\u0441\u043A\u0430\u0437\u043A\u0430 \u043F\u0430\u0440\u043E\u043B\u044F
+gb.ok = \u043E\u043A
+gb.invalidExpirationDate = \u043D\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043B\u044C\u043D\u0430\u044F \u0434\u0430\u0442\u0430 \u0438\u0441\u0442\u0435\u0447\u0435\u043D\u0438\u044F \u0441\u0440\u043E\u043A\u0430 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044F!
+gb.passwordHintRequired = \u043F\u043E\u0434\u0441\u043A\u0430\u0437\u043A\u0430 \u043F\u0430\u0440\u043E\u043B\u044F \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044F!
+gb.viewCertificate = \u043F\u043E\u0441\u043C\u043E\u0442\u0440\u0435\u0442\u044C \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043A\u0430\u0442
+gb.subject = \u0442\u0435\u043C\u0430
+gb.issuer = \u044D\u043C\u0438\u0442\u0435\u043D\u0442
+gb.validFrom = \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043B\u044C\u043D\u043E \u0441
+gb.validUntil = \u0434\u0435\u0439\u0441\u0442\u0432\u0443\u0435\u0442 \u0434\u043E
+gb.publicKey = \u043E\u0442\u043A\u0440\u044B\u0442\u044B\u0439 \u043A\u043B\u044E\u0447
+gb.signatureAlgorithm = \u0430\u043B\u0433\u043E\u0440\u0438\u0442\u043C \u043F\u043E\u0434\u043F\u0438\u0441\u0438
+gb.sha1FingerPrint = SHA-1 \u041E\u0442\u043F\u0435\u0447\u0430\u0442\u043E\u043A \u043F\u0430\u043B\u044C\u0446\u0430
+gb.md5FingerPrint = MD5 \u041E\u0442\u043F\u0435\u0447\u0430\u0442\u043E\u043A \u043F\u0430\u043B\u044C\u0446\u0430
+gb.reason = \u043F\u0440\u0438\u0447\u0438\u043D\u0430
+gb.revokeCertificateReason = \u041F\u043E\u0436\u0430\u043B\u0443\u0439\u0441\u0442\u0430, \u0432\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u043F\u0440\u0438\u0447\u0438\u043D\u0443 \u043E\u0442\u0437\u044B\u0432\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043A\u0430\u0442\u0430
+gb.unspecified = \u043D\u0435 \u0443\u043A\u0430\u0437\u0430\u043D\u043E
+gb.keyCompromise = \u043A\u043B\u044E\u0447\u0435\u0432\u043E\u0439 \u043A\u043E\u043C\u043F\u0440\u043E\u043C\u0438\u0441\u0441
+gb.caCompromise = CA \u043A\u043E\u043C\u043F\u0440\u043E\u043C\u0438\u0441\u0441
+gb.affiliationChanged = \u043F\u0440\u0438\u043D\u0430\u0434\u043B\u0435\u0436\u043D\u043E\u0441\u0442\u044C \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0430
+gb.superseded = \u0437\u0430\u043C\u0435\u043D\u0435\u043D\u043E
+gb.cessationOfOperation = \u043F\u0440\u0435\u043A\u0440\u0430\u0449\u0435\u043D\u0438\u0435 \u043E\u043F\u0435\u0440\u0430\u0446\u0438\u0438
+gb.privilegeWithdrawn = \u043F\u0440\u0438\u0432\u0438\u043B\u0435\u0433\u0438\u044F \u043E\u0442\u043E\u0437\u0432\u0430\u043D\u0430
+gb.time.inMinutes = \u0447\u0435\u0440\u0435\u0437 {0} \u043C\u0438\u043D\u0443\u0442
+gb.time.inHours = \u0447\u0435\u0440\u0435\u0437 {0} \u0447\u0430\u0441\u043E\u0432
+gb.time.inDays = \u0447\u0435\u0440\u0435\u0437 {0} \u0434\u043D\u0435\u0439
+gb.hostname = \u0438\u043C\u044F \u0445\u043E\u0441\u0442\u0430
+gb.hostnameRequired = \u041F\u043E\u0436\u0430\u043B\u0443\u0439\u0441\u0442\u0430, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043C\u044F \u0445\u043E\u0441\u0442\u0430
+gb.newSSLCertificate = \u043D\u043E\u0432\u044B\u0439 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043A\u0430\u0442 SSL \u0441\u0435\u0440\u0432\u0435\u0440\u0430
+gb.newCertificateDefaults = \u043D\u043E\u0432\u044B\u0439 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043A\u0430\u0442 \u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E
+gb.duration = \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0438\u0442\u0435\u043B\u044C\u043D\u043E\u0441\u0442\u044C
+gb.certificateRevoked = \u0421\u0435\u0440\u0442\u0438\u0444\u0438\u043A\u0430\u0442 {0,\u043D\u043E\u043C\u0435\u0440,0} \u0431\u044B\u043B \u043E\u0442\u043E\u0437\u0432\u0430\u043D
+gb.clientCertificateGenerated = \u0423\u0441\u043F\u0435\u0448\u043D\u043E \u0441\u043E\u0437\u0434\u0430\u043D \u043D\u043E\u0432\u044B\u0439 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043A\u0430\u0442 \u043A\u043B\u0438\u0435\u043D\u0442\u0430 \u0434\u043B\u044F {0}
+gb.sslCertificateGenerated = \u0423\u0441\u043F\u0435\u0448\u043D\u043E \u0441\u043E\u0437\u0434\u0430\u043D \u043D\u043E\u0432\u044B\u0439 \u0441\u0435\u0440\u0432\u0435\u0440\u043D\u044B\u0439 SSL-\u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043A\u0430\u0442 \u0434\u043B\u044F {0}
+gb.newClientCertificateMessage = \u041F\u0420\u0418\u041C\u0415\u0427\u0410\u041D\u0418\u0415: \n'password' - \u044D\u0442\u043E \u043D\u0435 \u043F\u0430\u0440\u043E\u043B\u044C \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F, \u044D\u0442\u043E \u043F\u0430\u0440\u043E\u043B\u044C \u0434\u043B\u044F \u0437\u0430\u0449\u0438\u0442\u044B \u0445\u0440\u0430\u043D\u0438\u043B\u0438\u0449\u0430 \u043A\u043B\u044E\u0447\u0435\u0439 \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F. \u042D\u0442\u043E\u0442 \u043F\u0430\u0440\u043E\u043B\u044C \u043D\u0435 \u0441\u043E\u0445\u0440\u0430\u043D\u044F\u0435\u0442\u0441\u044F, \u043F\u043E\u044D\u0442\u043E\u043C\u0443 \u0432\u044B \u0442\u0430\u043A\u0436\u0435 \u0434\u043E\u043B\u0436\u043D\u044B \u0432\u0432\u0435\u0441\u0442\u0438 \u043F\u043E\u0434\u0441\u043A\u0430\u0437\u043A\u0443, \u043A\u043E\u0442\u043E\u0440\u0430\u044F \u0431\u0443\u0434\u0435\u0442 \u0432\u043A\u043B\u044E\u0447\u0435\u043D\u0430 \u0432 \u0438\u043D\u0441\u0442\u0440\u0443\u043A\u0446\u0438\u0438 \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F README.
+gb.certificate = \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043A\u0430\u0442
+gb.emailCertificateBundle = \u043F\u0430\u043A\u0435\u0442 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043A\u0430\u0442\u043E\u0432 \u043A\u043B\u0438\u0435\u043D\u0442\u0430 \u044D\u043B\u0435\u043A\u0442\u0440\u043E\u043D\u043D\u043E\u0439 \u043F\u043E\u0447\u0442\u044B
+gb.pleaseGenerateClientCertificate = \u041F\u043E\u0436\u0430\u043B\u0443\u0439\u0441\u0442\u0430, \u0441\u043E\u0437\u0434\u0430\u0439\u0442\u0435 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043A\u0430\u0442 \u043A\u043B\u0438\u0435\u043D\u0442\u0430 \u0434\u043B\u044F {0}
+gb.clientCertificateBundleSent = \u041A\u043B\u0438\u0435\u043D\u0442\u0441\u043A\u0438\u0439 \u043F\u0430\u043A\u0435\u0442 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043A\u0430\u0442\u043E\u0432 \u0434\u043B\u044F {0} \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u043E
+gb.enterKeystorePassword = \u041F\u043E\u0436\u0430\u043B\u0443\u0439\u0441\u0442\u0430, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u043F\u0430\u0440\u043E\u043B\u044C \u0445\u0440\u0430\u043D\u0438\u043B\u0438\u0449\u0430 \u043A\u043B\u044E\u0447\u0435\u0439 Gitblit
+gb.warning = \u043F\u0440\u0435\u0434\u0443\u043F\u0440\u0435\u0436\u0434\u0435\u043D\u0438\u0435
+gb.jceWarning = \u0412\u0430\u0448\u0430 \u0441\u0440\u0435\u0434\u0430 \u0432\u044B\u043F\u043E\u043B\u043D\u0435\u043D\u0438\u044F Java \u043D\u0435 \u0438\u043C\u0435\u0435\u0442 \u0444\u0430\u0439\u043B\u043E\u0432 \"\u041F\u043E\u043B\u0438\u0442\u0438\u043A\u0430 \u043D\u0435\u043E\u0433\u0440\u0430\u043D\u0438\u0447\u0435\u043D\u043D\u043E\u0439 \u044E\u0440\u0438\u0441\u0434\u0438\u043A\u0446\u0438\u0438 JCE\". \n\u042D\u0442\u043E \u043E\u0433\u0440\u0430\u043D\u0438\u0447\u0438\u0442 \u0434\u043B\u0438\u043D\u0443 \u043F\u0430\u0440\u043E\u043B\u0435\u0439, \u043A\u043E\u0442\u043E\u0440\u044B\u0435 \u0432\u044B \u043C\u043E\u0436\u0435\u0442\u0435 \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u044C \u0434\u043B\u044F \u0448\u0438\u0444\u0440\u043E\u0432\u0430\u043D\u0438\u044F \u0445\u0440\u0430\u043D\u0438\u043B\u0438\u0449 \u043A\u043B\u044E\u0447\u0435\u0439, \u0434\u043E 7 \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432. \n\u042D\u0442\u0438 \u0444\u0430\u0439\u043B\u044B \u043F\u043E\u043B\u0438\u0442\u0438\u043A\u0438 \u043C\u043E\u0436\u043D\u043E \u0437\u0430\u0433\u0440\u0443\u0436\u0430\u0442\u044C \u0434\u043E\u043F\u043E\u043B\u043D\u0438\u0442\u0435\u043B\u044C\u043D\u043E \u0438\u0437 Oracle. \n \n\u0412 \u043B\u044E\u0431\u043E\u043C \u0441\u043B\u0443\u0447\u0430\u0435 \u0445\u043E\u0442\u0438\u0442\u0435 \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0438\u0442\u044C \u0438 \u0441\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0438\u043D\u0444\u0440\u0430\u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0443 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043A\u0430\u0442\u043E\u0432? \n \n\u041E\u0442\u0432\u0435\u0442 \u041D\u0435\u0442 \u043F\u0435\u0440\u0435\u043D\u0430\u043F\u0440\u0430\u0432\u0438\u0442 \u0432\u0430\u0448 \u0431\u0440\u0430\u0443\u0437\u0435\u0440 \u043D\u0430 \u0441\u0442\u0440\u0430\u043D\u0438\u0446\u0443 \u0437\u0430\u0433\u0440\u0443\u0437\u043A\u0438 Oracle, \u0447\u0442\u043E\u0431\u044B \u0432\u044B \u043C\u043E\u0433\u043B\u0438 \u0437\u0430\u0433\u0440\u0443\u0436\u0430\u0442\u044C \u0444\u0430\u0439\u043B\u044B \u043F\u043E\u043B\u0438\u0442\u0438\u043A\u0438.
+gb.maxActivityCommits = \u043C\u0430\u043A\u0441\u0438\u043C\u0430\u043B\u044C\u043D\u0430\u044F \u0430\u043A\u0442\u0438\u0432\u043D\u043E\u0441\u0442\u044C \u043A\u043E\u043C\u043C\u0438\u0442\u043E\u0432
+gb.maxActivityCommitsDescription = \u043C\u0430\u043A\u0441\u0438\u043C\u0430\u043B\u044C\u043D\u043E\u0435 \u043A\u043E\u043B\u0438\u0447\u0435\u0441\u0442\u0432\u043E \u043A\u043E\u043C\u043C\u0438\u0442\u043E\u0432 \u0434\u043B\u044F \u0434\u043E\u0431\u0430\u0432\u043B\u0435\u043D\u0438\u044F \u043D\u0430 \u0441\u0442\u0440\u0430\u043D\u0438\u0446\u0443 \u0430\u043A\u0442\u0438\u0432\u043D\u043E\u0441\u0442\u0438
+gb.noMaximum = \u043D\u0435\u0442 \u043C\u0430\u043A\u0441\u0438\u043C\u0443\u043C\u0430
+gb.attributes = \u0430\u0442\u0440\u0438\u0431\u0443\u0442\u044B
+gb.serveCertificate = \u0441\u043B\u0443\u0436\u0438\u0442\u044C https \u0441 \u044D\u0442\u0438\u043C \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043A\u0430\u0442\u043E\u043C
+gb.sslCertificateGeneratedRestart = \u0423\u0441\u043F\u0435\u0448\u043D\u043E \u0441\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u043E\u0432\u0430\u043D \u043D\u043E\u0432\u044B\u0439 \u0441\u0435\u0440\u0432\u0435\u0440\u043D\u044B\u0439 SSL-\u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043A\u0430\u0442 \u0434\u043B\u044F {0}. \n\u0412\u044B \u0434\u043E\u043B\u0436\u043D\u044B \u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u0442\u0438\u0442\u044C Gitblit, \u0447\u0442\u043E\u0431\u044B \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u044C \u043D\u043E\u0432\u044B\u0439 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043A\u0430\u0442. \n \n\u0415\u0441\u043B\u0438 \u0432\u044B \u0437\u0430\u043F\u0443\u0441\u043A\u0430\u0435\u0442\u0435 \u0441 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u043E\u043C '--alias', \u0432\u0430\u043C \u043D\u0443\u0436\u043D\u043E \u0431\u0443\u0434\u0435\u0442 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442\u044C \u0435\u0433\u043E \u0432 ''--alias {0}''.
+gb.validity = \u0434\u043E\u0441\u0442\u043E\u0432\u0435\u0440\u043D\u043E\u0441\u0442\u044C
+gb.siteName = \u0438\u043C\u044F \u0441\u0430\u0439\u0442\u0430
+gb.siteNameDescription = \u043A\u0440\u0430\u0442\u043A\u043E\u0435, \u043E\u043F\u0438\u0441\u0430\u0442\u0435\u043B\u044C\u043D\u043E\u0435 \u0438\u043C\u044F \u0432\u0430\u0448\u0435\u0433\u043E \u0441\u0435\u0440\u0432\u0435\u0440\u0430
+gb.excludeFromActivity = \u0438\u0441\u043A\u043B\u044E\u0447\u0438\u0442\u044C \u0441\u043E \u0441\u0442\u0440\u0430\u043D\u0438\u0446\u044B \u0430\u043A\u0442\u0438\u0432\u043D\u043E\u0441\u0442\u0438
+gb.isSparkleshared = \u0445\u0440\u0430\u043D\u0438\u043B\u0438\u0449\u0435 Sparkleshared
+gb.owners = \u0432\u043B\u0430\u0434\u0435\u043B\u044C\u0446\u0435\u0432
+gb.sessionEnded = \u0421\u0435\u0441\u0441\u0438\u044F \u0431\u044B\u043B\u0430 \u0437\u0430\u043A\u0440\u044B\u0442\u0430
+gb.closeBrowser = \u041F\u043E\u0436\u0430\u043B\u0443\u0439\u0441\u0442\u0430, \u0437\u0430\u043A\u0440\u043E\u0439\u0442\u0435 \u0431\u0440\u0430\u0443\u0437\u0435\u0440, \u0447\u0442\u043E\u0431\u044B \u043F\u0440\u0430\u0432\u0438\u043B\u044C\u043D\u043E \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044C \u0441\u0435\u0430\u043D\u0441.
+gb.doesNotExistInTree = {0} \u043D\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442 \u0432 \u0434\u0435\u0440\u0435\u0432\u0435 {1}
+gb.enableIncrementalPushTags = \u0432\u043A\u043B\u044E\u0447\u0438\u0442\u044C \u0434\u043E\u0431\u0430\u0432\u043E\u0447\u043D\u044B\u0435 push-\u0442\u0435\u0433\u0438
+gb.useIncrementalPushTagsDescription = \u043F\u0440\u0438 \u043D\u0430\u0436\u0430\u0442\u0438\u0438, \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438 \u043F\u043E\u043C\u0435\u0447\u0430\u0435\u0442 \u043A\u0430\u0436\u0434\u0443\u044E \u0432\u0435\u0442\u0432\u044C \u0441 \u043F\u043E\u043C\u043E\u0449\u044C\u044E \u0434\u043E\u0431\u0430\u0432\u043E\u0447\u043D\u043E\u0433\u043E \u043D\u043E\u043C\u0435\u0440\u0430 \u0440\u0435\u0434\u0430\u043A\u0446\u0438\u0438
+gb.incrementalPushTagMessage = \u0410\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438 \u043F\u043E\u043C\u0435\u0447\u0435\u043D\u043D\u0430\u044F \u0432\u0435\u0442\u0432\u044C [{0}] \u043F\u0440\u0438 \u043D\u0430\u0436\u0430\u0442\u0438\u0438
+gb.externalPermissions = {0} \u043F\u0440\u0430\u0432\u0430 \u0434\u043E\u0441\u0442\u0443\u043F\u0430 \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u044E\u0442\u0441\u044F \u0438\u0437\u0432\u043D\u0435
+gb.viewAccess = \u0423 \u0432\u0430\u0441 \u043D\u0435\u0442 \u0434\u043E\u0441\u0442\u0443\u043F\u0430 \u043A Gitblit \u0434\u043B\u044F \u0447\u0442\u0435\u043D\u0438\u044F \u0438\u043B\u0438 \u0437\u0430\u043F\u0438\u0441\u0438
+gb.overview = \u043E\u0431\u0437\u043E\u0440
+gb.dashboard = \u0441\u0442\u0435\u043D\u0430
+gb.monthlyActivity = \u0435\u0436\u0435\u043C\u0435\u0441\u044F\u0447\u043D\u0430\u044F \u0430\u043A\u0442\u0438\u0432\u043D\u043E\u0441\u0442\u044C
+gb.myProfile = \u043C\u043E\u0439 \u043F\u0440\u043E\u0444\u0438\u043B\u044C
+gb.compare = \u0441\u0440\u0430\u0432\u043D\u0438\u0442\u044C
+gb.manual = \u0432\u0440\u0443\u0447\u043D\u0443\u044E
+gb.from = \u043E\u0442
+gb.to = \u043A
+gb.at = \u043D\u0430
+gb.of = \u0438\u0437
+gb.in = \u0432
+gb.moreChanges = \u0432\u0441\u0435 \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u044F ...
+gb.pushedNCommitsTo = \u043E\u0442\u043F\u0440\u0430\u0432\u0438\u043B {0} \u043A\u043E\u043C\u043C\u0438\u0442\u043E\u0432 \u0432
+gb.pushedOneCommitTo = \u043E\u0442\u043F\u0440\u0430\u0432\u0438\u043B 1 \u043A\u043E\u043C\u043C\u0438\u0442 \u0432
+gb.commitsTo = {0} \u043A\u043E\u043C\u043C\u0438\u0442\u043E\u0432 \u0432
+gb.oneCommitTo = 1 \u043A\u043E\u043C\u043C\u0438\u0442 \u0432
+gb.byNAuthors = \u043E\u0442 {0} \u0430\u0432\u0442\u043E\u0440\u043E\u0432
+gb.byOneAuthor = \u043E\u0442 {0}
+gb.viewComparison = \u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440\u0435\u0442\u044C \u0441\u0440\u0430\u0432\u043D\u0435\u043D\u0438\u0435 \u044D\u0442\u0438\u0445 {0} \u043A\u043E\u043C\u043C\u0438\u0442\u043E\u0432 \u00bb
+gb.nMoreCommits = {0} \u0431\u043E\u043B\u044C\u0448\u0435 \u043A\u043E\u043C\u043C\u0438\u0442\u043E\u0432 \u00bb
+gb.oneMoreCommit = \u0435\u0449\u0435 1 \u043A\u043E\u043C\u043C\u0438\u0442 \u00bb
+gb.pushNewTag = \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u0435\u043D \u043D\u043E\u0432\u044B\u0439 \u0442\u0435\u0433
+gb.createdNewTag = \u0441\u043E\u0437\u0434\u0430\u043D \u043D\u043E\u0432\u044B\u0439 \u0442\u0435\u0433
+gb.deletedTag = \u0443\u0434\u0430\u043B\u0435\u043D \u0442\u0435\u0433
+gb.pushNewBranch = \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0430 \u043D\u043E\u0432\u0430\u044F \u0432\u0435\u0442\u043A\u0430
+gb.createdNewBranch = \u0441\u043E\u0437\u0434\u0430\u043D\u0430 \u043D\u043E\u0432\u0430\u044F \u0432\u0435\u0442\u043A\u0430
+gb.deletedBranch = \u0443\u0434\u0430\u043B\u0435\u043D\u0430 \u0432\u0435\u0442\u0432\u044C
+gb.createdNewPullRequest = \u0441\u043E\u0437\u0434\u0430\u043D \u043F\u0443\u043B-\u0437\u0430\u043F\u0440\u043E\u0441
+gb.mergedPullRequest = \u043E\u0431\u044A\u0435\u0434\u0438\u043D\u0435\u043D \u043F\u0443\u043B-\u0437\u0430\u043F\u0440\u043E\u0441
+gb.rewind = \u041E\u0422\u041A\u0410\u0422
+gb.star = \u043E\u0442\u043C\u0435\u0442\u0438\u0442\u044C
+gb.unstar = \u0443\u0431\u0440\u0430\u0442\u044C \u043E\u0442\u043C\u0435\u0442\u043A\u0443
+gb.stargazers = \u0437\u0432\u0435\u0437\u0434\u043E\u0447\u0435\u0442\u044B
+gb.starredRepositories = \u0438\u0437\u0431\u0440\u0430\u043D\u043D\u044B\u0435 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438
+gb.failedToUpdateUser = \u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043E\u0431\u043D\u043E\u0432\u0438\u0442\u044C \u0443\u0447\u0435\u0442\u043D\u0443\u044E \u0437\u0430\u043F\u0438\u0441\u044C \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F!
+gb.myRepositories = \u043C\u043E\u0438 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438
+gb.noActivity = \u043D\u0435 \u0431\u044B\u043B\u043E \u0430\u043A\u0442\u0438\u0432\u043D\u043E\u0441\u0442\u0438 \u0437\u0430 \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0438\u0435 {0} \u0434\u043D\u0435\u0439
+gb.findSomeRepositories = \u043D\u0430\u0439\u0442\u0438 \u043D\u0435\u0441\u043A\u043E\u043B\u044C\u043A\u043E \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0435\u0432
+gb.metricAuthorExclusion = \u0438\u0441\u043A\u043B\u044E\u0447\u0435\u043D\u0438\u044F \u0438\u0437 \u043C\u0435\u0442\u0440\u0438\u043A \u0430\u0432\u0442\u043E\u0440\u0430
+gb.myDashboard = \u043C\u043E\u044F \u0441\u0442\u0435\u043D\u0430
+gb.failedToFindAccount = \u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043D\u0430\u0439\u0442\u0438 \u0443\u0447\u0435\u0442\u043D\u0443\u044E \u0437\u0430\u043F\u0438\u0441\u044C \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F ''{0}''
+gb.reflog = reflog
+gb.active = \u0430\u043A\u0442\u0438\u0432\u043D\u044B\u0435
+gb.starred = \u043E\u0442\u043C\u0435\u0447\u0435\u043D\u043D\u044B\u0435
+gb.owned = \u0441\u043E\u0431\u0441\u0442\u0432\u0435\u043D\u043D\u044B\u0435
+gb.starredAndOwned = \u043F\u043E\u043C\u0435\u0447\u0435\u043D\u043E \u0438 \u043F\u0440\u0438\u043D\u0430\u0434\u043B\u0435\u0436\u0438\u0442
+gb.reviewPatchset = \u043E\u0431\u0437\u043E\u0440 {0} \u0438\u0441\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0438\u0439 {1}
+gb.todaysActivityStats = \u0441\u0435\u0433\u043E\u0434\u043D\u044F / {1} \u0437\u0430\u0444\u0438\u043A\u0441\u0438\u0440\u043E\u0432\u0430\u043D\u043E {2} \u0430\u0432\u0442\u043E\u0440\u0430\u043C\u0438
+gb.todaysActivityNone = \u0441\u0435\u0433\u043E\u0434\u043D\u044F / \u043D\u0435\u0442
+gb.noActivityToday = \u0441\u0435\u0433\u043E\u0434\u043D\u044F \u0430\u043A\u0442\u0438\u0432\u043D\u043E\u0441\u0442\u0438 \u043D\u0435 \u0431\u044B\u043B\u043E
+gb.anonymousUser = \u0430\u043D\u043E\u043D\u0438\u043C\u043D\u044B\u0439
+gb.commitMessageRenderer = \u0437\u0430\u0444\u0438\u043A\u0441\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0435 \u0440\u0435\u043D\u0434\u0435\u0440\u0435\u0440\u0430
+gb.diffStat = {0} \u0432\u0441\u0442\u0430\u0432\u043E\u043A \u0438 {1} \u0443\u0434\u0430\u043B\u0435\u043D\u0438\u0439
+gb.home = \u0434\u043E\u043C\u043E\u0439
+gb.isMirror = \u044D\u0442\u043E\u0442 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439 \u044F\u0432\u043B\u044F\u0435\u0442\u0441\u044F \u0437\u0435\u0440\u043A\u0430\u043B\u043E\u043C
+gb.mirrorOf = \u0437\u0435\u0440\u043A\u0430\u043B\u043E {0}
+gb.mirrorWarning = \u044D\u0442\u043E\u0442 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439 \u044F\u0432\u043B\u044F\u0435\u0442\u0441\u044F \u0437\u0435\u0440\u043A\u0430\u043B\u043E\u043C \u0438 \u043D\u0435 \u043C\u043E\u0436\u0435\u0442 \u043F\u043E\u043B\u0443\u0447\u0430\u0442\u044C \u043D\u0430\u0436\u0430\u0442\u0438\u044F
+gb.docsWelcome1 = \u0412\u044B \u043C\u043E\u0436\u0435\u0442\u0435 \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u044C \u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u044B \u0434\u043B\u044F \u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u044F \u0432\u0430\u0448\u0435\u0433\u043E \u0445\u0440\u0430\u043D\u0438\u043B\u0438\u0449\u0430.
+gb.docsWelcome2 = \u0417\u0430\u0444\u0438\u043A\u0441\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0444\u0430\u0439\u043B README.md \u0438\u043B\u0438 HOME.md \u0434\u043B\u044F \u043D\u0430\u0447\u0430\u043B\u0430.
+gb.createReadme = \u0441\u043E\u0437\u0434\u0430\u0442\u044C \u0444\u0430\u0439\u043B README
+gb.responsible = \u043E\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0435\u043D\u043D\u044B\u0439
+gb.createdThisTicket = \u0441\u043E\u0437\u0434\u0430\u043B \u044D\u0442\u043E\u0442 \u0431\u0438\u043B\u0435\u0442
+gb.proposedThisChange = \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0438\u043B \u044D\u0442\u043E \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u0435
+gb.uploadedPatchsetN = \u0437\u0430\u0433\u0440\u0443\u0436\u0435\u043D\u043D\u044B\u0439 \u043D\u0430\u0431\u043E\u0440 \u043F\u0430\u0442\u0447\u0435\u0439 {0}
+gb.uploadedPatchsetNRevisionN = \u0437\u0430\u0433\u0440\u0443\u0436\u0435\u043D\u043D\u044B\u0439 \u043D\u0430\u0431\u043E\u0440 \u043F\u0430\u0442\u0447\u0435\u0439 {0} \u0440\u0435\u0432\u0438\u0437\u0438\u044F {1}
+gb.mergedPatchset = \u043E\u0431\u044A\u0435\u0434\u0438\u043D\u0435\u043D\u043D\u044B\u0439 \u043D\u0430\u0431\u043E\u0440 \u043F\u0430\u0442\u0447\u0435\u0439
+gb.commented = \u043F\u0440\u043E\u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0438\u0440\u043E\u0432\u0430\u043B
+gb.noDescriptionGiven = \u043E\u043F\u0438\u0441\u0430\u043D\u0438\u0435 \u043D\u0435 \u0443\u043A\u0430\u0437\u0430\u043D\u043E
+gb.toBranch = \u0432 {0}
+gb.createdBy = \u0441\u043E\u0437\u0434\u0430\u043D
+gb.oneParticipant = {0} \u0443\u0447\u0430\u0441\u0442\u043D\u0438\u043A
+gb.nParticipants = {0} \u0443\u0447\u0430\u0441\u0442\u043D\u0438\u043A\u043E\u0432
+gb.noComments = \u043D\u0435\u0442 \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0435\u0432
+gb.oneComment = {0} \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0439
+gb.nComments = {0} \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0435\u0432
+gb.oneAttachment = {0} \u0432\u043B\u043E\u0436\u0435\u043D\u0438\u0435
+gb.nAttachments = {0} \u0432\u043B\u043E\u0436\u0435\u043D\u0438\u0439
+gb.milestone = \u0432\u0435\u0445\u0430
+gb.compareToMergeBase = \u0441\u0440\u0430\u0432\u043D\u0438\u0442\u044C \u0441 \u0431\u0430\u0437\u043E\u0439 \u0441\u043B\u0438\u044F\u043D\u0438\u044F
+gb.compareToN = \u0441\u0440\u0430\u0432\u043D\u0438\u0442\u044C \u0441 {0}
+gb.open = \u043E\u0442\u043A\u0440\u044B\u0442
+gb.closed = \u0437\u0430\u043A\u0440\u044B\u0442
+gb.merged = \u043E\u0431\u044A\u0435\u0434\u0438\u043D\u0435\u043D
+gb.ticketPatchset = \u0437\u0430\u044F\u0432\u043E\u043A {0}, \u0438\u0441\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0438\u0439 {1}
+gb.patchsetMergeable = \u042D\u0442\u043E\u0442 \u043D\u0430\u0431\u043E\u0440 \u043F\u0430\u0442\u0447\u0435\u0439 \u043C\u043E\u0436\u0435\u0442 \u0431\u044B\u0442\u044C \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438 \u043E\u0431\u044A\u0435\u0434\u0438\u043D\u0435\u043D \u0432 {0}.
+gb.patchsetMergeableMore = \u042D\u0442\u043E\u0442 \u043D\u0430\u0431\u043E\u0440 \u043F\u0430\u0442\u0447\u0435\u0439 \u0442\u0430\u043A\u0436\u0435 \u043C\u043E\u0436\u0435\u0442 \u0431\u044B\u0442\u044C \u043E\u0431\u044A\u0435\u0434\u0438\u043D\u0435\u043D \u0441 {0} \u0438\u0437 \u043A\u043E\u043C\u0430\u043D\u0434\u043D\u043E\u0439 \u0441\u0442\u0440\u043E\u043A\u0438.
+gb.patchsetAlreadyMerged = \u042D\u0442\u043E\u0442 \u043D\u0430\u0431\u043E\u0440 \u043F\u0430\u0442\u0447\u0435\u0439 \u0431\u044B\u043B \u043E\u0431\u044A\u0435\u0434\u0438\u043D\u0435\u043D \u0441 {0}.
+gb.patchsetNotMergeable = \u042D\u0442\u043E\u0442 \u043D\u0430\u0431\u043E\u0440 \u043F\u0430\u0442\u0447\u0435\u0439 \u043D\u0435 \u043C\u043E\u0436\u0435\u0442 \u0431\u044B\u0442\u044C \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438 \u043E\u0431\u044A\u0435\u0434\u0438\u043D\u0435\u043D \u0432 {0}.
+gb.patchsetNotMergeableMore = \u042D\u0442\u043E\u0442 \u043D\u0430\u0431\u043E\u0440 \u0438\u0441\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0438\u0439 \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043F\u0435\u0440\u0435\u0431\u0430\u0437\u0438\u0440\u043E\u0432\u0430\u043D \u0438\u043B\u0438 \u0432\u0440\u0443\u0447\u043D\u0443\u044E \u043E\u0431\u044A\u0435\u0434\u0438\u043D\u0435\u043D \u0432 {0} \u0434\u043B\u044F \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043D\u0438\u044F \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442\u043E\u0432.
+gb.patchsetNotApproved = \u042D\u0442\u0430 \u0432\u0435\u0440\u0441\u0438\u044F \u043D\u0430\u0431\u043E\u0440\u0430 \u0438\u0441\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0438\u0439 \u043D\u0435 \u0431\u044B\u043B\u0430 \u043E\u0434\u043E\u0431\u0440\u0435\u043D\u0430 \u0434\u043B\u044F \u043E\u0431\u044A\u0435\u0434\u0438\u043D\u0435\u043D\u0438\u044F \u0432 {0}.
+gb.patchsetNotApprovedMore = \u0420\u0435\u0446\u0435\u043D\u0437\u0435\u043D\u0442 \u0434\u043E\u043B\u0436\u0435\u043D \u043E\u0434\u043E\u0431\u0440\u0438\u0442\u044C \u044D\u0442\u043E\u0442 \u043D\u0430\u0431\u043E\u0440 \u043F\u0430\u0442\u0447\u0435\u0439.
+gb.patchsetVetoedMore = \u0420\u0435\u0446\u0435\u043D\u0437\u0435\u043D\u0442 \u043D\u0430\u043B\u043E\u0436\u0438\u043B \u0432\u0435\u0442\u043E \u043D\u0430 \u044D\u0442\u043E\u0442 \u043D\u0430\u0431\u043E\u0440 \u043F\u0430\u0442\u0447\u0435\u0439.
+gb.write = \u0437\u0430\u043F\u0438\u0441\u044C
+gb.comment = \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0439
+gb.preview = \u041F\u0440\u0435\u0434\u0432\u0430\u0440\u0438\u0442\u0435\u043B\u044C\u043D\u044B\u0439 \u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440
+gb.leaveComment = \u043E\u0441\u0442\u0430\u0432\u0438\u0442\u044C \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0439 ...
+gb.showHideDetails = \u043F\u043E\u043A\u0430\u0437\u0430\u0442\u044C / \u0441\u043A\u0440\u044B\u0442\u044C \u0434\u0435\u0442\u0430\u043B\u0438
+gb.acceptNewPatchsets = \u043F\u0440\u0438\u043D\u044F\u0442\u044C \u043D\u0430\u0431\u043E\u0440\u044B \u043F\u0430\u0442\u0447\u0435\u0439
+gb.acceptNewPatchsetsDescription = \u043F\u0440\u0438\u043D\u044F\u0442\u044C \u043D\u0430\u0431\u043E\u0440\u044B \u043F\u0430\u0442\u0447\u0435\u0439, \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u043D\u044B\u0435 \u0432 \u044D\u0442\u043E\u0442 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439
+gb.acceptNewTickets = \u0440\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044C \u043D\u043E\u0432\u044B\u0435 \u0437\u0430\u044F\u0432\u043E\u043A
+gb.acceptNewTicketsDescription = \u0440\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044C \u0441\u043E\u0437\u0434\u0430\u043D\u0438\u0435 \u043E\u0448\u0438\u0431\u043E\u043A, \u0443\u043B\u0443\u0447\u0448\u0435\u043D\u0438\u0439, \u0437\u0430\u0434\u0430\u0447 \u0438 \u0442. \u0434.
+gb.requireApproval = \u0442\u0440\u0435\u0431\u043E\u0432\u0430\u0442\u044C \u043E\u0434\u043E\u0431\u0440\u0435\u043D\u0438\u044F
+gb.requireApprovalDescription = \u043D\u0430\u0431\u043E\u0440\u044B \u043F\u0430\u0442\u0447\u0435\u0439 \u0434\u043E\u043B\u0436\u043D\u044B \u0431\u044B\u0442\u044C \u0443\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u044B \u0434\u043E \u0442\u043E\u0433\u043E, \u043A\u0430\u043A \u0431\u0443\u0434\u0435\u0442 \u0432\u043A\u043B\u044E\u0447\u0435\u043D\u0430 \u043A\u043D\u043E\u043F\u043A\u0430 \u0441\u043B\u0438\u044F\u043D\u0438\u044F
+gb.topic = \u0442\u0435\u043C\u0430
+gb.proposalTickets = \u043F\u0440\u0435\u0434\u043B\u0430\u0433\u0430\u0435\u043C\u044B\u0435 \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u044F
+gb.bugTickets = \u0431\u0430\u0433\u0438
+gb.enhancementTickets = \u0443\u043B\u0443\u0447\u0448\u0435\u043D\u0438\u044F
+gb.taskTickets = \u0437\u0430\u0434\u0430\u0447\u0438
+gb.questionTickets = \u0432\u043E\u043F\u0440\u043E\u0441\u044B
+gb.maintenanceTickets = \u043E\u0431\u0441\u043B\u0443\u0436\u0438\u0432\u0430\u043D\u0438\u0435
+gb.requestTickets = \u0443\u043B\u0443\u0447\u0448\u0435\u043D\u0438\u044F \u0438 \u0437\u0430\u0434\u0430\u0447\u0438
+gb.yourCreatedTickets = \u0441\u043E\u0437\u0434\u0430\u043D \u0432\u0430\u043C\u0438
+gb.yourWatchedTickets = \u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440\u0435\u043D\u043D\u044B\u0445 \u0432\u0430\u043C\u0438
+gb.mentionsMeTickets = \u0443\u043F\u043E\u043C\u0438\u043D\u0430\u043D\u0438\u0435 \u0432\u0430\u0441
+gb.updatedBy = \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u043E
+gb.sort = \u0441\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u043A\u0430
+gb.sortNewest = \u043F\u043E \u043D\u043E\u0432\u0438\u0437\u043D\u0435
+gb.sortOldest = \u043F\u043E \u0441\u0442\u0430\u0440\u043E\u0441\u0442\u0438
+gb.sortMostRecentlyUpdated = \u043D\u0435\u0434\u0430\u0432\u043D\u043E \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u043D\u044B\u0439
+gb.sortLeastRecentlyUpdated = \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0435\u0435 \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0438\u0435
+gb.sortMostComments = \u0431\u043E\u043B\u044C\u0448\u0438\u043D\u0441\u0442\u0432\u043E \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0435\u0432
+gb.sortLeastComments = \u043C\u0438\u043D\u0438\u043C\u0443\u043C \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0435\u0432
+gb.sortMostPatchsetRevisions = \u0431\u043E\u043B\u044C\u0448\u0438\u043D\u0441\u0442\u0432\u043E \u0440\u0435\u0432\u0438\u0437\u0438\u0439 \u043D\u0430\u0431\u043E\u0440\u0430 \u043F\u0430\u0442\u0447\u0435\u0439
+gb.sortLeastPatchsetRevisions = \u043C\u0438\u043D\u0438\u043C\u0443\u043C \u0440\u0435\u0432\u0438\u0437\u0438\u0439 \u043D\u0430\u0431\u043E\u0440\u0430 \u043F\u0430\u0442\u0447\u0435\u0439
+gb.sortMostVotes = \u0431\u043E\u043B\u044C\u0448\u0438\u043D\u0441\u0442\u0432\u043E \u0433\u043E\u043B\u043E\u0441\u043E\u0432
+gb.sortLeastVotes = \u043D\u0430\u0438\u043C\u0435\u043D\u044C\u0448\u0435\u0435 \u043A\u043E\u043B\u0438\u0447\u0435\u0441\u0442\u0432\u043E \u0433\u043E\u043B\u043E\u0441\u043E\u0432
+gb.topicsAndLabels = \u0442\u0435\u043C\u044B \u0438 \u044F\u0440\u043B\u044B\u043A\u0438
+gb.milestones = \u0432\u0435\u0445\u0438
+gb.noMilestoneSelected = \u0432\u0435\u0445\u0430 \u043D\u0435 \u0432\u044B\u0431\u0440\u0430\u043D\u0430
+gb.notSpecified = \u043D\u0435 \u0443\u043A\u0430\u0437\u0430\u043D\u043E
+gb.due = \u0434\u043E\u043B\u0436\u043D\u044B\u0439
+gb.queries = \u0437\u0430\u043F\u0440\u043E\u0441\u044B
+gb.searchTicketsTooltip = \u0438\u0441\u043A\u0430\u0442\u044C {0} \u0437\u0430\u044F\u0432\u043E\u043A
+gb.searchTickets = \u043F\u043E\u0438\u0441\u043A \u0437\u0430\u044F\u0432\u043E\u043A
+gb.new = \u043D\u043E\u0432\u044B\u0439
+gb.newTicket = \u043D\u043E\u0432\u0430\u044F \u0437\u0430\u044F\u0432\u043A\u0430
+gb.editTicket = \u0440\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0437\u0430\u044F\u0432\u043A\u0443
+gb.ticketsWelcome = \u0412\u044B \u043C\u043E\u0436\u0435\u0442\u0435 \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u044C \u0437\u0430\u044F\u0432\u043A\u0438 \u0434\u043B\u044F \u043E\u0440\u0433\u0430\u043D\u0438\u0437\u0430\u0446\u0438\u0438 \u0441\u0432\u043E\u0435\u0433\u043E \u0441\u043F\u0438\u0441\u043A\u0430 \u0437\u0430\u0434\u0430\u0447, \u043E\u0431\u0441\u0443\u0436\u0434\u0435\u043D\u0438\u044F \u043E\u0448\u0438\u0431\u043E\u043A \u0438 \u0441\u043E\u0432\u043C\u0435\u0441\u0442\u043D\u043E\u0439 \u0440\u0430\u0431\u043E\u0442\u044B \u043D\u0430\u0434 \u043D\u0430\u0431\u043E\u0440\u0430\u043C\u0438 \u043F\u0430\u0442\u0447\u0435\u0439.
+gb.createFirstTicket = \u0441\u043E\u0437\u0434\u0430\u0442\u044C \u0441\u0432\u043E\u0439 \u043F\u0435\u0440\u0432\u0443\u044E \u0437\u0430\u044F\u0432\u043A\u0443
+gb.title = \u043D\u0430\u0437\u0432\u0430\u043D\u0438\u0435
+gb.changedStatus = \u0438\u0437\u043C\u0435\u043D\u0438\u043B \u0441\u0442\u0430\u0442\u0443\u0441
+gb.discussion = \u043E\u0431\u0441\u0443\u0436\u0434\u0435\u043D\u0438\u0435
+gb.updated = \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u043E
+gb.proposePatchset = \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0438\u0442\u044C \u043D\u0430\u0431\u043E\u0440 \u043F\u0430\u0442\u0447\u0435\u0439
+gb.proposePatchsetNote = \u0412\u044B \u043C\u043E\u0436\u0435\u0442\u0435 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0438\u0442\u044C \u043D\u0430\u0431\u043E\u0440 \u043F\u0430\u0442\u0447\u0435\u0439 \u0434\u043B\u044F \u044D\u0442\u043E\u0433\u043E \u0431\u0438\u043B\u0435\u0442\u0430.
+gb.proposeInstructions = \u0414\u043B\u044F \u043D\u0430\u0447\u0430\u043B\u0430 \u0441\u043E\u0437\u0434\u0430\u0439\u0442\u0435 \u043D\u0430\u0431\u043E\u0440 \u043F\u0430\u0442\u0447\u0435\u0439 \u0438 \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u0435 \u0435\u0433\u043E \u0441 \u043F\u043E\u043C\u043E\u0449\u044C\u044E Git. Gitblit \u0441\u0432\u044F\u0436\u0435\u0442 \u0432\u0430\u0448 \u043D\u0430\u0431\u043E\u0440 \u043F\u0430\u0442\u0447\u0435\u0439 \u0441 \u044D\u0442\u0438\u043C \u0442\u0438\u043A\u0435\u0442\u043E\u043C \u043F\u043E id.
+gb.proposeWith = \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0438\u0442\u044C \u043D\u0430\u0431\u043E\u0440 \u043F\u0430\u0442\u0447\u0435\u0439 \u0441 {0}
+gb.revisionHistory = \u0438\u0441\u0442\u043E\u0440\u0438\u044F \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u0439
+gb.merge = \u043E\u0431\u044A\u0435\u0434\u0438\u043D\u0438\u0442\u044C
+gb.action = \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0435
+gb.patchset = \u043D\u0430\u0431\u043E\u0440 \u043F\u0430\u0442\u0447\u0435\u0439
+gb.all = \u0432\u0441\u0435
+gb.mergeBase = \u043E\u0431\u044A\u0435\u0434\u0438\u043D\u0438\u0442\u044C \u0431\u0430\u0437\u0443
+gb.checkout = checkout
+gb.checkoutViaCommandLine = \u041E\u0444\u043E\u0440\u043C\u0438\u0442\u044C \u0437\u0430\u043A\u0430\u0437 \u0447\u0435\u0440\u0435\u0437 \u043A\u043E\u043C\u0430\u043D\u0434\u043D\u0443\u044E \u0441\u0442\u0440\u043E\u043A\u0443
+gb.checkoutViaCommandLineNote = \u0412\u044B \u043C\u043E\u0436\u0435\u0442\u0435 \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C \u0438 \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C \u044D\u0442\u0438 \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u044F \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u043E \u0438\u0437 \u0441\u0432\u043E\u0435\u0433\u043E \u043A\u043B\u043E\u043D\u0430 \u044D\u0442\u043E\u0433\u043E \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F.
+gb.checkoutStep1 = \u0412\u044B\u0431\u0440\u0430\u0442\u044C \u0442\u0435\u043A\u0443\u0449\u0438\u0439 \u043D\u0430\u0431\u043E\u0440 \u043F\u0430\u0442\u0447\u0435\u0439 \u2014 \u0438 \u0437\u0430\u043F\u0443\u0441\u0442\u0438\u0442\u044C \u0435\u0433\u043E \u0438\u0437 \u043A\u0430\u0442\u0430\u043B\u043E\u0433\u0430 \u0432\u0430\u0448\u0435\u0433\u043E \u043F\u0440\u043E\u0435\u043A\u0442\u0430
+gb.checkoutStep2 = \u0418\u0437\u0432\u043B\u0435\u0447\u044C \u043D\u0430\u0431\u043E\u0440 \u043F\u0430\u0442\u0447\u0435\u0439 \u0432 \u043D\u043E\u0432\u0443\u044E \u0432\u0435\u0442\u043A\u0443 \u0438 \u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440\u0435\u0442\u044C
+gb.mergingViaCommandLine = \u0421\u043B\u0438\u044F\u043D\u0438\u0435 \u0447\u0435\u0440\u0435\u0437 \u043A\u043E\u043C\u0430\u043D\u0434\u043D\u0443\u044E \u0441\u0442\u0440\u043E\u043A\u0443
+gb.mergingViaCommandLineNote = \u0415\u0441\u043B\u0438 \u0432\u044B \u043D\u0435 \u0445\u043E\u0442\u0438\u0442\u0435 \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u044C \u043A\u043D\u043E\u043F\u043A\u0443 \u0441\u043B\u0438\u044F\u043D\u0438\u044F \u0438\u043B\u0438 \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u043E\u0435 \u0441\u043B\u0438\u044F\u043D\u0438\u0435 \u043D\u0435 \u043C\u043E\u0436\u0435\u0442 \u0431\u044B\u0442\u044C \u0432\u044B\u043F\u043E\u043B\u043D\u0435\u043D\u043E, \u0432\u044B \u043C\u043E\u0436\u0435\u0442\u0435 \u0432\u044B\u043F\u043E\u043B\u043D\u0438\u0442\u044C \u0441\u043B\u0438\u044F\u043D\u0438\u0435 \u0432\u0440\u0443\u0447\u043D\u0443\u044E \u0432 \u043A\u043E\u043C\u0430\u043D\u0434\u043D\u043E\u0439 \u0441\u0442\u0440\u043E\u043A\u0435.
+gb.mergeStep1 = \u0418\u0437\u0432\u043B\u0435\u043A\u0438\u0442\u0435 \u043D\u043E\u0432\u0443\u044E \u0432\u0435\u0442\u043A\u0443, \u0447\u0442\u043E\u0431\u044B \u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440\u0435\u0442\u044C \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u044F \u2014, \u0437\u0430\u043F\u0443\u0441\u0442\u0438\u0442\u0435 \u0435\u0435 \u0438\u0437 \u043A\u0430\u0442\u0430\u043B\u043E\u0433\u0430 \u0432\u0430\u0448\u0435\u0433\u043E \u043F\u0440\u043E\u0435\u043A\u0442\u0430
+gb.mergeStep2 = \u0412\u043D\u0435\u0441\u0438\u0442\u0435 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u043D\u044B\u0435 \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u044F \u0438 \u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440\u0438\u0442\u0435
+gb.mergeStep3 = \u041E\u0431\u044A\u0435\u0434\u0438\u043D\u0438\u0442\u044C \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u043D\u044B\u0435 \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u044F \u0438 \u043E\u0431\u043D\u043E\u0432\u0438\u0442\u044C \u0441\u0435\u0440\u0432\u0435\u0440
+gb.download = \u0441\u043A\u0430\u0447\u0430\u0442\u044C
+gb.ptDescription = \u0438\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442 Gatblit patchset
+gb.ptCheckout = \u0412\u044B\u0431\u0440\u0430\u0442\u044C \u0438 \u043E\u0444\u043E\u0440\u043C\u0438\u0442\u044C \u0442\u0435\u043A\u0443\u0449\u0438\u0439 \u043D\u0430\u0431\u043E\u0440 \u043F\u0430\u0442\u0447\u0435\u0439 \u0432 \u0432\u0435\u0442\u043A\u0435 \u043E\u0431\u0437\u043E\u0440\u0430
+gb.ptMerge = \u0412\u044B\u0431\u0440\u0430\u0442\u044C \u0438 \u043E\u0431\u044A\u0435\u0434\u0438\u043D\u0438\u0442\u044C \u0442\u0435\u043A\u0443\u0449\u0438\u0439 \u043D\u0430\u0431\u043E\u0440 \u043F\u0430\u0442\u0447\u0435\u0439 \u0441 \u0432\u0430\u0448\u0435\u0439 \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u043E\u0439 \u0432\u0435\u0442\u043A\u043E\u0439
+gb.ptDescription1 = Barnum - \u044D\u0442\u043E \u043A\u043E\u043C\u043F\u0430\u043D\u044C\u043E\u043D \u043A\u043E\u043C\u0430\u043D\u0434\u043D\u043E\u0439 \u0441\u0442\u0440\u043E\u043A\u0438 \u0434\u043B\u044F Git, \u043A\u043E\u0442\u043E\u0440\u044B\u0439 \u0443\u043F\u0440\u043E\u0449\u0430\u0435\u0442 \u0441\u0438\u043D\u0442\u0430\u043A\u0441\u0438\u0441 \u0434\u043B\u044F \u0440\u0430\u0431\u043E\u0442\u044B \u0441 \u0431\u0438\u043B\u0435\u0442\u0430\u043C\u0438 \u0438 \u043D\u0430\u0431\u043E\u0440\u0430\u043C\u0438 \u043F\u0430\u0442\u0447\u0435\u0439 Gitblit.
+gb.ptSimplifiedCollaboration = \u0443\u043F\u0440\u043E\u0449\u0435\u043D\u043D\u044B\u0439 \u0441\u0438\u043D\u0442\u0430\u043A\u0441\u0438\u0441 \u0441\u043E\u0432\u043C\u0435\u0441\u0442\u043D\u043E\u0439 \u0440\u0430\u0431\u043E\u0442\u044B
+gb.ptSimplifiedMerge = \u0443\u043F\u0440\u043E\u0449\u0435\u043D\u043D\u044B\u0439 \u0441\u0438\u043D\u0442\u0430\u043A\u0441\u0438\u0441 \u0441\u043B\u0438\u044F\u043D\u0438\u044F
+gb.ptDescription2 = Barnum \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044F Python 3 \u0438 \u0441\u043E\u0431\u0441\u0442\u0432\u0435\u043D\u043D\u044B\u0439 Git. \u041E\u043D \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442 \u043D\u0430 Windows, Linux \u0438 Mac OS X.
+gb.stepN = \u0428\u0430\u0433 {0}
+gb.watchers = \u043D\u0430\u0431\u043B\u044E\u0434\u0430\u0442\u0435\u043B\u0438
+gb.votes = \u0433\u043E\u043B\u043E\u0441\u043E\u0432
+gb.vote = \u0433\u043E\u043B\u043E\u0441\u043E\u0432\u0430\u0442\u044C \u0437\u0430 \u044D\u0442\u043E {0}
+gb.watch = \u043D\u0430\u0431\u043B\u044E\u0434\u0430\u0442\u044C \u044D\u0442\u043E {0}
+gb.removeVote = \u0443\u0434\u0430\u043B\u0438\u0442\u044C \u0433\u043E\u043B\u043E\u0441
+gb.stopWatching = \u043F\u0435\u0440\u0435\u0441\u0442\u0430\u0442\u044C \u043D\u0430\u0431\u043B\u044E\u0434\u0430\u0442\u044C
+gb.watching = \u043D\u0430\u0431\u043B\u044E\u0434\u0430\u0442\u044C
+gb.comments = \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438
+gb.addComment = \u0434\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0439
+gb.export = \u044D\u043A\u0441\u043F\u043E\u0440\u0442
+gb.oneCommit = \u043E\u0434\u0438\u043D \u043A\u043E\u043C\u043C\u0438\u0442
+gb.nCommits = {0} \u0444\u0438\u043A\u0441\u0438\u0440\u0443\u0435\u0442
+gb.addedOneCommit = \u0434\u043E\u0431\u0430\u0432\u043B\u0435\u043D\u043E 1 \u043A\u043E\u043C\u043C\u0438\u0442
+gb.addedNCommits = \u0434\u043E\u0431\u0430\u0432\u043B\u0435\u043D\u043D\u044B\u0435 {0} \u043A\u043E\u043C\u043C\u0438\u0442\u044B
+gb.commitsInPatchsetN = \u0444\u0438\u043A\u0441\u0438\u0440\u0443\u0435\u0442 \u0432 \u043D\u0430\u0431\u043E\u0440\u0435 \u043F\u0430\u0442\u0447\u0435\u0439 {0}
+gb.patchsetN = \u043D\u0430\u0431\u043E\u0440 \u043F\u0430\u0442\u0447\u0435\u0439 {0}
+gb.reviewedPatchsetRev = \u043F\u0440\u043E\u0432\u0435\u0440\u0435\u043D\u043D\u044B\u0439 \u043D\u0430\u0431\u043E\u0440 \u0438\u0441\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0438\u0439 {0}, \u0440\u0435\u0434\u0430\u043A\u0446\u0438\u044F {1}: {2}
+gb.review = \u043E\u0431\u0437\u043E\u0440
+gb.reviews = \u043E\u0442\u0437\u044B\u0432\u044B
+gb.veto = \u0432\u0435\u0442\u043E
+gb.needsImprovement = \u043D\u0443\u0436\u0434\u0430\u0435\u0442\u0441\u044F \u0432 \u0443\u043B\u0443\u0447\u0448\u0435\u043D\u0438\u0438
+gb.looksGood = \u0432\u044B\u0433\u043B\u044F\u0434\u0438\u0442 \u0445\u043E\u0440\u043E\u0448\u043E
+gb.approve = \u043E\u0434\u043E\u0431\u0440\u0438\u0442\u044C
+gb.hasNotReviewed = \u0435\u0449\u0435 \u043D\u0435 \u0440\u0435\u0446\u0435\u043D\u0437\u0438\u0440\u043E\u0432\u0430\u043B
+gb.about = \u043E
+gb.ticketN = \u0437\u0430\u044F\u0432\u043A\u0430 # {0}
+gb.disableUser = \u043E\u0442\u043A\u043B\u044E\u0447\u0438\u0442\u044C \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F
+gb.disableUserDescription = \u0437\u0430\u043F\u0440\u0435\u0442\u0438\u0442\u044C \u0430\u0443\u0442\u0435\u043D\u0442\u0438\u0444\u0438\u043A\u0430\u0446\u0438\u044E \u044D\u0442\u043E\u0439 \u0443\u0447\u0435\u0442\u043D\u043E\u0439 \u0437\u0430\u043F\u0438\u0441\u0438
+gb.any = \u043B\u044E\u0431\u043E\u0439
+gb.milestoneProgress = {0} \u043E\u0442\u043A\u0440\u044B\u0442\u043E, {1} \u0437\u0430\u043A\u0440\u044B\u0442\u043E
+gb.nOpenTickets = {0} \u043E\u0442\u043A\u0440\u044B\u0442
+gb.nClosedTickets = {0} \u0437\u0430\u043A\u0440\u044B\u0442
+gb.nTotalTickets = {0} \u0432\u0441\u0435\u0433\u043E
+gb.body = \u0442\u0435\u043B\u043E
+gb.mergeSha = mergeSha
+gb.mergeTo = \u043E\u0431\u044A\u0435\u0434\u0438\u043D\u0438\u0442\u044C \u0441
+gb.mergeType = \u0442\u0438\u043F \u0441\u043B\u0438\u044F\u043D\u0438\u044F
+gb.labels = \u044F\u0440\u043B\u044B\u043A\u0438
+gb.reviewers = \u0440\u0435\u0446\u0435\u043D\u0437\u0435\u043D\u0442\u044B
+gb.voters = \u0438\u0437\u0431\u0438\u0440\u0430\u0442\u0435\u043B\u0435\u0439
+gb.mentions = \u0443\u043F\u043E\u043C\u0438\u043D\u0430\u043D\u0438\u044F
+gb.canNotProposePatchset = \u043D\u0435 \u043C\u043E\u0436\u0435\u0442 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0438\u0442\u044C \u043D\u0430\u0431\u043E\u0440 \u043F\u0430\u0442\u0447\u0435\u0439
+gb.repositoryIsMirror = \u042D\u0442\u043E\u0442 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439 \u044F\u0432\u043B\u044F\u0435\u0442\u0441\u044F \u0437\u0435\u0440\u043A\u0430\u043B\u043E\u043C \u0442\u043E\u043B\u044C\u043A\u043E \u0434\u043B\u044F \u0447\u0442\u0435\u043D\u0438\u044F.
+gb.repositoryIsFrozen = \u042D\u0442\u043E\u0442 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439 \u0437\u0430\u043C\u043E\u0440\u043E\u0436\u0435\u043D.
+gb.repositoryDoesNotAcceptPatchsets = \u042D\u0442\u043E\u0442 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439 \u043D\u0435 \u043F\u0440\u0438\u043D\u0438\u043C\u0430\u0435\u0442 \u043D\u0430\u0431\u043E\u0440\u044B \u043F\u0430\u0442\u0447\u0435\u0439.
+gb.serverDoesNotAcceptPatchsets = \u042D\u0442\u043E\u0442 \u0441\u0435\u0440\u0432\u0435\u0440 \u043D\u0435 \u043F\u0440\u0438\u043D\u0438\u043C\u0430\u0435\u0442 \u043D\u0430\u0431\u043E\u0440\u044B \u0438\u0441\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0438\u0439.
+gb.ticketIsClosed = \u042D\u0442\u0430 \u0437\u0430\u044F\u0432\u043A\u0430 \u0437\u0430\u043A\u0440\u044B\u0442\u0430.
+gb.mergeToDescription = \u0432\u0435\u0442\u0432\u044C \u0438\u043D\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E \u0434\u043B\u044F \u043E\u0431\u044A\u0435\u0434\u0438\u043D\u0435\u043D\u0438\u044F \u043D\u0430\u0431\u043E\u0440\u043E\u0432 \u043F\u0430\u0442\u0447\u0435\u0439
+gb.mergeTypeDescription = \u043E\u0431\u044A\u0435\u0434\u0438\u043D\u0438\u0442\u044C \u0437\u0430\u044F\u0432\u043A\u0443 \u0442\u043E\u043B\u044C\u043A\u043E \u0434\u043B\u044F \u0431\u044B\u0441\u0442\u0440\u043E\u0439 \u043F\u0435\u0440\u0435\u0441\u044B\u043B\u043A\u0438, \u0435\u0441\u043B\u0438 \u043D\u0435\u043E\u0431\u0445\u043E\u0434\u0438\u043C\u043E, \u0438\u043B\u0438 \u0432\u0441\u0435\u0433\u0434\u0430 \u0441 \u0444\u0438\u043A\u0441\u0430\u0446\u0438\u0435\u0439 \u0441\u043B\u0438\u044F\u043D\u0438\u044F \u0432 \u0432\u0435\u0442\u043A\u0435 \u0438\u043D\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438
+gb.anonymousCanNotPropose = \u0410\u043D\u043E\u043D\u0438\u043C\u043D\u044B\u0435 \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u0438 \u043D\u0435 \u043C\u043E\u0433\u0443\u0442 \u043F\u0440\u0435\u0434\u043B\u0430\u0433\u0430\u0442\u044C \u043D\u0430\u0431\u043E\u0440\u044B \u043F\u0430\u0442\u0447\u0435\u0439.
+gb.youDoNotHaveClonePermission = \u0412\u0430\u043C \u043D\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043D\u043E \u043A\u043B\u043E\u043D\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u044D\u0442\u043E\u0442 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439.
+gb.myTickets = \u043C\u043E\u0438 \u0437\u0430\u044F\u0432\u043A\u0438
+gb.yourAssignedTickets = \u043D\u0430\u0437\u043D\u0430\u0447\u0435\u043D \u0432\u0430\u043C
+gb.newMilestone = \u043D\u043E\u0432\u044B\u0439 \u044D\u0442\u0430\u043F
+gb.editMilestone = \u0440\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0432\u0435\u0445\u0443
+gb.deleteMilestone = \u0423\u0434\u0430\u043B\u0438\u0442\u044C \u044D\u0442\u0430\u043F \"{0}\"?
+gb.milestoneDeleteFailed = \u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0443\u0434\u0430\u043B\u0438\u0442\u044C \u0432\u0435\u0445\u0443 ''{0}''!
+gb.notifyChangedOpenTickets = \u043E\u0442\u043F\u0440\u0430\u0432\u0438\u0442\u044C \u0443\u0432\u0435\u0434\u043E\u043C\u043B\u0435\u043D\u0438\u0435 \u043E\u0431 \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u0438 \u043E\u0442\u043A\u0440\u044B\u0442\u044B\u0445 \u0437\u0430\u044F\u0432\u043E\u043A
+gb.overdue = \u043F\u0440\u043E\u0441\u0440\u043E\u0447\u0435\u043D\u043D\u044B\u0439
+gb.openMilestones = \u043E\u0442\u043A\u0440\u044B\u0442\u044B\u0435 \u0432\u0435\u0445\u0438
+gb.closedMilestones = \u0437\u0430\u043A\u0440\u044B\u0442\u044B\u0435 \u0432\u0435\u0445\u0438
+gb.administration = \u0430\u0434\u043C\u0438\u043D\u0438\u0441\u0442\u0440\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u0435
+gb.plugins = \u043F\u043B\u0430\u0433\u0438\u043D\u044B
+gb.extensions = \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u044F
+gb.pleaseSelectProject = \u041F\u043E\u0436\u0430\u043B\u0443\u0439\u0441\u0442\u0430, \u0432\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u043F\u0440\u043E\u0435\u043A\u0442!
+gb.accessPolicy = \u041F\u043E\u043B\u0438\u0442\u0438\u043A\u0430 \u0434\u043E\u0441\u0442\u0443\u043F\u0430
+gb.accessPolicyDescription = \u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u043F\u043E\u043B\u0438\u0442\u0438\u043A\u0443 \u0434\u043E\u0441\u0442\u0443\u043F\u0430 \u0434\u043B\u044F \u0443\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0438\u044F \u0432\u0438\u0434\u0438\u043C\u043E\u0441\u0442\u044C\u044E \u0445\u0440\u0430\u043D\u0438\u043B\u0438\u0449\u0430 \u0438 \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043D\u0438\u044F\u043C\u0438 git.
+gb.anonymousPolicy = \u0410\u043D\u043E\u043D\u0438\u043C \u043C\u043E\u0436\u0435\u0442 View, Clone, & Push
+gb.anonymousPolicyDescription = \u041B\u044E\u0431\u043E\u0439 \u043C\u043E\u0436\u0435\u0442 \u0443\u0432\u0438\u0434\u0435\u0442\u044C, \u043A\u043B\u043E\u043D\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0438 \u043E\u0442\u043F\u0440\u0430\u0432\u0438\u0442\u044C \u0432 \u044D\u0442\u043E\u0442 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439.
+gb.authenticatedPushPolicy = \u041E\u0433\u0440\u0430\u043D\u0438\u0447\u0438\u0442\u044C Push (\u0430\u0432\u0442\u043E\u0440\u0438\u0437\u043E\u0432\u0430\u043D\u043D\u044B\u043C)
+gb.authenticatedPushPolicyDescription = \u041B\u044E\u0431\u043E\u0439 \u043C\u043E\u0436\u0435\u0442 \u0443\u0432\u0438\u0434\u0435\u0442\u044C \u0438 \u043A\u043B\u043E\u043D\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u044D\u0442\u043E\u0442 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439. \u0412\u0441\u0435 \u043F\u0440\u043E\u0448\u0435\u0434\u0448\u0438\u0435 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0443 \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u0438 \u0438\u043C\u0435\u044E\u0442 \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043D\u0438\u0435 RW + \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u044F\u0442\u044C.
+gb.namedPushPolicy = \u041E\u0433\u0440\u0430\u043D\u0438\u0447\u0438\u0442\u044C Push (\u043F\u043E \u0438\u043C\u0435\u043D\u0438)
+gb.namedPushPolicyDescription = \u041B\u044E\u0431\u043E\u0439 \u043C\u043E\u0436\u0435\u0442 \u0443\u0432\u0438\u0434\u0435\u0442\u044C \u0438 \u043A\u043B\u043E\u043D\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u044D\u0442\u043E\u0442 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439. \u0412\u044B \u0432\u044B\u0431\u0438\u0440\u0430\u0435\u0442\u0435, \u043A\u0442\u043E \u043C\u043E\u0436\u0435\u0442 \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u044F\u0442\u044C.
+gb.clonePolicy = \u041E\u0433\u0440\u0430\u043D\u0438\u0447\u0438\u0442\u044C clone \u0438 push
+gb.clonePolicyDescription = \u042D\u0442\u043E\u0442 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439 \u043C\u043E\u0436\u0435\u0442 \u0443\u0432\u0438\u0434\u0435\u0442\u044C \u043B\u044E\u0431\u043E\u0439. \u0412\u044B \u0432\u044B\u0431\u0438\u0440\u0430\u0435\u0442\u0435, \u043A\u0442\u043E \u043C\u043E\u0436\u0435\u0442 \u043A\u043B\u043E\u043D\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0438 \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u044F\u0442\u044C.
+gb.viewPolicy = \u041E\u0433\u0440\u0430\u043D\u0438\u0447\u0438\u0442\u044C \u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440, clone \u0438 Push
+gb.viewPolicyDescription = \u0412\u044B \u0432\u044B\u0431\u0438\u0440\u0430\u0435\u0442\u0435, \u043A\u0442\u043E \u043C\u043E\u0436\u0435\u0442 \u0432\u0438\u0434\u0435\u0442\u044C, \u043A\u043B\u043E\u043D\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0438 \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u044F\u0442\u044C \u0432 \u044D\u0442\u043E\u0442 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439.
+gb.initialCommit = \u041D\u0430\u0447\u0430\u043B\u044C\u043D\u0430\u044F \u0444\u0438\u043A\u0441\u0430\u0446\u0438\u044F
+gb.initialCommitDescription = \u042D\u0442\u043E \u043F\u043E\u0437\u0432\u043E\u043B\u0438\u0442 \u0432\u0430\u043C \u043D\u0435\u043C\u0435\u0434\u043B\u0435\u043D\u043D\u043E <code>git clone</code> \u044D\u0442\u043E \u0445\u0440\u0430\u043D\u0438\u043B\u0438\u0449\u0435. \u041F\u0440\u043E\u043F\u0443\u0441\u0442\u0438\u0442\u0435 \u044D\u0442\u043E\u0442 \u0448\u0430\u0433, \u0435\u0441\u043B\u0438 \u0432\u044B \u0443\u0436\u0435 \u0437\u0430\u043F\u0443\u0441\u0442\u0438\u043B\u0438 <code>git init</code> \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u043E.
+gb.initWithReadme = \u0412\u043A\u043B\u044E\u0447\u0438\u0442\u044C README
+gb.initWithReadmeDescription = \u042D\u0442\u043E \u0441\u043E\u0437\u0434\u0430\u0441\u0442 \u043F\u0440\u043E\u0441\u0442\u043E\u0439 \u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442 README \u0434\u043B\u044F \u0432\u0430\u0448\u0435\u0433\u043E \u0445\u0440\u0430\u043D\u0438\u043B\u0438\u0449\u0430.
+gb.initWithGitignore = \u0412\u043A\u043B\u044E\u0447\u0438\u0442\u044C \u0444\u0430\u0439\u043B .gitignore
+gb.initWithGitignoreDescription = \u042D\u0442\u043E \u0432\u0441\u0442\u0430\u0432\u0438\u0442 \u0444\u0430\u0439\u043B \u043A\u043E\u043D\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438, \u043A\u043E\u0442\u043E\u0440\u044B\u0439 \u0438\u043D\u0441\u0442\u0440\u0443\u043A\u0442\u0438\u0440\u0443\u0435\u0442 \u0432\u0430\u0448\u0438\u0445 \u043A\u043B\u0438\u0435\u043D\u0442\u043E\u0432 Git \u0438\u0433\u043D\u043E\u0440\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0444\u0430\u0439\u043B\u044B \u0438\u043B\u0438 \u043A\u0430\u0442\u0430\u043B\u043E\u0433\u0438, \u043A\u043E\u0442\u043E\u0440\u044B\u0435 \u0441\u043E\u043E\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044E\u0442 \u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0435\u043D\u043D\u044B\u043C \u0448\u0430\u0431\u043B\u043E\u043D\u0430\u043C.
+gb.pleaseSelectGitIgnore = \u041F\u043E\u0436\u0430\u043B\u0443\u0439\u0441\u0442\u0430, \u0432\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u0444\u0430\u0439\u043B .gitignore
+gb.receive = \u043F\u043E\u043B\u0443\u0447\u0435\u043D\u0438\u0435
+gb.permissions = \u043F\u043E\u043B\u043D\u043E\u043C\u043E\u0447\u0438\u044F
+gb.ownersDescription = \u0412\u043B\u0430\u0434\u0435\u043B\u044C\u0446\u044B \u043C\u043E\u0433\u0443\u0442 \u0443\u043F\u0440\u0430\u0432\u043B\u044F\u0442\u044C \u0432\u0441\u0435\u043C\u0438 \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0430\u043C\u0438 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F, \u043D\u043E \u0438\u043C \u043D\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0430\u0435\u0442\u0441\u044F \u043F\u0435\u0440\u0435\u0438\u043C\u0435\u043D\u043E\u0432\u044B\u0432\u0430\u0442\u044C \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439, \u0435\u0441\u043B\u0438 \u044D\u0442\u043E \u043D\u0435 \u0438\u0445 \u043B\u0438\u0447\u043D\u044B\u0439 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439.
+gb.userPermissionsDescription = \u0412\u044B \u043C\u043E\u0436\u0435\u0442\u0435 \u0443\u043A\u0430\u0437\u0430\u0442\u044C \u0438\u043D\u0434\u0438\u0432\u0438\u0434\u0443\u0430\u043B\u044C\u043D\u044B\u0435 \u043F\u0440\u0430\u0432\u0430 \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044F. \u042D\u0442\u0438 \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438 \u043F\u0435\u0440\u0435\u043E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u044E\u0442 \u043F\u0440\u0430\u0432\u0430 \u0434\u043E\u0441\u0442\u0443\u043F\u0430 \u043A\u043E\u043C\u0430\u043D\u0434\u044B \u0438\u043B\u0438 \u0440\u0435\u0433\u0443\u043B\u044F\u0440\u043D\u043E\u0433\u043E \u0432\u044B\u0440\u0430\u0436\u0435\u043D\u0438\u044F.
+gb.teamPermissionsDescription = \u0412\u044B \u043C\u043E\u0436\u0435\u0442\u0435 \u0443\u043A\u0430\u0437\u0430\u0442\u044C \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043D\u0438\u044F \u0434\u043B\u044F \u043E\u0442\u0434\u0435\u043B\u044C\u043D\u043E\u0439 \u043A\u043E\u043C\u0430\u043D\u0434\u044B. \u042D\u0442\u0438 \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438 \u043F\u0435\u0440\u0435\u043E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u044E\u0442 \u043F\u0440\u0430\u0432\u0430 \u043D\u0430 \u0440\u0435\u0433\u0443\u043B\u044F\u0440\u043D\u044B\u0435 \u0432\u044B\u0440\u0430\u0436\u0435\u043D\u0438\u044F.
+gb.ticketSettings = \u041D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438 \u0437\u0430\u044F\u0432\u043E\u043A
+gb.receiveSettings = \u041D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438 \u043F\u043E\u043B\u0443\u0447\u0435\u043D\u0438\u044F
+gb.receiveSettingsDescription = \u042D\u043B\u0435\u043C\u0435\u043D\u0442 \u0443\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0438\u044F \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u0430\u043C\u0438 \u043F\u0440\u0438\u0435\u043C\u0430 \u043F\u0435\u0440\u0435\u0434\u0430\u0435\u0442\u0441\u044F \u0432 \u0445\u0440\u0430\u043D\u0438\u043B\u0438\u0449\u0435.
+gb.preReceiveDescription = \u041F\u0435\u0440\u0435\u0445\u0432\u0430\u0442\u044B \u043F\u0440\u0435\u0434\u0432\u0430\u0440\u0438\u0442\u0435\u043B\u044C\u043D\u043E\u0433\u043E \u043F\u043E\u043B\u0443\u0447\u0435\u043D\u0438\u044F \u0432\u044B\u043F\u043E\u043B\u043D\u044F\u044E\u0442\u0441\u044F \u043F\u043E\u0441\u043B\u0435 \u043F\u043E\u043B\u0443\u0447\u0435\u043D\u0438\u044F \u043A\u043E\u043C\u043C\u0438\u0442\u043E\u0432, \u043D\u043E <em>\u041F\u0415\u0420\u0415\u0414</em> \u0441\u0441\u044B\u043B\u043A\u0438 \u043E\u0431\u043D\u043E\u0432\u043B\u044F\u044E\u0442\u0441\u044F.<p> \u042D\u0442\u043E \u043F\u043E\u0434\u0445\u043E\u0434\u044F\u0449\u0438\u0439 \u043F\u0435\u0440\u0435\u0445\u0432\u0430\u0442 \u0434\u043B\u044F \u043E\u0442\u043A\u043B\u043E\u043D\u0435\u043D\u0438\u044F push.</p>
+gb.postReceiveDescription = \u041F\u0435\u0440\u0435\u0445\u0432\u0430\u0442\u044B \u043F\u043E\u0441\u043B\u0435 \u043F\u043E\u043B\u0443\u0447\u0435\u043D\u0438\u044F \u0432\u044B\u043F\u043E\u043B\u043D\u044F\u044E\u0442\u0441\u044F \u043F\u043E\u0441\u043B\u0435 \u043F\u043E\u043B\u0443\u0447\u0435\u043D\u0438\u044F \u043A\u043E\u043C\u043C\u0438\u0442\u043E\u0432, \u043D\u043E <em>\u041F\u041E\u0421\u041B\u0415</em> \u0441\u0441\u044B\u043B\u043A\u0438 \u043E\u0431\u043D\u043E\u0432\u043B\u044F\u044E\u0442\u0441\u044F. <p>\u042D\u0442\u043E \u043F\u043E\u0434\u0445\u043E\u0434\u044F\u0449\u0438\u0439 \u043F\u0435\u0440\u0435\u0445\u0432\u0430\u0442 \u0434\u043B\u044F \u0443\u0432\u0435\u0434\u043E\u043C\u043B\u0435\u043D\u0438\u0439, \u0442\u0440\u0438\u0433\u0433\u0435\u0440\u043E\u0432 \u0441\u0431\u043E\u0440\u043A\u0438 \u0438 \u0442.\u0434.</p>
+gb.federationStrategyDescription = \u041A\u043E\u043D\u0442\u0440\u043E\u043B\u044C, \u0435\u0441\u043B\u0438 \u0438 \u043A\u0430\u043A \u043E\u0431\u044A\u0435\u0434\u0438\u043D\u0438\u0442\u044C \u044D\u0442\u043E\u0442 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439 \u0441 \u0434\u0440\u0443\u0433\u0438\u043C Gitblit.
+gb.federationSetsDescription = \u042D\u0442\u043E\u0442 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439 \u0431\u0443\u0434\u0435\u0442 \u0432\u043A\u043B\u044E\u0447\u0435\u043D \u0432 \u0432\u044B\u0431\u0440\u0430\u043D\u043D\u044B\u0435 \u043D\u0430\u0431\u043E\u0440\u044B \u043E\u0431\u044C\u0435\u0434\u0438\u043D\u0435\u043D\u0438\u0439.
+gb.miscellaneous = \u0440\u0430\u0437\u043D\u043E\u0435
+gb.originDescription = URL, \u0441 \u043A\u043E\u0442\u043E\u0440\u043E\u0433\u043E \u0431\u044B\u043B \u043A\u043B\u043E\u043D\u0438\u0440\u043E\u0432\u0430\u043D \u044D\u0442\u043E\u0442 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439.
+gb.gc = GC
+gb.garbageCollection = \u0421\u0431\u043E\u0440\u043A\u0430 \u043C\u0443\u0441\u043E\u0440\u0430
+gb.garbageCollectionDescription = \u0421\u0431\u043E\u0440\u0449\u0438\u043A \u043C\u0443\u0441\u043E\u0440\u0430 \u0443\u043F\u0430\u043A\u0443\u0435\u0442 \u043D\u0435\u0437\u0430\u043A\u0440\u0435\u043F\u043B\u0435\u043D\u043D\u044B\u0435 \u043E\u0431\u044A\u0435\u043A\u0442\u044B, \u0438\u0437\u0432\u043B\u0435\u0447\u0435\u043D\u043D\u044B\u0435 \u0438\u0437 \u043A\u043B\u0438\u0435\u043D\u0442\u043E\u0432, \u0438 \u0443\u0434\u0430\u043B\u0438\u0442 \u043D\u0435\u0441\u0432\u044F\u0437\u0430\u043D\u043D\u044B\u0435 \u043E\u0431\u044A\u0435\u043A\u0442\u044B \u0438\u0437 \u0445\u0440\u0430\u043D\u0438\u043B\u0438\u0449\u0430.
+gb.commitMessageRendererDescription = \u0421\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u044F \u043A\u043E\u043C\u043C\u0438\u0442\u0430 \u043C\u043E\u0433\u0443\u0442 \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u0442\u044C\u0441\u044F \u0432 \u0432\u0438\u0434\u0435 \u043E\u0442\u043A\u0440\u044B\u0442\u043E\u0433\u043E \u0442\u0435\u043A\u0441\u0442\u0430 \u0438\u043B\u0438 \u0432 \u0432\u0438\u0434\u0435 \u0440\u0430\u0437\u043C\u0435\u0442\u043A\u0438.
+gb.preferences = \u043F\u0440\u0435\u0434\u043F\u043E\u0447\u0442\u0435\u043D\u0438\u044F
+gb.accountPreferences = \u041D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438 \u0430\u043A\u043A\u0430\u0443\u043D\u0442\u0430
+gb.accountPreferencesDescription = \u0423\u043A\u0430\u0436\u0438\u0442\u0435 \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438 \u0441\u0432\u043E\u0435\u0433\u043E \u0430\u043A\u043A\u0430\u0443\u043D\u0442\u0430
+gb.languagePreference = \u043F\u0440\u0435\u0434\u043F\u043E\u0447\u0442\u0435\u043D\u0438\u044F \u044F\u0437\u044B\u043A\u0430
+gb.languagePreferenceDescription = \u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u043F\u0440\u0435\u0434\u043F\u043E\u0447\u0438\u0442\u0430\u0435\u043C\u044B\u0439 \u043F\u0435\u0440\u0435\u0432\u043E\u0434 \u0434\u043B\u044F Gitblit
+gb.emailMeOnMyTicketChanges = \u041D\u0430\u043F\u0438\u0448\u0438\u0442\u0435 \u043C\u043D\u0435 \u043E\u0431 \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u044F\u0445 \u043C\u043E\u0435\u0433\u043E \u0431\u0438\u043B\u0435\u0442\u0430
+gb.emailMeOnMyTicketChangesDescription = \u041E\u0442\u043F\u0440\u0430\u0432\u044C\u0442\u0435 \u043C\u043D\u0435 \u0443\u0432\u0435\u0434\u043E\u043C\u043B\u0435\u043D\u0438\u0435 \u043F\u043E \u044D\u043B\u0435\u043A\u0442\u0440\u043E\u043D\u043D\u043E\u0439 \u043F\u043E\u0447\u0442\u0435 \u043E\u0431 \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u044F\u0445, \u043A\u043E\u0442\u043E\u0440\u044B\u0435 \u044F \u0432\u043D\u0435\u0441\u0443 \u0432 \u0437\u0430\u044F\u0432\u043A\u0443
+gb.displayNameDescription = \u041F\u0440\u0435\u0434\u043F\u043E\u0447\u0442\u0438\u0442\u0435\u043B\u044C\u043D\u043E\u0435 \u0438\u043C\u044F \u0434\u043B\u044F \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u044F
+gb.emailAddressDescription = \u041E\u0441\u043D\u043E\u0432\u043D\u043E\u0439 \u0430\u0434\u0440\u0435\u0441 \u044D\u043B\u0435\u043A\u0442\u0440\u043E\u043D\u043D\u043E\u0439 \u043F\u043E\u0447\u0442\u044B \u0434\u043B\u044F \u043F\u043E\u043B\u0443\u0447\u0435\u043D\u0438\u044F \u0443\u0432\u0435\u0434\u043E\u043C\u043B\u0435\u043D\u0438\u0439
+gb.sshKeys = SSH Keys
+gb.sshKeysDescription = SSH \u0430\u0443\u0442\u0435\u043D\u0442\u0438\u0444\u0438\u043A\u0430\u0446\u0438\u044F \u0441 \u043E\u0442\u043A\u0440\u044B\u0442\u044B\u043C \u043A\u043B\u044E\u0447\u043E\u043C \u044F\u0432\u043B\u044F\u0435\u0442\u0441\u044F \u0431\u0435\u0437\u043E\u043F\u0430\u0441\u043D\u043E\u0439 \u0430\u043B\u044C\u0442\u0435\u0440\u043D\u0430\u0442\u0438\u0432\u043E\u0439 \u0430\u0443\u0442\u0435\u043D\u0442\u0438\u0444\u0438\u043A\u0430\u0446\u0438\u0438 \u043F\u043E \u043F\u0430\u0440\u043E\u043B\u044E
+gb.addSshKey = \u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u043A\u043B\u044E\u0447 SSH
+gb.key = Key
+gb.comment = \u041A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0439
+gb.sshKeyCommentDescription = \u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043D\u0435\u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u044B\u0439 \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0439. \u0415\u0441\u043B\u0438 \u043F\u043E\u043B\u0435 \u043F\u0443\u0441\u0442\u043E\u0435, \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0439 \u0431\u0443\u0434\u0435\u0442 \u0438\u0437\u0432\u043B\u0435\u0447\u0435\u043D \u0438\u0437 \u043A\u043B\u044E\u0447\u0435\u0432\u044B\u0445 \u0434\u0430\u043D\u043D\u044B\u0445.
+gb.permission = \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043D\u0438\u0435
+gb.sshKeyPermissionDescription = \u0423\u043A\u0430\u0436\u0438\u0442\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043D\u0438\u0435 \u0434\u043E\u0441\u0442\u0443\u043F\u0430 \u0434\u043B\u044F \u043A\u043B\u044E\u0447\u0430 SSH
+gb.transportPreference = \u0422\u0440\u0430\u043D\u0441\u043F\u043E\u0440\u0442\u043D\u044B\u0435 \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438
+gb.transportPreferenceDescription = \u0423\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442\u044C \u0442\u0440\u0430\u043D\u0441\u043F\u043E\u0440\u0442, \u043A\u043E\u0442\u043E\u0440\u044B\u0439 \u0432\u044B \u043F\u0440\u0435\u0434\u043F\u043E\u0447\u0438\u0442\u0430\u0435\u0442\u0435 \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u044C \u0434\u043B\u044F \u043A\u043B\u043E\u043D\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u044F
+gb.priority = \u043F\u0440\u0438\u043E\u0440\u0438\u0442\u0435\u0442
+gb.severity = \u0441\u0435\u0440\u044C\u0435\u0437\u043D\u043E\u0441\u0442\u044C
+gb.sortHighestPriority = \u0441\u0430\u043C\u044B\u0439 \u0432\u044B\u0441\u043E\u043A\u0438\u0439 \u043F\u0440\u0438\u043E\u0440\u0438\u0442\u0435\u0442
+gb.sortLowestPriority = \u0441\u0430\u043C\u044B\u0439 \u043D\u0438\u0437\u043A\u0438\u0439 \u043F\u0440\u0438\u043E\u0440\u0438\u0442\u0435\u0442
+gb.sortHighestSeverity = \u043D\u0430\u0438\u0431\u043E\u043B\u044C\u0448\u0430\u044F \u0441\u0435\u0440\u044C\u0435\u0437\u043D\u043E\u0441\u0442\u044C
+gb.sortLowestSeverity = \u0441\u0430\u043C\u0430\u044F \u043D\u0438\u0437\u043A\u0430\u044F \u0441\u0435\u0440\u044C\u0435\u0437\u043D\u043E\u0441\u0442\u044C
+gb.missingIntegrationBranchMore = \u0426\u0435\u043B\u0435\u0432\u0430\u044F \u0432\u0435\u0442\u0432\u044C \u0438\u043D\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u043D\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442 \u0432 \u0445\u0440\u0430\u043D\u0438\u043B\u0438\u0449\u0435!
+gb.diffDeletedFileSkipped = (\u0443\u0434\u0430\u043B\u0435\u043D\u043E)
+gb.diffFileDiffTooLarge = Diff \u0441\u043B\u0438\u0448\u043A\u043E\u043C \u0431\u043E\u043B\u044C\u0448\u043E\u0439
+gb.diffNewFile = \u041D\u043E\u0432\u044B\u0439 \u0444\u0430\u0439\u043B
+gb.diffDeletedFile = \u0424\u0430\u0439\u043B \u0431\u044B\u043B \u0443\u0434\u0430\u043B\u0435\u043D
+gb.diffRenamedFile = \u0424\u0430\u0439\u043B \u0431\u044B\u043B \u043F\u0435\u0440\u0435\u0438\u043C\u0435\u043D\u043E\u0432\u0430\u043D \u0438\u0437 {0}
+gb.diffCopiedFile = \u0424\u0430\u0439\u043B \u0431\u044B\u043B \u0441\u043A\u043E\u043F\u0438\u0440\u043E\u0432\u0430\u043D \u0438\u0437 {0}
+gb.diffTruncated = Diff \u0443\u0441\u0435\u0447\u0435\u043D\u043D\u044B\u0439 \u043F\u043E\u0441\u043B\u0435 \u0432\u044B\u0448\u0435\u0443\u043A\u0430\u0437\u0430\u043D\u043D\u043E\u0433\u043E \u0444\u0430\u0439\u043B\u0430
+gb.opacityAdjust = \u041D\u0430\u0441\u0442\u0440\u043E\u0438\u0442\u044C \u043D\u0435\u043F\u0440\u043E\u0437\u0440\u0430\u0447\u043D\u043E\u0441\u0442\u044C
+gb.blinkComparator = Blink \u043A\u043E\u043C\u043F\u0430\u0440\u0430\u0442\u043E\u0440
+gb.imgdiffSubtract = Subtract (\u0447\u0435\u0440\u043D\u044B\u0439 = \u0438\u0434\u0435\u043D\u0442\u0438\u0447\u043D\u044B\u0439)
+gb.deleteRepositoryHeader = \u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0439
+gb.deleteRepositoryDescription = \u0423\u0434\u0430\u043B\u0435\u043D\u043D\u044B\u0435 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u0431\u0443\u0434\u0443\u0442 \u043D\u0435\u0432\u043E\u0441\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u043C\u044B\u043C\u0438.
+gb.show_whitespace = \u043F\u043E\u043A\u0430\u0437\u0430\u0442\u044C \u043F\u0440\u043E\u0431\u0435\u043B\u044B
+gb.ignore_whitespace = \u0438\u0433\u043D\u043E\u0440\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u043F\u0440\u043E\u0431\u0435\u043B\u044B
+gb.allRepositories = \u0412\u0441\u0435 \u0445\u0440\u0430\u043D\u0438\u043B\u0438\u0449\u0430
+gb.oid = \u0438\u0434\u0435\u043D\u0442\u0438\u0444\u0438\u043A\u0430\u0442\u043E\u0440 \u043E\u0431\u044A\u0435\u043A\u0442\u0430
+gb.filestore = \u0444\u0430\u0439\u043B\u044B
+gb.filestoreStats = \u0445\u0440\u0430\u043D\u0438\u043B\u0438\u0449\u0435 \u0444\u0430\u0439\u043B\u043E\u0432 \u0441\u043E\u0434\u0435\u0440\u0436\u0438\u0442 {0} \u0444\u0430\u0439\u043B\u043E\u0432 \u0441 \u043E\u0431\u0449\u0438\u043C \u0440\u0430\u0437\u043C\u0435\u0440\u043E\u043C {1}. ({2} \u043E\u0441\u0442\u0430\u043B\u043E\u0441\u044C)
+gb.statusChangedOn = \u0441\u0442\u0430\u0442\u0443\u0441 \u0438\u0437\u043C\u0435\u043D\u0435\u043D \u043D\u0430
+gb.statusChangedBy = \u0438\u0437\u043C\u0435\u043D\u0438\u043B \u0441\u0442\u0430\u0442\u0443\u0441
+gb.filestoreHelp = \u041A\u0430\u043A \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u044C \u0445\u0440\u0430\u043D\u0438\u043B\u0438\u0449\u0435 \u0444\u0430\u0439\u043B\u043E\u0432?
+gb.editFile = \u0440\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0444\u0430\u0439\u043B
+gb.continueEditing = \u041F\u0440\u043E\u0434\u043E\u043B\u0436\u0438\u0442\u044C \u0440\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u0435
+gb.commitChanges = \u041F\u043E\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044C \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u044F
+gb.fileNotMergeable = \u041D\u0435\u0432\u043E\u0437\u043C\u043E\u0436\u043D\u043E \u0437\u0430\u0444\u0438\u043A\u0441\u0438\u0440\u043E\u0432\u0430\u0442\u044C {0}. \u042D\u0442\u043E\u0442 \u0444\u0430\u0439\u043B \u043D\u0435 \u043C\u043E\u0436\u0435\u0442 \u0431\u044B\u0442\u044C \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438 \u043E\u0431\u044A\u0435\u0434\u0438\u043D\u0435\u043D.
+gb.fileCommitted = \u0423\u0441\u043F\u0435\u0448\u043D\u043E \u0437\u0430\u0444\u0438\u043A\u0441\u0438\u0440\u043E\u0432\u0430\u043D\u043E {0}.
+gb.deletePatchset = \u0423\u0434\u0430\u043B\u0438\u0442\u044C \u043D\u0430\u0431\u043E\u0440 \u043F\u0430\u0442\u0447\u0435\u0439 {0}
+gb.deletePatchsetSuccess = \u0423\u0434\u0430\u043B\u0435\u043D\u043D\u044B\u0439 \u043D\u0430\u0431\u043E\u0440 \u043F\u0430\u0442\u0447\u0435\u0439 {0}.
+gb.deletePatchsetFailure = \u041E\u0448\u0438\u0431\u043A\u0430 \u0443\u0434\u0430\u043B\u0435\u043D\u0438\u044F \u043D\u0430\u0431\u043E\u0440\u0430 \u043F\u0430\u0442\u0447\u0435\u0439 {0}.
+gb.referencedByCommit = \u0421\u0441\u044B\u043B\u043A\u0430 \u043D\u0430 \u043A\u043E\u043C\u043C\u0438\u0442.
+gb.referencedByTicket = \u0421\u0441\u044B\u043B\u043A\u0430 \u043D\u0430 \u0437\u0430\u044F\u0432\u043A\u0443.
+gb.emailClientCertificateSubject = \u0412\u0430\u0448 \u043A\u043B\u0438\u0435\u043D\u0442\u0441\u043A\u0438\u0439 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043A\u0430\u0442 Gitblit \u0434\u043B\u044F {0}
+
+# This last property for unit tests to test successful loading of the resource file
+gb.loadLang = \u0420\u0443\u0441\u0441\u043a\u0438\u0439
diff --git a/src/main/java/com/gitblit/wicket/GitBlitWebApp_zh_CN.properties b/src/main/java/com/gitblit/wicket/GitBlitWebApp_zh_CN.properties
index b5e4f658..67c103c8 100644
--- a/src/main/java/com/gitblit/wicket/GitBlitWebApp_zh_CN.properties
+++ b/src/main/java/com/gitblit/wicket/GitBlitWebApp_zh_CN.properties
@@ -1,24 +1,23 @@
gb.repository = \u7248\u672c\u5e93
-gb.owner = \u62e5\u6709\u8005
+gb.owner = \u6240\u6709\u8005
gb.description = \u63cf\u8ff0
gb.lastChange = \u6700\u540e\u4fee\u6539
-gb.refs = refs
+gb.refs = \u5206\u652f
gb.tag = \u6807\u7b7e
gb.tags = \u6807\u7b7e
gb.author = \u7528\u6237
gb.committer = \u63d0\u4ea4\u8005
gb.commit = \u63d0\u4ea4
-gb.age = age
+gb.age = \u65f6\u95f4
gb.tree = \u76ee\u5f55
-gb.parent = parent
+gb.parent = \u4e0a\u4e00\u7248\u672c
gb.url = URL
gb.history = \u5386\u53f2
gb.raw = \u539f\u59cb\u6587\u6863
-gb.object = object
-gb.ticketId = \u5de5\u5355\u7f16\u53f7
+gb.object = \u5bf9\u8c61
+gb.ticketId = \u95ee\u9898\u7f16\u53f7
gb.ticketAssigned = \u5206\u914d\u60c5\u51b5
gb.ticketOpenDate = \u5f00\u542f\u65e5\u671f
-gb.ticketState = \u72b6\u6001
gb.ticketComments = \u8bc4\u8bba
gb.view = \u67e5\u770b
gb.local = \u672c\u5730
@@ -31,11 +30,11 @@ gb.moreLogs = \u66f4\u591a\u63d0\u4ea4...
gb.allTags = \u6240\u6709\u6807\u7b7e...
gb.allBranches = \u6240\u6709\u5206\u652f...
gb.summary = \u6982\u51b5
-gb.ticket = \u5de5\u5355
+gb.ticket = \u95ee\u9898
gb.newRepository = \u521b\u5efa\u7248\u672c\u5e93
gb.newUser = \u6dfb\u52a0\u7528\u6237
gb.commitdiff = \u63d0\u4ea4\u5bf9\u6bd4
-gb.tickets = \u5de5\u5355
+gb.tickets = \u95ee\u9898
gb.pageFirst = \u9996\u9875
gb.pagePrevious = \u524d\u4e00\u9875
gb.pageNext = \u4e0b\u4e00\u9875
@@ -55,15 +54,15 @@ gb.addition = \u6dfb\u52a0
gb.modification = \u4fee\u6539
gb.deletion = \u5220\u9664
gb.rename = \u91cd\u547d\u540d
-gb.metrics = \u7edf\u8ba1\u56fe\u8868
+gb.metrics = \u7edf\u8ba1
gb.stats = \u7edf\u8ba1
-gb.markdown = markdown
-gb.changedFiles = \u5df2\u4fee\u6539\u6587\u4ef6
-gb.filesAdded = {0}\u4e2a\u6587\u4ef6\u5df2\u6dfb\u52a0
-gb.filesModified = {0}\u4e2a\u6587\u4ef6\u5df2\u4fee\u6539
-gb.filesDeleted = {0}\u4e2a\u6587\u4ef6\u5df2\u5220\u9664
-gb.filesCopied = {0} \u6587\u4ef6\u5df2\u590d\u5236
-gb.filesRenamed = {0} \u6587\u4ef6\u5df2\u91cd\u547d\u540d
+gb.markdown = Markdown
+gb.changedFiles = \u6587\u4ef6\u5df2\u4fee\u6539
+gb.filesAdded = \u5df2\u6dfb\u52a0{0}\u4e2a\u6587\u4ef6
+gb.filesModified = \u5df2\u4fee\u6539{0}\u4e2a\u6587\u4ef6
+gb.filesDeleted = \u5df2\u5220\u9664{0}\u4e2a\u6587\u4ef6
+gb.filesCopied = \u5df2\u590d\u5236{0}\u4e2a\u6587\u4ef6
+gb.filesRenamed = \u5df2\u91cd\u547d\u540d{0}\u4e2a\u6587\u4ef6
gb.missingUsername = \u7528\u6237\u540d\u4e0d\u5b58\u5728
gb.edit = \u7f16\u8f91
gb.searchTypeTooltip = \u9009\u62e9\u641c\u7d22\u7c7b\u578b
@@ -72,43 +71,43 @@ gb.delete = \u5220\u9664
gb.docs = \u6587\u6863
gb.accessRestriction = \u8bbf\u95ee\u9650\u5236
gb.name = \u540d\u79f0
-gb.enableTickets = \u5141\u8bb8\u4f7f\u7528\u5de5\u5355
-gb.enableDocs = \u5141\u8bb8\u4f7f\u7528\u6587\u6863
+gb.enableTickets = \u542f\u7528\u95ee\u9898
+gb.enableDocs = \u542f\u7528\u6587\u6863
gb.save = \u4fdd\u5b58
gb.showRemoteBranches = \u663e\u793a\u8fdc\u7a0b\u5206\u652f
gb.editUsers = \u7f16\u8f91\u7528\u6237
gb.confirmPassword = \u786e\u8ba4\u5bc6\u7801
-gb.restrictedRepositories = \u53d7\u9650\u7248\u672c\u5e93
+gb.restrictedRepositories = \u53d7\u9650\u7684\u7248\u672c\u5e93
gb.canAdmin = \u7ba1\u7406\u6743\u9650
gb.notRestricted = \u533f\u540d\u6d4f\u89c8\uff0c\u514b\u9686\u4e0e\u63a8\u9001
-gb.pushRestricted = \u8ba4\u8bc1\u63a8\u9001
-gb.cloneRestricted = \u8ba4\u8bc1\u514b\u9686\u4e0e\u63a8\u9001
-gb.viewRestricted = \u8ba4\u8bc1\u6d4f\u89c8\uff0c\u514b\u9686\u4e0e\u63a8\u9001
-gb.useTicketsDescription = \u53ea\u8bfb\u5206\u5e03\u5f0f Ticgit \u4e8b\u52a1
+gb.pushRestricted = \u63a8\u9001\uff08\u9700\u8981\u8ba4\u8bc1\uff09
+gb.cloneRestricted = \u514b\u9686\u4e0e\u63a8\u9001\uff08\u9700\u8981\u8ba4\u8bc1\uff09
+gb.viewRestricted = \u6d4f\u89c8\uff0c\u514b\u9686\u4e0e\u63a8\u9001\uff08\u9700\u8981\u8ba4\u8bc1\uff09
+gb.useTicketsDescription = readonly, distributed Ticgit issues
gb.useDocsDescription = \u5217\u51fa\u7248\u672c\u5e93\u5185\u6240\u6709 Markdown \u6587\u6863
gb.showRemoteBranchesDescription = \u663e\u793a\u8fdc\u7a0b\u5206\u652f
gb.canAdminDescription = Gitblit \u670d\u52a1\u5668\u7ba1\u7406\u5458
-gb.permittedUsers = \u5141\u8bb8\u7528\u6237
+gb.permittedUsers = \u6388\u6743\u7684\u7528\u6237
gb.isFrozen = \u88ab\u51bb\u7ed3
gb.isFrozenDescription = \u7981\u6b62\u63a8\u9001\u64cd\u4f5c
gb.zip = zip
-gb.showReadme = \u663e\u793areadme
-gb.showReadmeDescription = \u5728\u6982\u51b5\u9875\u9762\u663e\u793a \\"readme\\" Markdown \u6587\u4ef6
+gb.showReadme = \u663e\u793areadme\u6587\u4ef6
+gb.showReadmeDescription = \u5728\u6982\u51b5\u9875\u9762\u663e\u793a \"readme\" Markdown \u6587\u4ef6
gb.nameDescription = \u4f7f\u7528 '/' \u5bf9\u7248\u672c\u5e93\u8fdb\u884c\u5206\u7ec4 \u4f8b\u5982\uff1a libraries/mycoollib.git
-gb.ownerDescription = \u521b\u5efa\u8005\u53ef\u4ee5\u7f16\u8f91\u7248\u672c\u5e93\u5c5e\u6027
+gb.ownerDescription = \u6240\u6709\u8005\u53ef\u4ee5\u7f16\u8f91\u7248\u672c\u5e93\u5c5e\u6027
gb.blob = blob
gb.commitActivityTrend = \u63d0\u4ea4\u6d3b\u52a8\u8d8b\u52bf
gb.commitActivityDOW = \u6bcf\u5468\u63d0\u4ea4\u6d3b\u52a8
-gb.commitActivityAuthors = \u63d0\u4ea4\u6d3b\u52a8\u4e3b\u8981\u7528\u6237
-gb.feed = feed
+gb.commitActivityAuthors = \u4e3b\u8981\u6d3b\u52a8\u7528\u6237
+gb.feed = \u8ba2\u9605
gb.cancel = \u53d6\u6d88
gb.changePassword = \u4fee\u6539\u5bc6\u7801
-gb.isFederated = is federated
-gb.federateThis = federate this repository
-gb.federateOrigin = federate the origin
-gb.excludeFromFederation = exclude from federation
+gb.isFederated = \u5df2\u7ecf federated
+gb.federateThis = \u4e0e\u6b64\u7248\u672c\u5e93 federate
+gb.federateOrigin = \u4e0e\u8fdc\u7a0b\u7248\u672c\u5e93 federate
+gb.excludeFromFederation = \u4ece federation \u4e2d\u6392\u9664
gb.excludeFromFederationDescription = \u7981\u6b62\u5df2 federated \u7684 Gitblit \u5b9e\u4f8b\u4ece\u672c\u8d26\u6237\u62c9\u53d6
-gb.tokens = federation tokens
+gb.tokens = federation token
gb.tokenAllDescription = \u6240\u6709\u7248\u672c\u5e93\uff0c\u7528\u6237\u548c\u8bbe\u7f6e
gb.tokenUnrDescription = \u6240\u6709\u7248\u672c\u5e93\u548c\u7528\u6237
gb.tokenJurDescription = \u6240\u6709\u7248\u672c\u5e93
@@ -116,7 +115,7 @@ gb.federatedRepositoryDefinitions = \u7248\u672c\u5e93\u5b9a\u4e49
gb.federatedUserDefinitions = \u7528\u6237\u5b9a\u4e49
gb.federatedSettingDefinitions = \u8bbe\u7f6e\u5b9a\u4e49
gb.proposals = federation proposals
-gb.received = \u5df2\u63a5\u53d7
+gb.received = \u5df2\u63a5\u6536
gb.type = \u7c7b\u522b
gb.token = token
gb.repositories = \u7248\u672c\u5e93
@@ -128,27 +127,27 @@ gb.nextPull = \u4e0b\u4e00\u6b21\u62c9\u53d6
gb.inclusions = \u5305\u542b\u5185\u5bb9
gb.exclusions = \u4f8b\u5916
gb.registration = \u6ce8\u518c
-gb.registrations = federation \u6ce8\u518c
+gb.registrations = federation registrations
gb.sendProposal = \u63d0\u4ea4\u5efa\u8bae
gb.status = \u72b6\u6001
-gb.origin = \u8fdc\u7aef
+gb.origin = \u8fdc\u7a0b
gb.headRef = \u9ed8\u8ba4\u5206\u652f (HEAD)
gb.headRefDescription = \u4fee\u6539 HEAD \u6240\u6307\u5411\u7684 ref\u3002 \u4f8b\u5982: refs/heads/master
gb.federationStrategy = federation \u7b56\u7565
-gb.federationRegistration = federation \u6ce8\u518c
-gb.federationResults = federation \u62c9\u53d6\u7ed3\u679c
-gb.federationSets = federation \u96c6
+gb.federationRegistration = federation registration
+gb.federationResults = federation results
+gb.federationSets = federation sets
gb.message = \u6d88\u606f
gb.myUrlDescription = \u60a8\u7684 Gitblit \u5b9e\u4f8b\u7684\u516c\u5171\u8bbf\u95ee\u7f51\u5740
gb.destinationUrl = \u53d1\u9001\u81f3
-gb.destinationUrlDescription = \u4f60\u6240\u8981\u53d1\u9001\u5efa\u8bae\u7684 Gitblit \u5b9e\u4f8b\u7f51\u5740
+gb.destinationUrlDescription = \u4f60\u5e0c\u671b\u53d1\u9001\u7684 Gitblit \u5b9e\u4f8b\u7f51\u5740
gb.users = \u7528\u6237
gb.federation = federation
gb.error = \u9519\u8bef
gb.refresh = \u5237\u65b0
gb.browse = \u6d4f\u89c8
gb.clone = \u514b\u9686
-gb.filter = \u8fc7\u6ee4
+gb.filter = \u7b5b\u9009
gb.create = \u521b\u5efa
gb.servers = \u670d\u52a1\u5668
gb.recent = \u6700\u8fd1
@@ -165,14 +164,13 @@ gb.lastLogin = \u4e0a\u6b21\u767b\u5f55
gb.skipSizeCalculation = \u5ffd\u7565\u5927\u5c0f\u4f30\u8ba1
gb.skipSizeCalculationDescription = \u4e0d\u8ba1\u7b97\u7248\u672c\u5e93\u5927\u5c0f\uff08\u8282\u7701\u9875\u9762\u8f7d\u5165\u65f6\u95f4\uff09
gb.skipSummaryMetrics = \u5ffd\u7565\u6982\u51b5\u5904\u7684 metrics
-gb.skipSummaryMetricsDescription = \u6982\u51b5\u9875\u9762\u4e0d\u8ba1\u7b97metrics\uff08\u8282\u7701\u9875\u9762\u8f7d\u5165\u65f6\u95f4\uff09
-gb.accessLevel = \u8bbf\u95ee\u7ea7\u522b
+gb.skipSummaryMetricsDescription = \u6982\u51b5\u9875\u9762\u4e0d\u8ba1\u7b97 metrics\uff08\u8282\u7701\u9875\u9762\u8f7d\u5165\u65f6\u95f4\uff09
+gb.accessLevel = \u8bbf\u95ee\u6743\u9650
gb.default = \u9ed8\u8ba4
gb.setDefault = \u9ed8\u8ba4\u8bbe\u7f6e
gb.since = \u81ea\u4ece
-gb.status = \u72b6\u6001
gb.bootDate = \u542f\u52a8\u65e5\u671f
-gb.servletContainer = servlet container
+gb.servletContainer = servlet \u5bb9\u5668
gb.heapMaximum = \u6700\u5927\u5806
gb.heapAllocated = \u5df2\u5206\u914d\u5806
gb.heapUsed = \u5df2\u4f7f\u7528\u5806
@@ -196,7 +194,7 @@ gb.teamName = \u56e2\u961f\u540d\u79f0
gb.teamMembers = \u56e2\u961f\u6210\u5458
gb.teamMemberships = \u56e2\u961f\u6210\u5458
gb.newTeam = \u6dfb\u52a0\u56e2\u961f
-gb.permittedTeams = \u5141\u8bb8\u56e2\u961f
+gb.permittedTeams = \u6388\u6743\u7684\u56e2\u961f
gb.emptyRepository = \u7a7a\u7248\u672c\u5e93
gb.repositoryUrl = \u7248\u672c\u5e93\u5730\u5740
gb.mailingLists = \u90ae\u4ef6\u5217\u8868
@@ -204,97 +202,98 @@ gb.preReceiveScripts = pre-receive \u811a\u672c
gb.postReceiveScripts = post-receive \u811a\u672c
gb.hookScripts = hook \u811a\u672c
gb.customFields = \u81ea\u5b9a\u4e49\u57df
-gb.customFieldsDescription = Groovy\u811a\u672c\u652f\u6301\u7684\u81ea\u5b9a\u4e49\u57df
+gb.customFieldsDescription = Groovy \u811a\u672c\u652f\u6301\u7684\u81ea\u5b9a\u4e49\u57df
gb.accessPermissions = \u8bbf\u95ee\u6743\u9650
-gb.filters = \u8fc7\u6ee4
+gb.filters = \u7b5b\u9009
gb.generalDescription = \u4e00\u822c\u8bbe\u7f6e
-gb.accessPermissionsDescription = \u6309\u7167\u7528\u6237\u548c\u56e2\u961f\u9650\u5236\u8bbf\u95ee
-gb.accessPermissionsForUserDescription = \u8bbe\u7f6e\u56e2\u961f\u6210\u5458\u6216\u8005\u6388\u4e88\u6307\u5b9a\u7248\u672c\u5e93\u6743\u9650
+gb.accessPermissionsDescription = \u6309\u7167\u7528\u6237\u548c\u56e2\u961f\u8bbe\u7f6e\u8bbf\u95ee\u6743\u9650
+gb.accessPermissionsForUserDescription = \u8bbe\u7f6e\u56e2\u961f\u6210\u5458\u6216\u6388\u4e88\u6307\u5b9a\u7248\u672c\u5e93\u6743\u9650
gb.accessPermissionsForTeamDescription = \u8bbe\u7f6e\u56e2\u961f\u6210\u5458\u5e76\u6388\u4e88\u6307\u5b9a\u7248\u672c\u5e93\u6743\u9650
-gb.federationRepositoryDescription = \u4e0e\u5176\u4ed6Gitblit\u670d\u52a1\u5668\u5206\u4eab\u7248\u672c\u5e93
-gb.hookScriptsDescription = \u5728\u670d\u52a1\u5668\u4e0a\u8fd0\u884cGroovy\u811a\u672c
+gb.federationRepositoryDescription = \u4e0e\u5176\u4ed6 Gitblit \u670d\u52a1\u5668\u5206\u4eab\u7248\u672c\u5e93
+gb.hookScriptsDescription = \u5728\u670d\u52a1\u5668\u4e0a\u8fd0\u884c Groovy \u811a\u672c
gb.reset = \u91cd\u7f6e
gb.pages = \u9875\u9762
gb.workingCopy = \u5de5\u4f5c\u526f\u672c
gb.workingCopyWarning = \u6b64\u7248\u672c\u5e93\u5b58\u5728\u4e00\u4efd\u5de5\u4f5c\u526f\u672c\uff0c\u65e0\u6cd5\u8fdb\u884c\u63a8\u9001
gb.query = \u67e5\u8be2
-gb.queryHelp = \u652f\u6301\u6807\u51c6\u67e5\u8be2\u683c\u5f0f.<p/><p/>\u8bf7\u67e5\u770b <a target="_new" href="http://lucene.apache.org/core/old_versioned_docs/versions/3_5_0/queryparsersyntax.html">Lucene \u67e5\u8be2\u5904\u7406\u5668\u683c\u5f0f</a> \u4ee5\u83b7\u53d6\u8be6\u7ec6\u5185\u5bb9\u3002
+gb.queryHelp = \u652f\u6301\u6807\u51c6\u67e5\u8be2\u683c\u5f0f\u3002<p/><p/>\u8bf7\u53c2\u9605 ${querySyntax} \u4ee5\u8fdb\u4e00\u6b65\u4e86\u89e3\u3002
+gb.querySyntax = Lucene \u67e5\u8be2\u5904\u7406\u5668\u683c\u5f0f
gb.queryResults = \u7ed3\u679c {0} - {1} ({2} \u6b21\u547d\u4e2d)
gb.noHits = \u672a\u547d\u4e2d
-gb.authored = authored
-gb.committed = committed
+gb.authored = \u4f5c\u8005
+gb.committed = \u5df2\u63d0\u4ea4
gb.indexedBranches = \u5df2\u7d22\u5f15\u5206\u652f
gb.indexedBranchesDescription = \u9009\u62e9\u8981\u653e\u5165\u60a8\u7684 Lucene \u7d22\u5f15\u7684\u5206\u652f
gb.noIndexedRepositoriesWarning = \u60a8\u7684\u6240\u6709\u7248\u672c\u5e93\u90fd\u6ca1\u6709\u7ecf\u8fc7 Lucene \u7d22\u5f15
-gb.undefinedQueryWarning = \u67e5\u8be2\u672a\u5b9a\u4e49!
-gb.noSelectedRepositoriesWarning = \u8bf7\u81f3\u5c11\u9009\u62e9\u4e00\u4e2a\u7248\u672c\u5e93!
-gb.luceneDisabled = Lucene \u7d22\u5f15\u5df2\u88ab\u7981\u6b62
+gb.undefinedQueryWarning = \u672a\u8bbe\u7f6e\u67e5\u8be2\u6761\u4ef6
+gb.noSelectedRepositoriesWarning = \u8bf7\u81f3\u5c11\u9009\u62e9\u4e00\u4e2a\u7248\u672c\u5e93
+gb.luceneDisabled = Lucene \u7d22\u5f15\u5df2\u7981\u7528
gb.failedtoRead = \u8bfb\u53d6\u5931\u8d25
-gb.isNotValidFile = \u4e0d\u662f\u5408\u6cd5\u6587\u4ef6
-gb.failedToReadMessage = \u5728 {0} \u4e2d\u8bfb\u53d6\u9ed8\u8ba4\u6d88\u606f\u5931\u8d25!
-gb.passwordsDoNotMatch = \u5bc6\u7801\u4e0d\u5339\u914d!
-gb.passwordTooShort = \u5bc6\u7801\u957f\u5ea6\u592a\u77ed\u3002\u6700\u77ed\u957f\u5ea6 {0} \u4e2a\u5b57\u7b26\u3002
-gb.passwordChanged = \u5bc6\u7801\u4fee\u6539\u6210\u529f\u3002
-gb.passwordChangeAborted = \u5bc6\u7801\u4fee\u6539\u7ec8\u6b62
-gb.pleaseSetRepositoryName = \u8bf7\u8bbe\u7f6e\u4e00\u4e2a\u7248\u672c\u5e93\u540d\u79f0!
+gb.isNotValidFile = \u4e0d\u662f\u6709\u6548\u6587\u4ef6
+gb.failedToReadMessage = \u5728 {0} \u4e2d\u8bfb\u53d6\u9ed8\u8ba4\u6d88\u606f\u5931\u8d25
+gb.passwordsDoNotMatch = \u5bc6\u7801\u4e0d\u5339\u914d
+gb.passwordTooShort = \u5bc6\u7801\u957f\u5ea6\u592a\u77ed\uff0c\u6700\u77ed\u957f\u5ea6 {0} \u4e2a\u5b57\u7b26\u3002
+gb.passwordChanged = \u5bc6\u7801\u5df2\u66f4\u6539\u3002
+gb.passwordChangeAborted = \u5bc6\u7801\u66f4\u6539\u5df2\u53d6\u6d88\u3002
+gb.pleaseSetRepositoryName = \u8bf7\u8bbe\u7f6e\u4e00\u4e2a\u7248\u672c\u5e93\u540d\u79f0
gb.illegalLeadingSlash = \u7981\u6b62\u4f7f\u7528\u6839\u76ee\u5f55\u5f15\u7528 (/) \u3002
gb.illegalRelativeSlash = \u76f8\u5bf9\u6587\u4ef6\u5939\u8def\u5f84(../)\u7981\u6b62\u4f7f\u7528
-gb.illegalCharacterRepositoryName = \u7248\u672c\u5e93\u4e2d\u542b\u6709\u4e0d\u5408\u6cd5\u5b57\u7b26 ''{0}'' !
-gb.selectAccessRestriction = \u8bf7\u9009\u62e9\u8bbf\u95ee\u6743\u9650\uff01
-gb.selectFederationStrategy = \u8bf7\u9009\u62e9federation\u7b56\u7565!
-gb.pleaseSetTeamName = \u8bf7\u8f93\u5165\u4e00\u4e2a\u56e2\u961f\u540d\u79f0\uff01
+gb.illegalCharacterRepositoryName = \u7248\u672c\u5e93\u4e2d\u542b\u6709\u4e0d\u5408\u6cd5\u5b57\u7b26 ''{0}''
+gb.selectAccessRestriction = \u8bf7\u9009\u62e9\u8bbf\u95ee\u6743\u9650
+gb.selectFederationStrategy = \u8bf7\u9009\u62e9 federation \u7b56\u7565
+gb.pleaseSetTeamName = \u8bf7\u8f93\u5165\u4e00\u4e2a\u56e2\u961f\u540d\u79f0
gb.teamNameUnavailable = \u56e2\u961f\u540d ''{0}'' \u4e0d\u5408\u6cd5.
gb.teamMustSpecifyRepository = \u56e2\u961f\u5fc5\u987b\u62e5\u6709\u81f3\u5c11\u4e00\u4e2a\u7248\u672c\u5e93\u3002
gb.teamCreated = \u6210\u529f\u521b\u5efa\u65b0\u56e2\u961f ''{0}'' .
-gb.pleaseSetUsername = \u8bf7\u8f93\u5165\u7528\u6237\u540d\uff01
-gb.usernameUnavailable = \u7528\u6237\u540d ''{0}'' \u4e0d\u53ef\u7528..
-gb.combinedMd5Rename = Gitblit\u91c7\u7528\u6df7\u5408md5\u5bc6\u7801\u54c8\u5e0c\u3002\u56e0\u6b64\u5fc5\u987b\u5728\u4fee\u6539\u7528\u6237\u540d\u540e\u4fee\u6539\u5bc6\u7801\u3002
-gb.userCreated = \u6210\u529f\u521b\u5efa\u65b0\u7528\u6237 \\"{0}\\"\u3002
-gb.couldNotFindFederationRegistration = \u65e0\u6cd5\u627e\u5230federation registration!
-gb.failedToFindGravatarProfile = \u52a0\u8f7d {0} \u7684Gravatar\u4fe1\u606f\u5931\u8d25
+gb.pleaseSetUsername = \u8bf7\u8f93\u5165\u7528\u6237\u540d
+gb.usernameUnavailable = \u7528\u6237\u540d ''{0}'' \u4e0d\u53ef\u7528
+gb.combinedMd5Rename = Gitblit \u91c7\u7528\u6df7\u5408 MD5 \u5bc6\u7801\u54c8\u5e0c\uff0c\u56e0\u6b64\u5fc5\u987b\u5728\u4fee\u6539\u7528\u6237\u540d\u540e\u4fee\u6539\u5bc6\u7801\u3002
+gb.userCreated = \u6210\u529f\u521b\u5efa\u65b0\u7528\u6237 \"{0}\"\u3002
+gb.couldNotFindFederationRegistration = \u65e0\u6cd5\u627e\u5230 federation registration
+gb.failedToFindGravatarProfile = \u52a0\u8f7d {0} \u7684 Gravatar \u4fe1\u606f\u5931\u8d25
gb.branchStats = {0} \u4e2a\u63d0\u4ea4\u548c {1} \u4e2a\u6807\u7b7e\u5728 {2} \u5185
-gb.repositoryNotSpecified = \u672a\u6307\u5b9a\u7248\u672c\u5e93!
-gb.repositoryNotSpecifiedFor = \u6ca1\u6709\u4e3a {0} \u8bbe\u7f6e\u7248\u672c\u5e93!
+gb.repositoryNotSpecified = \u672a\u6307\u5b9a\u7248\u672c\u5e93
+gb.repositoryNotSpecifiedFor = \u6ca1\u6709\u4e3a {0} \u8bbe\u7f6e\u7248\u672c\u5e93
gb.canNotLoadRepository = \u65e0\u6cd5\u8f7d\u5165\u7248\u672c\u5e93
gb.commitIsNull = \u63d0\u4ea4\u5185\u5bb9\u4e3a\u7a7a
gb.unauthorizedAccessForRepository = \u672a\u6388\u6743\u8bbf\u95ee\u7248\u672c\u5e93
-gb.failedToFindCommit = \u5728 {1} \u4e2d {2} \u4e2a\u9875\u9762\u5185\u67e5\u627e\u63d0\u4ea4 \\"{0}\\"\u5931\u8d25!
-gb.couldNotFindFederationProposal = \u65e0\u6cd5\u627e\u5230federation proposal!
-gb.invalidUsernameOrPassword = \u7528\u6237\u540d\u6216\u8005\u5bc6\u7801\u9519\u8bef\uff01
-gb.OneProposalToReview = 1\u4e2afederation proposals\u7b49\u5f85\u5ba1\u67e5\u3002
-gb.nFederationProposalsToReview = {0} \u4e2afederation proposals\u7b49\u5f85\u5ba1\u67e5
+gb.failedToFindCommit = \u5728 {1} \u4e2d {2} \u4e2a\u9875\u9762\u5185\u67e5\u627e\u63d0\u4ea4 \"{0}\"\u5931\u8d25
+gb.couldNotFindFederationProposal = \u65e0\u6cd5\u627e\u5230 federation proposal
+gb.invalidUsernameOrPassword = \u7528\u6237\u540d\u6216\u8005\u5bc6\u7801\u9519\u8bef
+gb.OneProposalToReview = 1\u4e2a federation proposal \u9700\u8981\u5ba1\u67e5
+gb.nFederationProposalsToReview = {0} \u4e2a federation proposals \u9700\u8981\u5ba1\u67e5
gb.couldNotFindTag = \u65e0\u6cd5\u627e\u5230\u6807\u7b7e {0}
-gb.couldNotCreateFederationProposal = \u65e0\u6cd5\u521b\u5efafederation proposal!
-gb.pleaseSetGitblitUrl = \u8bf7\u8f93\u5165\u4f60\u7684Gitblit\u7f51\u5740!
-gb.pleaseSetDestinationUrl = \u8bf7\u4e3a\u4f60\u7684proposal\u8f93\u5165\u4e00\u4e2a\u76ee\u6807\u5730\u5740!
-gb.proposalReceived = \u6210\u529f\u4ece {0} \u63a5\u6536Proposal.
-gb.noGitblitFound = \u62b1\u6b49, {0} \u65e0\u6cd5\u5728{1} \u4e2d\u627e\u5230Gitblit\u5b9e\u4f8b\u3002
-gb.noProposals = \u62b1\u6b49, {0} \u5f53\u524d\u4e0d\u63a5\u53d7proposals\u3002
-gb.noFederation = \u62b1\u6b49, {0} \u6ca1\u6709\u4e0e\u4efb\u4f55Gitblit\u5b9e\u4f8b\u8bbe\u7f6efederate\u3002.
-gb.proposalFailed = \u62b1\u6b49, {0} \u65e0\u6cd5\u63a5\u53d7\u4efb\u4f55proposal\u6570\u636e!
-gb.proposalError = \u62b1\u6b49\uff0c{0} \u62a5\u544a\u4e2d\u53d1\u73b0\u672a\u9884\u671f\u7684\u9519\u8bef\uff01
-gb.failedToSendProposal = \u53d1\u9001proposal\u5931\u8d25!
-gb.userServiceDoesNotPermitAddUser = {0} \u4e0d\u5141\u8bb8\u6dfb\u52a0\u7528\u6237!
-gb.userServiceDoesNotPermitPasswordChanges = {0} \u4e0d\u5141\u8bb8\u8fdb\u884c\u5bc6\u7801\u4fee\u6539!
+gb.couldNotCreateFederationProposal = \u65e0\u6cd5\u521b\u5efa federation proposal
+gb.pleaseSetGitblitUrl = \u8bf7\u8f93\u5165\u4f60\u7684 Gitblit URL
+gb.pleaseSetDestinationUrl = \u8bf7\u4e3a\u4f60\u7684 proposal \u8f93\u5165\u4e00\u4e2a\u76ee\u6807 URL
+gb.proposalReceived = \u6210\u529f\u4ece {0} \u63a5\u6536 Proposal
+gb.noGitblitFound = \u62b1\u6b49, {0} \u65e0\u6cd5\u5728 {1} \u4e2d\u627e\u5230 Gitblit \u5b9e\u4f8b\u3002
+gb.noProposals = \u62b1\u6b49, {0} \u5f53\u524d\u4e0d\u63a5\u53d7 proposals\u3002
+gb.noFederation = \u62b1\u6b49, {0} \u6ca1\u6709\u4e0e\u4efb\u4f55 Gitblit \u5b9e\u4f8b\u8bbe\u7f6e federate\u3002
+gb.proposalFailed = \u62b1\u6b49, {0} \u65e0\u6cd5\u63a5\u53d7\u4efb\u4f55 proposal \u6570\u636e\u3002
+gb.proposalError = \u62b1\u6b49\uff0c{0} \u62a5\u544a\u4e2d\u53d1\u73b0\u672a\u77e5\u9519\u8bef\u3002
+gb.failedToSendProposal = \u53d1\u9001 proposal \u5931\u8d25
+gb.userServiceDoesNotPermitAddUser = {0} \u4e0d\u5141\u8bb8\u6dfb\u52a0\u7528\u6237
+gb.userServiceDoesNotPermitPasswordChanges = {0} \u4e0d\u5141\u8bb8\u4fee\u6539\u5bc6\u7801
gb.displayName = \u663e\u793a\u540d\u79f0
-gb.emailAddress = \u90ae\u7bb1
-gb.errorAdminLoginRequired = \u9700\u8981\u7ba1\u7406\u5458\u767b\u9646
+gb.emailAddress = \u7535\u5b50\u90ae\u4ef6\u5730\u5740
+gb.errorAdminLoginRequired = \u9700\u8981\u4ee5\u7ba1\u7406\u5458\u8eab\u4efd\u767b\u5f55
gb.errorOnlyAdminMayCreateRepository = \u53ea\u6709\u7ba1\u7406\u5458\u624d\u53ef\u4ee5\u521b\u5efa\u7248\u672c\u5e93
-gb.errorOnlyAdminOrOwnerMayEditRepository = \u53ea\u6709\u7ba1\u7406\u5458\u6216\u8005\u6240\u6709\u8005\u624d\u53ef\u4ee5\u7f16\u8f91\u4ee3\u7801\u5e93
-gb.errorAdministrationDisabled = \u7ba1\u7406\u6743\u9650\u88ab\u7981\u6b62\u3002
+gb.errorOnlyAdminOrOwnerMayEditRepository = \u53ea\u6709\u7ba1\u7406\u5458\u6216\u8005\u6240\u6709\u8005\u624d\u53ef\u4ee5\u7f16\u8f91\u7248\u672c\u5e93
+gb.errorAdministrationDisabled = \u7ba1\u7406\u6743\u9650\u88ab\u7981\u6b62
gb.lastNDays = \u6700\u8fd1 {0} \u5929
-gb.completeGravatarProfile = \u5728Gravatar.com\u4e0a\u5b8c\u6210\u4e2a\u4eba\u8bbe\u5b9a
+gb.completeGravatarProfile = \u5728 Gravatar.com \u4e0a\u7f16\u8f91\u4e2a\u4eba\u8d44\u6599
gb.none = \u65e0
gb.line = \u884c
gb.content = \u5185\u5bb9
-gb.empty = \u7a7a\u767d\u7248\u672c\u5e93
+gb.empty = \u7a7a\u7248\u672c\u5e93
gb.inherited = \u7ee7\u627f
-gb.deleteRepository = \u5220\u9664\u7248\u672c\u5e93 \\"{0}\\" \uff1f
+gb.deleteRepository = \u5220\u9664\u7248\u672c\u5e93 \"{0}\" \uff1f
gb.repositoryDeleted = \u7248\u672c\u5e93 ''{0}'' \u5df2\u5220\u9664\u3002
-gb.repositoryDeleteFailed = \u5220\u9664\u7248\u672c\u5e93 \\"{0}\\" \u5931\u8d25\uff01
-gb.deleteUser = \u5220\u9664\u7528\u6237 \\"{0}\\" \uff1f
-gb.userDeleted = \u7528\u6237 ''{0}'' \u5df2\u5220\u9664\uff01
-gb.userDeleteFailed = \u5220\u9664\u7528\u6237''{0}''\u5931\u8d25\uff01
+gb.repositoryDeleteFailed = \u5220\u9664\u7248\u672c\u5e93 \"{0}\" \u5931\u8d25\u3002
+gb.deleteUser = \u5220\u9664\u7528\u6237 \"{0}\" \uff1f
+gb.userDeleted = \u7528\u6237 ''{0}'' \u5df2\u5220\u9664\u3002
+gb.userDeleteFailed = \u5220\u9664\u7528\u6237''{0}''\u5931\u8d25\u3002
gb.time.justNow = \u521a\u521a
gb.time.today = \u4eca\u5929
gb.time.yesterday = \u6628\u5929
@@ -314,68 +313,67 @@ gb.duration.years = {0} \u5e74
gb.authorizationControl = \u6388\u6743\u63a7\u5236
gb.allowAuthenticatedDescription = \u6388\u4e88\u6240\u6709\u8ba4\u8bc1\u7528\u6237\u53d7\u9650\u7684\u8bbf\u95ee\u6743\u9650
gb.allowNamedDescription = \u6388\u4e88\u6307\u5b9a\u540d\u79f0\u7684\u7528\u6237\u6216\u56e2\u961f\u53d7\u9650\u7684\u8bbf\u95ee\u6743\u9650
-gb.markdownFailure = \u8bfb\u53d6 Markdown \u5185\u5bb9\u5931\u8d25\uff01
+gb.markdownFailure = \u8bfb\u53d6 Markdown \u5185\u5bb9\u5931\u8d25\u3002
gb.clearCache = \u6e05\u9664\u7f13\u5b58
gb.projects = \u9879\u76ee
gb.project = \u9879\u76ee
gb.allProjects = \u6240\u6709\u9879\u76ee
gb.copyToClipboard = \u590d\u5236\u5230\u526a\u8d34\u677f
-gb.fork = \u6d3e\u751f
-gb.forks = \u6d3e\u751f
-gb.forkRepository = \u6d3e\u751f {0} ?
-gb.repositoryForked = {0} \u5df2\u88ab\u6d3e\u751f
-gb.repositoryForkFailed = \u6d3e\u751f\u5931\u8d25
-gb.personalRepositories = \u79c1\u4eba\u7248\u672c\u5e93
-gb.allowForks = \u5141\u8bb8\u6d3e\u751f
-gb.allowForksDescription = \u5141\u8bb8\u8ba4\u8bc1\u7528\u6237\u6d3e\u751f\u6b64\u7248\u672c\u5e93
-gb.forkedFrom = \u6d3e\u751f\u81ea
-gb.canFork = \u5141\u8bb8\u6d3e\u751f
-gb.canForkDescription = \u5141\u8bb8\u6d3e\u751f\u8ba4\u8bc1\u7248\u672c\u5e93\u5230\u79c1\u4eba\u7248\u672c\u5e93
-gb.myFork = \u67e5\u770b\u6211\u7684\u6d3e\u751f
-gb.forksProhibited = \u7981\u6b62\u6d3e\u751f
-gb.forksProhibitedWarning = \u5f53\u524d\u7248\u672c\u5e93\u7981\u6b62\u6d3e\u751f
-gb.noForks = {0} \u6ca1\u6709\u6d3e\u751f
-gb.forkNotAuthorized = \u62b1\u6b49\uff0c\u60a8\u65e0\u6743\u6d3e\u751f {0}
-gb.forkInProgress = \u6b63\u5728\u6d3e\u751f
-gb.preparingFork = \u6b63\u5728\u4e3a\u60a8\u51c6\u5907\u6d3e\u751f...
-gb.isFork = \u5df2\u6d3e\u751f
+gb.fork = \u5efa\u7acb\u5206\u652f
+gb.forks = \u5206\u652f
+gb.forkRepository = \u5efa\u7acb {0} \u7684\u5206\u652f\uff1f
+gb.repositoryForked = {0} \u5df2\u5efa\u7acb\u5206\u652f
+gb.repositoryForkFailed = \u5efa\u7acb\u5206\u652f\u5931\u8d25
+gb.personalRepositories = \u79c1\u6709\u7248\u672c\u5e93
+gb.allowForks = \u5141\u8bb8\u5efa\u7acb\u5206\u652f
+gb.allowForksDescription = \u5141\u8bb8\u6388\u6743\u7684\u7528\u6237\u5efa\u7acb\u6b64\u7248\u672c\u5e93\u7684\u5206\u652f
+gb.forkedFrom = \u5206\u652f\u81ea
+gb.canFork = \u5141\u8bb8\u5206\u652f
+gb.canForkDescription = \u5141\u8bb8\u5206\u652f\u7248\u672c\u5e93\u5e76\u590d\u5236\u5230\u79c1\u6709\u7248\u672c\u5e93\u4e2d
+gb.myFork = \u67e5\u770b\u6211\u7684\u5206\u652f
+gb.forksProhibited = \u7981\u6b62\u5efa\u7acb\u5206\u652f
+gb.forksProhibitedWarning = \u5f53\u524d\u7248\u672c\u5e93\u7981\u6b62\u5efa\u7acb\u5206\u652f
+gb.noForks = {0} \u6ca1\u6709\u5206\u652f
+gb.forkNotAuthorized = \u62b1\u6b49\uff0c\u60a8\u65e0\u6743\u5206\u652f {0}
+gb.forkInProgress = \u6b63\u5728\u590d\u5236
+gb.preparingFork = \u6b63\u5728\u4e3a\u60a8\u51c6\u5907\u5206\u652f...
+gb.isFork = \u5df2\u5efa\u7acb\u5206\u652f
gb.canCreate = \u5141\u8bb8\u521b\u5efa
-gb.canCreateDescription = \u5141\u8bb8\u521b\u5efa\u79c1\u4eba\u7248\u672c\u5e93
-gb.illegalPersonalRepositoryLocation = \u60a8\u7684\u79c1\u4eba\u7248\u672c\u5e93\u5fc5\u987b\u4f4d\u4e8e \\"{0}\\"
+gb.canCreateDescription = \u5141\u8bb8\u521b\u5efa\u79c1\u6709\u7248\u672c\u5e93
+gb.illegalPersonalRepositoryLocation = \u60a8\u7684\u79c1\u6709\u7248\u672c\u5e93\u5fc5\u987b\u4f4d\u4e8e \"{0}\"
gb.verifyCommitter = \u9a8c\u8bc1\u63d0\u4ea4\u8005
gb.verifyCommitterDescription = \u9700\u8981\u63d0\u4ea4\u8005\u7684\u8eab\u4efd\u4e0e Gitblit \u7528\u6237\u8eab\u4efd\u76f8\u7b26
-gb.verifyCommitterNote = \u6240\u6709\u5408\u5e76\u9009\u9879\u9700\u8981\u4f7f\u7528 \\"--no-ff\\" \u6765\u6267\u884c\u63d0\u4ea4\u8005\u9a8c\u8bc1
+gb.verifyCommitterNote = \u6240\u6709\u5408\u5e76\u9009\u9879\u9700\u8981\u4f7f\u7528 \"--no-ff\" \u6765\u6267\u884c\u63d0\u4ea4\u8005\u9a8c\u8bc1
gb.repositoryPermissions = \u7248\u672c\u5e93\u6743\u9650
gb.userPermissions = \u7528\u6237\u6743\u9650
gb.teamPermissions = \u56e2\u961f\u6743\u9650
gb.add = \u6dfb\u52a0
-gb.noPermission = \u5220\u9664\u6b64\u6743\u9650
-gb.excludePermission = {0} (\u6392\u9664)
-gb.viewPermission = {0} (\u6d4f\u89c8)
-gb.clonePermission = {0} (\u514b\u9686)
-gb.pushPermission = {0} (\u63a8\u9001)
-gb.createPermission = {0} (\u63a8\u9001, \u521b\u5efaref)
-gb.deletePermission = {0} (\u63a8\u9001, \u521b\u5efa\u5220\u9664ref)
-gb.rewindPermission = {0} (\u63a8\u9001, \u521b\u5efa\u5220\u9664\u4ee5\u53carewind ref)
-gb.permission = \u6743\u9650
-gb.regexPermission = \u6b64\u6743\u9650\u662f\u901a\u8fc7\u6b63\u5219\u8868\u8fbe\u5f0f \\"{0}\\" \u8bbe\u7f6e
+gb.noPermission = \u64a4\u9500\u6b64\u6743\u9650
+gb.excludePermission = {0}\uff08\u6392\u9664\uff09
+gb.viewPermission = {0}\uff08\u6d4f\u89c8\uff09
+gb.clonePermission = {0}\uff08\u514b\u9686\uff09
+gb.pushPermission = {0}\uff08\u63a8\u9001\uff09
+gb.createPermission = {0}\uff08\u63a8\u9001, \u521b\u5efa\uff0c\u5f15\u7528\uff09
+gb.deletePermission = {0}\uff08\u63a8\u9001, \u521b\u5efa\uff0c\u5220\u9664\uff0c\u5f15\u7528\uff09
+gb.rewindPermission = {0}\uff08\u63a8\u9001, \u521b\u5efa\uff0c\u5220\u9664\uff0c\u56de\u9000\uff09
+gb.regexPermission = \u6b64\u6743\u9650\u662f\u901a\u8fc7\u6b63\u5219\u8868\u8fbe\u5f0f \"{0}\" \u8bbe\u7f6e
gb.accessDenied = \u8bbf\u95ee\u88ab\u62d2\u7edd
gb.busyCollectingGarbage = \u62b1\u6b49\uff0cGitblit\u6b63\u5728 {0} \u5185\u6e05\u7406\u5783\u573e
gb.gcPeriod = \u5783\u573e\u6e05\u7406\u65f6\u95f4
gb.gcPeriodDescription = \u5783\u573e\u6e05\u7406\u7684\u6301\u7eed\u65f6\u95f4
gb.gcThreshold = \u5783\u573e\u6e05\u7406\u9600\u503c
-gb.gcThresholdDescription = \u6fc0\u53d1\u5783\u573e\u6e05\u7406\u7684\u6700\u5c0f\u5bf9\u8c61\u5927\u5c0f
-gb.ownerPermission = \u7248\u672c\u5e93\u521b\u5efa\u8005
+gb.gcThresholdDescription = \u89e6\u53d1\u5783\u573e\u6e05\u7406\u7684\u6700\u5c0f\u5bf9\u8c61\u5927\u5c0f
+gb.ownerPermission = \u7248\u672c\u5e93\u6240\u6709\u8005
gb.administrator = \u7ba1\u7406\u5458
gb.administratorPermission = Gitblit \u7ba1\u7406\u5458
gb.team = \u56e2\u961f
-gb.teamPermission = \u901a\u8fc7 \\"{0}\\" \u56e2\u961f\u6210\u5458\u8bbe\u7f6e\u6743\u9650
-gb.missing = \u4e0d\u5b58\u5728!
-gb.missingPermission = \u6b64\u6743\u9650\u7684\u7248\u672c\u5e93\u4e0d\u5b58\u5728!
-gb.mutable = mutable
-gb.specified = specified
-gb.effective = effective
-gb.organizationalUnit = \u7ec4\u7ec7\u5355\u5143
+gb.teamPermission = \u901a\u8fc7 \"{0}\" \u56e2\u961f\u6210\u5458\u8bbe\u7f6e\u6743\u9650
+gb.missing = \u4e0d\u5b58\u5728
+gb.missingPermission = \u6b64\u6743\u9650\u65e0\u6cd5\u5bf9\u5e94\u5230\u7248\u672c\u5e93
+gb.mutable = \u53ef\u53d8\u7684
+gb.specified = \u6307\u5b9a\u7684
+gb.effective = \u6709\u6548\u7684
+gb.organizationalUnit = \u7ec4\u7ec7\u5355\u4f4d
gb.organization = \u7ec4\u7ec7
gb.locality = \u5730\u533a
gb.stateProvince = \u5dde\u6216\u7701
@@ -393,8 +391,8 @@ gb.revokeCertificate = \u64a4\u9500\u8bc1\u4e66
gb.sendEmail = \u53d1\u9001\u90ae\u4ef6
gb.passwordHint = \u5bc6\u7801\u63d0\u793a
gb.ok = \u786e\u5b9a
-gb.invalidExpirationDate = \u65e0\u6548\u7684\u8fc7\u671f\u65f6\u95f4!
-gb.passwordHintRequired = \u9700\u8981\u586b\u5199\u5bc6\u7801\u63d0\u793a!
+gb.invalidExpirationDate = \u65e0\u6548\u7684\u8fc7\u671f\u65f6\u95f4
+gb.passwordHintRequired = \u9700\u8981\u586b\u5199\u5bc6\u7801\u63d0\u793a
gb.viewCertificate = \u67e5\u770b\u8bc1\u4e66
gb.subject = \u4e3b\u9898
gb.issuer = \u63d0\u4ea4\u8005
@@ -407,9 +405,9 @@ gb.md5FingerPrint = MD5 \u6307\u7eb9\u7b97\u6cd5
gb.reason = \u7406\u7531
gb.revokeCertificateReason = \u8bf7\u9009\u62e9\u64a4\u9500\u8bc1\u4e66\u7684\u7406\u7531
gb.unspecified = \u672a\u6307\u5b9a
-gb.keyCompromise = key compromise
-gb.caCompromise = CA compromise
-gb.affiliationChanged = \u96b6\u5c5e\u5173\u7cfb\u5df2\u4fee\u6539
+gb.keyCompromise = \u5bc6\u94a5\u6cc4\u9732
+gb.caCompromise = CA \u6cc4\u9732
+gb.affiliationChanged = \u96b6\u5c5e\u5173\u7cfb\u5df2\u66f4\u6539
gb.superseded = \u5df2\u53d6\u4ee3
gb.cessationOfOperation = \u505c\u6b62\u64cd\u4f5c
gb.privilegeWithdrawn = \u7279\u6743\u5df2\u64a4\u56de
@@ -424,46 +422,46 @@ gb.duration = \u6301\u7eed\u65f6\u95f4
gb.certificateRevoked = \u8bc1\u4e66 {0,number,0} \u5df2\u88ab\u64a4\u9500
gb.clientCertificateGenerated = \u6210\u529f\u4e3a {0} \u751f\u6210\u65b0\u7684\u5ba2\u6237\u7aef\u8bc1\u4e66
gb.sslCertificateGenerated = \u6210\u529f\u4e3a {0} \u751f\u6210\u65b0\u7684\u670d\u52a1\u5668 SSL \u8bc1\u4e66
-gb.newClientCertificateMessage = \u6ce8\u610f:\\n\u6b64\u5bc6\u7801\u5e76\u975e\u7528\u6237\u5bc6\u7801, \u8fd9\u662f\u4fdd\u5b58\u7528\u6237 keystore \u7684\u5bc6\u7801\u3002 \u7531\u4e8e\u672c\u5bc6\u7801\u672a\u5b58\u50a8\uff0c\u56e0\u6b64\u4f60\u5fc5\u987b\u4e00\u4e2a\u5bc6\u7801\u63d0\u793a\uff0c\u8fd9\u4e2a\u63d0\u793a\u4f1a\u8bb0\u5f55\u5728\u7528\u6237\u7684 README \u6587\u6863\u5185\u3002
+gb.newClientCertificateMessage = \u6ce8\u610f:\n\u6b64\u5bc6\u7801\u5e76\u975e\u7528\u6237\u5bc6\u7801, \u8fd9\u662f\u4fdd\u5b58\u7528\u6237 keystore \u7684\u5bc6\u7801\u3002 \u7531\u4e8e\u672c\u5bc6\u7801\u672a\u5b58\u50a8\uff0c\u56e0\u6b64\u4f60\u5fc5\u987b\u4e00\u4e2a\u5bc6\u7801\u63d0\u793a\uff0c\u8fd9\u4e2a\u63d0\u793a\u4f1a\u8bb0\u5f55\u5728\u7528\u6237\u7684 README \u6587\u6863\u5185\u3002
gb.certificate = \u8bc1\u4e66
gb.emailCertificateBundle = \u53d1\u9001\u5ba2\u6237\u7aef\u8bc1\u4e66
gb.pleaseGenerateClientCertificate = \u8bf7\u4e3a {0} \u751f\u6210\u4e00\u4e2a\u5ba2\u6237\u7aef\u8bc1\u4e66
-gb.clientCertificateBundleSent = {0} \u7684\u5ba2\u6237\u7aef\u8bc1\u4e66\u5df2\u53d1\u9001
+gb.clientCertificateBundleSent = \u5df2\u53d1\u9001 {0} \u7684\u5ba2\u6237\u7aef\u8bc1\u4e66
gb.enterKeystorePassword = \u8bf7\u8f93\u5165 Gitblit keystore \u5bc6\u7801
gb.warning = \u8b66\u544a
-gb.jceWarning = \u60a8\u7684 JAVA \u8fd0\u884c\u73af\u5883\u4e0d\u5305\u542b \\"JCE Unlimited Strength Jurisdiction Policy\\" \u6587\u4ef6\u3002\\n\u8fd9\u5c06\u5bfc\u81f4\u60a8\u6700\u591a\u53ea\u80fd\u75287\u4e2a\u5b57\u7b26\u7684\u5bc6\u7801\u4fdd\u62a4\u60a8\u7684 keystore\u3002 \\n\u8fd9\u4e9b\u662f\u4e00\u4e9b\u53ef\u9009\u4e0b\u8f7d\u7684\u653f\u7b56\u6587\u4ef6\u3002\\n\\n\u4f60\u662f\u5426\u8981\u7ee7\u7eed\u751f\u6210\u8bc1\u4e66\uff1f\\n\\n\u9009\u62e9\u5426\u7684\u8bdd\uff0c\u5c06\u4f1a\u6253\u5f00\u4e00\u4e2a\u6d4f\u89c8\u5668\u754c\u9762\u4f9b\u60a8\u4e0b\u8f7d\u76f8\u5173\u6587\u4ef6\u3002
+gb.jceWarning = \u60a8\u7684 JAVA \u8fd0\u884c\u73af\u5883\u4e0d\u5305\u542b \"JCE Unlimited Strength Jurisdiction Policy\" \u6587\u4ef6\u3002\n\u8fd9\u5c06\u5bfc\u81f4\u60a8\u6700\u591a\u53ea\u80fd\u7528 7 \u4e2a\u5b57\u7b26\u7684\u5bc6\u7801\u4fdd\u62a4\u60a8\u7684 keystore\u3002 \n\u8fd9\u4e9b\u662f\u4e00\u4e9b\u53ef\u9009\u4e0b\u8f7d\u7684\u6587\u4ef6\u3002\n\n\u4f60\u662f\u5426\u8981\u7ee7\u7eed\u751f\u6210\u8bc1\u4e66\uff1f\n\n\u9009\u62e9\u5426\u7684\u8bdd\uff0c\u5c06\u4f1a\u6253\u5f00\u4e00\u4e2a\u6d4f\u89c8\u5668\u9875\u9762\u4f9b\u60a8\u4e0b\u8f7d\u76f8\u5173\u6587\u4ef6\u3002
gb.maxActivityCommits = \u6700\u5927\u6d3b\u52a8\u63d0\u4ea4\u6570
gb.maxActivityCommitsDescription = \u6d3b\u52a8\u9875\u9762\u663e\u793a\u7684\u6700\u5927\u63d0\u4ea4\u6570
gb.noMaximum = \u65e0\u4e0a\u9650
gb.attributes = \u5c5e\u6027
gb.serveCertificate = \u4f7f\u7528\u6b64\u8bc1\u4e66\u63d0\u4f9b https \u652f\u6301
-gb.sslCertificateGeneratedRestart = \u6210\u529f\u4e3a {0} \u751f\u6210\u65b0\u7684 SSL \u8bc1\u4e66.\\n\u4f60\u5fc5\u987b\u91cd\u65b0\u542f\u52a8 Gitblit \u4ee5\u4f7f\u7528\u6b64\u8bc1\u4e66\u3002\\n\\n\u5982\u679c\u60a8\u4f7f\u7528 '--alias' \u53c2\u6570\u542f\u52a8\uff0c\u4f60\u5fc5\u987b\u4e5f\u8981\u8bbe\u7f6e ''--alias {0}''\u3002
-gb.validity = \u5408\u6cd5\u6027
+gb.sslCertificateGeneratedRestart = \u6210\u529f\u4e3a {0} \u751f\u6210\u65b0\u7684 SSL \u8bc1\u4e66.\n\u4f60\u5fc5\u987b\u91cd\u65b0\u542f\u52a8 Gitblit \u4ee5\u4f7f\u7528\u6b64\u8bc1\u4e66\u3002\n\n\u5982\u679c\u60a8\u4f7f\u7528 '--alias' \u53c2\u6570\u542f\u52a8\uff0c\u4f60\u5fc5\u987b\u4e5f\u8981\u8bbe\u7f6e ''--alias {0}''\u3002
+gb.validity = \u6709\u6548\u6027
gb.siteName = \u7f51\u7ad9\u540d\u79f0
gb.siteNameDescription = \u60a8\u7684\u670d\u52a1\u5668\u7684\u7b80\u8981\u63cf\u8ff0
gb.excludeFromActivity = \u4ece\u6d3b\u52a8\u9875\u9762\u6392\u9664
gb.isSparkleshared = \u7248\u672c\u5e93\u5df2\u901a\u8fc7 Sparkleshare \u5b8c\u6210\u540c\u6b65
-gb.owners = \u62e5\u6709\u8005
+gb.owners = \u6240\u6709\u8005
gb.sessionEnded = \u4f1a\u8bdd\u5df2\u5173\u95ed
gb.closeBrowser = \u8bf7\u5173\u95ed\u6d4f\u89c8\u5668\u4ee5\u4fbf\u6b63\u5e38\u5173\u95ed\u4f1a\u8bdd\u3002
gb.doesNotExistInTree = {1} \u76ee\u5f55\u4e2d\u4e0d\u5b58\u5728 {0}
gb.enableIncrementalPushTags = \u5141\u8bb8\u9012\u589e\u5f0f\u63a8\u9001\u6807\u7b7e
gb.useIncrementalPushTagsDescription = \u6bcf\u6b21\u63a8\u9001\u65f6\uff0c\u81ea\u52a8\u4e3a\u6bcf\u4e2a\u5206\u652f\u6dfb\u52a0\u9012\u589e\u7684\u4fee\u8ba2\u7f16\u53f7
gb.incrementalPushTagMessage = \u63a8\u9001\u65f6\u81ea\u52a8\u4e3a\u5206\u652f [{0}] \u6dfb\u52a0\u6807\u7b7e
-gb.externalPermissions = {0} \u7684\u8bbf\u95ee\u6743\u9650\u5c5e\u4e8e\u5916\u90e8\u63a7\u5236
-gb.viewAccess = \u60a8\u6ca1\u6709 Gitblit \u8bfb\u6216\u5199\u7684\u6743\u9650
-gb.overview = \u603b\u89c8
-gb.dashboard = \u516c\u544a\u677f
+gb.externalPermissions = {0} \u7684\u8bbf\u95ee\u6743\u9650\u7531\u5916\u90e8\u63a7\u5236
+gb.viewAccess = \u60a8\u6ca1\u6709 Gitblit \u8bfb\u53d6\u6216\u5199\u5165\u7684\u6743\u9650
+gb.overview = \u6982\u89c8
+gb.dashboard = \u4eea\u8868\u76d8
gb.monthlyActivity = \u6bcf\u6708\u6d3b\u52a8
-gb.myProfile = \u7528\u6237\u4e2d\u5fc3
+gb.myProfile = \u4e2a\u4eba\u8d44\u6599
gb.compare = \u5bf9\u6bd4
gb.manual = \u624b\u518c
-gb.from = from
-gb.to = to
-gb.at = at
+gb.from = \u4ece
+gb.to = \u81f3
+gb.at = \u4e8e
gb.of = of
gb.in = in
-gb.moreChanges = \u6240\u6709\u53d8\u52a8...
+gb.moreChanges = \u6240\u6709\u53d8\u66f4...
gb.pushedNCommitsTo = \u5df2\u63a8\u9001 {0} \u6b21\u81f3
gb.pushedOneCommitTo = \u5df2\u63a8\u9001 1 \u6b21\u81f3
gb.commitsTo = {0} \u6b21\u63a8\u9001\u81f3
@@ -479,25 +477,25 @@ gb.deletedTag = \u5220\u9664\u6807\u7b7e
gb.pushedNewBranch = \u63a8\u9001\u65b0\u5206\u652f
gb.createdNewBranch = \u521b\u5efa\u65b0\u5206\u652f
gb.deletedBranch = \u5df2\u5220\u9664\u5206\u652f
-gb.createdNewPullRequest = \u521b\u5efa pull request
-gb.mergedPullRequest = \u5408\u5e76 pull request
-gb.rewind = REWIND
+gb.createdNewPullRequest = \u521b\u5efa\u62c9\u53d6\u8bf7\u6c42
+gb.mergedPullRequest = \u5408\u5e76\u62c9\u53d6\u8bf7\u6c42
+gb.rewind = \u56de\u9000
gb.star = \u5173\u6ce8
gb.unstar = \u53d6\u6d88\u5173\u6ce8
gb.stargazers = \u5173\u6ce8\u8005
gb.starredRepositories = \u5df2\u5173\u6ce8\u7248\u672c\u5e93
-gb.failedToUpdateUser = \u66f4\u65b0\u7528\u6237\u8d26\u6237\u4fe1\u606f\u5931\u8d25!
+gb.failedToUpdateUser = \u66f4\u65b0\u7528\u6237\u4fe1\u606f\u5931\u8d25
gb.myRepositories = \u6211\u7684\u7248\u672c\u5e93
gb.noActivity = \u6700\u8fd1 {0} \u5929\u5185\u6ca1\u6709\u4efb\u4f55\u6d3b\u52a8
gb.findSomeRepositories = \u5bfb\u627e\u7248\u672c\u5e93
-gb.metricAuthorExclusions = author metric exclusions
-gb.myDashboard = \u6211\u7684\u516c\u544a\u677f
-gb.failedToFindAccount = \u5bfb\u627e\u8d26\u6237 ''{0}'' \u5931\u8d25
+gb.metricAuthorExclusions = \u7edf\u8ba1\u65f6\u6392\u9664\u6d3b\u8dc3\u8d26\u6237
+gb.myDashboard = \u6211\u7684\u4eea\u8868\u76d8
+gb.failedToFindAccount = \u672a\u627e\u5230\u8d26\u6237 ''{0}''
gb.reflog = \u64cd\u4f5c\u8bb0\u5f55
gb.active = \u6d3b\u52a8
gb.starred = \u5df2\u5173\u6ce8
-gb.owned = \u62e5\u6709\u7740
-gb.starredAndOwned = \u5df2\u5173\u6ce8 & \u62e5\u6709\u7740
+gb.owned = \u62e5\u6709
+gb.starredAndOwned = \u5df2\u5173\u6ce8 & \u6240\u6709
gb.reviewPatchset = \u4fee\u8ba2 {0} \u8865\u4e01\u96c6 {1}
gb.todaysActivityStats = \u4eca\u5929 / \u6765\u81ea {2} \u7684 {1} \u6b21\u63d0\u4ea4
gb.todaysActivityNone = \u4eca\u5929 / \u65e0
@@ -506,21 +504,21 @@ gb.anonymousUser = \u533f\u540d
gb.commitMessageRenderer = \u63d0\u4ea4\u4fe1\u606f\u4fee\u9970\u5668
gb.diffStat = {0} \u6b21\u63d2\u5165 & {1} \u6b21\u5220\u9664
gb.home = \u4e3b\u9875
-gb.isMirror = \u5f53\u524d\u7248\u672c\u662f\u4e00\u4e2a\u955c\u50cf
+gb.isMirror = \u5f53\u524d\u7248\u672c\u5e93\u662f\u955c\u50cf
gb.mirrorOf = {0} \u7684\u955c\u50cf
gb.mirrorWarning = \u5f53\u524d\u7248\u672c\u5e93\u662f\u955c\u50cf\uff0c\u65e0\u6cd5\u63a5\u53d7\u63a8\u9001
gb.docsWelcome1 = \u4f60\u53ef\u4ee5\u4f7f\u7528\u6587\u6863\u6587\u4ef6\u7ed9\u60a8\u7684\u7248\u672c\u5e93\u6dfb\u52a0\u6587\u6863\u3002
gb.docsWelcome2 = \u63d0\u4ea4\u4e00\u4e2a README.md \u6216\u8005\u4e00\u4e2a HOME.md \u6765\u5f00\u59cb\u3002
-gb.createReadme = \u521b\u5efa\u4e00\u4e2a README
+gb.createReadme = \u521b\u5efa\u4e00\u4e2a README \u6587\u4ef6
gb.responsible = \u8d1f\u8d23\u4eba
-gb.createdThisTicket = \u521b\u5efa\u5f53\u524d\u5de5\u5355
+gb.createdThisTicket = \u521b\u5efa\u5f53\u524d\u4efb\u52a1
gb.proposedThisChange = \u63d0\u4ea4\u5f53\u524d\u4fee\u6539
gb.uploadedPatchsetN = \u4e0a\u4f20\u8865\u4e01\u96c6 {0}
gb.uploadedPatchsetNRevisionN = \u4e0a\u6b21\u8865\u4e01\u96c6 {0} \u4fee\u8ba2 {1}
gb.mergedPatchset = \u5df2\u5408\u5e76\u8865\u4e01\u96c6
gb.commented = \u5df2\u8bc4\u8bba
gb.noDescriptionGiven = \u65e0\u63cf\u8ff0
-gb.toBranch = \u5230 {0}
+gb.toBranch = \u5230\u5206\u652f {0}
gb.createdBy = \u521b\u5efa\u8005
gb.oneParticipant = {0} \u4e2a\u53c2\u4e0e\u8005
gb.nParticipants = {0} \u4e2a\u53c2\u4e0e\u8005
@@ -529,21 +527,21 @@ gb.oneComment = {0} \u6761\u8bc4\u8bba
gb.nComments = {0} \u6761\u8bc4\u8bba
gb.oneAttachment = {0} \u4e2a\u9644\u4ef6
gb.nAttachments = {0} \u4e2a\u9644\u4ef6
-gb.milestone = milestone
-gb.compareToMergeBase = \u4e0e\u5408\u5e76base\u5bf9\u6bd4
+gb.milestone = \u91cc\u7a0b\u7891
+gb.compareToMergeBase = \u548c merge base \u5bf9\u6bd4
gb.compareToN = \u548c {0} \u5bf9\u6bd4
gb.open = \u5f00\u542f
gb.closed = \u5df2\u5173\u95ed
gb.merged = \u5df2\u5408\u5e76
-gb.ticketPatchset = \u5de5\u5355 {0}, \u8865\u4e01\u96c6 {1}
+gb.ticketPatchset = \u4efb\u52a1 {0}, \u8865\u4e01\u96c6 {1}
gb.patchsetMergeable = \u5f53\u524d\u8865\u4e01\u96c6\u53ef\u4ee5\u88ab\u81ea\u52a8\u5408\u5e76\u81f3 {0}\u3002
-gb.patchsetMergeableMore = \u5f53\u524d\u8865\u4e01\u96c6\u4e5f\u53ef\u4ee5\u7528\u547d\u4ee4\u884c\u5408\u5e76\u81f3 {0} \u3002
-gb.patchsetAlreadyMerged = \u5f53\u524d\u8865\u4e01\u96c6\u5df2\u88ab\u5408\u5e76\u81f3 {0}.
-gb.patchsetNotMergeable = \u5f53\u524d\u8865\u4e01\u96c6\u65e0\u6cd5\u88ab\u81ea\u52a8\u5408\u5e76\u81f3 {0}.
-gb.patchsetNotMergeableMore = \u5f53\u524d\u8865\u4e01\u96c6\u5fc5\u987brebase\u6216\u8005\u624b\u52a8\u5408\u5e76\u81f3 {0} \u4ee5\u89e3\u51b3\u51b2\u7a81\u3002
+gb.patchsetMergeableMore = \u5f53\u524d\u8865\u4e01\u96c6\u4e5f\u53ef\u4ee5\u7528\u547d\u4ee4\u884c\u5408\u5e76\u81f3 {0}\u3002
+gb.patchsetAlreadyMerged = \u5f53\u524d\u8865\u4e01\u96c6\u5df2\u88ab\u5408\u5e76\u81f3 {0}\u3002
+gb.patchsetNotMergeable = \u5f53\u524d\u8865\u4e01\u96c6\u65e0\u6cd5\u88ab\u81ea\u52a8\u5408\u5e76\u81f3 {0}\u3002
+gb.patchsetNotMergeableMore = \u5f53\u524d\u8865\u4e01\u96c6\u5fc5\u987b\u53d8\u57fa (rebase) \u6216\u8005\u624b\u52a8\u5408\u5e76\u81f3 {0} \u4ee5\u89e3\u51b3\u51b2\u7a81\u3002
gb.patchsetNotApproved = \u5f53\u524d\u8865\u4e01\u96c6\u672a\u88ab\u5141\u8bb8\u5408\u5e76\u81f3 {0}\u3002
-gb.patchsetNotApprovedMore = \u5fc5\u987b\u6709\u4fee\u8ba2\u8005\u786e\u8ba4\u6b64\u8865\u4e01\u96c6\u3002
-gb.patchsetVetoedMore = \u5df2\u6709\u4fee\u8ba2\u8005\u62d2\u7edd\u4e86\u6b64\u8865\u4e01\u96c6.
+gb.patchsetNotApprovedMore = \u5fc5\u987b\u6709\u4fee\u8ba2\u8005\u6279\u51c6\u6b64\u8865\u4e01\u96c6\u3002
+gb.patchsetVetoedMore = \u5df2\u6709\u4fee\u8ba2\u8005\u62d2\u7edd\u6b64\u8865\u4e01\u96c6\u3002
gb.write = \u64b0\u5199
gb.comment = \u8bc4\u8bba
gb.preview = \u9884\u89c8
@@ -551,10 +549,10 @@ gb.leaveComment = \u8bc4\u8bba\u4e00\u4e0b...
gb.showHideDetails = \u663e\u793a/\u9690\u85cf \u8be6\u7ec6\u5185\u5bb9
gb.acceptNewPatchsets = \u63a5\u53d7\u8865\u4e01\u96c6
gb.acceptNewPatchsetsDescription = \u63a5\u53d7\u63a8\u9001\u5230\u5f53\u524d\u7248\u672c\u5e93\u7684\u8865\u4e01\u96c6
-gb.acceptNewTickets = \u5141\u8bb8\u521b\u5efa\u5de5\u5355
-gb.acceptNewTicketsDescription = \u5141\u8bb8\u521b\u5efa\u5173\u4e8ebug, \u6539\u8fdb, \u4efb\u52a1\u76f8\u5173\u7684\u5de5\u5355
-gb.requireApproval = \u9700\u8981\u786e\u8ba4
-gb.requireApprovalDescription = \u5408\u5e76\u6309\u94ae\u53ea\u6709\u5728\u8865\u4e01\u96c6\u88ab\u786e\u8ba4\u540e\u624d\u4f1a\u88ab\u6fc0\u6d3b
+gb.acceptNewTickets = \u5141\u8bb8\u521b\u5efa\u4efb\u52a1
+gb.acceptNewTicketsDescription = \u5141\u8bb8\u521b\u5efa\u5173\u4e8e bug\uff0c\u6539\u8fdb\uff0c\u4efb\u52a1\u76f8\u5173\u7684\u4efb\u52a1
+gb.requireApproval = \u9700\u8981\u6279\u51c6
+gb.requireApprovalDescription = \u5408\u5e76\u6309\u94ae\u53ea\u6709\u5728\u8865\u4e01\u96c6\u88ab\u6279\u51c6\u540e\u624d\u53ef\u7528
gb.topic = \u4e3b\u9898
gb.proposalTickets = \u5df2\u53d1\u5e03\u5efa\u8bae
gb.bugTickets = bugs
@@ -578,57 +576,57 @@ gb.sortLeastPatchsetRevisions = \u6700\u5c11\u8865\u4e01\u96c6
gb.sortMostVotes = \u6700\u591a\u6295\u7968
gb.sortLeastVotes = \u6700\u5c11\u6295\u7968
gb.topicsAndLabels = \u4e3b\u9898\u4e0e\u6807\u7b7e
-gb.milestones = milestones
-gb.noMilestoneSelected = \u6ca1\u6709\u88ab\u9009\u4e2d\u7684milestone
+gb.milestones = \u91cc\u7a0b\u7891
+gb.noMilestoneSelected = \u6ca1\u6709\u88ab\u9009\u4e2d\u7684\u91cc\u7a0b\u7891
gb.notSpecified = \u672a\u6307\u5b9a
gb.due = \u622a\u6b62\u65e5\u671f
gb.queries = \u67e5\u8be2
-gb.searchTicketsTooltip = \u641c\u7d22 {0} \u4e2a\u516c\u9053
-gb.searchTickets = \u641c\u7d22\u5de5\u5355
+gb.searchTicketsTooltip = \u641c\u7d22 {0} \u4e2a\u4efb\u52a1
+gb.searchTickets = \u641c\u7d22\u4efb\u52a1
gb.new = \u65b0\u5efa
-gb.newTicket = \u65b0\u5efa\u5de5\u5355
-gb.editTicket = \u7f16\u8f91\u5de5\u5355
-gb.ticketsWelcome = \u4f60\u53ef\u4ee5\u4f7f\u7528\u5de5\u5355\u7ba1\u7406\u60a8\u7684todo list, \u8ba8\u8bbabugs, \u5408\u4f5c\u5236\u4f5c\u8865\u4e01\u96c6\u3002
-gb.createFirstTicket = \u521b\u5efa\u60a8\u7684\u7b2c\u4e00\u4e2a\u5de5\u5355
+gb.newTicket = \u65b0\u5efa\u4efb\u52a1
+gb.editTicket = \u7f16\u8f91\u4efb\u52a1
+gb.ticketsWelcome = \u4f60\u53ef\u4ee5\u4f7f\u7528\u4efb\u52a1\u7ba1\u7406\u60a8\u7684\u5f85\u529e\u4e8b\u9879\uff0c\u8ba8\u8bba bugs\uff0c\u5408\u4f5c\u5236\u4f5c\u8865\u4e01\u96c6\u3002
+gb.createFirstTicket = \u521b\u5efa\u60a8\u7684\u7b2c\u4e00\u4e2a\u4efb\u52a1
gb.title = \u6807\u9898
gb.changedStatus = \u4fee\u6539\u72b6\u6001
gb.discussion = \u8ba8\u8bba
gb.updated = \u5df2\u66f4\u65b0
gb.proposePatchset = \u63d0\u4ea4\u4e00\u4e2a\u8865\u4e01\u96c6
-gb.proposePatchsetNote = \u6b22\u8fce\u60a8\u4e3a\u5f53\u524d\u5de5\u5355\u63d0\u4ea4\u8865\u4e01\u96c6\u3002
-gb.proposeInstructions = \u9996\u5148\u521b\u5efa\u4e00\u4e2a\u8865\u4e01\u96c6\uff0c\u7136\u540e\u7528Git\u4e0a\u4f20\u3002 Gitblit \u4f1a\u81ea\u52a8\u901a\u8fc7id\u5c06\u8865\u4e01\u96c6\u4e0e\u5f53\u524dticket\u76f8\u8fde\u63a5\u3002
+gb.proposePatchsetNote = \u6b22\u8fce\u4e3a\u5f53\u524d\u4efb\u52a1\u63d0\u4ea4\u8865\u4e01\u96c6\u3002
+gb.proposeInstructions = \u9996\u5148\u521b\u5efa\u4e00\u4e2a\u8865\u4e01\u96c6\uff0c\u7136\u540e\u7528 Git \u4e0a\u4f20\u3002Gitblit \u4f1a\u81ea\u52a8\u901a\u8fc7 ID \u5c06\u8865\u4e01\u96c6\u4e0e\u5f53\u524d\u7684\u4efb\u52a1\u76f8\u5173\u8054\u3002
gb.proposeWith = \u5bf9 {0} \u63d0\u4ea4\u4e00\u4e2a\u8865\u4e01\u96c6
gb.revisionHistory = \u4fee\u8ba2\u5386\u53f2
gb.merge = \u5408\u5e76
gb.action = \u52a8\u4f5c
gb.patchset = \u8865\u4e01\u96c6
gb.all = \u6240\u6709
-gb.mergeBase = \u5408\u5e76\u57fa\u7840
-gb.checkout = \u68c0\u51fa
-gb.checkoutViaCommandLine = \u901a\u8fc7\u547d\u4ee4\u884c\u68c0\u51fa
-gb.checkoutViaCommandLineNote = \u60a8\u53ef\u4ee5\u68c0\u51fa\u7136\u540e\u5728\u672c\u5730\u4f7f\u7528\u514b\u9686\u6d4b\u8bd5\u4fee\u6539\u7684\u5185\u5bb9\u3002
-gb.checkoutStep1 = \u83b7\u53d6\u5f53\u524d\u8865\u4e01\u96c6 \u2014 \u7136\u540e\u5728\u60a8\u7684\u672c\u5730\u9879\u76ee\u76ee\u5f55\u8fd0\u884c
-gb.checkoutStep2 = \u5c06\u8865\u4e01\u96c6\u68c0\u51fa\u81f3\u65b0\u7684\u5206\u652f\u4ee5\u8fdb\u884c\u4fee\u8ba2
+gb.mergeBase = \u5408\u5e76 base
+gb.checkout = \u68c0\u51fa (checkout)
+gb.checkoutViaCommandLine = \u901a\u8fc7\u547d\u4ee4\u884c\u68c0\u51fa (checkout)
+gb.checkoutViaCommandLineNote = \u60a8\u53ef\u4ee5\u68c0\u51fa (checkout) \u7136\u540e\u5728\u672c\u5730\u4f7f\u7528\u514b\u9686\u7684\u7248\u672c\u5e93\u6d4b\u8bd5\u4fee\u6539\u7684\u5185\u5bb9\u3002
+gb.checkoutStep1 = \u83b7\u53d6\u5f53\u524d\u8865\u4e01\u96c6 \u2014 \u7136\u540e\u5728\u60a8\u7684\u672c\u5730\u9879\u76ee\u4e2d\u8fd0\u884c
+gb.checkoutStep2 = \u5c06\u8865\u4e01\u96c6\u68c0\u51fa (checkout) \u81f3\u65b0\u7684\u5206\u652f\u4ee5\u8fdb\u884c\u4fee\u8ba2
gb.mergingViaCommandLine = \u4f7f\u7528\u547d\u4ee4\u884c\u5408\u5e76
gb.mergingViaCommandLineNote = \u5982\u679c\u60a8\u4e0d\u60f3\u4f7f\u7528\u5408\u5e76\u6309\u94ae\uff0c\u6216\u8005\u65e0\u6cd5\u81ea\u52a8\u5408\u5e76\uff0c\u60a8\u53ef\u4ee5\u4f7f\u7528\u547d\u4ee4\u884c\u624b\u52a8\u5408\u5e76
-gb.mergeStep1 = \u68c0\u51fa\u4e00\u4e2a\u65b0\u7684\u5206\u652f\u4ee5\u4fee\u8ba2\u6539\u52a8 \u2014 \u7136\u540e\u5728\u60a8\u7684\u672c\u5730\u9879\u76ee\u76ee\u5f55\u8fd0\u884c
+gb.mergeStep1 = \u68c0\u51fa (checkout) \u4e00\u4e2a\u65b0\u7684\u5206\u652f\u4ee5\u4fee\u8ba2\u6539\u52a8 \u2014 \u7136\u540e\u5728\u60a8\u7684\u672c\u5730\u9879\u76ee\u4e2d\u8fd0\u884c
gb.mergeStep2 = \u5f15\u5165\u5df2\u63d0\u4ea4\u4fee\u6539\u540e\u4fee\u8ba2
gb.mergeStep3 = \u5408\u5e76\u5df2\u63d0\u4ea4\u7684\u4fee\u6539\u7136\u540e\u66f4\u65b0\u670d\u52a1\u5668
gb.download = \u4e0b\u8f7d
gb.ptDescription = Gitblit \u8865\u4e01\u96c6\u5de5\u5177
gb.ptCheckout = \u83b7\u53d6\u548c\u68c0\u51fa\u5f53\u524d\u8865\u4e01\u96c6\u5230\u4e00\u4e2a\u4fee\u8ba2\u5206\u652f
gb.ptMerge = \u83b7\u53d6\u548c\u68c0\u51fa\u5f53\u524d\u8865\u4e01\u96c6\u5230\u60a8\u7684\u672c\u5730\u5206\u652f
-gb.ptDescription1 = Barnum \u662f\u4e00\u4e2aGit\u7684\u547d\u4ee4\u884c\u5de5\u5177\uff0c\u5b83\u53ef\u4ee5\u7b80\u5316\u4e0eGitblit\u5de5\u5355\u548c\u8865\u4e01\u96c6\u534f\u4f5c\u7684\u6b65\u9aa4\u3002
+gb.ptDescription1 = Barnum \u662f\u4e00\u4e2a Git \u7684\u547d\u4ee4\u884c\u5de5\u5177\uff0c\u5b83\u53ef\u4ee5\u7b80\u5316\u4e0e Gitblit \u4efb\u52a1\u548c\u8865\u4e01\u96c6\u534f\u4f5c\u7684\u6b65\u9aa4\u3002
gb.ptSimplifiedCollaboration = \u7b80\u5316\u534f\u4f5c\u683c\u5f0f
gb.ptSimplifiedMerge = \u7b80\u5316\u5408\u5e76\u683c\u5f0f
-gb.ptDescription2 = Barnum \u9700\u8981 Python 3 \u4ee5\u53ca\u672c\u5730\u5b89\u88c5Git. \u5b83\u53ef\u4ee5\u5de5\u4f5c\u5728 Windows, Linux, \u4ee5\u53ca Mac OS X\u4e0a\u9762\u3002
+gb.ptDescription2 = Barnum \u9700\u8981 Python 3 \u4ee5\u53ca\u672c\u5730\u5b89\u88c5 Git\u3002\u5b83\u53ef\u4ee5\u8fd0\u884c\u5728 Windows, Linux, \u4ee5\u53ca macOS \u4e0a\u3002
gb.stepN = \u7b2c {0} \u6b65
gb.watchers = \u5173\u6ce8\u8005
gb.votes = \u6295\u7968
gb.vote = \u5bf9\u5f53\u524d {0} \u6295\u7968
gb.watch = \u5173\u6ce8 {0}
gb.removeVote = \u5220\u9664\u6295\u7968
-gb.stopWatching = \u505c\u6b62\u5173\u6ce8
+gb.stopWatching = \u53d6\u6d88\u5173\u6ce8
gb.watching = \u6b63\u5728\u5173\u6ce8
gb.comments = \u8bc4\u8bba
gb.addComment = \u6dfb\u52a0\u8bc4\u8bba
@@ -646,54 +644,54 @@ gb.veto = \u5426\u51b3
gb.needsImprovement = \u9700\u8981\u6539\u8fdb
gb.looksGood = \u770b\u8d77\u6765\u4e0d\u9519
gb.approve = \u540c\u610f
-gb.hasNotReviewed = \u672a\u88ab\u5ba1\u67e5\u8fc7
+gb.hasNotReviewed = \u672a\u7ecf\u5ba1\u67e5
gb.about = \u5173\u4e8e
-gb.ticketN = \u5de5\u5355 #{0}
-gb.disableUser = \u7981\u6b62\u7528\u6237
-gb.disableUserDescription = \u7981\u6b62\u5f53\u524d\u8d26\u6237\u8fdb\u884c\u8ba4\u8bc1
+gb.ticketN = \u4efb\u52a1 #{0}
+gb.disableUser = \u505c\u7528\u8d26\u6237
+gb.disableUserDescription = \u8be5\u8d26\u6237\u5df2\u7ecf\u505c\u7528
gb.any = \u4efb\u610f
-gb.milestoneProgress = {0} \u5f00\u542f, {1} \u5173\u95ed
+gb.milestoneProgress = {0} \u5f00\u542f\uff0c{1} \u5173\u95ed
gb.nOpenTickets = {0} \u5f00\u542f
gb.nClosedTickets = {0} \u5173\u95ed
gb.nTotalTickets = \u603b\u8ba1 {0}
gb.body = \u5185\u5bb9
-gb.mergeSha = \u5408\u5e76SHA
+gb.mergeSha = \u5408\u5e76 SHA
gb.mergeTo = \u5408\u5e76\u5230
gb.labels = \u6807\u7b7e
-gb.reviewers = \u4fee\u8ba2\u4eba
-gb.voters = \u6295\u7968\u4eba
+gb.reviewers = \u4fee\u8ba2\u8005
+gb.voters = \u6295\u7968\u8005
gb.mentions = \u63d0\u53ca
gb.canNotProposePatchset = \u65e0\u6cd5\u63d0\u4ea4\u8865\u4e01\u96c6
-gb.repositoryIsMirror = \u5f53\u524d\u7248\u672c\u5e93\u662f\u53ea\u8bfb\u955c\u50cf\u3002
-gb.repositoryIsFrozen = \u5f53\u524d\u7248\u672c\u5e93\u5df2\u88ab\u51bb\u7ed3\u3002
-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.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.repositoryIsMirror = \u5f53\u524d\u7248\u672c\u5e93\u662f\u53ea\u8bfb\u955c\u50cf
+gb.repositoryIsFrozen = \u5f53\u524d\u7248\u672c\u5e93\u5df2\u88ab\u51bb\u7ed3
+gb.repositoryDoesNotAcceptPatchsets = \u5f53\u524d\u7248\u672c\u5e93\u4e0d\u63a5\u53d7\u8865\u4e01\u96c6
+gb.serverDoesNotAcceptPatchsets = \u5f53\u524d\u670d\u52a1\u5668\u4e0d\u63a5\u53d7\u8865\u4e01\u96c6
+gb.ticketIsClosed = \u5f53\u524d\u4efb\u52a1\u5df2\u5173\u95ed
+gb.mergeToDescription = \u7528\u4e8e\u5408\u5e76\u4efb\u52a1\u4e2d\u7684\u8865\u4e01\u96c6\u7684\u9ed8\u8ba4\u96c6\u6210\u5206\u652f
+gb.anonymousCanNotPropose = \u7981\u6b62\u533f\u540d\u7528\u6237\u63d0\u4ea4\u8865\u4e01\u96c6
+gb.youDoNotHaveClonePermission = \u60a8\u6ca1\u6709\u6743\u9650\u514b\u9686\u5f53\u524d\u7248\u672c\u5e93
+gb.myTickets = \u6211\u7684\u4efb\u52a1
+gb.yourAssignedTickets = \u6307\u5b9a\u7ed9\u6211\u7684\u4efb\u52a1
+gb.newMilestone = \u521b\u5efa\u91cc\u7a0b\u7891
+gb.editMilestone = \u7f16\u8f91\u91cc\u7a0b\u7891
+gb.deleteMilestone = \u5220\u9664\u91cc\u7a0b\u7891 \"{0}\"?
+gb.milestoneDeleteFailed = \u65e0\u6cd5\u5220\u9664\u91cc\u7a0b\u7891 ''{0}''
+gb.notifyChangedOpenTickets = \u5df2\u5f00\u542f\u4efb\u52a1\u53d8\u52a8\u901a\u77e5
gb.overdue = \u8fc7\u671f
-gb.openMilestones = \u5df2\u5f00\u542f milestones
-gb.closedMilestones = \u5df2\u5173\u95ed milestones
+gb.openMilestones = \u5df2\u5f00\u542f\u7684\u91cc\u7a0b\u7891
+gb.closedMilestones = \u5df2\u5173\u95ed\u7684\u91cc\u7a0b\u7891
gb.administration = \u7ba1\u7406
gb.plugins = \u63d2\u4ef6
gb.extensions = \u6269\u5c55
-gb.pleaseSelectProject = \u8bf7\u9009\u62e9\u9879\u76ee!
+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.accessPolicyDescription = \u8bf7\u9009\u62e9\u4e00\u4e2a\u63a7\u5236\u7248\u672c\u5e93\u53ef\u89c1\u6027\u4ee5\u53ca Git \u8bbf\u95ee\u6743\u9650\u7684\u8bbf\u95ee\u7b56\u7565
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.anonymousPolicyDescription = \u4efb\u4f55\u4eba\u90fd\u53ef\u4ee5\u67e5\u770b\uff0c\u514b\u9686\u4ee5\u53ca\u63a8\u9001\u81f3\u6b64\u7248\u672c\u5e93
+gb.authenticatedPushPolicy = \u9650\u5236\u63a8\u9001\uff08\u6388\u6743\u8bbf\u95ee\uff09
+gb.authenticatedPushPolicyDescription = \u4efb\u4f55\u4eba\u90fd\u53ef\u4ee5\u67e5\u770b\u4ee5\u53ca\u514b\u9686\u6b64\u7248\u672c\u5e93\u3002\u5df2\u6388\u6743\u7528\u6237\u62e5\u6709\u8bfb\u53d6\u3001\u5199\u5165\u548c\u63a8\u9001\u6743\u9650\u3002
+gb.namedPushPolicy = \u9650\u5236\u63a8\u9001\uff08\u6307\u5b9a\u7528\u6237\uff09
+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
@@ -703,42 +701,87 @@ gb.initialCommitDescription = \u6b64\u529f\u80fd\u76f8\u5f53\u4e8e\u76f4\u63a5\u
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.initWithGitignoreDescription = \u6b64\u529f\u80fd\u4f1a\u751f\u6210\u4e00\u4e2a\u914d\u7f6e\u6587\u4ef6\uff0c\u7528\u4e8e\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.receive = \u63a5\u6536
+gb.permissions = \u6743\u9650
+gb.ownersDescription = \u7248\u672c\u5e93\u6240\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\u5355\u4e2a\u7528\u6237\u6743\u9650\u3002\u6b64\u8bbe\u7f6e\u4f1a\u8986\u76d6\u56e2\u961f\u6743\u9650\u4ee5\u53ca\u6b63\u5219\u8868\u8fbe\u5f0f\u8bbe\u7f6e\u7684\u6743\u9650\u3002
+gb.teamPermissionsDescription = \u60a8\u53ef\u4ee5\u8bbe\u7f6e\u5355\u4e2a\u56e2\u961f\u6743\u9650\u3002\u6b64\u8bbe\u7f6e\u4f1a\u8986\u76d6\u6b63\u5219\u8868\u8fbe\u5f0f\u8bbe\u7f6e\u7684\u6743\u9650\u3002
+gb.ticketSettings = \u4efb\u52a1\u8bbe\u7f6e
+gb.receiveSettings = \u63a5\u6536\u65b9\u5f0f\u8bbe\u7f6e
+gb.receiveSettingsDescription = \u8bbe\u7f6e\u63a8\u9001\u81f3\u7248\u672c\u5e93\u65f6\u7684\u63a5\u6536\u65b9\u5f0f\u3002
+gb.preReceiveDescription = Pre-receive hooks \u4f1a\u5728\u63a8\u9001\u63a5\u6536\u540e\uff0crefs \u66f4\u65b0<em>\u4e4b\u524d</em>\u6267\u884c\u3002<p>\u8fd9\u79cd hook \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\uff0crefs \u66f4\u65b0<em>\u4e4b\u540e</em>\u6267\u884c\u3002<p>\u8fd9\u79cd hook \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\u4ed6 Gitblit \u8fdb\u884c federate\u3002
+gb.federationSetsDescription = \u5f53\u524d\u7248\u672c\u5e93\u5c06\u4f1a\u88ab\u5305\u542b\u8fdb\u9009\u5b9a\u7684 federation \u96c6\u4e2d\u3002
+gb.miscellaneous = \u5176\u4ed6
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.gc = \u5783\u573e\u56de\u6536 (GC)
+gb.garbageCollection = \u5783\u573e\u56de\u6536
+gb.garbageCollectionDescription = \u5783\u573e\u6536\u96c6\u5668\u4f1a\u5c06\u5ba2\u6237\u7aef\u6240\u63a8\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\u7684 Markup \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.languagePreferenceDescription = \u9009\u62e9\u60a8\u7684\u8bed\u8a00
+gb.emailMeOnMyTicketChanges = \u5728\u6211\u7684\u4efb\u52a1\u53d1\u751f\u53d8\u66f4\u540e\u90ae\u4ef6\u901a\u77e5\u6211
+gb.emailMeOnMyTicketChangesDescription = \u5bf9\u6211\u5728\u4efb\u52a1\u4e2d\u4f5c\u51fa\u7684\u4fee\u6539\u53d1\u9001\u90ae\u4ef6\u901a\u77e5
+gb.displayNameDescription = \u8bbe\u7f6e\u663e\u793a\u540d\u79f0
+gb.emailAddressDescription = \u7528\u4e8e\u63a5\u6536\u901a\u77e5\u7684\u4e3b\u8981\u7535\u5b50\u90ae\u4ef6\u5730\u5740
+gb.sshKeys = SSH \u5bc6\u94a5
+gb.sshKeysDescription = SSH \u5bc6\u94a5\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 \u5bc6\u94a5
+gb.key = \u5bc6\u94a5
+gb.sshKeyCommentDescription = \u8f93\u5165\u8bc4\u8bba\uff08\u53ef\u4e3a\u7a7a\uff09\u3002\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 \ No newline at end of file
+gb.sshKeyPermissionDescription = \u8bbe\u7f6e SSH \u5bc6\u94a5\u7684\u8bbf\u95ee\u6743\u9650
+gb.transportPreference = \u4f20\u8f93\u504f\u597d
+gb.transportPreferenceDescription = \u9009\u62e9\u60a8\u7528\u6765\u514b\u9686\u7684\u4f20\u8f93\u65b9\u5f0f
+gb.ticketStatus = \u72b6\u6001
+gb.maintenanceTickets = \u7ef4\u62a4
+gb.mergeType = \u5408\u5e76\u7c7b\u578b
+gb.mergeTypeDescription = \u4ec5\u5728\u5fc5\u8981\u65f6\u5feb\u901f\u5408\u5e76\u4efb\u52a1\uff0c\u6216\u8005\u603b\u662f\u4e0e\u5408\u5e76\u63d0\u4ea4\u5230\u96c6\u6210\u5206\u652f
+gb.priority = \u4f18\u5148\u7ea7
+gb.severity = \u91cd\u8981\u6027
+gb.sortHighestPriority = \u6700\u9ad8\u4f18\u5148\u7ea7
+gb.sortLowestPriority = \u6700\u4f4e\u4f18\u5148\u7ea7
+gb.sortHighestSeverity = \u6700\u91cd\u8981
+gb.sortLowestSeverity = \u6700\u4e0d\u91cd\u8981
+gb.missingIntegrationBranchMore = \u76ee\u6807\u5206\u652f\u4e0d\u5728\u6b64\u7248\u672c\u5e93\u4e2d
+gb.diffDeletedFileSkipped = \uff08\u5df2\u5220\u9664\uff09
+gb.diffFileDiffTooLarge = \u6587\u4ef6\u592a\u5927
+gb.diffNewFile = \u5bf9\u6bd4\u65b0\u6587\u4ef6
+gb.diffDeletedFile = \u6587\u4ef6\u5df2\u5220\u9664
+gb.diffRenamedFile = \u6587\u4ef6\u540d\u4ece {0} \u4fee\u6539
+gb.diffCopiedFile = \u6587\u4ef6\u4ece {0} \u590d\u5236
+gb.diffTruncated = \u5728\u4e0a\u8ff0\u6587\u4ef6\u622a\u65ad\u540e\u5bf9\u6bd4
+gb.opacityAdjust = \u8c03\u6574\u900f\u660e\u5ea6
+gb.blinkComparator = \u7728\u773c\u6bd4\u8f83\u5668
+gb.imgdiffSubtract = \u76f8\u51cf\uff08\u9ed1\u8272\u4e3a\u76f8\u540c\uff09
+gb.deleteRepositoryHeader = \u5220\u9664\u7248\u672c\u5e93
+gb.deleteRepositoryDescription = \u5220\u9664\u7248\u672c\u5e93\u5c06\u4e0d\u53ef\u64a4\u9500\u3002
+gb.show_whitespace = \u663e\u793a\u7a7a\u767d
+gb.ignore_whitespace = \u5ffd\u7565\u7a7a\u767d
+gb.allRepositories = \u6240\u6709\u7248\u672c\u5e93
+gb.oid = \u5bf9\u8c61 ID
+gb.filestore = \u6587\u4ef6\u5b58\u50a8
+gb.filestoreStats = \u5df2\u5b58\u50a8 {0} \u4e2a\u6587\u4ef6\uff0c\u5360\u7528\u7a7a\u95f4 {1}\u3002 (\u53ef\u7528\u7a7a\u95f4 {2})
+gb.statusChangedOn = \u4fee\u6539\u65e5\u671f
+gb.statusChangedBy = \u4fee\u6539\u8005
+gb.filestoreHelp = \u8fdb\u4e00\u6b65\u4e86\u89e3\u6587\u4ef6\u5b58\u50a8\u529f\u80fd
+gb.editFile = \u7f16\u8f91\u6587\u4ef6
+gb.continueEditing = \u7ee7\u7eed\u7f16\u8f91
+gb.commitChanges = \u63d0\u4ea4\u66f4\u6539
+gb.fileNotMergeable = \u65e0\u6cd5\u63d0\u4ea4\u66f4\u6539 {0}\u3002\u6b64\u6587\u4ef6\u65e0\u6cd5\u81ea\u52a8\u5408\u5e76\u3002
+gb.fileCommitted = \u6210\u529f\u63d0\u4ea4\u66f4\u6539 {0}\u3002
+gb.deletePatchset = \u5220\u9664\u8865\u4e01\u96c6 {0}
+gb.deletePatchsetSuccess = \u5df2\u5220\u9664\u8865\u4e01\u96c6 {0}\u3002
+gb.deletePatchsetFailure = \u5220\u9664\u8865\u4e01\u96c6 {0} \u65f6\u53d1\u751f\u9519\u8bef\u3002
+gb.referencedByCommit = \u88ab\u63d0\u4ea4\u5f15\u7528\u3002
+gb.referencedByTicket = \u88ab\u4efb\u52a1\u5f15\u7528\u3002
+gb.emailClientCertificateSubject = \u7528\u4e8e {0} \u7684 Gitblit \u5ba2\u6237\u7aef\u8bc1\u4e66
+
+# This last property for unit tests to test successful loading of the resource file
+gb.loadLang = \u6c49\u5b57
diff --git a/src/main/java/com/gitblit/wicket/GitBlitWebApp_zh_TW.properties b/src/main/java/com/gitblit/wicket/GitBlitWebApp_zh_TW.properties
index bf2d2c32..337a18d5 100644
--- a/src/main/java/com/gitblit/wicket/GitBlitWebApp_zh_TW.properties
+++ b/src/main/java/com/gitblit/wicket/GitBlitWebApp_zh_TW.properties
@@ -219,7 +219,8 @@ gb.pages = \u6587\u4ef6
gb.workingCopy = \u66ab\u5b58\u8907\u672c
gb.workingCopyWarning = \u8a72\u7248\u672c\u5eab\u4ecd\u6709\u66ab\u5b58\u8907\u672c,\u56e0\u6b64\u7121\u6cd5\u63a5\u53d7\u63a8\u9001(push)
gb.query = \u67e5\u8a62
-gb.queryHelp = \u652f\u63f4\u6a19\u6e96\u67e5\u8a62\u8a9e\u6cd5.<p/><p/>\u8a73\u60c5\u8acb\u53c3\u8003 <a target\ = "_new" href\ = "http\://lucene.apache.org/core/old_versioned_docs/versions/3_5_0/queryparsersyntax.html">Lucene Query Parser Syntax</a>
+gb.queryHelp = \u652f\u63f4\u6a19\u6e96\u67e5\u8a62\u8a9e\u6cd5.<p/><p/>\u8a73\u60c5\u8acb\u53c3\u8003 ${querySyntax}
+gb.querySyntax = Lucene Query Parser Syntax
gb.queryResults = \u7d50\u679c {0} - {1} ({2} \u67e5\u8a62)
gb.noHits = \u7121\u9ede\u64ca
gb.authored = \u6388\u6b0a
@@ -781,3 +782,7 @@ gb.deletePatchsetSuccess = \u5df2\u522a\u9664 Patchset {0}.
gb.deletePatchsetFailure = \u522a\u9664 Patchset {0} \u932f\u8aa4.
gb.referencedByCommit = Referenced by commit.
gb.referencedByTicket = Referenced by ticket.
+gb.emailClientCertificateSubject = \u4F3A\u670D\u5668 {0} \u9023\u7DDA\u6191\u8B49
+
+# This last property for unit tests to test successful loading of the resource file
+gb.loadLang = \u6f22\u5b57
diff --git a/src/main/java/com/gitblit/wicket/MarkupProcessor.java b/src/main/java/com/gitblit/wicket/MarkupProcessor.java
index b2032049..90f3808b 100644
--- a/src/main/java/com/gitblit/wicket/MarkupProcessor.java
+++ b/src/main/java/com/gitblit/wicket/MarkupProcessor.java
@@ -19,8 +19,6 @@ import static org.pegdown.FastEncoder.encode;
import java.io.Serializable;
import java.io.StringWriter;
-import java.io.UnsupportedEncodingException;
-import java.net.URLEncoder;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
@@ -47,7 +45,9 @@ import org.pegdown.LinkRenderer;
import org.pegdown.ToHtmlSerializer;
import org.pegdown.VerbatimSerializer;
import org.pegdown.ast.ExpImageNode;
+import org.pegdown.ast.ExpLinkNode;
import org.pegdown.ast.RefImageNode;
+import org.pegdown.ast.RefLinkNode;
import org.pegdown.ast.WikiLinkNode;
import org.pegdown.plugins.ToHtmlSerializerPlugin;
import org.slf4j.Logger;
@@ -265,7 +265,7 @@ public class MarkupProcessor {
@Override
public void image(Attributes attributes, String imagePath) {
String url;
- if (imagePath.indexOf("://") == -1) {
+ if (! isFullUrl(imagePath)) {
// relative image
String path = doc.getRelativePath(imagePath);
String contextUrl = RequestCycle.get().getRequest().getRelativePathPrefixToContextRoot();
@@ -281,7 +281,7 @@ public class MarkupProcessor {
public void link(Attributes attributes, String hrefOrHashName, String text) {
String url;
if (hrefOrHashName.charAt(0) != '#') {
- if (hrefOrHashName.indexOf("://") == -1) {
+ if (! isFullUrl(hrefOrHashName)) {
// relative link
String path = doc.getRelativePath(hrefOrHashName);
url = getWicketUrl(DocPage.class, repositoryName, commitId, path);
@@ -322,7 +322,7 @@ public class MarkupProcessor {
@Override
public Rendering render(ExpImageNode node, String text) {
- if (node.url.indexOf("://") == -1) {
+ if (! isFullUrl(node.url)) {
// repository-relative image link
String path = doc.getRelativePath(node.url);
String contextUrl = RequestCycle.get().getRequest().getRelativePathPrefixToContextRoot();
@@ -336,7 +336,7 @@ public class MarkupProcessor {
@Override
public Rendering render(RefImageNode node, String url, String title, String alt) {
Rendering rendering;
- if (url.indexOf("://") == -1) {
+ if (! isFullUrl(url)) {
// repository-relative image link
String path = doc.getRelativePath(url);
String contextUrl = RequestCycle.get().getRequest().getRelativePathPrefixToContextRoot();
@@ -356,6 +356,37 @@ public class MarkupProcessor {
String url = getWicketUrl(DocPage.class, repositoryName, commitId, path);
return new Rendering(url, name);
}
+
+ @Override
+ public Rendering render(ExpLinkNode node, String text) {
+ // Relative file-like MD links needs to be re-mapped to be relative to
+ // repository name so that they display correctly sub-folder files
+ // Absolute links must be left un-touched.
+
+ // Note: The absolute lack of comments in ExpLinkNode is... well...
+ // I assume, that getRelativePath is handling "file like" links
+ // like "/xx/tt" or "../somefolder".
+ if (isFullUrl(node.url)) {
+ // This is URL, fallback to superclass.
+ return super.render(node,text);
+ }
+ // repository-relative link
+ String path = doc.getRelativePath(node.url);
+ String url = getWicketUrl(DocPage.class, repositoryName, commitId, path);
+ return new Rendering(url, text);
+ }
+
+ @Override
+ public Rendering render(RefLinkNode node, String url, String title, String text) {
+ if (isFullUrl(url)) {
+ // This is URL, fallback to superclass.
+ return super.render(node, url, title, text);
+ }
+ // repository-relative link
+ String path = doc.getRelativePath(url);
+ String local_url = getWicketUrl(DocPage.class, repositoryName, commitId, path);
+ return super.render(node, local_url, title, text);
+ }
};
final String content = MarkdownUtils.transformMarkdown(doc.markup, renderer);
@@ -364,14 +395,27 @@ public class MarkupProcessor {
doc.html = safeContent;
}
+ private boolean isFullUrl(String url)
+ {
+ // Relative file-like links needs to be re-mapped to be relative to
+ // repository name so that they display correctly sub-folder files
+ // Absolute links must be left un-touched.
+ // Check if given string is a full URL link. The easiest is to ask java to parse URL
+ // and let it fail. Shame java.net.URL has no method to validate URL without
+ // throwing.
+ try {
+ new java.net.URL(url);
+ // Success, this is a URL
+ return true;
+ } catch (java.net.MalformedURLException ignored) {};
+ // This is a relative link
+ return false;
+ }
+
+
private String getWicketUrl(Class<? extends Page> pageClass, final String repositoryName, final String commitId, final String document) {
String fsc = settings.getString(Keys.web.forwardSlashCharacter, "/");
String encodedPath = document.replace(' ', '-');
- try {
- encodedPath = URLEncoder.encode(encodedPath, "UTF-8");
- } catch (UnsupportedEncodingException e) {
- logger.error(null, e);
- }
encodedPath = encodedPath.replace("/", fsc).replace("%2F", fsc);
String url = RequestCycle.get().urlFor(pageClass, WicketUtils.newPathParameter(repositoryName, commitId, encodedPath)).toString();
diff --git a/src/main/java/com/gitblit/wicket/SessionlessForm.java b/src/main/java/com/gitblit/wicket/SessionlessForm.java
index 6f790717..c53f095e 100644
--- a/src/main/java/com/gitblit/wicket/SessionlessForm.java
+++ b/src/main/java/com/gitblit/wicket/SessionlessForm.java
@@ -58,7 +58,7 @@ public class SessionlessForm<T> extends StatelessForm<T> {
protected final PageParameters pageParameters;
- private final Logger log = LoggerFactory.getLogger(SessionlessForm.class);
+ private transient Logger logger;
/**
* Sessionless forms must have a bookmarkable page class. A bookmarkable
@@ -118,7 +118,10 @@ public class SessionlessForm<T> extends StatelessForm<T> {
if (c != null) {
// this form has a field id which matches the
// parameter name, skip embedding a hidden value
- log.warn(MessageFormat.format("Skipping page parameter \"{0}\" from sessionless form hidden fields because it collides with a form child wicket:id", key));
+ logger().warn(
+ MessageFormat
+ .format("Skipping page parameter \"{0}\" from sessionless form hidden fields because it collides with a form child wicket:id",
+ key));
continue;
}
String value = pageParameters.getString(key);
@@ -156,4 +159,11 @@ public class SessionlessForm<T> extends StatelessForm<T> {
String absoluteUrl = RequestUtils.toAbsolutePath(relativeUrl);
return absoluteUrl;
}
+
+ private Logger logger() {
+ if (logger == null) {
+ logger = LoggerFactory.getLogger(SessionlessForm.class);
+ }
+ return logger;
+ }
}
diff --git a/src/main/java/com/gitblit/wicket/pages/BasePage.html b/src/main/java/com/gitblit/wicket/pages/BasePage.html
index b998428c..49810ddc 100644
--- a/src/main/java/com/gitblit/wicket/pages/BasePage.html
+++ b/src/main/java/com/gitblit/wicket/pages/BasePage.html
@@ -17,6 +17,7 @@
<link rel="stylesheet" href="fontawesome/css/font-awesome.min.css"/>
<link rel="stylesheet" href="octicons/octicons.css"/>
<link rel="stylesheet" type="text/css" href="gitblit.css"/>
+ <link rel="stylesheet" type="text/css" href="bootstrap-fixes.css"/>
</wicket:head>
<body>
@@ -50,7 +51,8 @@
<!-- Include scripts at end for faster page loading -->
<script type="text/javascript" src="bootstrap/js/jquery.js"></script>
- <script type="text/javascript" src="bootstrap/js/bootstrap.js"></script>
+ <script type="text/javascript" src="bootstrap/js/bootstrap.js"></script>
+ <script type="text/javascript" src="gitblit/js/collapsible-table.js"></script>
<wicket:container wicket:id="bottomScripts"></wicket:container>
</body>
</html> \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/BlamePage.java b/src/main/java/com/gitblit/wicket/pages/BlamePage.java
index 2fcca0ae..8465058f 100644
--- a/src/main/java/com/gitblit/wicket/pages/BlamePage.java
+++ b/src/main/java/com/gitblit/wicket/pages/BlamePage.java
@@ -108,7 +108,7 @@ public class BlamePage extends RepositoryPage {
if (pathModel == null) {
final String notFound = MessageFormat.format("Blame page failed to find {0} in {1} @ {2}",
blobPath, repositoryName, objectId);
- logger.error(notFound);
+ logger().error(notFound);
add(new Label("annotation").setVisible(false));
add(new Label("missingBlob", missingBlob(blobPath, commit)).setEscapeModelStrings(false));
return;
diff --git a/src/main/java/com/gitblit/wicket/pages/ChangePasswordPage.java b/src/main/java/com/gitblit/wicket/pages/ChangePasswordPage.java
index 259a4bf4..53c3e14d 100644
--- a/src/main/java/com/gitblit/wicket/pages/ChangePasswordPage.java
+++ b/src/main/java/com/gitblit/wicket/pages/ChangePasswordPage.java
@@ -28,14 +28,14 @@ import org.apache.wicket.protocol.http.WebResponse;
import com.gitblit.GitBlitException;
import com.gitblit.Keys;
import com.gitblit.models.UserModel;
-import com.gitblit.utils.StringUtils;
+import com.gitblit.utils.PasswordHash;
import com.gitblit.wicket.GitBlitWebSession;
import com.gitblit.wicket.NonTrimmedPasswordTextField;
public class ChangePasswordPage extends RootSubPage {
- IModel<String> password = new Model<String>("");
- IModel<String> confirmPassword = new Model<String>("");
+ private IModel<String> password = new Model<String>("");
+ private IModel<String> confirmPassword = new Model<String>("");
public ChangePasswordPage() {
super();
@@ -85,15 +85,11 @@ public class ChangePasswordPage extends RootSubPage {
UserModel user = GitBlitWebSession.get().getUser();
- // convert to MD5 digest, if appropriate
- String type = app().settings().getString(Keys.realm.passwordStorage, "md5");
- if (type.equalsIgnoreCase("md5")) {
- // store MD5 digest of password
- password = StringUtils.MD5_TYPE + StringUtils.getMD5(password);
- } else if (type.equalsIgnoreCase("combined-md5")) {
- // store MD5 digest of username+password
- password = StringUtils.COMBINED_MD5_TYPE
- + StringUtils.getMD5(user.username.toLowerCase() + password);
+ // convert to digest, if appropriate
+ String type = app().settings().getString(Keys.realm.passwordStorage, PasswordHash.getDefaultType().name());
+ PasswordHash pwdHash = PasswordHash.instanceOf(type);
+ if (pwdHash != null) {
+ password = pwdHash.toHashedEntry(password, user.username);
}
user.password = password;
diff --git a/src/main/java/com/gitblit/wicket/pages/DocPage.java b/src/main/java/com/gitblit/wicket/pages/DocPage.java
index 5d711343..6e76f1cd 100644
--- a/src/main/java/com/gitblit/wicket/pages/DocPage.java
+++ b/src/main/java/com/gitblit/wicket/pages/DocPage.java
@@ -51,6 +51,11 @@ public class DocPage extends RepositoryPage {
Repository r = getRepository();
RevCommit commit = JGitUtils.getCommit(r, objectId);
+ if (commit == null) {
+ setResponsePage(NoDocsPage.class, params);
+ return;
+ }
+
String [] encodings = getEncodings();
// Read raw markup content and transform it to html
diff --git a/src/main/java/com/gitblit/wicket/pages/DocsPage.java b/src/main/java/com/gitblit/wicket/pages/DocsPage.java
index 52443862..f87d5134 100644
--- a/src/main/java/com/gitblit/wicket/pages/DocsPage.java
+++ b/src/main/java/com/gitblit/wicket/pages/DocsPage.java
@@ -62,6 +62,10 @@ public class DocsPage extends RepositoryPage {
final boolean userCanEdit = currentUser.canEdit(getRepositoryModel());
RevCommit head = JGitUtils.getCommit(r, objectId);
+ if (head == null) {
+ setResponsePage(NoDocsPage.class, params);
+ return;
+ }
final String commitId = getBestCommitId(head);
List<String> extensions = processor.getAllExtensions();
diff --git a/src/main/java/com/gitblit/wicket/pages/EditFilePage.java b/src/main/java/com/gitblit/wicket/pages/EditFilePage.java
index dbf8a79e..5ec9b6ee 100644
--- a/src/main/java/com/gitblit/wicket/pages/EditFilePage.java
+++ b/src/main/java/com/gitblit/wicket/pages/EditFilePage.java
@@ -123,7 +123,7 @@ public class EditFilePage extends RepositoryPage {
try {
ObjectId docAtLoad = getRepository().resolve(commitIdAtLoad.getObject());
- logger.trace("Commiting Edit File page: " + commitIdAtLoad.getObject());
+ logger().trace("Commiting Edit File page: " + commitIdAtLoad.getObject());
DirCache index = DirCache.newInCore();
DirCacheBuilder builder = index.builder();
diff --git a/src/main/java/com/gitblit/wicket/pages/EditRepositoryPage.html b/src/main/java/com/gitblit/wicket/pages/EditRepositoryPage.html
index 7a55b9f5..2c881efc 100644
--- a/src/main/java/com/gitblit/wicket/pages/EditRepositoryPage.html
+++ b/src/main/java/com/gitblit/wicket/pages/EditRepositoryPage.html
@@ -123,7 +123,8 @@
<div wicket:id="acceptNewTickets"></div>
<div wicket:id="requireApproval"></div>
<div wicket:id="mergeTo"></div>
-
+ <div wicket:id="mergeType"></div>
+
</div>
<!-- federation -->
diff --git a/src/main/java/com/gitblit/wicket/pages/EditRepositoryPage.java b/src/main/java/com/gitblit/wicket/pages/EditRepositoryPage.java
index 6bcf6f51..bf3eea8b 100644
--- a/src/main/java/com/gitblit/wicket/pages/EditRepositoryPage.java
+++ b/src/main/java/com/gitblit/wicket/pages/EditRepositoryPage.java
@@ -56,6 +56,7 @@ import com.gitblit.Constants.AccessRestrictionType;
import com.gitblit.Constants.AuthorizationControl;
import com.gitblit.Constants.CommitMessageRenderer;
import com.gitblit.Constants.FederationStrategy;
+import com.gitblit.Constants.MergeType;
import com.gitblit.Constants.RegistrantType;
import com.gitblit.GitBlitException;
import com.gitblit.Keys;
@@ -458,6 +459,11 @@ public class EditRepositoryPage extends RootSubPage {
getString("gb.mergeToDescription"),
new PropertyModel<String>(repositoryModel, "mergeTo"),
availableBranches));
+ form.add(new ChoiceOption<MergeType>("mergeType",
+ getString("gb.mergeType"),
+ getString("gb.mergeTypeDescription"),
+ new PropertyModel<MergeType>(repositoryModel, "mergeType"),
+ Arrays.asList(MergeType.values())));
//
// RECEIVE
diff --git a/src/main/java/com/gitblit/wicket/pages/EditUserPage.html b/src/main/java/com/gitblit/wicket/pages/EditUserPage.html
index 3bccdd59..48e5339f 100644
--- a/src/main/java/com/gitblit/wicket/pages/EditUserPage.html
+++ b/src/main/java/com/gitblit/wicket/pages/EditUserPage.html
@@ -31,11 +31,12 @@
<tr><th><wicket:message key="gb.confirmPassword"></wicket:message></th><td class="edit"><input type="password" wicket:id="confirmPassword" size="30" tabindex="3" /></td></tr>
<tr><th><wicket:message key="gb.displayName"></wicket:message></th><td class="edit"><input type="text" wicket:id="displayName" size="30" tabindex="4" /></td></tr>
<tr><th><wicket:message key="gb.emailAddress"></wicket:message></th><td class="edit"><input type="text" wicket:id="emailAddress" size="30" tabindex="5" /></td></tr>
- <tr><th><wicket:message key="gb.canAdmin"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="canAdmin" tabindex="6" /> &nbsp;<span class="help-inline"><wicket:message key="gb.canAdminDescription"></wicket:message></span></label></td></tr>
- <tr><th><wicket:message key="gb.canCreate"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="canCreate" tabindex="7" /> &nbsp;<span class="help-inline"><wicket:message key="gb.canCreateDescription"></wicket:message></span></label></td></tr>
- <tr><th><wicket:message key="gb.canFork"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="canFork" tabindex="8" /> &nbsp;<span class="help-inline"><wicket:message key="gb.canForkDescription"></wicket:message></span></label></td></tr>
- <tr><th><wicket:message key="gb.excludeFromFederation"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="excludeFromFederation" tabindex="9" /> &nbsp;<span class="help-inline"><wicket:message key="gb.excludeFromFederationDescription"></wicket:message></span></label></td></tr>
- <tr><th><wicket:message key="gb.disableUser"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="disabled" tabindex="10" /> &nbsp;<span class="help-inline"><wicket:message key="gb.disableUserDescription"></wicket:message></span></label></td></tr>
+ <tr><th><wicket:message key="gb.languagePreference"></wicket:message></th><td class="edit"><select wicket:id="language" ></select></td></tr>
+ <tr><th><wicket:message key="gb.canAdmin"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="canAdmin" tabindex="7" /> &nbsp;<span class="help-inline"><wicket:message key="gb.canAdminDescription"></wicket:message></span></label></td></tr>
+ <tr><th><wicket:message key="gb.canCreate"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="canCreate" tabindex="8" /> &nbsp;<span class="help-inline"><wicket:message key="gb.canCreateDescription"></wicket:message></span></label></td></tr>
+ <tr><th><wicket:message key="gb.canFork"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="canFork" tabindex="9" /> &nbsp;<span class="help-inline"><wicket:message key="gb.canForkDescription"></wicket:message></span></label></td></tr>
+ <tr><th><wicket:message key="gb.excludeFromFederation"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="excludeFromFederation" tabindex="10" /> &nbsp;<span class="help-inline"><wicket:message key="gb.excludeFromFederationDescription"></wicket:message></span></label></td></tr>
+ <tr><th><wicket:message key="gb.disableUser"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="disabled" tabindex="11" /> &nbsp;<span class="help-inline"><wicket:message key="gb.disableUserDescription"></wicket:message></span></label></td></tr>
</tbody>
</table>
</div>
diff --git a/src/main/java/com/gitblit/wicket/pages/EditUserPage.java b/src/main/java/com/gitblit/wicket/pages/EditUserPage.java
index 220bee3f..c6014e8f 100644
--- a/src/main/java/com/gitblit/wicket/pages/EditUserPage.java
+++ b/src/main/java/com/gitblit/wicket/pages/EditUserPage.java
@@ -20,15 +20,19 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
+import java.util.Locale;
+import com.gitblit.utils.PasswordHash;
import org.apache.wicket.PageParameters;
import org.apache.wicket.behavior.SimpleAttributeModifier;
import org.apache.wicket.extensions.markup.html.form.palette.Palette;
import org.apache.wicket.markup.html.form.Button;
import org.apache.wicket.markup.html.form.CheckBox;
+import org.apache.wicket.markup.html.form.DropDownChoice;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.form.TextField;
import org.apache.wicket.model.CompoundPropertyModel;
+import org.apache.wicket.model.IModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.model.util.CollectionModel;
import org.apache.wicket.model.util.ListModel;
@@ -108,6 +112,11 @@ public class EditUserPage extends RootSubPage {
final Palette<String> teams = new Palette<String>("teams", new ListModel<String>(
new ArrayList<String>(userTeams)), new CollectionModel<String>(app().users()
.getAllTeamNames()), new StringChoiceRenderer(), 10, false);
+
+ Locale locale = userModel.getPreferences().getLocale();
+ List<Language> languages = UserPage.getLanguages();
+ Language preferredLanguage = UserPage.getPreferredLanguage(locale, languages);
+ final IModel<Language> language = Model.of(preferredLanguage);
Form<UserModel> form = new Form<UserModel>("editForm", model) {
private static final long serialVersionUID = 1L;
@@ -123,6 +132,10 @@ public class EditUserPage extends RootSubPage {
error(getString("gb.pleaseSetUsername"));
return;
}
+ Language lang = language.getObject();
+ if (lang != null) {
+ userModel.getPreferences().setLocale(lang.code);
+ }
// force username to lower-case
userModel.username = userModel.username.toLowerCase();
String username = userModel.username;
@@ -141,36 +154,30 @@ public class EditUserPage extends RootSubPage {
return;
}
String password = userModel.password;
- if (!password.toUpperCase().startsWith(StringUtils.MD5_TYPE)
- && !password.toUpperCase().startsWith(StringUtils.COMBINED_MD5_TYPE)) {
+ if (!PasswordHash.isHashedEntry(password)) {
// This is a plain text password.
// Check length.
int minLength = app().settings().getInteger(Keys.realm.minPasswordLength, 5);
if (minLength < 4) {
minLength = 4;
}
- if (password.trim().length() < minLength) {
+ if (password.trim().length() < minLength) { // TODO: Why do we trim here, but not in EditUserDialog and ChangePasswordPage?
error(MessageFormat.format(getString("gb.passwordTooShort"),
minLength));
return;
}
// change the cookie
- userModel.cookie = StringUtils.getSHA1(userModel.username + password);
+ userModel.cookie = userModel.createCookie();
// Optionally store the password MD5 digest.
- String type = app().settings().getString(Keys.realm.passwordStorage, "md5");
- if (type.equalsIgnoreCase("md5")) {
- // store MD5 digest of password
- userModel.password = StringUtils.MD5_TYPE
- + StringUtils.getMD5(userModel.password);
- } else if (type.equalsIgnoreCase("combined-md5")) {
- // store MD5 digest of username+password
- userModel.password = StringUtils.COMBINED_MD5_TYPE
- + StringUtils.getMD5(username + userModel.password);
+ String type = app().settings().getString(Keys.realm.passwordStorage, PasswordHash.getDefaultType().name());
+ PasswordHash pwdh = PasswordHash.instanceOf(type);
+ if (pwdh != null) { // Hash the password
+ userModel.password = pwdh.toHashedEntry(password, username);
}
} else if (rename
- && password.toUpperCase().startsWith(StringUtils.COMBINED_MD5_TYPE)) {
+ && password.toUpperCase().startsWith(PasswordHash.Type.CMD5.name())) {
error(getString("gb.combinedMd5Rename"));
return;
}
@@ -251,7 +258,10 @@ public class EditUserPage extends RootSubPage {
form.add(confirmPasswordField.setEnabled(editCredentials));
form.add(new TextField<String>("displayName").setEnabled(editDisplayName));
form.add(new TextField<String>("emailAddress").setEnabled(editEmailAddress));
+
+ DropDownChoice<Language> choice = new DropDownChoice<Language>("language",language,languages );
+ form.add( choice.setEnabled(languages.size()>0) );
if (userModel.canAdmin() && !userModel.canAdmin) {
// user inherits Admin permission
// display a disabled-yet-checked checkbox
diff --git a/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage.html b/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage.html
index 31226ff5..2a151682 100644
--- a/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage.html
+++ b/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage.html
@@ -37,7 +37,7 @@
<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://gitextensions.github.io">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>
diff --git a/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_cs.html b/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_cs.html
new file mode 100644
index 00000000..d8fc0c1d
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_cs.html
@@ -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>Prázdné úložiště</center></h3>
+ <div class="alert alert-info">
+ <span wicket:id="repository" style="font-weight: bold;">[repository]</span> je prázdné úložiště a nemůže být prohlíženo pomocí Gitblitu.
+ <p></p>
+ Prosím použijte push pro nahrání nějakách commitů do <span wicket:id="pushurl"></span>
+ <hr/>
+ Poté, co použijete push pro nahrání commitů, můžete <b>aktualizovat</b> tuto stránku pro prohlížení vašeho úložiště.
+ </div>
+
+ <h3><center>Vytvoření nového úložiště z příkazové řádky</center></h3>
+
+ <pre wicket:id="createSyntax"></pre>
+
+ <h3><center>Nahrání existujícího úložiště s použitím push</center></h3>
+
+ <pre wicket:id="existingSyntax"></pre>
+
+ <div class="span8 offset1">
+ <h2><center>Naučit se Git</center></h2>
+ <p>Pokud si nejste jisti jak použít tuto informaci, zvažte prohlédnutí <a href="http://book.git-scm.com">Git komunitní knihy</a>, abyste lépe porozuměli, jak použít Git.</p>
+
+ <h4>Open Source Git klienti</h4>
+ <table>
+ <tbody>
+ <tr><td><a href="http://git-scm.com">Git</a></td><td>oficiální, z příkazové řádky</td></tr>
+ <tr><td><a href="http://tortoisegit.googlecode.com">TortoiseGit</a></td><td>Integrace do Průzkumníka Windows (vyžaduje oficiální řádkový Git)</td></tr>
+ <tr><td><a href="http://eclipse.org/egit">Eclipse/EGit</a></td><td>Git pro Eclipse IDE (založený na JGit, jako Gitblit)</td></tr>
+ <tr><td><a href="http://gitextensions.github.io">Git Extensions</a></td><td>C# frontend pro Git, který obsahuje integraci do Průzkumníka Windows a do Visual Studia</td></tr>
+ <tr><td><a href="http://rowanj.github.io/gitx/">GitX-dev</a></td><td>Mac OS X Git klient</td></tr>
+ </tbody>
+ </table>
+
+ <h4>Komerční/Closed-Source Git klienti</h4>
+ <table>
+ <tbody>
+ <tr><td><a href="http://www.syntevo.com/smartgithg">SmartGit/Hg</a></td><td>Na Javě založený klient pro Git and Mercurial pro Windows, Mac a Linux</td></tr>
+ <tr><td><a href="http://www.sourcetreeapp.com/">SourceTree</a></td><td>Volný Git a Mercurial klient pro Windows a Mac</td></tr>
+ <tr><td><a href="http://www.git-tower.com/">Tower</a></td><td>Mac OS X Git klient</td></tr>
+ </tbody>
+ </table>
+ </div>
+</div>
+</div>
+</div>
+</div>
+</wicket:extend>
+</body>
+</html>
diff --git a/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_de.html b/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_de.html
index 6888e1df..cf5262b5 100644
--- a/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_de.html
+++ b/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_de.html
@@ -37,7 +37,7 @@
<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://gitextensions.github.io">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>
diff --git a/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_es.html b/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_es.html
index af75b4ee..8c0cab56 100644
--- a/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_es.html
+++ b/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_es.html
@@ -39,7 +39,7 @@
<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://gitextensions.github.io">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>
diff --git a/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_it.html b/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_it.html
index 365f4135..d2bf8903 100644
--- a/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_it.html
+++ b/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_it.html
@@ -37,7 +37,7 @@
<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://gitextensions.github.io">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>
diff --git a/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_ja.html b/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_ja.html
new file mode 100644
index 00000000..5a845be0
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_ja.html
@@ -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>空のリポジトリ</center></h3>
+ <div class="alert alert-info">
+ <span wicket:id="repository" style="font-weight: bold;">[repository]</span> は空のリポジトリです。表示するものはありません。
+ <p></p>
+ 下記の URL にプッシュしてください。 <span wicket:id="pushurl"></span>
+ <hr/>
+ プッシュ完了後、このリポジトリページを<b>再読み込み</b>してください。
+ </div>
+
+ <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">Git Community Book</a> で Git の使い方をより理解してみませんか?</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>Windows エクスプローラ統合型 GUI (要 本家コマンドライン版 Git)</td></tr>
+ <tr><td><a href="http://eclipse.org/egit">Eclipse/EGit</a></td><td>エクリプス IDE 向け Git (Gitblit に似た JGit 使用 )</td></tr>
+ <tr><td><a href="http://gitextensions.github.io">Git Extensions</a></td><td>Windows エクスプローラとVisual Studio に統合された、Git の C# 製 UI</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>
+ <table>
+ <tbody>
+ <tr><td><a href="http://www.syntevo.com/smartgithg">SmartGit/Hg</a></td><td>Windows, Mac, Linux 向け、Java製 Git &amp; Mercurial クライアント</td></tr>
+ <tr><td><a href="http://www.sourcetreeapp.com/">SourceTree</a></td><td>フリーの Windows, Mac 向け Git &amp; 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>
+</html>
diff --git a/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_ko.html b/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_ko.html
index cd777b4b..c5623593 100644
--- a/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_ko.html
+++ b/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_ko.html
@@ -38,7 +38,7 @@
<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://gitextensions.github.io">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>
diff --git a/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_nl.html b/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_nl.html
index ab207d1e..0c4eb407 100644
--- a/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_nl.html
+++ b/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_nl.html
@@ -37,7 +37,7 @@
<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://gitextensions.github.io">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>
diff --git a/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_no.html b/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_no.html
index 273e15f9..48c5421f 100644
--- a/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_no.html
+++ b/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_no.html
@@ -37,7 +37,7 @@
<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://gitextensions.github.io">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>
diff --git a/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_pl.html b/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_pl.html
index b50bdac3..893683e1 100644
--- a/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_pl.html
+++ b/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_pl.html
@@ -39,7 +39,7 @@
<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://gitextensions.github.io">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>
diff --git a/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_pt_BR.html b/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_pt_BR.html
index fc201210..20fcc25c 100644
--- a/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_pt_BR.html
+++ b/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_pt_BR.html
@@ -37,7 +37,7 @@
<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://gitextensions.github.io">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>
diff --git a/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_ru.html b/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_ru.html
new file mode 100644
index 00000000..d7d06906
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_ru.html
@@ -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="ru"
+ lang="ru">
+
+<body>
+<wicket:extend>
+<div class="container">
+<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>
+ Пожалуйста, отправьте (push) некоторые коммиты <span wicket:id="pushurl"></span>
+ <hr/>
+ После того, как вы отправили коммиты, вы можете обновить эту страницу, чтобы просмотреть свой репозиторий.
+ </div>
+
+ <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">Git Community Book</a> чтобы лучше понять, как использовать Git.</p>
+
+ <h4>Git-клиенты с открытым исходным кодом</h4>
+ <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="hhttp://gitextensions.github.io">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>
+ <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>
+</html>
diff --git a/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_zh_CN.html b/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_zh_CN.html
index 72ce051e..462954c8 100644
--- a/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_zh_CN.html
+++ b/src/main/java/com/gitblit/wicket/pages/EmptyRepositoryPage_zh_CN.html
@@ -38,7 +38,7 @@
<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://gitextensions.github.io">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>
diff --git a/src/main/java/com/gitblit/wicket/pages/FilestoreUsage_ja.html b/src/main/java/com/gitblit/wicket/pages/FilestoreUsage_ja.html
new file mode 100644
index 00000000..f0631a36
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/FilestoreUsage_ja.html
@@ -0,0 +1,52 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
+ xml:lang="en"
+ lang="en">
+
+<body>
+<wicket:extend>
+<div class="container">
+<div class="markdown">
+<div class="row">
+<div class="span10 offset1">
+
+ <div class="alert alert-danger">
+ <h3><center>ファイルストアの使用方法</center></h3>
+ <p>
+ <strong>ファイルストアを使う場合、先ずは <a href="https://git-lfs.github.com/">Git-LFS Client</a> をインストールし、<code>git lfs install</code></strong> を実行しておかなければなりません。<br/>
+ <p>
+ その際、パスワード認証を使うのであれば、<a href="https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage">git credential storage</a> を設定するのがお勧めです。
+ Git-LFS が各ファイルに対してパスワードを要求するのを避けることが出来ます。<br/>
+ Windows での例: <code>git config --global credential.helper wincred</code>
+ </p>
+ </p>
+ </div>
+
+ <h3>クローン</h3>
+ <p>
+ 既定の Git-LFS に繋いで <code>{repository}/info/lfs/objects/</code> を参照するには、単に <code>git clone</code> とするだけです。それ以上の操作は必要ありません。<br/>
+ <i>リポジトリがサードパーティの Git-LFS サーバーを使っている場合、<a href="https://github.com/github/git-lfs/blob/master/docs/spec.md#the-server"> 手動での設定する必要が有ります。</a></i>
+ </p>
+
+ <h3>追加</h3>
+ <p><code>git lfs track "*.bin"</code> を使ってファイルタイプやパスの設定を行った後ならば、単に <code>git add</code> コマンドでファイルの追加が行えます。<br/>
+ <i>追跡されたファイルもまた、<code>.gitattributes</code> ファイルを使うことで手動で設定できます。</i></p>
+
+ <h3>削除</h3>
+ <p>Git-LFS からファイルを削除した際には、リンクファイルがリポジトリから削除されるだけです。<br/>
+ <i>全てのファイルは、以前のバージョンとしてチェックアウト出来るよう、サーバーに残っています。</i>
+ </p>
+
+ <h3>詳細は...</h3>
+ <p><a href="https://github.com/github/git-lfs/blob/master/docs/spec.md">より詳しい最新の Git-LFS の仕様</a>を参照してください。</p>
+ <br />
+
+
+</div>
+</div>
+</div>
+</div>
+</wicket:extend>
+</body>
+</html>
diff --git a/src/main/java/com/gitblit/wicket/pages/Language.java b/src/main/java/com/gitblit/wicket/pages/Language.java
new file mode 100644
index 00000000..6eafb4dc
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/pages/Language.java
@@ -0,0 +1,21 @@
+package com.gitblit.wicket.pages;
+
+import java.io.Serializable;
+
+public 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 + ")";
+ }
+} \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/LuceneSearchPage.html b/src/main/java/com/gitblit/wicket/pages/LuceneSearchPage.html
index d62b7b22..a386f425 100644
--- a/src/main/java/com/gitblit/wicket/pages/LuceneSearchPage.html
+++ b/src/main/java/com/gitblit/wicket/pages/LuceneSearchPage.html
@@ -52,7 +52,9 @@
</div>
</div>
<div style="margin-left:10px;" class="span2">
- <wicket:message key="gb.queryHelp"></wicket:message>
+ <wicket:message key="gb.queryHelp">
+ <a target="_new" wicket:id="querySyntax"><wicket:message key="gb.querySyntax"/></a>
+ </wicket:message>
</div>
</div>
</div>
diff --git a/src/main/java/com/gitblit/wicket/pages/LuceneSearchPage.java b/src/main/java/com/gitblit/wicket/pages/LuceneSearchPage.java
index 1d81061e..a97d37a8 100644
--- a/src/main/java/com/gitblit/wicket/pages/LuceneSearchPage.java
+++ b/src/main/java/com/gitblit/wicket/pages/LuceneSearchPage.java
@@ -27,6 +27,7 @@ import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.form.CheckBox;
import org.apache.wicket.markup.html.form.ListMultipleChoice;
import org.apache.wicket.markup.html.form.TextField;
+import org.apache.wicket.markup.html.link.ExternalLink;
import org.apache.wicket.markup.html.panel.Fragment;
import org.apache.wicket.markup.repeater.Item;
import org.apache.wicket.markup.repeater.data.DataView;
@@ -50,6 +51,8 @@ import com.gitblit.wicket.panels.PagerPanel;
public class LuceneSearchPage extends RootPage {
+ private final static String LUCENE_QUERY_SYNTAX_LINK = "https://lucene.apache.org/core/5_5_0/queryparser/org/apache/lucene/queryparser/classic/package-summary.html#package_description";
+
public LuceneSearchPage() {
super();
setup(null);
@@ -167,6 +170,7 @@ public class LuceneSearchPage extends RootPage {
form.add(selections.setEnabled(luceneEnabled));
form.add(new TextField<String>("query", queryModel).setEnabled(luceneEnabled));
form.add(new CheckBox("allrepos", allreposModel));
+ form.add(new ExternalLink("querySyntax", LUCENE_QUERY_SYNTAX_LINK));
add(form.setEnabled(luceneEnabled));
// execute search
diff --git a/src/main/java/com/gitblit/wicket/pages/MetricsPage.java b/src/main/java/com/gitblit/wicket/pages/MetricsPage.java
index 96113b0f..77811687 100644
--- a/src/main/java/com/gitblit/wicket/pages/MetricsPage.java
+++ b/src/main/java/com/gitblit/wicket/pages/MetricsPage.java
@@ -94,7 +94,7 @@ public class MetricsPage extends RepositoryPage {
try {
date = df.parse(metric.name);
} catch (ParseException e) {
- logger.error("Unable to parse date: " + metric.name);
+ logger().error("Unable to parse date: " + metric.name);
return;
}
chart.addValue(date, (int)metric.count);
diff --git a/src/main/java/com/gitblit/wicket/pages/RawPage.java b/src/main/java/com/gitblit/wicket/pages/RawPage.java
index c4357478..bd901f24 100644
--- a/src/main/java/com/gitblit/wicket/pages/RawPage.java
+++ b/src/main/java/com/gitblit/wicket/pages/RawPage.java
@@ -45,7 +45,7 @@ import com.gitblit.wicket.WicketUtils;
public class RawPage extends SessionPage {
- private final Logger logger = LoggerFactory.getLogger(getClass().getSimpleName());
+ private transient Logger logger;
String contentType;
@@ -95,7 +95,7 @@ public class RawPage extends SessionPage {
if (binary == null) {
final String objectNotFound = MessageFormat.format("Raw page failed to find object {0} in {1}",
objectId, repositoryName);
- logger.error(objectNotFound);
+ logger().error(objectNotFound);
throw new AbortWithWebErrorCodeException(HttpServletResponse.SC_NOT_FOUND, objectNotFound);
}
contentType = "application/octet-stream";
@@ -104,7 +104,7 @@ public class RawPage extends SessionPage {
try {
response.getOutputStream().write(binary);
} catch (Exception e) {
- logger.error("Failed to write binary response", e);
+ logger().error("Failed to write binary response", e);
}
} else {
// standard raw blob view
@@ -112,7 +112,7 @@ public class RawPage extends SessionPage {
if (commit == null) {
final String commitNotFound = MessageFormat.format("Raw page failed to find commit {0} in {1}",
objectId, repositoryName);
- logger.error(commitNotFound);
+ logger().error(commitNotFound);
throw new AbortWithWebErrorCodeException(HttpServletResponse.SC_NOT_FOUND, commitNotFound);
}
@@ -148,7 +148,7 @@ public class RawPage extends SessionPage {
// image blobs
byte[] image = JGitUtils.getByteContent(r, commit.getTree(), blobPath, true);
if (image == null) {
- logger.error(blobNotFound);
+ logger().error(blobNotFound);
throw new AbortWithWebErrorCodeException(HttpServletResponse.SC_NOT_FOUND, blobNotFound);
}
contentType = "image/" + extension.toLowerCase();
@@ -157,14 +157,14 @@ public class RawPage extends SessionPage {
try {
response.getOutputStream().write(image);
} catch (IOException e) {
- logger.error("Failed to write image response", e);
+ logger().error("Failed to write image response", e);
}
break;
case 3:
// binary blobs (download)
byte[] binary = JGitUtils.getByteContent(r, commit.getTree(), blobPath, true);
if (binary == null) {
- logger.error(blobNotFound);
+ logger().error(blobNotFound);
throw new AbortWithWebErrorCodeException(HttpServletResponse.SC_NOT_FOUND, blobNotFound);
}
contentType = "application/octet-stream";
@@ -193,7 +193,7 @@ public class RawPage extends SessionPage {
try {
response.getOutputStream().write(binary);
} catch (IOException e) {
- logger.error("Failed to write binary response", e);
+ logger().error("Failed to write binary response", e);
}
break;
default:
@@ -201,7 +201,7 @@ public class RawPage extends SessionPage {
String content = JGitUtils.getStringContent(r, commit.getTree(),
blobPath, encodings);
if (content == null) {
- logger.error(blobNotFound);
+ logger().error(blobNotFound);
throw new AbortWithWebErrorCodeException(HttpServletResponse.SC_NOT_FOUND, blobNotFound);
}
contentType = "text/plain; charset=UTF-8";
@@ -209,7 +209,7 @@ public class RawPage extends SessionPage {
try {
response.getOutputStream().write(content.getBytes("UTF-8"));
} catch (Exception e) {
- logger.error("Failed to write text response", e);
+ logger().error("Failed to write text response", e);
}
}
@@ -218,7 +218,7 @@ public class RawPage extends SessionPage {
String content = JGitUtils.getStringContent(r, commit.getTree(), blobPath,
encodings);
if (content == null) {
- logger.error(blobNotFound);
+ logger().error(blobNotFound);
throw new AbortWithWebErrorCodeException(HttpServletResponse.SC_NOT_FOUND, blobNotFound);
}
contentType = "text/plain; charset=UTF-8";
@@ -226,7 +226,7 @@ public class RawPage extends SessionPage {
try {
response.getOutputStream().write(content.getBytes("UTF-8"));
} catch (Exception e) {
- logger.error("Failed to write text response", e);
+ logger().error("Failed to write text response", e);
}
}
}
@@ -235,6 +235,13 @@ public class RawPage extends SessionPage {
});
}
+ protected Logger logger() {
+ if (logger == null) {
+ logger = LoggerFactory.getLogger(getClass());
+ }
+ return logger;
+ }
+
@Override
protected void setHeaders(WebResponse response) {
super.setHeaders(response);
diff --git a/src/main/java/com/gitblit/wicket/pages/RepositoryPage.java b/src/main/java/com/gitblit/wicket/pages/RepositoryPage.java
index 36c5ae16..4d30e049 100644
--- a/src/main/java/com/gitblit/wicket/pages/RepositoryPage.java
+++ b/src/main/java/com/gitblit/wicket/pages/RepositoryPage.java
@@ -42,8 +42,6 @@ import org.eclipse.jgit.diff.DiffEntry.ChangeType;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
import com.gitblit.Constants;
import com.gitblit.GitBlitException;
@@ -78,8 +76,6 @@ import com.google.common.base.Optional;
public abstract class RepositoryPage extends RootPage {
- protected final Logger logger = LoggerFactory.getLogger(getClass());
-
private final String PARAM_STAR = "star";
protected final String projectName;
@@ -93,7 +89,7 @@ public abstract class RepositoryPage extends RootPage {
private Map<String, SubmoduleModel> submodules;
private boolean showAdmin;
- private boolean isOwner;
+ private final boolean isOwner;
public RepositoryPage(PageParameters params) {
super(params);
@@ -144,7 +140,7 @@ public abstract class RepositoryPage extends RootPage {
try {
app().gitblit().reviseUser(user.username, user);
} catch (GitBlitException e) {
- logger.error("Failed to update user " + user.username, e);
+ logger().error("Failed to update user " + user.username, e);
error(getString("gb.failedToUpdateUser"), false);
}
}
@@ -579,7 +575,7 @@ public abstract class RepositoryPage extends RootPage {
}
protected void addRefs(Repository r, RevCommit c) {
- add(new RefsPanel("refsPanel", repositoryName, c, JGitUtils.getAllRefs(r, getRepositoryModel().showRemoteBranches)));
+ add(new RefsPanel("refsPanel", repositoryName, JGitUtils.getAllRefs(r, getRepositoryModel().showRemoteBranches).get(c.getId())));
}
protected void addFullText(String wicketId, String text) {
diff --git a/src/main/java/com/gitblit/wicket/pages/SummaryPage.java b/src/main/java/com/gitblit/wicket/pages/SummaryPage.java
index 3cfa152e..1a5f0518 100644
--- a/src/main/java/com/gitblit/wicket/pages/SummaryPage.java
+++ b/src/main/java/com/gitblit/wicket/pages/SummaryPage.java
@@ -190,7 +190,7 @@ public class SummaryPage extends RepositoryPage {
try {
date = df.parse(metric.name);
} catch (ParseException e) {
- logger.error("Unable to parse date: " + metric.name);
+ logger().error("Unable to parse date: " + metric.name);
return charts;
}
chart.addValue(date, (int)metric.count);
diff --git a/src/main/java/com/gitblit/wicket/pages/TicketPage.java b/src/main/java/com/gitblit/wicket/pages/TicketPage.java
index cd049f4d..1750b859 100644
--- a/src/main/java/com/gitblit/wicket/pages/TicketPage.java
+++ b/src/main/java/com/gitblit/wicket/pages/TicketPage.java
@@ -66,6 +66,7 @@ import com.gitblit.git.PatchsetCommand;
import com.gitblit.git.PatchsetReceivePack;
import com.gitblit.models.PathModel.PathChangeModel;
import com.gitblit.models.RegistrantAccessPermission;
+import com.gitblit.models.RepositoryCommit;
import com.gitblit.models.RepositoryModel;
import com.gitblit.models.RepositoryUrl;
import com.gitblit.models.SubmoduleModel;
@@ -815,13 +816,17 @@ public class TicketPage extends RepositoryPage {
// commits
List<RevCommit> commits = JGitUtils.getRevLog(getRepository(), currentPatchset.base, currentPatchset.tip);
- ListDataProvider<RevCommit> commitsDp = new ListDataProvider<RevCommit>(commits);
- DataView<RevCommit> commitsView = new DataView<RevCommit>("commit", commitsDp) {
+ List<RepositoryCommit> repoCommits = new ArrayList<>(commits.size());
+ for (RevCommit c : commits) {
+ repoCommits.add(new RepositoryCommit(repositoryName, "", c));
+ }
+ ListDataProvider<RepositoryCommit> commitsDp = new ListDataProvider<RepositoryCommit>(repoCommits);
+ DataView<RepositoryCommit> commitsView = new DataView<RepositoryCommit>("commit", commitsDp) {
private static final long serialVersionUID = 1L;
@Override
- public void populateItem(final Item<RevCommit> item) {
- RevCommit commit = item.getModelObject();
+ public void populateItem(final Item<RepositoryCommit> item) {
+ RepositoryCommit commit = item.getModelObject();
PersonIdent author = commit.getAuthorIdent();
item.add(new AvatarImage("authorAvatar", author.getName(), author.getEmailAddress(), null, 16, false));
item.add(new Label("author", commit.getAuthorIdent().getName()));
@@ -830,7 +835,7 @@ public class TicketPage extends RepositoryPage {
item.add(new LinkPanel("diff", "link", getString("gb.diff"), CommitDiffPage.class,
WicketUtils.newObjectParameter(repositoryName, commit.getName()), true));
item.add(new Label("title", StringUtils.trimString(commit.getShortMessage(), Constants.LEN_SHORTLOG_REFS)));
- item.add(WicketUtils.createDateLabel("commitDate", JGitUtils.getAuthorDate(commit), GitBlitWebSession
+ item.add(WicketUtils.createDateLabel("commitDate", author.getWhen(), GitBlitWebSession
.get().getTimezone(), getTimeUtils(), false));
item.add(new DiffStatPanel("commitDiffStat", 0, 0, true));
}
@@ -1405,14 +1410,14 @@ public class TicketPage extends RepositoryPage {
boolean allowMerge;
if (repository.requireApproval) {
- // rpeository requires approval
+ // repository requires approval
allowMerge = ticket.isOpen() && ticket.isApproved(patchset);
} else {
- // vetos are binding
+ // vetoes are binding
allowMerge = ticket.isOpen() && !ticket.isVetoed(patchset);
}
- MergeStatus mergeStatus = JGitUtils.canMerge(getRepository(), patchset.tip, ticket.mergeTo);
+ MergeStatus mergeStatus = JGitUtils.canMerge(getRepository(), patchset.tip, ticket.mergeTo, repository.mergeType);
if (allowMerge) {
if (MergeStatus.MERGEABLE == mergeStatus) {
// patchset can be cleanly merged to integration branch OR has already been merged
@@ -1451,7 +1456,7 @@ public class TicketPage extends RepositoryPage {
} else {
// merge failure
String msg = MessageFormat.format("Failed to merge ticket {0,number,0}: {1}", ticket.number, result.name());
- logger.error(msg);
+ logger().error(msg);
GitBlitWebSession.get().cacheErrorMessage(msg);
}
}
@@ -1461,13 +1466,13 @@ public class TicketPage extends RepositoryPage {
String msg = MessageFormat.format("Can not merge ticket {0,number,0}, patchset {1,number,0} has been vetoed!",
ticket.number, patchset.number);
GitBlitWebSession.get().cacheErrorMessage(msg);
- logger.error(msg);
+ logger().error(msg);
}
} else {
// not current patchset
String msg = MessageFormat.format("Can not merge ticket {0,number,0}, the patchset has been updated!", ticket.number);
GitBlitWebSession.get().cacheErrorMessage(msg);
- logger.error(msg);
+ logger().error(msg);
}
redirectTo(TicketsPage.class, getPageParameters());
diff --git a/src/main/java/com/gitblit/wicket/pages/TicketsPage.java b/src/main/java/com/gitblit/wicket/pages/TicketsPage.java
index ecfed250..ebf4dc52 100644
--- a/src/main/java/com/gitblit/wicket/pages/TicketsPage.java
+++ b/src/main/java/com/gitblit/wicket/pages/TicketsPage.java
@@ -521,14 +521,26 @@ public class TicketsPage extends RepositoryPage {
Collections.sort(openMilestones, new Comparator<TicketMilestone>() {
@Override
public int compare(TicketMilestone o1, TicketMilestone o2) {
- return o2.due.compareTo(o1.due);
+ if (o1.due == null) {
+ return (o2.due == null) ? 0 : 1;
+ } else if (o2.due == null) {
+ return -1;
+ } else {
+ return o1.due.compareTo(o2.due);
+ }
}
});
Collections.sort(closedMilestones, new Comparator<TicketMilestone>() {
@Override
public int compare(TicketMilestone o1, TicketMilestone o2) {
- return o2.due.compareTo(o1.due);
+ if (o1.due == null) {
+ return (o2.due == null) ? 0 : 1;
+ } else if (o2.due == null) {
+ return -1;
+ } else {
+ return o1.due.compareTo(o2.due);
+ }
}
});
diff --git a/src/main/java/com/gitblit/wicket/pages/UserPage.java b/src/main/java/com/gitblit/wicket/pages/UserPage.java
index ea68f25b..62ec894a 100644
--- a/src/main/java/com/gitblit/wicket/pages/UserPage.java
+++ b/src/main/java/com/gitblit/wicket/pages/UserPage.java
@@ -15,7 +15,6 @@
*/
package com.gitblit.wicket.pages;
-import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -166,12 +165,10 @@ public class UserPage extends RootPage {
navLinks.add(menu);
}
-
- private void addPreferences(UserModel user) {
- // add preferences
- Form<Void> prefs = new Form<Void>("prefsForm");
-
- List<Language> languages = Arrays.asList(
+
+ static List<Language> getLanguages(){
+ return Arrays.asList(
+ new Language("Česky","cs"),
new Language("Deutsch","de"),
new Language("English","en"),
new Language("Español", "es"),
@@ -183,26 +180,12 @@ public class UserPage extends RootPage {
new Language("Norsk", "no"),
new Language("Język Polski", "pl"),
new Language("Português", "pt_BR"),
+ new Language("Русский","ru"),
new Language("簡體中文", "zh_CN"),
new Language("正體中文", "zh_TW"));
+ }
- 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 {
-
- }
- }
-
+ static Language getPreferredLanguage(Locale locale, List<Language> languages) {
Language preferredLanguage = null;
if (locale != null) {
String localeCode = locale.getLanguage();
@@ -214,12 +197,36 @@ public class UserPage extends RootPage {
if (language.code.equals(localeCode)) {
// language_COUNTRY match
preferredLanguage = language;
- } else if (preferredLanguage != null && language.code.startsWith(locale.getLanguage())) {
+ } else if (preferredLanguage == null && language.code.startsWith(locale.getLanguage())) {
// language match
preferredLanguage = language;
}
}
}
+ return preferredLanguage;
+ }
+
+ private void addPreferences(UserModel user) {
+ // add preferences
+ Form<Void> prefs = new Form<Void>("prefsForm");
+
+ 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;
+ }
+ }
+ }
+
+ List<Language> languages = getLanguages();
+ Language preferredLanguage = getPreferredLanguage(locale, languages);
final IModel<String> displayName = Model.of(user.getDisplayName());
final IModel<String> emailAddress = Model.of(user.emailAddress == null ? "" : user.emailAddress);
@@ -315,22 +322,4 @@ public class UserPage extends RootPage {
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 +")";
- }
- }
}
diff --git a/src/main/java/com/gitblit/wicket/panels/HistoryPanel.java b/src/main/java/com/gitblit/wicket/panels/HistoryPanel.java
index 75fd70e7..737fe5aa 100644
--- a/src/main/java/com/gitblit/wicket/panels/HistoryPanel.java
+++ b/src/main/java/com/gitblit/wicket/panels/HistoryPanel.java
@@ -44,7 +44,9 @@ import com.gitblit.Keys;
import com.gitblit.models.PathModel;
import com.gitblit.models.PathModel.PathChangeModel;
import com.gitblit.models.RefModel;
+import com.gitblit.models.RepositoryCommit;
import com.gitblit.models.SubmoduleModel;
+import com.gitblit.utils.ArrayUtils;
import com.gitblit.utils.JGitUtils;
import com.gitblit.utils.MarkdownUtils;
import com.gitblit.utils.StringUtils;
@@ -61,7 +63,7 @@ public class HistoryPanel extends BasePanel {
private static final long serialVersionUID = 1L;
- private boolean hasMore;
+ private final boolean hasMore;
public HistoryPanel(String wicketId, final String repositoryName, final String objectId,
final String path, Repository r, int limit, int pageOffset, boolean showRemoteRefs) {
@@ -101,10 +103,9 @@ public class HistoryPanel extends BasePanel {
if (matchingPath == null) {
// path not in commit
// manually locate path in tree
- TreeWalk tw = new TreeWalk(r);
- tw.reset();
- tw.setRecursive(true);
- try {
+ try (TreeWalk tw = new TreeWalk(r)) {
+ tw.reset();
+ tw.setRecursive(true);
tw.addTree(commit.getTree());
tw.setFilter(PathFilterGroup.createFromStrings(Collections.singleton(path)));
while (tw.next()) {
@@ -115,8 +116,6 @@ public class HistoryPanel extends BasePanel {
}
}
} catch (Exception e) {
- } finally {
- tw.close();
}
}
}
@@ -136,7 +135,7 @@ public class HistoryPanel extends BasePanel {
hasSubmodule = false;
}
- final Map<ObjectId, List<RefModel>> allRefs = JGitUtils.getAllRefs(r, showRemoteRefs);
+ Map<ObjectId, List<RefModel>> allRefs = JGitUtils.getAllRefs(r, showRemoteRefs);
List<RevCommit> commits;
if (pageResults) {
// Paging result set
@@ -152,15 +151,23 @@ public class HistoryPanel extends BasePanel {
hasMore = commits.size() >= itemsPerPage;
final int hashLen = app().settings().getInteger(Keys.web.shortCommitIdLength, 6);
- ListDataProvider<RevCommit> dp = new ListDataProvider<RevCommit>(commits);
- DataView<RevCommit> logView = new DataView<RevCommit>("commit", dp) {
+ List<RepositoryCommit> repoCommits = new ArrayList<>(commits.size());
+ for (RevCommit c : commits) {
+ RepositoryCommit repoCommit = new RepositoryCommit(repositoryName, "", c);
+ if (allRefs.containsKey(c)) {
+ repoCommit.setRefs(allRefs.get(c));
+ }
+ repoCommits.add(repoCommit);
+ }
+ ListDataProvider<RepositoryCommit> dp = new ListDataProvider<RepositoryCommit>(repoCommits);
+ DataView<RepositoryCommit> logView = new DataView<RepositoryCommit>("commit", dp) {
private static final long serialVersionUID = 1L;
int counter;
@Override
- public void populateItem(final Item<RevCommit> item) {
- final RevCommit entry = item.getModelObject();
- final Date date = JGitUtils.getAuthorDate(entry);
+ public void populateItem(final Item<RepositoryCommit> item) {
+ final RepositoryCommit entry = item.getModelObject();
+ Date date = entry.getAuthorIdent().getWhen();
item.add(WicketUtils.createDateLabel("commitDate", date, getTimeZone(), getTimeUtils()));
@@ -182,7 +189,7 @@ public class HistoryPanel extends BasePanel {
String shortMessage = entry.getShortMessage();
String trimmedMessage = shortMessage;
- if (allRefs.containsKey(entry.getId())) {
+ if (!ArrayUtils.isEmpty(entry.getRefs())) {
trimmedMessage = StringUtils.trimString(shortMessage, Constants.LEN_SHORTLOG_REFS);
} else {
trimmedMessage = StringUtils.trimString(shortMessage, Constants.LEN_SHORTLOG);
@@ -195,7 +202,7 @@ public class HistoryPanel extends BasePanel {
}
item.add(shortlog);
- item.add(new RefsPanel("commitRefs", repositoryName, entry, allRefs));
+ item.add(new RefsPanel("commitRefs", repositoryName, entry.getRefs()));
if (isTree) {
// tree
@@ -214,7 +221,7 @@ public class HistoryPanel extends BasePanel {
} else if (isSubmodule) {
// submodule
Repository repository = app().repositories().getRepository(repositoryName);
- String submoduleId = JGitUtils.getSubmoduleCommitId(repository, path, entry);
+ String submoduleId = JGitUtils.getSubmoduleCommitId(repository, path, entry.getCommit());
repository.close();
if (StringUtils.isEmpty(submoduleId)) {
// not a submodule at this commit, just a matching path
diff --git a/src/main/java/com/gitblit/wicket/panels/LogPanel.java b/src/main/java/com/gitblit/wicket/panels/LogPanel.java
index e9d240d0..4521d430 100644
--- a/src/main/java/com/gitblit/wicket/panels/LogPanel.java
+++ b/src/main/java/com/gitblit/wicket/panels/LogPanel.java
@@ -15,6 +15,7 @@
*/
package com.gitblit.wicket.panels;
+import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
@@ -35,7 +36,9 @@ import org.eclipse.jgit.revwalk.RevCommit;
import com.gitblit.Constants;
import com.gitblit.Keys;
import com.gitblit.models.RefModel;
+import com.gitblit.models.RepositoryCommit;
import com.gitblit.servlet.BranchGraphServlet;
+import com.gitblit.utils.ArrayUtils;
import com.gitblit.utils.JGitUtils;
import com.gitblit.utils.StringUtils;
import com.gitblit.wicket.ExternalImage;
@@ -50,7 +53,7 @@ public class LogPanel extends BasePanel {
private static final long serialVersionUID = 1L;
- private boolean hasMore;
+ private final boolean hasMore;
public LogPanel(String wicketId, final String repositoryName, final String objectId,
Repository r, int limit, int pageOffset, boolean showRemoteRefs) {
@@ -61,7 +64,7 @@ public class LogPanel extends BasePanel {
itemsPerPage = 50;
}
- final Map<ObjectId, List<RefModel>> allRefs = JGitUtils.getAllRefs(r, showRemoteRefs);
+ Map<ObjectId, List<RefModel>> allRefs = JGitUtils.getAllRefs(r, showRemoteRefs);
List<RevCommit> commits;
if (pageResults) {
// Paging result set
@@ -75,8 +78,8 @@ public class LogPanel extends BasePanel {
// works unless commits.size() represents the exact end.
hasMore = commits.size() >= itemsPerPage;
- final String baseUrl = WicketUtils.getGitblitURL(getRequest());
- final boolean showGraph = app().settings().getBoolean(Keys.web.showBranchGraph, true);
+ String baseUrl = WicketUtils.getGitblitURL(getRequest());
+ boolean showGraph = app().settings().getBoolean(Keys.web.showBranchGraph, true);
MarkupContainer graph = new WebMarkupContainer("graph");
add(graph);
@@ -101,15 +104,23 @@ public class LogPanel extends BasePanel {
}
final int hashLen = app().settings().getInteger(Keys.web.shortCommitIdLength, 6);
- ListDataProvider<RevCommit> dp = new ListDataProvider<RevCommit>(commits);
- DataView<RevCommit> logView = new DataView<RevCommit>("commit", dp) {
+ List<RepositoryCommit> repoCommits = new ArrayList<>(commits.size());
+ for (RevCommit c : commits) {
+ RepositoryCommit repoCommit = new RepositoryCommit(repositoryName, "", c);
+ if (allRefs.containsKey(c)) {
+ repoCommit.setRefs(allRefs.get(c));
+ }
+ repoCommits.add(repoCommit);
+ }
+ ListDataProvider<RepositoryCommit> dp = new ListDataProvider<RepositoryCommit>(repoCommits);
+ DataView<RepositoryCommit> logView = new DataView<RepositoryCommit>("commit", dp) {
private static final long serialVersionUID = 1L;
int counter;
@Override
- public void populateItem(final Item<RevCommit> item) {
- final RevCommit entry = item.getModelObject();
- final Date date = JGitUtils.getAuthorDate(entry);
+ public void populateItem(final Item<RepositoryCommit> item) {
+ final RepositoryCommit entry = item.getModelObject();
+ final Date date = entry.getAuthorIdent().getWhen();
final boolean isMerge = entry.getParentCount() > 1;
item.add(WicketUtils.createDateLabel("commitDate", date, getTimeZone(), getTimeUtils()));
@@ -132,7 +143,7 @@ public class LogPanel extends BasePanel {
// short message
String shortMessage = entry.getShortMessage();
String trimmedMessage = shortMessage;
- if (allRefs.containsKey(entry.getId())) {
+ if (!ArrayUtils.isEmpty(entry.getRefs())) {
trimmedMessage = StringUtils.trimString(shortMessage, Constants.LEN_SHORTLOG_REFS);
} else {
trimmedMessage = StringUtils.trimString(shortMessage, Constants.LEN_SHORTLOG);
@@ -145,7 +156,7 @@ public class LogPanel extends BasePanel {
}
item.add(shortlog);
- item.add(new RefsPanel("commitRefs", repositoryName, entry, allRefs));
+ item.add(new RefsPanel("commitRefs", repositoryName, entry.getRefs()));
// commit hash link
LinkPanel commitHash = new LinkPanel("hashLink", null, entry.getName().substring(0, hashLen),
diff --git a/src/main/java/com/gitblit/wicket/panels/NestedRepositoryTreePanel.html b/src/main/java/com/gitblit/wicket/panels/NestedRepositoryTreePanel.html
new file mode 100644
index 00000000..5dfd27af
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/NestedRepositoryTreePanel.html
@@ -0,0 +1,106 @@
+<!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>
+ <tr style="background-color: #bbb" wicket:id="nodeHeader" data-row-type="folder"></tr>
+ <wicket:container wicket:id="repositories">
+ <tr wicket:id="rowContent" data-row-type="repo">
+ <td wicket:id="firstColumn" class="left"
+ style="padding-left: 3px;">
+ <div style="margin-left: 7px; width: 8px;display: inline-block;float: left;"
+ wicket:id="depth">&nbsp;</div>
+ <span wicket:id="repoIcon"></span><span
+ style="padding-left: 3px;" wicket:id="repositoryName">[repository
+ name]</span>
+ </td>
+ <td class="hidden-phone"><span class="list"
+ wicket:id="repositoryDescription">[repository description]</span></td>
+ <td class="hidden-tablet hidden-phone author"><span
+ wicket:id="repositoryOwner">[repository owner]</span></td>
+ <td class="hidden-phone"
+ style="text-align: right; padding-right: 10px;"><img
+ class="inlineIcon" wicket:id="sparkleshareIcon" /><img
+ class="inlineIcon" wicket:id="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="rightAlign hidden-phone"
+ style="text-align: right; padding-right: 15px;"><span
+ style="font-size: 0.8em;" wicket:id="repositorySize">[repository
+ size]</span></td>
+ </tr>
+ </wicket:container>
+ <tr wicket:id="subFolders">
+ <span wicket:id="rowContent"></span>
+ </tr>
+
+ <wicket:fragment wicket:id="emptyFragment">
+ </wicket:fragment>
+
+ <wicket:fragment wicket:id="repoIconFragment">
+ <span class="octicon octicon-centered octicon-repo"></span>
+ </wicket:fragment>
+
+ <wicket:fragment wicket:id="mirrorIconFragment">
+ <span class="octicon octicon-centered octicon-mirror"></span>
+ </wicket:fragment>
+
+ <wicket:fragment wicket:id="forkIconFragment">
+ <span class="octicon octicon-centered octicon-repo-forked"></span>
+ </wicket:fragment>
+
+ <wicket:fragment wicket:id="cloneIconFragment">
+ <span class="octicon octicon-centered octicon-repo-push"
+ wicket:message="title:gb.workingCopyWarning"></span>
+ </wicket:fragment>
+
+ <wicket:fragment wicket:id="tableGroupMinusCollapsible">
+ <i title="Click to expand/collapse" class="fa fa-minus-square-o table-group-collapsible" aria-hidden="true" style="padding-right:3px;cursor:pointer;"></i>
+ </wicket:fragment>
+
+ <wicket:fragment wicket:id="tableGroupPlusCollapsible">
+ <i title="Click to expand/collapse" class="fa fa-plus-square-o table-group-collapsible" aria-hidden="true" style="padding-right:3px;cursor:pointer;"></i>
+ </wicket:fragment>
+
+ <wicket:fragment wicket:id="tableAllCollapsible">
+ <i title="Click to expand all"
+ class="fa fa-plus-square-o table-openall-collapsible"
+ aria-hidden="true" style="padding-right: 3px; cursor: pointer;"></i>
+ <i title="Click to collapse all"
+ class="fa fa-minus-square-o table-closeall-collapsible"
+ aria-hidden="true" style="padding-right: 3px; cursor: pointer;"></i>
+ </wicket:fragment>
+
+ <wicket:fragment wicket:id="groupRepositoryHeader">
+ <tr>
+ <th class="left"><span wicket:id="allCollapsible"></span> <img
+ style="vertical-align: middle;" src="git-black-16x16.png" /> <wicket:message
+ key="gb.repository">Repository</wicket:message></th>
+ <th class="hidden-phone"><span><wicket:message
+ key="gb.description">Description</wicket:message></span></th>
+ <th class="hidden-tablet hidden-phone"><span><wicket:message
+ key="gb.owner">Owner</wicket:message></span></th>
+ <th class="hidden-phone"></th>
+ <th><wicket:message key="gb.lastChange">Last Change</wicket:message></th>
+ <th class="right hidden-phone"></th>
+ </tr>
+ </wicket:fragment>
+
+ <wicket:fragment wicket:id="groupRepositoryRow">
+ <td wicket:id="firstColumn" style="" colspan="1">
+ <div style="margin-left:6px; width: 10px; display: inline-block;float: left;"
+ wicket:id="depth">&nbsp;</div>
+ <span
+ wicket:id="groupCollapsible"></span><span wicket:id="groupName">[group
+ name]</span></td>
+ <td colspan="6" style="padding: 2px;"><span class="hidden-phone"
+ style="font-weight: normal; color: #666;"
+ wicket:id="groupDescription">[description]</span></td>
+ </wicket:fragment>
+
+ </wicket:panel>
+</body>
+</html>
diff --git a/src/main/java/com/gitblit/wicket/panels/NestedRepositoryTreePanel.java b/src/main/java/com/gitblit/wicket/panels/NestedRepositoryTreePanel.java
new file mode 100644
index 00000000..cc5df1a1
--- /dev/null
+++ b/src/main/java/com/gitblit/wicket/panels/NestedRepositoryTreePanel.java
@@ -0,0 +1,233 @@
+package com.gitblit.wicket.panels;
+
+import java.util.Map;
+
+import org.apache.wicket.Component;
+import org.apache.wicket.PageParameters;
+import org.apache.wicket.behavior.AttributeAppender;
+import org.apache.wicket.markup.html.WebMarkupContainer;
+import org.apache.wicket.markup.html.basic.Label;
+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.markup.repeater.RepeatingView;
+import org.apache.wicket.model.IModel;
+import org.apache.wicket.model.Model;
+
+import com.gitblit.Constants.AccessRestrictionType;
+import com.gitblit.Keys;
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.TreeNodeModel;
+import com.gitblit.models.UserModel;
+import com.gitblit.utils.ArrayUtils;
+import com.gitblit.utils.ModelUtils;
+import com.gitblit.utils.StringUtils;
+import com.gitblit.wicket.WicketUtils;
+import com.gitblit.wicket.pages.BasePage;
+import com.gitblit.wicket.pages.ProjectPage;
+import com.gitblit.wicket.pages.SummaryPage;
+import com.gitblit.wicket.pages.UserPage;
+
+public class NestedRepositoryTreePanel extends BasePanel {
+
+ private static final long serialVersionUID = 1L;
+
+ public NestedRepositoryTreePanel(final String wicketId, final IModel<TreeNodeModel> model, final Map<AccessRestrictionType, String> accessRestrictionTranslations, final boolean linksActive) {
+ super(wicketId);
+
+ final boolean showSize = app().settings().getBoolean(Keys.web.showRepositorySizes, true);
+ final boolean showSwatch = app().settings().getBoolean(Keys.web.repositoryListSwatches, true);
+
+ final TreeNodeModel node = model.getObject();
+ Fragment nodeHeader = new Fragment("nodeHeader", "groupRepositoryRow", this);
+ add(nodeHeader);
+ WebMarkupContainer firstColumn = new WebMarkupContainer("firstColumn");
+ nodeHeader.add(firstColumn);
+ RepeatingView depth = new RepeatingView("depth");
+ for(int i=0; i<node.getDepth();i++) {
+ depth.add(new WebMarkupContainer(depth.newChildId()));
+ }
+ firstColumn.add(depth);
+ firstColumn.add(new Fragment("groupCollapsible", "tableGroupMinusCollapsible", this));
+ if(node.getParent()!=null) {
+ addChildOfNodeIdCssClassesToRow(nodeHeader, node.getParent());
+ }
+ nodeHeader.add(new AttributeAppender("data-node-id", Model.of(node.hashCode()), " "));
+
+ String name = node.getName();
+ if (name.startsWith(ModelUtils.getUserRepoPrefix())) {
+ // user page
+ String username = ModelUtils.getUserNameFromRepoPath(name);
+ UserModel user = app().users().getUserModel(username);
+ firstColumn.add(new LinkPanel("groupName", null, (user == null ? username : user.getDisplayName()), UserPage.class, WicketUtils.newUsernameParameter(username)));
+ nodeHeader.add(new Label("groupDescription", getString("gb.personalRepositories")));
+ } else {
+ // project page
+ firstColumn.add(new LinkPanel("groupName", null, name, ProjectPage.class, WicketUtils.newProjectParameter(name)));
+ nodeHeader.add(new Label("groupDescription", ""));
+ }
+ WicketUtils.addCssClass(nodeHeader, "group collapsible tree");
+
+ add(new ListView<RepositoryModel>("repositories", node.getRepositories()) {
+ private static final long serialVersionUID = 1L;
+
+ int counter = 0;
+
+ @Override
+ public boolean isVisible() {
+ return super.isVisible() && !node.getRepositories().isEmpty();
+ }
+
+ @Override
+ protected void populateItem(ListItem<RepositoryModel> item) {
+
+ RepositoryModel entry = item.getModelObject();
+ WebMarkupContainer rowContent = new WebMarkupContainer("rowContent");
+ item.add(rowContent);
+ addChildOfNodeIdCssClassesToRow(rowContent, node);
+ WebMarkupContainer firstColumn = new WebMarkupContainer("firstColumn");
+ rowContent.add(firstColumn);
+ RepeatingView depth = new RepeatingView("depth");
+ for(int i=0; i<node.getDepth();i++) {
+ depth.add(new WebMarkupContainer(depth.newChildId()));
+ }
+ firstColumn.add(depth);
+
+ // show colored repository type icon
+ Fragment iconFragment;
+ if (entry.isMirror) {
+ iconFragment = new Fragment("repoIcon", "mirrorIconFragment", this);
+ } else if (entry.isFork()) {
+ iconFragment = new Fragment("repoIcon", "forkIconFragment", this);
+ } else if (entry.isBare) {
+ iconFragment = new Fragment("repoIcon", "repoIconFragment", this);
+ } else {
+ iconFragment = new Fragment("repoIcon", "cloneIconFragment", this);
+ }
+ if (showSwatch) {
+ WicketUtils.setCssStyle(iconFragment, "color:" + StringUtils.getColor(entry.toString()));
+ }
+ firstColumn.add(iconFragment);
+
+ // try to strip group name for less cluttered list
+ String repoName = StringUtils.getLastPathElement(entry.toString());
+
+ if (linksActive) {
+ Class<? extends BasePage> linkPage = SummaryPage.class;
+ PageParameters pp = WicketUtils.newRepositoryParameter(entry.name);
+ firstColumn.add(new LinkPanel("repositoryName", "list", repoName, linkPage, pp));
+ rowContent.add(new LinkPanel("repositoryDescription", "list", entry.description, linkPage, pp));
+ } else {
+ // no links like on a federation page
+ firstColumn.add(new Label("repositoryName", repoName));
+ rowContent.add(new Label("repositoryDescription", entry.description));
+ }
+ if (entry.hasCommits) {
+ // Existing repository
+ rowContent.add(new Label("repositorySize", entry.size).setVisible(showSize));
+ } else {
+ // New repository
+ rowContent.add(new Label("repositorySize", "<span class='empty'>(" + getString("gb.empty") + ")</span>").setEscapeModelStrings(false));
+ }
+
+ if (entry.isSparkleshared()) {
+ rowContent.add(WicketUtils.newImage("sparkleshareIcon", "star_16x16.png", getString("gb.isSparkleshared")));
+ } else {
+ rowContent.add(WicketUtils.newClearPixel("sparkleshareIcon").setVisible(false));
+ }
+
+ if (!entry.isMirror && entry.isFrozen) {
+ rowContent.add(WicketUtils.newImage("frozenIcon", "cold_16x16.png", getString("gb.isFrozen")));
+ } else {
+ rowContent.add(WicketUtils.newClearPixel("frozenIcon").setVisible(false));
+ }
+
+ if (entry.isFederated) {
+ rowContent.add(WicketUtils.newImage("federatedIcon", "federated_16x16.png", getString("gb.isFederated")));
+ } else {
+ rowContent.add(WicketUtils.newClearPixel("federatedIcon").setVisible(false));
+ }
+
+ if (entry.isMirror) {
+ rowContent.add(WicketUtils.newImage("accessRestrictionIcon", "mirror_16x16.png", getString("gb.isMirror")));
+ } else {
+ switch (entry.accessRestriction) {
+ case NONE:
+ rowContent.add(WicketUtils.newBlankImage("accessRestrictionIcon"));
+ break;
+ case PUSH:
+ rowContent.add(WicketUtils.newImage("accessRestrictionIcon", "lock_go_16x16.png", accessRestrictionTranslations.get(entry.accessRestriction)));
+ break;
+ case CLONE:
+ rowContent.add(WicketUtils.newImage("accessRestrictionIcon", "lock_pull_16x16.png", accessRestrictionTranslations.get(entry.accessRestriction)));
+ break;
+ case VIEW:
+ rowContent.add(WicketUtils.newImage("accessRestrictionIcon", "shield_16x16.png", accessRestrictionTranslations.get(entry.accessRestriction)));
+ break;
+ default:
+ rowContent.add(WicketUtils.newBlankImage("accessRestrictionIcon"));
+ }
+ }
+
+ String owner = "";
+ if (!ArrayUtils.isEmpty(entry.owners)) {
+ // display first owner
+ for (String username : entry.owners) {
+ UserModel ownerModel = app().users().getUserModel(username);
+ if (ownerModel != null) {
+ owner = ownerModel.getDisplayName();
+ break;
+ }
+ }
+ if (entry.owners.size() > 1) {
+ owner += ", ...";
+ }
+ }
+ Label ownerLabel = new Label("repositoryOwner", owner);
+ WicketUtils.setHtmlTooltip(ownerLabel, ArrayUtils.toString(entry.owners));
+ rowContent.add(ownerLabel);
+
+ String lastChange;
+ if (entry.lastChange.getTime() == 0) {
+ lastChange = "--";
+ } else {
+ lastChange = getTimeUtils().timeAgo(entry.lastChange);
+ }
+ Label lastChangeLabel = new Label("repositoryLastChange", lastChange);
+ rowContent.add(lastChangeLabel);
+ WicketUtils.setCssClass(lastChangeLabel, getTimeUtils().timeAgoCss(entry.lastChange));
+ if (!StringUtils.isEmpty(entry.lastChangeAuthor)) {
+ WicketUtils.setHtmlTooltip(lastChangeLabel, getString("gb.author") + ": " + entry.lastChangeAuthor);
+ }
+
+ String clazz = counter % 2 == 0 ? "light" : "dark";
+ WicketUtils.addCssClass(rowContent, clazz);
+ counter++;
+ }
+ });
+
+ add(new ListView<TreeNodeModel>("subFolders", node.getSubFolders()) {
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ protected void populateItem(ListItem<TreeNodeModel> item) {
+ item.add(new NestedRepositoryTreePanel("rowContent", item.getModel(), accessRestrictionTranslations, linksActive));
+ }
+
+ @Override
+ public boolean isVisible() {
+ return super.isVisible() && !node.getSubFolders().isEmpty();
+ }
+ });
+
+
+
+ }
+
+ private void addChildOfNodeIdCssClassesToRow(Component row, TreeNodeModel parentNode) {
+ row.add(new AttributeAppender("class", Model.of("child-of-"+ parentNode.hashCode()), " "));
+ if(parentNode.getParent() != null) {
+ addChildOfNodeIdCssClassesToRow(row, parentNode.getParent());
+ }
+ }
+}
diff --git a/src/main/java/com/gitblit/wicket/panels/PagerPanel.java b/src/main/java/com/gitblit/wicket/panels/PagerPanel.java
index 2d774c41..d1214cae 100644
--- a/src/main/java/com/gitblit/wicket/panels/PagerPanel.java
+++ b/src/main/java/com/gitblit/wicket/panels/PagerPanel.java
@@ -48,7 +48,7 @@ public class PagerPanel extends Panel {
deltas = new int[] { -2, -1, 0, 1, 2 };
}
- if (totalPages > 0) {
+ if (totalPages > 0 && currentPage > 1) {
pages.add(new PageObject("\u2190", currentPage - 1));
}
for (int delta : deltas) {
@@ -57,7 +57,7 @@ public class PagerPanel extends Panel {
pages.add(new PageObject("" + page, page));
}
}
- if (totalPages > 0) {
+ if (totalPages > 0 && currentPage < totalPages) {
pages.add(new PageObject("\u2192", currentPage + 1));
}
@@ -75,6 +75,7 @@ public class PagerPanel extends Panel {
item.add(link);
if (pageItem.page == currentPage || pageItem.page < 1 || pageItem.page > totalPages) {
WicketUtils.setCssClass(item, "disabled");
+ link.setEnabled(false);
}
}
};
diff --git a/src/main/java/com/gitblit/wicket/panels/RepositoriesPanel.html b/src/main/java/com/gitblit/wicket/panels/RepositoriesPanel.html
index 2de52b09..35a26b61 100644
--- a/src/main/java/com/gitblit/wicket/panels/RepositoriesPanel.html
+++ b/src/main/java/com/gitblit/wicket/panels/RepositoriesPanel.html
@@ -18,6 +18,9 @@
</tbody>
</table>
+ <wicket:fragment wicket:id="emptyFragment">
+ </wicket:fragment>
+
<wicket:fragment wicket:id="repoIconFragment">
<span class="octicon octicon-centered octicon-repo"></span>
</wicket:fragment>
@@ -72,9 +75,15 @@
</tr>
</wicket:fragment>
+ <wicket:fragment wicket:id="tableAllCollapsible">
+ <i title="Click to expand all" class="fa fa-plus-square-o table-openall-collapsible" aria-hidden="true" style="padding-right:3px;cursor:pointer;"></i>
+ <i title="Click to collapse all" class="fa fa-minus-square-o table-closeall-collapsible" aria-hidden="true" style="padding-right:3px;cursor:pointer;"></i>
+ </wicket:fragment>
+
<wicket:fragment wicket:id="groupRepositoryHeader">
<tr>
<th class="left">
+ <span wicket:id="allCollapsible"></span>
<img style="vertical-align: middle;" src="git-black-16x16.png"/>
<wicket:message key="gb.repository">Repository</wicket:message>
</th>
@@ -86,8 +95,16 @@
</tr>
</wicket:fragment>
+ <wicket:fragment wicket:id="tableGroupMinusCollapsible">
+ <i title="Click to expand/collapse" class="fa fa-minus-square-o table-group-collapsible" aria-hidden="true" style="padding-right:3px;cursor:pointer;"></i>
+ </wicket:fragment>
+
+ <wicket:fragment wicket:id="tableGroupPlusCollapsible">
+ <i title="Click to expand/collapse" class="fa fa-plus-square-o table-group-collapsible" aria-hidden="true" style="padding-right:3px;cursor:pointer;"></i>
+ </wicket:fragment>
+
<wicket:fragment wicket:id="groupRepositoryRow">
- <td colspan="1"><span wicket:id="groupName">[group name]</span></td>
+ <td colspan="1"><span wicket:id="groupCollapsible"></span><span wicket:id="groupName">[group name]</span></td>
<td colspan="6" style="padding: 2px;"><span class="hidden-phone" style="font-weight:normal;color:#666;" wicket:id="groupDescription">[description]</span></td>
</wicket:fragment>
@@ -102,4 +119,4 @@
</wicket:panel>
</body>
-</html> \ No newline at end of file
+</html>
diff --git a/src/main/java/com/gitblit/wicket/panels/RepositoriesPanel.java b/src/main/java/com/gitblit/wicket/panels/RepositoriesPanel.java
index c3f07099..982f8b2d 100644
--- a/src/main/java/com/gitblit/wicket/panels/RepositoriesPanel.java
+++ b/src/main/java/com/gitblit/wicket/panels/RepositoriesPanel.java
@@ -28,6 +28,7 @@ import org.apache.wicket.PageParameters;
import org.apache.wicket.extensions.markup.html.repeater.data.sort.OrderByBorder;
import org.apache.wicket.extensions.markup.html.repeater.util.SortParam;
import org.apache.wicket.extensions.markup.html.repeater.util.SortableDataProvider;
+import org.apache.wicket.markup.html.WebMarkupContainer;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.link.BookmarkablePageLink;
import org.apache.wicket.markup.html.link.Link;
@@ -43,6 +44,7 @@ import com.gitblit.Constants.AccessRestrictionType;
import com.gitblit.Keys;
import com.gitblit.models.ProjectModel;
import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.TreeNodeModel;
import com.gitblit.models.UserModel;
import com.gitblit.utils.ArrayUtils;
import com.gitblit.utils.ModelUtils;
@@ -59,6 +61,26 @@ public class RepositoriesPanel extends BasePanel {
private static final long serialVersionUID = 1L;
+
+ private enum CollapsibleRepositorySetting {
+ DISABLED,
+
+ EXPANDED,
+
+ COLLAPSED;
+
+ public static CollapsibleRepositorySetting get(String name) {
+ CollapsibleRepositorySetting returnVal = CollapsibleRepositorySetting.DISABLED;
+ for (CollapsibleRepositorySetting setting : values()) {
+ if (setting.name().equalsIgnoreCase(name)) {
+ returnVal = setting;
+ break;
+ }
+ }
+ return returnVal;
+ }
+ }
+
public RepositoriesPanel(String wicketId, final boolean showAdmin, final boolean showManagement,
List<RepositoryModel> models, boolean enableLinks,
final Map<AccessRestrictionType, String> accessRestrictionTranslations) {
@@ -66,10 +88,12 @@ public class RepositoriesPanel extends BasePanel {
final boolean linksActive = enableLinks;
final boolean showSize = app().settings().getBoolean(Keys.web.showRepositorySizes, true);
+ final String collapsibleRespositorySetting = app().settings().getString(Keys.web.collapsibleRepositoryGroups, null);
+ final CollapsibleRepositorySetting collapsibleRepoGroups = CollapsibleRepositorySetting.get(collapsibleRespositorySetting);
final UserModel user = GitBlitWebSession.get().getUser();
- final IDataProvider<RepositoryModel> dp;
+ IDataProvider<RepositoryModel> dp = null;
Fragment managementLinks;
if (showAdmin) {
@@ -97,7 +121,28 @@ public class RepositoriesPanel extends BasePanel {
add (new Label("managementPanel").setVisible(false));
}
- if (app().settings().getString(Keys.web.repositoryListType, "flat").equalsIgnoreCase("grouped")) {
+ if (app().settings().getString(Keys.web.repositoryListType, "flat").equalsIgnoreCase("tree")) {
+ TreeNodeModel tree = new TreeNodeModel();
+ for (RepositoryModel model : models) {
+ String rootPath = StringUtils.getRootPath(model.name);
+ if (StringUtils.isEmpty(rootPath)) {
+ tree.add(model);
+ } else {
+ // create folder structure
+ tree.add(rootPath, model);
+ }
+ }
+
+ WebMarkupContainer container = new WebMarkupContainer("row");
+ add(container);
+ container.add(new NestedRepositoryTreePanel("rowContent", Model.of(tree), accessRestrictionTranslations, enableLinks));
+
+ Fragment fragment = new Fragment("headerContent", "groupRepositoryHeader", this);
+ Fragment allCollapsible = new Fragment("allCollapsible", "tableAllCollapsible", this);
+ fragment.add(allCollapsible);
+ add(fragment);
+
+ } else if (app().settings().getString(Keys.web.repositoryListType, "flat").equalsIgnoreCase("grouped")) {
List<RepositoryModel> rootRepositories = new ArrayList<RepositoryModel>();
Map<String, List<RepositoryModel>> groups = new HashMap<String, List<RepositoryModel>>();
for (RepositoryModel model : models) {
@@ -140,6 +185,7 @@ public class RepositoriesPanel extends BasePanel {
dp = new SortableRepositoriesProvider(models);
}
+ if (dp != null) {
final boolean showSwatch = app().settings().getBoolean(Keys.web.repositoryListSwatches, true);
DataView<RepositoryModel> dataView = new DataView<RepositoryModel>("row", dp) {
@@ -160,6 +206,16 @@ public class RepositoriesPanel extends BasePanel {
GroupRepositoryModel groupRow = (GroupRepositoryModel) entry;
currGroupName = entry.name;
Fragment row = new Fragment("rowContent", "groupRepositoryRow", this);
+ if(collapsibleRepoGroups == CollapsibleRepositorySetting.EXPANDED) {
+ Fragment groupCollapsible = new Fragment("groupCollapsible", "tableGroupMinusCollapsible", this);
+ row.add(groupCollapsible);
+ } else if(collapsibleRepoGroups == CollapsibleRepositorySetting.COLLAPSED) {
+ Fragment groupCollapsible = new Fragment("groupCollapsible", "tableGroupPlusCollapsible", this);
+ row.add(groupCollapsible);
+ } else {
+ Fragment groupCollapsible = new Fragment("groupCollapsible", "emptyFragment", this);
+ row.add(groupCollapsible);
+ }
item.add(row);
String name = groupRow.name;
@@ -174,7 +230,7 @@ public class RepositoriesPanel extends BasePanel {
row.add(new LinkPanel("groupName", null, groupRow.toString(), ProjectPage.class, WicketUtils.newProjectParameter(entry.name)));
row.add(new Label("groupDescription", entry.description == null ? "":entry.description));
}
- WicketUtils.setCssClass(item, "group");
+ WicketUtils.setCssClass(item, "group collapsible");
// reset counter so that first row is light background
counter = 0;
return;
@@ -319,8 +375,17 @@ public class RepositoriesPanel extends BasePanel {
} else {
// not sortable
Fragment fragment = new Fragment("headerContent", "groupRepositoryHeader", this);
+ if(collapsibleRepoGroups == CollapsibleRepositorySetting.EXPANDED ||
+ collapsibleRepoGroups == CollapsibleRepositorySetting.COLLAPSED) {
+ Fragment allCollapsible = new Fragment("allCollapsible", "tableAllCollapsible", this);
+ fragment.add(allCollapsible);
+ } else {
+ Fragment allCollapsible = new Fragment("allCollapsible", "emptyFragment", this);
+ fragment.add(allCollapsible);
+ }
add(fragment);
}
+ }
}
private static class GroupRepositoryModel extends RepositoryModel {
diff --git a/src/main/java/com/gitblit/wicket/panels/SearchPanel.java b/src/main/java/com/gitblit/wicket/panels/SearchPanel.java
index 09322bc7..b29adcb5 100644
--- a/src/main/java/com/gitblit/wicket/panels/SearchPanel.java
+++ b/src/main/java/com/gitblit/wicket/panels/SearchPanel.java
@@ -15,6 +15,7 @@
*/
package com.gitblit.wicket.panels;
+import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
@@ -31,6 +32,8 @@ import org.eclipse.jgit.revwalk.RevCommit;
import com.gitblit.Constants;
import com.gitblit.Keys;
import com.gitblit.models.RefModel;
+import com.gitblit.models.RepositoryCommit;
+import com.gitblit.utils.ArrayUtils;
import com.gitblit.utils.JGitUtils;
import com.gitblit.utils.StringUtils;
import com.gitblit.wicket.WicketUtils;
@@ -43,7 +46,7 @@ public class SearchPanel extends BasePanel {
private static final long serialVersionUID = 1L;
- private boolean hasMore;
+ private final boolean hasMore;
public SearchPanel(String wicketId, final String repositoryName, final String objectId,
final String value, Constants.SearchType searchType, Repository r, int limit, int pageOffset,
@@ -57,7 +60,7 @@ public class SearchPanel extends BasePanel {
RevCommit commit = JGitUtils.getCommit(r, objectId);
- final Map<ObjectId, List<RefModel>> allRefs = JGitUtils.getAllRefs(r, showRemoteRefs);
+ Map<ObjectId, List<RefModel>> allRefs = JGitUtils.getAllRefs(r, showRemoteRefs);
List<RevCommit> commits;
if (pageResults) {
// Paging result set
@@ -78,15 +81,23 @@ public class SearchPanel extends BasePanel {
add(new Label("searchString", value));
add(new Label("searchType", searchType.toString()));
- ListDataProvider<RevCommit> dp = new ListDataProvider<RevCommit>(commits);
- DataView<RevCommit> searchView = new DataView<RevCommit>("commit", dp) {
+ List<RepositoryCommit> repoCommits = new ArrayList<>(commits.size());
+ for (RevCommit c : commits) {
+ RepositoryCommit repoCommit = new RepositoryCommit(repositoryName, "", c);
+ if (allRefs.containsKey(c)) {
+ repoCommit.setRefs(allRefs.get(c));
+ }
+ repoCommits.add(repoCommit);
+ }
+ ListDataProvider<RepositoryCommit> dp = new ListDataProvider<RepositoryCommit>(repoCommits);
+ DataView<RepositoryCommit> searchView = new DataView<RepositoryCommit>("commit", dp) {
private static final long serialVersionUID = 1L;
int counter;
@Override
- public void populateItem(final Item<RevCommit> item) {
- final RevCommit entry = item.getModelObject();
- final Date date = JGitUtils.getAuthorDate(entry);
+ public void populateItem(final Item<RepositoryCommit> item) {
+ final RepositoryCommit entry = item.getModelObject();
+ final Date date = entry.getAuthorIdent().getWhen();
item.add(WicketUtils.createDateLabel("commitDate", date, getTimeZone(), getTimeUtils()));
@@ -107,7 +118,7 @@ public class SearchPanel extends BasePanel {
String shortMessage = entry.getShortMessage();
String trimmedMessage = shortMessage;
- if (allRefs.containsKey(entry.getId())) {
+ if (!ArrayUtils.isEmpty(entry.getRefs())) {
trimmedMessage = StringUtils.trimString(shortMessage, Constants.LEN_SHORTLOG_REFS);
} else {
trimmedMessage = StringUtils.trimString(shortMessage, Constants.LEN_SHORTLOG);
@@ -120,7 +131,7 @@ public class SearchPanel extends BasePanel {
}
item.add(shortlog);
- item.add(new RefsPanel("commitRefs", repositoryName, entry, allRefs));
+ item.add(new RefsPanel("commitRefs", repositoryName, entry.getRefs()));
item.add(new BookmarkablePageLink<Void>("commit", CommitPage.class, WicketUtils
.newObjectParameter(repositoryName, entry.getName())));
diff --git a/src/main/java/com/gitblit/wicket/panels/SshKeysPanel.java b/src/main/java/com/gitblit/wicket/panels/SshKeysPanel.java
index 15ebd67b..4ab2483a 100644
--- a/src/main/java/com/gitblit/wicket/panels/SshKeysPanel.java
+++ b/src/main/java/com/gitblit/wicket/panels/SshKeysPanel.java
@@ -48,11 +48,13 @@ public class SshKeysPanel extends BasePanel {
private static final long serialVersionUID = 1L;
private final UserModel user;
+ private final boolean canWriteKeys;
public SshKeysPanel(String wicketId, UserModel user) {
super(wicketId);
this.user = user;
+ this.canWriteKeys = app().keys().supportsWritingKeys(user);
}
@Override
@@ -90,6 +92,9 @@ public class SshKeysPanel extends BasePanel {
}
}
};
+ if (!canWriteKeys) {
+ delete.setVisibilityAllowed(false);
+ }
item.add(delete);
}
};
@@ -113,7 +118,7 @@ public class SshKeysPanel extends BasePanel {
final IModel<String> keyComment = Model.of("");
addKeyForm.add(new TextOption("addKeyComment",
- getString("gb.comment"),
+ getString("gb.sshKeyComment"),
getString("gb.sshKeyCommentDescription"),
"span5",
keyComment));
@@ -164,6 +169,10 @@ public class SshKeysPanel extends BasePanel {
}
});
+ if (! canWriteKeys) {
+ addKeyForm.setVisibilityAllowed(false);
+ }
+
add(addKeyForm);
}
}
diff --git a/src/main/java/login_cs.mkd b/src/main/java/login_cs.mkd
new file mode 100644
index 00000000..23948d31
--- /dev/null
+++ b/src/main/java/login_cs.mkd
@@ -0,0 +1,3 @@
+## Přihlašte se prosím
+
+Zadejte prosím svoje přihlašovací údaje pro přístup na tuto stránku Gitblitu.
diff --git a/src/main/java/login_ja.mkd b/src/main/java/login_ja.mkd
new file mode 100644
index 00000000..535d9227
--- /dev/null
+++ b/src/main/java/login_ja.mkd
@@ -0,0 +1,3 @@
+## ログインしてください
+
+Gitblit サーバーに入るには、ユーザーアカウントとパスワードが必要です。
diff --git a/src/main/java/login_ru.mkd b/src/main/java/login_ru.mkd
new file mode 100644
index 00000000..d6c4d105
--- /dev/null
+++ b/src/main/java/login_ru.mkd
@@ -0,0 +1,3 @@
+## Please Login
+
+Пожалуйста, введите свои учетные данные для доступа к этому сайту Gitblit.
diff --git a/src/main/java/welcome_cs.mkd b/src/main/java/welcome_cs.mkd
new file mode 100644
index 00000000..355a94c1
--- /dev/null
+++ b/src/main/java/welcome_cs.mkd
@@ -0,0 +1,3 @@
+## Vítejte v Gitblitu
+
+Rychá a snadná cesta pro hostování a prohlížení vašich [Git](http://www.git-scm.com) repozitářů.
diff --git a/src/main/java/welcome_ja.mkd b/src/main/java/welcome_ja.mkd
new file mode 100644
index 00000000..75c3140f
--- /dev/null
+++ b/src/main/java/welcome_ja.mkd
@@ -0,0 +1,3 @@
+## Gitblit へようこそ !
+
+あなたの [Git](http://www.git-scm.com) リポジトリサーバーを、最も簡単に手早く構築するための手段です
diff --git a/src/main/java/welcome_ru.mkd b/src/main/java/welcome_ru.mkd
new file mode 100644
index 00000000..cd5ddd8f
--- /dev/null
+++ b/src/main/java/welcome_ru.mkd
@@ -0,0 +1,3 @@
+## Welcome to Gitblit
+
+Быстрый и простой способ разместить или просмотреть свой собственный [Git](http://www.git-scm.com)-репозиторий.
diff --git a/src/main/resources/bootstrap-fixes.css b/src/main/resources/bootstrap-fixes.css
new file mode 100644
index 00000000..c9b6154b
--- /dev/null
+++ b/src/main/resources/bootstrap-fixes.css
@@ -0,0 +1,25 @@
+/**
+ * Disabled links in a PagerPanel. Bootstrap 2.0.4 only handles <a>, but not <span>. Wicket renders disabled links as spans.
+ * The .pagination rules here are identical to the ones for <a> in bootstrap.css, but for <span>.
+ */
+.pagination span {
+ float: left;
+ padding: 0 14px;
+ line-height: 34px;
+ text-decoration: none;
+ border: 1px solid #ddd;
+ border-left-width: 0;
+}
+
+.pagination li:first-child span {
+ border-left-width: 1px;
+ -webkit-border-radius: 3px 0 0 3px;
+ -moz-border-radius: 3px 0 0 3px;
+ border-radius: 3px 0 0 3px;
+}
+
+.pagination li:last-child span {
+ -webkit-border-radius: 0 3px 3px 0;
+ -moz-border-radius: 0 3px 3px 0;
+ border-radius: 0 3px 3px 0;
+}
diff --git a/src/main/resources/gitblit.css b/src/main/resources/gitblit.css
index 10c9a0e8..f7271788 100644
--- a/src/main/resources/gitblit.css
+++ b/src/main/resources/gitblit.css
@@ -45,6 +45,7 @@ a.bugtraq {
.octicon-centered {
text-align: center;
width: 16px;
+ padding-left: 17px;
}
tr:hover .octicon-centered {
@@ -1963,6 +1964,7 @@ td.rightAlign {
td.treeLinks {
text-align: right;
width: 13em;
+ white-space: nowrap;
}
span.help-inline {
diff --git a/src/main/resources/gitblit/js/collapsible-table.js b/src/main/resources/gitblit/js/collapsible-table.js
new file mode 100644
index 00000000..ca89b8fd
--- /dev/null
+++ b/src/main/resources/gitblit/js/collapsible-table.js
@@ -0,0 +1,71 @@
+$(function() {
+ $('i.table-group-collapsible')
+ .click(function(){
+ var nodeId = $(this).closest('tr.group.collapsible.tree').data('nodeId');
+ if(nodeId!==undefined){
+ //we are in tree view
+ if($(this).hasClass('fa-minus-square-o')){
+ $(this).closest('tr.group.collapsible.tree').nextAll('tr.child-of-'+nodeId).hide();
+ $(this).closest('tr.group.collapsible.tree').nextAll('tr.child-of-'+nodeId).addClass('hidden-by-'+nodeId);
+ }else{
+ $(this).closest('tr.group.collapsible.tree').nextAll('tr.child-of-'+nodeId).removeClass('hidden-by-'+nodeId);
+ $(this).closest('tr.group.collapsible.tree').nextAll('tr.child-of-'+nodeId+':not([class*="hidden-by-"])').show();
+ }
+ }else{
+ $(this).closest('tr.group.collapsible').nextUntil('tr.group.collapsible').toggle();
+ }
+ $(this).toggleClass('fa-minus-square-o');
+ $(this).toggleClass('fa-plus-square-o');
+ });
+
+
+ $('i.table-openall-collapsible')
+ .click(function(){
+ $('tr.group.collapsible').first().find('i').addClass('fa-minus-square-o');
+ $('tr.group.collapsible').first().find('i').removeClass('fa-plus-square-o');
+ $('tr.group.collapsible').first().nextAll('tr:not(tr.group.collapsible),tr.group.collapsible.tree').show();
+ $('tr.group.collapsible').first().nextAll('tr.group.collapsible').find('i').addClass('fa-minus-square-o');
+ $('tr.group.collapsible').first().nextAll('tr.group.collapsible').find('i').removeClass('fa-plus-square-o');
+
+ var nodeId = $('tr.group.collapsible.tree').data('nodeId');
+ if(nodeId!==undefined){
+ //we are in tree view
+ $('tr[class*="child-of-"]').removeClass(function(index, className){
+ return (className.match(/\hidden-by-\S+/g)||[]).join(' ');
+ });
+ $('tr.group.collapsible > i').addClass('fa-minus-square-o');
+ $('tr.group.collapsible > i').removeClass('fa-plus-square-o');
+ }
+ });
+
+ $('i.table-closeall-collapsible')
+ .click(function(){
+ $('tr.group.collapsible').first().find('i').addClass('fa-plus-square-o');
+ $('tr.group.collapsible').first().find('i').removeClass('fa-minus-square-o');
+ $('tr.group.collapsible').first().nextAll('tr:not(tr.group.collapsible),tr.group.collapsible.tree').hide();
+ $('tr.group.collapsible').first().nextAll('tr.group.collapsible').find('i').addClass('fa-plus-square-o');
+ $('tr.group.collapsible').first().nextAll('tr.group.collapsible').find('i').removeClass('fa-minus-square-o');
+
+ var nodeId = $('tr.group.collapsible.tree').first().data('nodeId');
+ if(nodeId!==undefined){
+ //we are in tree view, hide all sub trees
+ $('tr[class*="child-of-"]').each(function(){
+ var row = $(this);
+ var classList = row.attr('class').split('/\s+/');
+ $.each(classList, function(index, c){
+ if(c.match(/^child-of-*/)){
+ row.addClass(c.replace(/^child-of-(\d)/, 'hidden-by-$1'));
+ }
+ });
+ });
+ $('tr.group.collapsible i').addClass('fa-plus-square-o');
+ $('tr.group.collapsible i').removeClass('fa-minus-square-o');
+ }
+ });
+
+ $( document ).ready(function() {
+ if($('tr.group.collapsible').first().find('i').hasClass('fa-plus-square-o')) {
+ $('tr.group.collapsible').first().nextAll('tr:not(tr.group.collapsible),tr.group.collapsible.tree').hide();
+ }
+ });
+});
diff --git a/src/site/administration.mkd b/src/site/administration.mkd
index 049a8273..fed71e8a 100644
--- a/src/site/administration.mkd
+++ b/src/site/administration.mkd
@@ -169,7 +169,7 @@ Usernames must be unique and are case-insensitive.
Whitespace is illegal.
### Passwords
-User passwords are CASE-SENSITIVE and may be *plain*, *md5*, or *combined-md5* formatted (see `gitblit.properties` -> *realm.passwordStorage*).
+User passwords are CASE-SENSITIVE and may be *plain*, *md5*, *combined-md5* or *pbkdf2* formatted (see `gitblit.properties` -> *realm.passwordStorage*).
### User Roles
There are four actual *roles* in Gitblit:
diff --git a/src/site/design.mkd b/src/site/design.mkd
index 9ef302c1..619880ea 100644
--- a/src/site/design.mkd
+++ b/src/site/design.mkd
@@ -15,7 +15,7 @@ The following dependencies are bundled with Gitblit.
- [Iconic](http://somerandomdude.com/work/iconic) (Creative Commons Share Alike 3.0)
- [AngularJS](http://angularjs.org) (MIT)
- [Clippy](https://github.com/mojombo/clippy) (MIT)
-- [google-code-prettify](http://code.google.com/p/google-code-prettify) (Apache 2.0)
+- [google-code-prettify](https://github.com/googlearchive/code-prettify) (Apache 2.0)
- [Commons Daemon](http://commons.apache.org/daemon) (Apache 2.0)
- [jQuery](https://jquery.org) (MIT)
- [flotr2](http://humblesoftware.com/flotr2) (BSD)
@@ -38,7 +38,7 @@ The following dependencies are automatically downloaded by Gitblit GO (or alread
- [JSch - Java Secure Channel](http://www.jcraft.com/jsch) (BSD)
- [Rome](http://rome.dev.java.net) (Apache 1.1)
- [jdom](http://www.jdom.org) (Apache-style JDOM license)
-- [google-gson](http://code.google.com/google-gson) (Apache 2.0)
+- [google-gson](https://github.com/google/gson) (Apache 2.0)
- [javamail](http://kenai.com/projects/javamail) (CDDL-1.0, BSD, GPL-2.0, GNU-Classpath)
- [Groovy](http://groovy.codehaus.org) (Apache 2.0)
- [Lucene](http://lucene.apache.org) (Apache 2.0)
@@ -50,14 +50,14 @@ The following dependencies are automatically downloaded by Gitblit GO (or alread
- [FreeMarker](http://www.freemarker.org) (modified BSD)
- [Waffle](http://dblock.github.io/waffle) (EPL 1.0)
- [JNA](https://github.com/twall/jna) (LGPL 2.1)
-- [Guava](https://code.google.com/p/guava-libraries) (Apache 2.0)
+- [Guava](https://github.com/google/guava) (Apache 2.0)
- [libpam4j](https://github.com/kohsuke/libpam4j) (MIT)
- [commons-codec](http://commons.apache.org/proper/commons-codec) (Apache 2.0)
- [pegdown](https://github.com/sirthias/pegdown) (Apache 2.0)
- [jedis](https://github.com/xetorthio/jedis) (MIT)
- [Mina SSHD](https://mina.apache.org) (Apache 2.0)
- [pf4j](https://github.com/decebals/pf4j) (Apache 2.0)
-- [google-guice](https://code.google.com/p/google-guice) (Apache 2.0)
+- [google-guice](https://github.com/google/guice) (Apache 2.0)
### Other Build Dependencies
- [Fancybox image viewer](http://fancybox.net) (MIT and GPL dual-licensed)
diff --git a/src/site/faq.mkd b/src/site/faq.mkd
index 160808d6..4b5d2d61 100644
--- a/src/site/faq.mkd
+++ b/src/site/faq.mkd
@@ -2,7 +2,7 @@
### push failed for branch (n/a (unpacker error))
-This is a bug in JGit (issue-408). TLDR: Newer git clients are optimized to send less data on the wire. JGit expects complete data to be sent, but there are scenarios where native git can optimize-out sending objects. By default, JGit requires everything sent be complete and referenceable.
+This is a bug in JGit (issue-704). TLDR: Newer git clients are optimized to send less data on the wire. JGit expects complete data to be sent, but there are scenarios where native git can optimize-out sending objects. By default, JGit requires everything sent be complete and referenceable.
If you experience this, the workaround is to temporarily disable the reachable check for the receive pack, push, and then re-enable the setting.
diff --git a/src/site/federation.mkd b/src/site/federation.mkd
index 7574a321..9fdcf4c6 100644
--- a/src/site/federation.mkd
+++ b/src/site/federation.mkd
@@ -17,7 +17,7 @@ The *Gitblit 0.8.0* federation protocol adds retrieval of teams and referenced p
The *Gitblit 0.7.0* federation protocol is incompatible with the 0.6.0 federation protocol because of a change in the way timestamps are formatted.
-Gitblit 0.6.0 uses the default [google-gson](http://google-gson.googlecode.com) timestamp serializer which generates locally formatted timestamps. Unfortunately, this creates problems for distributed repositories and distributed developers. Gitblit 0.7.0 corrects this error by serializing dates to the [iso8601](http://en.wikipedia.org/wiki/ISO_8601) standard. As a result 0.7.0 is not compatible with 0.6.0. A partial backwards-compatibility fallback was considered but it would only work one direction and since the federation mechanism is bidirectional it was not implemented.
+Gitblit 0.6.0 uses the default [google-gson](https://github.com/google/gson) timestamp serializer which generates locally formatted timestamps. Unfortunately, this creates problems for distributed repositories and distributed developers. Gitblit 0.7.0 corrects this error by serializing dates to the [iso8601](http://en.wikipedia.org/wiki/ISO_8601) standard. As a result 0.7.0 is not compatible with 0.6.0. A partial backwards-compatibility fallback was considered but it would only work one direction and since the federation mechanism is bidirectional it was not implemented.
### Origin Gitblit Instance Requirements
@@ -132,7 +132,7 @@ Origin Gitblit instances can not directly track the success or failure status of
### How does it work? (Origin Gitblit Instances)
-A pulling Gitblit instance will periodically contact your Gitblit instance and will provide the token as proof that you have granted it federation access. Your Gitblit instance will decide, based on the supplied token, if the requested data should be returned to the pulling Gitblit instance. Gitblit data (user accounts, repository metadata, and server settings) are serialized as [JSON](http://json.org) using [google-gson](http://google-gson.googlecode.com) and returned to the pulling Gitblit instance. Standard Git clone and pull operations are used to transfer commits.
+A pulling Gitblit instance will periodically contact your Gitblit instance and will provide the token as proof that you have granted it federation access. Your Gitblit instance will decide, based on the supplied token, if the requested data should be returned to the pulling Gitblit instance. Gitblit data (user accounts, repository metadata, and server settings) are serialized as [JSON](http://json.org) using [google-gson](https://github.com/google/gson) and returned to the pulling Gitblit instance. Standard Git clone and pull operations are used to transfer commits.
The federation process executes using an internal administrator account, *$gitblit*. All the normal authentication and authorization processes are used for federation requests. For example, Git commands are authenticated as *$gitblit / token*.
@@ -313,7 +313,7 @@ The repositories will be put in *git.repositoriesFolder*/example4.
## Federation Client
-Instead of setting up a full-blown pulling Gitblit instance, you can also use the [federation client](http://code.google.com/p/gitblit/downloads/detail?name=%FEDCLIENT%) command-line utility. This is a packaged subset of the federation feature in a smaller, simpler command-line only tool.
+Instead of setting up a full-blown pulling Gitblit instance, you can also use the [federation client](https://github.com/gitblit/gitblit/releases/latest) command-line utility. This is a packaged subset of the federation feature in a smaller, simpler command-line only tool.
The *federation client* relies on many of the same dependencies as Gitblit and will download them on first execution.
@@ -332,9 +332,9 @@ If you are pulling from a Gitblit with a self-signed SSL certificate you will ne
### Command-Line Parameters
Instead of using `federation.properties` you may directly specify a Gitblit instance to pull from with command-line parameters.
- java -jar fedclient.jar --url https://go.gitblit.com --mirror --bare --token 123456789
+ java -cp fedclient.jar;"%CD%/ext/*" com.gitblit.FederationClient --url https://go.gitblit.com --mirror --bare --token 123456789
--repositoriesFolder c:/mymirror
- java -jar fedclient.jar --url https://go.gitblit.com --mirror --bare --token 123456789
- --repositoriesFolder c:/mymirror --daemon --frequency "24 hours"
+ java -cp "fedclient.jar:ext/*" com.gitblit.FederationClient --url https://go.gitblit.com --mirror --bare --token 123456789
+ --repositoriesFolder /srv/mymirror --daemon --frequency "24 hours"
diff --git a/src/site/rpc.mkd b/src/site/rpc.mkd
index 4b065bf0..0e0093e8 100644
--- a/src/site/rpc.mkd
+++ b/src/site/rpc.mkd
@@ -8,7 +8,7 @@ Gitblit optionally allows a remote client to administer the Gitblit server. Thi
web.enableRpcManagement=false
web.enableRpcAdministration=false
-**https** is strongly recommended because passwords are insecurely transmitted form your browser/rpc client using Basic authentication!
+**https** is strongly recommended because passwords are insecurely transmitted from your browser/rpc client using Basic authentication!
The Gitblit JSON RPC mechanism, like the Gitblit JGit servlet, syndication/feed servlet, etc, supports request-based authentication. Making an *admin* request will trigger Gitblit's basic authentication mechanism. Listing of repositories, generally, will not trigger this authentication mechanism unless *web.authenticateViewPages=true*. That means its possible to allow anonymous enumeration of repositories that are not *view restricted* or *clone restricted*. Of course, if credentials are provided then all private repositories that are available to the user account will be enumerated in the JSON response.
@@ -16,7 +16,7 @@ The Gitblit JSON RPC mechanism, like the Gitblit JGit servlet, syndication/feed
The Gitblit Manager is an example Java/Swing application that allows remote management (repository and user objects) and administration (server settings) of a Gitblit server.
-This application uses a combination of RSS feeds and the JSON RPC interface, both of which are part of the [Gitblit API](http://code.google.com/p/gitblit/downloads/detail?name=%API%) library, to present live information from a Gitblit server. Some JSON RPC methods from the utility class `com.gitblit.utils.RpcUtils` are not currently used by the Gitblit Manager.
+This application uses a combination of RSS feeds and the JSON RPC interface, both of which are part of the [Gitblit API](https://github.com/gitblit/gitblit/releases/latest) library, to present live information from a Gitblit server. Some JSON RPC methods from the utility class `com.gitblit.utils.RpcUtils` are not currently used by the Gitblit Manager.
**NOTE:**
Gitblit Manager stores your login credentials **INSECURELY** in homedir/.gitblit/config.
@@ -62,6 +62,7 @@ The Gitblit API includes methods for retrieving and interpreting RSS feeds. The
<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>
+<tr><td>Gitblit v1.9.0</td><td>9</td></tr>
</tbody>
</table>
@@ -80,7 +81,7 @@ Use *SET_REPOSITORY_TEAM_PERMISSIONS* instead.
<tr><td colspan='6'><em>web.enableRpcServlet=true</em></td></tr>
<tr><td>GET_PROTOCOL</td><td>-</td><td>-</td><td>2</td><td>-</td><td>Integer</td></tr>
<tr><td>LIST_REPOSITORIES</td><td>-</td><td>-</td><td>1</td><td>-</td><td>Map&lt;String, RepositoryModel&gt;</td></tr>
-<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_BRANCHES</td><td>[repository name]</td><td>-</td><td>9</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>
diff --git a/src/site/setup_authentication.mkd b/src/site/setup_authentication.mkd
index 71136675..b199c769 100644
--- a/src/site/setup_authentication.mkd
+++ b/src/site/setup_authentication.mkd
@@ -25,7 +25,7 @@ To use the *LdapUserService* set *realm.authenticationProviders=ldap* in your `g
#### Example LDAP Layout
![block diagram](ldapSample.png "LDAP Sample")
-Please see [ldapUserServiceSampleData.ldif](https://github.com/gitblit/gitblit/blob/master/tests/com/gitblit/tests/resources/ldapUserServiceSampleData.ldif) to see the data in LDAP that reflects the above picture.
+Please see [ldapUserServiceSampleData.ldif](https://github.com/gitblit/gitblit/blob/master/src/test/resources/ldap/sampledata.ldif) to see the data in LDAP that reflects the above picture.
#### Gitblit Settings for Example LDAP Layout
The following are the settings required to configure Gitblit to authenticate against the example LDAP server with LDAP-controlled team memberships.
diff --git a/src/site/setup_go.mkd b/src/site/setup_go.mkd
index c46e04ba..e0470f31 100644
--- a/src/site/setup_go.mkd
+++ b/src/site/setup_go.mkd
@@ -8,14 +8,16 @@ Open `data/gitblit.properties` in your favorite text editor and make sure to rev
- *server.storePassword* (do not enter *#* characters)
**https** is strongly recommended because passwords are insecurely transmitted form your browser/git client using Basic authentication!
- *git.packedGitLimit* (set larger than the size of your largest repository)
-3. Execute `authority.cmd` or `java -cp gitblit.jar com.gitblit.authority.Launcher --baseFolder data` from a command-line
-**NOTE:** The Authority is a Swing GUI application. Use of this tool is not required as Gitblit GO will startup and create SSL certificates itself, BUT use of this tool allows you to control the identification metadata used in the generated self-signed certificates. Skipping this step will result in certificates with default metadata.
+3. Windows: Execute `authority.cmd` or `java -cp "gitblit.jar;%CD%\ext\*" com.gitblit.authority.GitblitAuthority --baseFolder data` from a command-line.
+ Linux/OSX: Execute `authority.sh` or `java -cp "gitblit.jar:ext/*" com.gitblit.authority.GitblitAuthority --baseFolder data` from a command-line.
+ **NOTE:** The Authority is a Swing GUI application. Use of this tool is not required as Gitblit GO will startup and create SSL certificates itself, BUT use of this tool allows you to control the identification metadata used in the generated self-signed certificates. Skipping this step will result in certificates with default metadata.
1. fill out the fields in the *new certificate defaults* dialog
2. enter the store password used in *server.storePassword* when prompted. This generates an SSL certificate for **localhost**.
3. you may want to generate an SSL certificate for the hostname or ip address hostnames you are serving from
**NOTE:** You can only have **one** SSL certificate specified for a port.
- 5. exit the authority app
-4. Execute `gitblit.cmd` or `java -jar gitblit.jar --baseFolder data` from a command-line
+ 4. exit the authority app
+4. Windows: Execute `gitblit.cmd` or `java -cp gitblit.jar;"%CD%\ext\*" com.gitblit.GitBlitServer --baseFolder data` from a command-line
+ Linux/OSX: Execute `gitblit.sh` or `java -cp "gitblit.jar:ext/*"" com.gitblit.GitBlitServer --baseFolder data` from a command-line
5. Open your browser to <http://localhost:8080> or <https://localhost:8443> depending on your chosen configuration.
6. Enter the default administrator credentials: **admin / admin** and click the *Login* button
**NOTE:** Make sure to change the administrator username and/or password!!
@@ -49,7 +51,8 @@ If you want to serve your repositories to another machine over https then you wi
**NOTE:** The Gitblit Authority is a GUI tool and will require X11 forwarding on headless UNIX boxes.
-1. `authority.cmd` or `java -jar authority.jar --baseFolder data`
+1. Windows: `authority.cmd` or `java -cp "gitblit.jar;%CD%\ext\*" com.gitblit.authority.GitblitAuthority --baseFolder data`
+ Linux/OSX: `authority.sh` or `java -cp "gitblit.jar:ext/*" com.gitblit.authority.GitblitAuthority --baseFolder data`
2. Click the *new ssl certificate* button (red rosette in the toolbar in upper left of window)
3. Enter the hostname or ip address
4. Make sure the checkbox *serve https with this certificate* is checked
@@ -88,7 +91,8 @@ Alternatively, Gitblit GO is designed to facilitate use of client certificate au
When you generate a new client certificate, a zip file bundle is created which includes a P12 keystore for browsers and a PEM keystore for Git. Both of these are password-protected. Additionally, a personalized README file is generated with setup instructions for popular browsers and Git. The README is generated from `data\certs\instructions.tmpl` and can be modified to suit your needs.
-1. `authority.cmd` or `java -jar authority.jar --baseFolder data`
+1. Windows: `authority.cmd` or `java -cp "gitblit.jar;%CD%\ext\*" com.gitblit.authority.GitblitAuthority --baseFolder data`
+ Linux/OSX: `authority.sh` or `java -cp "gitblit.jar:ext/*" com.gitblit.authority.GitblitAuthority --baseFolder data`
2. Select the user for which to generate the certificate
3. Click the *new certificate* button and enter the expiration date of the certificate. You must also enter a password for the generated keystore. This password is *not* the same as the user's login password. This password is used to protect the privatekey and public certificate you will generate for the selected user. You must also enter a password hint for the user.
4. If your mail server settings are properly configured you will have a *send email* checkbox which you can use to immediately send the generated certificate bundle to the user.
@@ -139,13 +143,13 @@ Command-Line parameters override the values in `gitblit.properties` at runtime.
**Example**
- java -jar gitblit.jar --userService c:/myrealm.config --storePassword something --baseFolder c:/data
+ java -cp gitblit.jar;"%CD%\ext\*" com.gitblit.GitBlitServer --userService c:/myrealm.config --storePassword something --baseFolder c:/data
#### Overriding Gitblit GO's Log4j Configuration
You can override Gitblit GO's default Log4j configuration with a command-line parameter to the JVM.
- java -Dlog4j.configuration=file:///home/james/log4j.properties -jar gitblit.jar <optional_gitblit_args>
+ java -Dlog4j.configuration=file:///home/james/log4j.properties -cp gitblit.jar:"ext/*" com.gitblit.GitBlitServer <optional_gitblit_args>
You can not use override the default log4j configuration *AND* specify the `--dailyLogFile` parameter. For reference, here is [Gitblit's default Log4j configuration](https://github.com/gitblit/gitblit/blob/master/src/log4j.properties). It includes some file appenders that are disabled by default.
diff --git a/src/site/setup_proxy.mkd b/src/site/setup_proxy.mkd
index 4cf263dd..3d628329 100644
--- a/src/site/setup_proxy.mkd
+++ b/src/site/setup_proxy.mkd
@@ -47,6 +47,7 @@ ProxyPreserveHost On
# If your httpd frontend is https but you are proxying http Gitblit WAR or GO
#Header edit Location ^http://([^/]+)/gitblit/ https://$1/gitblit/
+#Header edit Ajax-Location ^http://([^/]+)/gitblit/ https://$1/gitblit/
# Additionally you will want to tell Gitblit the original scheme and port
#RequestHeader set X-Forwarded-Proto https
diff --git a/src/site/siteindex.mkd b/src/site/siteindex.mkd
index aec5c42a..2415fb54 100644
--- a/src/site/siteindex.mkd
+++ b/src/site/siteindex.mkd
@@ -9,26 +9,29 @@
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-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="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-primary" href="%GCURL%manager-${project.releaseVersion}.zip">Download Gitblit Manager</a></div>
+ <div style="padding:5px;"><a style="width:175px;text-decoration:none;" class="btn btn-info" href="%DOCKERURL%">Gitblit GO Docker image</a></div>
+ </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>
- <tr><th>License</th><td><a href="http://www.apache.org/licenses/LICENSE-2.0">Apache License 2.0</a></td></tr>
- <tr><th>Sources</th><td><a href="${project.scmUrl}">GitHub</a></td></tr>
- <tr><th>Issues</th><td><a href="${project.issuesUrl}">GitHub</a></td></tr>
- <tr><th>Discussion</th><td><a href="${project.forumUrl}">Gitblit Group</a></td></tr>
- <tr><th>Ohloh</th><td><a target="_top" href="http://www.ohloh.net/p/gitblit"><img border="0" width="100" height="16" src="http://www.ohloh.net/p/gitblit/widgets/project_thin_badge.gif" alt="Ohloh project report for Gitblit" /></a></td></tr>
- </tbody>
+ <table class="table condensed-table">
+ <tbody>
+ <tr><th>License</th><td><a href="http://www.apache.org/licenses/LICENSE-2.0">Apache License 2.0</a></td></tr>
+ <tr><th>Sources</th><td><a href="${project.scmUrl}">GitHub</a></td></tr>
+ <tr><th>Issues</th><td><a href="${project.issuesUrl}">GitHub</a></td></tr>
+ <tr><th>Discussion</th><td><a href="${project.forumUrl}">Gitblit Group</a></td></tr>
+ <tr><th>Twitter</th><td><a href="https://twitter.com/gitblit">@gitblit</a></td></tr>
+ <tr><th>Open Hub</th><td><a target="_top" href="https://www.openhub.net/p/gitblit"><img border="0" width="100" height="16" src="https://www.openhub.net/p/gitblit/widgets/project_thin_badge.gif" alt="Open Hub project report for Gitblit" /></a></td></tr>
+ </tbody>
</table>
</div>
+ <div><a href="https://cloudsmith.com"><img src="https://img.shields.io/badge/OSS%20hosting%20by-cloudsmith-blue?logo=cloudsmith&style=for-the-badge" alt="Hosted By: Cloudsmith"></a><br>
+ Package repository hosting is graciously provided by <a href="https://cloudsmith.com">Cloudsmith</a>.</div>
</div>
## What is Gitblit?
@@ -88,7 +91,7 @@ Gitblit includes a backup mechanism (*federation*) which can be used to backup r
### Java Runtime Requirement
-Gitblit requires a Java 7 Runtime Environment (JRE) or a Java 7 Development Kit (JDK).
+Gitblit requires a Java 8 Runtime Environment (JRE) or a Java 8 Development Kit (JDK).
[jgit]: http://eclipse.org/jgit "Eclipse JGit Site"
[git]: http://git-scm.com "Official Git Site"
diff --git a/src/site/templates/ghreleasenotes.awk b/src/site/templates/ghreleasenotes.awk
new file mode 100755
index 00000000..f4479552
--- /dev/null
+++ b/src/site/templates/ghreleasenotes.awk
@@ -0,0 +1,66 @@
+#! /usr/bin/env awk -f
+
+BEGIN { on=0 ; skip=1 ; block=0 ; section=""}
+
+/^[[:blank:]]*id:/ { relId = $NF }
+
+/r[0-9]+: *{/ { on=1 ; next }
+/^[[:blank:]]*}[[:blank:]]*$/ { if (on) {
+ print "[Full release notes on gitblit.com](http://gitblit.com/releases.html#" relId ")"
+ exit 0
+ }
+ }
+
+
+on==1 && /^[[:blank:]]*[[:alnum:]]+:[[:blank:]]*(''|~)?$/ {
+ if (!block) {
+ skip=1
+ if (section == "fixes:" || section == "changes:" || section == "additions:") { printf "\n</details>\n"}
+ if (section != "") print ""
+ if (section == "note:") print "----------"
+ section = ""
+ if ($NF == "~") next
+ }
+ else {
+ printSection()
+ next
+ }
+ if ($NF == "''") {
+ block = !block
+ }
+ }
+on==1 && /^[[:blank:]]*note:/ { skip=0 ; section=$1; print "### Update Note" ; printSingleLineSection() ; next }
+on==1 && /^[[:blank:]]*text:/ { skip=0 ; section=$1; printf "\n\n"; printSingleLineSection() ; next }
+on==1 && /^[[:blank:]]*security:/ { skip=0 ; section=$1; print "### *Security*" ; next }
+on==1 && /^[[:blank:]]*fixes:/ { skip=0 ; section=$1; printf "<details><summary>Fixes</summary>\n\n### Fixes\n" ; next}
+on==1 && /^[[:blank:]]*changes:/ { skip=0 ; section=$1; printf "<details><summary>Changes</summary>\n\n### Changes\n" ; next}
+on==1 && /^[[:blank:]]*additions:/ { skip=0 ; section=$1; printf "<details><summary>Additions</summary>\n\n### Additions\n" ; next}
+
+on==1 {
+ if ($1 == "''") {
+ block = !block
+ next
+ }
+ if ((block || !skip)) {
+ printSection()
+ }
+ }
+
+function printSingleLineSection()
+{
+ if (NF>1 && $2 != "''" && $2 != "~") {
+ if (protect) gsub(/'/, "'\\''")
+ for (i=2; i<= NF; i++) printf "%s ", $i
+ print ""
+ }
+}
+
+function printSection()
+{
+ if (section != "text:") sub(/[[:blank:]]+/, "")
+ gsub(/pr-/, "PR #")
+ gsub(/issue-/, "issue #")
+ gsub(/commit-/, "commit ")
+ if (protect) gsub(/'/, "'\\''")
+ print $0
+} \ No newline at end of file
diff --git a/src/site/tickets_overview.mkd b/src/site/tickets_overview.mkd
index 10d0e18f..cee5c97d 100644
--- a/src/site/tickets_overview.mkd
+++ b/src/site/tickets_overview.mkd
@@ -35,7 +35,7 @@ Gitblit's ticket data is based on a ridiculously simple concept: a ticket is the
All ticket services inherit from the same base class which handles most of the high level logic for ticket management including caching, milestones (stored in .git/config), indexing, queries, and searches.
-You can find descriptions of the available persistence services in [[tickets setup]].
+You can find descriptions of the available persistence services in [tickets setup](tickets_setup.html).
#### Limitations
diff --git a/src/site/upgrade_go.mkd b/src/site/upgrade_go.mkd
index a0092588..4bc2272f 100644
--- a/src/site/upgrade_go.mkd
+++ b/src/site/upgrade_go.mkd
@@ -1,3 +1,25 @@
+## Upgrading Gitblit GO (1.9.1+)
+
+The command line to start Gitblit has changed from
+
+```
+java -jar gitblit.jar --baseFolder data
+```
+
+to
+
+```
+java -cp "gitblit.jar:ext/*" com.gitblit.GitBlitServer --baseFolder data
+```
+
+or on Windows to
+
+```
+java -cp gitblit.jar;"%CD%\ext\*" com.gitblit.GitBlitServer --baseFolder data
+```
+
+The class path and main class need to be specified now. If you have installed Gitblit as a service you will need to adjust the service scripts or definitions accordingly.
+
## Upgrading Gitblit GO (1.7.0+)
The default `gitblit.properties` file has been split into two files: `gitblit.properties`, which is the recommended file for setting your configuration, and `defaults.properties` which are Gitblit's default settings.
diff --git a/src/test/config/test-users.conf b/src/test/config/test-users.conf
index 4361410c..12f53a9e 100644
--- a/src/test/config/test-users.conf
+++ b/src/test/config/test-users.conf
@@ -1,5 +1,6 @@
+
[user "admin"]
- password = admin
+ password = MD5:21232f297a57a5a743894a0e4a801fc3
cookie = dd94709528bb1c83d08f3088d4043f4742891f4f
accountType = LOCAL
emailMeOnMyTicketChanges = true
diff --git a/src/test/data/.gitignore b/src/test/data/.gitignore
new file mode 100644
index 00000000..7056b036
--- /dev/null
+++ b/src/test/data/.gitignore
@@ -0,0 +1 @@
+/*.git/
diff --git a/src/test/data/ambition.git.zip b/src/test/data/ambition.git.zip
new file mode 100644
index 00000000..9aaa4997
--- /dev/null
+++ b/src/test/data/ambition.git.zip
Binary files differ
diff --git a/src/test/data/gitective.git.zip b/src/test/data/gitective.git.zip
new file mode 100644
index 00000000..1071d911
--- /dev/null
+++ b/src/test/data/gitective.git.zip
Binary files differ
diff --git a/src/test/data/hello-world.git.zip b/src/test/data/hello-world.git.zip
new file mode 100644
index 00000000..ef378f23
--- /dev/null
+++ b/src/test/data/hello-world.git.zip
Binary files differ
diff --git a/src/test/data/hello-world.properties b/src/test/data/hello-world.properties
new file mode 100644
index 00000000..7482c863
--- /dev/null
+++ b/src/test/data/hello-world.properties
@@ -0,0 +1,14 @@
+commit.first=192cdede1cc81da7b393aeb7aba9f88998b04713
+commit.second=8caad51
+commit.fifth=55f6796044dc51f0bb9301f07920f0fb64c3d12c
+commit.fifteen=5ebfaca
+commit.added=192cded
+commit.changed=b2c50ce
+commit.deleted=8613bee10bde27a0fbaca66447cdc3f0f9483365
+commits.since_20190605=10
+users.byEmail=11
+users.byName=10
+files.top=14
+files.C.top=2
+files.C.KnR=1
+files.Cpp=1 \ No newline at end of file
diff --git a/src/test/data/ticgit.git.zip b/src/test/data/ticgit.git.zip
new file mode 100644
index 00000000..18df11cc
--- /dev/null
+++ b/src/test/data/ticgit.git.zip
Binary files differ
diff --git a/src/test/java/com/gitblit/StoredUserConfigTest.java b/src/test/java/com/gitblit/StoredUserConfigTest.java
new file mode 100644
index 00000000..012139d3
--- /dev/null
+++ b/src/test/java/com/gitblit/StoredUserConfigTest.java
@@ -0,0 +1,207 @@
+package com.gitblit;
+
+import org.eclipse.jgit.lib.StoredConfig;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.File;
+
+import static org.junit.Assert.*;
+
+public class StoredUserConfigTest
+{
+ private static File file;
+
+ @Before
+ public void setup()
+ {
+ file = new File("./suc-test.conf");
+ file.delete();
+ }
+
+ @After
+ public void teardown()
+ {
+ file.delete();
+ }
+
+
+
+ @Test
+ public void testSection() throws Exception
+ {
+ StoredUserConfig config = new StoredUserConfig(file);
+ config.setString("USER", "norman", "key", "value");
+ config.setString("USER", "admin", "displayName", "marusha");
+ config.setString("USER", null, "role", "none");
+
+ config.setString("TEAM", "admin", "role", "admin");
+ config.setString("TEAM", "ci", "email", "ci@example.com");
+ config.setString("TEAM", null, "displayName", "noone");
+
+ config.save();
+
+ StoredConfig cfg = new FileBasedConfig(file, FS.detect());
+ cfg.load();
+ assertEquals("value", cfg.getString("USER", "norman", "key"));
+ assertEquals("marusha", cfg.getString("USER", "admin", "displayName"));
+ assertEquals("none", cfg.getString("USER", null, "role"));
+
+ assertEquals("admin", cfg.getString("TEAM", "admin", "role"));
+ assertEquals("ci@example.com", cfg.getString("TEAM", "ci", "email"));
+ assertEquals("noone", cfg.getString("TEAM", null, "displayName"));
+ }
+
+
+ @Test
+ public void testStringFields() throws Exception
+ {
+ StoredUserConfig config = new StoredUserConfig(file);
+ config.setString("USER", "admin", "password", "secret");
+ config.setString("USER", "admin", "displayName", "marusha");
+ config.setString("USER", "admin", "email", "name@example.com");
+
+ config.setString("USER", "other", "password", "password");
+ config.setString("USER", "other", "displayName", "mama");
+ config.setString("USER", "other", "email", "other@example.com");
+ config.setString("USER", "other", "repository", "RW+:repo1");
+ config.setString("USER", "other", "repository", "RW+:repo2");
+
+ config.setString("USER", null, "displayName", "default");
+
+ config.save();
+
+ StoredConfig cfg = new FileBasedConfig(file, FS.detect());
+ cfg.load();
+ assertEquals("secret", cfg.getString("USER", "admin", "password"));
+ assertEquals("marusha", cfg.getString("USER", "admin", "displayName"));
+ assertEquals("name@example.com", cfg.getString("USER", "admin", "email"));
+
+ assertEquals("password", cfg.getString("USER", "other", "password"));
+ assertEquals("mama", cfg.getString("USER", "other", "displayName"));
+ assertEquals("other@example.com", cfg.getString("USER", "other", "email"));
+
+ String[] stringList = cfg.getStringList("USER", "other", "repository");
+ assertNotNull(stringList);
+ assertEquals(2, stringList.length);
+ int i = 0;
+ for (String s : stringList) {
+ if (s.equalsIgnoreCase("RW+:repo1")) i += 1;
+ else if (s.equalsIgnoreCase("RW+:repo2")) i += 2;
+ }
+ assertEquals("Not all repository strings found", 3, i);
+
+ assertEquals("default", cfg.getString("USER", null, "displayName"));
+ }
+
+
+ @Test
+ public void testBooleanFields() throws Exception
+ {
+ StoredUserConfig config = new StoredUserConfig(file);
+ config.setBoolean("USER", "admin", "emailMeOnMyTicketChanges", true);
+ config.setBoolean("USER", "master", "emailMeOnMyTicketChanges", false);
+ config.setBoolean("TEAM", "admin", "excludeFromFederation", true);
+ config.setBoolean("USER", null, "excludeFromFederation", false);
+
+ config.save();
+
+ StoredConfig cfg = new FileBasedConfig(file, FS.detect());
+ cfg.load();
+ assertTrue(cfg.getBoolean("USER", "admin", "emailMeOnMyTicketChanges", false));
+ assertFalse(cfg.getBoolean("USER", "master", "emailMeOnMyTicketChanges", true));
+ assertTrue(cfg.getBoolean("TEAM", "admin", "excludeFromFederation", false));
+ assertFalse(cfg.getBoolean("USER", null, "excludeFromFederation", true));
+ }
+
+
+ @Test
+ public void testHashEscape() throws Exception
+ {
+ StoredUserConfig config = new StoredUserConfig(file);
+ config.setString("USER", "admin", "role", "#admin");
+
+ config.setString("USER", "other", "role", "#none");
+ config.setString("USER", "other", "displayName", "big#");
+ config.setString("USER", "other", "email", "user#name@home.de");
+
+ config.save();
+
+ StoredConfig cfg = new FileBasedConfig(file, FS.detect());
+ cfg.load();
+ assertEquals("#admin", cfg.getString("USER", "admin", "role"));
+ assertEquals("#none", cfg.getString("USER", "other", "role"));
+ assertEquals("big#", cfg.getString("USER", "other", "displayName"));
+ assertEquals("user#name@home.de", cfg.getString("USER", "other", "email"));
+ }
+
+
+ @Test
+ public void testCtrlEscape() throws Exception
+ {
+ StoredUserConfig config = new StoredUserConfig(file);
+ config.setString("USER", "name", "password", "bing\bbong");
+ config.setString("USER", "name", "role", "domain\\admin");
+ config.setString("USER", "name", "displayName", "horny\n\telephant");
+ config.setString("USER", "name", "org", "\tbig\tblue");
+ config.setString("USER", "name", "unit", "the end\n");
+
+ config.setString("USER", null, "unit", "the\ndefault");
+
+ config.save();
+
+ StoredConfig cfg = new FileBasedConfig(file, FS.detect());
+ cfg.load();
+ assertEquals("bing\bbong", cfg.getString("USER", "name", "password"));
+ assertEquals("domain\\admin", cfg.getString("USER", "name", "role"));
+ assertEquals("horny\n\telephant", cfg.getString("USER", "name", "displayName"));
+ assertEquals("\tbig\tblue", cfg.getString("USER", "name", "org"));
+ assertEquals("the end\n", cfg.getString("USER", "name", "unit"));
+
+ assertEquals("the\ndefault", cfg.getString("USER", null, "unit"));
+ }
+
+
+ @Test
+ public void testQuoteEscape() throws Exception
+ {
+ StoredUserConfig config = new StoredUserConfig(file);
+ config.setString("USER", "dude", "password", "going\"places");
+ config.setString("USER", "dude", "role", "\"dude\"");
+ config.setString("USER", "dude", "displayName", "John \"The Dude\" Lebowski");
+ config.setString("USER", "dude", "repo", "\"front matter");
+ config.setString("USER", "dude", "peepo", "leadout\"");
+
+ config.save();
+
+ StoredConfig cfg = new FileBasedConfig(file, FS.detect());
+ cfg.load();
+ assertEquals("going\"places", cfg.getString("USER", "dude", "password"));
+ assertEquals("\"dude\"", cfg.getString("USER", "dude", "role"));
+ assertEquals("John \"The Dude\" Lebowski", cfg.getString("USER", "dude", "displayName"));
+ assertEquals("\"front matter", cfg.getString("USER", "dude", "repo"));
+ assertEquals("leadout\"", cfg.getString("USER", "dude", "peepo"));
+ }
+
+
+ @Test
+ public void testUTF8() throws Exception
+ {
+ StoredUserConfig config = new StoredUserConfig(file);
+ config.setString("USER", "ming", "password", "一\t二\n三");
+ config.setString("USER", "ming", "displayName", "白老鼠");
+ config.setString("USER", "ming", "peepo", "Mickey \"白老鼠\" Whitfield");
+
+ config.save();
+
+ StoredConfig cfg = new FileBasedConfig(file, FS.detect());
+ cfg.load();
+ assertEquals("一\t二\n三", cfg.getString("USER", "ming", "password"));
+ assertEquals("白老鼠", cfg.getString("USER", "ming", "displayName"));
+ assertEquals("Mickey \"白老鼠\" Whitfield", cfg.getString("USER", "ming", "peepo"));
+ }
+
+}
diff --git a/src/test/java/com/gitblit/service/LuceneRepoIndexStoreTest.java b/src/test/java/com/gitblit/service/LuceneRepoIndexStoreTest.java
new file mode 100644
index 00000000..baac3516
--- /dev/null
+++ b/src/test/java/com/gitblit/service/LuceneRepoIndexStoreTest.java
@@ -0,0 +1,267 @@
+/*
+ * Copyright 2017 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.gitblit.service;
+
+import static org.junit.Assert.*;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Path;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import com.gitblit.utils.LuceneIndexStore;
+
+/**
+ * @author Florian Zschocke
+ *
+ */
+public class LuceneRepoIndexStoreTest
+{
+
+ private static final int LUCENE_VERSION = LuceneIndexStore.LUCENE_CODEC_VERSION;
+ private static final String LUCENE_DIR = "lucene";
+
+
+ @Rule
+ public TemporaryFolder baseFolder = new TemporaryFolder();
+
+
+ private String getIndexDir(int version)
+ {
+ return version + "_" + LUCENE_VERSION;
+ }
+
+
+ private String getLuceneIndexDir(int version)
+ {
+ return LUCENE_DIR + "/" + version + "_" + LUCENE_VERSION;
+ }
+
+
+ @Test
+ public void testGetConfigFile() throws IOException
+ {
+ int version = 1;
+ File repositoryFolder = baseFolder.getRoot();
+ LuceneRepoIndexStore li = new LuceneRepoIndexStore(repositoryFolder, version);
+ File confFile= li.getConfigFile();
+ File luceneDir = new File(repositoryFolder, getLuceneIndexDir(version) + "/gb_lucene.conf");
+ assertEquals(luceneDir, confFile);
+ }
+
+
+ @Test
+ public void testCreate()
+ {
+ int version = 0;
+ File repositoryFolder = baseFolder.getRoot();
+ File luceneDir = new File(repositoryFolder, getLuceneIndexDir(version));
+ assertFalse("Precondition failure: directory exists already", new File(repositoryFolder, LUCENE_DIR).exists());
+ assertFalse("Precondition failure: directory exists already", luceneDir.exists());
+
+ LuceneIndexStore li = new LuceneRepoIndexStore(repositoryFolder, version);
+ li.create();
+
+ assertTrue(luceneDir.exists());
+ assertTrue(luceneDir.isDirectory());
+ }
+
+ @Test
+ public void testCreateIndexDir()
+ {
+ int version = 7777;
+ File repositoryFolder = baseFolder.getRoot();
+ try {
+ baseFolder.newFolder(LUCENE_DIR);
+ } catch (IOException e) {
+ fail("Failed in setup of folder: " + e);
+ }
+ File luceneDir = new File(repositoryFolder, getLuceneIndexDir(version));
+
+ assertTrue("Precondition failure: directory does not exist", new File(repositoryFolder, LUCENE_DIR).exists());
+ assertFalse("Precondition failure: directory exists already", luceneDir.exists());
+
+ LuceneIndexStore li = new LuceneRepoIndexStore(repositoryFolder, version);
+ li.create();
+
+ assertTrue(luceneDir.exists());
+ assertTrue(luceneDir.isDirectory());
+
+ // Make sure nothing else was created.
+ assertEquals(0, luceneDir.list().length);
+ assertEquals(1, luceneDir.getParentFile().list().length);
+ }
+
+ @Test
+ public void testCreateIfNecessary()
+ {
+ int version = 7777888;
+ File repositoryFolder = baseFolder.getRoot();
+ File luceneDir = null;
+ try {
+ luceneDir = baseFolder.newFolder(LUCENE_DIR, getIndexDir(version));
+ } catch (IOException e) {
+ fail("Failed in setup of folder: " + e);
+ }
+ assertTrue("Precondition failure: directory does not exist", new File(repositoryFolder, LUCENE_DIR).exists());
+ assertTrue("Precondition failure: directory does not exist", luceneDir.exists());
+
+ LuceneIndexStore li = new LuceneRepoIndexStore(repositoryFolder, version);
+ li.create();
+
+ assertTrue(luceneDir.exists());
+ assertTrue(luceneDir.isDirectory());
+
+ // Make sure nothing else was created.
+ assertEquals(0, luceneDir.list().length);
+ assertEquals(1, luceneDir.getParentFile().list().length);
+ }
+
+
+ @Test
+ public void testDelete()
+ {
+ int version = 111222333;
+
+ File repositoryFolder = baseFolder.getRoot();
+ File luceneDir = null;
+ try {
+ luceneDir = baseFolder.newFolder(LUCENE_DIR, getIndexDir(version));
+ } catch (IOException e) {
+ fail("Failed in setup of folder: " + e);
+ }
+ assertTrue("Precondition failure: directory does not exist", luceneDir.exists());
+
+ LuceneIndexStore li = new LuceneRepoIndexStore(repositoryFolder, version);
+ assertTrue(li.delete());
+
+ assertFalse(luceneDir.exists());
+ assertTrue(new File(repositoryFolder, LUCENE_DIR).exists());
+ }
+
+
+ @Test
+ public void testDeleteNotExist()
+ {
+ int version = 0;
+
+ File repositoryFolder = baseFolder.getRoot();
+ try {
+ baseFolder.newFolder(LUCENE_DIR);
+ } catch (IOException e) {
+ fail("Failed in setup of folder: " + e);
+ }
+ File luceneDir = new File(repositoryFolder, getLuceneIndexDir(version));
+ assertTrue("Precondition failure: directory does not exist", new File(repositoryFolder, LUCENE_DIR).exists());
+ assertFalse("Precondition failure: directory does exist", luceneDir.exists());
+
+ LuceneIndexStore li = new LuceneRepoIndexStore(repositoryFolder, version);
+ assertTrue(li.delete());
+
+ assertFalse(luceneDir.exists());
+ assertTrue(new File(repositoryFolder, LUCENE_DIR).exists());
+ }
+
+
+ @Test
+ public void testDeleteWithFiles()
+ {
+ int version = 5;
+
+ File repositoryFolder = baseFolder.getRoot();
+ File luceneFolder = new File(baseFolder.getRoot(), LUCENE_DIR);
+ File luceneDir = null;
+
+ File otherDir = new File(luceneFolder, version + "_10");
+ File dbFile = null;
+ try {
+ luceneDir = baseFolder.newFolder(LUCENE_DIR, getIndexDir(version));
+ File file = new File(luceneDir, "_file1");
+ file.createNewFile();
+ file = new File(luceneDir, "_file2.db");
+ file.createNewFile();
+ file = new File(luceneDir, "conf.conf");
+ file.createNewFile();
+
+ otherDir.mkdirs();
+ dbFile = new File(otherDir, "_file2.db");
+ dbFile.createNewFile();
+ file = new File(otherDir, "conf.conf");
+ file.createNewFile();
+ }
+ catch (IOException e) {
+ fail("Failed in setup of folder: " + e);
+ }
+ assertTrue("Precondition failure: index directory does not exist", luceneDir.exists());
+ assertTrue("Precondition failure: other index directory does not exist", otherDir.exists());
+
+ LuceneIndexStore li = new LuceneRepoIndexStore(repositoryFolder, version);
+ li.delete();
+
+ assertFalse(luceneDir.exists());
+ assertTrue(luceneFolder.exists());
+ assertTrue(otherDir.exists());
+ assertTrue(dbFile.exists());
+ }
+
+
+
+ @Test
+ public void testGetPath() throws IOException
+ {
+ int version = 7;
+ File repositoryFolder = baseFolder.getRoot();
+ LuceneIndexStore li = new LuceneRepoIndexStore(repositoryFolder, version);
+ Path dir = li.getPath();
+ File luceneDir = new File(repositoryFolder, getLuceneIndexDir(version));
+ assertEquals(luceneDir.toPath(), dir);
+ }
+
+
+ @Test
+ public void testHasIndex() throws IOException
+ {
+ int version = 0;
+ File luceneFolder = new File(baseFolder.getRoot(), "lucene");
+
+ LuceneIndexStore li = new LuceneRepoIndexStore(luceneFolder, version);
+ assertFalse(li.hasIndex());
+
+ baseFolder.newFolder("lucene");
+ li = new LuceneIndexStore(luceneFolder, version);
+ assertFalse(li.hasIndex());
+
+ File luceneDir = baseFolder.newFolder("lucene", getIndexDir(version));
+ li = new LuceneIndexStore(luceneFolder, version);
+ assertFalse(li.hasIndex());
+
+ new File(luceneDir, "write.lock").createNewFile();
+ li = new LuceneIndexStore(luceneFolder, version);
+ assertFalse(li.hasIndex());
+
+ new File(luceneDir, "segments_1").createNewFile();
+ li = new LuceneIndexStore(luceneFolder, version);
+ System.out.println("Check " + luceneDir);
+ assertTrue(li.hasIndex());
+
+ }
+
+
+}
diff --git a/src/test/java/com/gitblit/servlet/RawServletTest.java b/src/test/java/com/gitblit/servlet/RawServletTest.java
new file mode 100644
index 00000000..50587178
--- /dev/null
+++ b/src/test/java/com/gitblit/servlet/RawServletTest.java
@@ -0,0 +1,1426 @@
+package com.gitblit.servlet;
+
+import com.gitblit.Constants;
+import com.gitblit.IStoredSettings;
+import com.gitblit.Keys;
+import com.gitblit.manager.IRepositoryManager;
+import com.gitblit.tests.mock.MockGitblitContext;
+import com.gitblit.tests.mock.MockRuntimeManager;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Ignore;
+import org.junit.Test;
+
+
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.mock;
+
+public class RawServletTest
+{
+ private static final char FSC = RawServlet.FSC;
+
+ private static MockRuntimeManager mockRuntimeManager = new MockRuntimeManager();
+ private static IStoredSettings settings;
+
+ private IRepositoryManager repositoryMngr;
+
+ private RawServlet rawServlet;
+
+
+ @BeforeClass
+ public static void init()
+ {
+ MockGitblitContext gitblitContext = new MockGitblitContext();
+ gitblitContext.addManager(mockRuntimeManager);
+ settings = mockRuntimeManager.getSettings();
+ }
+
+ @Before
+ public void setUp()
+ {
+ settings.overrideSetting(Keys.web.forwardSlashCharacter, "/");
+
+ repositoryMngr = mock(IRepositoryManager.class);
+ rawServlet = new RawServlet(mockRuntimeManager, repositoryMngr);
+ }
+
+
+
+ @Test
+ public void asLink_HttpUrlRepo()
+ {
+ String baseUrl = "http://localhost";
+ String repository = "test.git";
+ String branch = null;
+ String path = null;
+
+ String link = RawServlet.asLink(baseUrl, repository, branch, path);
+
+ assertNotNull(link);
+ assertEquals(baseUrl + Constants.RAW_PATH + repository + "/", link);
+ }
+
+ @Test
+ public void asLink_HttpUrlTrailingSlashRepo()
+ {
+ String baseUrl = "http://localhost/";
+ String repository = "test.git";
+ String branch = null;
+ String path = null;
+
+ String link = RawServlet.asLink(baseUrl, repository, branch, path);
+
+ assertNotNull(link);
+ assertEquals(baseUrl.substring(0, baseUrl.length()-1) + Constants.RAW_PATH + repository + "/", link);
+ }
+
+ @Test
+ public void asLink_HttpUrlRepoLeadingSlash()
+ {
+ String baseUrl = "http://localhost";
+ String repository = "/test.git";
+ String branch = null;
+ String path = null;
+
+ String link = RawServlet.asLink(baseUrl, repository, branch, path);
+
+ assertNotNull(link);
+ assertEquals(baseUrl + Constants.RAW_PATH + repository.substring(1) + "/", link);
+ }
+
+ @Test
+ public void asLink_HttpUrlTrailingSlashRepoLeadingSlash()
+ {
+ String baseUrl = "http://localhost/";
+ String repository = "/test.git";
+ String branch = null;
+ String path = null;
+
+ String link = RawServlet.asLink(baseUrl, repository, branch, path);
+
+ assertNotNull(link);
+ assertEquals(baseUrl.substring(0, baseUrl.length()-1) + Constants.RAW_PATH + repository.substring(1) + "/", link);
+ }
+
+ @Test
+ public void asLink_HttpUrlRepoBranch()
+ {
+ String baseUrl = "http://localhost";
+ String repository = "test.git";
+ String branch = "b52";
+ String path = null;
+
+ String link = RawServlet.asLink(baseUrl, repository, branch, path);
+
+ assertNotNull(link);
+ assertEquals(baseUrl + Constants.RAW_PATH + repository + "/" + branch + "/", link);
+ }
+
+ @Test
+ public void asLink_HttpUrlTrailingSlashRepoBranch()
+ {
+ String baseUrl = "http://localhost/";
+ String repository = "test.git";
+ String branch = "branch";
+ String path = null;
+
+ String link = RawServlet.asLink(baseUrl, repository, branch, path);
+
+ assertNotNull(link);
+ assertEquals(baseUrl.substring(0, baseUrl.length()-1) + Constants.RAW_PATH + repository + "/" + branch + "/", link);
+ }
+
+ @Test
+ public void asLink_HttpUrlRepoLeadingSlashBranch()
+ {
+ String baseUrl = "http://localhost";
+ String repository = "/test.git";
+ String branch = "featureOne";
+ String path = null;
+
+ String link = RawServlet.asLink(baseUrl, repository, branch, path);
+
+ assertNotNull(link);
+ assertEquals(baseUrl + Constants.RAW_PATH + repository.substring(1) + "/" + branch + "/", link);
+ }
+
+ @Test
+ public void asLink_HttpUrlTrailingSlashRepoLeadingSlashBranch()
+ {
+ String baseUrl = "http://localhost/";
+ String repository = "/test.git";
+ String branch = "b";
+ String path = null;
+
+ String link = RawServlet.asLink(baseUrl, repository, branch, path);
+
+ assertNotNull(link);
+ assertEquals(baseUrl.substring(0, baseUrl.length()-1) + Constants.RAW_PATH + repository.substring(1) + "/" + branch + "/", link);
+ }
+
+
+ @Test
+ public void asLink_HttpUrlRepoBranchWithSlash()
+ {
+ String baseUrl = "http://localhost";
+ String repository = "test.git";
+ String branch = "feature/whatever";
+ String path = null;
+
+ String link = RawServlet.asLink(baseUrl, repository, branch, path);
+
+ assertNotNull(link);
+ assertEquals(baseUrl + Constants.RAW_PATH + repository + "/" + branch.replace('/', FSC) + "/", link);
+ }
+
+ @Test
+ public void asLink_HttpUrlTrailingSlashRepoBranchWithSlash()
+ {
+ String baseUrl = "http://localhost/";
+ String repository = "test.git";
+ String branch = "branch/for/issue/16";
+ String path = null;
+
+ String link = RawServlet.asLink(baseUrl, repository, branch, path);
+
+ assertNotNull(link);
+ assertEquals(baseUrl.substring(0, baseUrl.length()-1) + Constants.RAW_PATH + repository + "/"
+ + branch.replace('/', FSC) + "/", link);
+ }
+
+ @Test
+ public void asLink_HttpUrlRepoLeadingSlashBranchWithSlash()
+ {
+ String baseUrl = "http://localhost";
+ String repository = "/test.git";
+ String branch = "releases/1.2.3";
+ String path = null;
+
+ String link = RawServlet.asLink(baseUrl, repository, branch, path);
+
+ assertNotNull(link);
+ assertEquals(baseUrl + Constants.RAW_PATH + repository.substring(1) + "/"
+ + branch.replace('/', FSC) + "/", link);
+ }
+
+ @Test
+ public void asLink_HttpUrlTrailingSlashRepoLeadingSlashBranchWithSlash()
+ {
+ String baseUrl = "http://localhost/";
+ String repository = "/test.git";
+ String branch = "b/52";
+ String path = null;
+
+ String link = RawServlet.asLink(baseUrl, repository, branch, path);
+
+ assertNotNull(link);
+ assertEquals(baseUrl.substring(0, baseUrl.length()-1) + Constants.RAW_PATH + repository.substring(1) + "/"
+ + branch.replace('/', FSC) + "/", link);
+ }
+
+
+ @Test
+ public void asLink_HttpUrlRepoBranchPathFile()
+ {
+ String baseUrl = "http://localhost";
+ String repository = "test.git";
+ String branch = "b52";
+ String path = "file.txt";
+
+ String link = RawServlet.asLink(baseUrl, repository, branch, path);
+
+ assertNotNull(link);
+ assertEquals(baseUrl + Constants.RAW_PATH + repository + "/" + branch + "/" + path, link);
+ }
+
+ @Test
+ public void asLink_HttpUrlTrailingSlashRepoBranchPathFolderFile()
+ {
+ String baseUrl = "http://localhost/";
+ String repository = "test.git";
+ String branch = "branch";
+ String path = "path/to/file.png";
+
+ String link = RawServlet.asLink(baseUrl, repository, branch, path);
+
+ assertNotNull(link);
+ assertEquals(baseUrl.substring(0, baseUrl.length()-1) + Constants.RAW_PATH + repository + "/"
+ + branch + "/" + path.replace('/', FSC), link);
+ }
+
+ @Test
+ public void asLink_HttpUrlRepoLeadingSlashBranchPathFolderLeadingSlash()
+ {
+ String baseUrl = "http://localhost";
+ String repository = "/test.git";
+ String branch = "featureOne";
+ String path = "/folder";
+
+ String link = RawServlet.asLink(baseUrl, repository, branch, path);
+
+ assertNotNull(link);
+ assertEquals(baseUrl + Constants.RAW_PATH + repository.substring(1) + "/" + branch + path, link);
+ }
+
+ @Test
+ public void asLink_HttpUrlTrailingSlashRepoLeadingSlashBranchSubFolder()
+ {
+ String baseUrl = "http://localhost/";
+ String repository = "/test.git";
+ String branch = "b";
+ String path = "sub/folder";
+
+ String link = RawServlet.asLink(baseUrl, repository, branch, path);
+
+ assertNotNull(link);
+ assertEquals(baseUrl.substring(0, baseUrl.length()-1) + Constants.RAW_PATH + repository.substring(1) + "/"
+ + branch + "/" + path.replace('/', FSC), link);
+ }
+
+
+ @Test
+ public void asLink_HttpUrlRepoBranchWithSlashPathFolder()
+ {
+ String baseUrl = "http://localhost";
+ String repository = "test.git";
+ String branch = "feature/whatever";
+ String path = "folder";
+
+ String link = RawServlet.asLink(baseUrl, repository, branch, path);
+
+ assertNotNull(link);
+ assertEquals(baseUrl + Constants.RAW_PATH + repository + "/"
+ + branch.replace('/', FSC) + "/" + path, link);
+ }
+
+ @Test
+ public void asLink_HttpUrlTrailingSlashRepoBranchWithSlashPathFolderFile()
+ {
+ String baseUrl = "http://localhost/";
+ String repository = "test.git";
+ String branch = "branch/for/issue/16";
+ String path = "a/file.gif";
+
+ String link = RawServlet.asLink(baseUrl, repository, branch, path);
+
+ assertNotNull(link);
+ assertEquals(baseUrl.substring(0, baseUrl.length()-1) + Constants.RAW_PATH + repository + "/"
+ + branch.replace('/', FSC) + "/" + path.replace('/', FSC), link);
+ }
+
+ @Test
+ public void asLink_HttpUrlRepoLeadingSlashBranchWithSlashPathFile()
+ {
+ String baseUrl = "http://localhost";
+ String repository = "/test.git";
+ String branch = "releases/1.2.3";
+ String path = "hurray.png";
+
+ String link = RawServlet.asLink(baseUrl, repository, branch, path);
+
+ assertNotNull(link);
+ assertEquals(baseUrl + Constants.RAW_PATH + repository.substring(1) + "/"
+ + branch.replace('/', FSC) + "/" + path, link);
+ }
+
+ @Test
+ public void asLink_HttpUrlTrailingSlashRepoLeadingSlashBranchWithSlashPathFolderFile()
+ {
+ String baseUrl = "http://localhost/";
+ String repository = "/test.git";
+ String branch = "b/52";
+ String path = "go/to/f.k";
+
+ String link = RawServlet.asLink(baseUrl, repository, branch, path);
+
+ assertNotNull(link);
+ assertEquals(baseUrl.substring(0, baseUrl.length()-1) + Constants.RAW_PATH + repository.substring(1) + "/"
+ + branch.replace('/', FSC) + "/" + path.replace('/', FSC), link);
+ }
+
+ @Test
+ public void asLink_HttpUrlRepoInFolder()
+ {
+ String baseUrl = "http://localhost";
+ String repository = "project/repo.git";
+ String branch = null;
+ String path = null;
+
+ String link = RawServlet.asLink(baseUrl, repository, branch, path);
+
+ assertNotNull(link);
+ assertEquals(baseUrl + Constants.RAW_PATH + repository + "/", link);
+ }
+
+ @Test
+ public void asLink_HttpUrlRepoInSubFolder()
+ {
+ String baseUrl = "http://localhost";
+ String repository = "some/project/repo.git";
+ String branch = null;
+ String path = null;
+
+ String link = RawServlet.asLink(baseUrl, repository, branch, path);
+
+ assertNotNull(link);
+ assertEquals(baseUrl + Constants.RAW_PATH + repository + "/", link);
+ }
+
+ @Test
+ public void asLink_HttpUrlRepoInSubFolderBranch()
+ {
+ String baseUrl = "http://localhost";
+ String repository = "some/project/repo.git";
+ String branch = "laluna";
+ String path = null;
+
+ String link = RawServlet.asLink(baseUrl, repository, branch, path);
+
+ assertNotNull(link);
+ assertEquals(baseUrl + Constants.RAW_PATH + repository + "/"
+ + branch + "/", link);
+ }
+
+ @Test
+ public void asLink_HttpUrlRepoInSubFolderBranchWithSlash()
+ {
+ String baseUrl = "http://localhost";
+ String repository = "some/project/repo.git";
+ String branch = "la/le/lu";
+ String path = null;
+
+ String link = RawServlet.asLink(baseUrl, repository, branch, path);
+
+ assertNotNull(link);
+ assertEquals(baseUrl + Constants.RAW_PATH + repository + "/"
+ + branch.replace('/', FSC) + "/", link);
+ }
+
+ @Test
+ public void asLink_HttpUrlRepoInSubFolderBranchPathFile()
+ {
+ String baseUrl = "http://localhost";
+ String repository = "some/project/repo.git";
+ String branch = "laluna";
+ String path = "elrtkx.fg";
+
+ String link = RawServlet.asLink(baseUrl, repository, branch, path);
+
+ assertNotNull(link);
+ assertEquals(baseUrl + Constants.RAW_PATH + repository + "/"
+ + branch + "/" + path, link);
+ }
+
+ @Test
+ public void asLink_HttpUrlRepoInSubFolderLeadingSlashBranchWithSlashPathFolderFile()
+ {
+ String baseUrl = "http://localhost";
+ String repository = "/some/project/repo.git";
+ String branch = "la/le/lu";
+ String path = "doremi/fa/SOLA/di.mp3";
+
+ String link = RawServlet.asLink(baseUrl, repository, branch, path);
+
+ assertNotNull(link);
+ assertEquals(baseUrl + Constants.RAW_PATH + repository.substring(1) + "/"
+ + branch.replace('/', FSC) + "/" + path.replace('/', FSC), link);
+ }
+
+ @Test
+ public void asLink_HttpUrlRepoPathFolderFile()
+ {
+ String baseUrl = "http://localhost";
+ String repository = "repo.git";
+ String branch = null;
+ String path = "doko/di.mp3";
+
+ String link = RawServlet.asLink(baseUrl, repository, branch, path);
+
+ assertNotNull(link);
+ assertEquals(baseUrl + Constants.RAW_PATH + repository + "/", link);
+ }
+
+ @Test
+ public void asLink_HttpUrlRepoTrailingSlashPathFileLeadingSlash()
+ {
+ String baseUrl = "http://localhost";
+ String repository = "repo.git/";
+ String branch = null;
+ String path = "/di.mp3";
+
+ String link = RawServlet.asLink(baseUrl, repository, branch, path);
+
+ assertNotNull(link);
+ assertEquals(baseUrl + Constants.RAW_PATH + repository, link);
+ }
+
+ @Test
+ public void asLink_HttpUrlRepoBranchPathFileLeadingSlash()
+ {
+ String baseUrl = "http://localhost";
+ String repository = "repo.git";
+ String branch = "bee";
+ String path = "/bop.mp3";
+
+ String link = RawServlet.asLink(baseUrl, repository, branch, path);
+
+ assertNotNull(link);
+ assertEquals(baseUrl + Constants.RAW_PATH + repository + "/"
+ + branch + path, link);
+ }
+
+ @Test
+ public void asLink_HttpUrlRepoBranchPathFolderLeadingSlashTrailingSlash()
+ {
+ String baseUrl = "http://localhost";
+ String repository = "repo.git";
+ String branch = "bee";
+ String path = "/bam/";
+
+ String link = RawServlet.asLink(baseUrl, repository, branch, path);
+
+ assertNotNull(link);
+ assertEquals(baseUrl + Constants.RAW_PATH + repository + "/"
+ + branch + "/bam" + FSC, link);
+ }
+
+ @Test
+ public void asLink_HttpUrlRepoBranchPathSubFolderLeadingSlashTrailingSlash()
+ {
+ String baseUrl = "http://localhost";
+ String repository = "repo.git";
+ String branch = "bee";
+ String path = "/bapedi/boo/";
+
+ String link = RawServlet.asLink(baseUrl, repository, branch, path);
+
+ assertNotNull(link);
+ assertEquals(baseUrl + Constants.RAW_PATH + repository + "/"
+ + branch + "/" + "bapedi" + FSC + "boo" + FSC, link);
+ }
+
+ @Test
+ public void asLink_HttpUrlRepoCommitId()
+ {
+ String baseUrl = "http://localhost";
+ String repository = "repo.git";
+ String branch = "c7eef37bfe5ae246cdf5ca5c502e4b5471290cb1";
+ String path = null;
+
+ String link = RawServlet.asLink(baseUrl, repository, branch, path);
+
+ assertNotNull(link);
+ assertEquals(baseUrl + Constants.RAW_PATH + repository + "/"
+ + branch + "/", link);
+ }
+
+ @Test
+ public void asLink_HttpUrlRepoCommitIdPathFile()
+ {
+ String baseUrl = "http://localhost";
+ String repository = "repo.git";
+ String branch = "c7eef37bfe5ae246cdf5ca5c502e4b5471290cb1";
+ String path = "file";
+
+ String link = RawServlet.asLink(baseUrl, repository, branch, path);
+
+ assertNotNull(link);
+ assertEquals(baseUrl + Constants.RAW_PATH + repository + "/"
+ + branch + "/" + path, link);
+ }
+
+ @Test
+ public void asLink_HttpUrlRepoCommitIdPathFolderFileFile()
+ {
+ String baseUrl = "http://localhost";
+ String repository = "repo.git";
+ String branch = "c7eef37bfe5ae246cdf5ca5c502e4b5471290cb1";
+ String path = "file/in/folder";
+
+ String link = RawServlet.asLink(baseUrl, repository, branch, path);
+
+ assertNotNull(link);
+ assertEquals(baseUrl + Constants.RAW_PATH + repository + "/"
+ + branch + "/" + path.replace('/', FSC), link);
+ }
+
+ @Test
+ public void asLink_HttpUrlRepoBranchWithSlash_differentFsc()
+ {
+ settings.overrideSetting(Keys.web.forwardSlashCharacter, "|");
+ String baseUrl = "http://localhost";
+ String repository = "repo.git";
+ String branch = "some/feature";
+ String path = null;
+
+ String link = RawServlet.asLink(baseUrl, repository, branch, path);
+
+ assertNotNull(link);
+ assertEquals(baseUrl + Constants.RAW_PATH + repository + "/"
+ + branch.replaceAll("/", "|") + "/", link);
+ }
+
+ @Test
+ public void asLink_HttpUrlRepoBranchWithSlashPathFolderFile_differentFsc()
+ {
+ settings.overrideSetting(Keys.web.forwardSlashCharacter, ";");
+ String baseUrl = "http://localhost";
+ String repository = "repo.git";
+ String branch = "some/feature";
+ String path = "file/in/folder";
+
+ String link = RawServlet.asLink(baseUrl, repository, branch, path);
+
+ assertNotNull(link);
+ assertEquals(baseUrl + Constants.RAW_PATH + repository + "/"
+ + branch.replaceAll("/", ";") + "/" + path.replaceAll("/", ";"), link);
+ }
+
+ @Test
+ public void asLink_HttpUrlRepoBranchWithSlash_explicitFscSameAsDefault()
+ {
+ settings.overrideSetting(Keys.web.forwardSlashCharacter, "!");
+ String baseUrl = "http://localhost";
+ String repository = "repo.git";
+ String branch = "some/feature";
+ String path = null;
+
+ String link = RawServlet.asLink(baseUrl, repository, branch, path);
+
+ assertNotNull(link);
+ assertEquals(baseUrl + Constants.RAW_PATH + repository + "/"
+ + branch.replaceAll("/", "!") + "/", link);
+ }
+
+ @Test
+ public void asLink_HttpUrlRepoBranchWithSlashPathFolderFile_explicitFscSameAsDefault()
+ {
+ settings.overrideSetting(Keys.web.forwardSlashCharacter, "!");
+ String baseUrl = "http://localhost";
+ String repository = "repo.git";
+ String branch = "some/feature";
+ String path = "file/in/folder";
+
+ String link = RawServlet.asLink(baseUrl, repository, branch, path);
+
+ assertNotNull(link);
+ assertEquals(baseUrl + Constants.RAW_PATH + repository + "/"
+ + branch.replaceAll("/", "!") + "/" + path.replaceAll("/", "!"), link);
+ }
+
+ @Test
+ public void asLink_HttpUrlRepoBranchWithDefaultFsc_differentFsc()
+ {
+ settings.overrideSetting(Keys.web.forwardSlashCharacter, ":");
+ String baseUrl = "http://localhost";
+ String repository = "repo.git";
+ String branch = "important" + FSC + "feature";
+ String path = null;
+
+ String link = RawServlet.asLink(baseUrl, repository, branch, path);
+
+ assertNotNull(link);
+ assertEquals(baseUrl + Constants.RAW_PATH + repository + "/"
+ + branch + "/", link);
+ }
+
+ @Test
+ public void asLink_HttpUrlRepoBranchWithSlashPathFileWithDefaultFsc_differentFsc()
+ {
+ settings.overrideSetting(Keys.web.forwardSlashCharacter, "|");
+ String baseUrl = "http://localhost";
+ String repository = "repo.git";
+ String branch = "some/feature";
+ String path = "large" + FSC + "file";
+
+ String link = RawServlet.asLink(baseUrl, repository, branch, path);
+
+ assertNotNull(link);
+ assertEquals(baseUrl + Constants.RAW_PATH + repository + "/"
+ + branch.replaceAll("/", "|") + "/" + path, link);
+ }
+
+ @Test
+ public void asLink_HttpUrlRepoBranchWithDefaultFscAndSlash_differentFsc()
+ {
+ settings.overrideSetting(Keys.web.forwardSlashCharacter, ":");
+ String baseUrl = "http://localhost";
+ String repository = "repo.git";
+ String branch = "hotf" + FSC + "x/issue/1234";
+ String path = null;
+
+ String link = RawServlet.asLink(baseUrl, repository, branch, path);
+
+ assertNotNull(link);
+ assertEquals(baseUrl + Constants.RAW_PATH + repository + "/"
+ + branch.replaceAll("/", ":") + "/", link);
+ }
+
+ @Test
+ public void asLink_HttpUrlRepoBranchWithDefaultFscAndSlashPathFolderFileWithDefaultFsc_differentFsc()
+ {
+ settings.overrideSetting(Keys.web.forwardSlashCharacter, "|");
+ String baseUrl = "http://localhost";
+ String repository = "repo.git";
+ String branch = "some/feature" + FSC + "in/here";
+ String path = "large" + FSC + "stuff/folder/file" + FSC + "16";
+
+ String link = RawServlet.asLink(baseUrl, repository, branch, path);
+
+ assertNotNull(link);
+ assertEquals(baseUrl + Constants.RAW_PATH + repository + "/"
+ + branch.replaceAll("/", "|") + "/" + path.replaceAll("/", "|"), link);
+ }
+
+
+ @Test
+ public void getBranch_Repo()
+ {
+ String branch = rawServlet.getBranch("test.git", "test.git/");
+
+ assertEquals("Branch was supposed to be empty as no branch was given.", "", branch);
+ }
+
+ @Test
+ public void getBranch_PiNull()
+ {
+ String branch = rawServlet.getBranch("test.git", null);
+
+ assertEquals("Branch was supposed to be empty as path info is null.", "", branch);
+ }
+
+
+ @Test
+ public void getBranch_PiEmpty()
+ {
+ String branch = rawServlet.getBranch("test.git", "");
+
+ assertEquals("Branch was supposed to be empty as no path info exists.", "", branch);
+ }
+
+ @Test
+ public void getBranch_LeadinRepo()
+ {
+ String branch = rawServlet.getBranch("test.git", "some/test.git/");
+
+ assertEquals("Branch was supposed to be empty as no branch was given.", "", branch);
+ }
+
+ @Test
+ public void getBranch_ProjectRepo()
+ {
+ String branch = rawServlet.getBranch("smack/dab.git", "smack/dab.git/");
+
+ assertEquals("Branch was supposed to be empty as no branch was given.", "", branch);
+ }
+
+ @Test
+ public void getBranch_RepoBranch()
+ {
+ String branch = rawServlet.getBranch("test.git", "test.git/bee");
+
+ assertEquals("bee", branch);
+ }
+
+ @Test
+ public void getBranch_LeadinRepoBranch()
+ {
+ String branch = rawServlet.getBranch("repo.git", "project/repo.git/bae");
+
+ assertEquals("bae", branch);
+ }
+
+ @Test
+ public void getBranch_ProjectRepoBranch()
+ {
+ String branch = rawServlet.getBranch("test/r.git", "test/r.git/b");
+
+ assertEquals("b", branch);
+ }
+
+ @Test
+ public void getBranch_LeadinProjectRepoBranch()
+ {
+ String branch = rawServlet.getBranch("test/r.git", "a/b/test/r.git/b");
+
+ assertEquals("b", branch);
+ }
+
+ @Test
+ public void getBranch_RepoBranchTrailingSlash()
+ {
+ String branch = rawServlet.getBranch("/test.git", "test.git/fixthis/");
+
+ assertEquals("fixthis", branch);
+ }
+
+ @Test
+ public void getBranch_RepoBranchFile()
+ {
+ String branch = rawServlet.getBranch("/bob.git", "bob.git/branch/file.txt");
+
+ assertEquals("branch", branch);
+ }
+
+ @Test
+ public void getBranch_RepoBranchFolderFile()
+ {
+ String branch = rawServlet.getBranch("/bill.git", "bill.git/flex/fold!laundr.y");
+
+ assertEquals("flex", branch);
+ }
+
+ @Test
+ public void getBranch_RepoBranchFoldersTrailingSlash()
+ {
+ String branch = rawServlet.getBranch("bill.git", "bill.git/flex/fold!gold/");
+
+ assertEquals("flex", branch);
+ }
+
+ @Test
+ public void getBranch_RepoBranchFoldersTrailingFsc()
+ {
+ String branch = rawServlet.getBranch("bill.git", "bill.git/flex/fold"+ FSC + "gold" + FSC);
+
+ assertEquals("flex", branch);
+ }
+
+ @Test
+ public void getBranch_LeadinProjectRepoBranchFoldersTrailingSlash()
+ {
+ String branch = rawServlet.getBranch("bam/bum.git", "bim/bam/bum.git/klingeling/dumm" + FSC + "di" + FSC + "dumm/");
+
+ assertEquals("klingeling", branch);
+ }
+
+ @Test
+ public void getBranch_LeadinProjectRepoBranchFoldersTrailingFsc()
+ {
+ String branch = rawServlet.getBranch("bam/bum.git", "bim/bam/bum.git/klingeling/dumm" + FSC + "di" + FSC + "dumm" + FSC);
+
+ assertEquals("klingeling", branch);
+ }
+
+ @Test
+ public void getBranch_RepoCommitid()
+ {
+ String branch = rawServlet.getBranch("git.git", "git.git/a02159e6378d63d0e1ad3c04a05462d9fc62fe89");
+
+ assertEquals("a02159e6378d63d0e1ad3c04a05462d9fc62fe89", branch);
+ }
+
+ @Test
+ public void getBranch_ProjectRepoCommitidFolderTrailingSlash()
+ {
+ String branch = rawServlet.getBranch("git/git.git", "git/git.git/a02159e6378d63d0e1ad3c04a05462d9fc62fe89/src/");
+
+ assertEquals("a02159e6378d63d0e1ad3c04a05462d9fc62fe89", branch);
+ }
+
+ @Test
+ public void getBranch_ProjectRepoCommitidFolderTrailingFsc()
+ {
+ String branch = rawServlet.getBranch("git/git.git", "git/git.git/a02159e6378d63d0e1ad3c04a05462d9fc62fe89/src" + FSC);
+
+ assertEquals("a02159e6378d63d0e1ad3c04a05462d9fc62fe89", branch);
+ }
+
+ @Test
+ public void getBranch_ProjectSubRepoCommitidFolderFile()
+ {
+ String branch = rawServlet.getBranch("git/git.git", "Git implementations/git/git.git/a02159e6378d63d0e1ad3c04a05462d9fc62fe89/src" + FSC + "README.md");
+
+ assertEquals("a02159e6378d63d0e1ad3c04a05462d9fc62fe89", branch);
+ }
+
+ @Test
+ public void getBranch_RepoBranchWithFsc()
+ {
+ String branch = rawServlet.getBranch("git.git", "git.git/feature" + FSC + "rebase");
+
+ assertEquals("feature/rebase", branch);
+ }
+
+ @Test
+ public void getBranch_RepoBranchWithTwoFsc()
+ {
+ String branch = rawServlet.getBranch("git.git", "git.git/feature" + FSC + "rebase" + FSC + "onto");
+
+ assertEquals("feature/rebase/onto", branch);
+ }
+
+ @Test
+ public void getBranch_ProjectRepoBranchWithTwoFsc()
+ {
+ String branch = rawServlet.getBranch("Java/git/git.git", "Java/git/git.git/feature" + FSC + "rebase" + FSC + "onto");
+
+ assertEquals("feature/rebase/onto", branch);
+ }
+
+ @Test
+ public void getBranch_RepoBranchWithTwoFscTrailingSlash()
+ {
+ String branch = rawServlet.getBranch("git.git", "git.git/feature" + FSC + "rebase" + FSC + "onto/");
+
+ assertEquals("feature/rebase/onto", branch);
+ }
+
+ @Test
+ public void getBranch_ProjectRepoBranchWithTwoFscTrailingSlash()
+ {
+ String branch = rawServlet.getBranch("in Go/git.git", "in Go/git.git/feature" + FSC + "rebase" + FSC + "onto/");
+
+ assertEquals("feature/rebase/onto", branch);
+ }
+
+ @Test
+ public void getBranch_LeadinProjectRepoBranchWithTwoFscTrailingSlash()
+ {
+ String branch = rawServlet.getBranch("Go/git.git", "all the gits/Go/git.git/feature" + FSC + "rebase" + FSC + "onto/");
+
+ assertEquals("feature/rebase/onto", branch);
+ }
+
+ @Test
+ public void getBranch_RepoBranchWithFscFolder()
+ {
+ String branch = rawServlet.getBranch("git.git", "git.git/feature" + FSC + "rebase/onto");
+
+ assertEquals("feature/rebase", branch);
+ }
+
+ @Test
+ public void getBranch_RepoBranchWithFscFolderFile()
+ {
+ String branch = rawServlet.getBranch("git.git", "git.git/feature" + FSC + "rebase/onto" + FSC + "head");
+
+ assertEquals("feature/rebase", branch);
+ }
+
+ @Test
+ public void getBranch_RepoBranchWithFsc_differentFsc()
+ {
+ settings.overrideSetting(Keys.web.forwardSlashCharacter, "|");
+
+ String branch = rawServlet.getBranch("git.git", "git.git/some|feature");
+
+ assertEquals("some/feature", branch);
+ }
+
+ @Test
+ public void getBranch_RepoBranchWithFsc_explicitFscSameAsDefault()
+ {
+ settings.overrideSetting(Keys.web.forwardSlashCharacter, FSC);
+
+ String branch = rawServlet.getBranch("git.git", "git.git/some" + FSC + "feature");
+
+ assertEquals("some/feature", branch);
+ }
+
+ @Test
+ public void getBranch_RepoBranchWithFscFolders_differentFsc()
+ {
+ settings.overrideSetting(Keys.web.forwardSlashCharacter, ":");
+
+ String branch = rawServlet.getBranch("git.git", "git.git/hotfix:1.2.3/src:main:java/");
+
+ assertEquals("hotfix/1.2.3", branch);
+ }
+
+ @Test
+ public void getBranch_LeadindRepoBranchWithFscFolderFile_explicitFscSameAsDefault()
+ {
+ settings.overrideSetting(Keys.web.forwardSlashCharacter, FSC);
+
+ String branch = rawServlet.getBranch("git.git", "IBM/git.git/some" + FSC + "feature/some" + FSC + "folder" + FSC + "file.dot");
+
+ assertEquals("some/feature", branch);
+ }
+
+ @Ignore // TODO: Why was it implemented this way?
+ @Test
+ public void getBranch_RepoBranchWithDefaultFsc_differentFsc()
+ {
+ settings.overrideSetting(Keys.web.forwardSlashCharacter, ";");
+
+ String branch = rawServlet.getBranch("git.git", "git.git/some" + FSC + "feature");
+
+ assertEquals("some" + FSC + "feature", branch);
+ }
+
+ @Test
+ public void getBranch_RepoBranchWithDifferentFscFolderFileWithDefaultFsc_differentFsc()
+ {
+ settings.overrideSetting(Keys.web.forwardSlashCharacter, ";");
+
+ String branch = rawServlet.getBranch("git.git", "git.git/some;feature/path" + FSC + "to" + FSC + "file.txt");
+
+ assertEquals("some/feature", branch);
+ }
+
+ @Ignore // TODO: Why was it implemented this way?
+ @Test
+ public void getBranch_RepoBranchWithDefaultFscAndDifferentFsc_differentFsc()
+ {
+ settings.overrideSetting(Keys.web.forwardSlashCharacter, ":");
+
+ String branch = rawServlet.getBranch("git.git", "git.git/go" + FSC + "to:start");
+
+ assertEquals("go" + FSC + "to/start", branch);
+ }
+
+ @Ignore // TODO: Why was it implemented this way?
+ @Test
+ public void getBranch_RepoBranchWithDefaultFscAndDifferentFscFolderWithDefaultFsc_differentFsc()
+ {
+ settings.overrideSetting(Keys.web.forwardSlashCharacter, "+");
+
+ String branch = rawServlet.getBranch("git.git", "git.git/go" + FSC + "to+prison/dont" + FSC + "collect");
+
+ assertEquals("go" + FSC + "to/prison", branch);
+ }
+
+ @Ignore // TODO: Why was it implemented this way?
+ @Test
+ public void getBranch_RepoBranchWithDefaultFscAndDifferentFscFolderWithDefaultFscTrailingSlash_differentFsc()
+ {
+ settings.overrideSetting(Keys.web.forwardSlashCharacter, "+");
+
+ String branch = rawServlet.getBranch("git.git", "git.git/go" + FSC + "to+ prison/dont" + FSC + "collect/");
+
+ assertEquals("go" + FSC + "to/prison", branch);
+ }
+
+ @Ignore // TODO: Why was it implemented this way?
+ @Test
+ public void getBranch_RepoBranchWithDefaultFscAndDifferentFscFolderWithDefaultFscTrailingFsc_differentFsc()
+ {
+ settings.overrideSetting(Keys.web.forwardSlashCharacter, "+");
+
+ String branch = rawServlet.getBranch("git.git", "git.git/go" + FSC + "to+prison/dont" + FSC + "collect+");
+
+ assertEquals("go" + FSC + "to/prison", branch);
+ }
+
+ @Ignore // TODO: Why was it implemented this way?
+ @Test
+ public void getBranch_RepoBranchWithDefaultFscAndDifferentFscFolderWithDefaultFscTrailingDefaultFsc_differentFsc()
+ {
+ settings.overrideSetting(Keys.web.forwardSlashCharacter, "+");
+
+ String branch = rawServlet.getBranch("git.git", "git.git/go" + FSC + "to+prison/dont" + FSC + "collect" + FSC);
+
+ assertEquals("go" + FSC + "to/prison", branch);
+ }
+
+ @Ignore // TODO: Why was it implemented this way?
+ @Test
+ public void getBranch_RepoBranchWithDefaultFscAndDifferentFscFolderFileWithDefaultFscAndDifferentFsc_differentFsc()
+ {
+ settings.overrideSetting(Keys.web.forwardSlashCharacter, "+");
+
+ String branch = rawServlet.getBranch("git.git", "git.git/go" + FSC + "to+prison/dont" + FSC + "collect+money.eur");
+
+ assertEquals("go" + FSC + "to/prison", branch);
+ }
+
+ @Ignore // TODO: Why was it implemented this way?
+ @Test
+ public void getBranch_LeadinProjectRepoBranchWithDefaultFscAndDifferentFscFolderFileWithDefaultFscAndDifferentFsc_differentFsc()
+ {
+ settings.overrideSetting(Keys.web.forwardSlashCharacter, "+");
+
+ String branch = rawServlet.getBranch("games/Monopoly/git.git", "blah/games/Monopoly/git.git/go" + FSC + "to+prison/dont" + FSC + "collect+money.eur");
+
+ assertEquals("go" + FSC + "to/prison", branch);
+ }
+
+
+
+
+
+
+
+
+ @Test
+ public void getPath_Repo()
+ {
+ String path = rawServlet.getPath("test.git", "", "test.git/");
+
+ assertEquals("Path was supposed to be empty as no path was given.", "", path);
+ }
+
+ @Test
+ public void getPath_PiNull()
+ {
+ String path = rawServlet.getPath("test.git", "", null);
+
+ assertEquals("Path was supposed to be empty as path info is null.", "", path);
+ }
+
+
+ @Test
+ public void getPath_PiEmpty()
+ {
+ String path = rawServlet.getPath("test.git", "", "");
+
+ assertEquals("Path was supposed to be empty as no path info exists.", "", path);
+ }
+
+ @Test
+ public void getPath_LeadinRepo()
+ {
+ String path = rawServlet.getPath("test.git", "", "some/test.git/");
+
+ assertEquals("Path was supposed to be empty as no path was given.", "", path);
+ }
+
+ @Test
+ public void getPath_ProjectRepo()
+ {
+ String path = rawServlet.getPath("smack/dab.git", "", "smack/dab.git/");
+
+ assertEquals("Path was supposed to be empty as no path was given.", "", path);
+ }
+
+ @Test
+ public void getPath_RepoBranch()
+ {
+ String path = rawServlet.getPath("test.git", "bee", "test.git/bee");
+
+ assertEquals("Expected returned path to be empty since no path was present", "", path);
+ }
+
+ @Test
+ public void getPath_LeadinRepoBranch()
+ {
+ String path = rawServlet.getPath("repo.git", "bae", "project/repo.git/bae");
+
+ assertEquals("Expected path to be empty since no path was present","" , path);
+ }
+
+ @Test
+ public void getPath_ProjectRepoBranch()
+ {
+ String path = rawServlet.getPath("test/r.git", "b", "test/r.git/b");
+
+ assertEquals("Expected returned path to be empty since no path was present", "", path);
+ }
+
+ @Test
+ public void getPath_LeadinProjectRepoBranch()
+ {
+ String path = rawServlet.getPath("test/r.git", "b", "a/b/test/r.git/b");
+
+ assertEquals("Expected returned path to be empty since no path was present", "", path);
+ }
+
+ @Test
+ public void getPath_RepoBranchTrailingSlash()
+ {
+ String path = rawServlet.getPath("test.git", "fixthis", "test.git/fixthis/");
+
+ assertEquals("Expected returned path to be empty since no path was present", "", path);
+ }
+
+ @Test
+ public void getPath_RepoBranchFile()
+ {
+ String path = rawServlet.getPath("/bob.git", "branch", "bob.git/branch/file.txt");
+
+ assertEquals("file.txt", path);
+ }
+
+ @Test
+ public void getPath_RepoBranchFolderFile()
+ {
+ String path = rawServlet.getPath("/bill.git", "flex", "bill.git/flex/fold" + FSC + "laundr.y");
+
+ assertEquals("fold/laundr.y", path);
+ }
+
+ @Test
+ public void getPath_RepoBranchFoldersTrailingSlash()
+ {
+ String path = rawServlet.getPath("bill.git", "flex", "bill.git/flex/fold"+ FSC + "gold/");
+
+ assertEquals("fold/gold", path);
+ }
+
+ @Test
+ public void getPath_RepoBranchFoldersTrailingFsc()
+ {
+ String path = rawServlet.getPath("bill.git", "flex", "bill.git/flex/fold"+ FSC + "gold" + FSC);
+
+ assertEquals("fold/gold", path);
+ }
+
+ @Test
+ public void getPath_LeadinProjectRepoBranchFoldersTrailingSlash()
+ {
+ String path = rawServlet.getPath("bam/bum.git", "klingeling", "bim/bam/bum.git/klingeling/dumm" + FSC + "di" + FSC + "dumm/");
+
+ assertEquals("dumm/di/dumm", path);
+ }
+
+ @Test
+ public void getPath_LeadinProjectRepoBranchFoldersTrailingFsc()
+ {
+ String path = rawServlet.getPath("bam/bum.git", "klingeling", "bim/bam/bum.git/klingeling/dumm" + FSC + "di" + FSC + "dumm" + FSC);
+
+ assertEquals("dumm/di/dumm", path);
+ }
+
+ @Test
+ public void getPath_RepoCommitid()
+ {
+ String path = rawServlet.getPath("/git.git", "a02159e6378d63d0e1ad3c04a05462d9fc62fe89", "git.git/a02159e6378d63d0e1ad3c04a05462d9fc62fe89/");
+
+ assertEquals("Expected returned path to be empty since no path was present", "", path);
+ }
+
+ @Test
+ public void getPath_RepoCommitidNoTrailingSlash()
+ {
+ String path = rawServlet.getPath("/git.git", "a02159e6378d63d0e1ad3c04a05462d9fc62fe89", "git.git/a02159e6378d63d0e1ad3c04a05462d9fc62fe89");
+
+ assertEquals("Expected returned path to be empty since no path was present", "", path);
+ }
+
+ @Test
+ public void getPath_ProjectRepoCommitidFolderTrailingSlash()
+ {
+ String path = rawServlet.getPath("git/git.git", "a02159e6378d63d0e1ad3c04a05462d9fc62fe89", "git/git.git/a02159e6378d63d0e1ad3c04a05462d9fc62fe89/src/");
+
+ assertEquals("src", path);
+ }
+
+ @Test
+ public void getPath_ProjectRepoCommitidFolderTrailingFsc()
+ {
+ String path = rawServlet.getPath("git/git.git", "a02159e6378d63d0e1ad3c04a05462d9fc62fe89", "git/git.git/a02159e6378d63d0e1ad3c04a05462d9fc62fe89/src" + FSC);
+
+ assertEquals("src", path);
+ }
+
+ @Test
+ public void getPath_ProjectSubRepoCommitidFolderFile()
+ {
+ String path = rawServlet.getPath("git/git.git", "a02159e6378d63d0e1ad3c04a05462d9fc62fe89", "Git implementations/git/git.git/a02159e6378d63d0e1ad3c04a05462d9fc62fe89/src" + FSC + "README.md");
+
+ assertEquals("src/README.md", path);
+ }
+
+ @Test
+ public void getPath_RepoBranchWithFsc()
+ {
+ String path = rawServlet.getPath("git.git", "feature/rebase", "git.git/feature" + FSC + "rebase");
+
+ assertEquals("Expected returned path to be empty since no path was present", "", path);
+ }
+
+ @Test
+ public void getPath_RepoBranchWithTwoFsc()
+ {
+ String path = rawServlet.getPath("git.git", "feature/rebase/onto", "git.git/feature" + FSC + "rebase" + FSC + "onto");
+
+ assertEquals("Expected returned path to be empty since no path was present", "", path);
+ }
+
+ @Test
+ public void getPath_ProjectRepoBranchWithTwoFsc()
+ {
+ String path = rawServlet.getPath("Java/git/git.git", "feature/rebase/onto", "Java/git/git.git/feature" + FSC + "rebase" + FSC + "onto");
+
+ assertEquals("Expected returned path to be empty since no path was present", "", path);
+ }
+
+ @Test
+ public void getPath_RepoBranchWithTwoFscTrailingSlash()
+ {
+ String path = rawServlet.getPath("git.git", "feature/rebase/onto", "git.git/feature" + FSC + "rebase" + FSC + "onto/");
+
+ assertEquals("Expected returned path to be empty since no path was present", "", path);
+ }
+
+ @Test
+ public void getPath_ProjectRepoBranchWithTwoFscTrailingSlash()
+ {
+ String path = rawServlet.getPath("in Go/git.git", "feature/rebase/onto", "in Go/git.git/feature" + FSC + "rebase" + FSC + "onto/");
+
+ assertEquals("Expected returned path to be empty since no path was present", "", path);
+ }
+
+ @Test
+ public void getPath_LeadinProjectRepoBranchWithTwoFscTrailingSlash()
+ {
+ String path = rawServlet.getPath("Go/git.git", "feature/rebase/onto", "all the gits/Go/git.git/feature" + FSC + "rebase" + FSC + "onto/");
+
+ assertEquals("Expected returned path to be empty since no path was present", "", path);
+ }
+
+ @Test
+ public void getPath_RepoBranchWithFscFolder()
+ {
+ String path = rawServlet.getPath("git.git", "feature/rebase", "git.git/feature" + FSC + "rebase/onto");
+
+ assertEquals("onto", path);
+ }
+
+ @Test
+ public void getPath_RepoBranchWithFscFolderFile()
+ {
+ String path = rawServlet.getPath("git.git", "feature/rebase", "git.git/feature" + FSC + "rebase/onto" + FSC + "head");
+
+ assertEquals("onto/head", path);
+ }
+
+ @Test
+ public void getPath_RepoBranchWithFsc_differentFsc()
+ {
+ settings.overrideSetting(Keys.web.forwardSlashCharacter, "|");
+
+ String path = rawServlet.getPath("git.git", "some/feature", "git.git/some|feature");
+
+ assertEquals("Expected returned path to be empty since no path was present", "", path);
+ }
+
+ @Test
+ public void getPath_RepoBranchWithFsc_explicitFscSameAsDefault()
+ {
+ settings.overrideSetting(Keys.web.forwardSlashCharacter, String.valueOf(FSC));
+
+ String path = rawServlet.getPath("git.git", "some/feature", "git.git/some" + FSC + "feature");
+
+ assertEquals("Expected returned path to be empty since no path was present", "", path);
+ }
+
+ @Test
+ public void getPath_RepoBranchWithFscFoldersTrailingSlash_differentFsc()
+ {
+ settings.overrideSetting(Keys.web.forwardSlashCharacter, ":");
+
+ String path = rawServlet.getPath("git.git", "hotfix/1.2.3", "git.git/hotfix:1.2.3/src:main:java/");
+
+ assertEquals("src/main/java", path);
+ }
+
+ @Test
+ public void getPath_LeadindRepoBranchWithFscFolderFile_explicitFscSameAsDefault()
+ {
+ settings.overrideSetting(Keys.web.forwardSlashCharacter, String.valueOf(FSC));
+
+ String path = rawServlet.getPath("git.git", "some/feature", "IBM/git.git/some" + FSC + "feature/some" + FSC + "folder" + FSC + "file.dot");
+
+ assertEquals("some/folder/file.dot", path);
+ }
+
+ @Test
+ public void getPath_RepoBranchWithDefaultFsc_differentFsc()
+ {
+ settings.overrideSetting(Keys.web.forwardSlashCharacter, ";");
+
+ String path = rawServlet.getPath("git.git", "some" + FSC + "feature", "git.git/some" + FSC + "feature");
+
+ assertEquals("Expected returned path to be empty since no path was present", "", path);
+ }
+
+
+ @Test
+ public void getPath_RepoBranchWithDefaultFscTrailingSlash_differentFsc()
+ {
+ settings.overrideSetting(Keys.web.forwardSlashCharacter, ";");
+
+ String path = rawServlet.getPath("git.git", "some" + FSC + "feature", "git.git/some" + FSC + "feature/");
+
+ assertEquals("Expected returned path to be empty since no path was present", "", path);
+ }
+
+ @Ignore // TODO: Why was it implemented this way?
+ @Test
+ public void getPath_RepoBranchWithDifferentFscFolderFileWithDefaultFsc_differentFsc()
+ {
+ settings.overrideSetting(Keys.web.forwardSlashCharacter, ";");
+
+ String path = rawServlet.getPath("git.git", "some/feature", "git.git/some;feature/path" + FSC + "to" + FSC + "file.txt");
+
+ assertEquals("path" + FSC + "to" + FSC + "file.txt", path);
+ }
+
+ @Test
+ public void getPath_RepoBranchWithDefaultFscAndDifferentFsc_differentFsc()
+ {
+ settings.overrideSetting(Keys.web.forwardSlashCharacter, ":");
+
+ String path = rawServlet.getPath("git.git", "go" + FSC + "to/start", "git.git/go" + FSC + "to:start");
+
+ assertEquals("Expected returned path to be empty since no path was present", "", path);
+ }
+
+ @Ignore // TODO: Why was it implemented this way?
+ @Test
+ public void getPath_RepoBranchWithDefaultFscAndDifferentFscFolderWithDefaultFsc_differentFsc()
+ {
+ settings.overrideSetting(Keys.web.forwardSlashCharacter, "+");
+
+ String path = rawServlet.getPath("git.git", "go" + FSC + "to/prison", "git.git/go" + FSC + "to+prison/dont" + FSC + "collect");
+
+ assertEquals("dont" + FSC + "collect", path);
+ }
+
+ @Ignore // TODO: Why was it implemented this way?
+ @Test
+ public void getPath_RepoBranchWithDefaultFscAndDifferentFscFolderWithDefaultFscTrailingSlash_differentFsc()
+ {
+ settings.overrideSetting(Keys.web.forwardSlashCharacter, "+");
+
+ String path = rawServlet.getPath("git.git", "go" + FSC + "to/prison", "git.git/go" + FSC + "to+prison/dont" + FSC + "collect/");
+
+ assertEquals("dont" + FSC + "collect", path);
+ }
+
+ @Ignore // TODO: Why was it implemented this way?
+ @Test
+ public void getPath_RepoBranchWithDefaultFscAndDifferentFscFolderWithDefaultFscTrailingFsc_differentFsc()
+ {
+ settings.overrideSetting(Keys.web.forwardSlashCharacter, "+");
+
+ String path = rawServlet.getPath("git.git", "go" + FSC + "to/prison", "git.git/go" + FSC + "to+prison/dont" + FSC + "collect+");
+
+ assertEquals("dont" + FSC + "collect", path);
+ }
+
+ @Ignore // TODO: Why was it implemented this way?
+ @Test
+ public void getPath_RepoBranchWithDefaultFscAndDifferentFscFolderWithDefaultFscTrailingDefaultFsc_differentFsc()
+ {
+ settings.overrideSetting(Keys.web.forwardSlashCharacter, "+");
+
+ String path = rawServlet.getPath("git.git", "go" + FSC + "to/prison", "git.git/go" + FSC + "to+prison/dont" + FSC + "collect" + FSC);
+
+ assertEquals("dont" + FSC + "collect" + FSC, path);
+ }
+
+ @Ignore // TODO: Why was it implemented this way?
+ @Test
+ public void getPath_RepoBranchWithDefaultFscAndDifferentFscFolderFileWithDefaultFscAndDifferentFsc_differentFsc()
+ {
+ settings.overrideSetting(Keys.web.forwardSlashCharacter, "+");
+
+ String path = rawServlet.getPath("git.git", "go" + FSC + "to/prison", "git.git/go" + FSC + "to+prison/" + FSC + "dont" + FSC + "collect+money.eur");
+
+ assertEquals(FSC + "dont" + FSC + "collect/money.eur", path);
+ }
+
+ @Ignore // TODO: Why was it implemented this way?
+ @Test
+ public void getPath_LeadinProjectRepoBranchWithDefaultFscAndDifferentFscFolderFileWithDefaultFscAndDifferentFsc_differentFsc()
+ {
+ settings.overrideSetting(Keys.web.forwardSlashCharacter, "+");
+
+ String path = rawServlet.getPath("games/Monopoly/git.git", "go" + FSC + "to/prison", "blah/games/Monopoly/git.git/go" + FSC + "to+prison/dont" + FSC + "collect+money.eur");
+
+ assertEquals("dont" + FSC + "collect/money.eur", path);
+ }
+}
diff --git a/src/test/java/com/gitblit/tests/.gitignore b/src/test/java/com/gitblit/tests/.gitignore
new file mode 100644
index 00000000..44874597
--- /dev/null
+++ b/src/test/java/com/gitblit/tests/.gitignore
@@ -0,0 +1 @@
+/HelloworldKeys.java
diff --git a/src/test/java/com/gitblit/tests/AuthenticationManagerTest.java b/src/test/java/com/gitblit/tests/AuthenticationManagerTest.java
index f8dc8885..81d68895 100644
--- a/src/test/java/com/gitblit/tests/AuthenticationManagerTest.java
+++ b/src/test/java/com/gitblit/tests/AuthenticationManagerTest.java
@@ -19,13 +19,7 @@ import java.io.BufferedReader;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.Principal;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Enumeration;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
+import java.util.*;
import javax.servlet.AsyncContext;
import javax.servlet.DispatcherType;
@@ -43,6 +37,7 @@ import javax.servlet.http.HttpSessionContext;
import javax.servlet.http.HttpUpgradeHandler;
import javax.servlet.http.Part;
+import com.gitblit.utils.PasswordHash;
import org.junit.Test;
import com.gitblit.IUserService;
@@ -653,18 +648,125 @@ public class AuthenticationManagerTest extends GitblitUnitTest {
public void testAuthenticate() throws Exception {
IAuthenticationManager auth = newAuthenticationManager();
+
+ String password = "pass word";
UserModel user = new UserModel("sunnyjim");
- user.password = "password";
+ user.password = password;
users.updateUserModel(user);
- assertNotNull(auth.authenticate(user.username, user.password.toCharArray(), null));
+ char[] pwd = password.toCharArray();
+ assertNotNull(auth.authenticate(user.username, pwd, null));
+
+ // validate that the passed in password has been zeroed out in memory
+ char[] zeroes = new char[pwd.length];
+ Arrays.fill(zeroes, Character.MIN_VALUE);
+ assertArrayEquals(zeroes, pwd);
+ }
+
+
+ @Test
+ public void testAuthenticateDisabledUser() throws Exception {
+ IAuthenticationManager auth = newAuthenticationManager();
+
+
+ String password = "password";
+ UserModel user = new UserModel("sunnyjim");
+ user.password = password;
user.disabled = true;
+ users.updateUserModel(user);
+
+ assertNull(auth.authenticate(user.username, password.toCharArray(), null));
+
+ user.disabled = false;
+ users.updateUserModel(user);
+ assertNotNull(auth.authenticate(user.username, password.toCharArray(), null));
+ }
+
+
+ @Test
+ public void testAuthenticateEmptyPassword() throws Exception {
+ IAuthenticationManager auth = newAuthenticationManager();
+
+ String password = "password";
+ UserModel user = new UserModel("sunnyjim");
+ user.password = password;
users.updateUserModel(user);
- assertNull(auth.authenticate(user.username, user.password.toCharArray(), null));
- users.deleteUserModel(user);
+
+ assertNull(auth.authenticate(user.username, "".toCharArray(), null));
+ assertNull(auth.authenticate(user.username, " ".toCharArray(), null));
+ assertNull(auth.authenticate(user.username, new char[]{' ', '\u0010', '\u0015'}, null));
+ }
+
+
+
+
+ @Test
+ public void testAuthenticateWrongPassword() throws Exception {
+ IAuthenticationManager auth = newAuthenticationManager();
+
+
+ String password = "password";
+ UserModel user = new UserModel("sunnyjim");
+ user.password = password;
+ users.updateUserModel(user);
+
+ assertNull(auth.authenticate(user.username, "helloworld".toCharArray(), null));
}
+
+ @Test
+ public void testAuthenticateNoSuchUser() throws Exception {
+ IAuthenticationManager auth = newAuthenticationManager();
+
+
+ String password = "password";
+ UserModel user = new UserModel("sunnyjim");
+ user.password = password;
+ users.updateUserModel(user);
+
+ assertNull(auth.authenticate("rainyjoe", password.toCharArray(), null));
+ }
+
+
+ @Test
+ public void testAuthenticateUpgradePlaintext() throws Exception {
+ IAuthenticationManager auth = newAuthenticationManager();
+
+ String password = "topsecret";
+ UserModel user = new UserModel("sunnyjim");
+ user.password = password;
+ users.updateUserModel(user);
+
+ assertNotNull(auth.authenticate(user.username, password.toCharArray(), null));
+
+ // validate that plaintext password was automatically updated to hashed one
+ assertTrue(user.password.startsWith(PasswordHash.getDefaultType().name() + ":"));
+
+ // validate that the password is still valid and the user can log in
+ assertNotNull(auth.authenticate(user.username, password.toCharArray(), null));
+ }
+
+
+ @Test
+ public void testAuthenticateUpgradeMD5() throws Exception {
+ IAuthenticationManager auth = newAuthenticationManager();
+
+ String password = "secretAndHashed";
+ UserModel user = new UserModel("sunnyjim");
+ user.password = "MD5:BD95A1CFD00868B59B3564112D1E5847";
+ users.updateUserModel(user);
+
+ assertNotNull(auth.authenticate(user.username, password.toCharArray(), null));
+
+ // validate that MD5 password was automatically updated to hashed one
+ assertTrue(user.password.startsWith(PasswordHash.getDefaultType().name() + ":"));
+
+ // validate that the password is still valid and the user can log in
+ assertNotNull(auth.authenticate(user.username, password.toCharArray(), null));
+ }
+
+
@Test
public void testContenairAuthenticate() throws Exception {
settings.put(Keys.realm.container.autoCreateAccounts, "true");
diff --git a/src/test/java/com/gitblit/tests/BranchTicketServiceTest.java b/src/test/java/com/gitblit/tests/BranchTicketServiceTest.java
index 0a5de196..0e9d8874 100644
--- a/src/test/java/com/gitblit/tests/BranchTicketServiceTest.java
+++ b/src/test/java/com/gitblit/tests/BranchTicketServiceTest.java
@@ -59,7 +59,7 @@ public class BranchTicketServiceTest extends TicketServiceTest {
IUserManager userManager = new UserManager(runtimeManager, pluginManager).start();
IRepositoryManager repositoryManager = new RepositoryManager(runtimeManager, pluginManager, userManager).start();
- BranchTicketService service = new BranchTicketService(
+ BranchTicketService service = (BranchTicketService) new BranchTicketService(
runtimeManager,
pluginManager,
notificationManager,
diff --git a/src/test/java/com/gitblit/tests/DiffUtilsTest.java b/src/test/java/com/gitblit/tests/DiffUtilsTest.java
index e8e839a5..9ba7202f 100644
--- a/src/test/java/com/gitblit/tests/DiffUtilsTest.java
+++ b/src/test/java/com/gitblit/tests/DiffUtilsTest.java
@@ -40,7 +40,7 @@ public class DiffUtilsTest extends GitblitUnitTest {
public void testParentCommitDiff() throws Exception {
Repository repository = GitBlitSuite.getHelloworldRepository();
RevCommit commit = JGitUtils.getCommit(repository,
- "1d0c2933a4ae69c362f76797d42d6bd182d05176");
+ GitBlitSuite.helloworldSettings.getRequiredString(HelloworldKeys.commit.second));
String diff = DiffUtils.getCommitDiff(repository, commit, DiffComparator.SHOW_WHITESPACE, DiffOutputType.PLAIN, 3).content;
repository.close();
assertTrue(diff != null && diff.length() > 0);
@@ -52,9 +52,9 @@ public class DiffUtilsTest extends GitblitUnitTest {
public void testArbitraryCommitDiff() throws Exception {
Repository repository = GitBlitSuite.getHelloworldRepository();
RevCommit baseCommit = JGitUtils.getCommit(repository,
- "8baf6a833b5579384d9b9ceb8a16b5d0ea2ec4ca");
+ GitBlitSuite.helloworldSettings.getRequiredString(HelloworldKeys.commit.first));
RevCommit commit = JGitUtils.getCommit(repository,
- "1d0c2933a4ae69c362f76797d42d6bd182d05176");
+ GitBlitSuite.helloworldSettings.getRequiredString(HelloworldKeys.commit.second));
String diff = DiffUtils.getDiff(repository, baseCommit, commit, DiffComparator.SHOW_WHITESPACE, DiffOutputType.PLAIN, 3).content;
repository.close();
assertTrue(diff != null && diff.length() > 0);
@@ -66,7 +66,7 @@ public class DiffUtilsTest extends GitblitUnitTest {
public void testPlainFileDiff() throws Exception {
Repository repository = GitBlitSuite.getHelloworldRepository();
RevCommit commit = JGitUtils.getCommit(repository,
- "1d0c2933a4ae69c362f76797d42d6bd182d05176");
+ GitBlitSuite.helloworldSettings.getRequiredString(HelloworldKeys.commit.second));
String diff = DiffUtils.getDiff(repository, commit, "java.java", DiffComparator.SHOW_WHITESPACE, DiffOutputType.PLAIN, 3).content;
repository.close();
assertTrue(diff != null && diff.length() > 0);
@@ -78,7 +78,7 @@ public class DiffUtilsTest extends GitblitUnitTest {
public void testFilePatch() throws Exception {
Repository repository = GitBlitSuite.getHelloworldRepository();
RevCommit commit = JGitUtils.getCommit(repository,
- "1d0c2933a4ae69c362f76797d42d6bd182d05176");
+ GitBlitSuite.helloworldSettings.getRequiredString(HelloworldKeys.commit.second));
String patch = DiffUtils.getCommitPatch(repository, null, commit, "java.java");
repository.close();
assertTrue(patch != null && patch.length() > 0);
@@ -90,9 +90,9 @@ public class DiffUtilsTest extends GitblitUnitTest {
public void testArbitraryFilePatch() throws Exception {
Repository repository = GitBlitSuite.getHelloworldRepository();
RevCommit baseCommit = JGitUtils.getCommit(repository,
- "8baf6a833b5579384d9b9ceb8a16b5d0ea2ec4ca");
+ GitBlitSuite.helloworldSettings.getRequiredString(HelloworldKeys.commit.first));
RevCommit commit = JGitUtils.getCommit(repository,
- "1d0c2933a4ae69c362f76797d42d6bd182d05176");
+ GitBlitSuite.helloworldSettings.getRequiredString(HelloworldKeys.commit.second));
String patch = DiffUtils.getCommitPatch(repository, baseCommit, commit, "java.java");
repository.close();
assertTrue(patch != null && patch.length() > 0);
@@ -104,9 +104,9 @@ public class DiffUtilsTest extends GitblitUnitTest {
public void testArbitraryCommitPatch() throws Exception {
Repository repository = GitBlitSuite.getHelloworldRepository();
RevCommit baseCommit = JGitUtils.getCommit(repository,
- "8baf6a833b5579384d9b9ceb8a16b5d0ea2ec4ca");
+ GitBlitSuite.helloworldSettings.getRequiredString(HelloworldKeys.commit.first));
RevCommit commit = JGitUtils.getCommit(repository,
- "1d0c2933a4ae69c362f76797d42d6bd182d05176");
+ GitBlitSuite.helloworldSettings.getRequiredString(HelloworldKeys.commit.second));
String patch = DiffUtils.getCommitPatch(repository, baseCommit, commit, null);
repository.close();
assertTrue(patch != null && patch.length() > 0);
@@ -118,9 +118,9 @@ public class DiffUtilsTest extends GitblitUnitTest {
public void testBlame() throws Exception {
Repository repository = GitBlitSuite.getHelloworldRepository();
List<AnnotatedLine> lines = DiffUtils.blame(repository, "java.java",
- "1d0c2933a4ae69c362f76797d42d6bd182d05176");
+ GitBlitSuite.helloworldSettings.getRequiredString(HelloworldKeys.commit.second));
repository.close();
assertTrue(lines.size() > 0);
- assertEquals("c6d31dccf5cc75e8e46299fc62d38f60ec6d41e0", lines.get(0).commitId);
+ assertEquals(GitBlitSuite.helloworldSettings.getRequiredString(HelloworldKeys.commit.first), lines.get(0).commitId);
}
}
diff --git a/src/test/java/com/gitblit/tests/FederationTests.java b/src/test/java/com/gitblit/tests/FederationTests.java
index 144fe3da..36ea56c3 100644
--- a/src/test/java/com/gitblit/tests/FederationTests.java
+++ b/src/test/java/com/gitblit/tests/FederationTests.java
@@ -15,6 +15,7 @@
*/
package com.gitblit.tests;
+import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
@@ -45,6 +46,12 @@ public class FederationTests extends GitblitUnitTest {
String password = GitBlitSuite.password;
String token = "d7cc58921a80b37e0329a4dae2f9af38bf61ef5c";
+ //test data
+ static final String testUser = "test";
+ static final String testUserPwd = "whocares";
+ static final String testTeam = "testteam";
+ static final String testTeamRepository = "helloworld.git";
+
private static final AtomicBoolean started = new AtomicBoolean(false);
@BeforeClass
@@ -54,11 +61,27 @@ public class FederationTests extends GitblitUnitTest {
@AfterClass
public static void stopGitblit() throws Exception {
+ //clean up test user and team if left over
+ deleteTestUser();
+ deleteTestTeam();
+
if (started.get()) {
GitBlitSuite.stopGitblit();
}
}
+ private static void deleteTestUser() throws IOException {
+ UserModel user = new UserModel(testUser);
+ user.password = testUserPwd;
+ RpcUtils.deleteUser(user, GitBlitSuite.url, GitBlitSuite.account, GitBlitSuite.password.toCharArray());
+ }
+
+ private static void deleteTestTeam() throws IOException {
+ TeamModel team = new TeamModel(testTeam);
+ team.addRepositoryPermission(testTeamRepository);
+ RpcUtils.deleteTeam(team, GitBlitSuite.url, GitBlitSuite.account, GitBlitSuite.password.toCharArray());
+ }
+
@Test
public void testProposal() throws Exception {
// create dummy repository data
@@ -121,18 +144,22 @@ public class FederationTests extends GitblitUnitTest {
@Test
public void testPullUsers() throws Exception {
+ //clean up test user and team left over from previous run, if any
+ deleteTestUser();
+ deleteTestTeam();
+
List<UserModel> users = FederationUtils.getUsers(getRegistration());
assertNotNull(users);
- // admin is excluded
- assertEquals(0, users.size());
+ // admin is excluded, hence there should be no other users in the list
+ assertEquals("Gitblit server still contains " + users + " user account(s).", 0, users.size());
- UserModel newUser = new UserModel("test");
- newUser.password = "whocares";
+ UserModel newUser = new UserModel(testUser);
+ newUser.password = testUserPwd;
assertTrue(RpcUtils.createUser(newUser, url, account, password.toCharArray()));
- TeamModel team = new TeamModel("testteam");
- team.addUser("test");
- team.addRepositoryPermission("helloworld.git");
+ TeamModel team = new TeamModel(testTeam);
+ team.addUser(testUser);
+ team.addRepositoryPermission(testTeamRepository);
assertTrue(RpcUtils.createTeam(team, url, account, password.toCharArray()));
users = FederationUtils.getUsers(getRegistration());
@@ -140,7 +167,7 @@ public class FederationTests extends GitblitUnitTest {
assertEquals(1, users.size());
newUser = users.get(0);
- assertTrue(newUser.isTeamMember("testteam"));
+ assertTrue(newUser.isTeamMember(testTeam));
assertTrue(RpcUtils.deleteUser(newUser, url, account, password.toCharArray()));
assertTrue(RpcUtils.deleteTeam(team, url, account, password.toCharArray()));
@@ -148,9 +175,12 @@ public class FederationTests extends GitblitUnitTest {
@Test
public void testPullTeams() throws Exception {
- TeamModel team = new TeamModel("testteam");
- team.addUser("test");
- team.addRepositoryPermission("helloworld.git");
+ //clean up test team left over from previous run, if any
+ deleteTestTeam();
+
+ TeamModel team = new TeamModel(testTeam);
+ team.addUser(testUser);
+ team.addRepositoryPermission(testTeamRepository);
assertTrue(RpcUtils.createTeam(team, url, account, password.toCharArray()));
List<TeamModel> teams = FederationUtils.getTeams(getRegistration());
diff --git a/src/test/java/com/gitblit/tests/FileTicketServiceTest.java b/src/test/java/com/gitblit/tests/FileTicketServiceTest.java
index 1fb2eed9..c4a63c41 100644
--- a/src/test/java/com/gitblit/tests/FileTicketServiceTest.java
+++ b/src/test/java/com/gitblit/tests/FileTicketServiceTest.java
@@ -58,7 +58,7 @@ public class FileTicketServiceTest extends TicketServiceTest {
IUserManager userManager = new UserManager(runtimeManager, pluginManager).start();
IRepositoryManager repositoryManager = new RepositoryManager(runtimeManager, pluginManager, userManager).start();
- FileTicketService service = new FileTicketService(
+ FileTicketService service = (FileTicketService) new FileTicketService(
runtimeManager,
pluginManager,
notificationManager,
diff --git a/src/test/java/com/gitblit/tests/GitBlitSuite.java b/src/test/java/com/gitblit/tests/GitBlitSuite.java
index 133be77f..fbae039c 100644
--- a/src/test/java/com/gitblit/tests/GitBlitSuite.java
+++ b/src/test/java/com/gitblit/tests/GitBlitSuite.java
@@ -16,23 +16,31 @@
package com.gitblit.tests;
import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
import java.lang.reflect.Field;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+import com.gitblit.utils.TimeUtilsTest;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.RepositoryCache;
import org.eclipse.jgit.lib.RepositoryCache.FileKey;
import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
import org.eclipse.jgit.util.FS;
+import org.eclipse.jgit.util.FileUtils;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
import org.junit.runners.Suite.SuiteClasses;
+import com.gitblit.FileSettings;
import com.gitblit.GitBlitException;
import com.gitblit.GitBlitServer;
import com.gitblit.manager.IRepositoryManager;
@@ -77,6 +85,18 @@ public class GitBlitSuite {
public static final File USERSCONF = new File("src/test/config/test-users.conf");
+ private static final File AMBITION_REPO_SOURCE = new File("src/test/data/ambition.git");
+
+ private static final File TICGIT_REPO_SOURCE = new File("src/test/data/ticgit.git");
+
+ private static final File GITECTIVE_REPO_SOURCE = new File("src/test/data/gitective.git");
+
+ private static final File HELLOWORLD_REPO_SOURCE = new File("src/test/data/hello-world.git");
+
+ private static final File HELLOWORLD_REPO_PROPERTIES = new File("src/test/data/hello-world.properties");
+
+ public static final FileSettings helloworldSettings = new FileSettings(HELLOWORLD_REPO_PROPERTIES.getAbsolutePath());
+
static int port = 8280;
static int gitPort = 8300;
static int shutdownPort = 8281;
@@ -167,17 +187,39 @@ public class GitBlitSuite {
Thread.sleep(5000);
}
+ public static void deleteRefChecksFolder() throws IOException {
+ File refChecks = new File(GitBlitSuite.REPOSITORIES, "refchecks");
+ if (refChecks.exists()) {
+ FileUtils.delete(refChecks, FileUtils.RECURSIVE | FileUtils.RETRY);
+ }
+ }
+
@BeforeClass
public static void setUp() throws Exception {
+ //"refchecks" folder is used in GitServletTest;
+ //need be deleted before Gitblit server instance is started
+ deleteRefChecksFolder();
startGitblit();
if (REPOSITORIES.exists() || REPOSITORIES.mkdirs()) {
- cloneOrFetch("helloworld.git", "https://github.com/git/hello-world.git");
- cloneOrFetch("ticgit.git", "https://github.com/schacon/ticgit.git");
+ if (!HELLOWORLD_REPO_SOURCE.exists()) {
+ unzipRepository(HELLOWORLD_REPO_SOURCE.getPath() + ".zip", HELLOWORLD_REPO_SOURCE.getParentFile());
+ }
+ if (!TICGIT_REPO_SOURCE.exists()) {
+ unzipRepository(TICGIT_REPO_SOURCE.getPath() + ".zip", TICGIT_REPO_SOURCE.getParentFile());
+ }
+ if (!AMBITION_REPO_SOURCE.exists()) {
+ unzipRepository(AMBITION_REPO_SOURCE.getPath() + ".zip", AMBITION_REPO_SOURCE.getParentFile());
+ }
+ if (!GITECTIVE_REPO_SOURCE.exists()) {
+ unzipRepository(GITECTIVE_REPO_SOURCE.getPath() + ".zip", GITECTIVE_REPO_SOURCE.getParentFile());
+ }
+ cloneOrFetch("helloworld.git", HELLOWORLD_REPO_SOURCE.getAbsolutePath());
+ cloneOrFetch("ticgit.git", TICGIT_REPO_SOURCE.getAbsolutePath());
cloneOrFetch("test/jgit.git", "https://github.com/eclipse/jgit.git");
- cloneOrFetch("test/helloworld.git", "https://github.com/git/hello-world.git");
- cloneOrFetch("test/ambition.git", "https://github.com/defunkt/ambition.git");
- cloneOrFetch("test/gitective.git", "https://github.com/kevinsawicki/gitective.git");
+ cloneOrFetch("test/helloworld.git", HELLOWORLD_REPO_SOURCE.getAbsolutePath());
+ cloneOrFetch("test/ambition.git", AMBITION_REPO_SOURCE.getAbsolutePath());
+ cloneOrFetch("test/gitective.git", GITECTIVE_REPO_SOURCE.getAbsolutePath());
showRemoteBranches("ticgit.git");
automaticallyTagBranchTips("ticgit.git");
@@ -254,4 +296,47 @@ public class GitBlitSuite {
r.close();
}
}
+
+ private static void unzipRepository(String zippedRepo, File destDir) throws IOException {
+ System.out.print("Unzipping " + zippedRepo + "... ");
+ if (!destDir.exists()) {
+ destDir.mkdir();
+ }
+ byte[] buffer = new byte[1024];
+ ZipInputStream zis = new ZipInputStream(new FileInputStream(zippedRepo));
+ ZipEntry zipEntry = zis.getNextEntry();
+ while (zipEntry != null) {
+ File newFile = newFile(destDir, zipEntry);
+ if (zipEntry.isDirectory()) {
+ newFile.mkdirs();
+ }
+ else {
+ FileOutputStream fos = new FileOutputStream(newFile);
+ int len;
+ while ((len = zis.read(buffer)) > 0) {
+ fos.write(buffer, 0, len);
+ }
+ fos.close();
+ }
+ zipEntry = zis.getNextEntry();
+ }
+ zis.closeEntry();
+ zis.close();
+ System.out.println("done.");
+ }
+
+ private static File newFile(File destinationDir, ZipEntry zipEntry) throws IOException {
+ File destFile = new File(destinationDir, zipEntry.getName());
+
+ String destDirPath = destinationDir.getCanonicalPath();
+ String destFilePath = destFile.getCanonicalPath();
+ //guards against writing files to the file system outside of the target folder
+ //to prevent Zip Slip exploit
+ if (!destFilePath.startsWith(destDirPath + File.separator)) {
+ throw new IOException("Entry is outside of the target dir: " + zipEntry.getName());
+ }
+
+ return destFile;
+ }
+
}
diff --git a/src/test/java/com/gitblit/tests/GitServletTest.java b/src/test/java/com/gitblit/tests/GitServletTest.java
index 705684a3..a816143d 100644
--- a/src/test/java/com/gitblit/tests/GitServletTest.java
+++ b/src/test/java/com/gitblit/tests/GitServletTest.java
@@ -25,6 +25,11 @@ import java.util.Date;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
+import org.apache.commons.io.IOUtils;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.HttpClientBuilder;
import org.eclipse.jgit.api.CloneCommand;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.MergeCommand.FastForwardMode;
@@ -89,6 +94,9 @@ public class GitServletTest extends GitblitUnitTest {
@BeforeClass
public static void startGitblit() throws Exception {
+ //"refchecks" folder is used in this test class;
+ //need be deleted before Gitblit server instance is started
+ GitBlitSuite.deleteRefChecksFolder();
started.set(GitBlitSuite.startGitblit());
delete(getUser());
@@ -107,19 +115,19 @@ public class GitServletTest extends GitblitUnitTest {
public static void deleteWorkingFolders() throws Exception {
if (ticgitFolder.exists()) {
GitBlitSuite.close(ticgitFolder);
- FileUtils.delete(ticgitFolder, FileUtils.RECURSIVE);
+ FileUtils.delete(ticgitFolder, FileUtils.RECURSIVE | FileUtils.RETRY);
}
if (ticgit2Folder.exists()) {
GitBlitSuite.close(ticgit2Folder);
- FileUtils.delete(ticgit2Folder, FileUtils.RECURSIVE);
+ FileUtils.delete(ticgit2Folder, FileUtils.RECURSIVE | FileUtils.RETRY);
}
if (jgitFolder.exists()) {
GitBlitSuite.close(jgitFolder);
- FileUtils.delete(jgitFolder, FileUtils.RECURSIVE);
+ FileUtils.delete(jgitFolder, FileUtils.RECURSIVE | FileUtils.RETRY);
}
if (jgit2Folder.exists()) {
GitBlitSuite.close(jgit2Folder);
- FileUtils.delete(jgit2Folder, FileUtils.RECURSIVE);
+ FileUtils.delete(jgit2Folder, FileUtils.RECURSIVE | FileUtils.RETRY);
}
}
@@ -400,7 +408,7 @@ public class GitServletTest extends GitblitUnitTest {
// fork from original to a temporary bare repo
File verification = new File(GitBlitSuite.REPOSITORIES, "refchecks/verify-committer.git");
if (verification.exists()) {
- FileUtils.delete(verification, FileUtils.RECURSIVE);
+ FileUtils.delete(verification, FileUtils.RECURSIVE | FileUtils.RETRY);
}
CloneCommand clone = Git.cloneRepository();
clone.setURI(MessageFormat.format("{0}/ticgit.git", url));
@@ -485,7 +493,7 @@ public class GitServletTest extends GitblitUnitTest {
// fork from original to a temporary bare repo
File verification = new File(GitBlitSuite.REPOSITORIES, "refchecks/verify-committer.git");
if (verification.exists()) {
- FileUtils.delete(verification, FileUtils.RECURSIVE);
+ FileUtils.delete(verification, FileUtils.RECURSIVE | FileUtils.RETRY);
}
CloneCommand clone = Git.cloneRepository();
clone.setURI(MessageFormat.format("{0}/ticgit.git", url));
@@ -630,7 +638,7 @@ public class GitServletTest extends GitblitUnitTest {
// fork from original to a temporary bare repo
File refChecks = new File(GitBlitSuite.REPOSITORIES, forkName);
if (refChecks.exists()) {
- FileUtils.delete(refChecks, FileUtils.RECURSIVE);
+ FileUtils.delete(refChecks, FileUtils.RECURSIVE | FileUtils.RETRY);
}
CloneCommand clone = Git.cloneRepository();
clone.setURI(url + "/" + originName);
@@ -663,7 +671,7 @@ public class GitServletTest extends GitblitUnitTest {
// clone temp bare repo to working copy
File local = new File(GitBlitSuite.REPOSITORIES, workingCopy);
if (local.exists()) {
- FileUtils.delete(local, FileUtils.RECURSIVE);
+ FileUtils.delete(local, FileUtils.RECURSIVE | FileUtils.RETRY);
}
clone = Git.cloneRepository();
clone.setURI(MessageFormat.format("{0}/{1}", url, model.name));
@@ -925,4 +933,65 @@ public class GitServletTest extends GitblitUnitTest {
GitBlitSuite.close(repository);
assertTrue("Repository has an empty push log!", pushes.size() > 0);
}
+
+
+
+ @Test
+ public void testInvalidURLNoRepoName() throws IOException {
+ final String testURL = GitBlitSuite.gitServletUrl + "/?service=git-upload-pack";
+
+ HttpClient client = HttpClientBuilder.create().build();
+ HttpGet request = new HttpGet(testURL);
+
+ HttpResponse response = client.execute(request);
+ assertEquals("Expected BAD REQUEST due to missing repository string", 400, response.getStatusLine().getStatusCode());
+ }
+
+ @Test
+ public void testInvalidURLNoRepoName2() throws IOException {
+ final String testURL = GitBlitSuite.gitServletUrl + "//info/refs";
+
+ HttpClient client = HttpClientBuilder.create().build();
+ HttpGet request = new HttpGet(testURL);
+
+ HttpResponse response = client.execute(request);
+ assertEquals("Expected BAD REQUEST due to missing repository string", 400, response.getStatusLine().getStatusCode());
+ }
+
+
+ @Test
+ public void testURLUnknownRepo() throws IOException {
+ final String testURL = GitBlitSuite.url + "/r/foobar.git/info/refs";
+
+ HttpClient client = HttpClientBuilder.create().build();
+ HttpGet request = new HttpGet(testURL);
+
+ HttpResponse response = client.execute(request);
+ assertEquals(401, response.getStatusLine().getStatusCode());
+ }
+
+ @Test
+ public void testURLUnknownAction() throws IOException {
+ final String testURL = GitBlitSuite.gitServletUrl + "/helloworld.git/something/unknown";
+
+ HttpClient client = HttpClientBuilder.create().build();
+ HttpGet request = new HttpGet(testURL);
+
+ HttpResponse response = client.execute(request);
+ assertEquals(400, response.getStatusLine().getStatusCode());
+ }
+
+ @Test
+ public void testInvalidURLCloneBundle() throws IOException {
+ final String testURL = GitBlitSuite.gitServletUrl + "/helloworld.git/clone.bundle";
+
+ HttpClient client = HttpClientBuilder.create().build();
+ HttpGet request = new HttpGet(testURL);
+
+ HttpResponse response = client.execute(request);
+ assertEquals(501, response.getStatusLine().getStatusCode());
+ String content = IOUtils.toString(response.getEntity().getContent(), "UTF-8");
+ assertNotNull(content);
+ }
+
}
diff --git a/src/test/java/com/gitblit/tests/GroovyScriptTest.java b/src/test/java/com/gitblit/tests/GroovyScriptTest.java
index ff40972f..81c6c23a 100644
--- a/src/test/java/com/gitblit/tests/GroovyScriptTest.java
+++ b/src/test/java/com/gitblit/tests/GroovyScriptTest.java
@@ -74,11 +74,11 @@ public class GroovyScriptTest extends GitblitUnitTest {
MockClientLogger clientLogger = new MockClientLogger();
List<ReceiveCommand> commands = new ArrayList<ReceiveCommand>();
commands.add(new ReceiveCommand(ObjectId
- .fromString("c18877690322dfc6ae3e37bb7f7085a24e94e887"), ObjectId
- .fromString("3fa7c46d11b11d61f1cbadc6888be5d0eae21969"), "refs/heads/master"));
+ .fromString(GitBlitSuite.helloworldSettings.getRequiredString(HelloworldKeys.commit.fifth)), ObjectId
+ .fromString(GitBlitSuite.helloworldSettings.getRequiredString(HelloworldKeys.commit.deleted)), "refs/heads/master"));
commands.add(new ReceiveCommand(ObjectId
- .fromString("c18877690322dfc6ae3e37bb7f7085a24e94e887"), ObjectId
- .fromString("3fa7c46d11b11d61f1cbadc6888be5d0eae21969"), "refs/heads/master2"));
+ .fromString(GitBlitSuite.helloworldSettings.getRequiredString(HelloworldKeys.commit.fifth)), ObjectId
+ .fromString(GitBlitSuite.helloworldSettings.getRequiredString(HelloworldKeys.commit.deleted)), "refs/heads/master2"));
RepositoryModel repository = repositories().getRepositoryModel("helloworld.git");
repository.customFields = new HashMap<String,String>();
@@ -96,11 +96,11 @@ public class GroovyScriptTest extends GitblitUnitTest {
MockClientLogger clientLogger = new MockClientLogger();
List<ReceiveCommand> commands = new ArrayList<ReceiveCommand>();
commands.add(new ReceiveCommand(ObjectId
- .fromString("c18877690322dfc6ae3e37bb7f7085a24e94e887"), ObjectId
- .fromString("3fa7c46d11b11d61f1cbadc6888be5d0eae21969"), "refs/heads/master"));
+ .fromString(GitBlitSuite.helloworldSettings.getRequiredString(HelloworldKeys.commit.fifth)), ObjectId
+ .fromString(GitBlitSuite.helloworldSettings.getRequiredString(HelloworldKeys.commit.deleted)), "refs/heads/master"));
commands.add(new ReceiveCommand(ObjectId
- .fromString("c18877690322dfc6ae3e37bb7f7085a24e94e887"), ObjectId
- .fromString("3fa7c46d11b11d61f1cbadc6888be5d0eae21969"), "refs/heads/master2"));
+ .fromString(GitBlitSuite.helloworldSettings.getRequiredString(HelloworldKeys.commit.fifth)), ObjectId
+ .fromString(GitBlitSuite.helloworldSettings.getRequiredString(HelloworldKeys.commit.deleted)), "refs/heads/master2"));
RepositoryModel repository = repositories().getRepositoryModel("helloworld.git");
repository.mailingLists.add("list@helloworld.git");
@@ -121,11 +121,11 @@ public class GroovyScriptTest extends GitblitUnitTest {
MockClientLogger clientLogger = new MockClientLogger();
List<ReceiveCommand> commands = new ArrayList<ReceiveCommand>();
commands.add(new ReceiveCommand(ObjectId
- .fromString("c18877690322dfc6ae3e37bb7f7085a24e94e887"), ObjectId
- .fromString("3fa7c46d11b11d61f1cbadc6888be5d0eae21969"), "refs/heads/master"));
+ .fromString(GitBlitSuite.helloworldSettings.getRequiredString(HelloworldKeys.commit.fifth)), ObjectId
+ .fromString(GitBlitSuite.helloworldSettings.getRequiredString(HelloworldKeys.commit.deleted)), "refs/heads/master"));
commands.add(new ReceiveCommand(ObjectId
- .fromString("c18877690322dfc6ae3e37bb7f7085a24e94e887"), ObjectId
- .fromString("3fa7c46d11b11d61f1cbadc6888be5d0eae21969"), "refs/heads/master2"));
+ .fromString(GitBlitSuite.helloworldSettings.getRequiredString(HelloworldKeys.commit.fifth)), ObjectId
+ .fromString(GitBlitSuite.helloworldSettings.getRequiredString(HelloworldKeys.commit.deleted)), "refs/heads/master2"));
RepositoryModel repository = repositories().getRepositoryModel("helloworld.git");
repository.mailingLists.add("list@helloworld.git");
@@ -145,7 +145,7 @@ public class GroovyScriptTest extends GitblitUnitTest {
MockClientLogger clientLogger = new MockClientLogger();
List<ReceiveCommand> commands = new ArrayList<ReceiveCommand>();
commands.add(new ReceiveCommand(ObjectId.zeroId(), ObjectId
- .fromString("3fa7c46d11b11d61f1cbadc6888be5d0eae21969"), "refs/heads/master"));
+ .fromString(GitBlitSuite.helloworldSettings.getRequiredString(HelloworldKeys.commit.deleted)), "refs/heads/master"));
RepositoryModel repository = new RepositoryModel("ex@mple.git", "", "admin", new Date());
@@ -159,7 +159,7 @@ public class GroovyScriptTest extends GitblitUnitTest {
MockClientLogger clientLogger = new MockClientLogger();
List<ReceiveCommand> commands = new ArrayList<ReceiveCommand>();
commands.add(new ReceiveCommand(ObjectId.zeroId(), ObjectId
- .fromString("3fa7c46d11b11d61f1cbadc6888be5d0eae21969"), "refs/tags/v1.0"));
+ .fromString(GitBlitSuite.helloworldSettings.getRequiredString(HelloworldKeys.commit.deleted)), "refs/tags/v1.0"));
RepositoryModel repository = new RepositoryModel("ex@mple.git", "", "admin", new Date());
@@ -174,8 +174,8 @@ public class GroovyScriptTest extends GitblitUnitTest {
MockClientLogger clientLogger = new MockClientLogger();
List<ReceiveCommand> commands = new ArrayList<ReceiveCommand>();
commands.add(new ReceiveCommand(ObjectId
- .fromString("c18877690322dfc6ae3e37bb7f7085a24e94e887"), ObjectId
- .fromString("3fa7c46d11b11d61f1cbadc6888be5d0eae21969"), "refs/heads/master"));
+ .fromString(GitBlitSuite.helloworldSettings.getRequiredString(HelloworldKeys.commit.fifth)), ObjectId
+ .fromString(GitBlitSuite.helloworldSettings.getRequiredString(HelloworldKeys.commit.deleted)), "refs/heads/master"));
RepositoryModel repository = new RepositoryModel("ex@mple.git", "", "admin", new Date());
@@ -190,7 +190,7 @@ public class GroovyScriptTest extends GitblitUnitTest {
MockClientLogger clientLogger = new MockClientLogger();
List<ReceiveCommand> commands = new ArrayList<ReceiveCommand>();
ReceiveCommand command = new ReceiveCommand(ObjectId
- .fromString("3fa7c46d11b11d61f1cbadc6888be5d0eae21969"), ObjectId.zeroId(),
+ .fromString(GitBlitSuite.helloworldSettings.getRequiredString(HelloworldKeys.commit.deleted)), ObjectId.zeroId(),
"refs/heads/master");
commands.add(command);
@@ -208,7 +208,7 @@ public class GroovyScriptTest extends GitblitUnitTest {
MockClientLogger clientLogger = new MockClientLogger();
List<ReceiveCommand> commands = new ArrayList<ReceiveCommand>();
commands.add(new ReceiveCommand(ObjectId
- .fromString("3fa7c46d11b11d61f1cbadc6888be5d0eae21969"), ObjectId.zeroId(),
+ .fromString(GitBlitSuite.helloworldSettings.getRequiredString(HelloworldKeys.commit.deleted)), ObjectId.zeroId(),
"refs/heads/other"));
RepositoryModel repository = new RepositoryModel("ex@mple.git", "", "admin", new Date());
@@ -224,7 +224,7 @@ public class GroovyScriptTest extends GitblitUnitTest {
MockClientLogger clientLogger = new MockClientLogger();
List<ReceiveCommand> commands = new ArrayList<ReceiveCommand>();
ReceiveCommand command = new ReceiveCommand(ObjectId
- .fromString("3fa7c46d11b11d61f1cbadc6888be5d0eae21969"), ObjectId.zeroId(),
+ .fromString(GitBlitSuite.helloworldSettings.getRequiredString(HelloworldKeys.commit.deleted)), ObjectId.zeroId(),
"refs/tags/v1.0");
commands.add(command);
@@ -242,8 +242,8 @@ public class GroovyScriptTest extends GitblitUnitTest {
MockClientLogger clientLogger = new MockClientLogger();
List<ReceiveCommand> commands = new ArrayList<ReceiveCommand>();
commands.add(new ReceiveCommand(ObjectId
- .fromString("c18877690322dfc6ae3e37bb7f7085a24e94e887"), ObjectId
- .fromString("3fa7c46d11b11d61f1cbadc6888be5d0eae21969"), "refs/heads/master"));
+ .fromString(GitBlitSuite.helloworldSettings.getRequiredString(HelloworldKeys.commit.fifth)), ObjectId
+ .fromString(GitBlitSuite.helloworldSettings.getRequiredString(HelloworldKeys.commit.deleted)), "refs/heads/master"));
RepositoryModel repository = new RepositoryModel("ex@mple.git", "", "admin", new Date());
@@ -262,8 +262,8 @@ public class GroovyScriptTest extends GitblitUnitTest {
MockClientLogger clientLogger = new MockClientLogger();
List<ReceiveCommand> commands = new ArrayList<ReceiveCommand>();
commands.add(new ReceiveCommand(ObjectId
- .fromString("c18877690322dfc6ae3e37bb7f7085a24e94e887"), ObjectId
- .fromString("3fa7c46d11b11d61f1cbadc6888be5d0eae21969"), "refs/heads/master"));
+ .fromString(GitBlitSuite.helloworldSettings.getRequiredString(HelloworldKeys.commit.fifth)), ObjectId
+ .fromString(GitBlitSuite.helloworldSettings.getRequiredString(HelloworldKeys.commit.deleted)), "refs/heads/master"));
RepositoryModel repository = new RepositoryModel("ex@mple.git", "", "admin", new Date());
diff --git a/src/test/java/com/gitblit/tests/HtpasswdAuthenticationTest.java b/src/test/java/com/gitblit/tests/HtpasswdAuthenticationTest.java
index 26a49b24..40379f7d 100644
--- a/src/test/java/com/gitblit/tests/HtpasswdAuthenticationTest.java
+++ b/src/test/java/com/gitblit/tests/HtpasswdAuthenticationTest.java
@@ -200,11 +200,11 @@ public class HtpasswdAuthenticationTest extends GitblitUnitTest {
public void testAuthenticationManager()
{
MS.put(KEY_SUPPORT_PLAINTEXT_PWD, "true");
- UserModel user = auth.authenticate("user1", "pass1".toCharArray(), null);
+ UserModel user = auth.authenticate("user1", "#externalAccount".toCharArray(), null);
assertNotNull(user);
assertEquals("user1", user.username);
- user = auth.authenticate("user2", "pass2".toCharArray(), null);
+ user = auth.authenticate("user2", "#externalAccount".toCharArray(), null);
assertNotNull(user);
assertEquals("user2", user.username);
diff --git a/src/test/java/com/gitblit/tests/JGitUtilsTest.java b/src/test/java/com/gitblit/tests/JGitUtilsTest.java
index c273e860..1d9150a1 100644
--- a/src/test/java/com/gitblit/tests/JGitUtilsTest.java
+++ b/src/test/java/com/gitblit/tests/JGitUtilsTest.java
@@ -17,6 +17,10 @@ package com.gitblit.tests;
import java.io.File;
import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
@@ -51,6 +55,8 @@ import com.gitblit.utils.JGitUtils;
import com.gitblit.utils.JnaUtils;
import com.gitblit.utils.StringUtils;
+import static org.junit.Assume.assumeTrue;
+
public class JGitUtilsTest extends GitblitUnitTest {
@Test
@@ -75,6 +81,117 @@ public class JGitUtilsTest extends GitblitUnitTest {
}
@Test
+ public void testFindRepositoriesSymLinked() {
+
+ String reposdirName = "gitrepos";
+ String repositoryName = "test-linked.git";
+ File extrepodir = new File(GitBlitSuite.REPOSITORIES.getParent(), reposdirName);
+ Path symlink = null;
+ Path alink = null;
+ try {
+ Path link = Paths.get( GitBlitSuite.REPOSITORIES.toString(), "test-rln.git");
+ Path target = Paths.get("../" + reposdirName,repositoryName);
+ symlink = Files.createSymbolicLink(link, target);
+
+ link = Paths.get( GitBlitSuite.REPOSITORIES.toString(), "test-ln.git");
+ target = Paths.get(extrepodir.getCanonicalPath(), repositoryName);
+ alink = Files.createSymbolicLink(link, target);
+ }
+ catch (UnsupportedOperationException e) {
+ assumeTrue("No symbolic links supported.", false);
+ }
+ catch (IOException ioe) {
+ try {
+ if (symlink != null) Files.delete(symlink);
+ if (alink != null) Files.delete(alink);
+ }
+ catch (IOException ignored) {}
+ fail(ioe.getMessage());
+ }
+
+
+ Path extDir = null;
+ Repository repository = null;
+
+ String testDirName = "test-linked";
+ String testTestDirName = "test-linked/test";
+ Path testDir = Paths.get(GitBlitSuite.REPOSITORIES.toString(),testDirName);
+ Path testTestDir = Paths.get(GitBlitSuite.REPOSITORIES.toString(),testTestDirName);
+ Path linkTestRepo = null;
+ Path linkTestTestRepo = null;
+ Path linkExtDir = null;
+ try {
+ List<String> list = JGitUtils.getRepositoryList(GitBlitSuite.REPOSITORIES, true, true, -1, null);
+ int preSize = list.size();
+
+ // Create test repo. This will make the link targets exist, so that the number of repos increases by two.
+ extDir = Files.createDirectory(extrepodir.toPath());
+ repository = JGitUtils.createRepository(extrepodir, repositoryName);
+
+ list = JGitUtils.getRepositoryList(GitBlitSuite.REPOSITORIES, true, true, -1, null);
+ assertEquals("No linked repositories found in " + GitBlitSuite.REPOSITORIES, 2, (list.size() - preSize));
+
+ list = JGitUtils.getRepositoryList(GitBlitSuite.REPOSITORIES, true, true, -1, Arrays.asList(".*ln\\.git"));
+ assertEquals("Filtering out linked repos failed.", preSize, list.size());
+
+ // Create subdirectories and place links into them
+ Files.createDirectories(testTestDir);
+
+ Path target = Paths.get(extrepodir.getCanonicalPath(), repositoryName);
+ Path link = Paths.get(testDir.toString(), "test-ln-one.git");
+ linkTestRepo = Files.createSymbolicLink(link, target);
+ link = Paths.get(testTestDir.toString(), "test-ln-two.git");
+ linkTestTestRepo = Files.createSymbolicLink(link, target);
+
+ list = JGitUtils.getRepositoryList(GitBlitSuite.REPOSITORIES, true, true, -1, null);
+ assertEquals("No linked repositories found in subdirectories of " + GitBlitSuite.REPOSITORIES, 4, (list.size() - preSize));
+ assertTrue("Did not find linked repo test-ln-one.git", list.contains(testDirName + "/test-ln-one.git"));
+ assertTrue("Did not find linked repo test-ln-two.git", list.contains(testTestDirName + "/test-ln-two.git"));
+ list = JGitUtils.getRepositoryList(new File(testDir.toString()), true, true, -1, null);
+ assertEquals("No linked repositories found in subdirectories of " + testDir, 2, list.size());
+ assertTrue("Did not find linked repo test-ln-one.git", list.contains("test-ln-one.git"));
+ assertTrue("Did not find linked repo test-ln-two.git", list.contains("test/test-ln-two.git"));
+
+
+ // Create link to external directory with repos
+ target = Paths.get(extrepodir.getCanonicalPath());
+ link = Paths.get(testDir.toString(), "test-linked");
+ linkExtDir = Files.createSymbolicLink(link, target);
+
+ list = JGitUtils.getRepositoryList(GitBlitSuite.REPOSITORIES, true, true, -1, null);
+ assertEquals("No repositories found in linked subdirectories of " + GitBlitSuite.REPOSITORIES, 5, (list.size() - preSize));
+ assertTrue("Did not find repo in linked subfolder.", list.contains(testDirName + "/test-linked/" + repositoryName));
+
+ list = JGitUtils.getRepositoryList(new File(testDir.toString()), true, true, -1, null);
+ assertEquals("No repositories found in linked subdirectories of " + testDir, 3, list.size());
+ assertTrue("Did not find repo in linked subfolder.", list.contains("test-linked/" + repositoryName));
+
+ } catch (IOException e) {
+ fail(e.toString());
+ } finally {
+ try {
+ if (repository != null) {
+ repository.close();
+ RepositoryCache.close(repository);
+ FileUtils.delete(repository.getDirectory(), FileUtils.RECURSIVE);
+ }
+ if (extDir != null) Files.delete(extDir);
+ if (symlink != null) Files.delete(symlink);
+ if (alink != null) Files.delete(alink);
+
+ if (linkExtDir != null) Files.deleteIfExists(linkExtDir);
+ if (linkTestTestRepo != null) Files.deleteIfExists(linkTestTestRepo);
+ if (linkTestRepo != null) Files.deleteIfExists(linkTestRepo);
+
+ Files.deleteIfExists(testTestDir);
+ Files.deleteIfExists(testDir);
+ }
+ catch (IOException ignored) {}
+ }
+
+ }
+
+ @Test
public void testFindExclusions() {
List<String> list = JGitUtils.getRepositoryList(GitBlitSuite.REPOSITORIES, false, true, -1, null);
assertTrue("Missing jgit repository?!", list.contains("test/jgit.git"));
@@ -108,7 +225,8 @@ public class JGitUtilsTest extends GitblitUnitTest {
Date firstChange = JGitUtils.getFirstChange(repository, null);
repository.close();
assertNotNull("Could not get first commit!", commit);
- assertEquals("Incorrect first commit!", "f554664a346629dc2b839f7292d06bad2db4aece",
+ assertEquals("Incorrect first commit!",
+ GitBlitSuite.helloworldSettings.getRequiredString(HelloworldKeys.commit.first),
commit.getName());
assertTrue(firstChange.equals(new Date(commit.getCommitTime() * 1000L)));
}
@@ -442,10 +560,10 @@ public class JGitUtilsTest extends GitblitUnitTest {
public void testFilesInCommit() throws Exception {
Repository repository = GitBlitSuite.getHelloworldRepository();
RevCommit commit = JGitUtils.getCommit(repository,
- "1d0c2933a4ae69c362f76797d42d6bd182d05176");
+ GitBlitSuite.helloworldSettings.getRequiredString(HelloworldKeys.commit.fifteen));
List<PathChangeModel> paths = JGitUtils.getFilesInCommit(repository, commit);
- commit = JGitUtils.getCommit(repository, "af0e9b2891fda85afc119f04a69acf7348922830");
+ commit = JGitUtils.getCommit(repository, GitBlitSuite.helloworldSettings.getRequiredString(HelloworldKeys.commit.deleted));
List<PathChangeModel> deletions = JGitUtils.getFilesInCommit(repository, commit);
commit = JGitUtils.getFirstCommit(repository, null);
@@ -478,10 +596,22 @@ public class JGitUtilsTest extends GitblitUnitTest {
@Test
public void testFilesInPath2() throws Exception {
assertEquals(0, JGitUtils.getFilesInPath2(null, null, null).size());
+
Repository repository = GitBlitSuite.getHelloworldRepository();
+
List<PathModel> files = JGitUtils.getFilesInPath2(repository, null, null);
+ assertEquals(GitBlitSuite.helloworldSettings.getInteger(HelloworldKeys.files.top, 15), files.size());
+
+ files = JGitUtils.getFilesInPath2(repository, "C", null);
+ assertEquals(GitBlitSuite.helloworldSettings.getInteger(HelloworldKeys.files.C.top, 1), files.size());
+
+ files = JGitUtils.getFilesInPath2(repository, "[C++]", null);
+ assertEquals(GitBlitSuite.helloworldSettings.getInteger(HelloworldKeys.files.Cpp, 1), files.size());
+
+ files = JGitUtils.getFilesInPath2(repository, "C/C (K&R)", null);
+ assertEquals(GitBlitSuite.helloworldSettings.getInteger(HelloworldKeys.files.C.KnR, 1), files.size());
+
repository.close();
- assertTrue(files.size() > 10);
}
@Test
@@ -526,10 +656,11 @@ public class JGitUtilsTest extends GitblitUnitTest {
commits = JGitUtils.getRevLog(repository, null, "java.java", 0, 2);
assertEquals(2, commits.size());
- // grab the commits since 2008-07-15
+ // grab the commits since 2019-06-05
commits = JGitUtils.getRevLog(repository, null,
- new SimpleDateFormat("yyyy-MM-dd").parse("2008-07-15"));
- assertEquals(12, commits.size());
+ new SimpleDateFormat("yyyy-MM-dd").parse("2019-06-05"));
+ assertEquals("Wrong number of commits since 2019-06-05.",
+ GitBlitSuite.helloworldSettings.getInteger(HelloworldKeys.commits.since_20190605, -1), commits.size());
repository.close();
}
@@ -537,8 +668,8 @@ public class JGitUtilsTest extends GitblitUnitTest {
public void testRevLogRange() throws Exception {
Repository repository = GitBlitSuite.getHelloworldRepository();
List<RevCommit> commits = JGitUtils.getRevLog(repository,
- "fbd14fa6d1a01d4aefa1fca725792683800fc67e",
- "85a0e4087b8439c0aa6b1f4f9e08c26052ab7e87");
+ GitBlitSuite.helloworldSettings.getRequiredString(HelloworldKeys.commit.second),
+ GitBlitSuite.helloworldSettings.getRequiredString(HelloworldKeys.commit.fifteen));
repository.close();
assertEquals(14, commits.size());
}
diff --git a/src/test/java/com/gitblit/tests/JsonUtilsTest.java b/src/test/java/com/gitblit/tests/JsonUtilsTest.java
index 1cd31b8b..b9cfc7a6 100644
--- a/src/test/java/com/gitblit/tests/JsonUtilsTest.java
+++ b/src/test/java/com/gitblit/tests/JsonUtilsTest.java
@@ -17,7 +17,7 @@ package com.gitblit.tests;
import java.text.SimpleDateFormat;
import java.util.Date;
-import java.util.HashMap;
+import java.util.LinkedHashMap;
import java.util.Map;
import org.junit.Test;
@@ -29,7 +29,8 @@ public class JsonUtilsTest extends GitblitUnitTest {
@Test
public void testSerialization() {
- Map<String, String> map = new HashMap<String, String>();
+ Map<String, String> map = new LinkedHashMap<String, String>();
+ //LinkedHashMap preserves the order of insertion
map.put("a", "alligator");
map.put("b", "bear");
map.put("c", "caterpillar");
@@ -37,7 +38,7 @@ public class JsonUtilsTest extends GitblitUnitTest {
map.put("e", "eagle");
String json = JsonUtils.toJsonString(map);
assertEquals(
- "{\"d\":\"dingo\",\"e\":\"eagle\",\"b\":\"bear\",\"c\":\"caterpillar\",\"a\":\"alligator\"}",
+ "{\"a\":\"alligator\",\"b\":\"bear\",\"c\":\"caterpillar\",\"d\":\"dingo\",\"e\":\"eagle\"}",
json);
Map<String, String> map2 = JsonUtils.fromJsonString(json,
new TypeToken<Map<String, String>>() {
@@ -56,4 +57,4 @@ public class JsonUtilsTest extends GitblitUnitTest {
Date date = new Date();
String name = "myJson";
}
-} \ No newline at end of file
+}
diff --git a/src/test/java/com/gitblit/tests/LdapAuthenticationTest.java b/src/test/java/com/gitblit/tests/LdapAuthenticationTest.java
index 84dd138d..4f79edfb 100644
--- a/src/test/java/com/gitblit/tests/LdapAuthenticationTest.java
+++ b/src/test/java/com/gitblit/tests/LdapAuthenticationTest.java
@@ -16,18 +16,12 @@
*/
package com.gitblit.tests;
-import java.io.File;
-import java.io.FileInputStream;
-import java.util.HashMap;
-import java.util.Map;
+import static org.junit.Assume.*;
-import org.apache.commons.io.FileUtils;
import org.junit.Before;
-import org.junit.BeforeClass;
-import org.junit.Rule;
import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
-
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
import com.gitblit.Constants.AccountType;
import com.gitblit.IStoredSettings;
import com.gitblit.Keys;
@@ -41,9 +35,6 @@ import com.gitblit.models.UserModel;
import com.gitblit.tests.mock.MemorySettings;
import com.gitblit.utils.XssFilter;
import com.gitblit.utils.XssFilter.AllowXssFilter;
-import com.unboundid.ldap.listener.InMemoryDirectoryServer;
-import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
-import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.sdk.SearchResult;
import com.unboundid.ldap.sdk.SearchScope;
import com.unboundid.ldif.LDIFReader;
@@ -55,44 +46,18 @@ import com.unboundid.ldif.LDIFReader;
* @author jcrygier
*
*/
-public class LdapAuthenticationTest extends GitblitUnitTest {
- @Rule
- public TemporaryFolder folder = new TemporaryFolder();
-
- private static final String RESOURCE_DIR = "src/test/resources/ldap/";
-
- private File usersConf;
-
- private LdapAuthProvider ldap;
+@RunWith(Parameterized.class)
+public class LdapAuthenticationTest extends LdapBasedUnitTest {
- static int ldapPort = 1389;
-
- private static InMemoryDirectoryServer ds;
+ private LdapAuthProvider ldap;
private IUserManager userManager;
private AuthenticationManager auth;
- private MemorySettings settings;
-
- @BeforeClass
- public static void createInMemoryLdapServer() throws Exception {
- InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig("dc=MyDomain");
- config.addAdditionalBindCredentials("cn=Directory Manager", "password");
- config.setListenerConfigs(InMemoryListenerConfig.createLDAPConfig("default", ldapPort));
- config.setSchema(null);
-
- ds = new InMemoryDirectoryServer(config);
- ds.startListening();
- }
@Before
- public void init() throws Exception {
- ds.clear();
- ds.importFromLDIF(true, new LDIFReader(new FileInputStream(RESOURCE_DIR + "sampledata.ldif")));
- usersConf = folder.newFile("users.conf");
- FileUtils.copyFile(new File(RESOURCE_DIR + "users.conf"), usersConf);
- settings = getSettings();
+ public void setup() throws Exception {
ldap = newLdapAuthentication(settings);
auth = newAuthenticationManager(settings);
}
@@ -114,35 +79,12 @@ public class LdapAuthenticationTest extends GitblitUnitTest {
return auth;
}
- private MemorySettings getSettings() {
- Map<String, Object> backingMap = new HashMap<String, Object>();
- backingMap.put(Keys.realm.userService, usersConf.getAbsolutePath());
- backingMap.put(Keys.realm.ldap.server, "ldap://localhost:" + ldapPort);
-// backingMap.put(Keys.realm.ldap.domain, "");
- backingMap.put(Keys.realm.ldap.username, "cn=Directory Manager");
- backingMap.put(Keys.realm.ldap.password, "password");
-// backingMap.put(Keys.realm.ldap.backingUserService, "users.conf");
- backingMap.put(Keys.realm.ldap.maintainTeams, "true");
- backingMap.put(Keys.realm.ldap.accountBase, "OU=Users,OU=UserControl,OU=MyOrganization,DC=MyDomain");
- backingMap.put(Keys.realm.ldap.accountPattern, "(&(objectClass=person)(sAMAccountName=${username}))");
- backingMap.put(Keys.realm.ldap.groupBase, "OU=Groups,OU=UserControl,OU=MyOrganization,DC=MyDomain");
- backingMap.put(Keys.realm.ldap.groupMemberPattern, "(&(objectClass=group)(member=${dn}))");
- backingMap.put(Keys.realm.ldap.admins, "UserThree @Git_Admins \"@Git Admins\"");
- backingMap.put(Keys.realm.ldap.displayName, "displayName");
- backingMap.put(Keys.realm.ldap.email, "email");
- backingMap.put(Keys.realm.ldap.uid, "sAMAccountName");
-
- MemorySettings ms = new MemorySettings(backingMap);
- return ms;
- }
-
@Test
public void testAuthenticate() {
UserModel userOneModel = ldap.authenticate("UserOne", "userOnePassword".toCharArray());
assertNotNull(userOneModel);
assertNotNull(userOneModel.getTeam("git_admins"));
assertNotNull(userOneModel.getTeam("git_users"));
- assertTrue(userOneModel.canAdmin);
UserModel userOneModelFailedAuth = ldap.authenticate("UserOne", "userTwoPassword".toCharArray());
assertNull(userOneModelFailedAuth);
@@ -152,13 +94,101 @@ public class LdapAuthenticationTest extends GitblitUnitTest {
assertNotNull(userTwoModel.getTeam("git_users"));
assertNull(userTwoModel.getTeam("git_admins"));
assertNotNull(userTwoModel.getTeam("git admins"));
- assertTrue(userTwoModel.canAdmin);
UserModel userThreeModel = ldap.authenticate("UserThree", "userThreePassword".toCharArray());
assertNotNull(userThreeModel);
assertNotNull(userThreeModel.getTeam("git_users"));
assertNull(userThreeModel.getTeam("git_admins"));
+
+ UserModel userFourModel = ldap.authenticate("UserFour", "userFourPassword".toCharArray());
+ assertNotNull(userFourModel);
+ assertNotNull(userFourModel.getTeam("git_users"));
+ assertNull(userFourModel.getTeam("git_admins"));
+ assertNull(userFourModel.getTeam("git admins"));
+ }
+
+ @Test
+ public void testAdminPropertyTeamsInLdap() {
+ UserModel userOneModel = ldap.authenticate("UserOne", "userOnePassword".toCharArray());
+ assertNotNull(userOneModel);
+ assertNotNull(userOneModel.getTeam("git_admins"));
+ assertNull(userOneModel.getTeam("git admins"));
+ assertNotNull(userOneModel.getTeam("git_users"));
+ assertFalse(userOneModel.canAdmin);
+ assertTrue(userOneModel.canAdmin());
+ assertTrue(userOneModel.getTeam("git_admins").canAdmin);
+ assertFalse(userOneModel.getTeam("git_users").canAdmin);
+
+ UserModel userTwoModel = ldap.authenticate("UserTwo", "userTwoPassword".toCharArray());
+ assertNotNull(userTwoModel);
+ assertNotNull(userTwoModel.getTeam("git_users"));
+ assertNull(userTwoModel.getTeam("git_admins"));
+ assertNotNull(userTwoModel.getTeam("git admins"));
+ assertFalse(userTwoModel.canAdmin);
+ assertTrue(userTwoModel.canAdmin());
+ assertTrue(userTwoModel.getTeam("git admins").canAdmin);
+ assertFalse(userTwoModel.getTeam("git_users").canAdmin);
+
+ UserModel userThreeModel = ldap.authenticate("UserThree", "userThreePassword".toCharArray());
+ assertNotNull(userThreeModel);
+ assertNotNull(userThreeModel.getTeam("git_users"));
+ assertNull(userThreeModel.getTeam("git_admins"));
+ assertNull(userThreeModel.getTeam("git admins"));
assertTrue(userThreeModel.canAdmin);
+ assertTrue(userThreeModel.canAdmin());
+ assertFalse(userThreeModel.getTeam("git_users").canAdmin);
+
+ UserModel userFourModel = ldap.authenticate("UserFour", "userFourPassword".toCharArray());
+ assertNotNull(userFourModel);
+ assertNotNull(userFourModel.getTeam("git_users"));
+ assertNull(userFourModel.getTeam("git_admins"));
+ assertNull(userFourModel.getTeam("git admins"));
+ assertFalse(userFourModel.canAdmin);
+ assertFalse(userFourModel.canAdmin());
+ assertFalse(userFourModel.getTeam("git_users").canAdmin);
+ }
+
+ @Test
+ public void testAdminPropertyTeamsNotInLdap() {
+ settings.put(Keys.realm.ldap.maintainTeams, "false");
+
+ UserModel userOneModel = ldap.authenticate("UserOne", "userOnePassword".toCharArray());
+ assertNotNull(userOneModel);
+ assertNotNull(userOneModel.getTeam("git_admins"));
+ assertNull(userOneModel.getTeam("git admins"));
+ assertNotNull(userOneModel.getTeam("git_users"));
+ assertTrue(userOneModel.canAdmin);
+ assertTrue(userOneModel.canAdmin());
+ assertFalse(userOneModel.getTeam("git_admins").canAdmin);
+ assertFalse(userOneModel.getTeam("git_users").canAdmin);
+
+ UserModel userTwoModel = ldap.authenticate("UserTwo", "userTwoPassword".toCharArray());
+ assertNotNull(userTwoModel);
+ assertNotNull(userTwoModel.getTeam("git_users"));
+ assertNull(userTwoModel.getTeam("git_admins"));
+ assertNotNull(userTwoModel.getTeam("git admins"));
+ assertFalse(userTwoModel.canAdmin);
+ assertTrue(userTwoModel.canAdmin());
+ assertTrue(userTwoModel.getTeam("git admins").canAdmin);
+ assertFalse(userTwoModel.getTeam("git_users").canAdmin);
+
+ UserModel userThreeModel = ldap.authenticate("UserThree", "userThreePassword".toCharArray());
+ assertNotNull(userThreeModel);
+ assertNotNull(userThreeModel.getTeam("git_users"));
+ assertNull(userThreeModel.getTeam("git_admins"));
+ assertNull(userThreeModel.getTeam("git admins"));
+ assertFalse(userThreeModel.canAdmin);
+ assertFalse(userThreeModel.canAdmin());
+ assertFalse(userThreeModel.getTeam("git_users").canAdmin);
+
+ UserModel userFourModel = ldap.authenticate("UserFour", "userFourPassword".toCharArray());
+ assertNotNull(userFourModel);
+ assertNotNull(userFourModel.getTeam("git_users"));
+ assertNull(userFourModel.getTeam("git_admins"));
+ assertNull(userFourModel.getTeam("git admins"));
+ assertFalse(userFourModel.canAdmin);
+ assertFalse(userFourModel.canAdmin());
+ assertFalse(userFourModel.getTeam("git_users").canAdmin);
}
@Test
@@ -204,13 +234,13 @@ public class LdapAuthenticationTest extends GitblitUnitTest {
@Test
public void checkIfUsersConfContainsAllUsersFromSampleDataLdif() throws Exception {
- SearchResult searchResult = ds.search("OU=Users,OU=UserControl,OU=MyOrganization,DC=MyDomain", SearchScope.SUB, "objectClass=person");
+ SearchResult searchResult = getDS().search(ACCOUNT_BASE, SearchScope.SUB, "objectClass=person");
assertEquals("Number of ldap users in gitblit user model", searchResult.getEntryCount(), countLdapUsersInUserManager());
}
@Test
public void addingUserInLdapShouldNotUpdateGitBlitUsersAndGroups() throws Exception {
- ds.addEntries(LDIFReader.readEntries(RESOURCE_DIR + "adduser.ldif"));
+ getDS().addEntries(LDIFReader.readEntries(RESOURCE_DIR + "adduser.ldif"));
ldap.sync();
assertEquals("Number of ldap users in gitblit user model", 5, countLdapUsersInUserManager());
}
@@ -218,33 +248,126 @@ public class LdapAuthenticationTest extends GitblitUnitTest {
@Test
public void addingUserInLdapShouldUpdateGitBlitUsersAndGroups() throws Exception {
settings.put(Keys.realm.ldap.synchronize, "true");
- ds.addEntries(LDIFReader.readEntries(RESOURCE_DIR + "adduser.ldif"));
+ getDS().addEntries(LDIFReader.readEntries(RESOURCE_DIR + "adduser.ldif"));
ldap.sync();
assertEquals("Number of ldap users in gitblit user model", 6, countLdapUsersInUserManager());
}
@Test
public void addingGroupsInLdapShouldNotUpdateGitBlitUsersAndGroups() throws Exception {
- ds.addEntries(LDIFReader.readEntries(RESOURCE_DIR + "addgroup.ldif"));
+ getDS().addEntries(LDIFReader.readEntries(RESOURCE_DIR + "addgroup.ldif"));
ldap.sync();
assertEquals("Number of ldap groups in gitblit team model", 0, countLdapTeamsInUserManager());
}
@Test
+ public void addingGroupsInLdapShouldUpdateGitBlitUsersNotGroups2() throws Exception {
+ settings.put(Keys.realm.ldap.synchronize, "true");
+ settings.put(Keys.realm.ldap.maintainTeams, "false");
+ getDS().addEntries(LDIFReader.readEntries(RESOURCE_DIR + "adduser.ldif"));
+ getDS().addEntries(LDIFReader.readEntries(RESOURCE_DIR + "addgroup.ldif"));
+ ldap.sync();
+ assertEquals("Number of ldap users in gitblit user model", 6, countLdapUsersInUserManager());
+ assertEquals("Number of ldap groups in gitblit team model", 0, countLdapTeamsInUserManager());
+ }
+
+ @Test
public void addingGroupsInLdapShouldUpdateGitBlitUsersAndGroups() throws Exception {
+ // This test only makes sense if the authentication mode allows for synchronization.
+ assumeTrue(authMode == AuthMode.ANONYMOUS || authMode == AuthMode.DS_MANAGER);
+
settings.put(Keys.realm.ldap.synchronize, "true");
- ds.addEntries(LDIFReader.readEntries(RESOURCE_DIR + "addgroup.ldif"));
+ getDS().addEntries(LDIFReader.readEntries(RESOURCE_DIR + "addgroup.ldif"));
ldap.sync();
assertEquals("Number of ldap groups in gitblit team model", 1, countLdapTeamsInUserManager());
}
@Test
+ public void syncUpdateUsersAndGroupsAdminProperty() throws Exception {
+ // This test only makes sense if the authentication mode allows for synchronization.
+ assumeTrue(authMode == AuthMode.ANONYMOUS || authMode == AuthMode.DS_MANAGER);
+
+ settings.put(Keys.realm.ldap.synchronize, "true");
+ ldap.sync();
+
+ UserModel user = userManager.getUserModel("UserOne");
+ assertNotNull(user);
+ assertFalse(user.canAdmin);
+ assertTrue(user.canAdmin());
+
+ user = userManager.getUserModel("UserTwo");
+ assertNotNull(user);
+ assertFalse(user.canAdmin);
+ assertTrue(user.canAdmin());
+
+ user = userManager.getUserModel("UserThree");
+ assertNotNull(user);
+ assertTrue(user.canAdmin);
+ assertTrue(user.canAdmin());
+
+ user = userManager.getUserModel("UserFour");
+ assertNotNull(user);
+ assertFalse(user.canAdmin);
+ assertFalse(user.canAdmin());
+
+ TeamModel team = userManager.getTeamModel("Git_Admins");
+ assertNotNull(team);
+ assertTrue(team.canAdmin);
+
+ team = userManager.getTeamModel("Git Admins");
+ assertNotNull(team);
+ assertTrue(team.canAdmin);
+
+ team = userManager.getTeamModel("Git_Users");
+ assertNotNull(team);
+ assertFalse(team.canAdmin);
+ }
+
+ @Test
+ public void syncNotUpdateUsersAndGroupsAdminProperty() throws Exception {
+ settings.put(Keys.realm.ldap.synchronize, "true");
+ settings.put(Keys.realm.ldap.maintainTeams, "false");
+ ldap.sync();
+
+ UserModel user = userManager.getUserModel("UserOne");
+ assertNotNull(user);
+ assertTrue(user.canAdmin);
+ assertTrue(user.canAdmin());
+
+ user = userManager.getUserModel("UserTwo");
+ assertNotNull(user);
+ assertFalse(user.canAdmin);
+ assertTrue(user.canAdmin());
+
+ user = userManager.getUserModel("UserThree");
+ assertNotNull(user);
+ assertFalse(user.canAdmin);
+ assertFalse(user.canAdmin());
+
+ user = userManager.getUserModel("UserFour");
+ assertNotNull(user);
+ assertFalse(user.canAdmin);
+ assertFalse(user.canAdmin());
+
+ TeamModel team = userManager.getTeamModel("Git_Admins");
+ assertNotNull(team);
+ assertFalse(team.canAdmin);
+
+ team = userManager.getTeamModel("Git Admins");
+ assertNotNull(team);
+ assertTrue(team.canAdmin);
+
+ team = userManager.getTeamModel("Git_Users");
+ assertNotNull(team);
+ assertFalse(team.canAdmin);
+ }
+
+ @Test
public void testAuthenticationManager() {
UserModel userOneModel = auth.authenticate("UserOne", "userOnePassword".toCharArray(), null);
assertNotNull(userOneModel);
assertNotNull(userOneModel.getTeam("git_admins"));
assertNotNull(userOneModel.getTeam("git_users"));
- assertTrue(userOneModel.canAdmin);
UserModel userOneModelFailedAuth = auth.authenticate("UserOne", "userTwoPassword".toCharArray(), null);
assertNull(userOneModelFailedAuth);
@@ -254,18 +377,115 @@ public class LdapAuthenticationTest extends GitblitUnitTest {
assertNotNull(userTwoModel.getTeam("git_users"));
assertNull(userTwoModel.getTeam("git_admins"));
assertNotNull(userTwoModel.getTeam("git admins"));
- assertTrue(userTwoModel.canAdmin);
UserModel userThreeModel = auth.authenticate("UserThree", "userThreePassword".toCharArray(), null);
assertNotNull(userThreeModel);
assertNotNull(userThreeModel.getTeam("git_users"));
assertNull(userThreeModel.getTeam("git_admins"));
+
+ UserModel userFourModel = auth.authenticate("UserFour", "userFourPassword".toCharArray(), null);
+ assertNotNull(userFourModel);
+ assertNotNull(userFourModel.getTeam("git_users"));
+ assertNull(userFourModel.getTeam("git_admins"));
+ assertNull(userFourModel.getTeam("git admins"));
+ }
+
+ @Test
+ public void testAuthenticationManagerAdminPropertyTeamsInLdap() {
+ UserModel userOneModel = auth.authenticate("UserOne", "userOnePassword".toCharArray(), null);
+ assertNotNull(userOneModel);
+ assertNotNull(userOneModel.getTeam("git_admins"));
+ assertNull(userOneModel.getTeam("git admins"));
+ assertNotNull(userOneModel.getTeam("git_users"));
+ assertFalse(userOneModel.canAdmin);
+ assertTrue(userOneModel.canAdmin());
+ assertTrue(userOneModel.getTeam("git_admins").canAdmin);
+ assertFalse(userOneModel.getTeam("git_users").canAdmin);
+
+ UserModel userOneModelFailedAuth = auth.authenticate("UserOne", "userTwoPassword".toCharArray(), null);
+ assertNull(userOneModelFailedAuth);
+
+ UserModel userTwoModel = auth.authenticate("UserTwo", "userTwoPassword".toCharArray(), null);
+ assertNotNull(userTwoModel);
+ assertNotNull(userTwoModel.getTeam("git_users"));
+ assertNull(userTwoModel.getTeam("git_admins"));
+ assertNotNull(userTwoModel.getTeam("git admins"));
+ assertFalse(userTwoModel.canAdmin);
+ assertTrue(userTwoModel.canAdmin());
+ assertTrue(userTwoModel.getTeam("git admins").canAdmin);
+ assertFalse(userTwoModel.getTeam("git_users").canAdmin);
+
+ UserModel userThreeModel = auth.authenticate("UserThree", "userThreePassword".toCharArray(), null);
+ assertNotNull(userThreeModel);
+ assertNotNull(userThreeModel.getTeam("git_users"));
+ assertNull(userThreeModel.getTeam("git_admins"));
+ assertNull(userThreeModel.getTeam("git admins"));
assertTrue(userThreeModel.canAdmin);
+ assertTrue(userThreeModel.canAdmin());
+ assertFalse(userThreeModel.getTeam("git_users").canAdmin);
+
+ UserModel userFourModel = auth.authenticate("UserFour", "userFourPassword".toCharArray(), null);
+ assertNotNull(userFourModel);
+ assertNotNull(userFourModel.getTeam("git_users"));
+ assertNull(userFourModel.getTeam("git_admins"));
+ assertNull(userFourModel.getTeam("git admins"));
+ assertFalse(userFourModel.canAdmin);
+ assertFalse(userFourModel.canAdmin());
+ assertFalse(userFourModel.getTeam("git_users").canAdmin);
+ }
+
+ @Test
+ public void testAuthenticationManagerAdminPropertyTeamsNotInLdap() {
+ settings.put(Keys.realm.ldap.maintainTeams, "false");
+
+ UserModel userOneModel = auth.authenticate("UserOne", "userOnePassword".toCharArray(), null);
+ assertNotNull(userOneModel);
+ assertNotNull(userOneModel.getTeam("git_admins"));
+ assertNull(userOneModel.getTeam("git admins"));
+ assertNotNull(userOneModel.getTeam("git_users"));
+ assertTrue(userOneModel.canAdmin);
+ assertTrue(userOneModel.canAdmin());
+ assertFalse(userOneModel.getTeam("git_admins").canAdmin);
+ assertFalse(userOneModel.getTeam("git_users").canAdmin);
+
+ UserModel userOneModelFailedAuth = auth.authenticate("UserOne", "userTwoPassword".toCharArray(), null);
+ assertNull(userOneModelFailedAuth);
+
+ UserModel userTwoModel = auth.authenticate("UserTwo", "userTwoPassword".toCharArray(), null);
+ assertNotNull(userTwoModel);
+ assertNotNull(userTwoModel.getTeam("git_users"));
+ assertNull(userTwoModel.getTeam("git_admins"));
+ assertNotNull(userTwoModel.getTeam("git admins"));
+ assertFalse(userTwoModel.canAdmin);
+ assertTrue(userTwoModel.canAdmin());
+ assertTrue(userTwoModel.getTeam("git admins").canAdmin);
+ assertFalse(userTwoModel.getTeam("git_users").canAdmin);
+
+ UserModel userThreeModel = auth.authenticate("UserThree", "userThreePassword".toCharArray(), null);
+ assertNotNull(userThreeModel);
+ assertNotNull(userThreeModel.getTeam("git_users"));
+ assertNull(userThreeModel.getTeam("git_admins"));
+ assertNull(userThreeModel.getTeam("git admins"));
+ assertFalse(userThreeModel.canAdmin);
+ assertFalse(userThreeModel.canAdmin());
+ assertFalse(userThreeModel.getTeam("git_users").canAdmin);
+
+ UserModel userFourModel = auth.authenticate("UserFour", "userFourPassword".toCharArray(), null);
+ assertNotNull(userFourModel);
+ assertNotNull(userFourModel.getTeam("git_users"));
+ assertNull(userFourModel.getTeam("git_admins"));
+ assertNull(userFourModel.getTeam("git admins"));
+ assertFalse(userFourModel.canAdmin);
+ assertFalse(userFourModel.canAdmin());
+ assertFalse(userFourModel.getTeam("git_users").canAdmin);
}
@Test
public void testBindWithUser() {
- settings.put(Keys.realm.ldap.bindpattern, "CN=${username},OU=US,OU=Users,OU=UserControl,OU=MyOrganization,DC=MyDomain");
+ // This test only makes sense if the user is not prevented from reading users and teams.
+ assumeTrue(authMode != AuthMode.DS_MANAGER);
+
+ settings.put(Keys.realm.ldap.bindpattern, "CN=${username},OU=US," + ACCOUNT_BASE);
settings.put(Keys.realm.ldap.username, "");
settings.put(Keys.realm.ldap.password, "");
@@ -276,6 +496,14 @@ public class LdapAuthenticationTest extends GitblitUnitTest {
assertNull(userOneModelFailedAuth);
}
+
+
+
+
+
+
+
+
private int countLdapUsersInUserManager() {
int ldapAccountCount = 0;
for (UserModel userModel : userManager.getAllUsers()) {
diff --git a/src/test/java/com/gitblit/tests/LdapBasedUnitTest.java b/src/test/java/com/gitblit/tests/LdapBasedUnitTest.java
new file mode 100644
index 00000000..7aec50e6
--- /dev/null
+++ b/src/test/java/com/gitblit/tests/LdapBasedUnitTest.java
@@ -0,0 +1,410 @@
+package com.gitblit.tests;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.commons.io.FileUtils;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+import com.gitblit.Keys;
+import com.gitblit.tests.mock.MemorySettings;
+import com.unboundid.ldap.listener.InMemoryDirectoryServer;
+import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
+import com.unboundid.ldap.listener.InMemoryDirectoryServerSnapshot;
+import com.unboundid.ldap.listener.InMemoryListenerConfig;
+import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedRequest;
+import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedResult;
+import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchEntry;
+import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchRequest;
+import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
+import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSimpleBindResult;
+import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
+import com.unboundid.ldap.sdk.BindRequest;
+import com.unboundid.ldap.sdk.BindResult;
+import com.unboundid.ldap.sdk.LDAPException;
+import com.unboundid.ldap.sdk.LDAPResult;
+import com.unboundid.ldap.sdk.OperationType;
+import com.unboundid.ldap.sdk.ResultCode;
+import com.unboundid.ldap.sdk.SimpleBindRequest;
+
+
+
+/**
+ * Base class for Unit (/Integration) tests that test going against an
+ * in-memory UnboundID LDAP server.
+ *
+ * This base class creates separate in-memory LDAP servers for different scenarios:
+ * - ANONYMOUS: anonymous bind to LDAP.
+ * - DS_MANAGER: The DIRECTORY_MANAGER is set as DN to bind as an admin.
+ * Normal users are prohibited to search the DS, they can only bind.
+ * - USR_MANAGER: The USER_MANAGER is set as DN to bind as an admin.
+ * This account can only search users but not groups. Normal users can search groups.
+ *
+ * @author Florian Zschocke
+ *
+ */
+public abstract class LdapBasedUnitTest extends GitblitUnitTest {
+
+ protected static final String RESOURCE_DIR = "src/test/resources/ldap/";
+ private static final String DIRECTORY_MANAGER = "cn=Directory Manager";
+ private static final String USER_MANAGER = "cn=UserManager";
+ protected static final String ACCOUNT_BASE = "OU=Users,OU=UserControl,OU=MyOrganization,DC=MyDomain";
+ private static final String GROUP_BASE = "OU=Groups,OU=UserControl,OU=MyOrganization,DC=MyDomain";
+ protected static final String DN_USER_ONE = "CN=UserOne,OU=US," + ACCOUNT_BASE;
+ protected static final String DN_USER_TWO = "CN=UserTwo,OU=US," + ACCOUNT_BASE;
+ protected static final String DN_USER_THREE = "CN=UserThree,OU=Canada," + ACCOUNT_BASE;
+
+
+ /**
+ * Enumeration of different test modes, representing different use scenarios.
+ * With ANONYMOUS anonymous binds are used to search LDAP.
+ * DS_MANAGER will use a DIRECTORY_MANAGER to search LDAP. Normal users are prohibited to search the DS.
+ * With USR_MANAGER, a USER_MANAGER account is used to search in LDAP. This account can only search users
+ * but not groups. Normal users can search groups, though.
+ *
+ */
+ protected enum AuthMode {
+ ANONYMOUS,
+ DS_MANAGER,
+ USR_MANAGER;
+
+
+ private int ldapPort;
+ private InMemoryDirectoryServer ds;
+ private InMemoryDirectoryServerSnapshot dsSnapshot;
+ private BindTracker bindTracker;
+
+ void setLdapPort(int port) {
+ this.ldapPort = port;
+ }
+
+ int ldapPort() {
+ return this.ldapPort;
+ }
+
+ void setDS(InMemoryDirectoryServer ds) {
+ if (this.ds == null) {
+ this.ds = ds;
+ this.dsSnapshot = ds.createSnapshot();
+ };
+ }
+
+ InMemoryDirectoryServer getDS() {
+ return ds;
+ }
+
+ void setBindTracker(BindTracker bindTracker) {
+ this.bindTracker = bindTracker;
+ }
+
+ BindTracker getBindTracker() {
+ return bindTracker;
+ }
+
+ void restoreSnapshot() {
+ ds.restoreSnapshot(dsSnapshot);
+ }
+ }
+
+ @Parameter
+ public AuthMode authMode = AuthMode.ANONYMOUS;
+
+ @Rule
+ public TemporaryFolder folder = new TemporaryFolder();
+
+
+ protected File usersConf;
+
+ protected MemorySettings settings;
+
+
+ /**
+ * Run the tests with each authentication scenario once.
+ */
+ @Parameters(name = "{0}")
+ public static Collection<Object[]> data() {
+ return Arrays.asList(new Object[][] { {AuthMode.ANONYMOUS}, {AuthMode.DS_MANAGER}, {AuthMode.USR_MANAGER} });
+ }
+
+
+ /**
+ * Create three different in memory DS.
+ *
+ * Each DS has a different configuration:
+ * The first allows anonymous binds.
+ * The second requires authentication for all operations. It will only allow the DIRECTORY_MANAGER account
+ * to search for users and groups.
+ * The third one is like the second, but it allows users to search for users and groups, and restricts the
+ * USER_MANAGER from searching for groups.
+ */
+ @BeforeClass
+ public static void ldapInit() throws Exception {
+ InMemoryDirectoryServer ds;
+ InMemoryDirectoryServerConfig config = createInMemoryLdapServerConfig(AuthMode.ANONYMOUS);
+ config.setListenerConfigs(InMemoryListenerConfig.createLDAPConfig("anonymous"));
+ ds = createInMemoryLdapServer(config);
+ AuthMode.ANONYMOUS.setDS(ds);
+ AuthMode.ANONYMOUS.setLdapPort(ds.getListenPort("anonymous"));
+
+
+ config = createInMemoryLdapServerConfig(AuthMode.DS_MANAGER);
+ config.setListenerConfigs(InMemoryListenerConfig.createLDAPConfig("ds_manager"));
+ config.setAuthenticationRequiredOperationTypes(EnumSet.allOf(OperationType.class));
+ ds = createInMemoryLdapServer(config);
+ AuthMode.DS_MANAGER.setDS(ds);
+ AuthMode.DS_MANAGER.setLdapPort(ds.getListenPort("ds_manager"));
+
+
+ config = createInMemoryLdapServerConfig(AuthMode.USR_MANAGER);
+ config.setListenerConfigs(InMemoryListenerConfig.createLDAPConfig("usr_manager"));
+ config.setAuthenticationRequiredOperationTypes(EnumSet.allOf(OperationType.class));
+ ds = createInMemoryLdapServer(config);
+ AuthMode.USR_MANAGER.setDS(ds);
+ AuthMode.USR_MANAGER.setLdapPort(ds.getListenPort("usr_manager"));
+
+ }
+
+ @AfterClass
+ public static void destroy() throws Exception {
+ for (AuthMode am : AuthMode.values()) {
+ am.getDS().shutDown(true);
+ }
+ }
+
+ public static InMemoryDirectoryServer createInMemoryLdapServer(InMemoryDirectoryServerConfig config) throws Exception {
+ InMemoryDirectoryServer imds = new InMemoryDirectoryServer(config);
+ imds.importFromLDIF(true, RESOURCE_DIR + "sampledata.ldif");
+ imds.startListening();
+ return imds;
+ }
+
+ public static InMemoryDirectoryServerConfig createInMemoryLdapServerConfig(AuthMode authMode) throws Exception {
+ InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig("dc=MyDomain");
+ config.addAdditionalBindCredentials(DIRECTORY_MANAGER, "password");
+ config.addAdditionalBindCredentials(USER_MANAGER, "passwd");
+ config.setSchema(null);
+
+ authMode.setBindTracker(new BindTracker());
+ config.addInMemoryOperationInterceptor(authMode.getBindTracker());
+ config.addInMemoryOperationInterceptor(new AccessInterceptor(authMode));
+
+ return config;
+ }
+
+
+
+ @Before
+ public void setupBase() throws Exception {
+ authMode.restoreSnapshot();
+ authMode.getBindTracker().reset();
+
+ usersConf = folder.newFile("users.conf");
+ FileUtils.copyFile(new File(RESOURCE_DIR + "users.conf"), usersConf);
+ settings = getSettings();
+ }
+
+
+ protected InMemoryDirectoryServer getDS() {
+ return authMode.getDS();
+ }
+
+
+
+ protected MemorySettings getSettings() {
+ Map<String, Object> backingMap = new HashMap<String, Object>();
+ backingMap.put(Keys.realm.userService, usersConf.getAbsolutePath());
+ backingMap.put(Keys.realm.ldap.server, "ldap://localhost:" + authMode.ldapPort());
+ switch(authMode) {
+ case ANONYMOUS:
+ backingMap.put(Keys.realm.ldap.username, "");
+ backingMap.put(Keys.realm.ldap.password, "");
+ break;
+ case DS_MANAGER:
+ backingMap.put(Keys.realm.ldap.username, DIRECTORY_MANAGER);
+ backingMap.put(Keys.realm.ldap.password, "password");
+ break;
+ case USR_MANAGER:
+ backingMap.put(Keys.realm.ldap.username, USER_MANAGER);
+ backingMap.put(Keys.realm.ldap.password, "passwd");
+ break;
+ default:
+ throw new RuntimeException("Unimplemented AuthMode case!");
+
+ }
+ backingMap.put(Keys.realm.ldap.maintainTeams, "true");
+ backingMap.put(Keys.realm.ldap.accountBase, ACCOUNT_BASE);
+ backingMap.put(Keys.realm.ldap.accountPattern, "(&(objectClass=person)(sAMAccountName=${username}))");
+ backingMap.put(Keys.realm.ldap.groupBase, GROUP_BASE);
+ backingMap.put(Keys.realm.ldap.groupMemberPattern, "(&(objectClass=group)(member=${dn}))");
+ backingMap.put(Keys.realm.ldap.admins, "UserThree @Git_Admins \"@Git Admins\"");
+ backingMap.put(Keys.realm.ldap.displayName, "displayName");
+ backingMap.put(Keys.realm.ldap.email, "email");
+ backingMap.put(Keys.realm.ldap.uid, "sAMAccountName");
+
+ MemorySettings ms = new MemorySettings(backingMap);
+ return ms;
+ }
+
+
+
+
+ /**
+ * Operation interceptor for the in memory DS. This interceptor
+ * tracks bind requests.
+ *
+ */
+ protected static class BindTracker extends InMemoryOperationInterceptor {
+ private Map<Integer,String> lastSuccessfulBindDNs = new HashMap<>();
+ private String lastSuccessfulBindDN;
+
+
+ @Override
+ public void processSimpleBindResult(InMemoryInterceptedSimpleBindResult bind) {
+ BindResult result = bind.getResult();
+ if (result.getResultCode() == ResultCode.SUCCESS) {
+ BindRequest bindRequest = bind.getRequest();
+ lastSuccessfulBindDNs.put(bind.getMessageID(), ((SimpleBindRequest)bindRequest).getBindDN());
+ lastSuccessfulBindDN = ((SimpleBindRequest)bindRequest).getBindDN();
+ }
+ }
+
+ String getLastSuccessfulBindDN() {
+ return lastSuccessfulBindDN;
+ }
+
+ String getLastSuccessfulBindDN(int messageID) {
+ return lastSuccessfulBindDNs.get(messageID);
+ }
+
+ void reset() {
+ lastSuccessfulBindDNs = new HashMap<>();
+ lastSuccessfulBindDN = null;
+ }
+ }
+
+
+
+ /**
+ * Operation interceptor for the in memory DS. This interceptor
+ * implements access restrictions for certain user/DN combinations.
+ *
+ * The USER_MANAGER is only allowed to search for users, but not for groups.
+ * This is to test the original behaviour where the teams were searched under
+ * the user binding.
+ * When running in a DIRECTORY_MANAGER scenario, only the manager account
+ * is allowed to search for users and groups, while a normal user may not do so.
+ * This tests the scenario where a normal user cannot read teams and thus the
+ * manager account needs to be used for all searches.
+ *
+ */
+ protected static class AccessInterceptor extends InMemoryOperationInterceptor {
+ AuthMode authMode;
+ Map<Long,String> lastSuccessfulBindDN = new HashMap<>();
+ Map<Long,Boolean> resultProhibited = new HashMap<>();
+
+ public AccessInterceptor(AuthMode authMode) {
+ this.authMode = authMode;
+ }
+
+
+ @Override
+ public void processSimpleBindResult(InMemoryInterceptedSimpleBindResult bind) {
+ BindResult result = bind.getResult();
+ if (result.getResultCode() == ResultCode.SUCCESS) {
+ BindRequest bindRequest = bind.getRequest();
+ lastSuccessfulBindDN.put(bind.getConnectionID(), ((SimpleBindRequest)bindRequest).getBindDN());
+ resultProhibited.remove(bind.getConnectionID());
+ }
+ }
+
+
+
+ @Override
+ public void processSearchRequest(InMemoryInterceptedSearchRequest request) throws LDAPException {
+ String bindDN = getLastBindDN(request);
+
+ if (USER_MANAGER.equals(bindDN)) {
+ if (request.getRequest().getBaseDN().endsWith(GROUP_BASE)) {
+ throw new LDAPException(ResultCode.NO_SUCH_OBJECT);
+ }
+ }
+ else if(authMode == AuthMode.DS_MANAGER && !DIRECTORY_MANAGER.equals(bindDN)) {
+ throw new LDAPException(ResultCode.NO_SUCH_OBJECT);
+ }
+ }
+
+
+ @Override
+ public void processSearchEntry(InMemoryInterceptedSearchEntry entry) {
+ String bindDN = getLastBindDN(entry);
+
+ boolean prohibited = false;
+
+ if (USER_MANAGER.equals(bindDN)) {
+ if (entry.getSearchEntry().getDN().endsWith(GROUP_BASE)) {
+ prohibited = true;
+ }
+ }
+ else if(authMode == AuthMode.DS_MANAGER && !DIRECTORY_MANAGER.equals(bindDN)) {
+ prohibited = true;
+ }
+
+ if (prohibited) {
+ // Found entry prohibited for bound user. Setting entry to null.
+ entry.setSearchEntry(null);
+ resultProhibited.put(entry.getConnectionID(), Boolean.TRUE);
+ }
+ }
+
+ @Override
+ public void processSearchResult(InMemoryInterceptedSearchResult result) {
+ String bindDN = getLastBindDN(result);
+
+ boolean prohibited = false;
+
+ Boolean rspb = resultProhibited.get(result.getConnectionID());
+ if (USER_MANAGER.equals(bindDN)) {
+ if (rspb != null && rspb) {
+ prohibited = true;
+ }
+ }
+ else if(authMode == AuthMode.DS_MANAGER && !DIRECTORY_MANAGER.equals(bindDN)) {
+ if (rspb != null && rspb) {
+ prohibited = true;
+ }
+ }
+
+ if (prohibited) {
+ // Result prohibited for bound user. Returning error
+ result.setResult(new LDAPResult(result.getMessageID(), ResultCode.INSUFFICIENT_ACCESS_RIGHTS));
+ resultProhibited.remove(result.getConnectionID());
+ }
+ }
+
+ private String getLastBindDN(InMemoryInterceptedResult result) {
+ String bindDN = lastSuccessfulBindDN.get(result.getConnectionID());
+ if (bindDN == null) {
+ return "UNKNOWN";
+ }
+ return bindDN;
+ }
+ private String getLastBindDN(InMemoryInterceptedRequest request) {
+ String bindDN = lastSuccessfulBindDN.get(request.getConnectionID());
+ if (bindDN == null) {
+ return "UNKNOWN";
+ }
+ return bindDN;
+ }
+ }
+
+}
diff --git a/src/test/java/com/gitblit/tests/LdapConnectionTest.java b/src/test/java/com/gitblit/tests/LdapConnectionTest.java
new file mode 100644
index 00000000..3da54777
--- /dev/null
+++ b/src/test/java/com/gitblit/tests/LdapConnectionTest.java
@@ -0,0 +1,280 @@
+package com.gitblit.tests;
+
+import static org.junit.Assume.assumeTrue;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import com.gitblit.Keys;
+import com.gitblit.ldap.LdapConnection;
+import com.unboundid.ldap.sdk.BindResult;
+import com.unboundid.ldap.sdk.LDAPException;
+import com.unboundid.ldap.sdk.ResultCode;
+import com.unboundid.ldap.sdk.SearchRequest;
+import com.unboundid.ldap.sdk.SearchResult;
+import com.unboundid.ldap.sdk.SearchResultEntry;
+import com.unboundid.ldap.sdk.SearchScope;
+
+/*
+ * Test for the LdapConnection
+ *
+ * @author Florian Zschocke
+ *
+ */
+@RunWith(Parameterized.class)
+public class LdapConnectionTest extends LdapBasedUnitTest {
+
+ @Test
+ public void testEscapeLDAPFilterString() {
+ // This test is independent from authentication mode, so run only once.
+ assumeTrue(authMode == AuthMode.ANONYMOUS);
+
+ // From: https://www.owasp.org/index.php/Preventing_LDAP_Injection_in_Java
+ assertEquals("No special characters to escape", "Hi This is a test #çà", LdapConnection.escapeLDAPSearchFilter("Hi This is a test #çà"));
+ assertEquals("LDAP Christams Tree", "Hi \\28This\\29 = is \\2a a \\5c test # ç à ô", LdapConnection.escapeLDAPSearchFilter("Hi (This) = is * a \\ test # ç à ô"));
+
+ assertEquals("Injection", "\\2a\\29\\28userPassword=secret", LdapConnection.escapeLDAPSearchFilter("*)(userPassword=secret"));
+ }
+
+
+ @Test
+ public void testConnect() {
+ // This test is independent from authentication mode, so run only once.
+ assumeTrue(authMode == AuthMode.ANONYMOUS);
+
+ LdapConnection conn = new LdapConnection(settings);
+ try {
+ assertTrue(conn.connect());
+ } finally {
+ conn.close();
+ }
+ }
+
+
+ @Test
+ public void testBindAnonymous() {
+ // This test tests for anonymous bind, so run only in authentication mode ANONYMOUS.
+ assumeTrue(authMode == AuthMode.ANONYMOUS);
+
+ LdapConnection conn = new LdapConnection(settings);
+ try {
+ assertTrue(conn.connect());
+
+ BindResult br = conn.bind();
+ assertNotNull(br);
+ assertEquals(ResultCode.SUCCESS, br.getResultCode());
+ assertEquals("", authMode.getBindTracker().getLastSuccessfulBindDN(br.getMessageID()));
+
+ } finally {
+ conn.close();
+ }
+ }
+
+
+ @Test
+ public void testBindAsAdmin() {
+ // This test tests for anonymous bind, so run only in authentication mode DS_MANAGER.
+ assumeTrue(authMode == AuthMode.DS_MANAGER);
+
+ LdapConnection conn = new LdapConnection(settings);
+ try {
+ assertTrue(conn.connect());
+
+ BindResult br = conn.bind();
+ assertNotNull(br);
+ assertEquals(ResultCode.SUCCESS, br.getResultCode());
+ assertEquals(settings.getString(Keys.realm.ldap.username, "UNSET"), authMode.getBindTracker().getLastSuccessfulBindDN(br.getMessageID()));
+
+ } finally {
+ conn.close();
+ }
+ }
+
+
+ @Test
+ public void testBindToBindpattern() {
+ LdapConnection conn = new LdapConnection(settings);
+ try {
+ assertTrue(conn.connect());
+
+ String bindPattern = "CN=${username},OU=Canada," + ACCOUNT_BASE;
+
+ BindResult br = conn.bind(bindPattern, "UserThree", "userThreePassword");
+ assertNotNull(br);
+ assertEquals(ResultCode.SUCCESS, br.getResultCode());
+ assertEquals("CN=UserThree,OU=Canada," + ACCOUNT_BASE, authMode.getBindTracker().getLastSuccessfulBindDN(br.getMessageID()));
+
+ br = conn.bind(bindPattern, "UserFour", "userThreePassword");
+ assertNull(br);
+
+ br = conn.bind(bindPattern, "UserTwo", "userTwoPassword");
+ assertNull(br);
+
+ } finally {
+ conn.close();
+ }
+ }
+
+
+ @Test
+ public void testRebindAsUser() {
+ LdapConnection conn = new LdapConnection(settings);
+ try {
+ assertTrue(conn.connect());
+
+ assertFalse(conn.rebindAsUser());
+
+ BindResult br = conn.bind();
+ assertNotNull(br);
+ assertFalse(conn.rebindAsUser());
+
+
+ String bindPattern = "CN=${username},OU=Canada," + ACCOUNT_BASE;
+ br = conn.bind(bindPattern, "UserThree", "userThreePassword");
+ assertNotNull(br);
+ assertFalse(conn.rebindAsUser());
+
+ br = conn.bind();
+ assertNotNull(br);
+ assertTrue(conn.rebindAsUser());
+ assertEquals(ResultCode.SUCCESS, br.getResultCode());
+ assertEquals("CN=UserThree,OU=Canada," + ACCOUNT_BASE, authMode.getBindTracker().getLastSuccessfulBindDN());
+
+ } finally {
+ conn.close();
+ }
+ }
+
+
+
+ @Test
+ public void testSearchRequest() throws LDAPException {
+ LdapConnection conn = new LdapConnection(settings);
+ try {
+ assertTrue(conn.connect());
+ BindResult br = conn.bind();
+ assertNotNull(br);
+
+ SearchRequest req;
+ SearchResult result;
+ SearchResultEntry entry;
+
+ req = new SearchRequest(ACCOUNT_BASE, SearchScope.BASE, "(CN=UserOne)");
+ result = conn.search(req);
+ assertNotNull(result);
+ assertEquals(0, result.getEntryCount());
+
+ req = new SearchRequest(ACCOUNT_BASE, SearchScope.ONE, "(CN=UserTwo)");
+ result = conn.search(req);
+ assertNotNull(result);
+ assertEquals(0, result.getEntryCount());
+
+ req = new SearchRequest(ACCOUNT_BASE, SearchScope.SUB, "(CN=UserThree)");
+ result = conn.search(req);
+ assertNotNull(result);
+ assertEquals(1, result.getEntryCount());
+ entry = result.getSearchEntries().get(0);
+ assertEquals("CN=UserThree,OU=Canada," + ACCOUNT_BASE, entry.getDN());
+
+ req = new SearchRequest(ACCOUNT_BASE, SearchScope.SUBORDINATE_SUBTREE, "(CN=UserFour)");
+ result = conn.search(req);
+ assertNotNull(result);
+ assertEquals(1, result.getEntryCount());
+ entry = result.getSearchEntries().get(0);
+ assertEquals("CN=UserFour,OU=Canada," + ACCOUNT_BASE, entry.getDN());
+
+ } finally {
+ conn.close();
+ }
+ }
+
+
+ @Test
+ public void testSearch() throws LDAPException {
+ LdapConnection conn = new LdapConnection(settings);
+ try {
+ assertTrue(conn.connect());
+ BindResult br = conn.bind();
+ assertNotNull(br);
+
+ SearchResult result;
+ SearchResultEntry entry;
+
+ result = conn.search(ACCOUNT_BASE, false, "(CN=UserOne)", null);
+ assertNotNull(result);
+ assertEquals(1, result.getEntryCount());
+ entry = result.getSearchEntries().get(0);
+ assertEquals("CN=UserOne,OU=US," + ACCOUNT_BASE, entry.getDN());
+
+ result = conn.search(ACCOUNT_BASE, true, "(&(CN=UserOne)(surname=One))", null);
+ assertNotNull(result);
+ assertEquals(1, result.getEntryCount());
+ entry = result.getSearchEntries().get(0);
+ assertEquals("CN=UserOne,OU=US," + ACCOUNT_BASE, entry.getDN());
+
+ result = conn.search(ACCOUNT_BASE, true, "(&(CN=UserOne)(surname=Two))", null);
+ assertNotNull(result);
+ assertEquals(0, result.getEntryCount());
+
+ result = conn.search(ACCOUNT_BASE, true, "(surname=Two)", Arrays.asList("givenName", "surname"));
+ assertNotNull(result);
+ assertEquals(1, result.getEntryCount());
+ entry = result.getSearchEntries().get(0);
+ assertEquals("CN=UserTwo,OU=US," + ACCOUNT_BASE, entry.getDN());
+ assertEquals(2, entry.getAttributes().size());
+ assertEquals("User", entry.getAttributeValue("givenName"));
+ assertEquals("Two", entry.getAttributeValue("surname"));
+
+ result = conn.search(ACCOUNT_BASE, true, "(personalTitle=Mr*)", null);
+ assertNotNull(result);
+ assertEquals(3, result.getEntryCount());
+ ArrayList<String> names = new ArrayList<>(3);
+ names.add(result.getSearchEntries().get(0).getAttributeValue("surname"));
+ names.add(result.getSearchEntries().get(1).getAttributeValue("surname"));
+ names.add(result.getSearchEntries().get(2).getAttributeValue("surname"));
+ assertTrue(names.contains("One"));
+ assertTrue(names.contains("Two"));
+ assertTrue(names.contains("Three"));
+
+ } finally {
+ conn.close();
+ }
+ }
+
+
+ @Test
+ public void testSearchUser() throws LDAPException {
+ LdapConnection conn = new LdapConnection(settings);
+ try {
+ assertTrue(conn.connect());
+ BindResult br = conn.bind();
+ assertNotNull(br);
+
+ SearchResult result;
+ SearchResultEntry entry;
+
+ result = conn.searchUser("UserOne");
+ assertNotNull(result);
+ assertEquals(1, result.getEntryCount());
+ entry = result.getSearchEntries().get(0);
+ assertEquals("CN=UserOne,OU=US," + ACCOUNT_BASE, entry.getDN());
+
+ result = conn.searchUser("UserFour", Arrays.asList("givenName", "surname"));
+ assertNotNull(result);
+ assertEquals(1, result.getEntryCount());
+ entry = result.getSearchEntries().get(0);
+ assertEquals("CN=UserFour,OU=Canada," + ACCOUNT_BASE, entry.getDN());
+ assertEquals(2, entry.getAttributes().size());
+ assertEquals("User", entry.getAttributeValue("givenName"));
+ assertEquals("Four", entry.getAttributeValue("surname"));
+
+ } finally {
+ conn.close();
+ }
+ }
+
+}
diff --git a/src/test/java/com/gitblit/tests/LdapPublicKeyManagerTest.java b/src/test/java/com/gitblit/tests/LdapPublicKeyManagerTest.java
new file mode 100644
index 00000000..c426254f
--- /dev/null
+++ b/src/test/java/com/gitblit/tests/LdapPublicKeyManagerTest.java
@@ -0,0 +1,723 @@
+/*
+ * Copyright 2016 Florian Zschocke
+ * Copyright 2016 gitblit.com
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.tests;
+
+import static org.junit.Assume.assumeTrue;
+
+import java.security.GeneralSecurityException;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.Signature;
+import java.security.spec.ECGenParameterSpec;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.sshd.common.util.SecurityUtils;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import com.gitblit.Keys;
+import com.gitblit.Constants.AccessPermission;
+import com.gitblit.transport.ssh.LdapKeyManager;
+import com.gitblit.transport.ssh.SshKey;
+import com.unboundid.ldap.sdk.LDAPException;
+import com.unboundid.ldap.sdk.Modification;
+import com.unboundid.ldap.sdk.ModificationType;
+
+/**
+ * Test LdapPublicKeyManager going against an in-memory UnboundID
+ * LDAP server.
+ *
+ * @author Florian Zschocke
+ *
+ */
+@RunWith(Parameterized.class)
+public class LdapPublicKeyManagerTest extends LdapBasedUnitTest {
+
+ private static Map<String,KeyPair> keyPairs = new HashMap<>(10);
+ private static KeyPairGenerator rsaGenerator;
+ private static KeyPairGenerator dsaGenerator;
+ private static KeyPairGenerator ecGenerator;
+
+
+
+ @BeforeClass
+ public static void init() throws GeneralSecurityException {
+ rsaGenerator = SecurityUtils.getKeyPairGenerator("RSA");
+ dsaGenerator = SecurityUtils.getKeyPairGenerator("DSA");
+ ecGenerator = SecurityUtils.getKeyPairGenerator("ECDSA");
+ }
+
+
+
+ @Test
+ public void testGetKeys() throws LDAPException {
+ String keyRsaOne = getRsaPubKey("UserOne@example.com");
+ getDS().modify(DN_USER_ONE, new Modification(ModificationType.ADD, "sshPublicKey", keyRsaOne));
+
+ String keyRsaTwo = getRsaPubKey("UserTwo@example.com");
+ String keyDsaTwo = getDsaPubKey("UserTwo@example.com");
+ getDS().modify(DN_USER_TWO, new Modification(ModificationType.ADD, "sshPublicKey", keyRsaTwo, keyDsaTwo));
+
+ String keyRsaThree = getRsaPubKey("UserThree@example.com");
+ String keyDsaThree = getDsaPubKey("UserThree@example.com");
+ String keyEcThree = getEcPubKey("UserThree@example.com");
+ getDS().modify(DN_USER_THREE, new Modification(ModificationType.ADD, "sshPublicKey", keyEcThree, keyRsaThree, keyDsaThree));
+
+ LdapKeyManager kmgr = new LdapKeyManager(settings);
+
+ List<SshKey> keys = kmgr.getKeys("UserOne");
+ assertNotNull(keys);
+ assertTrue(keys.size() == 1);
+ assertEquals(keyRsaOne, keys.get(0).getRawData());
+
+
+ keys = kmgr.getKeys("UserTwo");
+ assertNotNull(keys);
+ assertTrue(keys.size() == 2);
+ if (keyRsaTwo.equals(keys.get(0).getRawData())) {
+ assertEquals(keyDsaTwo, keys.get(1).getRawData());
+ } else if (keyDsaTwo.equals(keys.get(0).getRawData())) {
+ assertEquals(keyRsaTwo, keys.get(1).getRawData());
+ } else {
+ fail("Mismatch in UserTwo keys.");
+ }
+
+
+ keys = kmgr.getKeys("UserThree");
+ assertNotNull(keys);
+ assertTrue(keys.size() == 3);
+ assertEquals(keyEcThree, keys.get(0).getRawData());
+ assertEquals(keyRsaThree, keys.get(1).getRawData());
+ assertEquals(keyDsaThree, keys.get(2).getRawData());
+
+ keys = kmgr.getKeys("UserFour");
+ assertNotNull(keys);
+ assertTrue(keys.size() == 0);
+ }
+
+
+ @Test
+ public void testGetKeysAttributeName() throws LDAPException {
+ settings.put(Keys.realm.ldap.sshPublicKey, "sshPublicKey");
+
+ String keyRsaOne = getRsaPubKey("UserOne@example.com");
+ getDS().modify(DN_USER_ONE, new Modification(ModificationType.ADD, "sshPublicKey", keyRsaOne));
+
+ String keyDsaTwo = getDsaPubKey("UserTwo@example.com");
+ getDS().modify(DN_USER_TWO, new Modification(ModificationType.ADD, "publicsshkey", keyDsaTwo));
+
+ String keyRsaThree = getRsaPubKey("UserThree@example.com");
+ String keyDsaThree = getDsaPubKey("UserThree@example.com");
+ getDS().modify(DN_USER_THREE, new Modification(ModificationType.ADD, "sshPublicKey", keyRsaThree));
+ getDS().modify(DN_USER_THREE, new Modification(ModificationType.ADD, "publicsshkey", keyDsaThree));
+
+
+ LdapKeyManager kmgr = new LdapKeyManager(settings);
+
+ List<SshKey> keys = kmgr.getKeys("UserOne");
+ assertNotNull(keys);
+ assertEquals(1, keys.size());
+ assertEquals(keyRsaOne, keys.get(0).getRawData());
+
+ keys = kmgr.getKeys("UserTwo");
+ assertNotNull(keys);
+ assertEquals(0, keys.size());
+
+ keys = kmgr.getKeys("UserThree");
+ assertNotNull(keys);
+ assertEquals(1, keys.size());
+ assertEquals(keyRsaThree, keys.get(0).getRawData());
+
+ keys = kmgr.getKeys("UserFour");
+ assertNotNull(keys);
+ assertEquals(0, keys.size());
+
+
+ settings.put(Keys.realm.ldap.sshPublicKey, "publicsshkey");
+
+ keys = kmgr.getKeys("UserOne");
+ assertNotNull(keys);
+ assertEquals(0, keys.size());
+
+ keys = kmgr.getKeys("UserTwo");
+ assertNotNull(keys);
+ assertEquals(1, keys.size());
+ assertEquals(keyDsaTwo, keys.get(0).getRawData());
+
+ keys = kmgr.getKeys("UserThree");
+ assertNotNull(keys);
+ assertEquals(1, keys.size());
+ assertEquals(keyDsaThree, keys.get(0).getRawData());
+
+ keys = kmgr.getKeys("UserFour");
+ assertNotNull(keys);
+ assertEquals(0, keys.size());
+ }
+
+
+ @Test
+ public void testGetKeysPrefixed() throws LDAPException {
+ // This test is independent from authentication mode, so run only once.
+ assumeTrue(authMode == AuthMode.ANONYMOUS);
+
+ String keyRsaOne = getRsaPubKey("UserOne@example.com");
+ getDS().modify(DN_USER_ONE, new Modification(ModificationType.ADD, "sshPublicKey", keyRsaOne));
+
+ String keyRsaTwo = getRsaPubKey("UserTwo@example.com");
+ String keyDsaTwo = getDsaPubKey("UserTwo@example.com");
+ getDS().modify(DN_USER_TWO, new Modification(ModificationType.ADD, "altSecurityIdentities", keyRsaTwo));
+ getDS().modify(DN_USER_TWO, new Modification(ModificationType.ADD, "altSecurityIdentities", "SSHKey: " + keyDsaTwo));
+
+ String keyRsaThree = getRsaPubKey("UserThree@example.com");
+ String keyDsaThree = getDsaPubKey("UserThree@example.com");
+ String keyEcThree = getEcPubKey("UserThree@example.com");
+ getDS().modify(DN_USER_THREE, new Modification(ModificationType.ADD, "altSecurityIdentities", " SshKey :\r\n" + keyRsaThree));
+ getDS().modify(DN_USER_THREE, new Modification(ModificationType.ADD, "altSecurityIdentities", " sshkey: " + keyDsaThree));
+ getDS().modify(DN_USER_THREE, new Modification(ModificationType.ADD, "altSecurityIdentities", "ECDSAKey :\n " + keyEcThree));
+
+
+ LdapKeyManager kmgr = new LdapKeyManager(settings);
+
+ settings.put(Keys.realm.ldap.sshPublicKey, "altSecurityIdentities");
+
+ List<SshKey> keys = kmgr.getKeys("UserOne");
+ assertNotNull(keys);
+ assertEquals(0, keys.size());
+
+ keys = kmgr.getKeys("UserTwo");
+ assertNotNull(keys);
+ assertEquals(1, keys.size());
+ assertEquals(keyRsaTwo, keys.get(0).getRawData());
+
+ keys = kmgr.getKeys("UserThree");
+ assertNotNull(keys);
+ assertEquals(0, keys.size());
+
+ keys = kmgr.getKeys("UserFour");
+ assertNotNull(keys);
+ assertEquals(0, keys.size());
+
+
+
+ settings.put(Keys.realm.ldap.sshPublicKey, "altSecurityIdentities:SSHKey");
+
+ keys = kmgr.getKeys("UserOne");
+ assertNotNull(keys);
+ assertEquals(0, keys.size());
+
+ keys = kmgr.getKeys("UserTwo");
+ assertNotNull(keys);
+ assertEquals(1, keys.size());
+ assertEquals(keyDsaTwo, keys.get(0).getRawData());
+
+ keys = kmgr.getKeys("UserThree");
+ assertNotNull(keys);
+ assertEquals(2, keys.size());
+ assertEquals(keyRsaThree, keys.get(0).getRawData());
+ assertEquals(keyDsaThree, keys.get(1).getRawData());
+
+ keys = kmgr.getKeys("UserFour");
+ assertNotNull(keys);
+ assertEquals(0, keys.size());
+
+
+
+ settings.put(Keys.realm.ldap.sshPublicKey, "altSecurityIdentities:ECDSAKey");
+
+ keys = kmgr.getKeys("UserOne");
+ assertNotNull(keys);
+ assertEquals(0, keys.size());
+
+ keys = kmgr.getKeys("UserTwo");
+ assertNotNull(keys);
+ assertEquals(0, keys.size());
+
+ keys = kmgr.getKeys("UserThree");
+ assertNotNull(keys);
+ assertEquals(1, keys.size());
+ assertEquals(keyEcThree, keys.get(0).getRawData());
+
+ keys = kmgr.getKeys("UserFour");
+ assertNotNull(keys);
+ assertEquals(0, keys.size());
+ }
+
+
+ @Test
+ public void testGetKeysPermissions() throws LDAPException {
+ // This test is independent from authentication mode, so run only once.
+ assumeTrue(authMode == AuthMode.ANONYMOUS);
+
+ String keyRsaOne = getRsaPubKey("UserOne@example.com");
+ String keyRsaTwo = getRsaPubKey("");
+ String keyDsaTwo = getDsaPubKey("UserTwo at example.com");
+ String keyRsaThree = getRsaPubKey("UserThree@example.com");
+ String keyDsaThree = getDsaPubKey("READ key for user 'Three' @example.com");
+ String keyEcThree = getEcPubKey("UserThree@example.com");
+
+ getDS().modify(DN_USER_ONE, new Modification(ModificationType.ADD, "sshPublicKey", keyRsaOne));
+ getDS().modify(DN_USER_ONE, new Modification(ModificationType.ADD, "sshPublicKey", " " + keyRsaTwo));
+ getDS().modify(DN_USER_ONE, new Modification(ModificationType.ADD, "sshPublicKey", "no-agent-forwarding " + keyDsaTwo));
+ getDS().modify(DN_USER_ONE, new Modification(ModificationType.ADD, "sshPublicKey", " command=\"sh /etc/netstart tun0 \" " + keyRsaThree));
+ getDS().modify(DN_USER_ONE, new Modification(ModificationType.ADD, "sshPublicKey", " command=\"netstat -nult\",environment=\"gb=\\\"What now\\\"\" " + keyDsaThree));
+ getDS().modify(DN_USER_ONE, new Modification(ModificationType.ADD, "sshPublicKey", "environment=\"SSH=git\",command=\"netstat -nult\",environment=\"gbPerms=VIEW\" " + keyEcThree));
+
+ getDS().modify(DN_USER_TWO, new Modification(ModificationType.ADD, "sshPublicKey", "environment=\"gbPerm=R\" " + keyRsaOne));
+ getDS().modify(DN_USER_TWO, new Modification(ModificationType.ADD, "sshPublicKey", " restrict,environment=\"gbperm=V\" " + keyRsaTwo));
+ getDS().modify(DN_USER_TWO, new Modification(ModificationType.ADD, "sshPublicKey", "restrict,environment=\"GBPerm=RW\",pty " + keyDsaTwo));
+ getDS().modify(DN_USER_TWO, new Modification(ModificationType.ADD, "sshPublicKey", " environment=\"gbPerm=CLONE\",environment=\"X=\\\" Y \\\"\" " + keyRsaThree));
+ getDS().modify(DN_USER_TWO, new Modification(ModificationType.ADD, "sshPublicKey", " environment=\"A = B \",from=\"*.example.com,!pc.example.com\",environment=\"gbPerm=VIEW\" " + keyDsaThree));
+ getDS().modify(DN_USER_TWO, new Modification(ModificationType.ADD, "sshPublicKey", "environment=\"SSH=git\",environment=\"gbPerm=PUSH\",environment=\"XYZ='Ali Baba'\" " + keyEcThree));
+
+ getDS().modify(DN_USER_THREE, new Modification(ModificationType.ADD, "sshPublicKey", "environment=\"gbPerm=R\",environment=\"josh=\\\"mean\\\"\",tunnel=\"0\" " + keyRsaOne));
+ getDS().modify(DN_USER_THREE, new Modification(ModificationType.ADD, "sshPublicKey", " environment=\" gbPerm = V \" " + keyRsaTwo));
+ getDS().modify(DN_USER_THREE, new Modification(ModificationType.ADD, "sshPublicKey", "command=\"sh echo \\\"Nope, not you!\\\" \",user-rc,environment=\"gbPerm=RW\" " + keyDsaTwo));
+ getDS().modify(DN_USER_THREE, new Modification(ModificationType.ADD, "sshPublicKey", "environment=\"gbPerm=VIEW\",command=\"sh /etc/netstart tun0 \",environment=\"gbPerm=CLONE\",no-pty " + keyRsaThree));
+ getDS().modify(DN_USER_THREE, new Modification(ModificationType.ADD, "sshPublicKey", " command=\"netstat -nult\",environment=\"gbPerm=VIEW\" " + keyDsaThree));
+ getDS().modify(DN_USER_THREE, new Modification(ModificationType.ADD, "sshPublicKey", "environment=\"SSH=git\",command=\"netstat -nult\",environment=\"gbPerm=PUSH\" " + keyEcThree));
+
+
+ LdapKeyManager kmgr = new LdapKeyManager(settings);
+
+ List<SshKey> keys = kmgr.getKeys("UserOne");
+ assertNotNull(keys);
+ assertEquals(6, keys.size());
+ for (SshKey key : keys) {
+ assertEquals(AccessPermission.PUSH, key.getPermission());
+ }
+
+ keys = kmgr.getKeys("UserTwo");
+ assertNotNull(keys);
+ assertEquals(6, keys.size());
+ int seen = 0;
+ for (SshKey key : keys) {
+ if (keyRsaOne.equals(key.getRawData())) {
+ assertEquals(AccessPermission.CLONE, key.getPermission());
+ seen += 1 << 0;
+ }
+ else if (keyRsaTwo.equals(key.getRawData())) {
+ assertEquals(AccessPermission.VIEW, key.getPermission());
+ seen += 1 << 1;
+ }
+ else if (keyDsaTwo.equals(key.getRawData())) {
+ assertEquals(AccessPermission.PUSH, key.getPermission());
+ seen += 1 << 2;
+ }
+ else if (keyRsaThree.equals(key.getRawData())) {
+ assertEquals(AccessPermission.CLONE, key.getPermission());
+ seen += 1 << 3;
+ }
+ else if (keyDsaThree.equals(key.getRawData())) {
+ assertEquals(AccessPermission.VIEW, key.getPermission());
+ seen += 1 << 4;
+ }
+ else if (keyEcThree.equals(key.getRawData())) {
+ assertEquals(AccessPermission.PUSH, key.getPermission());
+ seen += 1 << 5;
+ }
+ }
+ assertEquals(63, seen);
+
+ keys = kmgr.getKeys("UserThree");
+ assertNotNull(keys);
+ assertEquals(6, keys.size());
+ seen = 0;
+ for (SshKey key : keys) {
+ if (keyRsaOne.equals(key.getRawData())) {
+ assertEquals(AccessPermission.CLONE, key.getPermission());
+ seen += 1 << 0;
+ }
+ else if (keyRsaTwo.equals(key.getRawData())) {
+ assertEquals(AccessPermission.VIEW, key.getPermission());
+ seen += 1 << 1;
+ }
+ else if (keyDsaTwo.equals(key.getRawData())) {
+ assertEquals(AccessPermission.PUSH, key.getPermission());
+ seen += 1 << 2;
+ }
+ else if (keyRsaThree.equals(key.getRawData())) {
+ assertEquals(AccessPermission.CLONE, key.getPermission());
+ seen += 1 << 3;
+ }
+ else if (keyDsaThree.equals(key.getRawData())) {
+ assertEquals(AccessPermission.VIEW, key.getPermission());
+ seen += 1 << 4;
+ }
+ else if (keyEcThree.equals(key.getRawData())) {
+ assertEquals(AccessPermission.PUSH, key.getPermission());
+ seen += 1 << 5;
+ }
+ }
+ assertEquals(63, seen);
+ }
+
+
+ @Test
+ public void testGetKeysPrefixedPermissions() throws LDAPException {
+ // This test is independent from authentication mode, so run only once.
+ assumeTrue(authMode == AuthMode.ANONYMOUS);
+
+ String keyRsaOne = getRsaPubKey("UserOne@example.com");
+ String keyRsaTwo = getRsaPubKey("UserTwo at example.com");
+ String keyDsaTwo = getDsaPubKey("UserTwo@example.com");
+ String keyRsaThree = getRsaPubKey("example.com: user Three");
+ String keyDsaThree = getDsaPubKey("");
+ String keyEcThree = getEcPubKey(" ");
+
+ getDS().modify(DN_USER_ONE, new Modification(ModificationType.ADD, "altSecurityIdentities", "permitopen=\"host:220\"" + keyRsaOne));
+ getDS().modify(DN_USER_ONE, new Modification(ModificationType.ADD, "altSecurityIdentities", "sshkey:" + " " + keyRsaTwo));
+ getDS().modify(DN_USER_ONE, new Modification(ModificationType.ADD, "altSecurityIdentities", "SSHKEY :" + "no-agent-forwarding " + keyDsaTwo));
+ getDS().modify(DN_USER_ONE, new Modification(ModificationType.ADD, "altSecurityIdentities", "pubkey: " + " command=\"sh /etc/netstart tun0 \" " + keyRsaThree));
+ getDS().modify(DN_USER_ONE, new Modification(ModificationType.ADD, "altSecurityIdentities", "pubkey: " + " command=\"netstat -nult\",environment=\"gb=\\\"What now\\\"\" " + keyDsaThree));
+ getDS().modify(DN_USER_ONE, new Modification(ModificationType.ADD, "altSecurityIdentities", "pubkey: " + "environment=\"SSH=git\",command=\"netstat -nult\",environment=\"gbPerms=VIEW\" " + keyEcThree));
+
+ getDS().modify(DN_USER_TWO, new Modification(ModificationType.ADD, "altSecurityIdentities", "SSHkey: " + "environment=\"gbPerm=R\" " + keyRsaOne));
+ getDS().modify(DN_USER_TWO, new Modification(ModificationType.ADD, "altSecurityIdentities", "SSHKey : " + " restrict,environment=\"gbPerm=V\",permitopen=\"sshkey: 220\" " + keyRsaTwo));
+ getDS().modify(DN_USER_TWO, new Modification(ModificationType.ADD, "altSecurityIdentities", "SSHkey: " + "permitopen=\"sshkey: 443\",restrict,environment=\"gbPerm=RW\",pty " + keyDsaTwo));
+ getDS().modify(DN_USER_TWO, new Modification(ModificationType.ADD, "altSecurityIdentities", "pubkey: " + "environment=\"gbPerm=CLONE\",permitopen=\"pubkey: 29184\",environment=\"X=\\\" Y \\\"\" " + keyRsaThree));
+ getDS().modify(DN_USER_TWO, new Modification(ModificationType.ADD, "altSecurityIdentities", "pubkey: " + " environment=\"A = B \",from=\"*.example.com,!pc.example.com\",environment=\"gbPerm=VIEW\" " + keyDsaThree));
+ getDS().modify(DN_USER_TWO, new Modification(ModificationType.ADD, "altSecurityIdentities", "pubkey: " + "environment=\"SSH=git\",environment=\"gbPerm=PUSH\",environemnt=\"XYZ='Ali Baba'\" " + keyEcThree));
+
+ getDS().modify(DN_USER_THREE, new Modification(ModificationType.ADD, "altSecurityIdentities", "SSHkey: " + "environment=\"gbPerm=R\",environment=\"josh=\\\"mean\\\"\",tunnel=\"0\" " + keyRsaOne));
+ getDS().modify(DN_USER_THREE, new Modification(ModificationType.ADD, "altSecurityIdentities", "SSHkey : " + " environment=\" gbPerm = V \" " + keyRsaTwo));
+ getDS().modify(DN_USER_THREE, new Modification(ModificationType.ADD, "altSecurityIdentities", "SSHkey: " + "command=\"sh echo \\\"Nope, not you! \\b (bell)\\\" \",user-rc,environment=\"gbPerm=RW\" " + keyDsaTwo));
+ getDS().modify(DN_USER_THREE, new Modification(ModificationType.ADD, "altSecurityIdentities", "pubkey: " + "environment=\"gbPerm=VIEW\",command=\"sh /etc/netstart tun0 \",environment=\"gbPerm=CLONE\",no-pty " + keyRsaThree));
+ getDS().modify(DN_USER_THREE, new Modification(ModificationType.ADD, "altSecurityIdentities", "pubkey: " + " command=\"netstat -nult\",environment=\"gbPerm=VIEW\" " + keyDsaThree));
+ getDS().modify(DN_USER_THREE, new Modification(ModificationType.ADD, "altSecurityIdentities", "pubkey: " + "environment=\"SSH=git\",command=\"netstat -nult\",environment=\"gbPerm=PUSH\" " + keyEcThree));
+
+ // Weird stuff, not to specification but shouldn't make it stumble.
+ getDS().modify(DN_USER_THREE, new Modification(ModificationType.ADD, "altSecurityIdentities", "opttest: " + "permitopen=host:443,command=,environment=\"gbPerm=CLONE\",no-pty= " + keyRsaThree));
+ getDS().modify(DN_USER_THREE, new Modification(ModificationType.ADD, "altSecurityIdentities", " opttest: " + " cmd=git,environment=\"gbPerm=\\\"VIEW\\\"\" " + keyDsaThree));
+ getDS().modify(DN_USER_THREE, new Modification(ModificationType.ADD, "altSecurityIdentities", " opttest:" + "environment=,command=netstat,environment=gbperm=push " + keyEcThree));
+
+
+ LdapKeyManager kmgr = new LdapKeyManager(settings);
+
+ settings.put(Keys.realm.ldap.sshPublicKey, "altSecurityIdentities:SSHkey");
+
+ List<SshKey> keys = kmgr.getKeys("UserOne");
+ assertNotNull(keys);
+ assertEquals(2, keys.size());
+ int seen = 0;
+ for (SshKey key : keys) {
+ assertEquals(AccessPermission.PUSH, key.getPermission());
+ if (keyRsaOne.equals(key.getRawData())) {
+ seen += 1 << 0;
+ }
+ else if (keyRsaTwo.equals(key.getRawData())) {
+ seen += 1 << 1;
+ }
+ else if (keyDsaTwo.equals(key.getRawData())) {
+ seen += 1 << 2;
+ }
+ else if (keyRsaThree.equals(key.getRawData())) {
+ seen += 1 << 3;
+ }
+ else if (keyDsaThree.equals(key.getRawData())) {
+ seen += 1 << 4;
+ }
+ else if (keyEcThree.equals(key.getRawData())) {
+ seen += 1 << 5;
+ }
+ }
+ assertEquals(6, seen);
+
+ keys = kmgr.getKeys("UserTwo");
+ assertNotNull(keys);
+ assertEquals(3, keys.size());
+ seen = 0;
+ for (SshKey key : keys) {
+ if (keyRsaOne.equals(key.getRawData())) {
+ assertEquals(AccessPermission.CLONE, key.getPermission());
+ seen += 1 << 0;
+ }
+ else if (keyRsaTwo.equals(key.getRawData())) {
+ assertEquals(AccessPermission.VIEW, key.getPermission());
+ seen += 1 << 1;
+ }
+ else if (keyDsaTwo.equals(key.getRawData())) {
+ assertEquals(AccessPermission.PUSH, key.getPermission());
+ seen += 1 << 2;
+ }
+ else if (keyRsaThree.equals(key.getRawData())) {
+ assertEquals(AccessPermission.CLONE, key.getPermission());
+ seen += 1 << 3;
+ }
+ else if (keyDsaThree.equals(key.getRawData())) {
+ assertEquals(AccessPermission.VIEW, key.getPermission());
+ seen += 1 << 4;
+ }
+ else if (keyEcThree.equals(key.getRawData())) {
+ assertEquals(AccessPermission.PUSH, key.getPermission());
+ seen += 1 << 5;
+ }
+ }
+ assertEquals(7, seen);
+
+ keys = kmgr.getKeys("UserThree");
+ assertNotNull(keys);
+ assertEquals(3, keys.size());
+ seen = 0;
+ for (SshKey key : keys) {
+ if (keyRsaOne.equals(key.getRawData())) {
+ assertEquals(AccessPermission.CLONE, key.getPermission());
+ seen += 1 << 0;
+ }
+ else if (keyRsaTwo.equals(key.getRawData())) {
+ assertEquals(AccessPermission.VIEW, key.getPermission());
+ seen += 1 << 1;
+ }
+ else if (keyDsaTwo.equals(key.getRawData())) {
+ assertEquals(AccessPermission.PUSH, key.getPermission());
+ seen += 1 << 2;
+ }
+ else if (keyRsaThree.equals(key.getRawData())) {
+ assertEquals(AccessPermission.CLONE, key.getPermission());
+ seen += 1 << 3;
+ }
+ else if (keyDsaThree.equals(key.getRawData())) {
+ assertEquals(AccessPermission.VIEW, key.getPermission());
+ seen += 1 << 4;
+ }
+ else if (keyEcThree.equals(key.getRawData())) {
+ assertEquals(AccessPermission.PUSH, key.getPermission());
+ seen += 1 << 5;
+ }
+ }
+ assertEquals(7, seen);
+
+
+
+ settings.put(Keys.realm.ldap.sshPublicKey, "altSecurityIdentities:pubKey");
+
+ keys = kmgr.getKeys("UserOne");
+ assertNotNull(keys);
+ assertEquals(3, keys.size());
+ seen = 0;
+ for (SshKey key : keys) {
+ assertEquals(AccessPermission.PUSH, key.getPermission());
+ if (keyRsaOne.equals(key.getRawData())) {
+ seen += 1 << 0;
+ }
+ else if (keyRsaTwo.equals(key.getRawData())) {
+ seen += 1 << 1;
+ }
+ else if (keyDsaTwo.equals(key.getRawData())) {
+ seen += 1 << 2;
+ }
+ else if (keyRsaThree.equals(key.getRawData())) {
+ seen += 1 << 3;
+ }
+ else if (keyDsaThree.equals(key.getRawData())) {
+ seen += 1 << 4;
+ }
+ else if (keyEcThree.equals(key.getRawData())) {
+ seen += 1 << 5;
+ }
+ }
+ assertEquals(56, seen);
+
+ keys = kmgr.getKeys("UserTwo");
+ assertNotNull(keys);
+ assertEquals(3, keys.size());
+ seen = 0;
+ for (SshKey key : keys) {
+ if (keyRsaOne.equals(key.getRawData())) {
+ assertEquals(AccessPermission.CLONE, key.getPermission());
+ seen += 1 << 0;
+ }
+ else if (keyRsaTwo.equals(key.getRawData())) {
+ assertEquals(AccessPermission.VIEW, key.getPermission());
+ seen += 1 << 1;
+ }
+ else if (keyDsaTwo.equals(key.getRawData())) {
+ assertEquals(AccessPermission.PUSH, key.getPermission());
+ seen += 1 << 2;
+ }
+ else if (keyRsaThree.equals(key.getRawData())) {
+ assertEquals(AccessPermission.CLONE, key.getPermission());
+ seen += 1 << 3;
+ }
+ else if (keyDsaThree.equals(key.getRawData())) {
+ assertEquals(AccessPermission.VIEW, key.getPermission());
+ seen += 1 << 4;
+ }
+ else if (keyEcThree.equals(key.getRawData())) {
+ assertEquals(AccessPermission.PUSH, key.getPermission());
+ seen += 1 << 5;
+ }
+ }
+ assertEquals(56, seen);
+
+ keys = kmgr.getKeys("UserThree");
+ assertNotNull(keys);
+ assertEquals(3, keys.size());
+ seen = 0;
+ for (SshKey key : keys) {
+ if (keyRsaOne.equals(key.getRawData())) {
+ assertEquals(AccessPermission.CLONE, key.getPermission());
+ seen += 1 << 0;
+ }
+ else if (keyRsaTwo.equals(key.getRawData())) {
+ assertEquals(AccessPermission.VIEW, key.getPermission());
+ seen += 1 << 1;
+ }
+ else if (keyDsaTwo.equals(key.getRawData())) {
+ assertEquals(AccessPermission.PUSH, key.getPermission());
+ seen += 1 << 2;
+ }
+ else if (keyRsaThree.equals(key.getRawData())) {
+ assertEquals(AccessPermission.CLONE, key.getPermission());
+ seen += 1 << 3;
+ }
+ else if (keyDsaThree.equals(key.getRawData())) {
+ assertEquals(AccessPermission.VIEW, key.getPermission());
+ seen += 1 << 4;
+ }
+ else if (keyEcThree.equals(key.getRawData())) {
+ assertEquals(AccessPermission.PUSH, key.getPermission());
+ seen += 1 << 5;
+ }
+ }
+ assertEquals(56, seen);
+
+
+ settings.put(Keys.realm.ldap.sshPublicKey, "altSecurityIdentities:opttest");
+ keys = kmgr.getKeys("UserThree");
+ assertNotNull(keys);
+ assertEquals(3, keys.size());
+ seen = 0;
+ for (SshKey key : keys) {
+ if (keyRsaOne.equals(key.getRawData())) {
+ assertEquals(AccessPermission.CLONE, key.getPermission());
+ seen += 1 << 0;
+ }
+ else if (keyRsaTwo.equals(key.getRawData())) {
+ assertEquals(AccessPermission.VIEW, key.getPermission());
+ seen += 1 << 1;
+ }
+ else if (keyDsaTwo.equals(key.getRawData())) {
+ assertEquals(AccessPermission.PUSH, key.getPermission());
+ seen += 1 << 2;
+ }
+ else if (keyRsaThree.equals(key.getRawData())) {
+ assertEquals(AccessPermission.CLONE, key.getPermission());
+ seen += 1 << 3;
+ }
+ else if (keyDsaThree.equals(key.getRawData())) {
+ assertEquals(AccessPermission.VIEW, key.getPermission());
+ seen += 1 << 4;
+ }
+ else if (keyEcThree.equals(key.getRawData())) {
+ assertEquals(AccessPermission.PUSH, key.getPermission());
+ seen += 1 << 5;
+ }
+ }
+ assertEquals(56, seen);
+
+ }
+
+
+ @Test
+ public void testKeyValidity() throws LDAPException, GeneralSecurityException {
+ LdapKeyManager kmgr = new LdapKeyManager(settings);
+
+ String comment = "UserTwo@example.com";
+ String keyDsaTwo = getDsaPubKey(comment);
+ getDS().modify(DN_USER_TWO, new Modification(ModificationType.ADD, "sshPublicKey", keyDsaTwo));
+
+
+ List<SshKey> keys = kmgr.getKeys("UserTwo");
+ assertNotNull(keys);
+ assertEquals(1, keys.size());
+ SshKey sshKey = keys.get(0);
+ assertEquals(keyDsaTwo, sshKey.getRawData());
+
+ Signature signature = SecurityUtils.getSignature("DSA");
+ signature.initSign(getDsaKeyPair(comment).getPrivate());
+ byte[] message = comment.getBytes();
+ signature.update(message);
+ byte[] sigBytes = signature.sign();
+
+ signature.initVerify(sshKey.getPublicKey());
+ signature.update(message);
+ assertTrue("Verify failed with retrieved SSH key.", signature.verify(sigBytes));
+ }
+
+
+
+
+
+
+
+
+ private KeyPair getDsaKeyPair(String comment) {
+ return getKeyPair("DSA", comment, dsaGenerator);
+ }
+
+ private KeyPair getKeyPair(String type, String comment, KeyPairGenerator generator) {
+ String kpkey = type + ":" + comment;
+ KeyPair kp = keyPairs.get(kpkey);
+ if (kp == null) {
+ if ("EC".equals(type)) {
+ ECGenParameterSpec ecSpec = new ECGenParameterSpec("P-384");
+ try {
+ ecGenerator.initialize(ecSpec);
+ } catch (InvalidAlgorithmParameterException e) {
+ kp = generator.generateKeyPair();
+ e.printStackTrace();
+ }
+ kp = ecGenerator.generateKeyPair();
+ } else {
+ kp = generator.generateKeyPair();
+ }
+ keyPairs.put(kpkey, kp);
+ }
+
+ return kp;
+ }
+
+
+ private String getRsaPubKey(String comment) {
+ return getPubKey("RSA", comment, rsaGenerator);
+ }
+
+ private String getDsaPubKey(String comment) {
+ return getPubKey("DSA", comment, dsaGenerator);
+ }
+
+ private String getEcPubKey(String comment) {
+ return getPubKey("EC", comment, ecGenerator);
+ }
+
+ private String getPubKey(String type, String comment, KeyPairGenerator generator) {
+ KeyPair kp = getKeyPair(type, comment, generator);
+ if (kp == null) {
+ return null;
+ }
+
+ SshKey sk = new SshKey(kp.getPublic());
+ sk.setComment(comment);
+ return sk.getRawData();
+ }
+
+}
diff --git a/src/test/java/com/gitblit/tests/MarkdownUtilsTest.java b/src/test/java/com/gitblit/tests/MarkdownUtilsTest.java
index e40f1057..bc7aad49 100644
--- a/src/test/java/com/gitblit/tests/MarkdownUtilsTest.java
+++ b/src/test/java/com/gitblit/tests/MarkdownUtilsTest.java
@@ -15,8 +15,14 @@
*/
package com.gitblit.tests;
+import java.util.HashMap;
+import java.util.Map;
+
import org.junit.Test;
+import com.gitblit.IStoredSettings;
+import com.gitblit.Keys;
+import com.gitblit.tests.mock.MemorySettings;
import com.gitblit.utils.MarkdownUtils;
public class MarkdownUtilsTest extends GitblitUnitTest {
@@ -39,4 +45,70 @@ public class MarkdownUtilsTest extends GitblitUnitTest {
assertEquals("<table><tr><td>&lt;test&gt;</td></tr></table>",
MarkdownUtils.transformMarkdown("<table><tr><td>&lt;test&gt;</td></tr></table>"));
}
-} \ No newline at end of file
+
+
+ @Test
+ public void testUserMentions() {
+ IStoredSettings settings = getSettings();
+ String repositoryName = "test3";
+ String mentionHtml = "<strong><a href=\"http://localhost/user/%1$s\">@%1$s</a></strong>";
+
+ String input = "@j.doe";
+ String output = "<p>" + String.format(mentionHtml, "j.doe") + "</p>";
+ assertEquals(output, MarkdownUtils.transformGFM(settings, input, repositoryName));
+
+ input = " @j.doe";
+ output = "<p>" + String.format(mentionHtml, "j.doe") + "</p>";
+ assertEquals(output, MarkdownUtils.transformGFM(settings, input, repositoryName));
+
+ input = "@j.doe.";
+ output = "<p>" + String.format(mentionHtml, "j.doe") + ".</p>";
+ assertEquals(output, MarkdownUtils.transformGFM(settings, input, repositoryName));
+
+ input = "To @j.doe: ask @jim.beam!";
+ output = "<p>To " + String.format(mentionHtml, "j.doe")
+ + ": ask " + String.format(mentionHtml, "jim.beam") + "!</p>";
+ assertEquals(output, MarkdownUtils.transformGFM(settings, input, repositoryName));
+
+ input = "@sta.rt\n"
+ + "\n"
+ + "User mentions in tickets are broken.\n"
+ + "So:\n"
+ + "@mc_guyver can fix this.\n"
+ + "@j.doe, can you test after the fix by @m+guyver?\n"
+ + "Please review this, @jim.beam!\n"
+ + "Was reported by @jill and @j!doe from jane@doe yesterday.\n"
+ + "\n"
+ + "@jack.daniels can vote for john@wayne.name hopefully.\n"
+ + "@en.de";
+ output = "<p>" + String.format(mentionHtml, "sta.rt") + "</p>"
+ + "<p>" + "User mentions in tickets are broken.<br/>"
+ + "So:<br/>"
+ + String.format(mentionHtml, "mc_guyver") + " can fix this.<br/>"
+ + String.format(mentionHtml, "j.doe") + ", can you test after the fix by " + String.format(mentionHtml, "m+guyver") + "?<br/>"
+ + "Please review this, " + String.format(mentionHtml, "jim.beam") + "!<br/>"
+ + "Was reported by " + String.format(mentionHtml, "jill")
+ + " and " + String.format(mentionHtml, "j!doe")
+ + " from <a href=\"mailto:&#106;a&#110;&#x65;&#x40;&#x64;&#x6f;&#101;\">&#106;a&#110;&#x65;&#x40;&#x64;&#x6f;&#101;</a> yesterday."
+ + "</p>"
+ + "<p>" + String.format(mentionHtml, "jack.daniels") + " can vote for "
+ + "<a href=\"mailto:&#x6a;&#x6f;h&#110;&#x40;&#119;a&#121;&#110;&#101;.&#110;a&#x6d;&#101;\">&#x6a;&#x6f;h&#110;&#x40;&#119;a&#121;&#110;&#101;.&#110;a&#x6d;&#101;</a> hopefully.<br/>"
+ + String.format(mentionHtml, "en.de")
+ + "</p>";
+ assertEquals(output, MarkdownUtils.transformGFM(settings, input, repositoryName));
+
+ }
+
+
+
+
+ private MemorySettings getSettings() {
+ Map<String, Object> backingMap = new HashMap<String, Object>();
+
+ backingMap.put(Keys.web.canonicalUrl, "http://localhost");
+ backingMap.put(Keys.web.shortCommitIdLength, "7");
+
+ MemorySettings ms = new MemorySettings(backingMap);
+ return ms;
+ }
+}
diff --git a/src/test/java/com/gitblit/tests/MetricUtilsTest.java b/src/test/java/com/gitblit/tests/MetricUtilsTest.java
index 4d620ae6..c5b943d4 100644
--- a/src/test/java/com/gitblit/tests/MetricUtilsTest.java
+++ b/src/test/java/com/gitblit/tests/MetricUtilsTest.java
@@ -45,7 +45,7 @@ public class MetricUtilsTest extends GitblitUnitTest {
List<Metric> byEmail = MetricUtils.getAuthorMetrics(repository, null, true);
List<Metric> byName = MetricUtils.getAuthorMetrics(repository, null, false);
repository.close();
- assertEquals("No author metrics found!", 9, byEmail.size());
- assertEquals("No author metrics found!", 8, byName.size());
+ assertEquals("No author metrics found!", GitBlitSuite.helloworldSettings.getInteger(HelloworldKeys.users.byEmail, -1), byEmail.size());
+ assertEquals("No author metrics found!", GitBlitSuite.helloworldSettings.getInteger(HelloworldKeys.users.byName, -1), byName.size());
}
} \ No newline at end of file
diff --git a/src/test/java/com/gitblit/tests/PushLogTest.java b/src/test/java/com/gitblit/tests/PushLogTest.java
deleted file mode 100644
index be097cc4..00000000
--- a/src/test/java/com/gitblit/tests/PushLogTest.java
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Copyright 2013 gitblit.com.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.gitblit.tests;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.List;
-
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.lib.RepositoryCache.FileKey;
-import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
-import org.eclipse.jgit.util.FS;
-import org.junit.Test;
-
-import com.gitblit.models.RefLogEntry;
-import com.gitblit.utils.RefLogUtils;
-
-public class PushLogTest extends GitblitUnitTest {
-
- @Test
- public void testPushLog() throws IOException {
- String name = "~james/helloworld.git";
- File gitDir = FileKey.resolve(new File(GitBlitSuite.REPOSITORIES, name), FS.DETECTED);
- Repository repository = new FileRepositoryBuilder().setGitDir(gitDir).build();
- List<RefLogEntry> pushes = RefLogUtils.getRefLog(name, repository);
- GitBlitSuite.close(repository);
- }
-} \ No newline at end of file
diff --git a/src/test/java/com/gitblit/tests/RedisTicketServiceTest.java b/src/test/java/com/gitblit/tests/RedisTicketServiceTest.java
index 48011ade..12b7917e 100644
--- a/src/test/java/com/gitblit/tests/RedisTicketServiceTest.java
+++ b/src/test/java/com/gitblit/tests/RedisTicketServiceTest.java
@@ -15,6 +15,8 @@
*/
package com.gitblit.tests;
+import org.junit.Ignore;
+
import com.gitblit.IStoredSettings;
import com.gitblit.Keys;
import com.gitblit.manager.INotificationManager;
@@ -39,6 +41,7 @@ import com.gitblit.utils.XssFilter.AllowXssFilter;
* @author James Moger
*
*/
+@Ignore("Redis tests currently broken, no service running.")
public class RedisTicketServiceTest extends TicketServiceTest {
final RepositoryModel repo = new RepositoryModel("tickets/redis.git", null, null, null);
@@ -66,7 +69,7 @@ public class RedisTicketServiceTest extends TicketServiceTest {
IUserManager userManager = new UserManager(runtimeManager, pluginManager).start();
IRepositoryManager repositoryManager = new RepositoryManager(runtimeManager, pluginManager, userManager).start();
- RedisTicketService service = new RedisTicketService(
+ RedisTicketService service = (RedisTicketService) new RedisTicketService(
runtimeManager,
pluginManager,
notificationManager,
diff --git a/src/test/java/com/gitblit/tests/RpcTests.java b/src/test/java/com/gitblit/tests/RpcTests.java
index 51b4671b..4c794656 100644
--- a/src/test/java/com/gitblit/tests/RpcTests.java
+++ b/src/test/java/com/gitblit/tests/RpcTests.java
@@ -68,6 +68,11 @@ public class RpcTests extends GitblitUnitTest {
@AfterClass
public static void stopGitblit() throws Exception {
+ //clean up the "A-Team" if left over
+ TeamModel aTeam = new TeamModel("A-Team");
+ aTeam.addRepositoryPermission("helloworld.git");
+ RpcUtils.deleteTeam(aTeam, GitBlitSuite.url, GitBlitSuite.account, GitBlitSuite.password.toCharArray());
+
if (started.get()) {
GitBlitSuite.stopGitblit();
}
@@ -265,11 +270,17 @@ public class RpcTests extends GitblitUnitTest {
@Test
public void testTeamAdministration() throws IOException {
+ //clean up the "A-Team" left over from previous run, if any
+ TeamModel aTeam = new TeamModel("A-Team");
+ aTeam.addRepositoryPermission("helloworld.git");
+ RpcUtils.deleteTeam(aTeam, url, account, password.toCharArray());
+
List<TeamModel> teams = RpcUtils.getTeams(url, account, password.toCharArray());
- assertEquals(1, teams.size());
+ //should be just the admins team
+ assertEquals("In addition to 'admins', too many left-over team(s) in Gitblit server: " + teams, 1, teams.size());
// Create the A-Team
- TeamModel aTeam = new TeamModel("A-Team");
+ aTeam = new TeamModel("A-Team");
aTeam.users.add("admin");
aTeam.addRepositoryPermission("helloworld.git");
assertTrue(RpcUtils.createTeam(aTeam, url, account, password.toCharArray()));
diff --git a/src/test/java/com/gitblit/tests/SshDaemonTest.java b/src/test/java/com/gitblit/tests/SshDaemonTest.java
index c5deb7d5..c7d06198 100644
--- a/src/test/java/com/gitblit/tests/SshDaemonTest.java
+++ b/src/test/java/com/gitblit/tests/SshDaemonTest.java
@@ -44,9 +44,9 @@ public class SshDaemonTest extends SshUnitTest {
@Test
public void testPublicKeyAuthentication() throws Exception {
SshClient client = getClient();
- ClientSession session = client.connect(username, "localhost", GitBlitSuite.sshPort).await().getSession();
+ ClientSession session = client.connect(username, "localhost", GitBlitSuite.sshPort).verify().getSession();
session.addPublicKeyIdentity(rwKeyPair);
- assertTrue(session.auth().await().isSuccess());
+ assertTrue(session.auth().await());
}
@Test
@@ -64,6 +64,7 @@ public class SshDaemonTest extends SshUnitTest {
// set clone restriction
RepositoryModel model = repositories().getRepositoryModel("ticgit.git");
+ assertNotNull("Could not get repository modle for ticgit.git", model);
model.accessRestriction = AccessRestrictionType.CLONE;
model.authorizationControl = AuthorizationControl.NAMED;
repositories().updateRepositoryModel(model.name, model, false);
diff --git a/src/test/java/com/gitblit/tests/SshKeysDispatcherTest.java b/src/test/java/com/gitblit/tests/SshKeysDispatcherTest.java
index 23e61795..4784e468 100644
--- a/src/test/java/com/gitblit/tests/SshKeysDispatcherTest.java
+++ b/src/test/java/com/gitblit/tests/SshKeysDispatcherTest.java
@@ -37,7 +37,7 @@ public class SshKeysDispatcherTest extends SshUnitTest {
String result = testSshCommand("keys ls -L");
List<SshKey> keys = getKeyManager().getKeys(username);
assertEquals(String.format("There are %d keys!", keys.size()), 2, keys.size());
- assertEquals(keys.get(0).getRawData() + "\n" + keys.get(1).getRawData(), result);
+ assertEquals(String.format("%s%n%s", keys.get(0).getRawData(), keys.get(1).getRawData()), result);
}
@Test
@@ -64,9 +64,9 @@ public class SshKeysDispatcherTest extends SshUnitTest {
assertEquals(String.format("There are %d keys!", keys.size()), 0, keys.size());
try {
testSshCommand("keys ls -L");
- assertTrue("Authentication worked without a public key?!", false);
+ fail("Authentication worked without a public key?!");
} catch (AssertionError e) {
- assertTrue(true);
+ // expected
}
}
@@ -77,9 +77,9 @@ public class SshKeysDispatcherTest extends SshUnitTest {
assertEquals(String.format("There are %d keys!", keys.size()), 0, keys.size());
try {
testSshCommand("keys ls -L");
- assertTrue("Authentication worked without a public key?!", false);
+ fail("Authentication worked without a public key?!");
} catch (AssertionError e) {
- assertTrue(true);
+ // expected
}
}
@@ -96,9 +96,9 @@ public class SshKeysDispatcherTest extends SshUnitTest {
StringBuilder sb = new StringBuilder();
for (SshKey sk : keys) {
sb.append(sk.getRawData());
- sb.append('\n');
+ sb.append(System.getProperty("line.separator", "\n"));
}
- sb.setLength(sb.length() - 1);
+ sb.setLength(sb.length() - System.getProperty("line.separator", "\n").length());
assertEquals(sb.toString(), result);
}
diff --git a/src/test/java/com/gitblit/tests/SshUnitTest.java b/src/test/java/com/gitblit/tests/SshUnitTest.java
index 27b4ec73..2f65fe99 100644
--- a/src/test/java/com/gitblit/tests/SshUnitTest.java
+++ b/src/test/java/com/gitblit/tests/SshUnitTest.java
@@ -21,16 +21,26 @@ import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.SocketAddress;
+import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PublicKey;
+import java.util.EnumSet;
import java.util.concurrent.atomic.AtomicBoolean;
-import org.apache.sshd.client.ServerKeyVerifier;
import org.apache.sshd.client.SshClient;
import org.apache.sshd.client.channel.ClientChannel;
+import org.apache.sshd.client.channel.ClientChannelEvent;
+import org.apache.sshd.client.config.keys.ClientIdentityLoader;
+import org.apache.sshd.client.future.AuthFuture;
+import org.apache.sshd.client.keyverifier.ServerKeyVerifier;
import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.common.config.keys.FilePasswordProvider;
import org.apache.sshd.common.util.SecurityUtils;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+import org.eclipse.jgit.util.SystemReader;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
@@ -57,6 +67,57 @@ public abstract class SshUnitTest extends GitblitUnitTest {
public static void startGitblit() throws Exception {
generator = SecurityUtils.getKeyPairGenerator("RSA");
started.set(GitBlitSuite.startGitblit());
+
+ final SystemReader dsr = SystemReader.getInstance();
+ SystemReader.setInstance(new SystemReader()
+ {
+ final SystemReader defaultsr = dsr;
+
+ @Override
+ public String getHostname()
+ {
+ return defaultsr.getHostname();
+ }
+
+ @Override
+ public String getenv(String variable)
+ {
+ if ("GIT_SSH".equalsIgnoreCase(variable)) {
+ return null;
+ }
+ return defaultsr.getenv(variable);
+ }
+
+ @Override
+ public String getProperty(String key)
+ {
+ return defaultsr.getProperty(key);
+ }
+
+ @Override
+ public FileBasedConfig openUserConfig(Config parent, FS fs)
+ {
+ return defaultsr.openUserConfig(parent, fs);
+ }
+
+ @Override
+ public FileBasedConfig openSystemConfig(Config parent, FS fs)
+ {
+ return defaultsr.openSystemConfig(parent, fs);
+ }
+
+ @Override
+ public long getCurrentTime()
+ {
+ return defaultsr.getCurrentTime();
+ }
+
+ @Override
+ public int getTimezone(long when)
+ {
+ return defaultsr.getTimezone(when);
+ }
+ });
}
@AfterClass
@@ -96,6 +157,16 @@ public abstract class SshUnitTest extends GitblitUnitTest {
protected SshClient getClient() {
SshClient client = SshClient.setUpDefaultClient();
+ client.setClientIdentityLoader(new ClientIdentityLoader() { // Ignore the files under ~/.ssh
+ @Override
+ public boolean isValidLocation(String location) throws IOException {
+ return true;
+ }
+ @Override
+ public KeyPair loadClientIdentity(String location, FilePasswordProvider provider) throws IOException, GeneralSecurityException {
+ return null;
+ }
+ });
client.setServerKeyVerifier(new ServerKeyVerifier() {
@Override
public boolean verifyServerKey(ClientSession sshClientSession, SocketAddress remoteAddress, PublicKey serverKey) {
@@ -112,9 +183,11 @@ public abstract class SshUnitTest extends GitblitUnitTest {
protected String testSshCommand(String cmd, String stdin) throws IOException, InterruptedException {
SshClient client = getClient();
- ClientSession session = client.connect(username, "localhost", GitBlitSuite.sshPort).await().getSession();
+ ClientSession session = client.connect(username, "localhost", GitBlitSuite.sshPort).verify().getSession();
session.addPublicKeyIdentity(rwKeyPair);
- assertTrue(session.auth().await().isSuccess());
+ AuthFuture authFuture = session.auth();
+ assertTrue(authFuture.await());
+ assertTrue(authFuture.isSuccess());
ClientChannel channel = session.createChannel(ClientChannel.CHANNEL_EXEC, cmd);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
@@ -131,7 +204,7 @@ public abstract class SshUnitTest extends GitblitUnitTest {
channel.setErr(err);
channel.open();
- channel.waitFor(ClientChannel.CLOSED, 0);
+ channel.waitFor(EnumSet.of(ClientChannelEvent.CLOSED, ClientChannelEvent.EOF), 0);
String result = out.toString().trim();
channel.close(false);
diff --git a/src/test/java/com/gitblit/tests/StringUtilsTest.java b/src/test/java/com/gitblit/tests/StringUtilsTest.java
index 7176b88c..cc579888 100644
--- a/src/test/java/com/gitblit/tests/StringUtilsTest.java
+++ b/src/test/java/com/gitblit/tests/StringUtilsTest.java
@@ -26,13 +26,26 @@ public class StringUtilsTest extends GitblitUnitTest {
@Test
public void testIsEmpty() throws Exception {
- assertTrue(StringUtils.isEmpty(null));
+ assertTrue(StringUtils.isEmpty((String)null));
assertTrue(StringUtils.isEmpty(""));
assertTrue(StringUtils.isEmpty(" "));
assertFalse(StringUtils.isEmpty("A"));
}
@Test
+ public void testIsEmptyCharArray() throws Exception {
+ assertTrue(StringUtils.isEmpty((char[])null));
+ assertTrue(StringUtils.isEmpty(new char[0]));
+ assertTrue(StringUtils.isEmpty(new char[]{ ' ' }));
+ assertTrue(StringUtils.isEmpty(new char[]{ ' '}));
+ assertTrue(StringUtils.isEmpty(new char[]{ ' ', ' ' }));
+ assertTrue(StringUtils.isEmpty(new char[]{ ' ', ' ', ' ' }));
+ assertFalse(StringUtils.isEmpty(new char[]{ '\u0020', 'f' }));
+ assertFalse(StringUtils.isEmpty(new char[]{ '\u0148', '\u0020' }));
+ assertFalse(StringUtils.isEmpty(new char[]{ 'A' }));
+ }
+
+ @Test
public void testBreakLinesForHtml() throws Exception {
String input = "this\nis\r\na\rtest\r\n\r\nof\n\nline\r\rbreaking";
String output = "this<br/>is<br/>a<br/>test<br/><br/>of<br/><br/>line<br/><br/>breaking";
@@ -48,11 +61,39 @@ public class StringUtilsTest extends GitblitUnitTest {
@Test
public void testEscapeForHtml() throws Exception {
- String input = "& < > \" \t";
- String outputNoChange = "&amp; &lt; &gt; &quot; \t";
- String outputChange = "&amp;&nbsp;&lt;&nbsp;&gt;&nbsp;&quot;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;";
+ String input = "\t & < > \"";
+ String outputNoChange = "\t &amp; &lt; &gt; &quot;";
+ String outputChange = "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&amp;&nbsp;&lt;&nbsp;&gt;&nbsp;&quot;";
assertEquals(outputNoChange, StringUtils.escapeForHtml(input, false));
assertEquals(outputChange, StringUtils.escapeForHtml(input, true));
+
+ input = "a\tb";
+ outputNoChange = "a\tb";
+ outputChange = "a&nbsp;&nbsp;&nbsp;b";
+ assertEquals(outputNoChange, StringUtils.escapeForHtml(input, false));
+ assertEquals(outputChange, StringUtils.escapeForHtml(input, true));
+
+ input = "\ta b\t";
+ outputNoChange = "\ta b\t";
+ outputChange = "&nbsp;&nbsp;&nbsp;&nbsp;a&nbsp;b&nbsp;";
+ assertEquals(outputNoChange, StringUtils.escapeForHtml(input, false));
+ assertEquals(outputChange, StringUtils.escapeForHtml(input, true));
+
+ input = "\t <> \t";
+ outputNoChange = "\t &lt;&gt; \t";
+ outputChange = "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;&gt;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;";
+ assertEquals(outputNoChange, StringUtils.escapeForHtml(input, false));
+ assertEquals(outputChange, StringUtils.escapeForHtml(input, true));
+
+ String tabs = "\t";
+ int tabSpaces;
+ int expectedLength;
+ for (int i = 0; i < 50; i++) {
+ tabSpaces = 4 - i % 4;
+ expectedLength = (i + tabSpaces) * 6; // &nbsp; = 6 chars
+ assertEquals(expectedLength, StringUtils.escapeForHtml(tabs, true).length());
+ tabs = " " + tabs;
+ }
}
@Test
diff --git a/src/test/java/com/gitblit/tests/TicketReferenceTest.java b/src/test/java/com/gitblit/tests/TicketReferenceTest.java
index 59e00c53..827ba59d 100644
--- a/src/test/java/com/gitblit/tests/TicketReferenceTest.java
+++ b/src/test/java/com/gitblit/tests/TicketReferenceTest.java
@@ -57,6 +57,7 @@ import com.gitblit.tickets.ITicketService;
*/
public class TicketReferenceTest extends GitblitUnitTest {
+ static final String repoName = "TicketReferenceTest.git";
static File workingCopy = new File(GitBlitSuite.REPOSITORIES, "working/TicketReferenceTest.git-wc");
static ITicketService ticketService;
@@ -73,13 +74,13 @@ public class TicketReferenceTest extends GitblitUnitTest {
@BeforeClass
public static void configure() throws Exception {
- File repositoryName = new File("TicketReferenceTest.git");;
+ File repositoryName = new File(repoName);
GitBlitSuite.close(repositoryName);
if (repositoryName.exists()) {
FileUtils.delete(repositoryName, FileUtils.RECURSIVE | FileUtils.RETRY);
}
- repo = new RepositoryModel("TicketReferenceTest.git", null, null, null);
+ repo = new RepositoryModel(repoName, null, null, null);
if (gitblit().hasRepository(repo.name)) {
gitblit().deleteRepositoryModel(repo);
@@ -141,7 +142,23 @@ public class TicketReferenceTest extends GitblitUnitTest {
@AfterClass
public static void cleanup() throws Exception {
+ //clean up the test user account if left over
+ if (gitblit().getUserModel(user.username) != null) {
+ gitblit().deleteUser(user.username);
+ }
+
GitBlitSuite.close(git);
+
+ //clean up the TicketReferenceTest.git repo
+ File repositoryName = new File(repoName);
+ GitBlitSuite.close(repositoryName);
+ if (repositoryName.exists()) {
+ FileUtils.delete(repositoryName, FileUtils.RECURSIVE | FileUtils.RETRY);
+ }
+ RepositoryModel repo = new RepositoryModel(repoName, null, null, null);
+ if (gitblit().hasRepository(repo.name)) {
+ gitblit().deleteRepositoryModel(repo);
+ }
}
@Test
diff --git a/src/test/java/com/gitblit/tests/TicketServiceTest.java b/src/test/java/com/gitblit/tests/TicketServiceTest.java
index c654383d..c0d93531 100644
--- a/src/test/java/com/gitblit/tests/TicketServiceTest.java
+++ b/src/test/java/com/gitblit/tests/TicketServiceTest.java
@@ -21,8 +21,8 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import org.apache.commons.io.FileUtils;
import org.bouncycastle.util.Arrays;
+import org.eclipse.jgit.util.FileUtils;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -65,7 +65,9 @@ public abstract class TicketServiceTest extends GitblitUnitTest {
protected IStoredSettings getSettings(boolean deleteAll) throws Exception {
File dir = new File(GitBlitSuite.REPOSITORIES, getRepository().name);
if (deleteAll) {
- FileUtils.deleteDirectory(dir);
+ if (dir.exists()) {
+ FileUtils.delete(dir, FileUtils.RECURSIVE | FileUtils.RETRY);
+ }
JGitUtils.createRepository(GitBlitSuite.REPOSITORIES, getRepository().name).close();
}
diff --git a/src/test/java/com/gitblit/tests/TimeUtilsTest.java b/src/test/java/com/gitblit/tests/TimeUtilsTest.java
deleted file mode 100644
index ef506ef4..00000000
--- a/src/test/java/com/gitblit/tests/TimeUtilsTest.java
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * 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.tests;
-
-import java.util.Date;
-
-import org.junit.Test;
-
-import com.gitblit.utils.TimeUtils;
-
-public class TimeUtilsTest extends GitblitUnitTest {
-
- private Date offset(long subtract) {
- return new Date(System.currentTimeMillis() - subtract);
- }
-
- @Test
- public void testBasicTimeFunctions() throws Exception {
- assertEquals(2, TimeUtils.minutesAgo(offset(2 * TimeUtils.MIN), false));
- assertEquals(3, TimeUtils.minutesAgo(offset((2 * TimeUtils.MIN) + (35 * 1000L)), true));
-
- assertEquals(2, TimeUtils.hoursAgo(offset(2 * TimeUtils.ONEHOUR), false));
- assertEquals(3, TimeUtils.hoursAgo(offset(5 * TimeUtils.HALFHOUR), true));
-
- assertEquals(4, TimeUtils.daysAgo(offset(4 * TimeUtils.ONEDAY)));
- }
-
- @Test
- public void testToday() throws Exception {
- assertTrue(TimeUtils.isToday(new Date(), null));
- }
-
- @Test
- public void testYesterday() throws Exception {
- assertTrue(TimeUtils.isYesterday(offset(TimeUtils.ONEDAY), null));
- }
-
- @Test
- public void testDurations() throws Exception {
- TimeUtils timeUtils = new TimeUtils();
- assertEquals("1 day", timeUtils.duration(1));
- assertEquals("5 days", timeUtils.duration(5));
- assertEquals("3 months", timeUtils.duration(75));
- assertEquals("12 months", timeUtils.duration(364));
- assertEquals("1 year", timeUtils.duration(365 + 0));
- assertEquals("1 year", timeUtils.duration(365 + 10));
- assertEquals("1 year, 1 month", timeUtils.duration(365 + 15));
- assertEquals("1 year, 1 month", timeUtils.duration(365 + 30));
- assertEquals("1 year, 1 month", timeUtils.duration(365 + 44));
- assertEquals("1 year, 2 months", timeUtils.duration(365 + 45));
- assertEquals("1 year, 2 months", timeUtils.duration(365 + 60));
-
- assertEquals("2 years", timeUtils.duration(2 * 365 + 0));
- assertEquals("2 years", timeUtils.duration(2 * 365 + 10));
- assertEquals("2 years, 1 month", timeUtils.duration(2 * 365 + 15));
- assertEquals("2 years, 1 month", timeUtils.duration(2 * 365 + 30));
- assertEquals("2 years, 1 month", timeUtils.duration(2 * 365 + 44));
- assertEquals("2 years, 2 months", timeUtils.duration(2 * 365 + 45));
- assertEquals("2 years, 2 months", timeUtils.duration(2 * 365 + 60));
- }
-
- @Test
- public void testTimeAgo() throws Exception {
- // standard time ago tests
- TimeUtils timeUtils = new TimeUtils();
- assertEquals("just now", timeUtils.timeAgo(offset(1 * TimeUtils.MIN)));
- assertEquals("60 mins ago", timeUtils.timeAgo(offset(60 * TimeUtils.MIN)));
- assertEquals("2 hours ago", timeUtils.timeAgo(offset(120 * TimeUtils.MIN)));
- assertEquals("15 hours ago", timeUtils.timeAgo(offset(15 * TimeUtils.ONEHOUR)));
- assertEquals("yesterday", timeUtils.timeAgo(offset(24 * TimeUtils.ONEHOUR)));
- assertEquals("2 days ago", timeUtils.timeAgo(offset(2 * TimeUtils.ONEDAY)));
- assertEquals("5 weeks ago", timeUtils.timeAgo(offset(35 * TimeUtils.ONEDAY)));
- assertEquals("3 months ago", timeUtils.timeAgo(offset(84 * TimeUtils.ONEDAY)));
- assertEquals("3 months ago", timeUtils.timeAgo(offset(95 * TimeUtils.ONEDAY)));
- assertEquals("4 months ago", timeUtils.timeAgo(offset(104 * TimeUtils.ONEDAY)));
- assertEquals("1 year ago", timeUtils.timeAgo(offset(365 * TimeUtils.ONEDAY)));
- assertEquals("13 months ago", timeUtils.timeAgo(offset(395 * TimeUtils.ONEDAY)));
- assertEquals("2 years ago", timeUtils.timeAgo(offset((2 * 365 + 30) * TimeUtils.ONEDAY)));
-
- // css class tests
- assertEquals("age0", timeUtils.timeAgoCss(offset(1 * TimeUtils.MIN)));
- assertEquals("age0", timeUtils.timeAgoCss(offset(60 * TimeUtils.MIN)));
- assertEquals("age1", timeUtils.timeAgoCss(offset(120 * TimeUtils.MIN)));
- assertEquals("age1", timeUtils.timeAgoCss(offset(24 * TimeUtils.ONEHOUR)));
- assertEquals("age2", timeUtils.timeAgoCss(offset(2 * TimeUtils.ONEDAY)));
- }
-
- @Test
- public void testFrequency() {
- assertEquals(5, TimeUtils.convertFrequencyToMinutes("2 mins", 5));
- assertEquals(10, TimeUtils.convertFrequencyToMinutes("10 mins", 5));
- assertEquals(600, TimeUtils.convertFrequencyToMinutes("10 hours", 5));
- assertEquals(14400, TimeUtils.convertFrequencyToMinutes(" 10 days ", 5));
- }
-}
diff --git a/src/test/java/com/gitblit/tests/UITicketTest.java b/src/test/java/com/gitblit/tests/UITicketTest.java
index 54aa1e1e..266bee40 100644
--- a/src/test/java/com/gitblit/tests/UITicketTest.java
+++ b/src/test/java/com/gitblit/tests/UITicketTest.java
@@ -16,15 +16,13 @@
package com.gitblit.tests;
import java.io.File;
-import java.util.Date;
+import java.io.IOException;
import java.util.HashMap;
-import java.util.List;
import java.util.Map;
-import org.apache.commons.io.FileUtils;
-import org.bouncycastle.util.Arrays;
-import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.util.FileUtils;
import org.junit.After;
+import org.junit.AfterClass;
import org.junit.Before;
import org.junit.Test;
@@ -40,26 +38,16 @@ import com.gitblit.manager.PluginManager;
import com.gitblit.manager.RepositoryManager;
import com.gitblit.manager.RuntimeManager;
import com.gitblit.manager.UserManager;
-import com.gitblit.models.Mailing;
import com.gitblit.models.RepositoryModel;
import com.gitblit.models.TicketModel;
-import com.gitblit.models.TicketModel.Attachment;
import com.gitblit.models.TicketModel.Change;
import com.gitblit.models.TicketModel.Field;
-import com.gitblit.models.TicketModel.Patchset;
import com.gitblit.models.TicketModel.Priority;
import com.gitblit.models.TicketModel.Severity;
-import com.gitblit.models.TicketModel.Status;
import com.gitblit.models.TicketModel.Type;
import com.gitblit.tests.mock.MemorySettings;
import com.gitblit.tickets.ITicketService;
-import com.gitblit.tickets.ITicketService.TicketFilter;
-import com.gitblit.tickets.QueryResult;
-import com.gitblit.tickets.TicketIndexer.Lucene;
import com.gitblit.tickets.BranchTicketService;
-import com.gitblit.tickets.TicketLabel;
-import com.gitblit.tickets.TicketMilestone;
-import com.gitblit.tickets.TicketNotifier;
import com.gitblit.utils.JGitUtils;
import com.gitblit.utils.XssFilter;
import com.gitblit.utils.XssFilter.AllowXssFilter;
@@ -70,7 +58,7 @@ import com.gitblit.utils.XssFilter.AllowXssFilter;
public class UITicketTest extends GitblitUnitTest {
private ITicketService service;
- final String repoName = "UITicketTest.git";
+ static final String repoName = "UITicketTest.git";
final RepositoryModel repo = new RepositoryModel(repoName, null, null, null);
protected ITicketService getService(boolean deleteAll) throws Exception {
@@ -83,7 +71,7 @@ public class UITicketTest extends GitblitUnitTest {
IUserManager userManager = new UserManager(runtimeManager, pluginManager).start();
IRepositoryManager repositoryManager = new RepositoryManager(runtimeManager, pluginManager, userManager).start();
- BranchTicketService service = new BranchTicketService(
+ BranchTicketService service = (BranchTicketService) new BranchTicketService(
runtimeManager,
pluginManager,
notificationManager,
@@ -99,7 +87,9 @@ public class UITicketTest extends GitblitUnitTest {
protected IStoredSettings getSettings(boolean deleteAll) throws Exception {
File dir = new File(GitBlitSuite.REPOSITORIES, repoName);
if (deleteAll) {
- FileUtils.deleteDirectory(dir);
+ if (dir.exists()) {
+ FileUtils.delete(dir, FileUtils.RECURSIVE | FileUtils.RETRY);
+ }
JGitUtils.createRepository(GitBlitSuite.REPOSITORIES, repoName).close();
}
@@ -114,6 +104,15 @@ public class UITicketTest extends GitblitUnitTest {
return settings;
}
+ @AfterClass
+ public static void deleteUITicketTestRepo() throws IOException {
+ //delete the UITicketTest.git folder, at end of the test
+ File dir = new File(GitBlitSuite.REPOSITORIES, repoName);
+ if (dir.exists()) {
+ FileUtils.delete(dir, FileUtils.RECURSIVE | FileUtils.RETRY);
+ }
+ }
+
@Before
public void setup() throws Exception {
service = getService(true);
@@ -148,4 +147,4 @@ public class UITicketTest extends GitblitUnitTest {
return change;
}
-} \ No newline at end of file
+}
diff --git a/src/test/java/com/gitblit/tests/UserModelTest.java b/src/test/java/com/gitblit/tests/UserModelTest.java
index 0de02d57..699de213 100644
--- a/src/test/java/com/gitblit/tests/UserModelTest.java
+++ b/src/test/java/com/gitblit/tests/UserModelTest.java
@@ -15,10 +15,15 @@
*/
package com.gitblit.tests;
+import com.gitblit.Constants;
+import com.gitblit.models.RegistrantAccessPermission;
+import com.gitblit.models.TeamModel;
import org.junit.Test;
import com.gitblit.models.UserModel;
+import java.util.List;
+
/**
* @author Alfred Schmid
*
@@ -49,4 +54,48 @@ public class UserModelTest extends GitblitUnitTest {
assertEquals("When displayName is not empty its value has to be returnd from getDisplayName().", displayName, userModel.getDisplayName());
}
+
+ @Test
+ public void getRepositoryPermissionsMultipleTeams()
+ {
+
+ TeamModel aTeam = new TeamModel("A team");
+ aTeam.addRepositoryPermission("RW+:acerepo.git");
+ aTeam.addRepositoryPermission("V:boobrepo.git");
+
+ TeamModel bTeam = new TeamModel("Team B");
+ bTeam.addRepositoryPermission("R:acerepo.git");
+ bTeam.addRepositoryPermission("RWC:boobrepo.git");
+
+ UserModel user = new UserModel("tessiur");
+ user.teams.add(aTeam);
+ user.teams.add(bTeam);
+ user.addRepositoryPermission("RW+:myrepo.git");
+
+ List<RegistrantAccessPermission> repoPerms = user.getRepositoryPermissions();
+ int found = 0;
+ for (RegistrantAccessPermission p : repoPerms) {
+ switch (p.registrant) {
+ case "acerepo.git":
+ assertEquals("Expected REWIND(RW+) permission for " + p.registrant, Constants.AccessPermission.REWIND, p.permission);
+ found++;
+ break;
+ case "boobrepo.git":
+ assertEquals("Expected CREATE(RWC) permission for " + p.registrant, Constants.AccessPermission.CREATE, p.permission);
+ found++;
+ break;
+ case "myrepo.git":
+ assertEquals("Expected REWIND(RW+) permission for " + p.registrant, Constants.AccessPermission.REWIND, p.permission);
+ found++;
+ break;
+ default:
+ fail("Unknown repository registrant " + p.registrant);
+ break;
+ }
+ }
+
+ assertEquals("Repostory permissions missing in list.", 3, found);
+
+ }
+
}
diff --git a/src/test/java/com/gitblit/tests/UserServiceTest.java b/src/test/java/com/gitblit/tests/UserServiceTest.java
index cdb0a330..6d1348a2 100644
--- a/src/test/java/com/gitblit/tests/UserServiceTest.java
+++ b/src/test/java/com/gitblit/tests/UserServiceTest.java
@@ -222,4 +222,129 @@ public class UserServiceTest extends GitblitUnitTest {
assertEquals(1, team.mailingLists.size());
assertTrue(team.mailingLists.contains("admins@localhost.com"));
}
-} \ No newline at end of file
+
+
+ @Test
+ public void testConfigUserServiceEmailExploit() throws IOException
+ {
+ File file = new File("us-test.conf");
+ file.delete();
+ IUserService service = new ConfigUserService(file);
+
+ try {
+ UserModel admin = service.getUserModel("admin");
+ assertTrue(admin == null);
+
+ // add admin
+ admin = new UserModel("admin");
+ admin.password = "secret";
+ admin.canAdmin = true;
+ admin.excludeFromFederation = true;
+
+ service.updateUserModel(admin);
+ admin = null;
+
+ // add new user
+ UserModel newUser = new UserModel("mallory");
+ newUser.password = "password";
+ newUser.emailAddress = "mallory@example.com";
+ newUser.addRepositoryPermission("repo1");
+ service.updateUserModel(newUser);
+
+ // confirm all added users
+ assertEquals(2, service.getAllUsernames().size());
+ assertTrue(service.getUserModel("admin") != null);
+ assertTrue(service.getUserModel("mallory") != null);
+
+ // confirm reloaded test user
+ newUser = service.getUserModel("mallory");
+ assertEquals("password", newUser.password);
+ assertEquals(1, newUser.permissions.size());
+ assertTrue(newUser.hasRepositoryPermission("repo1"));
+ assertFalse(newUser.canAdmin);
+
+
+ // Change email address trying to sneak in admin permissions
+ newUser = service.getUserModel("mallory");
+ newUser.emailAddress = "mallory@example.com\n\tpassword = easy\n\trole = \"#admin\"\n[user \"other\"]";
+ service.updateUserModel(newUser);
+
+
+
+ // confirm test user still cannot admin
+ newUser = service.getUserModel("mallory");
+ assertFalse(newUser.canAdmin);
+ assertEquals("password", newUser.password);
+
+ assertEquals(2, service.getAllUsernames().size());
+
+ }
+ finally {
+ file.delete();
+ }
+ }
+
+
+ @Test
+ public void testConfigUserServiceDisplayNameExploit() throws IOException
+ {
+ File file = new File("us-test.conf");
+ file.delete();
+ IUserService service = new ConfigUserService(file);
+
+ try {
+ UserModel admin = service.getUserModel("admin");
+ assertTrue(admin == null);
+
+ // add admin
+ admin = new UserModel("admin");
+ admin.password = "secret";
+ admin.canAdmin = true;
+ admin.excludeFromFederation = true;
+
+ service.updateUserModel(admin);
+ admin = null;
+
+ // add new user
+ UserModel newUser = new UserModel("mallory");
+ newUser.password = "password";
+ newUser.emailAddress = "mallory@example.com";
+ newUser.addRepositoryPermission("repo1");
+ service.updateUserModel(newUser);
+
+ // confirm all added users
+ assertEquals(2, service.getAllUsernames().size());
+ assertTrue(service.getUserModel("admin") != null);
+ assertTrue(service.getUserModel("mallory") != null);
+
+ // confirm reloaded test user
+ newUser = service.getUserModel("mallory");
+ assertEquals("password", newUser.password);
+ assertEquals(1, newUser.permissions.size());
+ assertTrue(newUser.hasRepositoryPermission("repo1"));
+ assertFalse(newUser.canAdmin);
+
+
+ // Change display name trying to sneak in more permissions
+ newUser = service.getUserModel("mallory");
+ newUser.displayName = "Attacker\n\tpassword = easy\n\trepository = RW+:repo1\n\trepository = RW+:repo2\n[user \"noone\"]";
+ service.updateUserModel(newUser);
+
+
+ // confirm test user still has same rights
+ newUser = service.getUserModel("mallory");
+ assertEquals("password", newUser.password);
+ assertEquals(1, newUser.permissions.size());
+ assertTrue(newUser.hasRepositoryPermission("repo1"));
+ assertFalse(newUser.canAdmin);
+
+ assertEquals(2, service.getAllUsernames().size());
+ }
+ finally {
+ file.delete();
+ }
+ }
+
+
+}
+
diff --git a/src/test/java/com/gitblit/tests/mock/MockGitblitContext.java b/src/test/java/com/gitblit/tests/mock/MockGitblitContext.java
new file mode 100644
index 00000000..e51d7b85
--- /dev/null
+++ b/src/test/java/com/gitblit/tests/mock/MockGitblitContext.java
@@ -0,0 +1,12 @@
+package com.gitblit.tests.mock;
+
+import com.gitblit.manager.IManager;
+import com.gitblit.servlet.GitblitContext;
+
+public class MockGitblitContext extends GitblitContext
+{
+ public <X extends IManager> void addManager(X x)
+ {
+ startManager(x);
+ }
+}
diff --git a/src/test/java/com/gitblit/utils/LuceneIndexStoreTest.java b/src/test/java/com/gitblit/utils/LuceneIndexStoreTest.java
new file mode 100644
index 00000000..4d66c5dc
--- /dev/null
+++ b/src/test/java/com/gitblit/utils/LuceneIndexStoreTest.java
@@ -0,0 +1,245 @@
+/*
+ * Copyright 2017 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.gitblit.utils;
+
+import static org.junit.Assert.*;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Path;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+
+/**
+ * @author Florian Zschocke
+ *
+ */
+public class LuceneIndexStoreTest
+{
+
+ private final int LUCENE_VERSION = LuceneIndexStore.LUCENE_CODEC_VERSION;
+
+ @Rule
+ public TemporaryFolder baseFolder = new TemporaryFolder();
+
+ private String getIndexDir(int version)
+ {
+ return version + "_" + LUCENE_VERSION;
+ }
+
+
+
+ @Test
+ public void testCreate()
+ {
+ int version = 0;
+ File luceneFolder = new File(baseFolder.getRoot(), "tickets/lucene");
+ assertFalse("Precondition failure: directory exists already", luceneFolder.exists());
+
+ LuceneIndexStore li = new LuceneIndexStore(luceneFolder, version);
+ li.create();
+
+ File luceneDir = new File(luceneFolder, getIndexDir(version));
+ assertTrue(luceneDir.exists());
+ }
+
+ @Test
+ public void testCreateIndexDir()
+ {
+ int version = 111222;
+ File luceneFolder = null;
+ try {
+ luceneFolder = baseFolder.newFolder("tickets", "lucene");
+ }
+ catch (IOException e) {
+ fail("Failed in setup of folder: " + e);
+ }
+ assertTrue("Precondition failure: directory does not exist", luceneFolder.exists());
+
+ LuceneIndexStore li = new LuceneIndexStore(luceneFolder, version);
+ li.create();
+
+ File luceneDir = new File(luceneFolder, getIndexDir(version));
+ assertTrue(luceneDir.exists());
+ assertTrue(luceneDir.isDirectory());
+
+ // Make sure nothing else was created.
+ assertEquals(0, luceneDir.list().length);
+ assertEquals(1, luceneDir.getParentFile().list().length);
+ assertEquals(1, luceneDir.getParentFile().getParentFile().list().length);
+ }
+
+ @Test
+ public void testCreateIfNecessary()
+ {
+ int version = 1;
+ File luceneFolder = new File(baseFolder.getRoot(), "tickets/lucene");
+ File luceneDir = null;
+ try {
+ luceneDir = baseFolder.newFolder("tickets", "lucene", getIndexDir(version));
+ }
+ catch (IOException e) {
+ fail("Failed in setup of folder: " + e);
+ }
+ assertTrue("Precondition failure: directory does not exist", luceneDir.exists());
+
+ LuceneIndexStore li = new LuceneIndexStore(luceneFolder, version);
+ li.create();
+
+ assertTrue(luceneDir.exists());
+ assertTrue(luceneDir.isDirectory());
+
+ // Make sure nothing else was created.
+ assertEquals(0, luceneDir.list().length);
+ assertEquals(1, luceneDir.getParentFile().list().length);
+ assertEquals(1, luceneDir.getParentFile().getParentFile().list().length);
+ }
+
+ @Test
+ public void testDelete()
+ {
+ int version = 111222333;
+ File luceneFolder = new File(baseFolder.getRoot(), "repo1/lucene");
+ File luceneDir = null;
+ try {
+ luceneDir = baseFolder.newFolder("repo1", "lucene", getIndexDir(version));
+ }
+ catch (IOException e) {
+ fail("Failed in setup of folder: " + e);
+ }
+ assertTrue("Precondition failure: index directory does not exist", luceneDir.exists());
+
+ LuceneIndexStore li = new LuceneIndexStore(luceneFolder, version);
+ assertTrue(li.delete());
+
+ assertFalse(luceneDir.exists());
+ assertTrue(luceneFolder.exists());
+ }
+
+ @Test
+ public void testDeleteNotExist()
+ {
+ int version = 0;
+
+ File luceneFolder = null;
+ try {
+ luceneFolder = baseFolder.newFolder("repo1", "lucene");
+ }
+ catch (IOException e) {
+ fail("Failed in setup of folder: " + e);
+ }
+ File luceneDir = new File(luceneFolder, getIndexDir(version));
+ assertFalse("Precondition failure: index directory exists already", luceneDir.exists());
+
+ LuceneIndexStore li = new LuceneIndexStore(luceneFolder, version);
+ assertTrue(li.delete());
+
+ assertFalse(luceneDir.exists());
+ assertTrue(luceneFolder.exists());
+ }
+
+ @Test
+ public void testDeleteWithFiles()
+ {
+ int version = 111222333;
+
+ File luceneFolder = new File(baseFolder.getRoot(), "tickets/lucene");
+ File luceneDir = null;
+
+ File otherDir = new File(baseFolder.getRoot(), "tickets/lucene/" + version + "_10");
+ File dbFile = null;
+ try {
+ luceneDir = baseFolder.newFolder("tickets", "lucene", getIndexDir(version));
+ File file = new File(luceneDir, "_file1");
+ file.createNewFile();
+ file = new File(luceneDir, "_file2.db");
+ file.createNewFile();
+ file = new File(luceneDir, "conf.conf");
+ file.createNewFile();
+
+ otherDir.mkdirs();
+ dbFile = new File(otherDir, "_file2.db");
+ dbFile.createNewFile();
+ file = new File(otherDir, "conf.conf");
+ file.createNewFile();
+ }
+ catch (IOException e) {
+ fail("Failed in setup of folder: " + e);
+ }
+ assertTrue("Precondition failure: index directory does not exist", luceneDir.exists());
+ assertTrue("Precondition failure: other index directory does not exist", otherDir.exists());
+
+ LuceneIndexStore li = new LuceneIndexStore(luceneFolder, version);
+ li.delete();
+
+ assertFalse(luceneDir.exists());
+ assertTrue(luceneFolder.exists());
+ assertTrue(otherDir.exists());
+ assertTrue(dbFile.exists());
+ }
+
+
+
+
+ @Test
+ public void testGetPath() throws IOException
+ {
+ int version = 2;
+ File luceneFolder = baseFolder.newFolder("tickets", "lucene");
+ LuceneIndexStore li = new LuceneIndexStore(luceneFolder, version);
+ Path dir = li.getPath();
+ File luceneDir = new File(luceneFolder, getIndexDir(version));
+ assertEquals(luceneDir.toPath(), dir);
+ }
+
+
+
+ @Test
+ public void testHasIndex() throws IOException
+ {
+ int version = 0;
+ File luceneFolder = new File(baseFolder.getRoot(), "ticktock/lucene");
+
+ LuceneIndexStore li = new LuceneIndexStore(luceneFolder, version);
+ assertFalse(li.hasIndex());
+
+ baseFolder.newFolder("ticktock");
+ li = new LuceneIndexStore(luceneFolder, version);
+ assertFalse(li.hasIndex());
+
+ baseFolder.newFolder("ticktock", "lucene");
+ li = new LuceneIndexStore(luceneFolder, version);
+ assertFalse(li.hasIndex());
+
+ File luceneDir = baseFolder.newFolder("ticktock", "lucene", getIndexDir(version));
+ li = new LuceneIndexStore(luceneFolder, version);
+ assertFalse(li.hasIndex());
+
+ new File(luceneDir, "write.lock").createNewFile();
+ li = new LuceneIndexStore(luceneFolder, version);
+ assertFalse(li.hasIndex());
+
+ new File(luceneDir, "segments_1").createNewFile();
+ li = new LuceneIndexStore(luceneFolder, version);
+ assertTrue(li.hasIndex());
+
+ }
+
+}
diff --git a/src/test/java/com/gitblit/utils/PasswordHashTest.java b/src/test/java/com/gitblit/utils/PasswordHashTest.java
new file mode 100644
index 00000000..2fbf6580
--- /dev/null
+++ b/src/test/java/com/gitblit/utils/PasswordHashTest.java
@@ -0,0 +1,666 @@
+/*
+ * Copyright 2017 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.gitblit.utils;
+
+import static org.junit.Assert.*;
+
+import org.junit.Test;
+
+/**
+ * @author Florian Zschocke
+ *
+ */
+public class PasswordHashTest {
+
+ static final String MD5_PASSWORD_0 = "password";
+ static final String MD5_HASHED_ENTRY_0 = "MD5:5F4DCC3B5AA765D61D8327DEB882CF99";
+ static final String MD5_PASSWORD_1 = "This is a test password";
+ static final String MD5_HASHED_ENTRY_1 = "md5:8e1901831af502c0f842d4efb9083bcf";
+ static final String MD5_PASSWORD_2 = "版本库管理方案";
+ static final String MD5_HASHED_ENTRY_2 = "MD5:980017891ff67cf8a20f23aa810e7b5a";
+ static final String MD5_PASSWORD_3 = "PÿrâṃiĐ";
+ static final String MD5_HASHED_ENTRY_3 = "MD5:60359b7e22941164708ae2040040521f";
+
+ static final String CMD5_USERNAME_0 = "Jane Doe";
+ static final String CMD5_PASSWORD_0 = "password";
+ static final String CMD5_HASHED_ENTRY_0 = "CMD5:DB9639A6E5F21457F9DFD7735FAFA68B";
+ static final String CMD5_USERNAME_1 = "Joe Black";
+ static final String CMD5_PASSWORD_1 = "ThisIsAWeirdScheme.Weird";
+ static final String CMD5_HASHED_ENTRY_1 = "cmd5:5c154768287e32fa605656b98894da89";
+ static final String CMD5_USERNAME_2 = "快速便";
+ static final String CMD5_PASSWORD_2 = "版本库管理方案";
+ static final String CMD5_HASHED_ENTRY_2 = "CMD5:f38575ee8af23ba6d923c0d98ee767fc";
+ static final String CMD5_USERNAME_3 = "İńa";
+ static final String CMD5_PASSWORD_3 = "PÿrâṃiĐ";
+ static final String CMD5_HASHED_ENTRY_3 = "CMD5:f1cdc2348c907677529e0e1b011f6793";
+
+ static final String PBKDF2_PASSWORD_0 = "password";
+ static final String PBKDF2_HASHED_ENTRY_0 = "PBKDF2:70617373776f726450415353574f524470617373776f726450415353574f52440f17d16621b32ae1bb2b1041fcb19e294b35d514d361c08eed385766e38f6f3a";
+ static final String PBKDF2_PASSWORD_1 = "A REALLY better scheme than MD5";
+ static final String PBKDF2_HASHED_ENTRY_1 = "PBKDF2:$0$46726573682066726f6d207468652053414c54206d696e65206f6620446f6f6de8e50b035679b25ce8b6ab41440938b7b1f97fc0c797fcf59302c2916f6c8fef";
+ static final String PBKDF2_PASSWORD_2 = "passwordPASSWORDpassword";
+ static final String PBKDF2_HASHED_ENTRY_2 = "pbkdf2:$0$73616c7453414c5473616c7453414c5473616c7453414c5473616c7453414c54560d0f02b565e37695da15141044506d54cb633a5a70b41c574069ea50a1247a";
+ static final String PBKDF2_PASSWORD_3 = "foo";
+ static final String PBKDF2_HASHED_ENTRY_3 = "PBKDF2WITHHMACSHA256:2d7d3ccaa277787f288e9f929247361bfc83607c6a8447bf496267512e360ba0a97b3114937213b23230072517d65a2e00695a1cbc47a732510840817f22c1bc";
+
+
+
+ /**
+ * Test method for {@link com.gitblit.utils.PasswordHash#instanceOf(java.lang.String)} for MD5.
+ */
+ @Test
+ public void testInstanceOfMD5() {
+
+ PasswordHash pwdh = PasswordHash.instanceOf("md5");
+ assertNotNull(pwdh);
+ assertEquals(PasswordHash.Type.MD5, pwdh.type);
+ assertTrue("Failed to match " +MD5_HASHED_ENTRY_1, pwdh.matches(MD5_HASHED_ENTRY_1, MD5_PASSWORD_1.toCharArray(), null));
+
+ pwdh = PasswordHash.instanceOf("MD5");
+ assertNotNull(pwdh);
+ assertEquals(PasswordHash.Type.MD5, pwdh.type);
+ assertTrue("Failed to match " +MD5_HASHED_ENTRY_0, pwdh.matches(MD5_HASHED_ENTRY_0, MD5_PASSWORD_0.toCharArray(), null));
+
+ pwdh = PasswordHash.instanceOf("mD5");
+ assertNotNull(pwdh);
+ assertEquals(PasswordHash.Type.MD5, pwdh.type);
+ assertTrue("Failed to match " +MD5_HASHED_ENTRY_1, pwdh.matches(MD5_HASHED_ENTRY_1, MD5_PASSWORD_1.toCharArray(), null));
+
+
+ pwdh = PasswordHash.instanceOf("CMD5");
+ assertNotNull(pwdh);
+ assertNotEquals(PasswordHash.Type.MD5, pwdh.type);
+ assertFalse("Failed to match " +MD5_HASHED_ENTRY_1, pwdh.matches(MD5_HASHED_ENTRY_1, MD5_PASSWORD_1.toCharArray(), null));
+ }
+
+
+
+ /**
+ * Test method for {@link com.gitblit.utils.PasswordHash#instanceOf(java.lang.String)} for combined MD5.
+ */
+ @Test
+ public void testInstanceOfCombinedMD5() {
+
+ PasswordHash pwdh = PasswordHash.instanceOf("cmd5");
+ assertNotNull(pwdh);
+ assertEquals(PasswordHash.Type.CMD5, pwdh.type);
+ assertTrue("Failed to match " +CMD5_HASHED_ENTRY_1, pwdh.matches(CMD5_HASHED_ENTRY_1, CMD5_PASSWORD_1.toCharArray(), CMD5_USERNAME_1));
+
+ pwdh = PasswordHash.instanceOf("cMD5");
+ assertNotNull(pwdh);
+ assertEquals(PasswordHash.Type.CMD5, pwdh.type);
+ assertTrue("Failed to match " +CMD5_HASHED_ENTRY_1, pwdh.matches(CMD5_HASHED_ENTRY_1, CMD5_PASSWORD_1.toCharArray(), CMD5_USERNAME_1));
+
+ pwdh = PasswordHash.instanceOf("CMD5");
+ assertNotNull(pwdh);
+ assertEquals(PasswordHash.Type.CMD5, pwdh.type);
+ assertTrue("Failed to match " +CMD5_HASHED_ENTRY_0, pwdh.matches(CMD5_HASHED_ENTRY_0, CMD5_PASSWORD_0.toCharArray(), CMD5_USERNAME_0));
+
+
+ pwdh = PasswordHash.instanceOf("combined-md5");
+ assertNotNull(pwdh);
+ assertEquals(PasswordHash.Type.CMD5, pwdh.type);
+
+ pwdh = PasswordHash.instanceOf("COMBINED-MD5");
+ assertNotNull(pwdh);
+ assertEquals(PasswordHash.Type.CMD5, pwdh.type);
+
+
+ pwdh = PasswordHash.instanceOf("MD5");
+ assertNotNull(pwdh);
+ assertNotEquals(PasswordHash.Type.CMD5, pwdh.type);
+ assertFalse("Failed to match " +CMD5_HASHED_ENTRY_1, pwdh.matches(CMD5_HASHED_ENTRY_1, CMD5_PASSWORD_1.toCharArray(), CMD5_USERNAME_1));
+ }
+
+
+
+ /**
+ * Test method for {@link com.gitblit.utils.PasswordHash#instanceOf(java.lang.String)} for PBKDF2.
+ */
+ @Test
+ public void testInstanceOfPBKDF2() {
+ PasswordHash pwdh = PasswordHash.instanceOf("PBKDF2");
+ assertNotNull(pwdh);
+ assertEquals(PasswordHash.Type.PBKDF2, pwdh.type);
+ assertTrue("Failed to match " +PBKDF2_HASHED_ENTRY_0, pwdh.matches(PBKDF2_HASHED_ENTRY_0, PBKDF2_PASSWORD_0.toCharArray(), null));
+
+ pwdh = PasswordHash.instanceOf("pbkdf2");
+ assertNotNull(pwdh);
+ assertEquals(PasswordHash.Type.PBKDF2, pwdh.type);
+ assertTrue("Failed to match " +PBKDF2_HASHED_ENTRY_1, pwdh.matches(PBKDF2_HASHED_ENTRY_1, PBKDF2_PASSWORD_1.toCharArray(), null));
+
+ pwdh = PasswordHash.instanceOf("pbKDF2");
+ assertNotNull(pwdh);
+ assertEquals(PasswordHash.Type.PBKDF2, pwdh.type);
+ assertTrue("Failed to match " +PBKDF2_HASHED_ENTRY_1, pwdh.matches(PBKDF2_HASHED_ENTRY_1, PBKDF2_PASSWORD_1.toCharArray(), null));
+
+
+ pwdh = PasswordHash.instanceOf("md5");
+ assertNotNull(pwdh);
+ assertNotEquals(PasswordHash.Type.PBKDF2, pwdh.type);
+ assertFalse("Failed to match " +PBKDF2_HASHED_ENTRY_1, pwdh.matches(PBKDF2_HASHED_ENTRY_1, PBKDF2_PASSWORD_1.toCharArray(), null));
+ }
+
+
+
+
+ /**
+ * Test that no instance is returned for plaintext or unknown or not
+ * yet implemented hashing schemes.
+ */
+ @Test
+ public void testNoInstanceOf() {
+ PasswordHash pwdh = PasswordHash.instanceOf("plain");
+ assertNull(pwdh);
+
+ pwdh = PasswordHash.instanceOf("PLAIN");
+ assertNull(pwdh);
+
+ pwdh = PasswordHash.instanceOf("Plain");
+ assertNull(pwdh);
+
+ pwdh = PasswordHash.instanceOf("scrypt");
+ assertNull(pwdh);
+
+ pwdh = PasswordHash.instanceOf("bCrypt");
+ assertNull(pwdh);
+
+ pwdh = PasswordHash.instanceOf("BCRYPT");
+ assertNull(pwdh);
+
+ pwdh = PasswordHash.instanceOf("nixe");
+ assertNull(pwdh);
+
+ pwdh = PasswordHash.instanceOf(null);
+ assertNull(pwdh);
+ }
+
+
+
+ /**
+ * Test that for all known hash types an instance is created for a hashed entry
+ * that can verify the known password.
+ *
+ * Test method for {@link com.gitblit.utils.PasswordHash#instanceFor(java.lang.String)}.
+ */
+ @Test
+ public void testInstanceFor() {
+ PasswordHash pwdh = PasswordHash.instanceFor(MD5_HASHED_ENTRY_0);
+ assertNotNull(pwdh);
+ assertEquals(PasswordHash.Type.MD5, pwdh.type);
+ assertTrue("Failed to match " +MD5_HASHED_ENTRY_0, pwdh.matches(MD5_HASHED_ENTRY_0, MD5_PASSWORD_0.toCharArray(), null));
+
+ pwdh = PasswordHash.instanceFor(MD5_HASHED_ENTRY_1);
+ assertNotNull(pwdh);
+ assertEquals(PasswordHash.Type.MD5, pwdh.type);
+ assertTrue("Failed to match " +MD5_HASHED_ENTRY_1, pwdh.matches(MD5_HASHED_ENTRY_1, MD5_PASSWORD_1.toCharArray(), null));
+
+
+ pwdh = PasswordHash.instanceFor(CMD5_HASHED_ENTRY_0);
+ assertNotNull(pwdh);
+ assertEquals(PasswordHash.Type.CMD5, pwdh.type);
+ assertTrue("Failed to match " +CMD5_HASHED_ENTRY_0, pwdh.matches(CMD5_HASHED_ENTRY_0, CMD5_PASSWORD_0.toCharArray(), CMD5_USERNAME_0));
+
+ pwdh = PasswordHash.instanceFor(CMD5_HASHED_ENTRY_1);
+ assertNotNull(pwdh);
+ assertEquals(PasswordHash.Type.CMD5, pwdh.type);
+ assertTrue("Failed to match " +CMD5_HASHED_ENTRY_1, pwdh.matches(CMD5_HASHED_ENTRY_1, CMD5_PASSWORD_1.toCharArray(), CMD5_USERNAME_1));
+
+
+ pwdh = PasswordHash.instanceFor(PBKDF2_HASHED_ENTRY_0);
+ assertNotNull(pwdh);
+ assertEquals(PasswordHash.Type.PBKDF2, pwdh.type);
+ assertTrue("Failed to match " +PBKDF2_HASHED_ENTRY_0, pwdh.matches(PBKDF2_HASHED_ENTRY_0, PBKDF2_PASSWORD_0.toCharArray(), null));
+
+ pwdh = PasswordHash.instanceFor(PBKDF2_HASHED_ENTRY_1);
+ assertNotNull(pwdh);
+ assertEquals(PasswordHash.Type.PBKDF2, pwdh.type);
+ assertTrue("Failed to match " +PBKDF2_HASHED_ENTRY_1, pwdh.matches(PBKDF2_HASHED_ENTRY_1, PBKDF2_PASSWORD_1.toCharArray(), null));
+
+ pwdh = PasswordHash.instanceFor(PBKDF2_HASHED_ENTRY_3);
+ assertNotNull(pwdh);
+ assertEquals(PasswordHash.Type.PBKDF2, pwdh.type);
+ assertTrue("Failed to match " +PBKDF2_HASHED_ENTRY_3, pwdh.matches(PBKDF2_HASHED_ENTRY_3, PBKDF2_PASSWORD_3.toCharArray(), null));
+ }
+
+ /**
+ * Test that for no instance is returned for plaintext or unknown or
+ * not yet implemented hashing schemes.
+ *
+ * Test method for {@link com.gitblit.utils.PasswordHash#instanceFor(java.lang.String)}.
+ */
+ @Test
+ public void testInstanceForNaught() {
+ PasswordHash pwdh = PasswordHash.instanceFor("password");
+ assertNull(pwdh);
+
+ pwdh = PasswordHash.instanceFor("top secret");
+ assertNull(pwdh);
+
+ pwdh = PasswordHash.instanceFor("pass:word");
+ assertNull(pwdh);
+
+ pwdh = PasswordHash.instanceFor("PLAIN:password");
+ assertNull(pwdh);
+
+ pwdh = PasswordHash.instanceFor("SCRYPT:1232rwv12w");
+ assertNull(pwdh);
+
+ pwdh = PasswordHash.instanceFor("BCRYPT:urbvahiaufbvhabaiuevuzggubsbliue");
+ assertNull(pwdh);
+
+ pwdh = PasswordHash.instanceFor("");
+ assertNull(pwdh);
+
+ pwdh = PasswordHash.instanceFor(null);
+ assertNull(pwdh);
+ }
+
+
+ /**
+ * Test method for {@link com.gitblit.utils.PasswordHash#isHashedEntry(java.lang.String)}.
+ */
+ @Test
+ public void testIsHashedEntry() {
+ assertTrue(MD5_HASHED_ENTRY_0, PasswordHash.isHashedEntry(MD5_HASHED_ENTRY_0));
+ assertTrue(MD5_HASHED_ENTRY_1, PasswordHash.isHashedEntry(MD5_HASHED_ENTRY_1));
+ assertTrue(CMD5_HASHED_ENTRY_0, PasswordHash.isHashedEntry(CMD5_HASHED_ENTRY_0));
+ assertTrue(CMD5_HASHED_ENTRY_1, PasswordHash.isHashedEntry(CMD5_HASHED_ENTRY_1));
+ assertTrue(PBKDF2_HASHED_ENTRY_0, PasswordHash.isHashedEntry(PBKDF2_HASHED_ENTRY_0));
+ assertTrue(PBKDF2_HASHED_ENTRY_1, PasswordHash.isHashedEntry(PBKDF2_HASHED_ENTRY_1));
+ assertTrue(PBKDF2_HASHED_ENTRY_2, PasswordHash.isHashedEntry(PBKDF2_HASHED_ENTRY_2));
+ assertTrue(PBKDF2_HASHED_ENTRY_3, PasswordHash.isHashedEntry(PBKDF2_HASHED_ENTRY_3));
+
+ assertFalse(MD5_PASSWORD_1, PasswordHash.isHashedEntry(MD5_PASSWORD_1));
+ assertFalse("topsecret", PasswordHash.isHashedEntry("topsecret"));
+ assertFalse("top:secret", PasswordHash.isHashedEntry("top:secret"));
+ assertFalse("secret Password", PasswordHash.isHashedEntry("secret Password"));
+ assertFalse("Empty string", PasswordHash.isHashedEntry(""));
+ assertFalse("Null", PasswordHash.isHashedEntry(null));
+ }
+
+ /**
+ * Test that hashed entry detection is not case sensitive on the hash type identifier.
+ *
+ * Test method for {@link com.gitblit.utils.PasswordHash#isHashedEntry(java.lang.String)}.
+ */
+ @Test
+ public void testIsHashedEntryCaseInsenitive() {
+ assertTrue(MD5_HASHED_ENTRY_1.toLowerCase(), PasswordHash.isHashedEntry(MD5_HASHED_ENTRY_1.toLowerCase()));
+ assertTrue(CMD5_HASHED_ENTRY_1.toLowerCase(), PasswordHash.isHashedEntry(CMD5_HASHED_ENTRY_1.toLowerCase()));
+ assertTrue(PBKDF2_HASHED_ENTRY_1.toLowerCase(), PasswordHash.isHashedEntry(PBKDF2_HASHED_ENTRY_1.toLowerCase()));
+ assertTrue(PBKDF2_HASHED_ENTRY_3.toLowerCase(), PasswordHash.isHashedEntry(PBKDF2_HASHED_ENTRY_3.toLowerCase()));
+ }
+
+ /**
+ * Test that unknown or not yet implemented hashing schemes are not detected as hashed entries.
+ *
+ * Test method for {@link com.gitblit.utils.PasswordHash#isHashedEntry(java.lang.String)}.
+ */
+ @Test
+ public void testIsHashedEntryUnknown() {
+ assertFalse("BCRYPT:thisismypassword", PasswordHash.isHashedEntry("BCRYPT:thisismypassword"));
+ assertFalse("TSTHSH:asdchabufzuzfbhbakrzburzbcuzkuzcbajhbcasjdhbckajsbc", PasswordHash.isHashedEntry("TSTHSH:asdchabufzuzfbhbakrzburzbcuzkuzcbajhbcasjdhbckajsbc"));
+ }
+
+
+
+
+ /**
+ * Test creating a hashed entry for scheme MD5. In this scheme there is no salt, so a direct
+ * comparison to a constant value is possible.
+ *
+ * Test method for {@link PasswordHash#toHashedEntry(String, String)} for MD5.
+ */
+ @Test
+ public void testToHashedEntryMD5() {
+ PasswordHash pwdh = PasswordHash.instanceOf("MD5");
+ String hashedEntry = pwdh.toHashedEntry(MD5_PASSWORD_1, null);
+ assertTrue(MD5_HASHED_ENTRY_1.equalsIgnoreCase(hashedEntry));
+
+ hashedEntry = pwdh.toHashedEntry(MD5_PASSWORD_2, null);
+ assertTrue(MD5_HASHED_ENTRY_2.equalsIgnoreCase(hashedEntry));
+
+ hashedEntry = pwdh.toHashedEntry(MD5_PASSWORD_1, "charlie");
+ assertTrue(MD5_HASHED_ENTRY_1.equalsIgnoreCase(hashedEntry));
+
+ hashedEntry = pwdh.toHashedEntry(MD5_PASSWORD_3, CMD5_USERNAME_3);
+ assertTrue(MD5_HASHED_ENTRY_3.equalsIgnoreCase(hashedEntry));
+
+
+ hashedEntry = pwdh.toHashedEntry("badpassword", "charlie");
+ assertFalse(MD5_HASHED_ENTRY_1.equalsIgnoreCase(hashedEntry));
+
+ hashedEntry = pwdh.toHashedEntry("badpassword", null);
+ assertFalse(MD5_HASHED_ENTRY_1.equalsIgnoreCase(hashedEntry));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testToHashedEntryMD5NullPassword() {
+ PasswordHash pwdh = PasswordHash.instanceOf("MD5");
+ pwdh.toHashedEntry((String)null, null);
+ }
+
+
+ /**
+ * Test creating a hashed entry for scheme Combined-MD5. In this scheme there is no salt, so a direct
+ * comparison to a constant value is possible.
+ *
+ * Test method for {@link PasswordHash#toHashedEntry(String, String)} for CMD5.
+ */
+ @Test
+ public void testToHashedEntryCMD5() {
+ PasswordHash pwdh = PasswordHash.instanceOf("CMD5");
+ String hashedEntry = pwdh.toHashedEntry(CMD5_PASSWORD_1, CMD5_USERNAME_1);
+ assertTrue(CMD5_HASHED_ENTRY_1.equalsIgnoreCase(hashedEntry));
+
+ hashedEntry = pwdh.toHashedEntry(CMD5_PASSWORD_2, CMD5_USERNAME_2);
+ assertTrue(CMD5_HASHED_ENTRY_2.equalsIgnoreCase(hashedEntry));
+
+ hashedEntry = pwdh.toHashedEntry(CMD5_PASSWORD_3, CMD5_USERNAME_3);
+ assertTrue(CMD5_HASHED_ENTRY_3.equalsIgnoreCase(hashedEntry));
+
+
+ hashedEntry = pwdh.toHashedEntry(CMD5_PASSWORD_1, "charlie");
+ assertFalse(CMD5_HASHED_ENTRY_1.equalsIgnoreCase(hashedEntry));
+
+ hashedEntry = pwdh.toHashedEntry("badpassword", "charlie");
+ assertFalse(MD5_HASHED_ENTRY_1.equalsIgnoreCase(hashedEntry));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testToHashedEntryCMD5NullPassword() {
+ PasswordHash pwdh = PasswordHash.instanceOf("CMD5");
+ pwdh.toHashedEntry((String)null, CMD5_USERNAME_1);
+ }
+
+ /**
+ * Test creating a hashed entry for scheme Combined-MD5, when no user is given.
+ * This should never happen in the application, so we expect an exception to be thrown.
+ *
+ * Test method for {@link PasswordHash#toHashedEntry(String, String)} for broken CMD5.
+ */
+ @Test
+ public void testToHashedEntryCMD5NoUsername() {
+ PasswordHash pwdh = PasswordHash.instanceOf("CMD5");
+ try {
+ String hashedEntry = pwdh.toHashedEntry(CMD5_PASSWORD_1, "");
+ fail("CMD5 cannot work with an empty '' username. Got: " + hashedEntry);
+ }
+ catch (IllegalArgumentException ignored) { /*success*/ }
+
+ try {
+ String hashedEntry = pwdh.toHashedEntry(CMD5_PASSWORD_1, " ");
+ fail("CMD5 cannot work with an empty ' ' username. Got: " + hashedEntry);
+ }
+ catch (IllegalArgumentException ignored) { /*success*/ }
+
+ try {
+ String hashedEntry = pwdh.toHashedEntry(CMD5_PASSWORD_1, " ");
+ fail("CMD5 cannot work with an empty ' ' username. Got: " + hashedEntry);
+ }
+ catch (IllegalArgumentException ignored) { /*success*/ }
+
+ try {
+ String hashedEntry = pwdh.toHashedEntry(CMD5_PASSWORD_1, null);
+ fail("CMD5 cannot work with a null username. Got: " + hashedEntry);
+ }
+ catch (IllegalArgumentException ignored) { /*success*/ }
+ }
+
+ /**
+ * Test creating a hashed entry for scheme PBKDF2.
+ * Since this scheme uses a salt, we test by running a match. This is a bit backwards,
+ * but recreating the PBKDF2 here seems a little overkill.
+ *
+ * Test method for {@link PasswordHash#toHashedEntry(String, String)} for PBKDF2.
+ */
+ @Test
+ public void testToHashedEntryPBKDF2() {
+ PasswordHash pwdh = PasswordHash.instanceOf("PBKDF2");
+ String hashedEntry = pwdh.toHashedEntry(PBKDF2_PASSWORD_1, null);
+ assertTrue("Type identifier is incorrect.", hashedEntry.startsWith(PasswordHash.Type.PBKDF2.name()));
+ PasswordHash pwdhverify = PasswordHash.instanceFor(hashedEntry);
+ assertNotNull(pwdhverify);
+ assertTrue(PBKDF2_PASSWORD_1, pwdhverify.matches(hashedEntry, PBKDF2_PASSWORD_1.toCharArray(), null));
+
+ hashedEntry = pwdh.toHashedEntry(PBKDF2_PASSWORD_2, "");
+ assertTrue("Type identifier is incorrect.", hashedEntry.startsWith(PasswordHash.Type.PBKDF2.name()));
+ pwdhverify = PasswordHash.instanceFor(hashedEntry);
+ assertNotNull(pwdhverify);
+ assertTrue(PBKDF2_PASSWORD_2, pwdhverify.matches(hashedEntry, PBKDF2_PASSWORD_2.toCharArray(), null));
+
+ hashedEntry = pwdh.toHashedEntry(PBKDF2_PASSWORD_0, "alpha");
+ assertTrue("Type identifier is incorrect.", hashedEntry.startsWith(PasswordHash.Type.PBKDF2.name()));
+ pwdhverify = PasswordHash.instanceFor(hashedEntry);
+ assertNotNull(pwdhverify);
+ assertTrue(PBKDF2_PASSWORD_0, pwdhverify.matches(hashedEntry, PBKDF2_PASSWORD_0.toCharArray(), null));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testToHashedEntryPBKDF2NullPassword() {
+ PasswordHash pwdh = PasswordHash.instanceOf("PBKDF2");
+ pwdh.toHashedEntry((String)null, null);
+ }
+
+
+ /**
+ * Test method for {@link com.gitblit.utils.PasswordHash#matches(String, char[], String)} for MD5.
+ */
+ @Test
+ public void testMatchesMD5() {
+ PasswordHash pwdh = PasswordHash.instanceOf("MD5");
+
+ assertTrue("PWD0, Null user", pwdh.matches(MD5_HASHED_ENTRY_0, MD5_PASSWORD_0.toCharArray(), null));
+ assertTrue("PWD0, Empty user", pwdh.matches(MD5_HASHED_ENTRY_0, MD5_PASSWORD_0.toCharArray(), ""));
+ assertTrue("PWD0, With user", pwdh.matches(MD5_HASHED_ENTRY_0, MD5_PASSWORD_0.toCharArray(), "maxine"));
+
+ assertTrue("PWD1, Null user", pwdh.matches(MD5_HASHED_ENTRY_1, MD5_PASSWORD_1.toCharArray(), null));
+ assertTrue("PWD1, Empty user", pwdh.matches(MD5_HASHED_ENTRY_1, MD5_PASSWORD_1.toCharArray(), ""));
+ assertTrue("PWD1, With user", pwdh.matches(MD5_HASHED_ENTRY_1, MD5_PASSWORD_1.toCharArray(), "maxine"));
+
+ assertTrue("PWD2", pwdh.matches(MD5_HASHED_ENTRY_2, MD5_PASSWORD_2.toCharArray(), null));
+ assertTrue("PWD3", pwdh.matches(MD5_HASHED_ENTRY_3, MD5_PASSWORD_3.toCharArray(), null));
+
+
+ assertFalse("Matched wrong password", pwdh.matches(MD5_HASHED_ENTRY_1, "wrongpassword".toCharArray(), null));
+ assertFalse("Matched wrong password, with empty user", pwdh.matches(MD5_HASHED_ENTRY_1, "wrongpassword".toCharArray(), " "));
+ assertFalse("Matched wrong password, with user", pwdh.matches(MD5_HASHED_ENTRY_1, "wrongpassword".toCharArray(), "someuser"));
+
+ assertFalse("Matched empty password", pwdh.matches(MD5_HASHED_ENTRY_1, "".toCharArray(), null));
+ assertFalse("Matched empty password, with empty user", pwdh.matches(MD5_HASHED_ENTRY_1, " ".toCharArray(), " "));
+ assertFalse("Matched empty password, with user", pwdh.matches(MD5_HASHED_ENTRY_1, " ".toCharArray(), "someuser"));
+
+ assertFalse("Matched null password", pwdh.matches(MD5_HASHED_ENTRY_1, null, null));
+ assertFalse("Matched null password, with empty user", pwdh.matches(MD5_HASHED_ENTRY_1, null, " "));
+ assertFalse("Matched null password, with user", pwdh.matches(MD5_HASHED_ENTRY_1, null, "someuser"));
+
+
+ assertFalse("Matched wrong hashed entry", pwdh.matches(MD5_HASHED_ENTRY_1, MD5_PASSWORD_0.toCharArray(), null));
+ assertFalse("Matched wrong hashed entry, with empty user", pwdh.matches(MD5_HASHED_ENTRY_1, MD5_PASSWORD_0.toCharArray(), ""));
+ assertFalse("Matched wrong hashed entry, with user", pwdh.matches(MD5_HASHED_ENTRY_1, MD5_PASSWORD_0.toCharArray(), "someuser"));
+
+ assertFalse("Matched empty hashed entry", pwdh.matches("", MD5_PASSWORD_0.toCharArray(), null));
+ assertFalse("Matched empty hashed entry, with empty user", pwdh.matches(" ", MD5_PASSWORD_0.toCharArray(), ""));
+ assertFalse("Matched empty hashed entry, with user", pwdh.matches(" ", MD5_PASSWORD_0.toCharArray(), "someuser"));
+
+ assertFalse("Matched null entry", pwdh.matches(null, MD5_PASSWORD_0.toCharArray(), null));
+ assertFalse("Matched null entry, with empty user", pwdh.matches(null, MD5_PASSWORD_0.toCharArray(), ""));
+ assertFalse("Matched null entry, with user", pwdh.matches(null, MD5_PASSWORD_0.toCharArray(), "someuser"));
+
+
+ assertFalse("Matched wrong scheme", pwdh.matches(CMD5_HASHED_ENTRY_0, MD5_PASSWORD_0.toCharArray(), null));
+ assertFalse("Matched wrong scheme", pwdh.matches(PBKDF2_HASHED_ENTRY_0, MD5_PASSWORD_0.toCharArray(), ""));
+ assertFalse("Matched wrong scheme", pwdh.matches(CMD5_HASHED_ENTRY_0, CMD5_PASSWORD_0.toCharArray(), CMD5_USERNAME_0));
+ }
+
+ /**
+ * Test method for {@link com.gitblit.utils.PasswordHash#matches(String, char[], String)} for Combined-MD5.
+ */
+ @Test
+ public void testMatchesCombinedMD5() {
+ PasswordHash pwdh = PasswordHash.instanceOf("CMD5");
+
+ assertTrue("PWD0", pwdh.matches(CMD5_HASHED_ENTRY_0, CMD5_PASSWORD_0.toCharArray(), CMD5_USERNAME_0));
+ assertTrue("PWD1", pwdh.matches(CMD5_HASHED_ENTRY_1, CMD5_PASSWORD_1.toCharArray(), CMD5_USERNAME_1));
+ assertTrue("PWD2", pwdh.matches(CMD5_HASHED_ENTRY_2, CMD5_PASSWORD_2.toCharArray(), CMD5_USERNAME_2));
+ assertTrue("PWD3", pwdh.matches(CMD5_HASHED_ENTRY_3, CMD5_PASSWORD_3.toCharArray(), CMD5_USERNAME_3));
+
+
+
+ assertFalse("Matched wrong password", pwdh.matches(CMD5_HASHED_ENTRY_1, "wrongpassword".toCharArray(), CMD5_USERNAME_1));
+ assertFalse("Matched wrong password", pwdh.matches(CMD5_HASHED_ENTRY_1, CMD5_PASSWORD_0.toCharArray(), CMD5_USERNAME_1));
+
+ assertFalse("Matched wrong user", pwdh.matches(CMD5_HASHED_ENTRY_1, CMD5_PASSWORD_1.toCharArray(), CMD5_USERNAME_0));
+ assertFalse("Matched wrong user", pwdh.matches(CMD5_HASHED_ENTRY_1, CMD5_PASSWORD_1.toCharArray(), "Samantha Jones"));
+
+ assertFalse("Matched empty user", pwdh.matches(CMD5_HASHED_ENTRY_1, CMD5_PASSWORD_1.toCharArray(), ""));
+ assertFalse("Matched empty user", pwdh.matches(CMD5_HASHED_ENTRY_1, CMD5_PASSWORD_1.toCharArray(), " "));
+ assertFalse("Matched null user", pwdh.matches(CMD5_HASHED_ENTRY_1, CMD5_PASSWORD_1.toCharArray(), null));
+
+ assertFalse("Matched empty hashed entry", pwdh.matches("", CMD5_PASSWORD_0.toCharArray(), CMD5_USERNAME_0));
+ assertFalse("Matched empty hashed entry, with empty user", pwdh.matches(" ", CMD5_PASSWORD_1.toCharArray(), ""));
+ assertFalse("Matched empty hashed entry, with null user", pwdh.matches(" ", CMD5_PASSWORD_0.toCharArray(), null));
+
+ assertFalse("Matched null entry, with user", pwdh.matches(null, CMD5_PASSWORD_1.toCharArray(), CMD5_USERNAME_1));
+ assertFalse("Matched null entry, with empty user", pwdh.matches(null, CMD5_PASSWORD_1.toCharArray(), ""));
+ assertFalse("Matched null entry, with null user", pwdh.matches(null, CMD5_PASSWORD_1.toCharArray(), null));
+
+
+ assertFalse("Matched wrong scheme", pwdh.matches(MD5_HASHED_ENTRY_0, CMD5_PASSWORD_0.toCharArray(), null));
+ assertFalse("Matched wrong scheme", pwdh.matches(PBKDF2_HASHED_ENTRY_0, CMD5_PASSWORD_0.toCharArray(), ""));
+ assertFalse("Matched wrong scheme", pwdh.matches(MD5_HASHED_ENTRY_0, CMD5_PASSWORD_0.toCharArray(), CMD5_USERNAME_0));
+ assertFalse("Matched wrong scheme", pwdh.matches(MD5_HASHED_ENTRY_0, MD5_PASSWORD_0.toCharArray(), CMD5_USERNAME_0));
+ }
+
+
+
+ /**
+ * Test method for {@link com.gitblit.utils.PasswordHash#matches(String, char[], String)} for PBKDF2.
+ */
+ @Test
+ public void testMatchesPBKDF2() {
+ PasswordHash pwdh = PasswordHash.instanceOf("PBKDF2");
+
+ assertTrue("PWD0, Null user", pwdh.matches(PBKDF2_HASHED_ENTRY_0, PBKDF2_PASSWORD_0.toCharArray(), null));
+ assertTrue("PWD0, Empty user", pwdh.matches(PBKDF2_HASHED_ENTRY_0, PBKDF2_PASSWORD_0.toCharArray(), ""));
+ assertTrue("PWD0, With user", pwdh.matches(PBKDF2_HASHED_ENTRY_0, PBKDF2_PASSWORD_0.toCharArray(), "maxine"));
+
+ assertTrue("PWD1, Null user", pwdh.matches(PBKDF2_HASHED_ENTRY_1, PBKDF2_PASSWORD_1.toCharArray(), null));
+ assertTrue("PWD1, Empty user", pwdh.matches(PBKDF2_HASHED_ENTRY_1, PBKDF2_PASSWORD_1.toCharArray(), ""));
+ assertTrue("PWD1, With user", pwdh.matches(PBKDF2_HASHED_ENTRY_1, PBKDF2_PASSWORD_1.toCharArray(), "Maxim Gorki"));
+
+ assertTrue("PWD2, Null user", pwdh.matches(PBKDF2_HASHED_ENTRY_2, PBKDF2_PASSWORD_2.toCharArray(), null));
+ assertTrue("PWD2, Empty user", pwdh.matches(PBKDF2_HASHED_ENTRY_2, PBKDF2_PASSWORD_2.toCharArray(), ""));
+ assertTrue("PWD2, With user", pwdh.matches(PBKDF2_HASHED_ENTRY_2, PBKDF2_PASSWORD_2.toCharArray(), "Epson"));
+
+
+
+ assertFalse("Matched wrong password", pwdh.matches(PBKDF2_HASHED_ENTRY_1, "wrongpassword".toCharArray(), null));
+ assertFalse("Matched wrong password, with empty user", pwdh.matches(PBKDF2_HASHED_ENTRY_1, "wrongpassword".toCharArray(), " "));
+ assertFalse("Matched wrong password, with user", pwdh.matches(PBKDF2_HASHED_ENTRY_1, "wrongpassword".toCharArray(), "someuser"));
+
+ assertFalse("Matched empty password", pwdh.matches(PBKDF2_HASHED_ENTRY_2, "".toCharArray(), null));
+ assertFalse("Matched empty password, with empty user", pwdh.matches(PBKDF2_HASHED_ENTRY_2, " ".toCharArray(), " "));
+ assertFalse("Matched empty password, with user", pwdh.matches(PBKDF2_HASHED_ENTRY_2, " ".toCharArray(), "someuser"));
+
+ assertFalse("Matched null password", pwdh.matches(PBKDF2_HASHED_ENTRY_0, null, null));
+ assertFalse("Matched null password, with empty user", pwdh.matches(PBKDF2_HASHED_ENTRY_0, null, " "));
+ assertFalse("Matched null password, with user", pwdh.matches(PBKDF2_HASHED_ENTRY_0, null, "someuser"));
+
+
+ assertFalse("Matched wrong hashed entry", pwdh.matches(PBKDF2_HASHED_ENTRY_1, PBKDF2_PASSWORD_0.toCharArray(), null));
+ assertFalse("Matched wrong hashed entry, with empty user", pwdh.matches(PBKDF2_HASHED_ENTRY_1, PBKDF2_PASSWORD_0.toCharArray(), ""));
+ assertFalse("Matched wrong hashed entry, with user", pwdh.matches(PBKDF2_HASHED_ENTRY_1, PBKDF2_PASSWORD_0.toCharArray(), "someuser"));
+
+ assertFalse("Matched empty hashed entry", pwdh.matches("", PBKDF2_PASSWORD_0.toCharArray(), null));
+ assertFalse("Matched empty hashed entry, with empty user", pwdh.matches(" ", PBKDF2_PASSWORD_0.toCharArray(), ""));
+ assertFalse("Matched empty hashed entry, with user", pwdh.matches(" ", PBKDF2_PASSWORD_0.toCharArray(), "someuser"));
+
+ assertFalse("Matched null entry", pwdh.matches(null, PBKDF2_PASSWORD_0.toCharArray(), null));
+ assertFalse("Matched null entry, with empty user", pwdh.matches(null, PBKDF2_PASSWORD_0.toCharArray(), ""));
+ assertFalse("Matched null entry, with user", pwdh.matches(null, PBKDF2_PASSWORD_0.toCharArray(), "someuser"));
+
+
+ assertFalse("Matched wrong scheme", pwdh.matches(CMD5_HASHED_ENTRY_0, PBKDF2_PASSWORD_0.toCharArray(), null));
+ assertFalse("Matched wrong scheme", pwdh.matches(MD5_HASHED_ENTRY_0, PBKDF2_PASSWORD_0.toCharArray(), ""));
+ assertFalse("Matched wrong scheme", pwdh.matches(CMD5_HASHED_ENTRY_0, PBKDF2_PASSWORD_0.toCharArray(), CMD5_USERNAME_0));
+ }
+
+
+ /**
+ * Test method for {@link com.gitblit.utils.PasswordHash#matches(String, char[], String)}
+ * for old existing entries with the "PBKDF2WITHHMACSHA256" type identifier.
+ */
+ @Test
+ public void testMatchesPBKDF2Compat() {
+ PasswordHash pwdh = PasswordHash.instanceOf("PBKDF2");
+
+ assertTrue("PWD3, Null user", pwdh.matches(PBKDF2_HASHED_ENTRY_3, PBKDF2_PASSWORD_3.toCharArray(), null));
+ assertTrue("PWD3, Empty user", pwdh.matches(PBKDF2_HASHED_ENTRY_3, PBKDF2_PASSWORD_3.toCharArray(), ""));
+ assertTrue("PWD3, With user", pwdh.matches(PBKDF2_HASHED_ENTRY_3, PBKDF2_PASSWORD_3.toCharArray(), "maxine"));
+
+
+ assertFalse("Matched wrong password", pwdh.matches(PBKDF2_HASHED_ENTRY_3, "bar".toCharArray(), null));
+ assertFalse("Matched wrong password, with empty user", pwdh.matches(PBKDF2_HASHED_ENTRY_3, "bar".toCharArray(), " "));
+ assertFalse("Matched wrong password, with user", pwdh.matches(PBKDF2_HASHED_ENTRY_3, "bar".toCharArray(), "someuser"));
+
+ assertFalse("Matched empty password", pwdh.matches(PBKDF2_HASHED_ENTRY_3, "".toCharArray(), null));
+ assertFalse("Matched empty password, with empty user", pwdh.matches(PBKDF2_HASHED_ENTRY_3, " ".toCharArray(), " "));
+ assertFalse("Matched empty password, with user", pwdh.matches(PBKDF2_HASHED_ENTRY_3, " ".toCharArray(), "someuser"));
+
+ assertFalse("Matched null password", pwdh.matches(PBKDF2_HASHED_ENTRY_3, null, null));
+ assertFalse("Matched null password, with empty user", pwdh.matches(PBKDF2_HASHED_ENTRY_3, null, " "));
+ assertFalse("Matched null password, with user", pwdh.matches(PBKDF2_HASHED_ENTRY_3, null, "someuser"));
+
+
+ assertFalse("Matched wrong hashed entry", pwdh.matches(PBKDF2_HASHED_ENTRY_3, PBKDF2_PASSWORD_0.toCharArray(), null));
+ assertFalse("Matched wrong hashed entry, with empty user", pwdh.matches(PBKDF2_HASHED_ENTRY_3, PBKDF2_PASSWORD_0.toCharArray(), ""));
+ assertFalse("Matched wrong hashed entry, with user", pwdh.matches(PBKDF2_HASHED_ENTRY_3, PBKDF2_PASSWORD_0.toCharArray(), "someuser"));
+ }
+
+ @Test
+ public void getEntryType() {
+ assertEquals(PasswordHash.Type.MD5, PasswordHash.getEntryType("MD5:blah"));
+ assertEquals(PasswordHash.Type.MD5, PasswordHash.getEntryType("md5:blah"));
+ assertEquals(PasswordHash.Type.MD5, PasswordHash.getEntryType("mD5:blah"));
+
+ assertEquals(PasswordHash.Type.CMD5, PasswordHash.getEntryType("CMD5:blah"));
+ assertEquals(PasswordHash.Type.CMD5, PasswordHash.getEntryType("cmd5:blah"));
+ assertEquals(PasswordHash.Type.CMD5, PasswordHash.getEntryType("Cmd5:blah"));
+
+ assertEquals(PasswordHash.Type.CMD5, PasswordHash.getEntryType("combined-md5:blah"));
+ assertEquals(PasswordHash.Type.CMD5, PasswordHash.getEntryType("COMBINED-MD5:blah"));
+ assertEquals(PasswordHash.Type.CMD5, PasswordHash.getEntryType("combined-MD5:blah"));
+
+ assertEquals(PasswordHash.Type.PBKDF2, PasswordHash.getEntryType("PBKDF2:blah"));
+ assertEquals(PasswordHash.Type.PBKDF2, PasswordHash.getEntryType("pbkdf2:blah"));
+ assertEquals(PasswordHash.Type.PBKDF2, PasswordHash.getEntryType("Pbkdf2:blah"));
+ assertEquals(PasswordHash.Type.PBKDF2, PasswordHash.getEntryType("pbKDF2:blah"));
+
+ assertEquals(PasswordHash.Type.PBKDF2, PasswordHash.getEntryType("PBKDF2WithHmacSHA256:blah"));
+ assertEquals(PasswordHash.Type.PBKDF2, PasswordHash.getEntryType("PBKDF2WITHHMACSHA256:blah"));
+ }
+
+ @Test
+ public void getEntryValue() {
+ assertEquals("value", PasswordHash.getEntryValue("MD5:value"));
+ assertEquals("plain text", PasswordHash.getEntryValue("plain text"));
+ assertEquals("what this", PasswordHash.getEntryValue(":what this"));
+ assertEquals("", PasswordHash.getEntryValue(":"));
+ }
+}
diff --git a/src/test/java/com/gitblit/utils/SecureRandomTest.java b/src/test/java/com/gitblit/utils/SecureRandomTest.java
new file mode 100644
index 00000000..c4098c2f
--- /dev/null
+++ b/src/test/java/com/gitblit/utils/SecureRandomTest.java
@@ -0,0 +1,33 @@
+package com.gitblit.utils;
+
+import static org.junit.Assert.*;
+
+import java.util.Arrays;
+
+import org.junit.Test;
+
+public class SecureRandomTest {
+
+ @Test
+ public void testRandomBytes() {
+ SecureRandom sr = new SecureRandom();
+ byte[] bytes1 = sr.randomBytes(10);
+ assertEquals(10, bytes1.length);
+ byte[] bytes2 = sr.randomBytes(10);
+ assertEquals(10, bytes2.length);
+ assertFalse(Arrays.equals(bytes1, bytes2));
+
+ assertEquals(0, sr.randomBytes(0).length);
+ assertEquals(200, sr.randomBytes(200).length);
+ }
+
+ @Test
+ public void testNextBytes() {
+ SecureRandom sr = new SecureRandom();
+ byte[] bytes1 = new byte[32];
+ sr.nextBytes(bytes1);
+ byte[] bytes2 = new byte[32];
+ sr.nextBytes(bytes2);
+ assertFalse(Arrays.equals(bytes1, bytes2));
+ }
+}
diff --git a/src/test/java/com/gitblit/utils/TimeUtilsTest.java b/src/test/java/com/gitblit/utils/TimeUtilsTest.java
new file mode 100644
index 00000000..0e58a364
--- /dev/null
+++ b/src/test/java/com/gitblit/utils/TimeUtilsTest.java
@@ -0,0 +1,690 @@
+/*
+ * Copyright 2011 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.utils;
+
+import java.util.Calendar;
+import java.util.Date;
+import java.util.TimeZone;
+
+import com.gitblit.tests.GitblitUnitTest;
+import org.junit.Test;
+
+
+public class TimeUtilsTest extends GitblitUnitTest
+{
+
+ private Date offset(long subtract) {
+ return new Date(System.currentTimeMillis() - subtract);
+ }
+
+ private Date offset(long now, long subtract) {
+ return new Date(now - subtract);
+ }
+
+ @Test
+ public void testBasicTimeFunctions() throws Exception {
+ assertEquals(2, TimeUtils.minutesAgo(offset(2 * TimeUtils.MIN), false));
+ assertEquals(3, TimeUtils.minutesAgo(offset((2 * TimeUtils.MIN) + (35 * 1000L)), true));
+
+ assertEquals(2, TimeUtils.hoursAgo(offset(2 * TimeUtils.ONEHOUR), false));
+ assertEquals(3, TimeUtils.hoursAgo(offset(5 * TimeUtils.HALFHOUR), true));
+
+ assertEquals(4, TimeUtils.daysAgo(offset(4 * TimeUtils.ONEDAY)));
+ }
+
+ @Test
+ public void testToday() throws Exception {
+ assertTrue(TimeUtils.isToday(new Date(), null));
+ }
+
+ @Test
+ public void testYesterday() throws Exception {
+ assertTrue(TimeUtils.isYesterday(offset(TimeUtils.ONEDAY), null));
+ }
+
+ @Test
+ public void testDurations() throws Exception {
+ TimeUtils timeUtils = new TimeUtils();
+ assertEquals("1 day", timeUtils.duration(1));
+ assertEquals("5 days", timeUtils.duration(5));
+ assertEquals("3 months", timeUtils.duration(75));
+ assertEquals("12 months", timeUtils.duration(364));
+ assertEquals("1 year", timeUtils.duration(365 + 0));
+ assertEquals("1 year", timeUtils.duration(365 + 10));
+ assertEquals("1 year, 1 month", timeUtils.duration(365 + 15));
+ assertEquals("1 year, 1 month", timeUtils.duration(365 + 30));
+ assertEquals("1 year, 1 month", timeUtils.duration(365 + 44));
+ assertEquals("1 year, 2 months", timeUtils.duration(365 + 45));
+ assertEquals("1 year, 2 months", timeUtils.duration(365 + 60));
+
+ assertEquals("2 years", timeUtils.duration(2 * 365 + 0));
+ assertEquals("2 years", timeUtils.duration(2 * 365 + 10));
+ assertEquals("2 years, 1 month", timeUtils.duration(2 * 365 + 15));
+ assertEquals("2 years, 1 month", timeUtils.duration(2 * 365 + 30));
+ assertEquals("2 years, 1 month", timeUtils.duration(2 * 365 + 44));
+ assertEquals("2 years, 2 months", timeUtils.duration(2 * 365 + 45));
+ assertEquals("2 years, 2 months", timeUtils.duration(2 * 365 + 60));
+ }
+
+ @Test
+ public void testTimeAgo() throws Exception {
+ // standard time ago tests
+ TimeUtils timeUtils = new TimeUtils();
+ assertEquals("just now", timeUtils.timeAgo(offset(1 * TimeUtils.MIN)));
+ assertEquals("60 mins ago", timeUtils.timeAgo(offset(60 * TimeUtils.MIN)));
+ assertEquals("2 hours ago", timeUtils.timeAgo(offset(120 * TimeUtils.MIN)));
+ assertEquals("15 hours ago", timeUtils.timeAgo(offset(15 * TimeUtils.ONEHOUR)));
+ assertEquals("yesterday", timeUtils.timeAgo(offset(24 * TimeUtils.ONEHOUR)));
+ assertEquals("2 days ago", timeUtils.timeAgo(offset(2 * TimeUtils.ONEDAY)));
+ assertEquals("5 weeks ago", timeUtils.timeAgo(offset(35 * TimeUtils.ONEDAY)));
+ assertEquals("3 months ago", timeUtils.timeAgo(offset(84 * TimeUtils.ONEDAY)));
+ assertEquals("3 months ago", timeUtils.timeAgo(offset(95 * TimeUtils.ONEDAY)));
+ assertEquals("4 months ago", timeUtils.timeAgo(offset(104 * TimeUtils.ONEDAY)));
+ assertEquals("1 year ago", timeUtils.timeAgo(offset(365 * TimeUtils.ONEDAY)));
+ assertEquals("13 months ago", timeUtils.timeAgo(offset(395 * TimeUtils.ONEDAY)));
+ assertEquals("2 years ago", timeUtils.timeAgo(offset((2 * 365 + 30) * TimeUtils.ONEDAY)));
+ }
+
+ @Test
+ public void testTimeAgoCss() throws Exception {
+ // css class tests
+ TimeUtils timeUtils = new TimeUtils();
+ assertEquals("age0", timeUtils.timeAgoCss(offset(1 * TimeUtils.MIN)));
+ assertEquals("age0", timeUtils.timeAgoCss(offset(60 * TimeUtils.MIN)));
+ assertEquals("age1", timeUtils.timeAgoCss(offset(120 * TimeUtils.MIN)));
+ assertEquals("age1", timeUtils.timeAgoCss(offset(24 * TimeUtils.ONEHOUR)));
+ assertEquals("age2", timeUtils.timeAgoCss(offset(2 * TimeUtils.ONEDAY)));
+ }
+
+
+ @Test
+ public void testTimeAgoYesterday() throws Exception {
+ TimeZone myTimezone = TimeZone.getTimeZone("GMT");
+ TimeUtils timeUtils = new TimeUtils(null, myTimezone);
+
+ Calendar myCal = Calendar.getInstance(myTimezone);
+ myCal.set(Calendar.HOUR_OF_DAY, 12);
+ myCal.set(Calendar.MINUTE, 0);
+ long now = myCal.getTime().getTime();
+
+ assertNotEquals("yesterday", timeUtils.timeAgo(offset(now,23 * TimeUtils.ONEHOUR), false, now));
+ assertNotEquals("yesterday", timeUtils.timeAgo(offset(now,(23 * TimeUtils.ONEHOUR) + (29 * TimeUtils.MIN)), false, now));
+
+ assertEquals("yesterday", timeUtils.timeAgo(offset(now,(23 * TimeUtils.ONEHOUR) + (31 * TimeUtils.MIN)), false, now));
+ assertEquals("yesterday", timeUtils.timeAgo(offset(now,24 * TimeUtils.ONEHOUR), false, now));
+ assertEquals("yesterday", timeUtils.timeAgo(offset(now,35 * TimeUtils.ONEHOUR), false, now));
+ assertEquals("yesterday", timeUtils.timeAgo(offset(now,36 * TimeUtils.ONEHOUR), false, now));
+
+ assertNotEquals("yesterday", timeUtils.timeAgo(offset(now,37 * TimeUtils.ONEHOUR), false, now));
+ assertNotEquals("yesterday", timeUtils.timeAgo(offset(now,44 * TimeUtils.ONEHOUR), false, now));
+ }
+
+ @Test
+ public void testTimeAgoYesterdayCET() throws Exception {
+ TimeZone myTimezone = TimeZone.getTimeZone("CET");
+ TimeUtils timeUtils = new TimeUtils(null, myTimezone);
+
+ Calendar myCal = Calendar.getInstance(myTimezone);
+ myCal.set(Calendar.HOUR_OF_DAY, 22);
+ myCal.set(Calendar.MINUTE, 0);
+ long now = myCal.getTime().getTime();
+
+ assertNotEquals("yesterday", timeUtils.timeAgo(offset(now,23 * TimeUtils.ONEHOUR), false, now));
+ assertNotEquals("yesterday", timeUtils.timeAgo(offset(now,(23 * TimeUtils.ONEHOUR) + (29 * TimeUtils.MIN)), false, now));
+
+ assertEquals("yesterday", timeUtils.timeAgo(offset(now,(23 * TimeUtils.ONEHOUR) + (31 * TimeUtils.MIN)), false, now));
+ assertEquals("yesterday", timeUtils.timeAgo(offset(now,24 * TimeUtils.ONEHOUR), false, now));
+ assertEquals("yesterday", timeUtils.timeAgo(offset(now,36 * TimeUtils.ONEHOUR), false, now));
+ assertEquals("yesterday", timeUtils.timeAgo(offset(now,46 * TimeUtils.ONEHOUR), false, now));
+
+ assertNotEquals("yesterday", timeUtils.timeAgo(offset(now,47 * TimeUtils.ONEHOUR), false, now));
+ assertNotEquals("yesterday", timeUtils.timeAgo(offset(now,56 * TimeUtils.ONEHOUR), false, now));
+ }
+
+
+ @Test
+ public void testTimeAgoYesterdayPST() throws Exception {
+ TimeZone myTimezone = TimeZone.getTimeZone("PST");
+ TimeUtils timeUtils = new TimeUtils(null, myTimezone);
+
+ Calendar myCal = Calendar.getInstance(myTimezone);
+ myCal.set(Calendar.HOUR_OF_DAY, 8);
+ myCal.set(Calendar.MINUTE, 0);
+ long now = myCal.getTime().getTime();
+
+ assertNotEquals("yesterday", timeUtils.timeAgo(offset(now,23 * TimeUtils.ONEHOUR), false, now));
+ assertNotEquals("yesterday", timeUtils.timeAgo(offset(now,(23 * TimeUtils.ONEHOUR) + (29 * TimeUtils.MIN)), false, now));
+
+ assertEquals("yesterday", timeUtils.timeAgo(offset(now,(23 * TimeUtils.ONEHOUR) + (31 * TimeUtils.MIN)), false, now));
+ assertEquals("yesterday", timeUtils.timeAgo(offset(now,24 * TimeUtils.ONEHOUR), false, now));
+ assertEquals("yesterday", timeUtils.timeAgo(offset(now,30 * TimeUtils.ONEHOUR), false, now));
+ assertEquals("yesterday", timeUtils.timeAgo(offset(now,32 * TimeUtils.ONEHOUR), false, now));
+
+ assertNotEquals("yesterday", timeUtils.timeAgo(offset(now,33 * TimeUtils.ONEHOUR), false, now));
+ assertNotEquals("yesterday", timeUtils.timeAgo(offset(now,48 * TimeUtils.ONEHOUR), false, now));
+ }
+
+ @Test
+ public void testFrequency() {
+ assertEquals(5, TimeUtils.convertFrequencyToMinutes("2 mins", 5));
+ assertEquals(10, TimeUtils.convertFrequencyToMinutes("10 mins", 5));
+ assertEquals(600, TimeUtils.convertFrequencyToMinutes("10 hours", 5));
+ assertEquals(14400, TimeUtils.convertFrequencyToMinutes(" 10 days ", 5));
+ }
+
+
+ @Test
+ public void testTimeAgoDaysAgo() throws Exception {
+ TimeZone myTimezone = TimeZone.getTimeZone("GMT");
+ TimeUtils timeUtils = new TimeUtils(null, myTimezone);
+
+ Calendar myCal = Calendar.getInstance(myTimezone);
+ myCal.set(Calendar.HOUR_OF_DAY, 12);
+ myCal.set(Calendar.MINUTE, 0);
+ long now = myCal.getTime().getTime();
+
+ assertEquals("yesterday", timeUtils.timeAgo(offset(now,24 * TimeUtils.ONEHOUR), false, now));
+ assertEquals("yesterday", timeUtils.timeAgo(offset(now,36 * TimeUtils.ONEHOUR), false, now));
+
+ assertEquals("2 days ago", timeUtils.timeAgo(offset(now,37 * TimeUtils.ONEHOUR), false, now));
+ assertEquals("2 days ago", timeUtils.timeAgo(offset(now,48 * TimeUtils.ONEHOUR), false, now));
+ assertEquals("2 days ago", timeUtils.timeAgo(offset(now,60 * TimeUtils.ONEHOUR), false, now));
+
+ assertEquals("3 days ago", timeUtils.timeAgo(offset(now,61 * TimeUtils.ONEHOUR), false, now));
+ assertEquals("3 days ago", timeUtils.timeAgo(offset(now,72 * TimeUtils.ONEHOUR), false, now));
+ assertEquals("3 days ago", timeUtils.timeAgo(offset(now,84 * TimeUtils.ONEHOUR), false, now));
+
+ assertEquals("4 days ago", timeUtils.timeAgo(offset(now,85 * TimeUtils.ONEHOUR), false, now));
+ }
+
+
+
+ @Test
+ public void testTimeAgoDaysAgoCET() throws Exception {
+ TimeZone myTimezone = TimeZone.getTimeZone("CET");
+ TimeUtils timeUtils = new TimeUtils(null, myTimezone);
+
+ Calendar myCal = Calendar.getInstance(myTimezone);
+ myCal.set(Calendar.HOUR_OF_DAY, 8);
+ myCal.set(Calendar.MINUTE, 0);
+ long now = myCal.getTime().getTime();
+
+
+ assertEquals("yesterday", timeUtils.timeAgo(offset(now,24 * TimeUtils.ONEHOUR), false, now));
+ assertEquals("yesterday", timeUtils.timeAgo(offset(now,32 * TimeUtils.ONEHOUR), false, now));
+
+ assertEquals("2 days ago", timeUtils.timeAgo(offset(now,33 * TimeUtils.ONEHOUR), false, now));
+ assertEquals("2 days ago", timeUtils.timeAgo(offset(now,48 * TimeUtils.ONEHOUR), false, now));
+ assertEquals("2 days ago", timeUtils.timeAgo(offset(now,56 * TimeUtils.ONEHOUR), false, now));
+
+ assertEquals("3 days ago", timeUtils.timeAgo(offset(now,57 * TimeUtils.ONEHOUR), false, now));
+ assertEquals("3 days ago", timeUtils.timeAgo(offset(now,72 * TimeUtils.ONEHOUR), false, now));
+ assertEquals("3 days ago", timeUtils.timeAgo(offset(now,80 * TimeUtils.ONEHOUR), false, now));
+
+ assertEquals("4 days ago", timeUtils.timeAgo(offset(now,81 * TimeUtils.ONEHOUR), false, now));
+ }
+
+
+
+ @Test
+ public void testTimeAgoDaysAgoPST() throws Exception {
+ TimeZone myTimezone = TimeZone.getTimeZone("PST");
+ TimeUtils timeUtils = new TimeUtils(null, myTimezone);
+
+ Calendar myCal = Calendar.getInstance(myTimezone);
+ myCal.set(Calendar.HOUR_OF_DAY, 22);
+ myCal.set(Calendar.MINUTE, 0);
+ long now = myCal.getTime().getTime();
+
+ assertEquals("yesterday", timeUtils.timeAgo(offset(now,24 * TimeUtils.ONEHOUR), false, now));
+ assertEquals("yesterday", timeUtils.timeAgo(offset(now,46 * TimeUtils.ONEHOUR), false, now));
+
+ assertEquals("2 days ago", timeUtils.timeAgo(offset(now,47 * TimeUtils.ONEHOUR), false, now));
+ assertEquals("2 days ago", timeUtils.timeAgo(offset(now,48 * TimeUtils.ONEHOUR), false, now));
+ assertEquals("2 days ago", timeUtils.timeAgo(offset(now,70 * TimeUtils.ONEHOUR), false, now));
+
+ assertEquals("3 days ago", timeUtils.timeAgo(offset(now,71 * TimeUtils.ONEHOUR), false, now));
+ assertEquals("3 days ago", timeUtils.timeAgo(offset(now,72 * TimeUtils.ONEHOUR), false, now));
+ assertEquals("3 days ago", timeUtils.timeAgo(offset(now,94 * TimeUtils.ONEHOUR), false, now));
+
+ assertEquals("4 days ago", timeUtils.timeAgo(offset(now,95 * TimeUtils.ONEHOUR), false, now));
+ }
+
+
+
+ /*
+ * Test if time difference is correctly calculated in full calendar days relative to GMT.
+ */
+ @Test
+ public void testCalendarDaysAgoToGmt() {
+ TimeZone myTimezone = TimeZone.getTimeZone("GMT");
+
+ Calendar myCal = Calendar.getInstance(myTimezone);
+ myCal.set(2021, Calendar.AUGUST, 19, 12, 0, 5);
+ long now = myCal.getTime().getTime();
+ // Date from the same time zone
+ assertEquals(0, TimeUtils.calendarDaysAgo(offset(now, 11 * TimeUtils.ONEHOUR ), myTimezone, now));
+ assertEquals(0, TimeUtils.calendarDaysAgo(offset(now, 12 * TimeUtils.ONEHOUR ), myTimezone, now));
+
+ assertEquals(1, TimeUtils.calendarDaysAgo(offset(now, 12 * TimeUtils.ONEHOUR + 1 * TimeUtils.MIN), myTimezone, now));
+ assertEquals(1, TimeUtils.calendarDaysAgo(offset(now, 24 * TimeUtils.ONEHOUR ), myTimezone, now));
+ assertEquals(1, TimeUtils.calendarDaysAgo(offset(now, 36 * TimeUtils.ONEHOUR ), myTimezone, now));
+
+ assertEquals(2, TimeUtils.calendarDaysAgo(offset(now, 36 * TimeUtils.ONEHOUR + 10 * 1000), myTimezone, now));
+ assertEquals(2, TimeUtils.calendarDaysAgo(offset(now, 48 * TimeUtils.ONEHOUR ), myTimezone, now));
+ assertEquals(2, TimeUtils.calendarDaysAgo(offset(now, 60 * TimeUtils.ONEHOUR ), myTimezone, now));
+
+ assertEquals(3, TimeUtils.calendarDaysAgo(offset(now, 61 * TimeUtils.ONEHOUR ), myTimezone, now));
+
+
+ // What if we get passed a date created from a UTC timestamp that came from a different time zone?
+ // CET in August is +2 hours from GMT (CEST). So the day border is shifted two hours forward
+
+ Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("CET"));
+
+ cal.set(2021, Calendar.AUGUST, 19, 8, 0, 5);
+ Date date = cal.getTime();
+ assertEquals(0, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+ cal.set(2021, Calendar.AUGUST, 19, 2, 0, 5);
+ date = cal.getTime();
+ assertEquals(0, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+
+ cal.set(2021, Calendar.AUGUST, 19, 1, 30, 5);
+ date = cal.getTime();
+ assertEquals(1, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+ cal.set(2021, Calendar.AUGUST, 18, 12, 0, 5);
+ date = cal.getTime();
+ assertEquals(1, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+ cal.set(2021, Calendar.AUGUST, 18, 2, 0, 5);
+ date = cal.getTime();
+ assertEquals(1, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+
+ cal.set(2021, Calendar.AUGUST, 18, 0, 0, 5);
+ date = cal.getTime();
+ assertEquals(2, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+ cal.set(2021, Calendar.AUGUST, 17, 23, 0, 5);
+ date = cal.getTime();
+ assertEquals(2, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+ cal.set(2021, Calendar.AUGUST, 17, 3, 0, 5);
+ date = cal.getTime();
+ assertEquals(2, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+
+ cal.set(2021, Calendar.AUGUST, 17, 1, 0, 5);
+ date = cal.getTime();
+ assertEquals(3, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+
+ // Now we travel westwards.
+ // PST in August is -7 hours from GMT (PDT). So the day border is shifted seven hours back
+
+ cal = Calendar.getInstance(TimeZone.getTimeZone("PST"));
+ cal.clear();
+
+ cal.set(2021, Calendar.AUGUST, 19, 5, 0, 0);
+ date = cal.getTime();
+ assertEquals(0, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+ cal.set(2021, Calendar.AUGUST, 19, 0, 0, 5);
+ date = cal.getTime();
+ assertEquals(0, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+ cal.set(2021, Calendar.AUGUST, 18, 17, 0, 5);
+ date = cal.getTime();
+ assertEquals(0, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+
+ cal.set(2021, Calendar.AUGUST, 18, 16, 55, 5);
+ date = cal.getTime();
+ assertEquals(1, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+ cal.set(2021, Calendar.AUGUST, 18, 12, 0, 5);
+ date = cal.getTime();
+ assertEquals(1, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+ cal.set(2021, Calendar.AUGUST, 17, 17, 0, 5);
+ date = cal.getTime();
+ assertEquals(1, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+
+ cal.set(2021, Calendar.AUGUST, 17, 16, 0, 5);
+ date = cal.getTime();
+ assertEquals(2, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+ cal.set(2021, Calendar.AUGUST, 17, 1, 0, 5);
+ date = cal.getTime();
+ assertEquals(2, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+ cal.set(2021, Calendar.AUGUST, 16, 17, 0, 5);
+ date = cal.getTime();
+ assertEquals(2, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+
+ cal.set(2021, Calendar.AUGUST, 16, 14, 0, 5);
+ date = cal.getTime();
+ assertEquals(3, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+ }
+
+
+ /*
+ * Test if time difference is correctly calculated in full calendar days relative to CET.
+ */
+ @Test
+ public void testCalendarDaysAgoToCet() {
+ TimeZone myTimezone = TimeZone.getTimeZone("CET");
+
+ Calendar myCal = Calendar.getInstance(myTimezone);
+ myCal.set(2020, Calendar.JUNE, 5, 12, 0, 5);
+ long now = myCal.getTime().getTime();
+
+ // Date from the same time zone
+ assertEquals(0, TimeUtils.calendarDaysAgo(offset(now, 11 * TimeUtils.ONEHOUR ), myTimezone, now));
+ assertEquals(0, TimeUtils.calendarDaysAgo(offset(now, 12 * TimeUtils.ONEHOUR ), myTimezone, now));
+
+ assertEquals(1, TimeUtils.calendarDaysAgo(offset(now, 12 * TimeUtils.ONEHOUR + 1 * TimeUtils.MIN), myTimezone, now));
+ assertEquals(1, TimeUtils.calendarDaysAgo(offset(now, 24 * TimeUtils.ONEHOUR ), myTimezone, now));
+ assertEquals(1, TimeUtils.calendarDaysAgo(offset(now, 36 * TimeUtils.ONEHOUR ), myTimezone, now));
+
+ assertEquals(2, TimeUtils.calendarDaysAgo(offset(now, 36 * TimeUtils.ONEHOUR + 10 * 1000), myTimezone, now));
+ assertEquals(2, TimeUtils.calendarDaysAgo(offset(now, 48 * TimeUtils.ONEHOUR ), myTimezone, now));
+ assertEquals(2, TimeUtils.calendarDaysAgo(offset(now, 60 * TimeUtils.ONEHOUR ), myTimezone, now));
+
+ assertEquals(3, TimeUtils.calendarDaysAgo(offset(now, 61 * TimeUtils.ONEHOUR ), myTimezone, now));
+
+
+ // What if we get passed a date created from a UTC timestamp that came from a different time zone?
+ // IST in June is +3:30 hours from CEST. So the day border is shifted three and a half hours forward
+
+ Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("IST"));
+
+ cal.set(2020, Calendar.JUNE, 5, 13, 0, 5);
+ Date date = cal.getTime();
+ assertEquals(0, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+ cal.set(2020, Calendar.JUNE, 5, 3, 30, 5);
+ date = cal.getTime();
+ assertEquals(0, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+
+ cal.set(2020, Calendar.JUNE, 5, 3, 29, 5);
+ date = cal.getTime();
+ assertEquals(1, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+ cal.set(2020, Calendar.JUNE, 5, 0, 0, 0);
+ date = cal.getTime();
+ assertEquals(1, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+ cal.set(2020, Calendar.JUNE, 4, 10, 0, 0);
+ date = cal.getTime();
+ assertEquals(1, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+ cal.set(2020, Calendar.JUNE, 4, 4, 0, 5);
+ date = cal.getTime();
+ assertEquals(1, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+
+ cal.set(2020, Calendar.JUNE, 4, 3, 0, 5);
+ date = cal.getTime();
+ assertEquals(2, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+ cal.set(2020, Calendar.JUNE, 3, 12, 0, 5);
+ date = cal.getTime();
+ assertEquals(2, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+ cal.set(2020, Calendar.JUNE, 3, 4, 0, 5);
+ date = cal.getTime();
+ assertEquals(2, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+
+ cal.set(2020, Calendar.JUNE, 3, 3, 20, 5);
+ date = cal.getTime();
+ assertEquals(3, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+
+ // Now we travel westwards to New York.
+ // EST in June is -6 hours from CEST (EDT). So the day border is shifted six hours back
+
+ cal = Calendar.getInstance(TimeZone.getTimeZone("EST5EDT"));
+ cal.clear();
+
+ cal.set(2020, Calendar.JUNE, 5, 5, 0, 5);
+ date = cal.getTime();
+ assertEquals(0, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+ cal.set(2020, Calendar.JUNE, 5, 0, 0, 5);
+ date = cal.getTime();
+ assertEquals(0, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+ cal.set(2020, Calendar.JUNE, 4, 18, 0, 5);
+ date = cal.getTime();
+ assertEquals(0, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+
+ cal.set(2020, Calendar.JUNE, 4, 17, 59, 5);
+ date = cal.getTime();
+ assertEquals(1, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+ cal.set(2020, Calendar.JUNE, 4, 12, 0, 5);
+ date = cal.getTime();
+ assertEquals(1, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+ cal.set(2020, Calendar.JUNE, 3, 19, 0, 5);
+ date = cal.getTime();
+ assertEquals(1, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+
+ cal.set(2020, Calendar.JUNE, 3, 17, 0, 5);
+ date = cal.getTime();
+ assertEquals(2, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+ cal.set(2020, Calendar.JUNE, 3, 8, 0, 5);
+ date = cal.getTime();
+ assertEquals(2, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+ cal.set(2020, Calendar.JUNE, 2, 18, 0, 5);
+ date = cal.getTime();
+ assertEquals(2, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+
+ cal.set(2020, Calendar.JUNE, 2, 17, 20, 5);
+ date = cal.getTime();
+ assertEquals(3, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+ }
+
+
+ /*
+ * Test if time difference is correctly calculated in full calendar days relative to AET (Australia).
+ */
+ @Test
+ public void testCalendarDaysAgoToAet() {
+ TimeZone myTimezone = TimeZone.getTimeZone("AET");
+
+ Calendar myCal = Calendar.getInstance(myTimezone);
+ myCal.set(2022, Calendar.FEBRUARY, 22, 12, 0, 5);
+ long now = myCal.getTime().getTime();
+
+ // Date from the same time zone
+ assertEquals(0, TimeUtils.calendarDaysAgo(offset(now, 11 * TimeUtils.ONEHOUR ), myTimezone, now));
+ assertEquals(0, TimeUtils.calendarDaysAgo(offset(now, 12 * TimeUtils.ONEHOUR ), myTimezone, now));
+
+ assertEquals(1, TimeUtils.calendarDaysAgo(offset(now, 12 * TimeUtils.ONEHOUR + 1 * TimeUtils.MIN), myTimezone, now));
+ assertEquals(1, TimeUtils.calendarDaysAgo(offset(now, 24 * TimeUtils.ONEHOUR ), myTimezone, now));
+ assertEquals(1, TimeUtils.calendarDaysAgo(offset(now, 36 * TimeUtils.ONEHOUR ), myTimezone, now));
+
+ assertEquals(2, TimeUtils.calendarDaysAgo(offset(now, 36 * TimeUtils.ONEHOUR + 10 * 1000), myTimezone, now));
+ assertEquals(2, TimeUtils.calendarDaysAgo(offset(now, 48 * TimeUtils.ONEHOUR ), myTimezone, now));
+ assertEquals(2, TimeUtils.calendarDaysAgo(offset(now, 60 * TimeUtils.ONEHOUR ), myTimezone, now));
+
+ assertEquals(3, TimeUtils.calendarDaysAgo(offset(now, 61 * TimeUtils.ONEHOUR ), myTimezone, now));
+
+
+ // What if we get passed a date created from a UTC timestamp that came from a different time zone?
+ // NZ in February is +2 hours from AEDT. So the day border is shifted two hours forward
+
+ Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("NZ"));
+
+ cal.set(2022, Calendar.FEBRUARY, 22, 12, 0, 5);
+ Date date = cal.getTime();
+ assertEquals(0, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+ cal.set(2022, Calendar.FEBRUARY, 22, 2, 0, 5);
+ date = cal.getTime();
+ assertEquals(0, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+
+ cal.set(2022, Calendar.FEBRUARY, 22, 1, 45, 5);
+ date = cal.getTime();
+ assertEquals(1, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+ cal.set(2022, Calendar.FEBRUARY, 21, 22, 0, 5);
+ date = cal.getTime();
+ assertEquals(1, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+ cal.set(2022, Calendar.FEBRUARY, 21, 12, 0, 5);
+ date = cal.getTime();
+ assertEquals(1, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+ cal.set(2022, Calendar.FEBRUARY, 21, 2, 0, 5);
+ date = cal.getTime();
+ assertEquals(1, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+
+ cal.set(2022, Calendar.FEBRUARY, 21, 1, 0, 5);
+ date = cal.getTime();
+ assertEquals(2, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+ cal.set(2022, Calendar.FEBRUARY, 20, 10, 0, 5);
+ date = cal.getTime();
+ assertEquals(2, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+ cal.set(2022, Calendar.FEBRUARY, 20, 2, 0, 5);
+ date = cal.getTime();
+ assertEquals(2, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+
+ cal.set(2022, Calendar.FEBRUARY, 20, 1, 0, 5);
+ date = cal.getTime();
+ assertEquals(3, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+
+ // Now we travel westwards to Europe.
+ // CET in February is -10 hours from AEDT. So the day border is shifted ten hours back
+
+ cal = Calendar.getInstance(TimeZone.getTimeZone("CET"));
+ cal.clear();
+
+ cal.set(2022, Calendar.FEBRUARY, 22, 2, 0, 5);
+ date = cal.getTime();
+ assertEquals(0, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+ cal.set(2022, Calendar.FEBRUARY, 22, 0, 0, 5);
+ date = cal.getTime();
+ assertEquals(0, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+ cal.set(2022, Calendar.FEBRUARY, 21, 14, 0, 5);
+ date = cal.getTime();
+ assertEquals(0, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+
+ cal.set(2022, Calendar.FEBRUARY, 21, 13, 30, 5);
+ date = cal.getTime();
+ assertEquals(1, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+ cal.set(2022, Calendar.FEBRUARY, 21, 7, 0, 5);
+ date = cal.getTime();
+ assertEquals(1, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+ cal.set(2022, Calendar.FEBRUARY, 20, 15, 0, 5);
+ date = cal.getTime();
+ assertEquals(1, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+
+ cal.set(2022, Calendar.FEBRUARY, 20, 13, 59, 5);
+ date = cal.getTime();
+ assertEquals(2, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+ cal.set(2022, Calendar.FEBRUARY, 20, 1, 0, 5);
+ date = cal.getTime();
+ assertEquals(2, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+ cal.set(2022, Calendar.FEBRUARY, 19, 14, 0, 5);
+ date = cal.getTime();
+ assertEquals(2, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+
+ cal.set(2022, Calendar.FEBRUARY, 19, 9, 0, 5);
+ date = cal.getTime();
+ assertEquals(3, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+
+ // Lets continue even further west.
+ // AST in February is -15 hours from AEDT. So the day border is shifted fifteen hours back
+
+ cal = Calendar.getInstance(TimeZone.getTimeZone("America/Curacao"));
+ cal.clear();
+
+ cal.set(2022, Calendar.FEBRUARY, 21, 21, 0, 0);
+ date = cal.getTime();
+ assertEquals(0, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+ cal.set(2022, Calendar.FEBRUARY, 21, 12, 0, 5);
+ date = cal.getTime();
+ assertEquals(0, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+ cal.set(2022, Calendar.FEBRUARY, 21, 9, 0, 5);
+ date = cal.getTime();
+ assertEquals(0, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+
+ cal.set(2022, Calendar.FEBRUARY, 21, 8, 0, 5);
+ date = cal.getTime();
+ assertEquals(1, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+ cal.set(2022, Calendar.FEBRUARY, 20, 19, 0, 5);
+ date = cal.getTime();
+ assertEquals(1, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+ cal.set(2022, Calendar.FEBRUARY, 20, 10, 0, 5);
+ date = cal.getTime();
+ assertEquals(1, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+
+ cal.set(2022, Calendar.FEBRUARY, 20, 8, 50, 5);
+ date = cal.getTime();
+ assertEquals(2, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+ cal.set(2022, Calendar.FEBRUARY, 19, 17, 0, 5);
+ date = cal.getTime();
+ assertEquals(2, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+ cal.set(2022, Calendar.FEBRUARY, 19, 9, 0, 5);
+ date = cal.getTime();
+ assertEquals(2, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+
+
+ cal.set(2022, Calendar.FEBRUARY, 19, 7, 0, 5);
+ date = cal.getTime();
+ assertEquals(3, TimeUtils.calendarDaysAgo(date, myTimezone, now));
+ }
+
+}
diff --git a/src/test/java/com/gitblit/wicket/GitBlitWebAppResourceBundleTest.java b/src/test/java/com/gitblit/wicket/GitBlitWebAppResourceBundleTest.java
new file mode 100644
index 00000000..f680ee7b
--- /dev/null
+++ b/src/test/java/com/gitblit/wicket/GitBlitWebAppResourceBundleTest.java
@@ -0,0 +1,156 @@
+package com.gitblit.wicket;
+
+import org.junit.Test;
+
+import java.util.Locale;
+import java.util.ResourceBundle;
+
+import static org.junit.Assert.*;
+
+public class GitBlitWebAppResourceBundleTest
+{
+ @Test
+ public void testDefaultResource()
+ {
+ ResourceBundle bundle = ResourceBundle.getBundle("com.gitblit.wicket.GitBlitWebApp");
+ assertNotNull(bundle);
+ assertEquals("default", bundle.getString("gb.loadLang"));
+ }
+
+ @Test
+ public void testCsResource()
+ {
+ Locale l = Locale.forLanguageTag("cs");
+ ResourceBundle bundle = ResourceBundle.getBundle("com.gitblit.wicket.GitBlitWebApp", l);
+ assertNotNull(bundle);
+ assertEquals("čeština", bundle.getString("gb.loadLang"));
+ }
+
+ @Test
+ public void testDeResource()
+ {
+ Locale l = Locale.GERMAN;
+ ResourceBundle bundle = ResourceBundle.getBundle("com.gitblit.wicket.GitBlitWebApp", l);
+ assertNotNull(bundle);
+ assertEquals("Deutsch", bundle.getString("gb.loadLang"));
+ }
+
+ @Test
+ public void testEnResource()
+ {
+ Locale l = Locale.ENGLISH;
+ ResourceBundle bundle = ResourceBundle.getBundle("com.gitblit.wicket.GitBlitWebApp", l);
+ assertNotNull(bundle);
+ // The "en" file is just a placeholder for the default one.
+ assertEquals("default", bundle.getString("gb.loadLang"));
+ }
+
+ @Test
+ public void testEsResource()
+ {
+ Locale l = Locale.forLanguageTag("es");
+ ResourceBundle bundle = ResourceBundle.getBundle("com.gitblit.wicket.GitBlitWebApp", l);
+ assertNotNull(bundle);
+ assertEquals("Español", bundle.getString("gb.loadLang"));
+ }
+
+ @Test
+ public void testFrResource() throws Exception
+ {
+ Locale l = Locale.FRENCH;
+ ResourceBundle bundle = ResourceBundle.getBundle("com.gitblit.wicket.GitBlitWebApp", l);
+ assertNotNull(bundle);
+ assertEquals("français", bundle.getString("gb.loadLang"));
+ }
+
+ @Test
+ public void testItResource() throws Exception
+ {
+ Locale l = Locale.ITALIAN;
+ ResourceBundle bundle = ResourceBundle.getBundle("com.gitblit.wicket.GitBlitWebApp", l);
+ assertNotNull(bundle);
+ assertEquals("Italiano", bundle.getString("gb.loadLang"));
+ }
+
+ @Test
+ public void testJaResource() throws Exception
+ {
+ Locale l = Locale.JAPANESE;
+ ResourceBundle bundle = ResourceBundle.getBundle("com.gitblit.wicket.GitBlitWebApp", l);
+ assertNotNull(bundle);
+ assertEquals("にほんご", bundle.getString("gb.loadLang"));
+ }
+
+ @Test
+ public void testKoResource() throws Exception
+ {
+ Locale l = Locale.KOREAN;
+ ResourceBundle bundle = ResourceBundle.getBundle("com.gitblit.wicket.GitBlitWebApp", l);
+ assertNotNull(bundle);
+ assertEquals("한국어", bundle.getString("gb.loadLang"));
+ }
+
+ @Test
+ public void testNlResource() throws Exception
+ {
+ Locale l = Locale.forLanguageTag("nl");
+ ResourceBundle bundle = ResourceBundle.getBundle("com.gitblit.wicket.GitBlitWebApp", l);
+ assertNotNull(bundle);
+ assertEquals("Nederlands", bundle.getString("gb.loadLang"));
+ }
+
+ @Test
+ public void testNoResource() throws Exception
+ {
+ Locale l = Locale.forLanguageTag("no");
+ ResourceBundle bundle = ResourceBundle.getBundle("com.gitblit.wicket.GitBlitWebApp", l);
+ assertNotNull(bundle);
+ assertEquals("Norsk", bundle.getString("gb.loadLang"));
+ }
+
+ @Test
+ public void testPlResource() throws Exception
+ {
+ Locale l = Locale.forLanguageTag("pl");
+ ResourceBundle bundle = ResourceBundle.getBundle("com.gitblit.wicket.GitBlitWebApp", l);
+ assertNotNull(bundle);
+ assertEquals("polszczyzna", bundle.getString("gb.loadLang"));
+ }
+
+ @Test
+ public void testPtBrResource() throws Exception
+ {
+ Locale l = Locale.forLanguageTag("pt-BR");
+ ResourceBundle bundle = ResourceBundle.getBundle("com.gitblit.wicket.GitBlitWebApp", l);
+ assertNotNull(bundle);
+ assertEquals("Português", bundle.getString("gb.loadLang"));
+ }
+
+
+ @Test
+ public void testRuResource() throws Exception
+ {
+ Locale l = Locale.forLanguageTag("ru");
+ ResourceBundle bundle = ResourceBundle.getBundle("com.gitblit.wicket.GitBlitWebApp", l);
+ assertNotNull(bundle);
+ assertEquals("Русский", bundle.getString("gb.loadLang"));
+ }
+
+ @Test
+ public void testZhCnResource() throws Exception
+ {
+ Locale l = Locale.SIMPLIFIED_CHINESE;
+ ResourceBundle bundle = ResourceBundle.getBundle("com.gitblit.wicket.GitBlitWebApp", l);
+ assertNotNull(bundle);
+ assertEquals("汉字", bundle.getString("gb.loadLang"));
+ }
+
+ @Test
+ public void testZhTwResource() throws Exception
+ {
+ Locale l = Locale.TRADITIONAL_CHINESE;
+ ResourceBundle bundle = ResourceBundle.getBundle("com.gitblit.wicket.GitBlitWebApp", l);
+ assertNotNull(bundle);
+ assertEquals("漢字", bundle.getString("gb.loadLang"));
+ }
+} \ No newline at end of file
diff --git a/src/test/java/com/gitblit/wicket/MarkupProcessorTest.java b/src/test/java/com/gitblit/wicket/MarkupProcessorTest.java
new file mode 100644
index 00000000..2a2f5174
--- /dev/null
+++ b/src/test/java/com/gitblit/wicket/MarkupProcessorTest.java
@@ -0,0 +1,734 @@
+package com.gitblit.wicket;
+
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.gitblit.manager.IManager;
+import com.gitblit.manager.IPluginManager;
+import com.gitblit.manager.IRuntimeManager;
+import com.gitblit.models.PluginRegistry;
+import com.gitblit.tests.mock.MockGitblitContext;
+import com.gitblit.tests.mock.MockRuntimeManager;
+import org.apache.wicket.util.tester.WicketTester;
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.gitblit.Keys;
+import com.gitblit.tests.mock.MemorySettings;
+import com.gitblit.utils.JSoupXssFilter;
+import com.gitblit.wicket.MarkupProcessor.MarkupDocument;
+import ro.fortsoft.pf4j.PluginState;
+import ro.fortsoft.pf4j.PluginWrapper;
+import ro.fortsoft.pf4j.Version;
+
+public class MarkupProcessorTest
+{
+ private WicketTester tester;
+ private MockGitblitContext gbctx;
+
+ @Before
+ public void setUp()
+ {
+ IRuntimeManager rm = new MockRuntimeManager(getSettings());
+ gbctx = new MockGitblitContext();
+ gbctx.addManager(rm);
+ tester = new WicketTester(new GitBlitWebApp(null, null,
+ rm,
+ getPluginManager(),
+ null, null, null,
+ null, null, null,
+ null, null, null));
+ }
+
+
+
+ /*
+ * The unit tests for MarkupProcessor have two major goals.
+ * One is to check that links are rendered correctly, and
+ * the second one is that XSS protection is working.
+ *
+ * The proper rendering of markup for the various Wiki/Markdown
+ * languages is not in focus. This, as a secondary goal, the Wiki/Md
+ * syntax rendering can be tested to make sure that when switching to
+ * a new or updated wiki syntax library nothing breaks and the pages
+ * are still rendered correctly.
+ * Or, to make sure things actually render correctly, because currently
+ * they don't as cen be seen with reference images and wiki links,
+ * for example.
+ */
+
+
+
+ @Test
+ public void testParseMdRepoRelativeLink()
+ {
+ MarkupProcessor mp = new MarkupProcessor(getSettings(), new JSoupXssFilter());
+
+ String markup = "link: [a page](file.md)";
+ MarkupDocument mdoc = mp.parse("testrepo", "12345abcde", "main.md", markup);
+ Document doc = Jsoup.parseBodyFragment(mdoc.html);
+ String href = doc.getElementsByAttribute("href").attr("href");
+ assertEquals("Incorrect link rendered","doc/testrepo/12345abcde/file.md", href);
+ }
+
+ @Test
+ public void testParseMdRepoRelativeLinkSubfolder()
+ {
+ String psep ="%2F";
+ MarkupProcessor mp = new MarkupProcessor(getSettings(), new JSoupXssFilter());
+
+ String markup = "link: [a file](folder/file.md)";
+ MarkupDocument mdoc = mp.parse("testrepo", "12345abcde", "main.md", markup);
+ Document doc = Jsoup.parseBodyFragment(mdoc.html);
+ String href = doc.getElementsByAttribute("href").attr("href");
+ assertEquals("Incorrect link rendered","doc/testrepo/12345abcde/folder"+psep+"file.md", href);
+ }
+
+ @Test
+ public void testParseMdRepoRelativeLinkSubSubfolder()
+ {
+ String psep ="%2F";
+ MarkupProcessor mp = new MarkupProcessor(getSettings(), new JSoupXssFilter());
+
+ String markup = "link: [a file](sub/folder/file.md)";
+ MarkupDocument mdoc = mp.parse("testrepo", "12345abcde", "main.md", markup);
+ Document doc = Jsoup.parseBodyFragment(mdoc.html);
+ String href = doc.getElementsByAttribute("href").attr("href");
+ assertEquals("Incorrect link rendered","doc/testrepo/12345abcde/sub"+psep+"folder"+psep+"file.md", href);
+ }
+
+ @Test
+ public void testParseMdRepoRelativeLinkUtf8()
+ {
+ MarkupProcessor mp = new MarkupProcessor(getSettings(), new JSoupXssFilter());
+
+ String markup = "link: [a page](日本語.md)";
+ MarkupDocument mdoc = mp.parse("testrepo", "12345abcde", "main.md", markup);
+ Document doc = Jsoup.parseBodyFragment(mdoc.html);
+ String href = doc.getElementsByAttribute("href").attr("href");
+ assertEquals("Incorrect link rendered","doc/testrepo/12345abcde/%E6%97%A5%E6%9C%AC%E8%AA%9E.md", href);
+ }
+
+ @Test
+ public void testParseMdRepoRelativeLinkSubfolderUtf8()
+ {
+ String psep ="%2F";
+ MarkupProcessor mp = new MarkupProcessor(getSettings(), new JSoupXssFilter());
+
+ String markup = "link: [a file](folder/receitas_de_culinária/file.md)";
+ MarkupDocument mdoc = mp.parse("testrepo", "12345abcde", "main.md", markup);
+ Document doc = Jsoup.parseBodyFragment(mdoc.html);
+ String href = doc.getElementsByAttribute("href").attr("href");
+ assertEquals("Incorrect link rendered","doc/testrepo/12345abcde/folder"+psep+"receitas_de_culin%C3%A1ria"+psep+"file.md", href);
+ }
+
+
+ @Test
+ public void testParseMdExternalLink()
+ {
+ MarkupProcessor mp = new MarkupProcessor(getSettings(), new JSoupXssFilter());
+
+ String markup = "link: [a website](http://example.com/page.html)";
+ MarkupDocument mdoc = mp.parse("testrepo", "12345abcde", "main.md", markup);
+ Document doc = Jsoup.parseBodyFragment(mdoc.html);
+ String href = doc.getElementsByAttribute("href").attr("href");
+ assertEquals("Incorrect link rendered","http://example.com/page.html", href);
+ }
+
+ @Test
+ public void testParseMdExternalLinkBare()
+ {
+ MarkupProcessor mp = new MarkupProcessor(getSettings(), new JSoupXssFilter());
+
+ String markup = "link: <http://example.com/page.html>";
+ MarkupDocument mdoc = mp.parse("testrepo", "12345abcde", "main.md", markup);
+ Document doc = Jsoup.parseBodyFragment(mdoc.html);
+ String href = doc.getElementsByAttribute("href").attr("href");
+ assertEquals("Incorrect link rendered","http://example.com/page.html", href);
+ }
+
+
+ // We leave it up to the document author to write working links in the document
+ @Test
+ public void testParseMdExternalLinkUtf8()
+ {
+ MarkupProcessor mp = new MarkupProcessor(getSettings(), new JSoupXssFilter());
+
+ String markup = "link: [Japanese](http://example.com/lang/日本語.html)";
+ MarkupDocument mdoc = mp.parse("testrepo", "12345abcde", "main.md", markup);
+ Document doc = Jsoup.parseBodyFragment(mdoc.html);
+ String href = doc.getElementsByAttribute("href").attr("href");
+ assertEquals("Incorrect link rendered","http://example.com/lang/日本語.html", href);
+ }
+
+
+
+ @Test
+ public void testParseMdRepoRelativeRefLink()
+ {
+ MarkupProcessor mp = new MarkupProcessor(getSettings(), new JSoupXssFilter());
+
+ String markup = "link: [a page][1]\n\n[1]: file.md";
+ MarkupDocument mdoc = mp.parse("testrepo", "12345abcde", "main.md", markup);
+ Document doc = Jsoup.parseBodyFragment(mdoc.html);
+ String href = doc.getElementsByAttribute("href").attr("href");
+ assertEquals("Incorrect link rendered","doc/testrepo/12345abcde/file.md", href);
+ }
+
+ @Test
+ public void testParseMdRepoRelativeRefLinkSubfolder()
+ {
+ String psep ="%2F";
+ MarkupProcessor mp = new MarkupProcessor(getSettings(), new JSoupXssFilter());
+
+ String markup = "link: [a file][file]\n\n[file]: folder/file.md";
+ MarkupDocument mdoc = mp.parse("testrepo", "12345abcde", "main.md", markup);
+ Document doc = Jsoup.parseBodyFragment(mdoc.html);
+ String href = doc.getElementsByAttribute("href").attr("href");
+ assertEquals("Incorrect link rendered","doc/testrepo/12345abcde/folder"+psep+"file.md", href);
+ }
+
+ @Test
+ public void testParseMdRepoRelativeRefLinkSubSubfolder()
+ {
+ String psep ="%2F";
+ MarkupProcessor mp = new MarkupProcessor(getSettings(), new JSoupXssFilter());
+
+ String markup = "link: [a file][l1] \n\n[l1]: sub/folder/file.md";
+ MarkupDocument mdoc = mp.parse("testrepo", "12345abcde", "main.md", markup);
+ Document doc = Jsoup.parseBodyFragment(mdoc.html);
+ String href = doc.getElementsByAttribute("href").attr("href");
+ assertEquals("Incorrect link rendered","doc/testrepo/12345abcde/sub"+psep+"folder"+psep+"file.md", href);
+ }
+
+ @Test
+ public void testParseMdRepoRelativeRefLinkUtf8()
+ {
+ MarkupProcessor mp = new MarkupProcessor(getSettings(), new JSoupXssFilter());
+
+ String markup = "link: [a page][x]\n\n[x]: 日本語.md";
+ MarkupDocument mdoc = mp.parse("testrepo", "12345abcde", "main.md", markup);
+ Document doc = Jsoup.parseBodyFragment(mdoc.html);
+ String href = doc.getElementsByAttribute("href").attr("href");
+ assertEquals("Incorrect link rendered","doc/testrepo/12345abcde/%E6%97%A5%E6%9C%AC%E8%AA%9E.md", href);
+ }
+
+ @Test
+ public void testParseMdRepoRelativeRefLinkSubfolderUtf8()
+ {
+ String psep ="%2F";
+ MarkupProcessor mp = new MarkupProcessor(getSettings(), new JSoupXssFilter());
+
+ String markup = "link: [a file][xy]\n\n[xy]: folder/receitas_de_culinária/file.md";
+ MarkupDocument mdoc = mp.parse("testrepo", "12345abcde", "main.md", markup);
+ Document doc = Jsoup.parseBodyFragment(mdoc.html);
+ String href = doc.getElementsByAttribute("href").attr("href");
+ assertEquals("Incorrect link rendered","doc/testrepo/12345abcde/folder"+psep+"receitas_de_culin%C3%A1ria"+psep+"file.md", href);
+ }
+
+
+ @Test
+ public void testParseMdExternalRefLink()
+ {
+ MarkupProcessor mp = new MarkupProcessor(getSettings(), new JSoupXssFilter());
+
+ String markup = "link: [a website][ex]\n\n[ex]: http://example.com/page.html";
+ MarkupDocument mdoc = mp.parse("testrepo", "12345abcde", "main.md", markup);
+ Document doc = Jsoup.parseBodyFragment(mdoc.html);
+ String href = doc.getElementsByAttribute("href").attr("href");
+ assertEquals("Incorrect link rendered","http://example.com/page.html", href);
+ }
+
+
+
+
+
+ /*
+ * Apparently wiki style links currently do not work in Markdown.
+
+ @Test
+ public void testParseMdWikiLink()
+ {
+ MarkupProcessor mp = new MarkupProcessor(getSettings(), new JSoupXssFilter());
+
+ String markup = "link: [[page]]";
+ MarkupDocument mdoc = mp.parse("testrepo", "12345abcde", "main.md", markup);
+ Document doc = Jsoup.parseBodyFragment(mdoc.html);
+ String href = doc.getElementsByAttribute("href").attr("href");
+ assertEquals("Incorrect link rendered","doc/testrepo/12345abcde/page", href);
+ }
+ */
+
+
+ @Test
+ public void testParseMdRepoRelativeImage()
+ {
+ MarkupProcessor mp = new MarkupProcessor(getSettings(), new JSoupXssFilter());
+
+ String markup = "image: ![a graphic](graphic.gif)";
+ MarkupDocument mdoc = mp.parse("testrepo", "12345abcde", "main.md", markup);
+ Document doc = Jsoup.parseBodyFragment(mdoc.html);
+ String ref = doc.getElementsByAttribute("src").attr("src");
+ assertFalse("No reference attribute found: " + mdoc.html, ref.isEmpty());
+ assertEquals("Incorrect image src rendered","../raw/testrepo/12345abcde/graphic.gif", ref);
+
+ markup = "image: ![a graphic](graphic.gif \"Some graphic\")";
+ mdoc = mp.parse("testrepo", "12345abcde", "main.md", markup);
+ doc = Jsoup.parseBodyFragment(mdoc.html);
+ ref = doc.getElementsByAttribute("src").attr("src");
+ assertFalse("No reference attribute found: " + mdoc.html, ref.isEmpty());
+ assertEquals("Incorrect image src rendered","../raw/testrepo/12345abcde/graphic.gif", ref);
+ }
+
+ @Test
+ public void testParseMdRepoRelativeImageUtf8()
+ {
+ MarkupProcessor mp = new MarkupProcessor(getSettings(), new JSoupXssFilter());
+
+ String markup = "image: ![look the dog](ドッグ.gif)";
+ MarkupDocument mdoc = mp.parse("testrepo", "12345abcde", "main.md", markup);
+ Document doc = Jsoup.parseBodyFragment(mdoc.html);
+ String ref = doc.getElementsByAttribute("src").attr("src");
+ assertFalse("No reference attribute found: " + mdoc.html, ref.isEmpty());
+ assertEquals("Incorrect image src rendered","../raw/testrepo/12345abcde/%E3%83%89%E3%83%83%E3%82%B0.gif", ref);
+
+ markup = "image: ![look the dog](ドッグ.gif \"シーバ\")";
+ mdoc = mp.parse("testrepo", "12345abcde", "main.md", markup);
+ doc = Jsoup.parseBodyFragment(mdoc.html);
+ ref = doc.getElementsByAttribute("src").attr("src");
+ assertFalse("No reference attribute found: " + mdoc.html, ref.isEmpty());
+ assertEquals("Incorrect image src rendered","../raw/testrepo/12345abcde/%E3%83%89%E3%83%83%E3%82%B0.gif", ref);
+ }
+
+ @Test
+ public void testParseMdRepoRelativeImageSubfolder()
+ {
+ String psep ="!";
+ MarkupProcessor mp = new MarkupProcessor(getSettings(), new JSoupXssFilter());
+
+ String markup = "image: ![a graphic](results/graphic.gif)";
+ MarkupDocument mdoc = mp.parse("testrepo", "12345abcde", "main.md", markup);
+ Document doc = Jsoup.parseBodyFragment(mdoc.html);
+ String ref = doc.getElementsByAttribute("src").attr("src");
+ assertFalse("No reference attribute found: " + mdoc.html, ref.isEmpty());
+ assertEquals("Incorrect image src rendered","../raw/testrepo/12345abcde/results"+psep+"graphic.gif", ref);
+
+ markup = "image: ![a graphic](results/graphic.gif \"Some graphic\")";
+ mdoc = mp.parse("testrepo", "12345abcde", "main.md", markup);
+ doc = Jsoup.parseBodyFragment(mdoc.html);
+ ref = doc.getElementsByAttribute("src").attr("src");
+ assertFalse("No reference attribute found: " + mdoc.html, ref.isEmpty());
+ assertEquals("Incorrect image src rendered","../raw/testrepo/12345abcde/results"+psep+"graphic.gif", ref);
+ }
+
+ @Test
+ public void testParseMdRepoRelativeImageSubfolderUtf8()
+ {
+ String psep ="!";
+ MarkupProcessor mp = new MarkupProcessor(getSettings(), new JSoupXssFilter());
+
+ String markup = "image: ![a cat](folder/картинки/cat.jpg)";
+ MarkupDocument mdoc = mp.parse("testrepo", "12345abcde", "main.md", markup);
+ Document doc = Jsoup.parseBodyFragment(mdoc.html);
+ String ref = doc.getElementsByAttribute("src").attr("src");
+ assertFalse("No reference attribute found: " + mdoc.html, ref.isEmpty());
+ assertEquals("Incorrect image src rendered","../raw/testrepo/12345abcde/folder"+psep+"%D0%BA%D0%B0%D1%80%D1%82%D0%B8%D0%BD%D0%BA%D0%B8"+psep+"cat.jpg", ref);
+
+ markup = "image: ![a cat](folder/картинки/cat.jpg \"Кошка\")";
+ mdoc = mp.parse("testrepo", "12345abcde", "main.md", markup);
+ doc = Jsoup.parseBodyFragment(mdoc.html);
+ ref = doc.getElementsByAttribute("src").attr("src");
+ assertFalse("No reference attribute found: " + mdoc.html, ref.isEmpty());
+ assertEquals("Incorrect image src rendered","../raw/testrepo/12345abcde/folder"+psep+"%D0%BA%D0%B0%D1%80%D1%82%D0%B8%D0%BD%D0%BA%D0%B8"+psep+"cat.jpg", ref);
+ }
+
+
+ @Test
+ public void testParseMdExternalImage()
+ {
+ MarkupProcessor mp = new MarkupProcessor(getSettings(), new JSoupXssFilter());
+
+ String markup = "image: ![a cat](http://example.com/cats/meow.jpg)";
+ MarkupDocument mdoc = mp.parse("testrepo", "12345abcde", "main.md", markup);
+ Document doc = Jsoup.parseBodyFragment(mdoc.html);
+ String ref = doc.getElementsByAttribute("src").attr("src");
+ assertFalse("No reference attribute found: " + mdoc.html, ref.isEmpty());
+ assertEquals("Incorrect image src rendered","http://example.com/cats/meow.jpg", ref);
+
+ markup = "image: ![a cat](http://example.com/cats/meow.jpg \"Miau\")";
+ mdoc = mp.parse("testrepo", "12345abcde", "main.md", markup);
+ doc = Jsoup.parseBodyFragment(mdoc.html);
+ ref = doc.getElementsByAttribute("src").attr("src");
+ assertFalse("No reference attribute found: " + mdoc.html, ref.isEmpty());
+ assertEquals("Incorrect image src rendered","http://example.com/cats/meow.jpg", ref);
+ }
+
+
+
+
+
+
+ @Test
+ public void testParseMdRepoRelativeRefImage()
+ {
+ MarkupProcessor mp = new MarkupProcessor(getSettings(), new JSoupXssFilter());
+
+ String markup = "image: ![a graphic][1]\n\n[1]: graphic.gif";
+ MarkupDocument mdoc = mp.parse("testrepo", "12345abcde", "main.md", markup);
+ Document doc = Jsoup.parseBodyFragment(mdoc.html);
+ String ref = doc.getElementsByAttribute("src").attr("src");
+ assertFalse("No reference attribute found: " + mdoc.html, ref.isEmpty());
+ assertEquals("Incorrect image src rendered","../raw/testrepo/12345abcde/graphic.gif", ref);
+
+ markup = "image: ![a graphic][2]\n\n[2]: graphic.gif \"Some graphic\"";
+ mdoc = mp.parse("testrepo", "12345abcde", "main.md", markup);
+ doc = Jsoup.parseBodyFragment(mdoc.html);
+ ref = doc.getElementsByAttribute("src").attr("src");
+ assertFalse("No reference attribute found: " + mdoc.html, ref.isEmpty());
+ assertEquals("Incorrect image src rendered","../raw/testrepo/12345abcde/graphic.gif", ref);
+ }
+
+ @Test
+ public void testParseMdRepoRelativeRefImageUtf8()
+ {
+ MarkupProcessor mp = new MarkupProcessor(getSettings(), new JSoupXssFilter());
+
+ String markup = "image: ![look the dog][1]\n\n[1]: ドッグ.gif";
+ MarkupDocument mdoc = mp.parse("testrepo", "12345abcde", "main.md", markup);
+ Document doc = Jsoup.parseBodyFragment(mdoc.html);
+ String ref = doc.getElementsByAttribute("src").attr("src");
+ assertFalse("No reference attribute found: " + mdoc.html, ref.isEmpty());
+ assertEquals("Incorrect image src rendered","../raw/testrepo/12345abcde/%E3%83%89%E3%83%83%E3%82%B0.gif", ref);
+
+ markup = "image: ![look the dog][1]\n\n[1]: ドッグ.gif \"シーバ\"";
+ mdoc = mp.parse("testrepo", "12345abcde", "main.md", markup);
+ doc = Jsoup.parseBodyFragment(mdoc.html);
+ ref = doc.getElementsByAttribute("src").attr("src");
+ assertFalse("No reference attribute found: " + mdoc.html, ref.isEmpty());
+ assertEquals("Incorrect image src rendered","../raw/testrepo/12345abcde/%E3%83%89%E3%83%83%E3%82%B0.gif", ref);
+ }
+
+ @Test
+ public void testParseMdRepoRelativeRefImageSubfolder()
+ {
+ String psep ="!";
+ MarkupProcessor mp = new MarkupProcessor(getSettings(), new JSoupXssFilter());
+
+ String markup = "image: ![a cat][cat]\n\n[cat]: folder/cat.jpg";
+ MarkupDocument mdoc = mp.parse("testrepo", "12345abcde", "main.md", markup);
+ Document doc = Jsoup.parseBodyFragment(mdoc.html);
+ String ref = doc.getElementsByAttribute("src").attr("src");
+ assertFalse("No reference attribute found: " + mdoc.html, ref.isEmpty());
+ assertEquals("Incorrect image src rendered","../raw/testrepo/12345abcde/folder"+psep+"cat.jpg", ref);
+
+ markup = "image: ![a cat][cat]\n\n[cat]: folder/cat.jpg \"Кошка\"";
+ mdoc = mp.parse("testrepo", "12345abcde", "main.md", markup);
+ doc = Jsoup.parseBodyFragment(mdoc.html);
+ ref = doc.getElementsByAttribute("src").attr("src");
+ assertFalse("No reference attribute found: " + mdoc.html, ref.isEmpty());
+ assertEquals("Incorrect image src rendered","../raw/testrepo/12345abcde/folder"+psep+"cat.jpg", ref);
+ }
+
+ @Test
+ public void testParseMdRepoRelativeRefImageSubfolderUtf8()
+ {
+ String psep ="!";
+ MarkupProcessor mp = new MarkupProcessor(getSettings(), new JSoupXssFilter());
+
+ String markup = "image: ![a cat][i1]\n\n[i1]: folder/картинки/cat.jpg";
+ MarkupDocument mdoc = mp.parse("testrepo", "12345abcde", "main.md", markup);
+ Document doc = Jsoup.parseBodyFragment(mdoc.html);
+ String ref = doc.getElementsByAttribute("src").attr("src");
+ assertFalse("No reference attribute found: " + mdoc.html, ref.isEmpty());
+ assertEquals("Incorrect image src rendered","../raw/testrepo/12345abcde/folder"+psep+"%D0%BA%D0%B0%D1%80%D1%82%D0%B8%D0%BD%D0%BA%D0%B8"+psep+"cat.jpg", ref);
+
+ markup = "image: ![a cat][i1]\n\n[i1]: folder/картинки/cat.jpg \"Кошка\"";
+ mdoc = mp.parse("testrepo", "12345abcde", "main.md", markup);
+ doc = Jsoup.parseBodyFragment(mdoc.html);
+ ref = doc.getElementsByAttribute("src").attr("src");
+ assertFalse("No reference attribute found: " + mdoc.html, ref.isEmpty());
+ assertEquals("Incorrect image src rendered","../raw/testrepo/12345abcde/folder"+psep+"%D0%BA%D0%B0%D1%80%D1%82%D0%B8%D0%BD%D0%BA%D0%B8"+psep+"cat.jpg", ref);
+ }
+
+
+ @Test
+ public void testParseMdExternalRefImage()
+ {
+ MarkupProcessor mp = new MarkupProcessor(getSettings(), new JSoupXssFilter());
+
+ String markup = "image: ![a cat][1]\n\n[1]: http://example.com/cats/meow.jpg";
+ MarkupDocument mdoc = mp.parse("testrepo", "12345abcde", "main.md", markup);
+ Document doc = Jsoup.parseBodyFragment(mdoc.html);
+ String ref = doc.getElementsByAttribute("src").attr("src");
+ assertFalse("No reference attribute found: " + mdoc.html, ref.isEmpty());
+ assertEquals("Incorrect image src rendered","http://example.com/cats/meow.jpg", ref);
+
+ markup = "image: ![a cat][1]\n\n[1]: http://example.com/cats/meow.jpg \"Miau\"";
+ mdoc = mp.parse("testrepo", "12345abcde", "main.md", markup);
+ doc = Jsoup.parseBodyFragment(mdoc.html);
+ ref = doc.getElementsByAttribute("src").attr("src");
+ assertFalse("No reference attribute found: " + mdoc.html, ref.isEmpty());
+ assertEquals("Incorrect image src rendered","http://example.com/cats/meow.jpg", ref);
+ }
+
+
+
+
+
+ @Test
+ public void testParseMediaWikiRepoRelativeLink()
+ {
+ MarkupProcessor mp = new MarkupProcessor(getSettings(), new JSoupXssFilter());
+
+ String psep = "%2F";
+ String markup = "link: [[document]]";
+ MarkupDocument mdoc = mp.parse("wikirepo", "12345abcde", "main.mw", markup);
+ Document doc = Jsoup.parseBodyFragment(mdoc.html);
+ String ref = doc.getElementsByAttribute("href").attr("href");
+ assertFalse("No reference attribute found: " + mdoc.html, ref.isEmpty());
+ assertEquals("Incorrect link href rendered","doc/wikirepo/12345abcde/wiki"+ psep + "document", ref);
+
+ }
+
+ @Test
+ public void testParseMediaWikiRepoRelativeLinkUtf8()
+ {
+ MarkupProcessor mp = new MarkupProcessor(getSettings(), new JSoupXssFilter());
+
+ String psep = "%2F";
+ String markup = "link: [[日本語]]";
+ MarkupDocument mdoc = mp.parse("wikirepo", "12345abcde", "main.mw", markup);
+ Document doc = Jsoup.parseBodyFragment(mdoc.html);
+ String href = doc.getElementsByAttribute("href").attr("href");
+ assertEquals("Incorrect link rendered","doc/wikirepo/12345abcde/wiki" + psep + "%E6%97%A5%E6%9C%AC%E8%AA%9E", href);
+ }
+
+
+ @Test
+ public void testParseMediaWikiExternalLink()
+ {
+ MarkupProcessor mp = new MarkupProcessor(getSettings(), new JSoupXssFilter());
+
+ String psep = "%2F";
+ String markup = "link: [http://example.com/some/document.html document]";
+ MarkupDocument mdoc = mp.parse("wikirepo", "12345abcde", "main.mw", markup);
+ Document doc = Jsoup.parseBodyFragment(mdoc.html);
+ String ref = doc.getElementsByAttribute("href").attr("href");
+ assertFalse("No reference attribute found: " + mdoc.html, ref.isEmpty());
+ assertEquals("Incorrect link href rendered","http://example.com/some/document.html", ref);
+
+ }
+
+ @Test
+ public void testParseMediaWikiExternalLinkNumbered()
+ {
+ MarkupProcessor mp = new MarkupProcessor(getSettings(), new JSoupXssFilter());
+
+ String psep = "%2F";
+ String markup = "link: [http://example.com/some/document.html]";
+ MarkupDocument mdoc = mp.parse("wikirepo", "12345abcde", "main.mw", markup);
+ Document doc = Jsoup.parseBodyFragment(mdoc.html);
+ String ref = doc.getElementsByAttribute("href").attr("href");
+ assertFalse("No reference attribute found: " + mdoc.html, ref.isEmpty());
+ assertEquals("Incorrect link href rendered","http://example.com/some/document.html", ref);
+
+ }
+
+ @Test
+ public void testParseMediaWikiExternalLinkBare()
+ {
+ MarkupProcessor mp = new MarkupProcessor(getSettings(), new JSoupXssFilter());
+
+ String psep = "%2F";
+ String markup = "link: http://example.com/some/document.html";
+ MarkupDocument mdoc = mp.parse("wikirepo", "12345abcde", "main.mw", markup);
+ Document doc = Jsoup.parseBodyFragment(mdoc.html);
+ String ref = doc.getElementsByAttribute("href").attr("href");
+ assertFalse("No reference attribute found: " + mdoc.html, ref.isEmpty());
+ assertEquals("Incorrect link href rendered","http://example.com/some/document.html", ref);
+
+ }
+
+ // We leave it up to the document author to write working links in the document
+ @Test
+ public void testParseMediaWikiExternalLinkUtf8()
+ {
+ MarkupProcessor mp = new MarkupProcessor(getSettings(), new JSoupXssFilter());
+
+ String psep = "%2F";
+ String markup = "link: [http://example.com/lang/日本語.html Japanese]";
+ MarkupDocument mdoc = mp.parse("wikirepo", "12345abcde", "main.mw", markup);
+ Document doc = Jsoup.parseBodyFragment(mdoc.html);
+ String href = doc.getElementsByAttribute("href").attr("href");
+ assertEquals("Incorrect link rendered","http://example.com/lang/日本語.html", href);
+ }
+
+
+
+
+ private MemorySettings getSettings()
+ {
+ Map<String, Object> backingMap = new HashMap<String, Object>();
+
+ backingMap.put(Keys.web.documents, "readme");
+ backingMap.put(Keys.web.blobEncodings, "UTF-8 ISO-8859-1");
+ backingMap.put(Keys.web.confluenceExtensions, "confluence");
+ backingMap.put(Keys.web.markdownExtensions, "md mkd markdown MD MKD");
+ backingMap.put(Keys.web.mediawikiExtensions, "mw mediawiki");
+ backingMap.put(Keys.web.textileExtensions, "textile");
+ backingMap.put(Keys.web.tracwikiExtensions, "tracwiki");
+ backingMap.put(Keys.web.twikiExtensions, "twiki");
+ backingMap.put(Keys.web.forwardSlashCharacter, "/");
+ backingMap.put(Keys.web.mountParameters, true);
+
+ MemorySettings ms = new MemorySettings(backingMap);
+ return ms;
+ }
+
+
+ private IPluginManager getPluginManager()
+ {
+ return new IPluginManager()
+ {
+ @Override
+ public Version getSystemVersion()
+ {
+ return null;
+ }
+
+ @Override
+ public void startPlugins()
+ {
+
+ }
+
+ @Override
+ public void stopPlugins()
+ {
+
+ }
+
+ @Override
+ public PluginState startPlugin(String pluginId)
+ {
+ return null;
+ }
+
+ @Override
+ public PluginState stopPlugin(String pluginId)
+ {
+ return null;
+ }
+
+ @Override
+ public List<Class<?>> getExtensionClasses(String pluginId)
+ {
+ return null;
+ }
+
+ @Override
+ public <T> List<T> getExtensions(Class<T> type)
+ {
+ return null;
+ }
+
+ @Override
+ public List<PluginWrapper> getPlugins()
+ {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public PluginWrapper getPlugin(String pluginId)
+ {
+ return null;
+ }
+
+ @Override
+ public PluginWrapper whichPlugin(Class<?> clazz)
+ {
+ return null;
+ }
+
+ @Override
+ public boolean disablePlugin(String pluginId)
+ {
+ return false;
+ }
+
+ @Override
+ public boolean enablePlugin(String pluginId)
+ {
+ return false;
+ }
+
+ @Override
+ public boolean uninstallPlugin(String pluginId)
+ {
+ return false;
+ }
+
+ @Override
+ public boolean refreshRegistry(boolean verifyChecksum)
+ {
+ return false;
+ }
+
+ @Override
+ public boolean installPlugin(String url, boolean verifyChecksum) throws IOException
+ {
+ return false;
+ }
+
+ @Override
+ public boolean upgradePlugin(String pluginId, String url, boolean verifyChecksum) throws IOException
+ {
+ return false;
+ }
+
+ @Override
+ public List<PluginRegistry.PluginRegistration> getRegisteredPlugins()
+ {
+ return null;
+ }
+
+ @Override
+ public List<PluginRegistry.PluginRegistration> getRegisteredPlugins(PluginRegistry.InstallState state)
+ {
+ return null;
+ }
+
+ @Override
+ public PluginRegistry.PluginRegistration lookupPlugin(String idOrName)
+ {
+ return null;
+ }
+
+ @Override
+ public PluginRegistry.PluginRelease lookupRelease(String idOrName, String version)
+ {
+ return null;
+ }
+
+ @Override
+ public IManager start()
+ {
+ return null;
+ }
+
+ @Override
+ public IManager stop()
+ {
+ return null;
+ }
+ };
+ }
+
+}
diff --git a/src/test/java/com/gitblit/wicket/panels/TreeNodeModelTest.java b/src/test/java/com/gitblit/wicket/panels/TreeNodeModelTest.java
new file mode 100644
index 00000000..449688b0
--- /dev/null
+++ b/src/test/java/com/gitblit/wicket/panels/TreeNodeModelTest.java
@@ -0,0 +1,49 @@
+package com.gitblit.wicket.panels;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.TreeNodeModel;
+
+public class TreeNodeModelTest {
+
+ @Test
+ public void testContainsSubFolder() {
+ TreeNodeModel tree = new TreeNodeModel();
+ tree.add("foo").add("bar").add("baz");
+
+ assertTrue(tree.containsSubFolder("foo/bar/baz"));
+ assertTrue(tree.containsSubFolder("foo/bar"));
+ assertFalse(tree.containsSubFolder("foo/bar/blub"));
+ }
+
+ @Test
+ public void testAddInHierarchy() {
+ TreeNodeModel tree = new TreeNodeModel();
+ tree.add("foo").add("bar");
+
+ RepositoryModel model = new RepositoryModel("test","","",null);
+
+ // add model to non-existing folder. should be created automatically
+ tree.add("foo/bar/baz", model);
+ tree.add("another/non/existing/folder", model);
+
+ assertTrue(tree.containsSubFolder("foo/bar/baz"));
+ assertTrue(tree.containsSubFolder("another/non/existing/folder"));
+ }
+
+ @Test
+ public void testGetDepth() {
+ TreeNodeModel tree = new TreeNodeModel();
+ TreeNodeModel bar = tree.add("foo").add("bar").add("baz");
+
+ assertEquals(0, tree.getDepth());
+ assertEquals(3, bar.getDepth());
+ }
+
+
+}
diff --git a/src/test/resources/ldap/users.conf b/src/test/resources/ldap/users.conf
index 7d1e3197..a2390fa9 100644
--- a/src/test/resources/ldap/users.conf
+++ b/src/test/resources/ldap/users.conf
@@ -10,7 +10,7 @@
displayName = Mrs. User Three
emailAddress = userthree@gitblit.com
accountType = LDAP
- role = "#admin"
+ role = "#none"
[user "userfive"]
password = "#externalAccount"
cookie = 220bafef069b8b399b2597644015b6b0f4667982
@@ -31,7 +31,7 @@
displayName = Mr. User Two
emailAddress = usertwo@gitblit.com
accountType = LDAP
- role = "#admin"
+ role = "#none"
[user "basic"]
password = MD5:f17aaabc20bfe045075927934fed52d2
cookie = dd94709528bb1c83d08f3088d4043f4742891f4f
@@ -63,6 +63,6 @@
user = userthree
user = userfour
[team "Git Admins"]
- role = "#none"
+ role = "#admin"
accountType = LOCAL
user = usertwo