Latest Threat Research:SANDWORM_MODE: Shai-Hulud-Style npm Worm Hijacks CI Workflows and Poisons AI Toolchains.Details
Socket
Book a DemoInstallSign in
Socket

www.github.com/gitblit/gitblit.git

Package Overview
Dependencies
Versions
37
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

www.github.com/gitblit/gitblit.git - npm Package Compare versions

Comparing version
v1.8.0
to
v1.9.0
+51
.circleci/config.yml
gbsteps: &gbsteps
steps:
- run:
name: Report on build environment
command: |
java -version
javac -version
- checkout
- run: ant
- run: ant test
- run:
name: Collect test results
command: |
mkdir -p test_reports/junit/
cp -a build/tests/TEST-*.xml test_reports/junit/
- store_test_results:
path: test_reports
- store_artifacts:
path: build/target/reports
version: 2
jobs:
buildJ8:
docker:
- image: circleci/openjdk:8-jdk
<<: *gbsteps
buildJ9:
docker:
- image: circleci/openjdk:9-jdk
<<: *gbsteps
buildJ11:
docker:
- image: circleci/openjdk:11-jdk
<<: *gbsteps
workflows:
version: 2
build:
jobs:
- "buildJ8"
- "buildJ9"
- "buildJ11"

Sorry, the diff of this file is too big to display

name: Continous build - build and test on every push
on:
push:
branches-ignore:
- 'release*'
- gh-pages
jobs:
build:
name: Build and test
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
java-version: [8, 11]
steps:
- name: Checkout
uses: actions/checkout@v1
with:
submodules: true
- name: Setup Java ${{ matrix.java-version }}
uses: actions/setup-java@v1
with:
java-version: ${{ matrix.java-version }}
- name: Report Java version
run: |
java -version
javac -version
- name: Build with Ant
run: ant test
build_j7:
name: Build and test on Java 7
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v1
with:
submodules: true
- name: Setup Java 7
uses: actions/setup-java@v1
with:
java-version: 7
- name: Report Java version
run: |
java -version
javac -version
- name: Setup Moxie
run: |
wget http://gitblit.github.io/moxie/maven/com/gitblit/moxie/moxie+ant/0.9.4/moxie+ant-0.9.4.tar.gz
tar -xzf moxie+ant-0.9.4.tar.gz
moxie-0.9.4/bin/moxie -version
- name: Build with Moxie
run: moxie-0.9.4/bin/moxie test
dist: trusty
language: java
jdk:
- openjdk7
- openjdk8
- oraclejdk11

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

/*
* 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;
}
}
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;
}
}
/*
* 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);
}
}
/*
* 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;
}
}
}
/*
* 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'
}
}
/*
* 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);
}
}
}
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);
}
}
/*
* 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();
}
}
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.ticketState=Stav \u00fakolu
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.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\n
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
<!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="https://code.google.com/p/gitextensions/">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>
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 + ")";
}
}
<!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>
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());
}
}
}

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

/**
* 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;
}
$(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();
}
});
});

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

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

Sorry, the diff of this file is too big to display

/*
* 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());
}
}
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;
}
}
}
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();
}
}
}
/*
* 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();
}
}
/*
* 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());
}
}
/*
* 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(":"));
}
}
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));
}
}
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());
}
}
+233
-32

@@ -30,3 +30,10 @@ <?xml version="1.0" encoding="UTF-8"?>

<property name="project.distrib.dir" value="${basedir}/src/main/distrib" />
<!-- Tools -->
<property name="octokit" location="${basedir}/.github/ok.sh" />
<property name="relnoawk" location="${basedir}/src/site/templates/ghreleasenotes.awk" />
<!-- GitHub user/organization name -->
<property name="gh.org" value="gitblit" />
<!--

@@ -45,2 +52,4 @@ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

<!-- Set Ant project properties -->
<property name="release.tag" value="v${project.version}" />
<property name="currentRelease.tag" value="v${project.releaseVersion}" />
<property name="release.name" value="gitblit-${project.version}"/>

@@ -55,5 +64,14 @@ <property name="distribution.zipfile" value="${release.name}.zip" />

<property name="maven.directory" value="${basedir}/../gitblit-maven" />
<property name="releaselog" value="${basedir}/releases.moxie" />
<!-- Download links -->
<property name="gc.url" value="http://dl.bintray.com/gitblit/releases/" />
<property name="gc.url" value="https://github.com/${gh.org}/gitblit/releases/download/" />
<!-- Report Java version -->
<echo>JDK version: ${ant.java.version}</echo>
<exec executable="javac">
<arg value="-version" />
</exec>
<echo>Java/JVM version: ${java.version}</echo>
</target>

@@ -138,2 +156,7 @@

<!-- Generate the HelloworldKeys class from the hello-world.properties file -->
<mx:keys propertiesfile="${basedir}/src/test/data/hello-world.properties"
outputclass="com.gitblit.tests.HelloworldKeys"
todir="${basedir}/src/test/java" />
<!-- Compile unit tests -->

@@ -200,3 +223,2 @@ <mx:javac scope="test" />

<mainclass name="com.gitblit.GitBlitServer" />
<launcher paths="ext" />
</mx:jar>

@@ -305,3 +327,2 @@

<class name="com.gitblit.Keys" />
<launcher paths="ext" />
<resource file="${project.compileOutputDirectory}/log4j.properties" />

@@ -377,3 +398,3 @@ </mx:genjar>

<mainclass name="com.gitblit.client.GitblitManagerLauncher" />
<mainclass name="com.gitblit.client.GitblitManager" />
<class name="com.gitblit.Keys" />

@@ -480,5 +501,5 @@ <class name="com.gitblit.client.GitblitClient" />

</target>
<!--
<!--
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

@@ -492,7 +513,5 @@ Build the Gitblit Website

<property name="releaselog" value="${basedir}/releases.moxie" />
<!-- Build Site -->
<mx:doc googleplusid="114464678392593421684" googleanalyticsid="UA-24377072-1"
googlePlusOne="true" minify="true" customless="custom.less">
<mx:doc googleanalyticsid="UA-24377072-1"
minify="true" customless="custom.less">
<structure>

@@ -569,5 +588,5 @@ <menu name="about">

<menu name="downloads">
<link name="Gitblit GO (Windows)" src="${gc.url}gitblit-${project.releaseVersion}.zip" />
<link name="Gitblit GO (Linux/OSX)" src="${gc.url}gitblit-${project.releaseVersion}.tar.gz" />
<link name="Gitblit WAR" src="${gc.url}gitblit-${project.releaseVersion}.war" />
<link name="Gitblit GO (Windows)" src="${gc.url}${currentRelease.tag}/gitblit-${project.releaseVersion}.zip" />
<link name="Gitblit GO (Linux/OSX)" src="${gc.url}${currentRelease.tag}/gitblit-${project.releaseVersion}.tar.gz" />
<link name="Gitblit WAR" src="${gc.url}${currentRelease.tag}/gitblit-${project.releaseVersion}.war" />
<divider />

@@ -578,8 +597,9 @@ <link name="Gitblit GO (Docker)" src="https://registry.hub.docker.com/u/jmoger/gitblit/" />

<divider />
<link name="Gitblit Manager" src="${gc.url}manager-${project.releaseVersion}.zip" />
<link name="Federation Client" src="${gc.url}fedclient-${project.releaseVersion}.zip" />
<link name="Gitblit Manager" src="${gc.url}${currentRelease.tag}/manager-${project.releaseVersion}.zip" />
<link name="Federation Client" src="${gc.url}${currentRelease.tag}/fedclient-${project.releaseVersion}.zip" />
<divider />
<link name="API Library" src="${gc.url}gbapi-${project.releaseVersion}.zip" />
<link name="API Library" src="${gc.url}${currentRelease.tag}/gbapi-${project.releaseVersion}.zip" />
<divider />
<link name="Bintray (1.4.0+)" src="https://bintray.com/gitblit/releases/gitblit" />
<link name="GitHub (1.9.0+)" src="https://github.com/${gh.org}/gitblit/releases" />
<link name="Bintray (1.4.0-1.8.0)" src="https://bintray.com/gitblit/releases/gitblit" />
<link name="GoogleCode (pre-1.4.0)" src="https://code.google.com/p/gitblit/downloads/list?can=1" />

@@ -610,7 +630,8 @@ <divider />

<replace token="%GCURL%" value="${gc.url}" />
<replace token="%GCURL%" value="${gc.url}${currentRelease.tag}/" />
<properties token="%PROPERTIES%" file="${project.distrib.dir}/data/defaults.properties" />
<regex searchPattern="\b(issue)(\s*[#]?|-){0,1}(\d+)\b" replacePattern="&lt;a href='http://code.google.com/p/gitblit/issues/detail?id=$3'&gt;issue $3&lt;/a&gt;" />
<regex searchPattern="\b(commit)(\s*[#]?|-){0,1}([0-9a-fA-F]{5,})\b" replacePattern="&lt;a href='https://github.com/gitblit/gitblit/commit/$3'&gt;commit $3&lt;/a&gt;" />
<regex searchPattern="\b(issue)(\s*[#]?|-){0,1}(\d+)\b" replacePattern="&lt;a href='https://github.com/gitblit/gitblit/issues/$3'&gt;issue $3&lt;/a&gt;" />
<regex searchPattern="\b(pr|pull request)(\s*[#]?|-){0,1}(\d+)\b" replacePattern="&lt;a href='https://github.com/gitblit/gitblit/pull/$3'&gt;pull request #$3&lt;/a&gt;" />

@@ -692,4 +713,4 @@ <regex searchPattern="\b(ticket)(\s*[#]?|-){0,1}(\d+)\b" replacePattern="&lt;a href='https://dev.gitblit.com/tickets/gitblit.git/$3'&gt;ticket $3&lt;/a&gt;" />

</target>
<!--

@@ -736,4 +757,81 @@ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

<!--
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Publish binaries to GitHub release
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-->
<target name="releaseBinaries" depends="prepare" description="Publish the Gitblit binaries to a GitHub release">
<ghReleaseDraft
releaselog="${releaselog}"
releasetag="${release.tag}"/>
<echo>Uploading Gitblit ${project.version} binaries</echo>
<!-- Upload Gitblit GO Windows ZIP file -->
<githubUpload
source="${project.targetDirectory}/${distribution.zipfile}"
target="gitblit-${project.version}.zip" />
<!-- Upload Gitblit GO Linux/Unix tar.gz file -->
<githubUpload
source="${project.targetDirectory}/${distribution.tgzfile}"
target="gitblit-${project.version}.tar.gz" />
<!-- Upload Gitblit WAR file -->
<githubUpload
source="${project.targetDirectory}/${distribution.warfile}"
target="gitblit-${project.version}.war" />
<!-- Upload Gitblit FedClient -->
<githubUpload
source="${project.targetDirectory}/${fedclient.zipfile}"
target="fedclient-${project.version}.zip" />
<!-- Upload Gitblit Manager -->
<githubUpload
source="${project.targetDirectory}/${manager.zipfile}"
target="manager-${project.version}.zip" />
<!-- Upload Gitblit API Library -->
<githubUpload
source="${project.targetDirectory}/${gbapi.zipfile}"
target="gbapi-${project.version}.zip" />
</target>
<!--
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Publish GH release draft
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-->
<target name="publishRelease" depends="prepare" description="Publish the GitHub release draft" >
<echo>Publishing Gitblit ${project.version} release draft on GitHub for tag ${release.tag}</echo>
<ghGetReleaseId
releaseVersion="${project.version}"/>
<exec executable="bash" logError="true" >
<arg value="-c" />
<arg value="${octokit} -q edit_release ${gh.org} gitblit ${ghrelease.id} tag_name='${release.tag}'"></arg>
</exec>
<ghPublishReleaseDraft
releaseid="${ghrelease.id}"/>
</target>
<!--
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Build site and update GH pages for publishing
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-->
<target name="updateSite" depends="buildSite,updateGhPages" description="Update the Gitblit pages site" >
</target>
<!--
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

@@ -766,7 +864,7 @@ Publish site to site hosting service

<property name="dryrun" value="false" />
<mx:version stage="release" dryrun="${dryrun}" />
<mx:version stage="release" dryrun="${dryrun}" />
<property name="project.tag" value="v${project.version}" />
<!-- commit build.moxie & releases.moxie (automatic) -->
<mx:commit showtitle="no">
<message>Prepare ${project.version} release</message>
<message>Prepare ${project.version} release</message>
<tag name="${project.tag}">

@@ -777,2 +875,16 @@ <message>${project.name} ${project.version} release</message>

<!-- output version information for other scripts/programs to pick up -->
<mx:if>
<and>
<isset property="versionInfo" />
<not><equals arg1="${versionInfo}" arg2="" trim="true"/></not>
</and>
<then>
<echo file="${basedir}/${versionInfo}">
GB_RELEASE_VERSION=${project.version}
GB_RELEASE_TAG=${project.tag}
</echo>
</then>
</mx:if>
<!-- create the release process script -->

@@ -870,2 +982,4 @@ <mx:if>

<page name="scaling" src="setup_scaling.mkd" />
<page name="fail2ban" src="setup_fail2ban.mkd" />
<page name="filestore (Git LFS)" src="setup_filestore.mkd" />
<divider />

@@ -902,6 +1016,12 @@ <page name="Gitblit as a viewer" src="setup_viewer.mkd" />

</menu>
<menu name="changelog">
<page name="current release" src="releasecurrent.mkd" />
<page name="older releases" src="releasehistory.mkd" />
<page name="current release" out="releasenotes.html">
<template src="releasecurrent.ftl" data="${releaselog}" />
</page>
<page name="all releases" out="releases.html">
<template src="releasehistory.ftl" data="${releaselog}" />
</page>
</menu>
<menu name="links">

@@ -915,9 +1035,13 @@ <link name="dev.gitblit.com (self-hosted)" src="https://dev.gitblit.com" />

<link name="Discussion" src="${project.forumUrl}" />
<link name="Twitter" src="https://twitter.com/gitblit" />
<link name="Ohloh" src="http://www.ohloh.net/p/gitblit" />
</menu>
</structure>
<replace token="%GCURL%" value="${gc.url}${currentRelease.tag}/" />
<properties token="%PROPERTIES%" file="${project.distrib.dir}/data/defaults.properties" />
<regex searchPattern="\b(issue)(\s*[#]?|-){0,1}(\d+)\b" replacePattern="&lt;a href='http://code.google.com/p/gitblit/issues/detail?id=$3'&gt;issue $3&lt;/a&gt;" />
<regex searchPattern="\b(commit)(\s*[#]?|-){0,1}([0-9a-fA-F]{5,})\b" replacePattern="&lt;a href='https://github.com/gitblit/gitblit/commit/$3'&gt;commit $3&lt;/a&gt;" />
<regex searchPattern="\b(issue)(\s*[#]?|-){0,1}(\d+)\b" replacePattern="&lt;a href='https://github.com/gitblit/gitblit/issues/$3'&gt;issue $3&lt;/a&gt;" />
<regex searchPattern="\b(pr|pull request)(\s*[#]?|-){0,1}(\d+)\b" replacePattern="&lt;a href='https://github.com/gitblit/gitblit/pull/$3'&gt;pull request #$3&lt;/a&gt;" />

@@ -994,3 +1118,3 @@ <regex searchPattern="\b(ticket)(\s*[#]?|-){0,1}(\d+)\b" replacePattern="&lt;a href='https://dev.gitblit.com/tickets/gitblit.git/$3'&gt;ticket $3&lt;/a&gt;" />

</macrodef>
<!--

@@ -1014,2 +1138,79 @@ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Macro to create release draft on GitHub
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-->
<macrodef name="ghReleaseDraft">
<attribute name="releaselog" />
<attribute name="releasetag" />
<sequential>
<echo>creating release ${release.tag} draft on GitHub</echo>
<exec executable="bash" logError="true" failonerror="true" outputproperty="ghrelease.id">
<arg value="-c" />
<arg value="${octokit} create_release ${gh.org} gitblit @{releasetag} name=${project.version} draft=true | cut -f2"></arg>
</exec>
<exec executable="bash" logError="true" failonerror="true" outputproperty="ghrelease.upldUrl">
<arg value="-c" />
<arg value="${octokit} release ${gh.org} gitblit ${ghrelease.id} _filter=.upload_url | sed 's/{.*$/?name=/'"></arg>
</exec>
<exec executable="bash" logError="true" failonerror="true" outputproperty="ghrelease.notes">
<arg value="-c" />
<arg value="cat @{releaselog} | awk -f ${relnoawk} protect=true"></arg>
</exec>
<exec executable="bash" logError="true" >
<arg value="-c" />
<arg value="${octokit} -q edit_release ${gh.org} gitblit ${ghrelease.id} body='${ghrelease.notes}'"></arg>
</exec>
</sequential>
</macrodef>
<!--
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Macro to upload binaries to GitHub
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-->
<macrodef name="githubUpload">
<attribute name="source"/>
<attribute name="target"/>
<sequential>
<echo>uploading @{source} to GitHub release ${ghrelease.id}</echo>
<exec executable="bash" logError="true" failonerror="true" >
<arg value="-c" />
<arg value="${octokit} upload_asset ${ghrelease.upldUrl}@{target} @{source}"></arg>
</exec>
</sequential>
</macrodef>
<!--
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Macro to publish release draft on GitHub
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-->
<macrodef name="ghPublishReleaseDraft">
<attribute name="releaseid"/>
<sequential>
<echo>publishing GitHub release draft @{releaseid}</echo>
<exec executable="bash" logError="true" >
<arg value="-c" />
<arg value="${octokit} -q edit_release ${gh.org} gitblit @{releaseid} draft=false"></arg>
</exec>
</sequential>
</macrodef>
<!--
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Macro to publish release draft on GitHub
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-->
<macrodef name="ghGetReleaseId">
<attribute name="releaseVersion"/>
<sequential>
<exec executable="bash" logError="true" failonerror="true" outputproperty="ghrelease.id">
<arg value="-c" />
<arg value="${octokit} list_releases ${gh.org} gitblit _filter='.[] | &quot;\(.name)\t\(.tag_name)\t\(.id)&quot;' | grep @{releaseVersion} | cut -f3"></arg>
</exec>
</sequential>
</macrodef>
<!--
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Install Gitblit JAR for usage as Maven module

@@ -1016,0 +1217,0 @@ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

+1
-1

@@ -339,3 +339,3 @@ /*

mkp.yield header.oldPath
mkp.yieldUnescaped "<b> -&rt; </b>"
mkp.yieldUnescaped "<b> -&gt; </b>"
a(href:blobDiffUrl(id, header.newPath), header.newPath)

@@ -342,0 +342,0 @@ }

#!/bin/bash
java -cp gitblit.jar com.gitblit.authority.Launcher --baseFolder data
java -cp gitblit.jar:ext/* com.gitblit.authority.GitblitAuthority --baseFolder data
#!/bin/bash
java -jar gitblit.jar --baseFolder data --stop
java -cp "gitblit.jar:ext/*" com.gitblit.GitBlitServer --baseFolder data --stop
#!/bin/bash
java -jar gitblit.jar --baseFolder data
java -cp "gitblit.jar:ext/*" com.gitblit.GitBlitServer --baseFolder data

@@ -1,1 +0,1 @@

@java -cp gitblit.jar com.gitblit.authority.Launcher --baseFolder data %*
@java -cp gitblit.jar;"%CD%\ext\*" com.gitblit.authority.GitblitAuthority --baseFolder data %*

@@ -1,1 +0,1 @@

@java -jar gitblit.jar --stop --baseFolder data %*
@java -cp gitblit.jar;"%CD%\ext\*" com.gitblit.GitBlitServer --stop --baseFolder data %*

@@ -1,1 +0,1 @@

@java -jar gitblit.jar --baseFolder data %*
@java -cp gitblit.jar;"%CD%\ext\*" com.gitblit.GitBlitServer --baseFolder data %*

@@ -26,3 +26,3 @@ @REM Install Gitblit as a Windows service.

--StartPath="%CD%" ^
--StartClass=org.moxie.MxLauncher ^
--StartClass=com.gitblit.GitBlitServer ^
--StartMethod=main ^

@@ -32,9 +32,9 @@ --StartParams="--storePassword;gitblit;--baseFolder;%CD%\data" ^

--StopPath="%CD%" ^
--StopClass=org.moxie.MxLauncher ^
--StopClass=com.gitblit.GitBlitServer ^
--StopMethod=main ^
--StopParams="--stop;--baseFolder;%CD%\data" ^
--StopMode=jvm ^
--Classpath="%CD%\gitblit.jar" ^
--Classpath="%CD%\gitblit.jar;%CD%\ext\*" ^
--Jvm=auto ^
--JvmMx=1024

@@ -81,6 +81,6 @@ /*

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();
}

@@ -87,0 +87,0 @@ }

@@ -199,3 +199,3 @@ /*

// create a user cookie
setCookie(user, password);
setCookie(user);

@@ -202,0 +202,0 @@ // Set user attributes, hide password from backing user service.

@@ -19,5 +19,2 @@ /*

import java.net.URI;
import java.net.URISyntaxException;
import java.security.GeneralSecurityException;
import java.text.MessageFormat;

@@ -37,2 +34,3 @@ import java.util.Arrays;

import com.gitblit.auth.AuthenticationProvider.UsernamePasswordAuthenticationProvider;
import com.gitblit.ldap.LdapConnection;
import com.gitblit.models.TeamModel;

@@ -44,7 +42,4 @@ import com.gitblit.models.UserModel;

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;

@@ -55,6 +50,2 @@ import com.unboundid.ldap.sdk.SearchRequest;

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;

@@ -114,8 +105,14 @@ /**

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}", "*");

@@ -171,2 +168,4 @@

for (TeamModel userTeam : user.teams) {
// Is this an administrative team?
setAdminAttribute(userTeam);
userTeams.put(userTeam.name, userTeam);

@@ -188,62 +187,2 @@ }

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;
}
/**

@@ -300,6 +239,3 @@ * Credentials are defined in the LDAP server and can not be manipulated

if (!supportsTeamMembershipChanges()) {
List<String> admins = settings.getStrings(Keys.realm.ldap.admins);
if (admins.contains(user.username)) {
return false;
}
return false;
}

@@ -314,6 +250,3 @@ }

if (!supportsTeamMembershipChanges()) {
List<String> admins = settings.getStrings(Keys.realm.ldap.admins);
if (admins.contains("@" + team.name)) {
return false;
}
return false;
}

@@ -333,25 +266,24 @@ }

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) {

@@ -361,3 +293,3 @@ SearchResultEntry loggingInUser = result.getSearchEntries().get(0);

if (alreadyAuthenticated || isAuthenticated(ldapConnection, loggingInUserDN, new String(password))) {
if (ldapConnection.isAuthenticated(loggingInUserDN, passwd)) {
logger.debug("LDAP authenticated: " + username);

@@ -374,3 +306,3 @@

// create a user cookie
setCookie(user, password);
setCookie(user);

@@ -389,2 +321,4 @@ if (!supportsTeamMembershipChanges()) {

for (TeamModel userTeam : user.teams) {
// Is this an administrative team?
setAdminAttribute(userTeam);
updateTeam(userTeam);

@@ -420,6 +354,3 @@ }

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

@@ -433,2 +364,26 @@ user.canAdmin = true;

/**
* 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) {

@@ -480,3 +435,3 @@ // Is this user an admin?

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();

@@ -490,11 +445,11 @@

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) {

@@ -516,3 +471,3 @@ for (int i = 0; i < teamMembershipResult.getEntryCount(); i++) {

private void getEmptyTeamsFromLdap(LDAPConnection ldapConnection) {
private void getEmptyTeamsFromLdap(LdapConnection ldapConnection) {
logger.info("Start fetching empty teams from ldap.");

@@ -522,3 +477,3 @@ String groupBase = settings.getString(Keys.realm.ldap.groupBase, "");

SearchResult teamMembershipResult = doSearch(ldapConnection, groupBase, true, groupMemberPattern, null);
SearchResult teamMembershipResult = searchTeamsInLdap(ldapConnection, groupBase, true, groupMemberPattern, null);
if (teamMembershipResult != null && teamMembershipResult.getEntryCount() > 0) {

@@ -533,2 +488,3 @@ for (int i = 0; i < teamMembershipResult.getEntryCount(); i++) {

teamModel = createTeamFromLdap(teamEntry);
setAdminAttribute(teamModel);
userManager.updateTeamModel(teamModel);

@@ -542,2 +498,26 @@ }

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) {

@@ -551,27 +531,10 @@ TeamModel answer = new TeamModel(teamEntry.getAttributeValue("cn"));

private SearchResult doSearch(LDAPConnection ldapConnection, String base, String filter) {
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) {
try {
SearchRequest searchRequest = new SearchRequest(base, SearchScope.SUB, filter);
if (dereferenceAliases) {
searchRequest.setDerefPolicy(DereferencePolicy.SEARCHING);
SearchResult result = ldapConnection.search(searchRequest);
if (result.getResultCode() != ResultCode.SUCCESS) {
return null;
}
if (attributes != null) {
searchRequest.setAttributes(attributes);
}
return ldapConnection.search(searchRequest);
} catch (LDAPSearchException e) {
logger.error("Problem Searching LDAP", e);
return null;
return result;
} catch (LDAPException e) {

@@ -583,13 +546,5 @@ logger.error("Problem creating LDAP search", e);

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;
}
}
/**

@@ -610,30 +565,2 @@ * Returns a simple username without any domain prefixes.

// 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() {

@@ -651,3 +578,2 @@ LdapSyncService ldapSyncService = new LdapSyncService(settings, this);

}
}

@@ -125,3 +125,3 @@ /*

// create a user cookie
setCookie(user, password);
setCookie(user);

@@ -128,0 +128,0 @@ // update user attributes from UnixUser

@@ -142,3 +142,3 @@ /*

// create a user cookie
setCookie(user, password);
setCookie(user);

@@ -145,0 +145,0 @@ // update user attributes from Redmine

@@ -69,3 +69,3 @@ package com.gitblit.auth;

setCookie(user, password);
setCookie(user);
setUserAttributes(user, info);

@@ -72,0 +72,0 @@

@@ -156,3 +156,3 @@ /*

// create a user cookie
setCookie(user, password);
setCookie(user);

@@ -159,0 +159,0 @@ // update user attributes from Windows identity

@@ -51,3 +51,5 @@ /*

import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.ResourceBundle;

@@ -102,2 +104,3 @@ import javax.mail.Message;

import com.gitblit.utils.X509Utils.X509Metadata;
import com.gitblit.wicket.GitBlitWebSession;

@@ -452,3 +455,3 @@ /**

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);

@@ -856,5 +859,15 @@ // save latest expiration date

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)) {

@@ -861,0 +874,0 @@ body = MessageFormat.format("Hi {0}\n\nHere is your client certificate bundle.\nInside the zip file are installation instructions.", user.getDisplayName());

@@ -61,4 +61,6 @@ /*

import com.gitblit.models.UserModel;
import com.gitblit.utils.PasswordHash;
import com.gitblit.utils.StringUtils;
public class EditUserDialog extends JDialog {

@@ -321,4 +323,6 @@

}
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());

@@ -335,17 +339,15 @@ if (cpw == null || cpw.length() != password.length()) {

// change the cookie
user.cookie = StringUtils.getSHA1(user.username + password);
user.cookie = user.createCookie();
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);
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.");

@@ -352,0 +354,0 @@ return false;

@@ -901,3 +901,3 @@ /*

if (StringUtils.isEmpty(user.cookie) && !StringUtils.isEmpty(user.password)) {
user.cookie = StringUtils.getSHA1(user.username + user.password);
user.cookie = user.createCookie();
}

@@ -904,0 +904,0 @@

@@ -65,2 +65,8 @@ /*

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";

@@ -643,2 +649,33 @@ public static final String ZIP_PATH = "/zip/";

/**
* 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

@@ -645,0 +682,0 @@ @Retention(RetentionPolicy.RUNTIME)

@@ -602,3 +602,3 @@ /*

// 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) {

@@ -1283,2 +1283,3 @@ case ALREADY_MERGED:

ticket.mergeTo,
getRepositoryModel().mergeType,
committer,

@@ -1285,0 +1286,0 @@ message);

@@ -146,3 +146,3 @@ /*

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");
}

@@ -229,2 +229,6 @@ System.exit(0);

String javaversion = System.getProperty("java.version");
String javavendor = System.getProperty("java.vendor");
logger.info("JVM version " + javaversion + " (" + javavendor + ")");
QueuedThreadPool threadPool = new QueuedThreadPool();

@@ -298,3 +302,3 @@ int maxThreads = settings.getInteger(Keys.server.threadPoolSize, 50);

connector.setSoLingerTime(-1);
connector.setIdleTimeout(30000);
connector.setIdleTimeout(settings.getLong(Keys.server.httpIdleTimeout, 30000L));
connector.setPort(params.securePort);

@@ -336,3 +340,3 @@ String bindInterface = settings.getString(Keys.server.httpsBindInterface, null);

connector.setSoLingerTime(-1);
connector.setIdleTimeout(30000);
connector.setIdleTimeout(settings.getLong(Keys.server.httpIdleTimeout, 30000L));
connector.setPort(params.port);

@@ -382,3 +386,4 @@ String bindInterface = settings.getString(Keys.server.httpBindInterface, null);

// 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);

@@ -615,2 +620,2 @@

}
}
}

@@ -29,2 +29,5 @@ /*

*
* 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

@@ -31,0 +34,0 @@ *

@@ -55,2 +55,3 @@ /*

import com.gitblit.utils.HttpUtils;
import com.gitblit.utils.PasswordHash;
import com.gitblit.utils.StringUtils;

@@ -522,22 +523,47 @@ import com.gitblit.utils.X509Utils.X509Metadata;

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)) {
PasswordHash pwdHash = PasswordHash.instanceFor(user.password);
if (pwdHash != null) {
if (pwdHash.matches(user.password, password, user.username)) {
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;
}
} else if (user.password.equals(new String(password))) {
// plain-text password
returnedUser = user;
}
// validate user
returnedUser = validateAuthentication(returnedUser, AuthenticationType.CREDENTIALS);
// try to upgrade the stored password hash to a stronger hash, if necessary
upgradeStoredPassword(returnedUser, password, pwdHash);
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);
}
}
return validateAuthentication(returnedUser, AuthenticationType.CREDENTIALS);
}
/**

@@ -613,2 +639,7 @@ * Returns the Gitlbit cookie in the request.

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());
}

@@ -628,2 +659,11 @@ }

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) );
}
/**

@@ -630,0 +670,0 @@ * Logout a user.

@@ -20,2 +20,4 @@ /*

import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.text.MessageFormat;

@@ -123,4 +125,6 @@ import java.util.ArrayList;

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);
}

@@ -133,2 +137,18 @@ }

/**
* 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) {

@@ -135,0 +155,0 @@ IUserService service = null;

@@ -119,3 +119,3 @@ /*

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");
}

@@ -122,0 +122,0 @@ System.exit(0);

@@ -31,2 +31,3 @@ /*

import com.gitblit.Constants.FederationStrategy;
import com.gitblit.Constants.MergeType;
import com.gitblit.utils.ArrayUtils;

@@ -93,2 +94,3 @@ import com.gitblit.utils.ModelUtils;

public String mergeTo;
public MergeType mergeType;

@@ -116,2 +118,3 @@ public transient boolean isCollectingGarbage;

this.acceptNewPatchsets = true;
this.mergeType = MergeType.DEFAULT_MERGE_TYPE;

@@ -118,0 +121,0 @@ addOwner(owner);

@@ -46,2 +46,4 @@ /*

import com.gitblit.Constants;
/**

@@ -777,6 +779,6 @@ * The Gitblit Ticket model, its component classes, and enums.

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);

@@ -783,0 +785,0 @@ }

@@ -39,2 +39,3 @@ /*

import com.gitblit.utils.ModelUtils;
import com.gitblit.utils.SecureRandom;
import com.gitblit.utils.StringUtils;

@@ -56,2 +57,4 @@

private static final SecureRandom RANDOM = new SecureRandom();
// field names are reflectively mapped in EditUser page

@@ -665,2 +668,6 @@ public String username;

}
public String createCookie() {
return StringUtils.getSHA1(RANDOM.randomBytes(32));
}
}

@@ -114,3 +114,3 @@ /*

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");
}

@@ -117,0 +117,0 @@ System.exit(0);

@@ -68,3 +68,2 @@ /*

import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.util.Version;
import org.eclipse.jgit.diff.DiffEntry.ChangeType;

@@ -122,11 +121,5 @@ import org.eclipse.jgit.lib.Constants;

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);

@@ -272,3 +265,3 @@

try {
writers.get(writer).close(true);
writers.get(writer).close();
} catch (Throwable t) {

@@ -299,22 +292,9 @@ logger.error("Failed to close Lucene writer for " + writer, t);

public boolean deleteIndex(String repositoryName) {
try {
// close any open writer/searcher
close(repositoryName);
// 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);
}
// delete the index folder
File repositoryFolder = FileKey.resolve(new File(repositoriesFolder, repositoryName), FS.DETECTED);
LuceneRepoIndexStore luceneIndex = new LuceneRepoIndexStore(repositoryFolder, INDEX_VERSION);
return luceneIndex.delete();
}

@@ -393,4 +373,4 @@

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;

@@ -400,19 +380,10 @@ }

/**
* 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());
}

@@ -627,3 +598,2 @@

// commit all changes and reset the searcher
config.setInt(CONF_INDEX, null, CONF_VERSION, INDEX_VERSION);
config.save();

@@ -731,6 +701,5 @@ writer.commit();

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();

@@ -859,3 +828,2 @@ IndexWriter writer = getIndexWriter(repositoryName);

// update the config
config.setInt(CONF_INDEX, null, CONF_VERSION, INDEX_VERSION);
config.setString(CONF_ALIAS, null, keyName, branchName);

@@ -978,12 +946,9 @@ config.setString(CONF_BRANCH, null, keyName, branch.getObjectId().getName());

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);

@@ -1041,14 +1006,14 @@ indexWriter = new IndexWriter(directory, config);

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);

@@ -1071,6 +1036,7 @@ IndexSearcher searcher;

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);

@@ -1240,3 +1206,3 @@ int offset = Math.max(0, (page - 1) * pageSize);

MultiSourceReader(IndexReader [] readers) {
MultiSourceReader(IndexReader [] readers) throws IOException {
super(readers, false);

@@ -1243,0 +1209,0 @@ }

@@ -20,2 +20,3 @@ /*

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;

@@ -35,2 +36,3 @@ import java.util.List;

import javax.mail.PasswordAuthentication;
import javax.mail.SendFailedException;
import javax.mail.Session;

@@ -148,3 +150,3 @@ import javax.mail.Transport;

}
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);

@@ -278,5 +280,18 @@

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) {

@@ -283,0 +298,0 @@ logger.error("Failed to send message", e);

@@ -170,2 +170,3 @@ /*

if (StringUtils.isEmpty(repository)) {
logger.info("ARF: Rejecting request, empty repository name in URL {}", fullUrl);
httpResponse.setStatus(HttpServletResponse.SC_BAD_REQUEST);

@@ -185,2 +186,17 @@ return;

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);

@@ -187,0 +203,0 @@

@@ -296,7 +296,9 @@ /*

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);
}
}

@@ -303,0 +305,0 @@ }

@@ -49,10 +49,12 @@ /*

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};

@@ -80,9 +82,15 @@ private IStoredSettings settings;

*
* @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) {

@@ -116,14 +124,16 @@ repository = repository.substring(0, repository.indexOf(urlSuffix));

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;
}

@@ -156,8 +166,14 @@ }

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);

@@ -177,3 +193,3 @@ }

// error messages
if (gitLfs.equals(action)) {
if (GIT_LFS.equals(action)) {
if (!method.matches("GET|POST|PUT|HEAD")) {

@@ -202,9 +218,9 @@ return false;

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)) {

@@ -236,6 +252,6 @@ if (method.matches("GET|HEAD")) {

}
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

@@ -265,5 +281,5 @@ if (user.canClone(repository)) {

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

@@ -336,3 +352,3 @@ return null;

if (action.equals(gitLfs)) {
if (GIT_LFS.equals(action)) {
if (hasContentInRequestHeader(httpRequest, "Accept", FilestoreServlet.GIT_LFS_META_MIME)) {

@@ -356,3 +372,3 @@ return "LFS-Authenticate";

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)

@@ -359,0 +375,0 @@ || !hasContentInRequestHeader(request, "Content-Type", FilestoreServlet.GIT_LFS_META_MIME)) {

@@ -136,2 +136,6 @@ /*

}
if(!StringUtils.isEmpty(objectName) && !objectName.equals(model.name)) {
// skip repository if a name was submitted and it doesn't match
continue;
}
// get local branches

@@ -138,0 +142,0 @@ Repository repository = gitblit.getRepository(model.name);

@@ -113,5 +113,4 @@ /*

@Override
public BranchTicketService start() {
public void onStart() {
log.info("{} started", getClass().getSimpleName());
return this;
}

@@ -118,0 +117,0 @@

@@ -83,5 +83,4 @@ /*

@Override
public FileTicketService start() {
public void onStart() {
log.info("{} started", getClass().getSimpleName());
return this;
}

@@ -88,0 +87,0 @@

@@ -184,5 +184,22 @@ /*

@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.

@@ -201,2 +218,8 @@ * @since 1.4.0

/**
* 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!

@@ -279,8 +302,2 @@ * @since 1.4.0

/**
* Closes any open resources used by this service.
* @since 1.4.0
*/
protected abstract void close();
/**
* Reset all caches in the service.

@@ -1349,3 +1366,15 @@ * @since 1.4.0

/**
* 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.

@@ -1352,0 +1381,0 @@ * This operation may be expensive and time-consuming.

@@ -64,5 +64,4 @@ /*

@Override
public NullTicketService start() {
public void onStart() {
log.info("{} started", getClass().getSimpleName());
return this;
}

@@ -69,0 +68,0 @@

@@ -86,3 +86,3 @@ /*

@Override
public RedisTicketService start() {
public void onStart() {
log.info("{} started", getClass().getSimpleName());

@@ -92,3 +92,2 @@ if (!isReady()) {

}
return this;
}

@@ -95,0 +94,0 @@

@@ -34,2 +34,4 @@ /*

import org.apache.lucene.document.LongField;
import org.apache.lucene.document.NumericDocValuesField;
import org.apache.lucene.document.SortedDocValuesField;
import org.apache.lucene.document.TextField;

@@ -53,3 +55,3 @@ import org.apache.lucene.index.DirectoryReader;

import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.util.Version;
import org.apache.lucene.util.BytesRef;
import org.slf4j.Logger;

@@ -65,3 +67,3 @@ import org.slf4j.LoggerFactory;

import com.gitblit.models.TicketModel.Status;
import com.gitblit.utils.FileUtils;
import com.gitblit.utils.LuceneIndexStore;
import com.gitblit.utils.StringUtils;

@@ -114,2 +116,4 @@

final static int INDEX_VERSION = 2;
final Type fieldType;

@@ -174,6 +178,4 @@

private final Version luceneVersion = Version.LUCENE_46;
private final LuceneIndexStore indexStore;
private final File luceneDir;
private IndexWriter writer;

@@ -184,3 +186,4 @@

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);
}

@@ -201,3 +204,3 @@

close();
FileUtils.delete(luceneDir);
indexStore.delete();
}

@@ -211,6 +214,5 @@

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();

@@ -236,2 +238,14 @@ int numDocsBefore = writer.numDocs();

/**
* 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

@@ -299,6 +313,5 @@ *

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();

@@ -344,26 +357,26 @@ int numDocsBefore = writer.numDocs();

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);

@@ -406,5 +419,5 @@ int offset = Math.max(0, (page - 1) * pageSize);

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);

@@ -424,3 +437,3 @@

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;

@@ -459,10 +472,7 @@ int offset = Math.max(0, (page - 1) * size);

if (writer == null) {
Directory directory = FSDirectory.open(luceneDir);
indexStore.create();
if (!luceneDir.exists()) {
luceneDir.mkdirs();
}
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);

@@ -571,2 +581,3 @@ writer = new IndexWriter(directory, config);

doc.add(new LongField(lucene.name(), value.getTime(), Store.YES));
doc.add(new NumericDocValuesField(lucene.name(), value.getTime()));
}

@@ -576,2 +587,3 @@

doc.add(new LongField(lucene.name(), value, Store.YES));
doc.add(new NumericDocValuesField(lucene.name(), value));
}

@@ -581,2 +593,3 @@

doc.add(new IntField(lucene.name(), value, Store.YES));
doc.add(new NumericDocValuesField(lucene.name(), value));
}

@@ -589,2 +602,3 @@

doc.add(new org.apache.lucene.document.Field(lucene.name(), value, TextField.TYPE_STORED));
doc.add(new SortedDocValuesField(lucene.name(), new BytesRef(value)));
}

@@ -684,2 +698,2 @@

}
}
}

@@ -576,6 +576,6 @@ /*

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);

@@ -582,0 +582,0 @@ }

@@ -28,2 +28,3 @@ /*

import com.gitblit.manager.IManager;
import com.gitblit.models.UserModel;
import com.google.common.cache.CacheBuilder;

@@ -103,2 +104,14 @@ import com.google.common.cache.CacheLoader;

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);
}
}

@@ -28,2 +28,3 @@ /*

import com.gitblit.Constants.AccessPermission;
import com.gitblit.models.UserModel;
import com.gitblit.transport.ssh.IPublicKeyManager;

@@ -51,8 +52,16 @@ import com.gitblit.transport.ssh.SshKey;

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);
}
}

@@ -59,0 +68,0 @@

@@ -18,4 +18,4 @@ /*

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;

@@ -22,0 +22,0 @@

@@ -26,2 +26,3 @@ /*

import java.text.MessageFormat;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;

@@ -34,3 +35,3 @@

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;

@@ -60,2 +61,9 @@ import org.eclipse.jgit.internal.JGitText;

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 {

@@ -103,5 +111,2 @@ MINA, NIO2

// Client public key authenticator
SshKeyAuthenticator keyAuthenticator =
new SshKeyAuthenticator(gitblit.getPublicKeyManager(), gitblit);

@@ -132,12 +137,35 @@ // Configure the preferred SSHD backend

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));

@@ -144,0 +172,0 @@ // Set the server id. This can be queried with:

@@ -20,3 +20,3 @@ /*

import org.apache.sshd.common.session.Session.AttributeKey;
import org.apache.sshd.common.AttributeStore.AttributeKey;

@@ -23,0 +23,0 @@ import com.gitblit.models.UserModel;

@@ -25,3 +25,4 @@ /*

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;

@@ -40,7 +41,8 @@ import org.slf4j.Logger;

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());

@@ -71,5 +73,5 @@

@Override
protected AbstractSession doCreateSession(IoSession ioSession) throws Exception {
protected ServerSessionImpl doCreateSession(IoSession ioSession) throws Exception {
return new SshServerSession(getServer(), ioSession);
}
}

@@ -37,2 +37,3 @@ /*

import com.gitblit.Keys;
import com.gitblit.manager.IGitblit;
import com.gitblit.models.UserModel;

@@ -49,6 +50,6 @@ import com.gitblit.transport.ssh.commands.DispatchCommand;

private final IStoredSettings settings;
private final IGitblit gitblit;
public WelcomeShell(IStoredSettings settings) {
this.settings = settings;
public WelcomeShell(IGitblit gitblit) {
this.gitblit = gitblit;
}

@@ -58,3 +59,3 @@

public Command create() {
return new SendMessage(settings);
return new SendMessage(gitblit);
}

@@ -64,2 +65,3 @@

private final IPublicKeyManager km;
private final IStoredSettings settings;

@@ -73,4 +75,5 @@ private ServerSession session;

SendMessage(IStoredSettings settings) {
this.settings = settings;
SendMessage(IGitblit gitblit) {
this.km = gitblit.getPublicKeyManager();
this.settings = gitblit.getSettings();
}

@@ -124,2 +127,6 @@

int port = settings.getInteger(Keys.git.sshPort, 0);
boolean writeKeysIsSupported = true;
if (km != null) {
writeKeysIsSupported = km.supportsWritingKeys(user);
}

@@ -168,3 +175,3 @@ final String b1 = StringUtils.rightPad("", 72, '═');

if (client.getKey() == null) {
if (writeKeysIsSupported && client.getKey() == null) {
// user has authenticated with a password

@@ -171,0 +178,0 @@ // display add public key instructions

@@ -45,3 +45,3 @@ /*

public static boolean isEmpty(Collection<?> collection) {
return collection == null || collection.size() == 0;
return collection == null || collection.isEmpty();
}

@@ -48,0 +48,0 @@

@@ -22,5 +22,5 @@ /*

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;

@@ -62,3 +62,3 @@

protected CommitCache() {
cache = new ConcurrentHashMap<String, ObjectCache<List<RepositoryCommit>>>();
cache = new HashMap<>();
}

@@ -98,3 +98,5 @@

public void clear() {
cache.clear();
synchronized (cache) {
cache.clear();
}
}

@@ -109,4 +111,7 @@

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));

@@ -124,9 +129,13 @@ }

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));
}
}

@@ -164,7 +173,2 @@

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();

@@ -175,36 +179,47 @@

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);
ObjectCache<List<RepositoryCommit>> repoCache;
synchronized (cache) {
repoCache = cache.get(repoKey);
if (repoCache == null) {
repoCache = new ObjectCache<>();
cache.put(repoKey, repoCache);
}
}
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);
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)));
}
if (sinceDate.equals(cacheCutoffDate)) {
// Mustn't hand out the cached list; that's not thread-safe
list = new ArrayList<>(commits);
} 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;
// reduce the commits to those since the specified date
list = reduce(commits, sinceDate);
}
} 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);
}
logger.debug(MessageFormat.format("retrieved {0} commits from cache of {1}:{2} since {3,date,yyyy-MM-dd} in {4} msecs",

@@ -232,4 +247,5 @@ list.size(), repositoryName, branch, sinceDate, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start)));

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);

@@ -254,4 +270,5 @@ List<RefModel> commitRefs = allRefs.get(commitModel.getId());

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);

@@ -273,3 +290,3 @@ List<RefModel> commitRefs = allRefs.get(commitModel.getId());

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) {

@@ -276,0 +293,0 @@ if (commit.getCommitDate().compareTo(sinceDate) >= 0) {

@@ -29,2 +29,3 @@ /*

import java.nio.charset.Charset;
import java.nio.file.LinkOption;
import java.nio.file.Path;

@@ -306,4 +307,4 @@ import java.nio.file.Paths;

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)) {

@@ -317,4 +318,6 @@ return exactBase.relativize(exactPath).toString().replace('\\', '/');

/**
* 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.
*

@@ -324,10 +327,16 @@ * @param path

*/
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) {

@@ -334,0 +343,0 @@ if (aFolder == null) {

@@ -113,3 +113,5 @@ /*

|| ("https".equals(scheme) && port != 443)) {
sb.append(":").append(port);
if (!host.endsWith(":" + port)) {
sb.append(":").append(port);
}
}

@@ -116,0 +118,0 @@ sb.append(context);

@@ -33,2 +33,3 @@ /*

import com.gitblit.Constants;
import com.gitblit.IStoredSettings;

@@ -141,4 +142,4 @@ import com.gitblit.Keys;

// 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);

@@ -145,0 +146,0 @@ // link ticket refs

@@ -49,6 +49,2 @@ /*

public static final String MD5_TYPE = "MD5:";
public static final String COMBINED_MD5_TYPE = "CMD5:";
/**

@@ -55,0 +51,0 @@ * Returns true if the string is null or empty.

@@ -746,2 +746,21 @@ /*

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 {

@@ -759,4 +778,13 @@ // read the Gitblit CA key and certificate

// 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

@@ -763,0 +791,0 @@ File zipFile = new File(targetFolder, clientMetadata.commonName + ".zip");

@@ -221,3 +221,4 @@ gb.repository = Repository

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)

@@ -224,0 +225,0 @@ gb.noHits = Keine Treffer

@@ -221,3 +221,4 @@ gb.repository = Repositorio

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)

@@ -224,0 +225,0 @@ gb.noHits = Sin coincidencias

@@ -221,3 +221,4 @@ gb.repository = d\u00e9p\u00f4t

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)

@@ -224,0 +225,0 @@ gb.noHits = aucun r\u00e9sultats

@@ -221,3 +221,4 @@ gb.repository = repository

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)

@@ -224,0 +225,0 @@ gb.noHits = nessuna corrispondenza

@@ -220,3 +220,4 @@ gb.repository = \u30ea\u30dd\u30b8\u30c8\u30ea

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.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
gb.querySyntax = Lucene Query Parser Syntax
gb.queryResults = \u691c\u7d22\u7d50\u679c {0} - {1} ({2} \u30d2\u30c3\u30c8)

@@ -223,0 +224,0 @@ gb.noHits = \u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f\u3002

@@ -1,182 +0,182 @@

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.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.date = date

@@ -221,3 +221,4 @@ gb.activity = \uc561\ud2f0\ube44\ud2f0

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)

@@ -289,175 +290,175 @@ gb.noHits = \uac80\uc0c9 \uacb0\uacfc \uc5c6\uc74c

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

@@ -468,280 +469,321 @@ gb.to = to

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.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.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.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.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.

@@ -221,3 +221,4 @@ gb.repository = repositorie

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)

@@ -480,3 +481,3 @@ gb.noHits = geen hits

gb.deletedTag = tag verwijderd
gb.pushedNewBranch = push neuwe branch
gb.pushedNewBranch = push nieuwe branch
gb.createdNewBranch = nieuwe branch gemaakt

@@ -483,0 +484,0 @@ gb.deletedBranch = branch verwijderd

@@ -221,3 +221,4 @@ gb.repository = repository

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)

@@ -224,0 +225,0 @@ gb.noHits = ingen treff

@@ -220,3 +220,4 @@ gb.repository = Repozytorium

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)

@@ -223,0 +224,0 @@ gb.noHits = Brak wynik\u00F3w

@@ -220,3 +220,4 @@ gb.repository = reposit�rio

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 � suportada.<p/><p/>Por favor veja ${querySyntax} para mais detalhes.
gb.querySyntax = Lucene Query Parser Syntax
gb.queryResults = resultados {0} - {1} ({2} hits)

@@ -223,0 +224,0 @@ gb.noHits = sem hits

@@ -221,3 +221,4 @@ gb.repository = \u7248\u672c\u5e93

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.<p/><p/>\u8bf7\u67e5\u770b ${querySyntax} \u4ee5\u83b7\u53d6\u8be6\u7ec6\u5185\u5bb9\u3002
gb.querySyntax = Lucene \u67e5\u8be2\u5904\u7406\u5668\u683c\u5f0f
gb.queryResults = \u7ed3\u679c {0} - {1} ({2} \u6b21\u547d\u4e2d)

@@ -224,0 +225,0 @@ gb.noHits = \u672a\u547d\u4e2d

#!
#! created/edited by Popeye version 0.54 (popeye.sourceforge.net)
#! created/edited by Popeye version 0.55 (https://github.com/koppor/popeye)
#! encoding:ISO-8859-1
gb.about = \u95dc\u65bc
gb.acceptNewPatchsets = \u5141\u8a31\u88dc\u4e01
gb.acceptNewPatchsetsDescription = \u63a5\u53d7\u5230\u7248\u672c\u5009\u9032\u884c\u4fee\u88dc\u52d5\u4f5c
gb.acceptNewTickets = \u5141\u8a31\u5efa\u7acb\u4efb\u52d9\u55ae
gb.acceptNewTicketsDescription = \u5141\u8a31\u65b0\u589e"\u81ed\u87f2","\u512a\u5316","\u4efb\u52d9"\u5404\u985e\u578b\u4efb\u52d9\u55ae
gb.accessDenied = \u62d2\u7d55\u5b58\u53d6
gb.accessLevel = \u5b58\u53d6\u7b49\u7d1a
gb.accessPermissions = \u5b58\u53d6\u6b0a\u9650
gb.accessPermissionsDescription = restrict access by users and teams
gb.accessPermissionsForTeamDescription = set team members and grant access to specific restricted repositories
gb.accessPermissionsForUserDescription = set team memberships or grant access to specific restricted repositories
gb.accessPolicy = \u5b58\u53d6\u653f\u7b56
gb.accessPolicyDescription = \u9078\u64c7\u7528\u4f86\u63a7\u5236\u6587\u4ef6\u5eab\u7684\u5b58\u53d6\u653f\u7b56\u4ee5\u53ca\u6b0a\u9650\u8a2d\u5b9a
gb.accessRestriction = \u9650\u5236\u5b58\u53d6
gb.accountPreferences = \u5e33\u865f\u8a2d\u5b9a
gb.accountPreferencesDescription = \u8a2d\u5b9a\u5e33\u865f\u9810\u8a2d\u503c
gb.action = \u52d5\u4f5c
gb.active = \u6d3b\u52d5
gb.activeAuthors = \u6d3b\u8e8d\u7528\u6236
gb.activeRepositories = \u6d3b\u8e8d\u7248\u672c\u5eab
gb.activity = \u6d3b\u52d5
gb.add = \u65b0\u589e
gb.addComment = \u65b0\u589e\u8a3b\u89e3
gb.addedNCommits = {0}\u500b\u6a94\u6848\u63d0\u4ea4\u5b8c\u7562
gb.addedOneCommit = \u63d0\u4ea41\u500b\u6a94\u6848
gb.addition = addition
gb.addSshKey = \u65b0\u589e SSH Key
gb.administration = \u7ba1\u7406\u6b0a\u9650
gb.administrator = \u7ba1\u7406\u54e1
gb.administratorPermission = Gitblit \u7ba1\u7406\u54e1
gb.affiliationChanged = affiliation changed
gb.repository = \u7248\u672c\u5eab
gb.owner = \u64c1\u6709\u8005
gb.description = \u6982\u8ff0
gb.lastChange = \u6700\u8fd1\u4fee\u6539
gb.refs = \u6bd4\u5c0d
gb.tag = \u6a19\u7c64
gb.tags = \u6a19\u7c64
gb.author = \u4f5c\u8005
gb.committer = \u78ba\u8a8d\u8005
gb.commit = \u63d0\u4ea4
gb.age = \u6642\u9593
gb.all = \u5168\u90e8
gb.tree = \u76ee\u9304
gb.parent = \u4e0a\u500b\u7248\u672c
gb.url = URL
gb.history = \u6b77\u53f2
gb.raw = \u539f\u59cb
gb.object = \u7269\u4ef6
gb.ticketId = \u4efb\u52d9ID
gb.ticketAssigned = \u5df2\u6307\u5b9a
gb.ticketOpenDate = \u767c\u884c\u65e5
gb.ticketComments = \u8a3b\u89e3
gb.view = \u6aa2\u8996
gb.local = \u672c\u5730\u7aef
gb.remote = \u9060\u7aef
gb.branches = \u5206\u652f
gb.patch = \u4fee\u88dc\u6a94
gb.diff = \u5dee\u7570
gb.log = \u65e5\u8a8c
gb.moreLogs = \u66f4\u591a\u63d0\u4ea4 ...
gb.allTags = \u6240\u6709\u6a19\u7c64
gb.allBranches = \u6240\u6709\u5206\u652f
gb.allowAuthenticatedDescription = \u6279\u51c6 RW+ \u6b0a\u9650\u7d66\u4e88\u5c08\u6848\u6210\u54e1
gb.allowForks = \u5141\u8a31\u5efa\u7acb\u5206\u652f(forks)
gb.allowForksDescription = \u5141\u8a31\u5df2\u6388\u6b0a\u7684\u4f7f\u7528\u8005\u5f9e\u6587\u4ef6\u5eab\u5efa\u7acb\u5206\u652f(fork)
gb.allowNamedDescription = grant fine-grained permissions to named users or teams
gb.allProjects = \u5168\u90e8\u7fa4\u7d44
gb.allTags = \u6240\u6709\u6a19\u7c64
gb.anonymousCanNotPropose = \u533f\u540d\u8005\u4e0d\u80fd\u63d0\u4f9b\u88dc\u4e01
gb.anonymousPolicy = \u533f\u540d\u72c0\u614b\u53ef\u4ee5View, Clone\u8207Push
gb.anonymousPolicyDescription = \u4efb\u4f55\u4eba\u53ef\u6aa2\u8996,\u8907\u88fd(clone)\u8207\u63a8\u9001(push)\u6587\u4ef6\u5230\u6587\u4ef6\u5eab
gb.anonymousUser= \u533f\u540d\u72c0\u614b
gb.any = \u4efb\u4f55
gb.approve = \u901a\u904e
gb.at = at
gb.attributes = \u5c6c\u6027
gb.authenticatedPushPolicy = Restrict Push (Authenticated)
gb.authenticatedPushPolicyDescription = \u4efb\u4f55\u4eba\u53ef\u4ee5\u6aa2\u8996\u8207\u8907\u88fd(clone).\u6240\u6709\u6587\u4ef6\u5eab\u6210\u54e1\u7686\u6709RW+\u8207\u63a8\u9001(push)\u529f\u80fd.
gb.author = \u4f5c\u8005
gb.authored = \u5df2\u6388\u6b0a
gb.authorizationControl = \u6388\u6b0a\u7ba1\u63a7
gb.available = \u53ef\u7528
gb.blame = \u7a76\u67e5
gb.blinkComparator = Blink comparator
gb.blob = \u5340\u584a
gb.body = body
gb.bootDate = \u555f\u52d5\u65e5
gb.branch = \u5206\u652f
gb.branches = \u5206\u652f
gb.branchStats = \u9019\u500b\u5206\u652f{2}\u6709{0}\u500b\u63d0\u4ea4\u4ee5\u53ca{1}\u500b\u6a19\u7c64
gb.browse = \u700f\u89bd
gb.bugTickets = \u81ed\u87f2
gb.busyCollectingGarbage = \u62b1\u6b49,Gitblit\u6b63\u5728\u56de\u6536\u7cfb\u7d71\u8cc7\u6e90\u4e2d:{0}
gb.byNAuthors = \u7d93\u7531{0}\u500b\u4f5c\u8005
gb.byOneAuthor = \u7d93\u7531{0}
gb.caCompromise = CA compromise
gb.summary = \u532f\u7e3d
gb.ticket = \u4efb\u52d9
gb.newRepository = \u5efa\u7acb\u7248\u672c\u5eab
gb.newUser = \u5efa\u7acb\u4f7f\u7528\u8005
gb.commitdiff = \u5dee\u7570
gb.tickets = \u4efb\u52d9
gb.pageFirst = \u7b2c\u4e00\u7b46
gb.pagePrevious = \u4e0a\u4e00\u9801
gb.pageNext = \u4e0b\u4e00\u9801
gb.head = HEAD
gb.blame = \u8a73\u67e5
gb.login = \u767b\u5165
gb.logout = \u767b\u51fa
gb.username = \u4f7f\u7528\u8005\u540d\u7a31
gb.password = \u5bc6\u78bc
gb.tagger = \u6a19\u8a18\u8005
gb.moreHistory = \u66f4\u591a\u6b77\u53f2\u7d00\u9304...
gb.difftocurrent = \u6bd4\u5c0d\u5dee\u7570
gb.search = \u641c\u5c0b
gb.searchForAuthor = \u4f9d\u5be9\u6838\u8005\u641c\u5c0b\u63d0\u4ea4\u5167\u5bb9
gb.searchForCommitter = \u4f9d\u63d0\u4ea4\u8005\u641c\u5c0b\u63d0\u4ea4\u5167\u5bb9
gb.addition = \u589e\u52a0
gb.modification = \u4fee\u6539
gb.deletion = \u522a\u9664
gb.rename = \u6539\u540d\u7a31
gb.metrics = \u7d71\u8a08
gb.stats = \u7d71\u8a08
gb.markdown = markdown
gb.changedFiles = \u5df2\u8b8a\u66f4\u904e\u7684\u6a94\u6848
gb.filesAdded = \u65b0\u589e{0}\u500b\u6a94\u6848
gb.filesModified = \u4fee\u6539{0}\u500b\u6a94\u6848
gb.filesDeleted = \u522a\u9664{0}\u500b\u6a94\u6848
gb.filesCopied = \u8907\u88fd{0}\u500b\u6a94\u6848
gb.filesRenamed = \u4fee\u6539{0}\u500b\u6a94\u6848\u540d\u7a31
gb.missingUsername = \u7121\u4f7f\u7528\u8005\u540d\u7a31
gb.edit = \u7de8\u8f2f
gb.searchTypeTooltip = \u9078\u64c7\u641c\u5c0b\u985e\u578b
gb.searchTooltip = \u641c\u5c0b{0}
gb.delete = \u522a\u9664
gb.docs = \u6587\u4ef6
gb.accessRestriction = \u9650\u5236\u5b58\u53d6
gb.name = \u540d\u5b57
gb.enableTickets = \u555f\u7528\u4efb\u52d9(Ticket)\u7cfb\u7d71
gb.enableDocs = \u555f\u7528\u8aaa\u660e\u6587\u4ef6
gb.save = \u5132\u5b58
gb.showRemoteBranches = \u986f\u793a\u9060\u7aef\u5206\u652f
gb.editUsers = \u4fee\u6539\u5e33\u865f
gb.confirmPassword = \u78ba\u8a8d\u5bc6\u78bc
gb.restrictedRepositories = \u53d7\u9650\u5236\u7684\u7248\u672c\u5eab
gb.canAdmin = \u53ef\u7ba1\u7406
gb.notRestricted = \u533f\u540d\u72c0\u614b\u53ef\u4ee5View, Clone\u8207Push
gb.pushRestricted = \u6709\u6388\u6b0a\u624d\u80fd\u63a8\u9001(push)
gb.cloneRestricted = \u6709\u6388\u6b0a\u624d\u80fd\u8907\u88fd(clone)\u8207\u63a8\u9001(push)
gb.viewRestricted = \u6709\u6388\u6b0a\u624d\u80fd\u6aa2\u8996(view),\u8907\u88fd(clone), \u8207\u63a8\u9001(push)
gb.useTicketsDescription = readonly, distributed Ticgit issues
gb.useDocsDescription = \u8a08\u7b97\u7248\u672c\u5eab\u88e1\u9762\u7684Markdown\u6a94\u6848
gb.showRemoteBranchesDescription = \u986f\u793a\u9060\u7aef\u5206\u652f(branches)
gb.canAdminDescription = \u53ef\u7ba1\u7406Gitblit\u4f3a\u670d\u5668
gb.permittedUsers = \u5141\u8a31\u7528\u6236
gb.isFrozen = \u51cd\u7d50\u63a5\u6536
gb.isFrozenDescription = \u7981\u6b62\u63a8\u9001(push)
gb.zip = zip\u58d3\u7e2e\u6a94
gb.showReadme = \u986f\u793areadme\u6587\u4ef6
gb.showReadmeDescription = \u5728\u532f\u7e3d\u9801\u9762\u4e2d\u986f\u793a"readme"(markdown\u683c\u5f0f)
gb.nameDescription = \u4f7f\u7528"/"\u505a\u70ba\u7248\u672c\u5eab\u7fa4\u7d44\u5206\u985e. \u5982: library/mycoolib.git
gb.ownerDescription = \u64c1\u6709\u8005\u53ef\u4fee\u6539\u7248\u672c\u5eab\u8a2d\u5b9a\u503c
gb.blob = \u5340\u584a
gb.commitActivityTrend = \u63d0\u4ea4\u8da8\u52e2
gb.commitActivityDOW = \u6bcf(\u65e5)\u9031\u63d0\u4ea4
gb.commitActivityAuthors = \u63d0\u4ea4\u6d3b\u8e8d\u7387(\u4f7f\u7528\u8005)
gb.feed = \u8cc7\u6599\u8a02\u95b1
gb.cancel = \u53d6\u6d88
gb.canCreate = \u53ef\u5efa\u7acb
gb.canCreateDescription = \u80fd\u5920\u5efa\u7acb\u79c1\u4eba\u6587\u4ef6\u5eab
gb.canFork = \u53ef\u5efa\u7acb\u5206\u652f(fork)
gb.canForkDescription = \u53ef\u4ee5\u5efa\u7acb\u6587\u4ef6\u5eab\u5206\u652f(fork),\u4e26\u4e14\u8907\u88fd\u5230\u79c1\u4eba\u6587\u4ef6\u5eab\u4e2d
gb.canNotLoadRepository = \u7121\u6cd5\u8f09\u5165\u7248\u672c\u5eab
gb.canNotProposePatchset = \u4e0d\u80fd\u63d0\u4f9b\u88dc\u4e01
gb.certificate = \u8b49\u66f8
gb.certificateRevoked = \u8b49\u66f8{0,number,0} \u5df2\u7d93\u88ab\u53d6\u6d88
gb.certificates = \u8b49\u66f8
gb.cessationOfOperation = cessation of operation
gb.changedFiles = \u5df2\u8b8a\u66f4\u904e\u7684\u6a94\u6848
gb.changedStatus = changed the status
gb.changePassword = \u4fee\u6539\u5bc6\u78bc
gb.checkout = \u6aa2\u51fa(checkout)
gb.checkoutStep1 = Fetch the current patchset \u2014 run this from your project directory
gb.checkoutStep2 = \u5c07\u8a72\u88dc\u4e01\u8f49\u51fa\u5230\u65b0\u7684\u5206\u652f\u7136\u5f8c\u7528\u4f86\u6aa2\u8996
gb.checkoutViaCommandLine = \u4f7f\u7528\u6307\u4ee4Checkout
gb.checkoutViaCommandLineNote = \u4f60\u53ef\u4ee5\u5f9e\u4f60\u6587\u4ef6\u5eab\u4e2dcheckout\u4e00\u4efd,\u7136\u5f8c\u9032\u884c\u6e2c\u8a66
gb.clearCache = \u6e05\u9664\u5feb\u53d6
gb.clientCertificateBundleSent = {0}\u7684\u7528\u6236\u8b49\u66f8\u5df2\u5bc4\u767c
gb.clientCertificateGenerated = \u6210\u529f\u7522\u751f{0}\u7684\u65b0\u8b49\u66f8
gb.isFederated = \u5df2\u7d93federated
gb.federateThis = \u8207\u6b64\u7248\u672c\u5eab federate
gb.federateOrigin = federate the origin
gb.excludeFromFederation = exclude from federation
gb.excludeFromFederationDescription = \u7981\u6b62federated \u7684Gitblit\u4f3a\u670d\u5668
gb.tokens = federation tokens
gb.tokenAllDescription = \u6240\u6709\u7248\u672c\u5eab,\u4f7f\u7528\u8005\u8207\u8a2d\u5b9a
gb.tokenUnrDescription = \u6240\u6709\u7248\u672c\u5eab\u8207\u4f7f\u7528\u8005
gb.tokenJurDescription = \u6240\u6709\u7248\u672c\u5eab
gb.federatedRepositoryDefinitions = \u7248\u672c\u5eab\u5b9a\u7fa9
gb.federatedUserDefinitions = \u4f7f\u7528\u8005\u5b9a\u7fa9
gb.federatedSettingDefinitions = setting definitions
gb.proposals = federation proposals
gb.received = \u5df2\u63a5\u6536
gb.type = \u985e\u578b
gb.token = token
gb.repositories = \u7248\u672c\u5eab
gb.proposal = \u63d0\u6848
gb.frequency = \u983b\u7387
gb.folder = \u76ee\u9304
gb.lastPull = \u4e0a\u6b21\u4e0b\u8f09(pull)
gb.nextPull = \u4e0b\u4e00\u500b pull
gb.inclusions = inclusions
gb.exclusions = \u6392\u9664
gb.registration = \u8a3b\u518a
gb.registrations = federation registrations
gb.sendProposal = \u63d0\u6848
gb.status = \u72c0\u614b
gb.origin = origin
gb.headRef = \u9810\u8a2d\u5206\u652f(HEAD)
gb.headRefDescription = \u9810\u8a2d\u5206\u652f\u5c07\u6703\u8907\u88fd\u4ee5\u53ca\u986f\u793a\u5230\u532f\u7e3d\u9801\u9762
gb.federationStrategy = federation \u7b56\u7565
gb.federationRegistration = federation registration
gb.federationResults = federation pull results
gb.federationSets = federation sets
gb.message = \u8a0a\u606f
gb.myUrlDescription = \u60a8Gitblit\u4f3a\u670d\u5668\u7684\u516c\u958bURL
gb.destinationUrl = \u50b3\u9001
gb.destinationUrlDescription = \u50b3\u9001Gitblit\u9023\u7d50\u5230\u4f60\u7684\u5c08\u6848(proposal)
gb.users = \u4f7f\u7528\u8005
gb.federation = federation
gb.error = \u932f\u8aa4
gb.refresh = \u91cd\u6574
gb.browse = \u700f\u89bd
gb.clone = \u8907\u88fd(clone)
gb.clonePermission = {0} \u8907\u88fd(clone)
gb.clonePolicy = Restrict Clone & Push
gb.clonePolicyDescription = \u4efb\u4f55\u4eba\u53ef\u4ee5\u770b\u6587\u4ef6\u5eab. \u4f46\u4f60\u80fd\u5920\u8907\u88fd(clone)\u8207\u63a8\u9001(push)
gb.cloneRestricted = authenticated clone & push
gb.closeBrowser = \u8acb\u95dc\u9589\u700f\u89bd\u5668\u7d50\u675f\u6b64\u767b\u5165\u968e\u6bb5
gb.closed = \u95dc\u9589
gb.closedMilestones = \u5df2\u95dc\u9589\u7684\u91cc\u7a0b\u7891(milestones)
gb.filter = \u7be9\u9078
gb.create = \u5efa\u7acb
gb.servers = \u4f3a\u670d\u5668
gb.recent = \u6700\u8fd1
gb.available = \u53ef\u7528
gb.selected = \u9078\u5b9a
gb.size = \u5bb9\u91cf
gb.downloading = \u4e0b\u8f09\u4e2d
gb.loading = \u8f09\u5165
gb.starting = \u555f\u52d5\u4e2d
gb.general = \u4e00\u822c
gb.settings = \u8a2d\u5b9a
gb.manage = \u7ba1\u7406
gb.lastLogin = \u6700\u8fd1\u767b\u5165
gb.skipSizeCalculation = \u7565\u904e\u5bb9\u91cf\u8a08\u7b97
gb.skipSizeCalculationDescription = \u4e0d\u8a08\u7b97\u7248\u672c\u5eab\u5bb9\u91cf(\u52a0\u5feb\u7db2\u9801\u8f09\u5165\u901f\u5ea6)
gb.skipSummaryMetrics = \u7565\u904e\u91cf\u5316\u532f\u7e3d
gb.skipSummaryMetricsDescription = \u4e0d\u8981\u8a08\u7b97\u91cf\u5316\u4e26\u4e14\u986f\u793a\u5728\u532f\u7e3d\u9801\u9762\u4e0a(\u52a0\u5feb\u901f\u5ea6)
gb.accessLevel = \u5b58\u53d6\u7b49\u7d1a
gb.default = \u9810\u8a2d
gb.setDefault = \u8a2d\u70ba\u9810\u8a2d\u503c
gb.since = \u5f9e
gb.bootDate = \u555f\u52d5\u65e5
gb.servletContainer = servlet\u5bb9\u5668
gb.heapMaximum = \u6700\u5927\u5806\u7a4d(heap)
gb.heapAllocated = \u5df2\u4f7f\u7528\u5806\u7a4d(Heap)
gb.heapUsed = \u5df2\u4f7f\u7528\u7684\u5806\u7a4d(heap)
gb.free = \u91cb\u653e
gb.version = \u7248\u672c
gb.releaseDate = \u767c\u8868\u65e5
gb.date = \u65e5\u671f
gb.activity = \u6d3b\u52d5
gb.subscribe = \u8a02\u95b1
gb.branch = \u5206\u652f
gb.maxHits = \u6700\u5927\u9ede\u64ca
gb.recentActivity = \u6700\u8fd1\u6d3b\u8e8d\u72c0\u6cc1
gb.recentActivityStats = \u904e\u53bb{0}\u5929,\u4e00\u5171\u6709{2}\u4eba\u505a\u4e86{1}\u4efd\u63d0\u4ea4
gb.recentActivityNone = \u904e\u53bb{0}\u5929/\u7121
gb.dailyActivity = \u6bcf\u65e5\u6d3b\u52d5
gb.activeRepositories = \u6d3b\u8e8d\u7248\u672c\u5eab
gb.activeAuthors = \u6d3b\u8e8d\u7528\u6236
gb.commits = \u63d0\u4ea4
gb.teams = \u5718\u968a
gb.teamName = \u5718\u968a\u540d\u7a31
gb.teamMembers = \u5718\u968a\u6210\u54e1
gb.teamMemberships = \u5718\u968a\u6210\u54e1(memberships)
gb.newTeam = \u5efa\u7acb\u5718\u968a
gb.permittedTeams = permitted teams
gb.emptyRepository = \u7a7a\u7684\u7248\u672c\u5eab
gb.repositoryUrl = \u7248\u672c\u5eab url
gb.mailingLists = \u90f5\u4ef6\u540d\u55ae
gb.preReceiveScripts = pre-receive \u8173\u672c
gb.postReceiveScripts = post-receive\u8173\u672c
gb.hookScripts = hook\u7684\u8173\u672c
gb.customFields = custom fields
gb.customFieldsDescription = custom fields available to Groovy hooks
gb.accessPermissions = \u5b58\u53d6\u6b0a\u9650
gb.filters = \u7be9\u9078
gb.generalDescription = \u4e00\u822c\u8a2d\u5b9a
gb.accessPermissionsDescription = restrict access by users and teams
gb.accessPermissionsForUserDescription = set team memberships or grant access to specific restricted repositories
gb.accessPermissionsForTeamDescription = set team members and grant access to specific restricted repositories
gb.federationRepositoryDescription = \u8207\u5176\u4ed6gitblit\u4f3a\u670d\u5668\u5206\u4eab\u4e00\u8d77\u4f7f\u7528\u9019\u500b\u7248\u672c\u5eab
gb.hookScriptsDescription = \u7576\u63a8\u9001(push)\u81f3\u6b64Gitblit\u7248\u63a7\u4f3a\u670d\u5668\u6642, \u57f7\u884cGroovy\u8173\u672c
gb.reset = \u6e05\u9664
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 ${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
gb.committed = \u5df2\u63d0\u4ea4
gb.indexedBranches = \u5206\u652f\u7d22\u5f15
gb.indexedBranchesDescription = \u9078\u5b9a\u6b32\u57f7\u884cLucene\u7d22\u5f15\u529f\u80fd\u7684\u5206\u652f
gb.noIndexedRepositoriesWarning = \u8ddf\u4f60\u76f8\u95dc\u7684\u7248\u672c\u5eab\u4e26\u6c92\u6709\u505aLucene\u7d22\u5f15
gb.undefinedQueryWarning = \u672a\u8a2d\u5b9a\u67e5\u8a62\u689d\u4ef6
gb.noSelectedRepositoriesWarning = \u8acb\u81f3\u5c11\u9078\u64c7\u4e00\u500b\u7248\u672c\u5eab
gb.luceneDisabled = \u505c\u7528Lucene\u7d22\u5f15\u529f\u80fd
gb.failedtoRead = \u8b80\u53d6\u5931\u6557
gb.isNotValidFile = \u4e0d\u662f\u6709\u6548\u6a94\u6848
gb.failedToReadMessage = Failed to read default message from {0}\!
gb.passwordsDoNotMatch = \u5bc6\u78bc\u4e0d\u76f8\u7b26
gb.passwordTooShort = \u5bc6\u78bc\u904e\u77ed, \u6700\u5c11{0}\u500b\u5b57\u5143
gb.passwordChanged = \u5bc6\u78bc\u8b8a\u66f4\u6210\u529f
gb.passwordChangeAborted = \u53d6\u6d88\u5bc6\u78bc\u8b8a\u66f4
gb.pleaseSetRepositoryName = \u8acb\u8a2d\u5b9a\u7248\u672c\u5eab\u540d\u7a31
gb.illegalLeadingSlash = \u7981\u6b62\u6839\u76ee\u9304(/)
gb.illegalRelativeSlash = \u7981\u6b62\u76f8\u5c0d\u76ee\u9304(../)
gb.illegalCharacterRepositoryName = \u7248\u672c\u5eab\u540d\u7a31\u6709\u4e0d\u5408\u6cd5\u7684\u5b57\u5143"{0}"
gb.selectAccessRestriction = Please select access restriction\!
gb.selectFederationStrategy = Please select federation strategy\!
gb.pleaseSetTeamName = \u8acb\u8f38\u5165\u5718\u968a\u540d\u7a31
gb.teamNameUnavailable = \u5718\u968a"{0}"\u4e0d\u5b58\u5728.
gb.teamMustSpecifyRepository = \u5718\u968a\u6700\u5c11\u8981\u6307\u5b9a\u4e00\u500b\u7248\u672c\u5eab
gb.teamCreated = \u5718\u968a"{0}"\u65b0\u589e\u6210\u529f.
gb.pleaseSetUsername = \u8acb\u8f38\u5165\u4f7f\u7528\u8005\u540d\u7a31
gb.usernameUnavailable = \u4f7f\u7528\u8005\u540d\u7a31"{0}"\u4e0d\u53ef\u7528
gb.combinedMd5Rename = Gitblit\u4f7f\u7528md5\u65b9\u5f0f\u5c07\u5bc6\u78bc\u7de8\u78bc(\u7121\u6cd5\u9084\u539f).\u4f60\u5fc5\u9808\u8f38\u5165\u65b0\u5bc6\u78bc.
gb.comment = \u8a3b\u89e3
gb.commented = \u5df2\u8a3b\u89e3
gb.comments = \u8a3b\u89e3
gb.commit = \u63d0\u4ea4
gb.commitActivityAuthors = \u63d0\u4ea4\u6d3b\u8e8d\u7387(\u4f7f\u7528\u8005)
gb.commitActivityDOW = \u6bcf(\u65e5)\u9031\u63d0\u4ea4
gb.commitActivityTrend = \u63d0\u4ea4\u8da8\u52e2\u5716
gb.commitdiff = \u63d0\u4ea4\u5dee\u7570
gb.userCreated = \u6210\u529f\u5efa\u7acb\u65b0\u4f7f\u7528\u8005"{0}"
gb.couldNotFindFederationRegistration = \u627e\u4e0d\u5230federation registration!
gb.failedToFindGravatarProfile = \u7121\u6cd5\u627e\u5230\u5e33\u865f{0}\u7684Gravator\u8cc7\u6599
gb.branchStats = \u9019\u500b\u5206\u652f{2}\u6709{0}\u500b\u63d0\u4ea4\u4ee5\u53ca{1}\u500b\u6a19\u7c64
gb.repositoryNotSpecified = \u672a\u6307\u5b9a\u7248\u672c\u5eab!
gb.repositoryNotSpecifiedFor = \u7248\u672c\u5eab\u4e26\u6c92\u6709\u6307\u5b9a\u7d66 {0}\!
gb.canNotLoadRepository = \u7121\u6cd5\u8f09\u5165\u7248\u672c\u5eab
gb.commitIsNull = \u63d0\u4ea4\u5167\u5bb9\u662f\u7a7a\u7684
gb.commitMessageRenderer = \u63d0\u4ea4\u8a0a\u606f\u5448\u73fe\u65b9\u5f0f
gb.commitMessageRendererDescription = \u63d0\u4ea4\u8a0a\u606f\u53ef\u4ee5\u4f7f\u7528\u6587\u5b57\u6216\u662f\u6a19\u8a18\u8a9e\u8a00(markup)\u5448\u73fe
gb.commits = \u63d0\u4ea4
gb.commitsInPatchsetN = \u88dc\u4e01 {0} \u7684\u63d0\u4ea4
gb.commitsTo = {0} commits to
gb.committed = \u5df2\u63d0\u4ea4
gb.committer = \u78ba\u8a8d\u63d0\u4ea4\u8005
gb.compare = \u6bd4\u5c0d
gb.compareToMergeBase = \u6bd4\u5c0d\u5f8c,\u5408\u4f75\u5230\u4e3b\u8981\u5de5\u4f5c\u5340
gb.compareToN = \u8207{0}\u9032\u884c\u6bd4\u5c0d
gb.unauthorizedAccessForRepository = \u7248\u672c\u5eab\u672a\u6388\u6b0a\u5b58\u53d6
gb.failedToFindCommit = \u5728{1}\u4e2d\u7121\u6cd5\u627e\u5230\u8a72 {0} \u63d0\u4ea4!
gb.couldNotFindFederationProposal = \u641c\u5c0b\u4e0d\u5230federation proposal!
gb.invalidUsernameOrPassword = \u932f\u8aa4\u7684\u4f7f\u7528\u8005\u540d\u7a31\u6216\u5bc6\u78bc!
gb.OneProposalToReview = \u6709\u4e00\u500bfederation proposal \u7b49\u5f85\u5be9\u67e5
gb.nFederationProposalsToReview = \u7e3d\u5171\u6709{0}\u500bfederation proposals\u7b49\u5f85\u5be9\u8996
gb.couldNotFindTag = \u627e\u4e0d\u5230\u6a19\u7c64{0}
gb.couldNotCreateFederationProposal = \u7121\u6cd5\u5efa\u7acbfederation proposals
gb.pleaseSetGitblitUrl = \u8acb\u8f38\u5165Gitblit URL !
gb.pleaseSetDestinationUrl = Please enter a destination url for your proposal\!
gb.proposalReceived = Proposal successfully received by {0}.
gb.noGitblitFound = Sorry, {0} could not find a Gitblit instance at {1}.
gb.noProposals = \u62b1\u6b49, {0}\u6b64\u6642\u4e26\u4e0d\u662f\u53ef\u63a5\u53d7\u7684proposals.
gb.noFederation = Sorry, {0} is not configured to federate with any Gitblit instances.
gb.proposalFailed = Sorry, {0} did not receive any proposal data\!
gb.proposalError = \u62b1\u6b49, {0} \u4efd\u5831\u544a\u767c\u751f\u9810\u671f\u5916\u7684\u932f\u8aa4!
gb.failedToSendProposal = \u63d0\u6848\u767c\u9001\u5931\u6557\!
gb.userServiceDoesNotPermitAddUser = {0}\u4e0d\u5141\u8a31\u65b0\u589e\u4f7f\u7528\u8005\u5e33\u865f
gb.userServiceDoesNotPermitPasswordChanges = {0}\u4e0d\u5141\u8a31\u4fee\u6539\u5bc6\u78bc
gb.displayName = \u986f\u793a\u7684\u540d\u7a31
gb.emailAddress = \u96fb\u5b50\u90f5\u4ef6
gb.errorAdminLoginRequired = \u767b\u5165\u9700\u6709\u7ba1\u7406\u6b0a\u9650
gb.errorOnlyAdminMayCreateRepository = \u53ea\u6709\u7ba1\u7406\u8005\u80fd\u5efa\u7acb\u7248\u672c\u5eab
gb.errorOnlyAdminOrOwnerMayEditRepository = \u53ea\u6709\u7ba1\u7406\u8005\u8207\u7248\u672c\u5eab\u64c1\u6709\u8005\u80fd\u4fee\u6539\u7248\u672c\u5eab\u5c6c\u6027
gb.errorAdministrationDisabled = \u7ba1\u7406\u6b0a\u9650\u5df2\u53d6\u6d88
gb.lastNDays = \u6700\u8fd1{0}\u5929
gb.completeGravatarProfile = \u5b8c\u6210Gravator.com\u4e0a\u7684\u57fa\u672c\u8cc7\u6599\u8a2d\u5b9a
gb.confirmPassword = \u78ba\u8a8d\u5bc6\u78bc
gb.none = \u7121
gb.line = \u884c
gb.content = \u5167\u5bb9
gb.copyToClipboard = \u8907\u88fd\u5230\u526a\u8cbc\u677f
gb.couldNotCreateFederationProposal = \u7121\u6cd5\u5efa\u7acb\u4e32\u9023\u7684\u5408\u4f5c\u63d0\u6848
gb.couldNotFindFederationProposal = \u641c\u5c0b\u4e0d\u5230\u8981\u6c42\u4e32\u9023\u7684\u63d0\u6848
gb.couldNotFindFederationRegistration = \u627e\u4e0d\u5230\u4e32\u9023\u8a3b\u518a\u55ae
gb.couldNotFindTag = \u627e\u4e0d\u5230\u6a19\u7c64{0}
gb.countryCode = \u570b\u5bb6\u4ee3\u78bc
gb.create = \u5efa\u7acb
gb.createdBy = created by
gb.createdNewBranch = \u5efa\u7acb\u65b0\u5206\u652f
gb.createdNewPullRequest = created pull request
gb.createdNewTag = \u5efa\u7acb\u65b0\u6a19\u7c64
gb.createdThisTicket = \u5df2\u958b\u7acb\u7684\u4efb\u52d9\u55ae
gb.createFirstTicket = \u6309\u6b64\u9996\u767c\u4efb\u52d9\u55ae
gb.createPermission = {0} (push, ref creation)
gb.createReadme = \u5efa\u7acbREADME\u6a94\u6848
gb.customFields = custom fields
gb.customFieldsDescription = custom fields available to Groovy hooks
gb.dailyActivity = \u6bcf\u65e5\u6d3b\u52d5
gb.dashboard = \u5100\u8868\u677f
gb.date = \u65e5\u671f
gb.default = \u9810\u8a2d
gb.delete = \u522a\u9664
gb.deletedBranch = deleted branch
gb.deletedTag = \u522a\u9664\u6a19\u7c64
gb.deleteMilestone = \u522a\u9664\u91cc\u7a0b\u7891"{0}"?
gb.deletePermission = {0} (push, ref creation+deletion)
gb.empty = \u7a7a\u7684
gb.inherited = \u7e7c\u627f
gb.deleteRepository = \u522a\u9664\u7248\u672c\u5eab"{0}"?
gb.deleteRepositoryDescription = \u7248\u672c\u5eab\u522a\u9664\u5c07\u7121\u6cd5\u9084\u539f
gb.deleteRepositoryHeader = \u522a\u9664\u7248\u672c\u5eab
gb.repositoryDeleted = \u7248\u672c\u5eab"{0}"\u5df2\u522a\u9664
gb.repositoryDeleteFailed = \u522a\u9664\u7248\u672c\u5eab"{0}"\u5931\u6557!
gb.deleteUser = \u522a\u9664\u4f7f\u7528\u8005"{0}"?
gb.deletion = \u522a\u9664
gb.description = \u6982\u8ff0
gb.destinationUrl = \u50b3\u9001
gb.destinationUrlDescription = \u50b3\u9001Gitblit\u9023\u7d50\u5230\u4f60\u7684\u5c08\u6848(proposal)
gb.diff = \u5dee\u7570
gb.diffCopiedFile = File was copied from {0}
gb.diffDeletedFile = \u6a94\u6848\u5df2\u522a\u9664
gb.diffDeletedFileSkipped = (\u522a\u9664)
gb.diffFileDiffTooLarge = \u6a94\u6848\u592a\u5927
gb.diffNewFile = \u6bd4\u5c0d\u65b0\u6a94\u6848
gb.diffRenamedFile = File was renamed from {0}
gb.diffStat = \u65b0\u589e{0}\u5217\u8207\u522a\u9664{1}\u5217
gb.difftocurrent = \u6bd4\u5c0d\u5dee\u7570
gb.diffTruncated = Diff truncated after the above file
gb.disableUser = \u505c\u7528\u5e33\u6236
gb.disableUserDescription = \u8a72\u5e33\u6236\u7121\u6cd5\u4f7f\u7528
gb.discussion = \u8a0e\u8ad6
gb.displayName = \u986f\u793a\u7684\u540d\u7a31
gb.displayNameDescription = \u5e0c\u671b\u986f\u793a\u7684\u540d\u7a31
gb.docs = \u6a94\u6848\u5340
gb.docsWelcome1 = \u4f60\u53ef\u4ee5\u4f7f\u7528\u6a94\u6848\u5340\u5efa\u7acb\u6587\u4ef6\u5eab\u7684\u6559\u5b78\u6a94\u6848
gb.docsWelcome2 = \u63d0\u4ea4README.md \u6216 HOME.md\u5f8c,\u518d\u958b\u59cb\u65b0\u7684\u6587\u4ef6\u5eab
gb.doesNotExistInTree = {0}\u4e26\u6c92\u6709\u5728\u76ee\u9304{1}\u88e1\u9762
gb.download = \u4e0b\u8f09
gb.downloading = \u4e0b\u8f09ing
gb.due = due
gb.duration = \u9031\u671f
gb.userDeleted = \u4f7f\u7528\u8005"{0}"\u5df2\u522a\u9664
gb.userDeleteFailed = \u4f7f\u7528\u8005"{0}"\u522a\u9664\u5931\u6557
gb.time.justNow = \u525b\u525b
gb.time.today = \u4eca\u5929
gb.time.yesterday = \u6628\u5929
gb.time.minsAgo = {0}\u5206\u9418\u524d
gb.time.hoursAgo = {0}\u5c0f\u6642\u524d
gb.time.daysAgo = {0}\u5929\u524d
gb.time.weeksAgo = {0}\u5468\u524d
gb.time.monthsAgo = {0}\u6708\u524d
gb.time.oneYearAgo = 1\u5e74\u524d
gb.time.yearsAgo = {0}\u5e74\u524d
gb.duration.oneDay = 1\u5929
gb.duration.days = {0}\u5929
gb.duration.oneMonth = 1\u6708
gb.duration.months = {0}\u6708
gb.duration.oneDay = 1\u5929
gb.duration.oneMonth = 1\u6708
gb.duration.oneYear = 1\u5e74
gb.duration.years = {0}\u5e74
gb.edit = \u7de8\u8f2f
gb.editMilestone = \u4fee\u6539milestone
gb.editTicket = \u4fee\u6539\u4efb\u52d9\u55ae
gb.editUsers = \u4fee\u6539\u5e33\u865f
gb.effective = \u6240\u6709\u6b0a\u9650
gb.emailAddress = \u96fb\u5b50\u90f5\u4ef6
gb.emailAddressDescription = \u7528\u4f86\u63a5\u6536\u901a\u77e5\u7684\u4e3b\u8981\u96fb\u5b50\u90f5\u4ef6
gb.emailCertificateBundle = \u5bc4\u767c\u7528\u6236\u7aef\u8b49\u66f8
gb.emailMeOnMyTicketChanges = \u6211\u7684\u4efb\u52d9\u55ae\u82e5\u6709\u8b8a\u66f4,\u8acb800\u91cc\u52a0\u6025(email)\u901a\u77e5\u6211
gb.emailMeOnMyTicketChangesDescription = \u6211\u8655\u7406\u904e\u7684\u4efb\u52d9\u55ae\u8acbemail\u901a\u77e5\u6211
gb.empty = \u7a7a\u7684
gb.emptyRepository = \u7a7a\u7684\u7248\u672c\u5eab
gb.enableDocs = \u555f\u7528\u6a94\u6848\u5340
gb.enableIncrementalPushTags = \u555f\u7528\u81ea\u52d5\u65b0\u589e\u6a19\u7c64\u529f\u80fd
gb.enableTickets = \u555f\u7528\u4efb\u52d9\u55ae\u7cfb\u7d71
gb.enhancementTickets = \u512a\u5316
gb.enterKeystorePassword = \u8acb\u8f38\u5165Gitblit\u7684keystore\u5c08\u7528\u5bc6\u78bc
gb.error = \u932f\u8aa4
gb.errorAdministrationDisabled = \u7ba1\u7406\u6b0a\u9650\u5df2\u53d6\u6d88
gb.errorAdminLoginRequired = \u767b\u5165\u9700\u6709\u7ba1\u7406\u6b0a\u9650
gb.errorOnlyAdminMayCreateRepository = \u53ea\u6709\u7ba1\u7406\u8005\u80fd\u5efa\u7acb\u7248\u672c\u5eab
gb.errorOnlyAdminOrOwnerMayEditRepository = \u53ea\u6709\u7ba1\u7406\u8005\u8207\u7248\u672c\u5eab\u64c1\u6709\u8005\u80fd\u4fee\u6539\u7248\u672c\u5eab\u5c6c\u6027
gb.excludeFromActivity = exclude from activity page
gb.excludeFromFederation = \u6392\u9664\u4e32\u9023
gb.excludeFromFederationDescription = \u963b\u64cb\u5df2\u4e32\u9023\u7684Gitblit\u4f3a\u670d\u5668
gb.excludePermission = {0} (\u6392\u9664)
gb.exclusions = \u6392\u9664
gb.expired = \u904e\u671f
gb.expires = \u5230\u671f
gb.expiring = \u5c07\u8981\u904e\u671f
gb.export = \u532f\u51fa
gb.extensions = \u64f4\u5145
gb.externalPermissions = {0} access permissions are externally maintained
gb.failedToFindAccount = \u7121\u6cd5\u641c\u5c0b\u5230\u5e33\u865f"{0}"
gb.failedToFindCommit = Failed to find commit "{0}" in {1}\!
gb.failedToFindGravatarProfile = \u7121\u6cd5\u627e\u5230\u5e33\u865f{0}\u7684Gravator\u8cc7\u6599
gb.failedtoRead = \u8b80\u53d6\u5931\u6557
gb.failedToReadMessage = Failed to read default message from {0}\!
gb.failedToSendProposal = \u63d0\u6848\u767c\u9001\u5931\u6557\!
gb.failedToUpdateUser = \u7121\u6cd5\u66f4\u65b0\u4f7f\u7528\u8005\u5e33\u865f
gb.federatedRepositoryDefinitions = \u7248\u672c\u5eab\u5b9a\u7fa9
gb.federatedSettingDefinitions = setting definitions
gb.federatedUserDefinitions = user definitions
gb.federateOrigin = federate the origin
gb.federateThis = \u8207\u672c\u6587\u4ef6\u5eab\u4e32\u9023
gb.federation = \u4e32\u9023
gb.federationRegistration = federation registration
gb.federationRepositoryDescription = \u8207\u5176\u4ed6gitblit\u4f3a\u670d\u5668\u5206\u4eab\u4e00\u8d77\u4f7f\u7528\u9019\u500b\u7248\u672c\u5eab
gb.federationResults = federation pull results
gb.federationSets = \u4e32\u9023\u7d44\u5408
gb.federationSetsDescription = \u6b64\u6587\u4ef6\u5eab\u5c07\u5305\u542b\u65bc\u6307\u5b9a\u7684\u4e32\u9023\u7fa4\u7d44(federation sets)
gb.federationStrategy = \u4e32\u9023\u7b56\u7565
gb.federationStrategyDescription = \u63a7\u5236\u5982\u4f55\u5c07\u6587\u4ef6\u5eab\u8207\u5176\u4ed6Gitblit\u7248\u63a7\u4f3a\u670d\u5668\u4e32\u9023
gb.feed = \u8cc7\u6599\u8a02\u95b1
gb.filesAdded = \u65b0\u589e{0}\u500b\u6a94\u6848
gb.filesCopied = \u8907\u88fd{0}\u500b\u6a94\u6848
gb.filesDeleted = \u522a\u9664{0}\u500b\u6a94\u6848
gb.filesModified = \u4fee\u6539{0}\u500b\u6a94\u6848
gb.filesRenamed = \u4fee\u6539{0}\u500b\u6a94\u6848\u540d\u7a31
gb.filter = \u689d\u4ef6\u904e\u6ffe
gb.filters = \u67e5\u8a62\u689d\u4ef6
gb.findSomeRepositories = \u641c\u5c0b\u6587\u4ef6\u5eab
gb.folder = \u76ee\u9304
gb.authorizationControl = \u6388\u6b0a\u7ba1\u63a7
gb.allowAuthenticatedDescription = \u6279\u51c6 RW+ \u6b0a\u9650\u7d66\u4e88\u5c08\u6848\u6210\u54e1
gb.allowNamedDescription = grant fine-grained permissions to named users or teams
gb.markdownFailure = \u89e3\u6790Markdown\u5931\u6557
gb.clearCache = \u6e05\u9664\u5feb\u53d6
gb.projects = \u7fa4\u7d44
gb.project = \u7fa4\u7d44
gb.allProjects = \u5168\u90e8\u7fa4\u7d44
gb.copyToClipboard = \u8907\u88fd\u5230\u526a\u8cbc\u677f
gb.fork = \u5efa\u7acb\u5206\u652f(fork)
gb.forkedFrom = forked from
gb.forkInProgress = fork in progress
gb.forkNotAuthorized = \u5f88\u62b1\u6b49, \u4f60\u7121\u5efa\u7acb\u6587\u4ef6\u5eab{0}\u5206\u652f(fork)\u7684\u6b0a\u9650
gb.forks = \u5206\u652f(forks)
gb.forkRepository = \u7248\u672c\u5eab{0}\u5efa\u7acb\u5206\u652f(fork)?
gb.forks = \u5206\u652f(forks)
gb.repositoryForked = \u7248\u672c\u5eab{0}\u5df2\u7d93\u5efa\u7acb\u5206\u652f(fork)
gb.repositoryForkFailed = \u5efa\u7acb\u5206\u652f(fork)\u5931\u6557
gb.personalRepositories = \u500b\u4eba\u7248\u672c\u5eab
gb.allowForks = \u5141\u8a31\u5efa\u7acb\u5206\u652f(forks)
gb.allowForksDescription = \u5141\u8a31\u5df2\u6388\u6b0a\u7684\u4f7f\u7528\u8005\u5f9e\u7248\u672c\u5eab\u5efa\u7acb\u5206\u652f(fork)
gb.forkedFrom = \u6e90\u81ea\u65bc
gb.canFork = \u53ef\u5efa\u7acb\u5206\u652f(fork)
gb.canForkDescription = \u53ef\u4ee5\u5efa\u7acb\u7248\u672c\u5eab\u5206\u652f(fork),\u4e26\u4e14\u8907\u88fd\u5230\u79c1\u4eba\u7248\u672c\u5eab\u4e2d
gb.myFork = \u6aa2\u8996\u6211\u5efa\u7acb\u7684\u5206\u652f(fork)
gb.forksProhibited = \u7981\u6b62\u5efa\u7acb\u5206\u652f(forks)
gb.forksProhibitedWarning = \u672c\u6587\u4ef6\u5eab\u7981\u6b62\u5206\u652f(fork)
gb.free = \u91cb\u653e
gb.frequency = \u983b\u7387
gb.from = from
gb.garbageCollection = \u56de\u6536\u7cfb\u7d71\u8cc7\u6e90
gb.garbageCollectionDescription = \u7cfb\u7d71\u8cc7\u6e90\u56de\u6536\u529f\u80fd\u5c07\u6703\u6574\u9813\u9b06\u6563\u7528\u6236\u7aef\u63a8\u9001(push)\u7684\u7269\u4ef6, \u4e5f\u6703\u79fb\u9664\u6587\u4ef6\u5eab\u4e0a\u7121\u7528\u7684\u7269\u4ef6
gb.gc = \u7cfb\u7d71\u8cc7\u6e90\u56de\u6536\u5668
gb.forksProhibitedWarning = \u672c\u7248\u672c\u5eab\u7981\u6b62\u5206\u652f(fork)
gb.noForks = {0}\u6c92\u6709\u5206\u652f(fork)
gb.forkNotAuthorized = \u5f88\u62b1\u6b49, \u4f60\u7121\u5efa\u7acb\u7248\u672c\u5eab{0}\u5206\u652f(fork)\u7684\u6b0a\u9650
gb.forkInProgress = \u6b63\u5728\u8907\u88fd\u4e2d(fork)
gb.preparingFork = \u6b63\u5728\u6e96\u5099\u8907\u88fd\u4e2d(fork)...
gb.isFork = \u662f\u5206\u652f\u985e\u578b(fork)
gb.canCreate = \u53ef\u5efa\u7acb
gb.canCreateDescription = \u80fd\u5920\u5efa\u7acb\u500b\u4eba\u7248\u672c\u5eab
gb.illegalPersonalRepositoryLocation = \u4f60\u500b\u4eba\u7248\u672c\u5eab\u5fc5\u9808\u653e\u5728"{0}"
gb.verifyCommitter = \u63d0\u4ea4\u8005\u9700\u9a57\u8b49
gb.verifyCommitterDescription = \u9700\u8981\u63d0\u4ea4\u8005\u7b26\u5408\u63a8\u9001\u5e33\u865f
gb.verifyCommitterNote = \u6240\u6709\u5408\u4f75\u52d5\u4f5c\u7686\u9808\u5f37\u5236\u4f7f\u7528"--no-ff"\u53c3\u6578
gb.repositoryPermissions = \u7248\u672c\u5eab\u6b0a\u9650
gb.userPermissions = \u4f7f\u7528\u8005\u6b0a\u9650
gb.teamPermissions = \u5718\u968a\u6b0a\u9650
gb.add = \u65b0\u589e
gb.noPermission = \u522a\u9664\u9019\u500b\u6b0a\u9650
gb.excludePermission = {0} \u6392\u9664(exclude)
gb.viewPermission = {0} \u6aa2\u8996(view)
gb.clonePermission = {0} \u8907\u88fd(clone)
gb.pushPermission = {0} \u63a8\u9001(push)
gb.createPermission = {0} (push, ref creation)
gb.deletePermission = {0} (push, ref creation+deletion)
gb.rewindPermission = {0} (push, ref creation+deletion+rewind)
gb.permission = \u6b0a\u9650
gb.regexPermission = \u5df2\u7d93\u4f7f\u7528\u6b63\u898f\u8868\u793a\u5f0f(regular expression)"{0}" \u8a2d\u5b9a\u6b0a\u9650\u5b8c\u7562
gb.accessDenied = \u62d2\u7d55\u5b58\u53d6
gb.busyCollectingGarbage = \u62b1\u6b49,Gitblit\u6b63\u5728\u56de\u6536\u7cfb\u7d71\u8cc7\u6e90\u4e2d:{0}
gb.gcPeriod = \u7cfb\u7d71\u8cc7\u6e90\u56de\u6536\u968e\u6bb5

@@ -271,503 +370,417 @@ gb.gcPeriodDescription = \u56de\u6536\u9031\u671f

gb.gcThresholdDescription = \u89f8\u767c\u7cfb\u7d71\u8cc7\u6e90\u56de\u6536\u7684\u6700\u5c0f\u7269\u4ef6\u5bb9\u91cf
gb.general = \u4e00\u822c
gb.generalDescription = \u4e00\u822c\u8a2d\u5b9a
gb.hasNotReviewed = \u5c1a\u672a\u6aa2\u6838\u904e
gb.head = HEAD
gb.headRef = \u9810\u8a2d\u5206\u652f(HEAD)
gb.headRefDescription = \u9810\u8a2d\u5206\u652f\u5c07\u6703\u8907\u88fd\u4ee5\u53ca\u986f\u793a\u5230\u532f\u7e3d\u9801\u9762
gb.heapAllocated = \u5df2\u4f7f\u7528\u5806\u7a4d(Heap)
gb.heapMaximum = \u6700\u5927\u5806\u7a4d(heap)
gb.heapUsed = \u5df2\u4f7f\u7528\u7684\u5806\u7a4d(heap)
gb.history = \u6b77\u7a0b
gb.home = \u9996\u9801
gb.hookScripts = hook\u7684\u8173\u672c
gb.hookScriptsDescription = \u7576\u63a8\u9001(push)\u81f3\u6b64Gitblit\u7248\u63a7\u4f3a\u670d\u5668\u6642, \u57f7\u884cGroovy\u8173\u672c
gb.ownerPermission = \u7248\u672c\u5eab\u64c1\u6709\u8005
gb.administrator = \u7ba1\u7406\u54e1
gb.administratorPermission = Gitblit \u7ba1\u7406\u54e1
gb.team = \u5718\u968a
gb.teamPermission = "{0}" \u5718\u968a\u6210\u54e1\u7684\u6b0a\u9650
gb.missing = \u5931\u8aa4!
gb.missingPermission = \u8a72\u6b0a\u9650\u7121\u6cd5\u5c0d\u61c9\u5230\u7248\u672c\u5eab!
gb.mutable = \u52d5\u614b\u7d66\u4e88
gb.specified = \u6307\u5b9a\u7d66\u4e88(\u542b\u7cfb\u7d71\u9810\u8a2d)
gb.effective = \u6240\u6709\u6b0a\u9650
gb.organizationalUnit = \u7d44\u7e54\u55ae\u4f4d
gb.organization = \u7d44\u7e54
gb.locality = \u4f4d\u7f6e
gb.stateProvince = \u5dde\u6216\u7701
gb.countryCode = \u570b\u5bb6\u4ee3\u78bc
gb.properties = \u5c6c\u6027
gb.issued = \u767c\u51fa
gb.expires = \u5230\u671f
gb.expired = \u904e\u671f
gb.expiring = \u5c07\u8981\u904e\u671f
gb.revoked = \u5df2\u64a4\u92b7
gb.serialNumber = \u5e8f\u865f
gb.certificates = \u8b49\u66f8
gb.newCertificate = \u5efa\u7acb\u8b49\u66f8
gb.revokeCertificate = \u64a4\u56de\u8b49\u66f8
gb.sendEmail = \u767cemail
gb.passwordHint = \u5bc6\u78bc\u63d0\u793a
gb.ok = ok
gb.invalidExpirationDate = \u4e0d\u6b63\u78ba\u7684\u5230\u671f\u65e5
gb.passwordHintRequired = \u5bc6\u78bc\u63d0\u793a(\u5fc5\u8981)
gb.viewCertificate = \u6aa2\u8996\u8b49\u66f8
gb.subject = \u6a19\u984c
gb.issuer = \u767c\u884c\u8005
gb.validFrom = \u6709\u6548\u671f\u5f9e
gb.validUntil = \u6709\u6548\u671f\u81f3
gb.publicKey = \u516c\u958b\u91d1\u9470
gb.signatureAlgorithm = \u7c3d\u7ae0\u6f14\u7b97\u6cd5
gb.sha1FingerPrint = SHA-1 Fingerprint
gb.md5FingerPrint = MD5 Fingerprint
gb.reason = \u539f\u56e0
gb.revokeCertificateReason = \u8acb\u8f38\u5165\u64a4\u56de\u8b49\u66f8\u7406\u7531
gb.unspecified = \u672a\u6307\u5b9a
gb.keyCompromise = \u91d1\u9470\u5bc6\u78bc\u5916\u6d29
gb.caCompromise = CA compromise
gb.affiliationChanged = affiliation changed
gb.superseded = \u5df2\u88ab\u66ff\u4ee3
gb.cessationOfOperation = cessation of operation
gb.privilegeWithdrawn = \u53d6\u6d88\u6b0a\u9650
gb.time.inMinutes = {0}\u5206\u9418\u5167
gb.time.inHours = {0}\u5c0f\u6642\u5167
gb.time.inDays = {0}\u5929\u5167
gb.hostname = \u4e3b\u6a5f\u540d\u7a31
gb.hostnameRequired = \u8acb\u8f38\u5165\u4e3b\u6a5f\u540d\u7a31
gb.ignore_whitespace =\u5ffd\u7565\u7a7a\u767d
gb.illegalCharacterRepositoryName = \u7248\u672c\u5eab\u540d\u7a31\u6709\u4e0d\u5408\u6cd5\u7684\u5b57\u5143"{0}"
gb.illegalLeadingSlash = \u7981\u6b62\u6839\u76ee\u9304(/)
gb.illegalPersonalRepositoryLocation = \u4f60\u79c1\u4eba\u7248\u672c\u5eab\u5fc5\u9808\u653e\u5728"{0}"
gb.illegalRelativeSlash = \u7981\u6b62\u76f8\u5c0d\u76ee\u9304(../)
gb.imgdiffSubtract = Subtract (black = identical)
gb.in = in
gb.inclusions = inclusions
gb.incrementalPushTagMessage = \u7576[{0}]\u5206\u652f\u63a8\u9001\u5f8c,\u81ea\u52d5\u7d66\u4e88\u6a19\u7c64\u865f.
gb.indexedBranches = \u5206\u652f\u7d22\u5f15
gb.indexedBranchesDescription = \u9078\u5b9a\u6b32\u57f7\u884cLucene\u7d22\u5f15\u529f\u80fd\u7684\u5206\u652f
gb.inherited = \u7e7c\u627f
gb.initialCommit = \u521d\u6b21\u63d0\u4ea4
gb.initialCommitDescription = \u4ee5\u4e0b\u6b65\u9a5f\u5c07\u6703\u8b93\u4f60\u99ac\u4e0a\u57f7\u884c<code>git clone</code>.\u5982\u679c\u4f60\u672c\u6a5f\u5df2\u6709\u6b64\u6587\u4ef6\u5eab\u4e14\u57f7\u884c\u904e<code>git init</code>,\u8acb\u8df3\u904e\u6b64\u6b65\u9a5f.
gb.initWithGitignore = \u5305\u542b .gitignore \u6a94\u6848
gb.initWithGitignoreDescription = \u65b0\u589e\u4e00\u500b\u8a2d\u5b9a\u6a94\u7528\u4f86\u6307\u5b9a\u54ea\u4e9b\u6a94\u6848\u6216\u76ee\u9304\u9700\u8981\u5ffd\u7565
gb.initWithReadme = \u5305\u542bREADME\u6587\u4ef6
gb.initWithReadmeDescription = \u6587\u4ef6\u5eab\u5c07\u7522\u751f\u7c21\u55aeREADME\u6587\u4ef6
gb.invalidExpirationDate = \u4e0d\u6b63\u78ba\u7684\u5230\u671f\u65e5
gb.invalidUsernameOrPassword = \u932f\u8aa4\u7684\u4f7f\u7528\u8005\u540d\u7a31\u6216\u5bc6\u78bc!
gb.isFederated = \u5df2\u7d93\u4e32\u9023
gb.isFork = \u662f\u5206\u652f\u985e\u578b(fork)
gb.isFrozen = \u51cd\u7d50\u63a5\u6536
gb.isFrozenDescription = \u7981\u6b62\u63a8\u9001(push)
gb.isMirror = \u8a72\u6587\u4ef6\u5eab\u70ba\u93e1\u50cf(mirror)
gb.isNotValidFile = \u4e0d\u662f\u6b63\u5e38\u6a94\u6848
gb.isSparkleshared = \u8a72\u6587\u4ef6\u5eab\u5df2\u70baSparkleshared (http://sparkleshare.org)
gb.issued = \u767c\u51fa
gb.issuer = issuer
gb.newSSLCertificate = \u65b0\u7684\u4f3a\u670d\u5668SSL\u8b49\u66f8
gb.newCertificateDefaults = \u65b0\u8b49\u66f8\u9810\u8a2d\u503c
gb.duration = \u9031\u671f
gb.certificateRevoked = \u8b49\u66f8{0,number,0} \u5df2\u7d93\u88ab\u53d6\u6d88
gb.clientCertificateGenerated = \u6210\u529f\u7522\u751f{0}\u7684\u65b0\u8b49\u66f8
gb.sslCertificateGenerated = \u6210\u529f\u7522\u751f\u7d66{0}\u7684\u670d\u5668SSL\u8b49\u66f8
gb.newClientCertificateMessage = \u6ce8\u610f:\n'password'\u5bc6\u78bc\u4e26\u4e0d\u662f\u4f7f\u7528\u8005\u5bc6\u78bc, \u800c\u662f\u7528\u4f86\u4fdd\u8b77\u4f7f\u7528\u8005\u500b\u4eba\u7684keystore.\u8a72\u5bc6\u78bc\u4e26\u4e0d\u6703\u5132\u5b58, \u56e0\u6b64\u5fc5\u9808\u8a2d\u5b9a\u63d0\u793a(hint), \u8a72\u63d0\u793a\u5c07\u6703\u5beb\u5728\u4f7f\u7528\u8005\u7684README\u6587\u4ef6\u88e1\u9762.
gb.certificate = \u8b49\u66f8
gb.emailCertificateBundle = \u5bc4\u767c\u7528\u6236\u7aef\u8b49\u66f8
gb.pleaseGenerateClientCertificate = \u8acb\u7522\u751f\u7d66{0}\u4f7f\u7528\u7684\u7528\u6236\u7aef\u8b49\u66f8
gb.clientCertificateBundleSent = {0}\u7684\u7528\u6236\u8b49\u66f8\u5df2\u5bc4\u767c
gb.enterKeystorePassword = \u8acb\u8f38\u5165Gitblit\u7684keystore\u5c08\u7528\u5bc6\u78bc
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.key = \u91d1\u9470
gb.keyCompromise = \u91d1\u9470\u5bc6\u78bc\u5916\u6d29
gb.labels = \u6a19\u8a18
gb.languagePreference = \u5e38\u7528\u8a9e\u8a00
gb.languagePreferenceDescription = \u9078\u64c7\u4f60\u60f3\u8981\u7684Gitblit\u7ffb\u8b6f
gb.lastChange = \u6700\u8fd1\u4fee\u6539
gb.lastLogin = \u6700\u8fd1\u767b\u5165
gb.lastNDays = \u6700\u8fd1{0}\u5929
gb.lastPull = \u4e0a\u6b21\u4e0b\u8f09(pull)
gb.leaveComment = \u7559\u4e0b\u8a3b\u89e3
gb.line = \u884c
gb.loading = \u8f09\u5165
gb.local = \u672c\u5730\u7aef
gb.locality = \u4f4d\u7f6e
gb.log = \u65e5\u8a8c
gb.login = \u767b\u5165
gb.logout = \u767b\u51fa
gb.looksGood = \u770b\u8d77\u4f86\u5f88\u597d
gb.luceneDisabled = \u505c\u7528Lucene\u7d22\u5f15\u529f\u80fd
gb.mailingLists = \u90f5\u4ef6\u540d\u55ae
gb.maintenanceTickets = \u7dad\u8b77
gb.manage = \u7ba1\u7406
gb.manual = \u81ea\u884c\u8f38\u5165
gb.markdown = markdown
gb.markdownFailure = \u89e3\u6790Markdown\u5931\u6557
gb.maxActivityCommits = \u6700\u5927\u63d0\u4ea4\u6d3b\u8e8d\u7387
gb.maxActivityCommitsDescription = \u6700\u5927\u63d0\u4ea4\u6d3b\u8e8d\u6578\u91cf
gb.maxHits = \u6700\u5927\u9ede\u64ca
gb.md5FingerPrint = MD5 Fingerprint
gb.mentions = \u63d0\u5230
gb.mentionsMeTickets = \u63d0\u5230\u4f60
gb.merge = \u5408\u4f75
gb.mergeBase = \u57fa\u672c\u5408\u4f75
gb.merged = \u5df2\u5408\u4f75
gb.mergedPatchset = \u5c07\u88dc\u4e01\u5408\u4f75
gb.mergedPullRequest = \u5408\u4f75\u63a8\u9001\u8981\u6c42
gb.mergeSha = mergeSha
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 = \u5c07\u63d0\u6848\u4fee\u6539\u5167\u5bb9\u5408\u4f75\u5230\u4f3a\u670d\u5668\u4e0a
gb.mergeTo = \u5408\u4f75\u5230
gb.mergeToDescription = \u9810\u8a2d\u5c07\u6587\u4ef6\u76f8\u95dc\u88dc\u4e01\u5305\u8207\u6307\u5b9a\u5206\u652f(branch)\u5408\u4f75
gb.mergingViaCommandLine = \u7d93\u7531\u6307\u4ee4\u57f7\u884c\u5408\u4f75
gb.mergingViaCommandLineNote = \u5982\u679c\u4f60\u4e0d\u60f3\u8981\u4f7f\u7528\u81ea\u52d5\u5408\u4f75\u529f\u80fd,\u6216\u662f\u6309\u4e0b\u5408\u4f75\u6309\u9215, \u4f60\u53ef\u4ee5\u4e0b\u6307\u4ee4\u624b\u52d5\u5408\u4f75
gb.message = \u8a0a\u606f
gb.metricAuthorExclusions = \u91cf\u5316\u7d71\u8a08\u6642\u6392\u9664\u6d3b\u8e8d\u5e33\u6236
gb.metrics = \u91cf\u5316\u7d71\u8a08
gb.milestone = \u91cc\u7a0b\u7891
gb.milestoneDeleteFailed = \u522a\u9664\u91cc\u7a0b\u7891"{0}"\u5931\u6557
gb.milestoneProgress = {0}\u958b\u555f,{1}\u7d50\u675f
gb.milestones = \u91cc\u7a0b\u7891
gb.mirrorOf = {0}\u7684\u93e1\u50cf
gb.mirrorWarning = \u8a72\u6587\u4ef6\u5eab\u5c6c\u65bc\u93e1\u50cf, \u4e0d\u80fd\u5920\u63a5\u6536\u63a8\u9001(push)
gb.miscellaneous = \u5176\u4ed6
gb.missing = \u5931\u8aa4!
gb.missingIntegrationBranchMore = \u76ee\u6a19\u5206\u652f\u4e0d\u5728\u6b64\u7248\u672c\u5eab
gb.missingPermission = the repository for this permission is missing\!
gb.missingUsername = \u7f3a\u5c11\u4f7f\u7528\u8005\u540d\u7a31
gb.modification = \u4fee\u6539
gb.noMaximum = \u7121\u6700\u5927\u503c
gb.attributes = \u5c6c\u6027
gb.serveCertificate = \u555f\u7528\u4f7f\u7528\u6b64\u8b49\u66f8\u7684https\u529f\u80fd
gb.sslCertificateGeneratedRestart = \u6210\u529f\u7522\u751f\u7d66{0}\u4f7f\u7528\u7684SSL\u8b49\u66f8\n\u4f60\u5fc5\u9808\u91cd\u65b0\u555f\u52d5Gitblit\u7248\u63a7\u4f3a\u670d\u5668\u624d\u80fd\u555f\u7528\u65b0\u7684\u8b49\u66f8\n\nf you are launching with the '--alias' parameter you will have to set that to ''--alias {0}''.
gb.validity = validity
gb.siteName = \u7db2\u7ad9\u540d\u7a31
gb.siteNameDescription = \u4f3a\u670d\u5668\u7c21\u7a31
gb.excludeFromActivity = exclude from activity page
gb.isSparkleshared = \u8a72\u7248\u672c\u5eab\u5df2\u70baSparkleshared (http://sparkleshare.org)
gb.owners = \u64c1\u6709\u8005
gb.sessionEnded = session\u5df2\u7d93\u53d6\u6d88
gb.closeBrowser = \u8acb\u95dc\u9589\u700f\u89bd\u5668\u7d50\u675f\u6b64\u767b\u5165\u968e\u6bb5
gb.doesNotExistInTree = {0}\u4e26\u6c92\u6709\u5728\u76ee\u9304{1}\u88e1\u9762
gb.enableIncrementalPushTags = \u555f\u7528\u81ea\u52d5\u65b0\u589e\u6a19\u7c64\u529f\u80fd
gb.useIncrementalPushTagsDescription = \u63a8\u9001\u6642\u5c07\u81ea\u52d5\u65b0\u589e\u6a19\u7c64\u865f\u78bc
gb.incrementalPushTagMessage = \u7576[{0}]\u5206\u652f\u63a8\u9001\u5f8c,\u81ea\u52d5\u7d66\u4e88\u6a19\u7c64\u865f.
gb.externalPermissions = {0} access permissions are externally maintained
gb.viewAccess = \u4f60\u6c92\u6709Gitblit\u8b80\u53d6\u6216\u662f\u4fee\u6539\u6b0a\u9650
gb.overview = \u6982\u89c0
gb.dashboard = \u5100\u8868\u677f
gb.monthlyActivity = \u6708\u6d3b\u52d5
gb.myProfile = \u6211\u7684\u57fa\u672c\u8cc7\u6599
gb.compare = \u6bd4\u5c0d
gb.manual = \u81ea\u884c\u8f38\u5165
gb.from = \u5f9e
gb.to = \u81f3
gb.at = at
gb.of = \u5c08\u6848\u70ba
gb.in = in
gb.moreChanges = \u6240\u6709\u8b8a\u66f4...
gb.moreHistory = \u66f4\u591a\u6b77\u53f2\u7d00\u9304...
gb.moreLogs = \u66f4\u591a\u63d0\u4ea4 ...
gb.mutable = \u52d5\u614b\u7d66\u4e88
gb.myDashboard = \u6211\u7684\u5100\u8868\u677f
gb.myFork = \u6aa2\u8996\u6211\u5efa\u7acb\u7684\u5206\u652f(fork)
gb.myProfile = \u6211\u7684\u57fa\u672c\u8cc7\u6599
gb.pushedNCommitsTo = {0}\u500b\u63d0\u4ea4\u5df2\u63a8\u9001\u81f3
gb.pushedOneCommitTo = 1\u500b\u63d0\u4ea4\u5df2\u63a8\u9001\u81f3
gb.commitsTo = {0} \u4efd\u63d0\u4ea4\u81f3\u5206\u652f
gb.oneCommitTo = 1\u500b\u63d0\u4ea4\u81f3\u5206\u652f
gb.byNAuthors = \u7d93\u7531{0}\u500b\u4f5c\u8005
gb.byOneAuthor = \u7d93\u7531{0}
gb.viewComparison = \u6bd4\u8f03\u9019{0}\u500b\u63d0\u4ea4 \u00bb
gb.nMoreCommits = \u9084\u6709{0}\u4efd\u63d0\u4ea4 \u00bb
gb.oneMoreCommit = \u9084\u6709\u4e00\u500b\u63d0\u4ea4 \u00bb
gb.pushedNewTag = \u65b0\u6a19\u7c64\u5df2\u63a8\u9001(pushed)
gb.createdNewTag = \u5efa\u7acb\u65b0\u6a19\u7c64
gb.deletedTag = \u522a\u9664\u6a19\u7c64
gb.pushedNewBranch = \u5df2\u63a8\u9001(pushed)\u4e4b\u5206\u652f -
gb.createdNewBranch = \u5efa\u7acb\u65b0\u5206\u652f
gb.deletedBranch = \u5df2\u522a\u9664\u7684\u5206\u652f
gb.createdNewPullRequest = \u5efa\u7acb pull request
gb.mergedPullRequest = \u5408\u4f75\u63a8\u9001\u8981\u6c42
gb.rewind = REWIND
gb.star = \u91cd\u8981
gb.unstar = \u53d6\u6d88
gb.stargazers = stargazers
gb.starredRepositories = \u91cd\u8981\u7684\u7248\u672c\u5eab
gb.failedToUpdateUser = \u7121\u6cd5\u66f4\u65b0\u4f7f\u7528\u8005\u5e33\u865f
gb.myRepositories = \u6211\u7684\u7248\u672c\u5eab
gb.myTickets = \u6211\u7684\u4efb\u52d9\u55ae
gb.myUrlDescription = \u4f60Gitblit\u4f3a\u670d\u5668\u7684\u516c\u958bURL
gb.name = \u540d\u5b57
gb.nameDescription = \u4f7f\u7528"/"\u505a\u70ba\u6587\u4ef6\u5eab\u7fa4\u7d44\u5206\u985e. \u5982: library/mycoolib.git
gb.namedPushPolicy = Restrict Push (Named)
gb.namedPushPolicyDescription = \u4efb\u4f55\u4eba\u7686\u53ef\u6aa2\u8996\u8207\u8907\u88fd(clone)\u6587\u4ef6\u5eab. \u4f60\u53ef\u53e6\u5916\u6307\u5b9a\u8ab0\u80fd\u5920\u6709\u63a8\u9001\u529f\u80fd(push)
gb.nAttachments = {0}\u500b\u9644\u4ef6
gb.nClosedTickets = {0}\u9805\u7d50\u675f
gb.nComments = {0}\u500b\u8a3b\u89e3
gb.nCommits = {0}\u4efd\u63d0\u4ea4
gb.needsImprovement = \u9700\u8981\u512a\u5316
gb.new = \u5efa\u7acb
gb.newCertificate = \u5efa\u7acb\u8b49\u66f8
gb.newCertificateDefaults = \u65b0\u8b49\u66f8\u9810\u8a2d\u503c
gb.newClientCertificateMessage = \u6ce8\u610f:\n'password'\u5bc6\u78bc\u4e26\u4e0d\u662f\u4f7f\u7528\u8005\u5bc6\u78bc, \u800c\u662f\u7528\u4f86\u4fdd\u8b77\u4f7f\u7528\u8005\u500b\u4eba\u7684keystore.\u8a72\u5bc6\u78bc\u4e26\u4e0d\u6703\u5132\u5b58, \u56e0\u6b64\u5fc5\u9808\u8a2d\u5b9a\u63d0\u793a(hint), \u8a72\u63d0\u793a\u5c07\u6703\u5beb\u5728\u4f7f\u7528\u8005\u7684README\u6587\u4ef6\u88e1\u9762.
gb.newMilestone = \u5efa\u7acb\u91cc\u7a0b\u7891
gb.newRepository = \u5efa\u7acb\u7248\u672c\u5eab
gb.newSSLCertificate = \u65b0\u7684\u4f3a\u670d\u5668SSL\u8b49\u66f8
gb.newTeam = \u5efa\u7acb\u5718\u968a
gb.newTicket = \u65b0\u589e\u4efb\u52d9\u55ae
gb.newUser = \u5efa\u7acb\u4f7f\u7528\u8005
gb.nextPull = next pull
gb.nFederationProposalsToReview = \u7e3d\u5171\u6709{0}\u500b\u4e32\u9023\u8a08\u756b\u7b49\u5f85\u5be9\u8996
gb.nMoreCommits = \u9084\u6709{0}\u4efd\u63d0\u4ea4 \u00bb
gb.noActivity = \u904e\u53bb{0}\u5929\u4f86,\u4e26\u6c92\u6709\u6d3b\u52d5\u7d00\u9304
gb.findSomeRepositories = \u641c\u5c0b\u7248\u672c\u5eab
gb.metricAuthorExclusions = \u7d71\u8a08\u6642\u6392\u9664\u6d3b\u8e8d\u5e33\u6236
gb.myDashboard = \u5100\u8868\u677f
gb.failedToFindAccount = \u7121\u6cd5\u641c\u5c0b\u5230\u5e33\u865f"{0}"
gb.reflog = \u76f8\u95dc\u65e5\u8a8c
gb.active = \u6d3b\u52d5
gb.starred = \u91cd\u8981
gb.owned = \u64c1\u6709
gb.starredAndOwned = \u91cd\u8981 & \u64c1\u6709
gb.reviewPatchset = {0}\u500breview {1}\u500bpatchset
gb.todaysActivityStats = \u4eca\u5929/\u6709{2}\u500b\u4f5c\u8005\u5b8c\u6210{1}\u500b\u63d0\u4ea4
gb.todaysActivityNone = \u4eca\u5929/\u7121
gb.noActivityToday = \u4eca\u5929\u6c92\u6709\u6d3b\u52d5\u7d00\u9304
gb.noComments = \u6c92\u6709\u5099\u8a3b
gb.anonymousUser = \u533f\u540d
gb.commitMessageRenderer = \u63d0\u4ea4\u8a0a\u606f\u5448\u73fe\u65b9\u5f0f
gb.diffStat = \u65b0\u589e{0}\u5217\u8207\u522a\u9664{1}\u5217
gb.home = \u9996\u9801
gb.isMirror = \u8a72\u7248\u672c\u5eab\u70ba\u93e1\u50cf(mirror)
gb.mirrorOf = {0}\u7684\u93e1\u50cf
gb.mirrorWarning = \u8a72\u7248\u672c\u5eab\u5c6c\u65bc\u93e1\u50cf, \u4e0d\u80fd\u5920\u63a5\u6536\u63a8\u9001(push)
gb.docsWelcome1 = \u4f60\u53ef\u4ee5\u4f7f\u7528\u6a94\u6848\u5340\u5efa\u7acb\u7248\u672c\u5eab\u7684\u6559\u5b78\u6a94\u6848
gb.docsWelcome2 = \u63d0\u4ea4README.md \u6216 HOME.md\u5f8c,\u518d\u958b\u59cb\u65b0\u7684\u7248\u672c\u5eab
gb.createReadme = \u5efa\u7acbREADME\u6a94\u6848
gb.responsible = \u8ca0\u8cac\u4eba\u54e1
gb.createdThisTicket = \u5df2\u958b\u7acb\u7684\u4efb\u52d9
gb.proposedThisChange = proposed this change
gb.uploadedPatchsetN = \u88dc\u4e01{0}\u5df2\u4e0a\u50b3
gb.uploadedPatchsetNRevisionN = \u88dc\u4e01{0}\u4fee\u6539\u7248\u672c{1}\u5df2\u4e0a\u50b3
gb.mergedPatchset = \u5c07\u88dc\u4e01\u5408\u4f75
gb.commented = \u5df2\u8a3b\u89e3
gb.noDescriptionGiven = \u6c92\u6709\u7d66\u4e88\u7c21\u8ff0
gb.noFederation = Sorry, {0} is not configured to federate with any Gitblit instances.
gb.noForks = {0}\u6c92\u6709\u5206\u652f(fork)
gb.noGitblitFound = Sorry, {0} could not find a Gitblit instance at {1}.
gb.noHits = \u7121\u9ede\u64ca
gb.noIndexedRepositoriesWarning = \u8ddf\u4f60\u76f8\u95dc\u7684\u6587\u4ef6\u5eab\u4e26\u6c92\u6709\u505aLucene\u7d22\u5f15
gb.noMaximum = \u7121\u6700\u5927\u503c
gb.noMilestoneSelected = \u672a\u9078\u53d6\u91cc\u7a0b\u7891
gb.none = \u7121
gb.nOpenTickets = {0}\u9805\u958b\u555f\u4e2d
gb.noPermission = \u522a\u9664\u9019\u500b\u6b0a\u9650
gb.noProposals = \u62b1\u6b49, {0}\u6b64\u6642\u4e26\u4e0d\u662f\u53ef\u63a5\u53d7\u7684\u8a08\u756b
gb.noSelectedRepositoriesWarning = \u8acb\u81f3\u5c11\u9078\u64c7\u4e00\u500b\u6587\u4ef6\u5eab
gb.notifyChangedOpenTickets = \u5df2\u958b\u555f\u7684\u4efb\u52d9\u55ae\u6709\u7570\u52d5\u8acb\u767c\u9001\u901a\u77e5
gb.notRestricted = \u533f\u540d\u72c0\u614b\u53ef\u4ee5View, Clone\u8207Push
gb.notSpecified = \u7121\u6307\u5b9a
gb.toBranch = \u5230\u5206\u652f {0}
gb.createdBy = \u5efa\u7acb\u8005
gb.oneParticipant = {0}\u53c3\u8207
gb.nParticipants = {0}\u500b\u53c3\u8207
gb.nTotalTickets = \u7e3d\u5171{0}\u9805
gb.object = \u7269\u4ef6
gb.of = \u7684
gb.ok = ok
gb.oneAttachment = {0}\u500b\u9644\u4ef6
gb.noComments = \u6c92\u6709\u5099\u8a3b
gb.oneComment = {0}\u500b\u8a3b\u89e3
gb.oneCommit = 1\u500b\u63d0\u4ea4
gb.oneCommitTo = 1\u500b\u63d0\u4ea4\u5230
gb.oneMoreCommit = \u9084\u6709\u4e00\u500b\u63d0\u4ea4 \u00bb
gb.oneParticipant = {0}\u53c3\u8207
gb.OneProposalToReview = \u6709\u4e00\u500b\u4e32\u9023\u7684\u63d0\u6848\u7b49\u5f85\u5be9\u67e5
gb.opacityAdjust = Adjust opacity
gb.nComments = {0}\u500b\u8a3b\u89e3
gb.oneAttachment = {0}\u500b\u9644\u4ef6
gb.nAttachments = {0}\u500b\u9644\u4ef6
gb.milestone = milestone
gb.compareToMergeBase = \u6bd4\u5c0d\u5f8c,\u5408\u4f75\u5230\u4e3b\u8981\u5de5\u4f5c\u5340
gb.compareToN = \u8207{0}\u9032\u884c\u6bd4\u5c0d
gb.open = \u958b\u555f
gb.openMilestones = \u6253\u958b\u91cc\u7a0b\u7891
gb.organization = \u7d44\u7e54
gb.organizationalUnit = \u7d44\u7e54\u55ae\u4f4d
gb.origin = origin
gb.originDescription = \u6b64\u6587\u4ef6\u5eabURL\u5df2\u7d93\u88ab\u8907\u88fd(cloned)\u4e86
gb.overdue = \u904e\u671f
gb.overview = \u6982\u89c0
gb.owned = \u64c1\u6709\u7684
gb.owner = \u64c1\u6709\u8005
gb.ownerDescription = \u64c1\u6709\u8005\u53ef\u4fee\u6539\u6587\u4ef6\u5eab\u8a2d\u5b9a\u503c
gb.ownerPermission = \u6587\u4ef6\u5eab\u6240\u6709\u8005
gb.owners = \u6240\u6709\u8005
gb.ownersDescription = \u6240\u6709\u8005\u53ef\u4ee5\u7ba1\u7406\u6587\u4ef6\u5eab,\u4f46\u662f\u4e0d\u5141\u8a31\u4fee\u6539\u540d\u7a31(\u79c1\u4eba\u6587\u4ef6\u5eab\u4f8b\u5916)
gb.pageFirst = \u7b2c\u4e00\u7b46
gb.pageNext = \u4e0b\u4e00\u9801
gb.pagePrevious = \u4e0a\u4e00\u9801
gb.pages = \u6587\u4ef6
gb.parent = \u4e0a\u500b\u7248\u672c
gb.password = \u5bc6\u78bc
gb.passwordChangeAborted = \u53d6\u6d88\u5bc6\u78bc\u8b8a\u66f4
gb.passwordChanged = \u5bc6\u78bc\u8b8a\u66f4\u6210\u529f
gb.passwordHint = \u5bc6\u78bc\u63d0\u793a
gb.passwordHintRequired = \u5bc6\u78bc\u63d0\u793a(\u5fc5\u8981)
gb.passwordsDoNotMatch = \u5bc6\u78bc\u4e0d\u76f8\u7b26
gb.passwordTooShort = \u5bc6\u78bc\u904e\u77ed, \u6700\u5c11{0}\u500b\u5b57\u5143
gb.patch = \u4fee\u88dc\u6a94
gb.patchset = \u88dc\u4e01
gb.patchsetAlreadyMerged = \u8a72\u88dc\u4e01\u5df2\u7d93\u5408\u4f75\u5230{0}
gb.closed = \u95dc\u9589
gb.merged = \u5df2\u5408\u4f75
gb.ticketPatchset = {0}\u4efb\u52d9\u55ae,{1}\u88dc\u4e01
gb.patchsetMergeable = \u8a72\u88dc\u4e01\u53ef\u4ee5\u81ea\u52d5\u8207{0}\u5408\u4f75
gb.patchsetMergeableMore = \u4f7f\u7528\u547d\u4ee4\u529f\u80fd,\u8b93\u6b64\u88dc\u4e01\u53ef\u4ee5\u8207{0}\u5408\u4f75
gb.patchsetN = \u88dc\u4e01{0}
gb.patchsetAlreadyMerged = \u8a72\u88dc\u4e01\u5df2\u7d93\u5408\u4f75\u5230{0}
gb.patchsetNotMergeable = \u8a72\u88dc\u4e01\u4e0d\u80fd\u81ea\u52d5\u8207{0}\u5408\u4f75
gb.patchsetNotMergeableMore = \u5fc5\u9808\u4ee5rebased\u6216\u662f\u624b\u52d5\u8207{0}\u5408\u4f75\u7684\u65b9\u5f0f\u624d\u80fd\u89e3\u6c7a\u8a72\u88dc\u4e01\u9020\u6210\u7684\u885d\u7a81
gb.patchsetNotApproved = \u8a72\u88dc\u4e01\u7248\u672c\u4e26\u6c92\u6709\u88ab\u6279\u51c6\u8207{0}\u5408\u4f75
gb.patchsetNotApprovedMore = \u8a72\u88dc\u4e01\u5fc5\u9808\u7531\u5be9\u67e5\u8005\u6279\u51c6
gb.patchsetNotMergeable = \u8a72\u88dc\u4e01\u4e0d\u80fd\u81ea\u52d5\u8207{0}\u5408\u4f75
gb.patchsetNotMergeableMore = \u5fc5\u9808\u4ee5rebased\u6216\u662f\u624b\u52d5\u8207{0}\u5408\u4f75\u7684\u65b9\u5f0f\u624d\u80fd\u89e3\u6c7a\u8a72\u88dc\u4e01\u9020\u6210\u7684\u885d\u7a81
gb.patchsetVetoedMore = \u5be9\u8996\u8005\u5df2\u7d93\u5c0d\u6b64\u88dc\u4e01\u6295\u7968
gb.permission = \u6b0a\u9650
gb.permissions = \u6b0a\u9650
gb.permittedTeams = permitted teams
gb.permittedUsers = permitted users
gb.personalRepositories = \u500b\u4eba\u6587\u4ef6\u5eab
gb.pleaseGenerateClientCertificate = \u8acb\u7522\u751f\u7d66{0}\u4f7f\u7528\u7684\u7528\u6236\u7aef\u8b49\u66f8
gb.pleaseSelectGitIgnore = \u8acb\u9078\u64c7\u4e00\u500b.gitignore\u6a94\u6848
gb.pleaseSelectProject = \u8acb\u9078\u64c7\u5c08\u6848!
gb.pleaseSetDestinationUrl = Please enter a destination url for your proposal\!
gb.pleaseSetGitblitUrl = \u8acb\u8f38\u5165Gitblit URL !
gb.pleaseSetRepositoryName = \u8acb\u8a2d\u5b9a\u7248\u672c\u5eab\u540d\u7a31
gb.pleaseSetTeamName = \u8acb\u8f38\u5165\u5718\u968a\u540d\u7a31
gb.pleaseSetUsername = \u8acb\u8f38\u5165\u4f7f\u7528\u8005\u540d\u7a31
gb.plugins = \u63d2\u4ef6
gb.postReceiveDescription = \u63a5\u5230\u63d0\u4ea4\u7533\u8acb\u5f8c,<em>\u4e26\u4e14\u5728refs\u5b8c\u7562\u5f8c</em>, \u5c07\u6703\u57f7\u884cPost-receive hook..<p>This is the appropriate hook for notifications, build triggers, etc.</p>
gb.postReceiveScripts = post-receive\u8173\u672c
gb.preferences = \u9810\u8a2d\u5e38\u7528\u503c
gb.preparingFork = \u6b63\u5728\u6e96\u5099\u8907\u88fd\u4e2d(fork)...
gb.preReceiveDescription = \u63a5\u5230\u63d0\u4ea4\u7533\u8acb\u5f8c,<em>\u4f46\u5728\u9084\u6c92\u6709\u66f4\u65b0refs\u524d</em>, \u5c07\u6703\u57f7\u884cPre-receive hook. <p>This is the appropriate hook for rejecting a push.</p>
gb.preReceiveScripts = pre-receive \u8173\u672c
gb.patchsetVetoedMore = \u5be9\u67e5\u8005\u5df2\u7d93\u5c0d\u6b64\u88dc\u4e01\u6295\u7968
gb.write = \u8f38\u5165
gb.comment = \u8a3b\u89e3
gb.preview = \u9810\u89bd
gb.priority = \u512a\u5148
gb.privilegeWithdrawn = \u53d6\u6d88\u6b0a\u9650
gb.project = \u7fa4\u7d44
gb.projects = \u7fa4\u7d44
gb.properties = \u5c6c\u6027
gb.proposal = \u63d0\u6848
gb.proposalError = \u62b1\u6b49, {0} \u4efd\u5831\u544a\u767c\u751f\u9810\u671f\u5916\u7684\u932f\u8aa4!
gb.proposalFailed = Sorry, {0} did not receive any proposal data\!
gb.proposalReceived = Proposal successfully received by {0}.
gb.proposals = \u8981\u6c42\u806f\u5408\u7684\u63d0\u6848
gb.leaveComment = \u7559\u4e0b\u8a3b\u89e3
gb.showHideDetails = \u986f\u793a/\u96b1\u85cf \u8a73\u89e3\u5167\u5bb9
gb.acceptNewPatchsets = \u5141\u8a31\u88dc\u4e01
gb.acceptNewPatchsetsDescription = \u63a5\u53d7\u5230\u7248\u672c\u5eab\u9032\u884c\u4fee\u88dc\u52d5\u4f5c
gb.acceptNewTickets = \u5141\u8a31\u5efa\u7acb\u4efb\u52d9
gb.acceptNewTicketsDescription = \u5141\u8a31\u65b0\u589e"\u81ed\u87f2","\u512a\u5316","\u4efb\u52d9"\u5404\u985e\u578b\u4efb\u52d9
gb.requireApproval = \u9700\u6279\u51c6
gb.requireApprovalDescription = \u5408\u4f75\u6309\u9215\u555f\u7528\u524d,\u88dc\u4e01\u5305\u5fc5\u9808\u5148\u6279\u51c6
gb.topic = \u8a71\u984c
gb.proposalTickets = \u63d0\u6848\u4fee\u6539
gb.proposedThisChange = proposed this change
gb.proposeInstructions = To start, craft a patchset and upload it with Git. Gitblit will link your patchset to this ticket by the id.
gb.bugTickets = \u81ed\u87f2
gb.enhancementTickets = \u512a\u5316
gb.taskTickets = \u4efb\u52d9
gb.questionTickets = \u63d0\u554f
gb.requestTickets = \u512a\u5316 & \u4efb\u52d9
gb.yourCreatedTickets = \u4f60\u65b0\u589e\u7684
gb.yourWatchedTickets = \u4f60\u76e3\u770b\u7684
gb.mentionsMeTickets = \u8207\u4f60\u76f8\u95dc
gb.updatedBy = \u66f4\u65b0\u8005
gb.sort = \u6392\u5e8f
gb.sortNewest = \u6700\u65b0
gb.sortOldest = \u6700\u820a
gb.sortMostRecentlyUpdated = \u6700\u8fd1\u66f4\u65b0
gb.sortLeastRecentlyUpdated = \u6700\u8fd1\u6700\u5c11\u8b8a\u52d5
gb.sortMostComments = \u6700\u591a\u5099\u8a3b
gb.sortLeastComments = \u6700\u5c11\u5099\u8a3b
gb.sortMostPatchsetRevisions = \u6700\u591a\u88dc\u4e01\u4fee\u6b63
gb.sortLeastPatchsetRevisions = \u6700\u5c11\u88dc\u4e01\u4fee\u6539
gb.sortMostVotes = \u6700\u591a\u6295\u7968
gb.sortLeastVotes = \u6700\u5c11\u6295\u7968
gb.topicsAndLabels = \u8a71\u984c\u8207\u6a19\u8a18
gb.milestones = milestones
gb.noMilestoneSelected = \u672a\u9078\u53d6milestone
gb.notSpecified = \u7121\u6307\u5b9a
gb.due = \u622a\u6b62
gb.queries = \u67e5\u8a62\u7d50\u679c
gb.searchTicketsTooltip = \u627e\u5230{0}\u4efd\u4efb\u52d9
gb.searchTickets = \u641c\u5c0b\u4efb\u52d9
gb.new = \u5efa\u7acb
gb.newTicket = \u65b0\u589e\u4efb\u52d9
gb.editTicket = \u4fee\u6539\u4efb\u52d9
gb.ticketsWelcome = \u4f60\u53ef\u4ee5\u5229\u7528\u4efb\u52d9\u7cfb\u7d71\u5efa\u69cb\u51fa\u5f85\u8fa6\u4e8b\u9805, \u81ed\u87f2\u56de\u5831\u5340\u4ee5\u53ca\u88dc\u4e01\u5305\u7684\u5354\u540c\u5408\u4f5c
gb.createFirstTicket = \u6309\u6b64\u5efa\u7acb\u7b2c\u4e00\u500b\u4efb\u52d9
gb.title = \u6a19\u984c
gb.changedStatus = changed the status
gb.discussion = \u8a0e\u8ad6
gb.updated = \u5df2\u66f4\u65b0
gb.proposePatchset = \u63d0\u51fa\u88dc\u4e01
gb.proposePatchsetNote = \u6b61\u8fce\u5c0d\u6b64\u4efb\u52d9\u55ae\u63d0\u4f9b\u88dc\u4e01
gb.proposeWith = propose a patchset with {0}
gb.proposePatchsetNote = \u6b61\u8fce\u5c0d\u6b64\u4efb\u52d9\u63d0\u4f9b\u88dc\u4e01
gb.proposeInstructions = \u9996\u5148, \u5efa\u7acb\u88dc\u4e01\u4e26\u4e14\u540c\u6b65\u5230\u6b64gitblit\u4f3a\u670d\u5668. Gitblit \u8b93\u88dc\u4e01\u8207\u672c\u6b21\u4efb\u52d9ID(ticket ID)\u9023\u7d50.
gb.proposeWith = \u5982\u4f55\u5728{0} \u4e0a\u5efa\u7acb\u88dc\u4e01
gb.revisionHistory = \u4fee\u6539\u7d00\u9304
gb.merge = \u5408\u4f75
gb.action = \u52d5\u4f5c
gb.patchset = \u88dc\u4e01
gb.all = \u5168\u90e8
gb.mergeBase = \u57fa\u672c\u5408\u4f75
gb.checkout = \u6aa2\u51fa(checkout)
gb.checkoutViaCommandLine = \u4f7f\u7528\u6307\u4ee4Checkout
gb.checkoutViaCommandLineNote = \u4f60\u53ef\u4ee5\u5f9e\u4f60\u7248\u672c\u5eab\u4e2dcheckout\u4e00\u4efd,\u7136\u5f8c\u9032\u884c\u6e2c\u8a66
gb.checkoutStep1 = Fetch the current patchset \u2014 run this from your project directory
gb.checkoutStep2 = \u5c07\u8a72\u88dc\u4e01\u8f49\u51fa\u5230\u65b0\u7684\u5206\u652f\u5f8c\u7528\u4f86\u6aa2\u8996
gb.mergingViaCommandLine = \u7d93\u7531\u6307\u4ee4\u57f7\u884c\u5408\u4f75
gb.mergingViaCommandLineNote = \u5982\u679c\u4f60\u4e0d\u60f3\u8981\u4f7f\u7528\u81ea\u52d5\u5408\u4f75\u529f\u80fd,\u6216\u662f\u6309\u4e0b\u5408\u4f75\u6309\u9215, \u4f60\u53ef\u4ee5\u4e0b\u6307\u4ee4\u624b\u52d5\u5408\u4f75
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 = \u5c07\u63d0\u6848\u4fee\u6539\u5167\u5bb9\u66f4\u65b0\u5230\u4f3a\u670d\u5668\u4e0a
gb.download = \u4e0b\u8f09
gb.ptDescription = Gitblit \u88dc\u4e01\u5de5\u5177
gb.ptCheckout = Fetch & checkout the current patchset to a review branch
gb.ptDescription = the Gitblit patchset tool
gb.ptMerge = \u53d6\u5f97\u76ee\u524dpatchset,\u7136\u5f8c\u8207\u4f60\u672c\u6a5f\u7aef\u7684\u5206\u652f\u5408\u4f75
gb.ptDescription1 = Barnum is a command-line companion for Git that simplifies the syntax for working with Gitblit Tickets and Patchsets.
gb.ptDescription2 = Barnum requires Python 3 and native Git. It runs on Windows, Linux, and Mac OS X.
gb.ptMerge = \u53d6\u5f97\u76ee\u524d\u88dc\u4e01,\u7136\u5f8c\u8207\u4f60\u672c\u6a5f\u7aef\u7684\u5206\u652f\u5408\u4f75
gb.ptSimplifiedCollaboration = simplified collaboration syntax
gb.ptSimplifiedMerge = simplified merge syntax
gb.publicKey = \u516c\u958b\u91d1\u9470
gb.pushedNCommitsTo = {0}\u500b\u63d0\u4ea4\u5df2\u63a8\u9001\u81f3
gb.pushedNewBranch = \u65b0\u5206\u652f\u5df2\u63a8\u9001(pushed)
gb.pushedNewTag = \u65b0\u6a19\u7c64\u5df2\u63a8\u9001(pushed)
gb.pushedOneCommitTo = 1\u500b\u63d0\u4ea4\u5df2\u63a8\u9001\u81f3
gb.pushPermission = {0}(\u63a8\u9001)
gb.pushRestricted = authenticated push
gb.queries = \u67e5\u8a62\u7d50\u679c
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.queryResults = results {0} - {1} ({2} hits)
gb.questionTickets = \u63d0\u554f
gb.raw = \u539f\u59cb
gb.reason = \u539f\u56e0
gb.receive = \u63a5\u6536
gb.received = \u5df2\u63a5\u6536
gb.receiveSettings = \u8a2d\u5b9a\u63a5\u6536\u65b9\u5f0f
gb.receiveSettingsDescription = \u63a7\u7ba1\u63a8\u9001\u5230\u6587\u4ef6\u5eab\u7684\u63a5\u6536\u65b9\u5f0f
gb.recent = \u6700\u8fd1
gb.recentActivity = \u6700\u8fd1\u6d3b\u8e8d\u72c0\u6cc1
gb.recentActivityNone = \u904e\u53bb{0}\u5929/\u7121
gb.recentActivityStats = \u904e\u53bb{0}\u5929,\u4e00\u5171\u6709{2}\u4eba\u57f7\u884c{1}\u4efd\u63d0\u4ea4
gb.reflog = \u76f8\u95dc\u65e5\u8a8c
gb.refresh = \u5237\u65b0
gb.refs = \u5f15\u7528
gb.regexPermission = \u5df2\u7d93\u4f7f\u7528\u6b63\u898f\u8868\u793a\u5f0f(regular expression)"{0}" \u8a2d\u5b9a\u6b0a\u9650\u5b8c\u7562
gb.registration = \u8a3b\u518a
gb.registrations = federation registrations
gb.releaseDate = \u767c\u8868\u65e5
gb.remote = \u9060\u7aef
gb.ptDescription2 = Barnum requires Python 3 and native Git. It runs on Windows, Linux, and Mac OS X.
gb.stepN = \u6b65\u9a5f{0}
gb.watchers = \u76e3\u7763\u8005
gb.votes = \u6295\u7968
gb.vote = \u5c0d{0}\u6295\u7968
gb.watch = \u76e3\u770b{0}
gb.removeVote = \u79fb\u9664\u6295\u7968
gb.rename = \u6539\u540d\u7a31
gb.repositories = \u6587\u4ef6\u5eab
gb.repository = \u7248\u672c\u5eab
gb.repositoryDeleted = \u7248\u672c\u5eab"{0}"\u5df2\u522a\u9664
gb.repositoryDeleteFailed = \u522a\u9664\u7248\u672c\u5eab"{0}"\u5931\u6557!
gb.repositoryDoesNotAcceptPatchsets = \u8a72\u7248\u672c\u5eab\u4e0d\u63a5\u53d7\u88dc\u4e01
gb.repositoryForked = \u7248\u672c\u5eab{0}\u5df2\u7d93\u5efa\u7acb\u5206\u652f(fork)
gb.repositoryForkFailed= \u5efa\u7acb\u5206\u652f(fork)\u5931\u6557
gb.repositoryIsFrozen = \u8a72\u7248\u672c\u5eab\u5df2\u51cd\u7d50
gb.repositoryIsMirror = \u8a72\u7248\u672c\u5eab\u70ba\u552f\u8b80\u8907\u672c
gb.repositoryNotSpecified = \u672a\u6307\u5b9a\u7248\u672c\u5eab!
gb.repositoryNotSpecifiedFor = \u7248\u672c\u5eab\u4e26\u6c92\u6709\u6307\u5b9a\u7d66 {0}\!
gb.repositoryPermissions = \u7248\u672c\u5eab\u6b0a\u9650
gb.repositoryUrl = \u7248\u672c\u5eab url
gb.requestTickets = \u512a\u5316 & \u4efb\u52d9
gb.requireApproval = \u9700\u6279\u51c6
gb.requireApprovalDescription = \u5408\u4f75\u6309\u9215\u555f\u7528\u524d,\u88dc\u4e01\u5305\u5fc5\u9808\u5148\u6279\u51c6
gb.reset = \u6e05\u9664
gb.responsible = \u8ca0\u8cac\u4eba\u54e1
gb.restrictedRepositories = restricted repositories
gb.review = \u8907\u67e5(review)
gb.stopWatching = \u505c\u6b62\u8ffd\u8e64(watching)
gb.watching = \u76e3\u770b\u4e2d
gb.comments = \u8a3b\u89e3
gb.addComment = \u65b0\u589e\u8a3b\u89e3
gb.export = \u532f\u51fa
gb.oneCommit = 1\u500b\u63d0\u4ea4
gb.nCommits = {0}\u4efd\u63d0\u4ea4
gb.addedOneCommit = \u63d0\u4ea41\u500b\u6a94\u6848
gb.addedNCommits = {0}\u500b\u6a94\u6848\u63d0\u4ea4\u5b8c\u7562
gb.commitsInPatchsetN = \u88dc\u4e01 {0} \u7684\u63d0\u4ea4
gb.patchsetN = \u88dc\u4e01{0}
gb.reviewedPatchsetRev = reviewed patchset {0} revision {1}\: {2}
gb.review = \u6aa2\u67e5(review)
gb.reviews = \u6aa2\u67e5(reviews)
gb.veto = \u5426\u6c7a
gb.needsImprovement = \u9700\u8981\u512a\u5316
gb.looksGood = \u770b\u8d77\u4f86\u5f88\u597d
gb.approve = \u901a\u904e
gb.hasNotReviewed = \u5c1a\u672a\u6aa2\u6838\u904e
gb.about = \u95dc\u65bc
gb.ticketN = \u4efb\u52d9\u7de8\u865f#{0}
gb.disableUser = \u505c\u7528\u5e33\u6236
gb.disableUserDescription = \u8a72\u5e33\u6236\u7121\u6cd5\u4f7f\u7528
gb.any = \u4efb\u4f55
gb.milestoneProgress = {0}\u958b\u555f,{1}\u7d50\u675f
gb.nOpenTickets = {0}\u9805\u958b\u555f\u4e2d
gb.nClosedTickets = {0}\u9805\u7d50\u675f
gb.nTotalTickets = \u7e3d\u5171{0}\u9805
gb.body = \u5167\u5bb9
gb.mergeSha = mergeSha
gb.mergeTo = \u5408\u4f75\u5230
gb.labels = \u6a19\u8a18
gb.reviewers = \u5be9\u67e5\u8005
gb.reviewPatchset = review {0} patchset {1}
gb.reviews = reviews
gb.revisionHistory = \u4fee\u6539\u7d00\u9304
gb.revokeCertificate = \u64a4\u56de\u8b49\u66f8
gb.revokeCertificateReason = \u8acb\u8f38\u5165\u64a4\u56de\u8b49\u66f8\u7406\u7531
gb.revoked = \u5df2\u64a4\u92b7
gb.rewind = REWIND
gb.rewindPermission = {0} (push, ref creation+deletion+rewind)
gb.save = \u5132\u5b58
gb.search = \u641c\u5c0b
gb.searchForAuthor = Search for commits authored by
gb.searchForCommitter = Search for commits committed by
gb.searchTickets = \u641c\u5c0b\u4efb\u52d9\u55ae
gb.searchTicketsTooltip = \u627e\u5230{0}\u4efd\u4efb\u52d9\u55ae
gb.searchTooltip = \u641c\u5c0b{0}
gb.searchTypeTooltip = \u9078\u64c7\u641c\u5c0b\u985e\u578b
gb.selectAccessRestriction = Please select access restriction\!
gb.selected = \u9078\u5b9a
gb.selectFederationStrategy = Please select federation strategy\!
gb.sendEmail = \u767cemail
gb.sendProposal = \u63d0\u6848
gb.serialNumber = \u5e8f\u865f
gb.serveCertificate = \u555f\u7528\u4f7f\u7528\u6b64\u8b49\u66f8\u7684https\u529f\u80fd
gb.voters = votes
gb.mentions = \u63d0\u5230
gb.canNotProposePatchset = \u4e0d\u80fd\u63d0\u4f9b\u88dc\u4e01
gb.repositoryIsMirror = \u8a72\u7248\u672c\u5eab\u70ba\u552f\u8b80\u8907\u672c
gb.repositoryIsFrozen = \u8a72\u7248\u672c\u5eab\u5df2\u51cd\u7d50
gb.repositoryDoesNotAcceptPatchsets = \u8a72\u7248\u672c\u5eab\u4e0d\u63a5\u53d7\u88dc\u4e01
gb.serverDoesNotAcceptPatchsets = \u672c\u4f3a\u670d\u5668\u4e0d\u63a5\u53d7\u88dc\u4e01
gb.servers = \u4f3a\u670d\u5668
gb.servletContainer = servlet\u5bb9\u5668
gb.sessionEnded = session\u5df2\u7d93\u53d6\u6d88
gb.setDefault = \u8a2d\u70ba\u9810\u8a2d\u503c
gb.settings = \u8a2d\u5b9a
gb.severity = \u91cd\u8981
gb.sha1FingerPrint = SHA-1 Fingerprint
gb.ticketIsClosed = \u8a72\u4efb\u52d9\u5df2\u7d93\u7d50\u6848
gb.mergeToDescription = \u9810\u8a2d\u5c07\u6587\u4ef6\u76f8\u95dc\u88dc\u4e01\u5305\u8207\u6307\u5b9a\u5206\u652f(branch)\u5408\u4f75
gb.anonymousCanNotPropose = \u533f\u540d\u8005\u4e0d\u80fd\u63d0\u4f9b\u88dc\u4e01
gb.youDoNotHaveClonePermission = \u4f60\u4e0d\u5141\u8a31\u8907\u88fd(clone)\u6b64\u7248\u672c\u5eab
gb.myTickets = \u6211\u7684\u4efb\u52d9
gb.yourAssignedTickets = \u6307\u6d3e\u7d66\u4f60\u7684
gb.newMilestone = \u5efa\u7acbmilestone
gb.editMilestone = \u4fee\u6539milestone
gb.deleteMilestone = \u522a\u9664milestone"{0}"?
gb.milestoneDeleteFailed = \u522a\u9664milestone"{0}"\u5931\u6557
gb.notifyChangedOpenTickets = \u5df2\u958b\u555f\u7684\u4efb\u52d9\u6709\u7570\u52d5\u8acb\u767c\u9001\u901a\u77e5
gb.overdue = \u904e\u671f
gb.openMilestones = \u5df2\u958b\u555f\u7684 milestones
gb.closedMilestones = \u5df2\u95dc\u9589\u7684 milestones
gb.administration = \u7ba1\u7406\u6b0a\u9650
gb.plugins = \u5957\u4ef6
gb.extensions = \u64f4\u5145
gb.pleaseSelectProject = \u8acb\u9078\u64c7\u5c08\u6848!
gb.accessPolicy = \u5b58\u53d6\u653f\u7b56
gb.accessPolicyDescription = \u9078\u64c7\u7528\u4f86\u63a7\u5236\u7248\u672c\u5eab\u7684\u5b58\u53d6\u653f\u7b56\u4ee5\u53ca\u6b0a\u9650\u8a2d\u5b9a
gb.anonymousPolicy = \u533f\u540d\u72c0\u614b\u53ef\u4ee5\u6aa2\u8996(view),\u8907\u88fd(clone)\u8207\u63a8\u9001(push)
gb.anonymousPolicyDescription = \u6240\u6709\u4eba\u7686\u53ef\u6aa2\u8996(view),\u8907\u88fd(clone)\u8207\u63a8\u9001(push)\u6587\u4ef6\u5230\u7248\u672c\u5eab
gb.authenticatedPushPolicy = \u9650\u5236\u63a8\u9001(Push)(\u6388\u6b0a)
gb.authenticatedPushPolicyDescription = \u6240\u6709\u4eba\u7686\u53ef\u6aa2\u8996\u8207\u8907\u88fd(clone). \u4f46\u53ea\u6709\u6210\u54e1\u6709RW+\u8207\u63a8\u9001(push)\u529f\u80fd.
gb.namedPushPolicy = \u9650\u5236\u63a8\u9001(Push)(\u6307\u5b9a\u5e33\u865f)
gb.namedPushPolicyDescription = \u6240\u6709\u4eba\u7686\u53ef\u6aa2\u8996\u8207\u8907\u88fd(clone)\u7248\u672c\u5eab. \u53ef\u53e6\u5916\u6307\u5b9a\u8ab0\u80fd\u5920\u6709\u63a8\u9001\u529f\u80fd(push)
gb.clonePolicy = \u9650\u5236\u8907\u88fd(Clone)\u8207\u63a8\u9001(Push)
gb.clonePolicyDescription = \u6240\u6709\u4eba\u7686\u53ef\u770b\u7248\u672c\u5eab. \u53ef\u53e6\u5916\u6307\u5b9a\u8ab0\u6709\u8907\u88fd(clone)\u8207\u63a8\u9001(push)\u6b0a\u9650
gb.viewPolicy = \u9650\u5236\u6aa2\u8996(view),\u8907\u88fd(clone)\u8207\u63a8\u9001(push)
gb.viewPolicyDescription = \u9078\u64c7\u53ef\u4ee5\u5728\u7248\u672c\u5eab\u6aa2\u8996,\u8907\u88fd(clone)\u8207\u63a8\u9001(push)\u7684\u4f7f\u7528\u8005, \u9664\u6b64\u4e4b\u5916\u5176\u4ed6\u4eba\u7686\u7121\u6b0a\u9650
gb.initialCommit = \u521d\u6b21\u63d0\u4ea4
gb.initialCommitDescription = \u4ee5\u4e0b\u6b65\u9a5f\u5c07\u99ac\u4e0a\u57f7\u884c<code>git clone</code>.\u5982\u679c\u4f60\u672c\u6a5f\u5df2\u6709\u6b64\u7248\u672c\u5eab\u4e14\u57f7\u884c\u904e<code>git init</code>,\u8acb\u8df3\u904e\u6b64\u6b65\u9a5f.
gb.initWithReadme = \u5305\u542bREADME\u6587\u4ef6
gb.initWithReadmeDescription = \u7248\u672c\u5eab\u5c07\u7522\u751f\u7c21\u55aeREADME\u6587\u4ef6
gb.initWithGitignore = \u5305\u542b .gitignore \u6a94\u6848
gb.initWithGitignoreDescription = \u65b0\u589e\u4e00\u500b\u8a2d\u5b9a\u6a94\u7528\u4f86\u6307\u5b9a\u54ea\u4e9b\u6a94\u6848\u6216\u76ee\u9304\u9700\u8981\u5ffd\u7565
gb.pleaseSelectGitIgnore = \u8acb\u9078\u64c7\u4e00\u500b.gitignore\u6a94\u6848
gb.receive = \u63a5\u6536
gb.permissions = \u6b0a\u9650
gb.ownersDescription = \u6240\u6709\u8005\u53ef\u4ee5\u7ba1\u7406\u7248\u672c\u5eab,\u4f46\u662f\u4e0d\u5141\u8a31\u4fee\u6539\u540d\u7a31(\u500b\u4eba\u7248\u672c\u5eab\u4f8b\u5916)
gb.userPermissionsDescription = \u4f60\u53ef\u4ee5\u91dd\u5c0d\u5e33\u865f\u8a2d\u5b9a\u6b0a\u9650(\u9019\u4e9b\u8a2d\u5b9a\u5c07\u8986\u84cb\u5718\u968a\u6216\u5176\u4ed6\u6b0a\u9650)
gb.teamPermissionsDescription = \u4f60\u53ef\u4ee5\u6307\u5b9a\u5718\u968a\u6b0a\u9650.\u9019\u4e9b\u8a2d\u5b9a\u5c07\u6703\u53d6\u4ee3\u539f\u672c\u5718\u968a\u9810\u8a2d\u6b0a\u9650
gb.ticketSettings = \u4efb\u52d9\u5167\u5bb9\u8a2d\u5b9a
gb.receiveSettings = \u8a2d\u5b9a\u63a5\u6536\u65b9\u5f0f
gb.receiveSettingsDescription = \u63a7\u7ba1\u63a8\u9001\u5230\u7248\u672c\u5eab\u7684\u63a5\u6536\u65b9\u5f0f
gb.preReceiveDescription = \u63a5\u5230\u63d0\u4ea4\u7533\u8acb\u5f8c,<em>\u4f46\u5728\u9084\u6c92\u6709\u66f4\u65b0refs\u524d</em>, \u5c07\u6703\u57f7\u884cPre-receive hook. <p>This is the appropriate hook for rejecting a push.</p>
gb.postReceiveDescription = \u63a5\u5230\u63d0\u4ea4\u7533\u8acb\u5f8c,<em>\u4e26\u4e14\u5728refs\u5b8c\u7562\u5f8c</em>, \u5c07\u6703\u57f7\u884cPost-receive hook..<p>This is the appropriate hook for notifications, build triggers, etc.</p>
gb.federationStrategyDescription = \u63a7\u5236\u5982\u4f55\u5c07\u7248\u672c\u5eab\u8207\u5176\u4ed6Gitblit\u7248\u63a7\u4f3a\u670d\u5668\u4e32\u9023
gb.federationSetsDescription = \u6b64\u7248\u672c\u5eab\u5c07\u5305\u542b\u65bc\u6307\u5b9a\u7684federation sets
gb.miscellaneous = \u5176\u4ed6
gb.originDescription = \u6b64\u7248\u672c\u5eabURL\u5df2\u7d93\u88ab\u8907\u88fd(cloned)\u4e86
gb.gc = \u7cfb\u7d71\u8cc7\u6e90\u56de\u6536\u5668
gb.garbageCollection = \u56de\u6536\u7cfb\u7d71\u8cc7\u6e90
gb.garbageCollectionDescription = \u7cfb\u7d71\u8cc7\u6e90\u56de\u6536\u529f\u80fd\u5c07\u6703\u6574\u9813\u9b06\u6563\u7528\u6236\u7aef\u63a8\u9001(push)\u7684\u7269\u4ef6, \u4e5f\u6703\u79fb\u9664\u7248\u672c\u5eab\u4e0a\u7121\u7528\u7684\u7269\u4ef6
gb.commitMessageRendererDescription = \u63d0\u4ea4\u8a0a\u606f\u53ef\u4ee5\u4f7f\u7528\u6587\u5b57\u6216\u662f\u6a19\u8a18\u8a9e\u8a00(markup)\u5448\u73fe
gb.preferences = \u9810\u8a2d\u5e38\u7528\u503c
gb.accountPreferences = \u5e33\u865f\u8a2d\u5b9a
gb.accountPreferencesDescription = \u8a2d\u5b9a\u5e33\u865f\u9810\u8a2d\u503c
gb.languagePreference = \u5e38\u7528\u8a9e\u8a00
gb.languagePreferenceDescription = \u9078\u64c7\u8a9e\u7cfb
gb.emailMeOnMyTicketChanges = \u4efb\u52d9\u82e5\u6709\u8b8a\u66f4,\u8acb\u7acb\u5373(email)\u901a\u77e5\u6211
gb.emailMeOnMyTicketChangesDescription =\u6211\u8655\u7406\u904e\u7684\u4efb\u52d9\u8acbemail\u901a\u77e5\u6211
gb.displayNameDescription = \u5e0c\u671b\u986f\u793a\u7684\u540d\u7a31
gb.emailAddressDescription = \u7528\u4f86\u63a5\u6536\u901a\u77e5\u7684\u4e3b\u8981\u96fb\u5b50\u90f5\u4ef6
gb.sshKeys = SSH Keys
gb.sshKeysDescription = SSH \u516c\u958b\u91d1\u9470\u662f\u5bc6\u78bc\u8a8d\u8b49\u5916\u66f4\u5b89\u5168\u7684\u9078\u9805
gb.addSshKey = \u65b0\u589e SSH Key
gb.key = \u91d1\u9470
gb.sshKeyCommentDescription = \u8acb\u8f38\u5165\u5099\u8a3b, \u82e5\u7121\u5099\u8a3b, \u5c07\u81ea\u8a02\u586b\u5165key data
gb.sshKeyPermissionDescription = \u6307\u5b9a\u8a72SSH key\u6240\u64c1\u6709\u7684\u5b58\u53d6\u6b0a\u9650
gb.transportPreference = \u9810\u8a2d\u901a\u8a0a\u5354\u5b9a
gb.transportPreferenceDescription = \u8a2d\u5b9a\u4f60\u5e38\u7528\u7684\u9023\u7dda\u7528\u4f86\u8907\u88fd(clone)
gb.blinkComparator = Blink comparator
gb.deleteRepositoryDescription = \u7248\u672c\u5eab\u522a\u9664\u5c07\u7121\u6cd5\u9084\u539f
gb.deleteRepositoryHeader = \u522a\u9664\u7248\u672c\u5eab
gb.diffCopiedFile = \u6a94\u6848\u7531 {0} \u8907\u88fd
gb.diffDeletedFile = \u6a94\u6848\u5df2\u522a\u9664
gb.diffDeletedFileSkipped = (\u522a\u9664)
gb.diffFileDiffTooLarge = \u6a94\u6848\u592a\u5927
gb.diffNewFile = \u6bd4\u5c0d\u65b0\u6a94\u6848
gb.diffRenamedFile = \u6a94\u540d\u7531 {0} \u4fee\u6539
gb.diffTruncated = Diff truncated after the above file
gb.ignore_whitespace =\u5ffd\u7565\u7a7a\u767d
gb.imgdiffSubtract = Subtract (black = identical)
gb.maintenanceTickets = \u7dad\u8b77
gb.missingIntegrationBranchMore = \u76ee\u6a19\u5206\u652f\u4e0d\u5728\u6b64\u7248\u672c\u5eab
gb.opacityAdjust = Adjust opacity
gb.priority = \u512a\u5148
gb.severity = \u91cd\u8981
gb.show_whitespace = \u986f\u793a\u7a7a\u767d
gb.showHideDetails = \u986f\u793a/\u96b1\u85cf \u8a73\u89e3\u5167\u5bb9
gb.showReadme = \u986f\u793areadme\u6587\u4ef6
gb.showReadmeDescription = \u5728\u532f\u7e3d\u9801\u9762\u4e2d\u986f\u793a"readme"(markdown\u683c\u5f0f)
gb.showRemoteBranches = \u986f\u793a\u9060\u7aef\u5206\u652f
gb.showRemoteBranchesDescription = \u986f\u793a\u9060\u7aef\u5206\u652f(branches)
gb.signatureAlgorithm = \u7c3d\u7ae0\u6f14\u7b97\u6cd5
gb.since = \u5f9e
gb.siteName = \u7ad9\u53f0\u540d\u7a31
gb.siteNameDescription = \u4f3a\u670d\u5668\u7c21\u7a31
gb.size = \u5bb9\u91cf
gb.skipSizeCalculation = \u7565\u904e\u5bb9\u91cf\u8a08\u7b97
gb.skipSizeCalculationDescription = \u4e0d\u8a08\u7b97\u6587\u4ef6\u5eab\u5bb9\u91cf(\u52a0\u5feb\u7db2\u9801\u8f09\u5165\u901f\u5ea6)
gb.skipSummaryMetrics = \u7565\u904e\u91cf\u5316\u532f\u7e3d
gb.skipSummaryMetricsDescription = \u4e0d\u8981\u8a08\u7b97\u91cf\u5316\u4e26\u4e14\u986f\u793a\u5728\u532f\u7e3d\u9801\u9762\u4e0a(\u52a0\u5feb\u901f\u5ea6)
gb.sort = \u6392\u5e8f
gb.sortHighestPriority = \u6700\u9ad8\u512a\u5148
gb.sortHighestPriority = \u6700\u9ad8
gb.sortHighestSeverity = \u6700\u91cd\u8981
gb.sortLeastComments = \u6700\u5c11\u5099\u8a3b
gb.sortLeastPatchsetRevisions = \u6700\u5c11\u88dc\u4e01\u4fee\u6539
gb.sortLeastRecentlyUpdated = \u6700\u8fd1\u6700\u5c11\u8b8a\u52d5
gb.sortLeastVotes = \u6700\u5c11\u6295\u7968
gb.sortLowestPriority = \u6700\u4f4e\u512a\u5148
gb.sortLowestPriority = \u6700\u4f4e
gb.sortLowestSeverity = \u6700\u4e0d\u91cd\u8981
gb.sortMostComments = \u6700\u591a\u5099\u8a3b
gb.sortMostPatchsetRevisions = \u6700\u591a\u88dc\u4e01\u4fee\u6b63
gb.sortMostRecentlyUpdated = \u6700\u8fd1\u66f4\u65b0
gb.sortMostVotes = \u6700\u591a\u6295\u7968
gb.sortNewest = \u6700\u65b0
gb.sortOldest = \u6700\u820a
gb.specified = \u6307\u5b9a\u7d66\u4e88(\u542b\u7cfb\u7d71\u9810\u8a2d)
gb.sshKeyCommentDescription = \u8acb\u8f38\u5165\u5099\u8a3b, \u82e5\u7121\u5099\u8a3b, \u5c07\u81ea\u8a02\u586b\u5165key data
gb.sshKeyPermissionDescription = \u6307\u5b9a\u8a72SSH key\u6240\u64c1\u6709\u7684\u5b58\u53d6\u6b0a\u9650
gb.sshKeys = SSH Keys
gb.sshKeysDescription = SSH \u516c\u958b\u91d1\u9470\u662f\u5bc6\u78bc\u8a8d\u8b49\u5916\u66f4\u5b89\u5168\u7684\u9078\u9805
gb.sslCertificateGenerated = \u6210\u529f\u7522\u751f\u7d66{0}\u7684\u670d\u5668SSL\u8b49\u66f8
gb.sslCertificateGeneratedRestart = \u6210\u529f\u7522\u751f\u7d66{0}\u4f7f\u7528\u7684SSL\u8b49\u66f8\n\u4f60\u5fc5\u9808\u91cd\u65b0\u555f\u52d5Gitblit\u7248\u63a7\u4f3a\u670d\u5668\u624d\u80fd\u555f\u7528\u65b0\u7684\u8b49\u66f8\n\nf you are launching with the '--alias' parameter you will have to set that to ''--alias {0}''.
gb.star = \u91cd\u8981
gb.stargazers = stargazers
gb.starred = \u91cd\u8981
gb.starredAndOwned = \u91cd\u8981\u7684 & \u64c1\u6709\u7684
gb.starredRepositories = \u91cd\u8981\u7684\u6587\u4ef6\u5eab
gb.starting = \u555f\u52d5\u4e2d
gb.stateProvince = \u5dde\u6216\u7701
gb.stats = \u7d71\u8a08
gb.status = \u72c0\u614b
gb.stepN = \u6b65\u9a5f{0}
gb.stopWatching = \u505c\u6b62\u8ffd\u8e64(watching)
gb.subject = \u6a19\u984c
gb.subscribe = \u8a02\u95b1
gb.summary = \u532f\u7e3d
gb.superseded = \u5df2\u88ab\u66ff\u4ee3
gb.tag = \u6a19\u7c64
gb.tagger = tagger
gb.tags = \u6a19\u7c64
gb.taskTickets = \u4efb\u52d9
gb.team = \u5718\u968a
gb.teamCreated = \u5718\u968a"{0}"\u65b0\u589e\u6210\u529f.
gb.teamMembers = \u5718\u968a\u6210\u54e1
gb.teamMemberships = \u5718\u968a\u6210\u54e1(memberships)
gb.teamMustSpecifyRepository = \u5718\u968a\u6700\u5c11\u8981\u6307\u5b9a\u4e00\u500b\u7248\u672c\u5eab
gb.teamName = \u5718\u968a\u540d\u7a31
gb.teamNameUnavailable = \u5718\u968a"{0}"\u4e0d\u5b58\u5728.
gb.teamPermission = "{0}" \u5718\u968a\u6210\u54e1\u7684\u6b0a\u9650
gb.teamPermissions = \u5718\u968a\u6b0a\u9650
gb.teamPermissionsDescription = \u4f60\u53ef\u4ee5\u6307\u5b9a\u5718\u968a\u6b0a\u9650.\u9019\u4e9b\u8a2d\u5b9a\u5c07\u6703\u53d6\u4ee3\u539f\u672c\u5718\u968a\u9810\u8a2d\u6b0a\u9650
gb.teams = \u53c3\u8207\u7684\u5718\u968a
gb.ticket = \u4efb\u52d9\u55ae
gb.ticketAssigned = \u5df2\u6307\u5b9a
gb.ticketComments = \u8a3b\u89e3
gb.ticketId = \u4efb\u52d9\u55aeID
gb.ticketIsClosed = \u8a72\u4efb\u52d9\u55ae\u5df2\u7d93\u7d50\u6848
gb.ticketN = \u4efb\u52d9\u55ae\u865f#{0}
gb.ticketOpenDate = \u767c\u884c\u65e5
gb.ticketPatchset = {0}\u4efb\u52d9\u55ae,{1}\u88dc\u4e01
gb.tickets = \u4efb\u52d9\u55ae
gb.ticketSettings = \u4efb\u52d9\u55ae\u5167\u5bb9\u8a2d\u5b9a
gb.ticketStatus = \u72c0\u614b
gb.ticketsWelcome = \u4f60\u53ef\u4ee5\u5229\u7528\u4efb\u52d9\u55ae\u7cfb\u7d71\u5efa\u69cb\u51fa\u5f85\u8fa6\u4e8b\u9805, \u81ed\u87f2\u56de\u5831\u5340\u4ee5\u53ca\u88dc\u4e01\u5305\u7684\u5354\u540c\u5408\u4f5c
gb.time.daysAgo = {0}\u5929\u524d
gb.time.hoursAgo = {0}\u5c0f\u6642\u524d
gb.time.inDays = {0}\u5929\u5167
gb.time.inHours = {0}\u5c0f\u6642\u5167
gb.time.inMinutes = {0}\u5206\u9418\u5167
gb.time.justNow = \u525b\u525b
gb.time.minsAgo = {0}\u5206\u9418\u524d
gb.time.monthsAgo = {0}\u6708\u524d
gb.time.oneYearAgo = 1\u5e74\u524d
gb.time.today = \u4eca\u5929
gb.time.weeksAgo = {0}\u5468\u524d
gb.time.yearsAgo = {0}\u5e74\u524d
gb.time.yesterday = \u6628\u5929
gb.title = \u6a19\u984c
gb.to = to
gb.toBranch = to {0}
gb.todaysActivityNone = \u4eca\u5929/\u7121
gb.todaysActivityStats = \u4eca\u5929/\u6709{2}\u500b\u4f5c\u8005\u5b8c\u6210{1}\u500b\u63d0\u4ea4
gb.token = token
gb.tokenAllDescription = \u6240\u6709\u7248\u672c\u5eab,\u4f7f\u7528\u8005\u8207\u8a2d\u5b9a
gb.tokenJurDescription = \u6240\u6709\u7248\u672c\u5eab
gb.tokens = federation tokens
gb.tokenUnrDescription = \u6240\u6709\u7248\u672c\u5eab\u8207\u4f7f\u7528\u8005
gb.topic = \u8a71\u984c
gb.topicsAndLabels = \u8a71\u984c\u8207\u6a19\u8a18
gb.transportPreference = \u9810\u8a2d\u901a\u8a0a\u5354\u5b9a
gb.transportPreferenceDescription = \u8a2d\u5b9a\u4f60\u5e38\u7528\u7684\u9023\u7dda\u901a\u8a0a\u5354\u5b9a\u4ee5\u7528\u4f86\u8907\u88fd(clone)
gb.tree = \u76ee\u9304
gb.type = \u985e\u578b
gb.unauthorizedAccessForRepository = \u7248\u672c\u5eab\u672a\u6388\u6b0a\u5b58\u53d6
gb.undefinedQueryWarning = \u672a\u8a2d\u5b9a\u67e5\u8a62\u689d\u4ef6
gb.unspecified = \u672a\u6307\u5b9a
gb.unstar = \u53d6\u6d88
gb.updated = \u5df2\u66f4\u65b0
gb.updatedBy = updated by
gb.uploadedPatchsetN = \u88dc\u4e01{0}\u5df2\u4e0a\u50b3
gb.uploadedPatchsetNRevisionN = \u88dc\u4e01{0}\u4fee\u6539\u7248\u672c{1}\u5df2\u4e0a\u50b3
gb.url = URL
gb.useDocsDescription = \u8a08\u7b97\u6587\u4ef6\u5eab\u88e1\u9762\u7684Markdown\u6a94\u6848
gb.useIncrementalPushTagsDescription = \u63a8\u9001\u6642\u5c07\u81ea\u52d5\u65b0\u589e\u6a19\u7c64\u865f\u78bc
gb.userCreated = \u6210\u529f\u5efa\u7acb\u65b0\u4f7f\u7528\u8005"{0}"
gb.userDeleted = \u4f7f\u7528\u8005"{0}"\u5df2\u522a\u9664
gb.userDeleteFailed = \u4f7f\u7528\u8005"{0}"\u522a\u9664\u5931\u6557
gb.username = \u4f7f\u7528\u8005\u540d\u7a31
gb.usernameUnavailable = \u4f7f\u7528\u8005\u540d\u7a31"{0}"\u4e0d\u53ef\u7528
gb.userPermissions = \u4f7f\u7528\u8005\u6b0a\u9650
gb.userPermissionsDescription = \u4f60\u53ef\u4ee5\u91dd\u5c0d\u5e33\u865f\u8a2d\u5b9a\u6b0a\u9650(\u9019\u4e9b\u8a2d\u5b9a\u5c07\u8986\u84cb\u5718\u968a\u6216\u5176\u4ed6\u6b0a\u9650)
gb.users = \u4f7f\u7528\u8005
gb.userServiceDoesNotPermitAddUser = {0}\u4e0d\u5141\u8a31\u65b0\u589e\u4f7f\u7528\u8005\u5e33\u865f
gb.userServiceDoesNotPermitPasswordChanges = {0}\u4e0d\u5141\u8a31\u4fee\u6539\u5bc6\u78bc
gb.useTicketsDescription = readonly, distributed Ticgit issues
gb.validFrom = valid from
gb.validity = validity
gb.validUntil = valid until
gb.verifyCommitter = \u63d0\u4ea4\u8005\u9700\u9a57\u8b49
gb.verifyCommitterDescription = \u9700\u8981\u63d0\u4ea4\u8005\u7b26\u5408\u63a8\u9001\u5e33\u865f
gb.verifyCommitterNote = \u6240\u6709\u5408\u4f75\u52d5\u4f5c\u7686\u9808\u5f37\u5236\u4f7f\u7528"--no-ff"\u53c3\u6578
gb.version = \u7248\u672c
gb.veto = veto
gb.view = \u6aa2\u8996
gb.viewAccess = \u4f60\u6c92\u6709Gitblit\u8b80\u53d6\u6216\u662f\u4fee\u6539\u6b0a\u9650
gb.viewCertificate = \u6aa2\u8996\u8b49\u66f8
gb.viewComparison = \u6bd4\u8f03\u9019{0}\u500b\u63d0\u4ea4 \u00bb
gb.viewPermission = {0} (\u6aa2\u8996)
gb.viewPolicy = Restrict View, Clone, & Push
gb.viewPolicyDescription = \u9078\u64c7\u53ef\u4ee5\u5728\u6587\u4ef6\u5eab\u6aa2\u8996,\u8907\u88fd(clone)\u8207\u63a8\u9001(push)\u7684\u4f7f\u7528\u8005, \u9664\u6b64\u4e4b\u5916\u5176\u4ed6\u4eba\u7686\u7121\u6b0a\u9650
gb.viewRestricted = authenticated view, clone, & push
gb.vote = \u5c0d{0}\u6295\u7968
gb.voters = votes
gb.votes = votes
gb.warning = \u8b66\u544a
gb.watch = \u76e3\u770b{0}
gb.watchers = \u76e3\u770b\u8005
gb.watching = \u76e3\u770b\u4e2d
gb.workingCopy = \u5de5\u4f5c\u8907\u672c
gb.workingCopyWarning = \u8a72\u6587\u4ef6\u5eab\u4ecd\u6709\u5de5\u4f5c\u8907\u672c,\u56e0\u6b64\u7121\u6cd5\u63a5\u53d7\u63a8\u9001(push)
gb.write = write
gb.youDoNotHaveClonePermission = \u4f60\u4e0d\u5141\u8a31\u8907\u88fd(clone)\u6b64\u6587\u4ef6\u5eab
gb.yourAssignedTickets = \u6307\u6d3e\u7d66\u4f60\u7684
gb.yourCreatedTickets = \u7531\u4f60\u65b0\u589e\u7684
gb.yourWatchedTickets = \u4f60\u60f3\u770b\u7684
gb.zip = zip\u58d3\u7e2e\u6a94
gb.ticketState =
gb.repositoryForkFailed =
gb.anonymousUser =
gb.oneAttachment =
gb.viewPolicy =
gb.emailMeOnMyTicketChangesDescription =
gb.allRepositories = \u6240\u6709\u7248\u672c\u5eab
gb.oid = \u7de8\u865f
gb.filestore = \u5927\u6a94\u6848\u5340
gb.filestoreStats = \u5927\u6a94\u6848\u5340(Filestore)\u5305\u542b {0} \u6a94\u6848\u5bb9\u91cf {1}. (\u9084\u5269\u4e0b{2}\u53ef\u7528)
gb.statusChangedOn = \u4fee\u6539\u65e5\u671f
gb.statusChangedBy = \u4fee\u6539\u8005
gb.filestoreHelp = \u6309\u6b64\u4e86\u89e3\u5927\u6a94\u6848\u5340(FileStore)\u5132\u5b58\u529f\u80fd
gb.editFile = \u7de8\u8f2f\u6a94\u6848
gb.continueEditing = \u7e7c\u7e8c\u7de8\u8f2f
gb.commitChanges = Commit Changes
gb.fileNotMergeable = \u7121\u6cd5\u63d0\u4ea4 {0}. \u6a94\u6848\u7121\u6cd5\u81ea\u52d5\u5408\u4f75.
gb.fileCommitted = \u6210\u529f\u63d0\u4ea4
gb.deletePatchset = \u522a\u9664 Patchset {0}
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

@@ -221,3 +221,4 @@ gb.repository = repository

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)

@@ -664,2 +665,3 @@ gb.noHits = no hits

gb.mergeTo = merge to
gb.mergeType = merge type
gb.labels = labels

@@ -676,2 +678,3 @@ gb.reviewers = reviewers

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.

@@ -787,1 +790,2 @@ gb.youDoNotHaveClonePermission = You are not permitted to clone this repository.

gb.referencedByTicket = Referenced by ticket.
gb.emailClientCertificateSubject = Your Gitblit client certificate for {0}

@@ -20,2 +20,3 @@ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

<link rel="stylesheet" type="text/css" href="gitblit.css"/>
<link rel="stylesheet" type="text/css" href="bootstrap-fixes.css"/>
</wicket:head>

@@ -54,5 +55,6 @@

<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>

@@ -31,3 +31,3 @@ /*

import com.gitblit.models.UserModel;
import com.gitblit.utils.StringUtils;
import com.gitblit.utils.PasswordHash;
import com.gitblit.wicket.GitBlitWebSession;

@@ -38,4 +38,4 @@ import com.gitblit.wicket.NonTrimmedPasswordTextField;

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>("");

@@ -90,11 +90,7 @@ public ChangePasswordPage() {

// 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);
}

@@ -101,0 +97,0 @@

@@ -54,2 +54,7 @@ /*

RevCommit commit = JGitUtils.getCommit(r, objectId);
if (commit == null) {
setResponsePage(NoDocsPage.class, params);
return;
}
String [] encodings = getEncodings();

@@ -56,0 +61,0 @@

@@ -65,2 +65,6 @@ /*

RevCommit head = JGitUtils.getCommit(r, objectId);
if (head == null) {
setResponsePage(NoDocsPage.class, params);
return;
}
final String commitId = getBestCommitId(head);

@@ -67,0 +71,0 @@

@@ -126,3 +126,4 @@ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

<div wicket:id="mergeTo"></div>
<div wicket:id="mergeType"></div>
</div>

@@ -129,0 +130,0 @@

@@ -59,2 +59,3 @@ /*

import com.gitblit.Constants.FederationStrategy;
import com.gitblit.Constants.MergeType;
import com.gitblit.Constants.RegistrantType;

@@ -462,2 +463,7 @@ import com.gitblit.GitBlitException;

availableBranches));
form.add(new ChoiceOption<MergeType>("mergeType",
getString("gb.mergeType"),
getString("gb.mergeTypeDescription"),
new PropertyModel<MergeType>(repositoryModel, "mergeType"),
Arrays.asList(MergeType.values())));

@@ -464,0 +470,0 @@ //

@@ -34,7 +34,8 @@ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

<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>

@@ -41,0 +42,0 @@ </table>

@@ -23,3 +23,5 @@ /*

import java.util.List;
import java.util.Locale;
import com.gitblit.utils.PasswordHash;
import org.apache.wicket.PageParameters;

@@ -30,5 +32,7 @@ import org.apache.wicket.behavior.SimpleAttributeModifier;

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;

@@ -113,2 +117,7 @@ import org.apache.wicket.model.util.CollectionModel;

.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) {

@@ -129,2 +138,6 @@

}
Language lang = language.getObject();
if (lang != null) {
userModel.getPreferences().setLocale(lang.code);
}
// force username to lower-case

@@ -148,4 +161,3 @@ userModel.username = userModel.username.toLowerCase();

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.

@@ -157,3 +169,3 @@ // Check length.

}
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"),

@@ -165,17 +177,12 @@ minLength));

// 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"));

@@ -261,3 +268,6 @@ return;

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) {

@@ -264,0 +274,0 @@ // user inherits Admin permission

@@ -55,3 +55,5 @@ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

<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>

@@ -58,0 +60,0 @@ </div>

@@ -30,2 +30,3 @@ /*

import org.apache.wicket.markup.html.form.TextField;
import org.apache.wicket.markup.html.link.ExternalLink;
import org.apache.wicket.markup.html.panel.Fragment;

@@ -54,2 +55,4 @@ import org.apache.wicket.markup.repeater.Item;

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() {

@@ -172,2 +175,3 @@ super();

form.add(new CheckBox("allrepos", allreposModel));
form.add(new ExternalLink("querySyntax", LUCENE_QUERY_SYNTAX_LINK));
add(form.setEnabled(luceneEnabled));

@@ -174,0 +178,0 @@

@@ -524,3 +524,9 @@ /*

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);
}
}

@@ -532,3 +538,9 @@ });

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);
}
}

@@ -535,0 +547,0 @@ });

@@ -18,3 +18,2 @@ /*

import java.io.Serializable;
import java.util.ArrayList;

@@ -170,8 +169,6 @@ import java.util.Arrays;

}
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"),

@@ -190,20 +187,5 @@ new Language("English","en"),

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;

@@ -220,3 +202,3 @@ if (locale != null) {

preferredLanguage = language;
} else if (preferredLanguage != null && language.code.startsWith(locale.getLanguage())) {
} else if (preferredLanguage == null && language.code.startsWith(locale.getLanguage())) {
// language match

@@ -227,3 +209,27 @@ 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());

@@ -323,20 +329,2 @@ final IModel<String> emailAddress = Model.of(user.emailAddress == null ? "" : user.emailAddress);

}
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 +")";
}
}
}

@@ -51,3 +51,3 @@ /*

if (totalPages > 0) {
if (totalPages > 0 && currentPage > 1) {
pages.add(new PageObject("\u2190", currentPage - 1));

@@ -61,3 +61,3 @@ }

}
if (totalPages > 0) {
if (totalPages > 0 && currentPage < totalPages) {
pages.add(new PageObject("\u2192", currentPage + 1));

@@ -80,2 +80,3 @@ }

WicketUtils.setCssClass(item, "disabled");
link.setEnabled(false);
}

@@ -82,0 +83,0 @@ }

@@ -21,2 +21,5 @@ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

<wicket:fragment wicket:id="emptyFragment">
</wicket:fragment>
<wicket:fragment wicket:id="repoIconFragment">

@@ -76,5 +79,11 @@ <span class="octicon octicon-centered octicon-repo"></span>

<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"/>

@@ -91,4 +100,12 @@ <wicket:message key="gb.repository">Repository</wicket:message>

<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>

@@ -108,2 +125,2 @@ </wicket:fragment>

</body>
</html>
</html>

@@ -31,2 +31,3 @@ /*

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;

@@ -47,2 +48,3 @@ import org.apache.wicket.markup.html.link.BookmarkablePageLink;

import com.gitblit.models.RepositoryModel;
import com.gitblit.models.TreeNodeModel;
import com.gitblit.models.UserModel;

@@ -64,2 +66,22 @@ import com.gitblit.utils.ArrayUtils;

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,

@@ -72,6 +94,8 @@ List<RepositoryModel> models, boolean 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;

@@ -104,3 +128,24 @@ Fragment managementLinks;

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>();

@@ -148,2 +193,3 @@ Map<String, List<RepositoryModel>> groups = new HashMap<String, List<RepositoryModel>>();

if (dp != null) {
final boolean showSwatch = app().settings().getBoolean(Keys.web.repositoryListSwatches, true);

@@ -169,2 +215,12 @@

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);

@@ -184,3 +240,3 @@

}
WicketUtils.setCssClass(item, "group");
WicketUtils.setCssClass(item, "group collapsible");
// reset counter so that first row is light background

@@ -330,4 +386,13 @@ counter = 0;

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);
}
}
}

@@ -334,0 +399,0 @@

@@ -51,2 +51,3 @@ /*

private final UserModel user;
private final boolean canWriteKeys;

@@ -57,2 +58,3 @@ public SshKeysPanel(String wicketId, UserModel user) {

this.user = user;
this.canWriteKeys = app().keys().supportsWritingKeys(user);
}

@@ -95,2 +97,5 @@

};
if (!canWriteKeys) {
delete.setVisibilityAllowed(false);
}
item.add(delete);

@@ -170,4 +175,8 @@ }

if (! canWriteKeys) {
addKeyForm.setVisibilityAllowed(false);
}
add(addKeyForm);
}
}
<?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">

@@ -52,2 +52,2 @@ <!-- The base folder is used to specify the root location of your Gitblit data.

</session-config>
</web-app>
</web-app>
<?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>

@@ -9,2 +9,2 @@ <wls:context-root>gitblit</wls:context-root>

</wls:container-descriptor>
</wls:weblogic-web-app>
</wls:weblogic-web-app>

@@ -48,2 +48,3 @@ body {

width: 16px;
padding-left: 17px;
}

@@ -1967,2 +1968,3 @@

width: 13em;
white-space: nowrap;
}

@@ -1969,0 +1971,0 @@

@@ -0,3 +1,4 @@

[user "admin"]
password = admin
password = MD5:21232f297a57a5a743894a0e4a801fc3
cookie = dd94709528bb1c83d08f3088d4043f4742891f4f

@@ -4,0 +5,0 @@ accountType = LOCAL

@@ -46,2 +46,3 @@ /*

import com.gitblit.utils.PasswordHash;
import org.junit.Test;

@@ -669,3 +670,34 @@

@Test
public void testAuthenticateUpgradePlaintext() throws Exception {
IAuthenticationManager auth = newAuthenticationManager();
UserModel user = new UserModel("sunnyjim");
user.password = "password";
users.updateUserModel(user);
assertNotNull(auth.authenticate(user.username, user.password.toCharArray(), null));
// validate that plaintext password was automatically updated to hashed one
assertTrue(user.password.startsWith(PasswordHash.getDefaultType().name() + ":"));
}
@Test
public void testAuthenticateUpgradeMD5() throws Exception {
IAuthenticationManager auth = newAuthenticationManager();
UserModel user = new UserModel("sunnyjim");
user.password = "MD5:5F4DCC3B5AA765D61D8327DEB882CF99";
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() + ":"));
}
@Test
public void testContenairAuthenticate() throws Exception {

@@ -672,0 +704,0 @@ settings.put(Keys.realm.container.autoCreateAccounts, "true");

@@ -62,3 +62,3 @@ /*

BranchTicketService service = new BranchTicketService(
BranchTicketService service = (BranchTicketService) new BranchTicketService(
runtimeManager,

@@ -65,0 +65,0 @@ pluginManager,

@@ -43,3 +43,3 @@ /*

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;

@@ -56,5 +56,5 @@ repository.close();

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;

@@ -71,3 +71,3 @@ repository.close();

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;

@@ -84,3 +84,3 @@ repository.close();

RevCommit commit = JGitUtils.getCommit(repository,
"1d0c2933a4ae69c362f76797d42d6bd182d05176");
GitBlitSuite.helloworldSettings.getRequiredString(HelloworldKeys.commit.second));
String patch = DiffUtils.getCommitPatch(repository, null, commit, "java.java");

@@ -97,5 +97,5 @@ repository.close();

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");

@@ -112,5 +112,5 @@ repository.close();

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);

@@ -127,7 +127,7 @@ repository.close();

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);
}
}

@@ -18,2 +18,3 @@ /*

import java.io.IOException;
import java.util.Date;

@@ -49,2 +50,8 @@ import java.util.HashMap;

//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);

@@ -59,2 +66,6 @@

public static void stopGitblit() throws Exception {
//clean up test user and team if left over
deleteTestUser();
deleteTestTeam();
if (started.get()) {

@@ -65,2 +76,14 @@ 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

@@ -128,14 +151,18 @@ public void testProposal() throws Exception {

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()));

@@ -148,3 +175,3 @@

newUser = users.get(0);
assertTrue(newUser.isTeamMember("testteam"));
assertTrue(newUser.isTeamMember(testTeam));

@@ -157,5 +184,8 @@ assertTrue(RpcUtils.deleteUser(newUser, url, account, password.toCharArray()));

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()));

@@ -162,0 +192,0 @@

@@ -61,3 +61,3 @@ /*

FileTicketService service = new FileTicketService(
FileTicketService service = (FileTicketService) new FileTicketService(
runtimeManager,

@@ -64,0 +64,0 @@ pluginManager,

@@ -19,2 +19,5 @@ /*

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Field;

@@ -24,2 +27,4 @@ import java.util.concurrent.Executors;

import java.util.concurrent.atomic.AtomicInteger;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

@@ -32,2 +37,3 @@ import org.eclipse.jgit.api.Git;

import org.eclipse.jgit.util.FS;
import org.eclipse.jgit.util.FileUtils;
import org.junit.AfterClass;

@@ -39,2 +45,3 @@ import org.junit.BeforeClass;

import com.gitblit.FileSettings;
import com.gitblit.GitBlitException;

@@ -84,2 +91,14 @@ import com.gitblit.GitBlitServer;

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;

@@ -175,13 +194,35 @@ static int gitPort = 8300;

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());

@@ -263,2 +304,45 @@ showRemoteBranches("ticgit.git");

}
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;
}
}

@@ -28,2 +28,7 @@ /*

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;

@@ -93,2 +98,5 @@ import org.eclipse.jgit.api.Git;

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());

@@ -112,15 +120,15 @@

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);
}

@@ -406,3 +414,3 @@ }

if (verification.exists()) {
FileUtils.delete(verification, FileUtils.RECURSIVE);
FileUtils.delete(verification, FileUtils.RECURSIVE | FileUtils.RETRY);
}

@@ -492,3 +500,3 @@ CloneCommand clone = Git.cloneRepository();

if (verification.exists()) {
FileUtils.delete(verification, FileUtils.RECURSIVE);
FileUtils.delete(verification, FileUtils.RECURSIVE | FileUtils.RETRY);
}

@@ -638,3 +646,3 @@ CloneCommand clone = Git.cloneRepository();

if (refChecks.exists()) {
FileUtils.delete(refChecks, FileUtils.RECURSIVE);
FileUtils.delete(refChecks, FileUtils.RECURSIVE | FileUtils.RETRY);
}

@@ -672,3 +680,3 @@ CloneCommand clone = Git.cloneRepository();

if (local.exists()) {
FileUtils.delete(local, FileUtils.RECURSIVE);
FileUtils.delete(local, FileUtils.RECURSIVE | FileUtils.RETRY);
}

@@ -935,2 +943,63 @@ clone = Git.cloneRepository();

}
@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);
}
}

@@ -77,7 +77,7 @@ /*

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"));

@@ -100,7 +100,7 @@ RepositoryModel repository = repositories().getRepositoryModel("helloworld.git");

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"));

@@ -126,7 +126,7 @@ RepositoryModel repository = repositories().getRepositoryModel("helloworld.git");

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"));

@@ -151,3 +151,3 @@ RepositoryModel repository = repositories().getRepositoryModel("helloworld.git");

commands.add(new ReceiveCommand(ObjectId.zeroId(), ObjectId
.fromString("3fa7c46d11b11d61f1cbadc6888be5d0eae21969"), "refs/heads/master"));
.fromString(GitBlitSuite.helloworldSettings.getRequiredString(HelloworldKeys.commit.deleted)), "refs/heads/master"));

@@ -166,3 +166,3 @@ RepositoryModel repository = new RepositoryModel("ex@mple.git", "", "admin", new Date());

commands.add(new ReceiveCommand(ObjectId.zeroId(), ObjectId
.fromString("3fa7c46d11b11d61f1cbadc6888be5d0eae21969"), "refs/tags/v1.0"));
.fromString(GitBlitSuite.helloworldSettings.getRequiredString(HelloworldKeys.commit.deleted)), "refs/tags/v1.0"));

@@ -182,4 +182,4 @@ RepositoryModel repository = new RepositoryModel("ex@mple.git", "", "admin", new Date());

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"));

@@ -199,3 +199,3 @@ RepositoryModel repository = new RepositoryModel("ex@mple.git", "", "admin", new Date());

ReceiveCommand command = new ReceiveCommand(ObjectId
.fromString("3fa7c46d11b11d61f1cbadc6888be5d0eae21969"), ObjectId.zeroId(),
.fromString(GitBlitSuite.helloworldSettings.getRequiredString(HelloworldKeys.commit.deleted)), ObjectId.zeroId(),
"refs/heads/master");

@@ -218,3 +218,3 @@ commands.add(command);

commands.add(new ReceiveCommand(ObjectId
.fromString("3fa7c46d11b11d61f1cbadc6888be5d0eae21969"), ObjectId.zeroId(),
.fromString(GitBlitSuite.helloworldSettings.getRequiredString(HelloworldKeys.commit.deleted)), ObjectId.zeroId(),
"refs/heads/other"));

@@ -235,3 +235,3 @@

ReceiveCommand command = new ReceiveCommand(ObjectId
.fromString("3fa7c46d11b11d61f1cbadc6888be5d0eae21969"), ObjectId.zeroId(),
.fromString(GitBlitSuite.helloworldSettings.getRequiredString(HelloworldKeys.commit.deleted)), ObjectId.zeroId(),
"refs/tags/v1.0");

@@ -254,4 +254,4 @@ commands.add(command);

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"));

@@ -275,4 +275,4 @@ RepositoryModel repository = new RepositoryModel("ex@mple.git", "", "admin", new Date());

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"));

@@ -279,0 +279,0 @@ RepositoryModel repository = new RepositoryModel("ex@mple.git", "", "admin", new Date());

@@ -203,7 +203,7 @@ /*

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);

@@ -210,0 +210,0 @@ assertEquals("user2", user.username);

@@ -20,2 +20,6 @@ /*

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;

@@ -55,2 +59,4 @@ import java.util.Arrays;

import static org.junit.Assume.assumeTrue;
public class JGitUtilsTest extends GitblitUnitTest {

@@ -80,2 +86,113 @@

@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() {

@@ -114,3 +231,4 @@ List<String> list = JGitUtils.getRepositoryList(GitBlitSuite.REPOSITORIES, false, true, -1, null);

assertNotNull("Could not get first commit!", commit);
assertEquals("Incorrect first commit!", "f554664a346629dc2b839f7292d06bad2db4aece",
assertEquals("Incorrect first commit!",
GitBlitSuite.helloworldSettings.getRequiredString(HelloworldKeys.commit.first),
commit.getName());

@@ -449,6 +567,6 @@ assertTrue(firstChange.equals(new Date(commit.getCommitTime() * 1000L)));

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);

@@ -486,6 +604,18 @@

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);
}

@@ -535,6 +665,7 @@

// 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();

@@ -547,4 +678,4 @@ }

List<RevCommit> commits = JGitUtils.getRevLog(repository,
"fbd14fa6d1a01d4aefa1fca725792683800fc67e",
"85a0e4087b8439c0aa6b1f4f9e08c26052ab7e87");
GitBlitSuite.helloworldSettings.getRequiredString(HelloworldKeys.commit.second),
GitBlitSuite.helloworldSettings.getRequiredString(HelloworldKeys.commit.fifteen));
repository.close();

@@ -551,0 +682,0 @@ assertEquals(14, commits.size());

@@ -20,3 +20,3 @@ /*

import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

@@ -33,3 +33,4 @@

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");

@@ -42,3 +43,3 @@ map.put("b", "bear");

assertEquals(
"{\"d\":\"dingo\",\"e\":\"eagle\",\"b\":\"bear\",\"c\":\"caterpillar\",\"a\":\"alligator\"}",
"{\"a\":\"alligator\",\"b\":\"bear\",\"c\":\"caterpillar\",\"d\":\"dingo\",\"e\":\"eagle\"}",
json);

@@ -62,2 +63,2 @@ Map<String, String> map2 = JsonUtils.fromJsonString(json,

}
}
}

@@ -19,14 +19,8 @@ /*

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;

@@ -45,5 +39,2 @@ import com.gitblit.IStoredSettings;

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;

@@ -60,16 +51,7 @@ import com.unboundid.ldap.sdk.SearchScope;

*/
public class LdapAuthenticationTest extends GitblitUnitTest {
@Rule
public TemporaryFolder folder = new TemporaryFolder();
@RunWith(Parameterized.class)
public class LdapAuthenticationTest extends LdapBasedUnitTest {
private static final String RESOURCE_DIR = "src/test/resources/ldap/";
private LdapAuthProvider ldap;
private File usersConf;
private LdapAuthProvider ldap;
static int ldapPort = 1389;
private static InMemoryDirectoryServer ds;
private IUserManager userManager;

@@ -79,22 +61,5 @@

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);

@@ -121,24 +86,2 @@ auth = newAuthenticationManager(settings);

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

@@ -150,3 +93,2 @@ public void testAuthenticate() {

assertNotNull(userOneModel.getTeam("git_users"));
assertTrue(userOneModel.canAdmin);

@@ -161,3 +103,2 @@ UserModel userOneModelFailedAuth = ldap.authenticate("UserOne", "userTwoPassword".toCharArray());

assertNotNull(userTwoModel.getTeam("git admins"));
assertTrue(userTwoModel.canAdmin);

@@ -168,6 +109,95 @@ UserModel userThreeModel = ldap.authenticate("UserThree", "userThreePassword".toCharArray());

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
public void testDisplayName() {

@@ -215,3 +245,3 @@ UserModel userOneModel = ldap.authenticate("UserOne", "userOnePassword".toCharArray());

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());

@@ -222,3 +252,3 @@ }

public void addingUserInLdapShouldNotUpdateGitBlitUsersAndGroups() throws Exception {
ds.addEntries(LDIFReader.readEntries(RESOURCE_DIR + "adduser.ldif"));
getDS().addEntries(LDIFReader.readEntries(RESOURCE_DIR + "adduser.ldif"));
ldap.sync();

@@ -231,3 +261,3 @@ assertEquals("Number of ldap users in gitblit user model", 5, countLdapUsersInUserManager());

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();

@@ -239,3 +269,3 @@ assertEquals("Number of ldap users in gitblit user model", 6, countLdapUsersInUserManager());

public void addingGroupsInLdapShouldNotUpdateGitBlitUsersAndGroups() throws Exception {
ds.addEntries(LDIFReader.readEntries(RESOURCE_DIR + "addgroup.ldif"));
getDS().addEntries(LDIFReader.readEntries(RESOURCE_DIR + "addgroup.ldif"));
ldap.sync();

@@ -246,5 +276,19 @@ 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();

@@ -255,2 +299,82 @@ 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() {

@@ -261,3 +385,2 @@ UserModel userOneModel = auth.authenticate("UserOne", "userOnePassword".toCharArray(), null);

assertNotNull(userOneModel.getTeam("git_users"));
assertTrue(userOneModel.canAdmin);

@@ -272,3 +395,2 @@ UserModel userOneModelFailedAuth = auth.authenticate("UserOne", "userTwoPassword".toCharArray(), null);

assertNotNull(userTwoModel.getTeam("git admins"));
assertTrue(userTwoModel.canAdmin);

@@ -279,8 +401,106 @@ UserModel userThreeModel = auth.authenticate("UserThree", "userThreePassword".toCharArray(), null);

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, "");

@@ -296,2 +516,10 @@ settings.put(Keys.realm.ldap.password, "");

private int countLdapUsersInUserManager() {

@@ -298,0 +526,0 @@ int ldapAccountCount = 0;

@@ -18,4 +18,10 @@ /*

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;

@@ -43,2 +49,68 @@

}
}
@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;
}
}

@@ -48,5 +48,5 @@ /*

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());
}
}

@@ -18,2 +18,4 @@ /*

import org.junit.Ignore;
import com.gitblit.IStoredSettings;

@@ -43,2 +45,3 @@ import com.gitblit.Keys;

*/
@Ignore("Redis tests currently broken, no service running.")
public class RedisTicketServiceTest extends TicketServiceTest {

@@ -71,3 +74,3 @@

RedisTicketService service = new RedisTicketService(
RedisTicketService service = (RedisTicketService) new RedisTicketService(
runtimeManager,

@@ -74,0 +77,0 @@ pluginManager,

@@ -71,2 +71,7 @@ /*

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()) {

@@ -269,7 +274,13 @@ GitBlitSuite.stopGitblit();

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");

@@ -276,0 +287,0 @@ aTeam.addRepositoryPermission("helloworld.git");

@@ -47,5 +47,5 @@ /*

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());
}

@@ -68,2 +68,3 @@

RepositoryModel model = repositories().getRepositoryModel("ticgit.git");
assertNotNull("Could not get repository modle for ticgit.git", model);
model.accessRestriction = AccessRestrictionType.CLONE;

@@ -70,0 +71,0 @@ model.authorizationControl = AuthorizationControl.NAMED;

@@ -40,3 +40,3 @@ /*

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);
}

@@ -68,5 +68,5 @@

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
}

@@ -82,5 +82,5 @@ }

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
}

@@ -102,5 +102,5 @@ }

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);

@@ -107,0 +107,0 @@ }

@@ -24,12 +24,22 @@ /*

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;

@@ -61,2 +71,53 @@ import org.junit.AfterClass;

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);
}
});
}

@@ -101,2 +162,12 @@

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() {

@@ -118,5 +189,7 @@ @Override

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());

@@ -138,3 +211,3 @@ ClientChannel channel = session.createChannel(ClientChannel.CHANNEL_EXEC, cmd);

channel.waitFor(ClientChannel.CLOSED, 0);
channel.waitFor(EnumSet.of(ClientChannelEvent.CLOSED, ClientChannelEvent.EOF), 0);

@@ -141,0 +214,0 @@ String result = out.toString().trim();

@@ -60,2 +60,3 @@ /*

static final String repoName = "TicketReferenceTest.git";
static File workingCopy = new File(GitBlitSuite.REPOSITORIES, "working/TicketReferenceTest.git-wc");

@@ -77,3 +78,3 @@

public static void configure() throws Exception {
File repositoryName = new File("TicketReferenceTest.git");;
File repositoryName = new File(repoName);

@@ -84,3 +85,3 @@ GitBlitSuite.close(repositoryName);

}
repo = new RepositoryModel("TicketReferenceTest.git", null, null, null);
repo = new RepositoryModel(repoName, null, null, null);

@@ -147,3 +148,19 @@ if (gitblit().hasRepository(repo.name)) {

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);
}
}

@@ -150,0 +167,0 @@

@@ -24,4 +24,4 @@ /*

import org.apache.commons.io.FileUtils;
import org.bouncycastle.util.Arrays;
import org.eclipse.jgit.util.FileUtils;
import org.junit.After;

@@ -69,3 +69,5 @@ import org.junit.Before;

if (deleteAll) {
FileUtils.deleteDirectory(dir);
if (dir.exists()) {
FileUtils.delete(dir, FileUtils.RECURSIVE | FileUtils.RETRY);
}
JGitUtils.createRepository(GitBlitSuite.REPOSITORIES, getRepository().name).close();

@@ -72,0 +74,0 @@ }

@@ -19,11 +19,9 @@ /*

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;

@@ -44,22 +42,12 @@ import org.junit.Test;

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;

@@ -75,3 +63,3 @@ import com.gitblit.utils.XssFilter;

private ITicketService service;
final String repoName = "UITicketTest.git";
static final String repoName = "UITicketTest.git";
final RepositoryModel repo = new RepositoryModel(repoName, null, null, null);

@@ -89,3 +77,3 @@

BranchTicketService service = new BranchTicketService(
BranchTicketService service = (BranchTicketService) new BranchTicketService(
runtimeManager,

@@ -106,3 +94,5 @@ pluginManager,

if (deleteAll) {
FileUtils.deleteDirectory(dir);
if (dir.exists()) {
FileUtils.delete(dir, FileUtils.RECURSIVE | FileUtils.RETRY);
}
JGitUtils.createRepository(GitBlitSuite.REPOSITORIES, repoName).close();

@@ -122,2 +112,11 @@ }

@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

@@ -157,2 +156,2 @@ public void setup() throws Exception {

}
}

@@ -13,3 +13,3 @@ [user "admin"]

accountType = LDAP
role = "#admin"
role = "#none"
[user "userfive"]

@@ -35,3 +35,3 @@ password = "#externalAccount"

accountType = LDAP
role = "#admin"
role = "#none"
[user "basic"]

@@ -68,4 +68,4 @@ password = MD5:f17aaabc20bfe045075927934fed52d2

[team "Git Admins"]
role = "#none"
role = "#admin"
accountType = LOCAL
user = usertwo
/*
* 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);
}
}
}
/*
* 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);
}
}
}
/*
* 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);
}
}
}
/*
* 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);
}
}

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet