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.7.1
to
v1.8.0
+30
src/main/distrib/data/groovy/youtrack-readme.md
# GitBlit YouTrack Receive Hook
GitBlit receive hook for updating referenced YouTrack issues.
This script has only been tested with the cloud hosted YouTrack instance.
## Usage
Due to limited authentication options when using the YouTrack REST API, you have to store a username and password for an account with appropriate permissions for adding comments to any issue. Hopefully in the future YouTrack will support API keys or similar.
1. Update your `gitblit.properties` file with the following entries:
* `groovy.customFields = "youtrackProjectID=YouTrack Project ID" ` *(or append to existing setting)*
* `youtrack.host = example.myjetbrains.com`
* `youtrack.user = ytUser`
* `youtrack.pass = insecurep@sswordsRus`
(But using your own host and credential info).
2. Copy the `youtrack.groovy` script to the `<gitblit-data-dir>/groovy` scripts directory.
3. In GitBlit, go to a repository, click the *edit* button, then click the *receive* link. In the *post0receive scripts* section you should see `youtrack` as an option. Move it over to the *Selected* column.
4. At the bottom of this same screen should should be a *custom fields* section with a **YouTrack Project ID** field. Enter the YouTrack Project ID associated with the repository.
5. When you commit changes, reference YouTrack issues with `#{projectID}-{issueID}` where `{projectID}` is the YouTrack Project ID, and `{issueID}` is the issue number. For example, to references issue `34` in project `fizz`:
git commit -m'Changed bazinator to fix issue #fizz-34.'
Multiple issues may be referenced in the same commit message.
## Attribution
Much of this script was cobbled together from the example receive hooks in the official [GitBlit](https://github.com/gitblit/gitblit) distribution.
/*
* 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.
*/
import com.gitblit.GitBlit
import com.gitblit.Keys
import com.gitblit.models.RepositoryModel
import com.gitblit.models.TeamModel
import com.gitblit.models.UserModel
import com.gitblit.utils.JGitUtils
import java.text.SimpleDateFormat
import org.eclipse.jgit.lib.Repository
import org.eclipse.jgit.lib.Config
import org.eclipse.jgit.transport.ReceiveCommand
import org.eclipse.jgit.transport.ReceiveCommand.Result
import org.slf4j.Logger
import org.eclipse.jgit.api.Status;
import org.eclipse.jgit.api.errors.JGitInternalException;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.diff.DiffFormatter;
import org.eclipse.jgit.diff.RawTextComparator;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.IndexDiff;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.patch.FileHeader;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.treewalk.FileTreeIterator;
import org.eclipse.jgit.treewalk.EmptyTreeIterator;
import org.eclipse.jgit.treewalk.CanonicalTreeParser;
import org.eclipse.jgit.util.io.DisabledOutputStream;
import java.util.Set;
import java.util.HashSet;
import org.apache.http.HttpHost;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.AuthCache;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.protocol.*;
import org.apache.http.client.protocol.*;
import org.apache.http.client.methods.*;
import org.apache.http.impl.client.*;
import org.apache.http.impl.auth.BasicScheme;
import org.apache.http.util.EntityUtils;
/**
* GitBlit Post-Receive Hook for YouTrack
*
* The purpose of this script is to invoke the YouTrack API and update a case when
* push is received based.
*
* The Post-Receive hook is executed AFTER the pushed commits have been applied
* to the Git repository. This is the appropriate point to trigger an
* integration build or to send a notification.
*
* If you want this hook script to fail and abort all subsequent scripts in the
* chain, "return false" at the appropriate failure points.
*
* Bound Variables:
* gitblit Gitblit Server com.gitblit.GitBlit
* repository Gitblit Repository com.gitblit.models.RepositoryModel
* receivePack JGit Receive Pack org.eclipse.jgit.transport.ReceivePack
* user Gitblit User com.gitblit.models.UserModel
* commands JGit commands Collection<org.eclipse.jgit.transport.ReceiveCommand>
* url Base url for Gitblit String
* logger Logs messages to Gitblit org.slf4j.Logger
* clientLogger Logs messages to Git client com.gitblit.utils.ClientLogger
*
*
* Custom Fileds Used by This script
* youtrackProjectID - Project ID in YouTrack
*
* Make sure to add the following to your gitblit.properties file:
* groovy.customFields = "youtrackProjectID=YouTrack Project ID"
* youtrack.host = example.myjetbrains.com
* youtrack.user = ytUser
* youtrack.pass = insecurep@sswordsRus
*/
// Indicate we have started the script
logger.info("youtrack hook triggered in ${url} by ${user.username} for ${repository.name}")
Repository r = gitblit.getRepository(repository.name)
// pull custom fields from repository specific values
def youtrackProjectID = repository.customFields.youtrackProjectID
if(youtrackProjectID == null || youtrackProjectID.length() == 0) return true;
def youtrackHost = gitblit.getString('youtrack.host', 'nohost')
def bugIdRegex = gitblit.getString('youtrack.commitMessageRegex', "#${youtrackProjectID}-([0-9]+)")
def youtrackUser = gitblit.getString('youtrack.user', 'nouser')
def youtrackPass = gitblit.getString('youtrack.pass', 'nopassword')
HttpHost target = new HttpHost(youtrackHost, 80, "http");
CredentialsProvider credsProvider = new BasicCredentialsProvider();
credsProvider.setCredentials(
new AuthScope(target.getHostName(), target.getPort()),
new UsernamePasswordCredentials(youtrackUser, youtrackPass));
def httpclient = new DefaultHttpClient();
httpclient.setCredentialsProvider(credsProvider);
try {
AuthCache authCache = new BasicAuthCache();
BasicScheme basicAuth = new BasicScheme();
authCache.put(target, basicAuth);
BasicHttpContext localcontext = new BasicHttpContext();
localcontext.setAttribute(ClientContext.AUTH_CACHE, authCache);
for (command in commands) {
for( commit in JGitUtils.getRevLog(r, command.oldId.name, command.newId.name).reverse() ) {
def bugIds = new java.util.HashSet()
def longMsg = commit.getFullMessage()
// Grab the second match group and then filter out each numeric ID and add it to array
(longMsg =~ bugIdRegex).each{ (it[1] =~ "\\d+").each { bugIds.add(it)} }
if(bugIds.size() > 0) {
def comment = createIssueComment(command, commit)
logger.debug("Submitting youtrack comment:\n" + comment)
def encoded = URLEncoder.encode(comment)
for(bugId in bugIds ) {
def baseURL = "http://${youtrackHost}/youtrack/rest/issue/${youtrackProjectID}-${bugId}/execute?command=&comment=" + encoded
def post = new HttpPost(baseURL);
clientLogger.info("Executing request " + post.getRequestLine() + " to target " + target);
def response = httpclient.execute(target, post, localcontext);
logger.debug(response.getStatusLine().toString());
EntityUtils.consume(response.getEntity());
}
}
}
}
}
finally {
r.close()
}
def createIssueComment(command, commit) {
def commits = [commit] // Borrowed code expects a collection.
Repository r = gitblit.getRepository(repository.name)
// define the summary and commit urls
def repo = repository.name
def summaryUrl
def commitUrl
if (gitblit.getBoolean(Keys.web.mountParameters, true)) {
repo = repo.replace('/', gitblit.getString(Keys.web.forwardSlashCharacter, '/')).replace('/', '%2F')
summaryUrl = url + "/summary/$repo"
commitUrl = url + "/commit/$repo/"
} else {
summaryUrl = url + "/summary?r=$repo"
commitUrl = url + "/commit?r=$repo&h="
}
// construct a simple text summary of the changes contained in the push
def commitBreak = '\n'
def commitCount = 0
def changes = ''
SimpleDateFormat df = new SimpleDateFormat(gitblit.getString(Keys.web.datetimestampLongFormat, 'EEEE, MMMM d, yyyy h:mm a z'))
def table = {
def shortSha = it.id.name.substring(0, 8)
"* [$commitUrl$it.id.name ${shortSha}] by *${it.authorIdent.name}* on ${df.format(JGitUtils.getCommitDate(it))}\n" +
" {cut $it.shortMessage}\n{noformat}$it.fullMessage{noformat}{cut}"
}
def ref = command.refName
def refType = 'branch'
if (ref.startsWith('refs/heads/')) {
ref = command.refName.substring('refs/heads/'.length())
} else if (ref.startsWith('refs/tags/')) {
ref = command.refName.substring('refs/tags/'.length())
refType = 'tag'
}
switch (command.type) {
case ReceiveCommand.Type.CREATE:
// new branch
changes += "''new $refType $ref created''\n"
changes += commits.collect(table).join(commitBreak)
changes += '\n'
break
case ReceiveCommand.Type.UPDATE:
// fast-forward branch commits table
changes += "''$ref $refType updated''\n"
changes += commits.collect(table).join(commitBreak)
changes += '\n'
break
case ReceiveCommand.Type.UPDATE_NONFASTFORWARD:
// non-fast-forward branch commits table
changes += "''$ref $refType updated [NON fast-forward]''"
changes += commits.collect(table).join(commitBreak)
changes += '\n'
break
case ReceiveCommand.Type.DELETE:
// deleted branch/tag
changes += "''$ref $refType deleted''"
break
default:
break
}
return "$user.username pushed commits to [$summaryUrl $repository.name]\n$changes"
}
/*
* Copyright 2015 gitblit.com.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.gitblit.auth;
import java.util.HashSet;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.gitblit.Constants;
import com.gitblit.Constants.AccountType;
import com.gitblit.Constants.AuthenticationType;
import com.gitblit.Constants.Role;
import com.gitblit.Keys;
import com.gitblit.models.TeamModel;
import com.gitblit.models.UserModel;
import com.gitblit.utils.StringUtils;
public class HttpHeaderAuthProvider extends AuthenticationProvider {
protected final Logger logger = LoggerFactory.getLogger(getClass());
protected String userHeaderName;
protected String teamHeaderName;
protected String teamHeaderSeparator;
public HttpHeaderAuthProvider() {
super("httpheader");
}
@Override
public void setup() {
// Load HTTP header configuration
userHeaderName = settings.getString(Keys.realm.httpheader.userheader, null);
teamHeaderName = settings.getString(Keys.realm.httpheader.teamheader, null);
teamHeaderSeparator = settings.getString(Keys.realm.httpheader.teamseparator, ",");
if (StringUtils.isEmpty(userHeaderName)) {
logger.warn("HTTP Header authentication is enabled, but no header is not defined in " + Keys.realm.httpheader.userheader);
}
}
@Override
public void stop() {}
@Override
public UserModel authenticate(HttpServletRequest httpRequest) {
// Try to authenticate using custom HTTP header if user header is defined
if (!StringUtils.isEmpty(userHeaderName)) {
String headerUserName = httpRequest.getHeader(userHeaderName);
if (!StringUtils.isEmpty(headerUserName) && !userManager.isInternalAccount(headerUserName)) {
// We have a user, try to load team names as well
Set<TeamModel> userTeams = new HashSet<>();
if (!StringUtils.isEmpty(teamHeaderName)) {
String headerTeamValue = httpRequest.getHeader(teamHeaderName);
if (!StringUtils.isEmpty(headerTeamValue)) {
String[] headerTeamNames = headerTeamValue.split(teamHeaderSeparator);
for (String teamName : headerTeamNames) {
teamName = teamName.trim();
if (!StringUtils.isEmpty(teamName)) {
TeamModel team = userManager.getTeamModel(teamName);
if (null == team) {
// Create teams here so they can marked with the correct AccountType
team = new TeamModel(teamName);
team.accountType = AccountType.HTTPHEADER;
updateTeam(team);
}
userTeams.add(team);
}
}
}
}
UserModel user = userManager.getUserModel(headerUserName);
if (user != null) {
// If team header is provided in request, reset all team memberships, even if resetting to empty set
if (!StringUtils.isEmpty(teamHeaderName)) {
user.teams.clear();
user.teams.addAll(userTeams);
}
updateUser(user);
return user;
} else if (settings.getBoolean(Keys.realm.httpheader.autoCreateAccounts, false)) {
// auto-create user from HTTP header
user = new UserModel(headerUserName.toLowerCase());
user.displayName = headerUserName;
user.password = Constants.EXTERNAL_ACCOUNT;
user.accountType = AccountType.HTTPHEADER;
user.teams.addAll(userTeams);
updateUser(user);
return user;
}
}
}
return null;
}
@Override
public UserModel authenticate(String username, char[] password){
// Username/password is not supported for HTTP header authentication
return null;
}
@Override
public AccountType getAccountType() {
return AccountType.HTTPHEADER;
}
@Override
public AuthenticationType getAuthenticationType() {
return AuthenticationType.HTTPHEADER;
}
@Override
public boolean supportsCredentialChanges() {
return false;
}
@Override
public boolean supportsDisplayNameChanges() {
return false;
}
@Override
public boolean supportsEmailAddressChanges() {
return false;
}
@Override
public boolean supportsTeamMembershipChanges() {
return StringUtils.isEmpty(teamHeaderName);
}
@Override
public boolean supportsRoleChanges(UserModel user, Role role) {
return true;
}
@Override
public boolean supportsRoleChanges(TeamModel team, Role role) {
return true;
}
}
package com.gitblit.wicket;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import org.apache.wicket.Session;
import org.apache.wicket.markup.html.form.TextField;
import org.apache.wicket.markup.html.form.AbstractTextComponent.ITextFormatProvider;
import org.apache.wicket.model.IModel;
import org.apache.wicket.util.convert.IConverter;
import org.apache.wicket.util.convert.converters.DateConverter;
public class Html5DateField extends TextField<Date> implements ITextFormatProvider {
private static final long serialVersionUID = 1L;
private static final String DEFAULT_PATTERN = "MM/dd/yyyy";
private String datePattern = null;
private IConverter converter = null;
/**
* Creates a new Html5DateField, without a specified pattern. This is the same as calling
* <code>new Html5DateField(id, Date.class)</code>
*
* @param id
* The id of the date field
*/
public Html5DateField(String id)
{
this(id, null, defaultDatePattern());
}
/**
* Creates a new Html5DateField, without a specified pattern. This is the same as calling
* <code>new Html5DateField(id, object, Date.class)</code>
*
* @param id
* The id of the date field
* @param model
* The model
*/
public Html5DateField(String id, IModel<Date> model)
{
this(id, model, defaultDatePattern());
}
/**
* Creates a new Html5DateField bound with a specific <code>SimpleDateFormat</code> pattern.
*
* @param id
* The id of the date field
* @param datePattern
* A <code>SimpleDateFormat</code> pattern
*
*/
public Html5DateField(String id, String datePattern)
{
this(id, null, datePattern);
}
/**
* Creates a new DateTextField bound with a specific <code>SimpleDateFormat</code> pattern.
*
* @param id
* The id of the date field
* @param model
* The model
* @param datePattern
* A <code>SimpleDateFormat</code> pattern
*/
public Html5DateField(String id, IModel<Date> model, String datePattern)
{
super(id, model, Date.class);
this.datePattern = datePattern;
converter = new DateConverter()
{
private static final long serialVersionUID = 1L;
/**
* @see org.apache.wicket.util.convert.converters.DateConverter#getDateFormat(java.util.Locale)
*/
@Override
public DateFormat getDateFormat(Locale locale)
{
if (locale == null)
{
locale = Locale.getDefault();
}
return new SimpleDateFormat(Html5DateField.this.datePattern, locale);
}
};
}
/**
* Returns the default converter if created without pattern; otherwise it returns a
* pattern-specific converter.
*
* @param type
* The type for which the converter should work
*
* @return A pattern-specific converter
*/
@Override
public IConverter getConverter(Class<?> type)
{
if (converter == null)
{
return super.getConverter(type);
}
else
{
return converter;
}
}
/**
* Returns the date pattern.
*
* @see org.apache.wicket.markup.html.form.AbstractTextComponent.ITextFormatProvider#getTextFormat()
*/
public String getTextFormat()
{
return datePattern;
}
/**
* Try to get datePattern from user session locale. If it is not possible, it will return
* {@link #DEFAULT_PATTERN}
*
* @return date pattern
*/
private static String defaultDatePattern()
{
Locale locale = Session.get().getLocale();
if (locale != null)
{
DateFormat format = DateFormat.getDateInstance(DateFormat.SHORT, locale);
if (format instanceof SimpleDateFormat)
{
return ((SimpleDateFormat)format).toPattern();
}
}
return DEFAULT_PATTERN;
}
@Override
protected String getInputType()
{
return "date";
}
}
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
xml:lang="en"
lang="en">
<!-- contribute editor resources to the page header -->
<wicket:head>
<link rel="stylesheet" href="gitblit-editor.min.css" />
<script type="text/javascript" src="gitblit-editor.min.js"></script>
</wicket:head>
<body>
<wicket:extend>
<div wicket:id="doc"></div>
<wicket:fragment wicket:id="markupContent">
<div class="docs" style="margin-top: -10px;">
<!-- doc nav links -->
<div style="float: right;position: relative;z-index: 100;margin-top: 1px;border-radius: 0px 3px 0px 3px;" class="docnav">
<a wicket:id="blameLink"><wicket:message key="gb.blame"></wicket:message></a> | <a wicket:id="historyLink"><wicket:message key="gb.history"></wicket:message></a> | <a wicket:id="rawLink"><wicket:message key="gb.raw"></wicket:message></a>
</div>
<div id="visualEditor"></div>
<form id="documentEditor" style="padding-top:5px;" wicket:id="documentEditor">
<textarea id="editor" wicket:id="content">[content]</textarea>
<div id="commitDialog" class="modal hide fade">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h3>Commit Document Changes</h3>
</div>
<div class="modal-body">
<div wicket:id="commitAuthor"></div>
<textarea style="width:100%; resize:none" wicket:id="commitMessage"></textarea>
</div>
<div class="modal-footer">
<a href="#" data-dismiss="modal" class="btn"><wicket:message key="gb.continueEditing"></wicket:message></a>
<a href="#" onclick="commitChanges()" class="btn btn-primary"><wicket:message key="gb.commitChanges"></wicket:message></a>
</div>
</div>
</form>
</div>
</wicket:fragment>
<wicket:fragment wicket:id="plainContent">
<div class="docs">
<!-- doc nav links -->
<div style="float: right;" class="docnav">
<a wicket:id="blameLink"><wicket:message key="gb.blame"></wicket:message></a> | <a wicket:id="historyLink"><wicket:message key="gb.history"></wicket:message></a> | <a wicket:id="rawLink"><wicket:message key="gb.raw"></wicket:message></a>
</div>
<!-- document content -->
<div wicket:id="content">[content]</div>
</div>
</wicket:fragment>
</wicket:extend>
</body>
</html>
/*
* 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.wicket.pages;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.apache.wicket.PageParameters;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.form.TextArea;
import org.apache.wicket.markup.html.link.BookmarkablePageLink;
import org.apache.wicket.markup.html.link.ExternalLink;
import org.apache.wicket.markup.html.panel.Fragment;
import org.apache.wicket.model.Model;
import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
import org.eclipse.jgit.dircache.DirCache;
import org.eclipse.jgit.dircache.DirCacheBuilder;
import org.eclipse.jgit.dircache.DirCacheEntry;
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import com.gitblit.Constants;
import com.gitblit.models.UserModel;
import com.gitblit.servlet.RawServlet;
import com.gitblit.utils.BugtraqProcessor;
import com.gitblit.utils.JGitUtils;
import com.gitblit.utils.StringUtils;
import com.gitblit.wicket.CacheControl;
import com.gitblit.wicket.GitBlitWebSession;
import com.gitblit.wicket.CacheControl.LastModified;
import com.gitblit.wicket.MarkupProcessor;
import com.gitblit.wicket.MarkupProcessor.MarkupDocument;
import com.gitblit.wicket.WicketUtils;
@CacheControl(LastModified.REPOSITORY)
public class EditFilePage extends RepositoryPage {
public EditFilePage(final PageParameters params) {
super(params);
final UserModel currentUser = (GitBlitWebSession.get().getUser() != null) ? GitBlitWebSession.get().getUser() : UserModel.ANONYMOUS;
final String path = WicketUtils.getPath(params).replace("%2f", "/").replace("%2F", "/");
MarkupProcessor processor = new MarkupProcessor(app().settings(), app().xssFilter());
Repository r = getRepository();
RevCommit commit = JGitUtils.getCommit(r, objectId);
String [] encodings = getEncodings();
// Read raw markup content and transform it to html
String documentPath = path;
String markupText = JGitUtils.getStringContent(r, commit.getTree(), path, encodings);
// Hunt for document
if (StringUtils.isEmpty(markupText)) {
String name = StringUtils.stripFileExtension(path);
List<String> docExtensions = processor.getAllExtensions();
for (String ext : docExtensions) {
String checkName = name + "." + ext;
markupText = JGitUtils.getStringContent(r, commit.getTree(), checkName, encodings);
if (!StringUtils.isEmpty(markupText)) {
// found it
documentPath = path;
break;
}
}
}
if (markupText == null) {
markupText = "";
}
BugtraqProcessor bugtraq = new BugtraqProcessor(app().settings());
markupText = bugtraq.processText(getRepository(), repositoryName, markupText);
Fragment fragment;
String displayedCommitId = commit.getId().getName();
if (currentUser.canEdit(getRepositoryModel()) && JGitUtils.isTip(getRepository(), objectId.toString())) {
final Model<String> documentContent = new Model<String>(markupText);
final Model<String> commitMessage = new Model<String>("Document update");
final Model<String> commitIdAtLoad = new Model<String>(displayedCommitId);
fragment = new Fragment("doc", "markupContent", this);
Form<Void> form = new Form<Void>("documentEditor") {
private static final long serialVersionUID = 1L;
@Override
protected void onSubmit() {
final Repository repository = getRepository();
final String document = documentContent.getObject();
final String message = commitMessage.getObject();
final String branchName = JGitUtils.getBranch(getRepository(), objectId).getName();
final String authorEmail = StringUtils.isEmpty(currentUser.emailAddress) ? (currentUser.username + "@gitblit") : currentUser.emailAddress;
boolean success = false;
try {
ObjectId docAtLoad = getRepository().resolve(commitIdAtLoad.getObject());
logger.trace("Commiting Edit File page: " + commitIdAtLoad.getObject());
DirCache index = DirCache.newInCore();
DirCacheBuilder builder = index.builder();
byte[] bytes = document.getBytes( Constants.ENCODING );
final DirCacheEntry fileUpdate = new DirCacheEntry(path);
fileUpdate.setLength(bytes.length);
fileUpdate.setLastModified(System.currentTimeMillis());
fileUpdate.setFileMode(FileMode.REGULAR_FILE);
fileUpdate.setObjectId(repository.newObjectInserter().insert( org.eclipse.jgit.lib.Constants.OBJ_BLOB, bytes ));
builder.add(fileUpdate);
Set<String> ignorePaths = new HashSet<String>();
ignorePaths.add(path);
for (DirCacheEntry entry : JGitUtils.getTreeEntries(repository, branchName, ignorePaths)) {
builder.add(entry);
}
builder.finish();
final boolean forceCommit = false;
success = JGitUtils.commitIndex(repository, branchName, index, docAtLoad, forceCommit, currentUser.getDisplayName(), authorEmail, message);
} catch (IOException | ConcurrentRefUpdateException e) {
e.printStackTrace();
}
if (success == false) {
getSession().error(MessageFormat.format(getString("gb.fileNotMergeable"),path));
return;
}
getSession().info(MessageFormat.format(getString("gb.fileCommitted"),path));
setResponsePage(EditFilePage.class, params);
}
};
final TextArea<String> docIO = new TextArea<String>("content", documentContent);
docIO.setOutputMarkupId(false);
form.add(new Label("commitAuthor", String.format("%s <%s>", currentUser.getDisplayName(), currentUser.emailAddress)));
form.add(new TextArea<String>("commitMessage", commitMessage));
form.setOutputMarkupId(false);
form.add(docIO);
addBottomScriptInline("attachDocumentEditor(document.querySelector('textarea#editor'), $('#commitDialog'));");
fragment.add(form);
} else {
MarkupDocument markupDoc = processor.parse(repositoryName, displayedCommitId, documentPath, markupText);
final Model<String> documentContent = new Model<String>(markupDoc.html);
fragment = new Fragment("doc", "plainContent", this);
fragment.add(new Label("content", documentContent).setEscapeModelStrings(false));
}
// document page links
fragment.add(new BookmarkablePageLink<Void>("blameLink", BlamePage.class,
WicketUtils.newPathParameter(repositoryName, objectId, documentPath)));
fragment.add(new BookmarkablePageLink<Void>("historyLink", HistoryPage.class,
WicketUtils.newPathParameter(repositoryName, objectId, documentPath)));
String rawUrl = RawServlet.asLink(getContextUrl(), repositoryName, objectId, documentPath);
fragment.add(new ExternalLink("rawLink", rawUrl));
add(fragment);
}
@Override
protected String getPageName() {
return getString("gb.editFile");
}
@Override
protected boolean isCommitPage() {
return true;
}
@Override
protected Class<? extends BasePage> getRepoNavPageClass() {
return EditFilePage.class;
}
}
//This provides a basic patch/hack to allow Wicket 1.4 to support HTML5 input types
Wicket.Form.serializeInput_original = Wicket.Form.serializeInput;
Wicket.Form.serializeInput = function(input)
{
if (input.type.toLowerCase() == "date")
{
return Wicket.Form.encode(input.name) + "=" + Wicket.Form.encode(input.value) + "&";
}
return Wicket.Form.serializeInput_original(input);
}
.ProseMirror-menubar {
border-top-left-radius: inherit;
border-top-right-radius: inherit;
color: #666;
padding: 1px 6px;
border-bottom: 1px solid silver;
background: white;
z-index: 10;
-moz-box-sizing: border-box;
box-sizing: border-box;
overflow: visible;
}
.ProseMirror-menubar.scrolling {
top: 47px;
}
.ProseMirror-menuitem {
display: inline-block;
font-size: 1.2em;
text-align: center;
min-width: 30px;
line-height: 30px;
box-sizing: border-box;
margin: 1px;
border-width: 1px;
border-style: solid;
border-color: white;
}
.ProseMirror-menuitem:hover {
border-radius: 5px;
background: #fcfcfc;
border-color: #95a5a6;
border-width: 1px;
border-style: solid;
box-sizing: border-box;
}
.ProseMirror-icon {
line-height: inherit;
vertical-align: middle;
}
.ProseMirror-menubar .fa-header-x:after {
font-family: Arial, Helvetica, sans-serif;
font-size: 65%;
vertical-align: text-bottom;
position: relative;
top: 2px;
}
.fa-header-1:after {
content: "1";
}
.fa-header-2:after {
content: "2";
}
.fa-header-3:after {
content: "3";
}
.forceHide {
display:none !important;
}
attachDocumentEditor = function (editorElement, commitDialogElement)
{
var edit = require("./prosemirror/dist/edit")
require("./prosemirror/dist/inputrules/autoinput")
require("./prosemirror/dist/menu/menubar")
require("./prosemirror/dist/markdown")
var _menu = require("./prosemirror/dist/menu/menu")
var content = document.querySelector('#editor');
content.style.display = "none";
var gitblitCommands = new _menu.MenuCommandGroup("gitblitCommands");
var viewCommands = new _menu.MenuCommandGroup("viewCommands");
var textCommands = new _menu.MenuCommandGroup("textCommands");
var insertCommands = new _menu.MenuCommandGroup("insertCommands");
var menuItems = [gitblitCommands, viewCommands, textCommands, _menu.inlineGroup, _menu.blockGroup, _menu.historyGroup, insertCommands];
const updateCmd = Object.create(null);
updateCmd["GitblitCommit"] = {
label: "GitblitCommit",
run: function() {
commitDialogElement.modal({show:true});
editorElement.value = pm.getContent('markdown');
},
menu: {
group: "gitblitCommands", rank: 10,
display: {
render: function(cmd, pm) { return renderFontAwesomeIcon(cmd, pm, "fa-save"); }
}
}
};
updateCmd["FullScreen"] = {
label: "Toggle Fullscreen",
derive: "toggle",
run: function(pm) {
//Maintain the scroll context
var initialScroll = window.scrollY;
var navs = [document.querySelector("div.repositorynavbar"), document.querySelector("div.navbar"), document.querySelector("div.docnav")];
var offset = navs.reduce(function(p, c) { return p + c.offsetHeight; }, 0);
navs.forEach(function(e) { e.classList.toggle("forceHide"); });
if (!toggleFullScreen(document.documentElement)) {
offset = 60;
} else {
offset -= 60;
}
pm.signal("commandsChanged");
//Browsers don't seem to accept a scrollTo straight after a full screen
setTimeout(function(){window.scrollTo(0, Math.max(0,initialScroll - offset));}, 100);
},
menu: {
group: "viewCommands", rank: 11,
display: {
render: function(cmd, pm) { return renderFontAwesomeIcon(cmd, pm, "fa-arrows-alt"); }
}
},
active: function active(pm) { return getFullScreenElement() ? true : false; }
};
updateCmd["heading1"] = {
derive: "toggle",
run: function(pm) {
var selection = pm.selection;
var from = selection.from;
var to = selection.to;
var attr = {name:"make", level:"1"};
var node = pm.doc.resolve(from).parent;
if (node && node.hasMarkup(pm.schema.nodes.heading, attr)) {
return pm.tr.setBlockType(from, to, pm.schema.defaultTextblockType(), {}).apply(pm.apply.scroll);
} else {
return pm.tr.setBlockType(from, to, pm.schema.nodes.heading, attr).apply(pm.apply.scroll);
}
},
active: function active(pm) {
var node = pm.doc.resolve(pm.selection.from).parent;
if (node && node.hasMarkup(pm.schema.nodes.heading, {name:"make", level:"1"})) {
return true;
}
return false;
},
menu: {
group: "textCommands", rank: 1,
display: {
render: function(cmd, pm) { return renderFontAwesomeIcon(cmd, pm, "fa-header fa-header-x fa-header-1"); }
},
},
select: function(){return true;}
};
updateCmd["heading2"] = {
derive: "toggle",
run: function(pm) {
var selection = pm.selection;
var from = selection.from;
var to = selection.to;
var attr = {name:"make", level:"2"};
var node = pm.doc.resolve(from).parent;
if (node && node.hasMarkup(pm.schema.nodes.heading, attr)) {
return pm.tr.setBlockType(from, to, pm.schema.defaultTextblockType(), {}).apply(pm.apply.scroll);
} else {
return pm.tr.setBlockType(from, to, pm.schema.nodes.heading, attr).apply(pm.apply.scroll);
}
},
active: function active(pm) {
var node = pm.doc.resolve(pm.selection.from).parent;
if (node && node.hasMarkup(pm.schema.nodes.heading, {name:"make", level:"2"})) {
return true;
}
return false;
},
menu: {
group: "textCommands", rank: 2,
display: {
render: function(cmd, pm) { return renderFontAwesomeIcon(cmd, pm, "fa-header fa-header-x fa-header-2"); }
},
},
select: function(){return true;}
};
updateCmd["heading3"] = {
derive: "toggle",
run: function(pm) {
var selection = pm.selection;
var from = selection.from;
var to = selection.to;
var attr = {name:"make", level:"3"};
var node = pm.doc.resolve(from).parent;
if (node && node.hasMarkup(pm.schema.nodes.heading, attr)) {
return pm.tr.setBlockType(from, to, pm.schema.defaultTextblockType(), {}).apply(pm.apply.scroll);
} else {
return pm.tr.setBlockType(from, to, pm.schema.nodes.heading, attr).apply(pm.apply.scroll);
}
},
active: function active(pm) {
var node = pm.doc.resolve(pm.selection.from).parent;
if (node && node.hasMarkup(pm.schema.nodes.heading, {name:"make", level:"3"})) {
return true;
}
return false;
},
menu: {
group: "textCommands", rank: 3,
display: {
render: function(cmd, pm) { return renderFontAwesomeIcon(cmd, pm, "fa-header fa-header-x fa-header-3"); }
},
},
select: function(){return true;}
};
updateCmd["strong:toggle"] = {
menu: {
group: "textCommands", rank: 4,
display: {
render: function(cmd, pm) { return renderFontAwesomeIcon(cmd, pm, "fa-bold"); }
}
},
select: function(){return true;}
};
updateCmd["em:toggle"] = {
menu: {
group: "textCommands", rank: 5,
display: {
render: function(cmd, pm) { return renderFontAwesomeIcon(cmd, pm, "fa-italic"); }
}
},
select: function(){return true;}
};
updateCmd["code:toggle"] = {
menu: {
group: "textCommands", rank: 6,
display: {
render: function(cmd, pm) { return renderFontAwesomeIcon(cmd, pm, "fa-code"); }
}
},
select: function(){return true;}
};
updateCmd["image:insert"] = {
menu: {
group: "insertCommands", rank: 1,
display: {
render: function(cmd, pm) { return renderFontAwesomeIcon(cmd, pm, "fa-picture-o"); }
}
}
};
updateCmd["selectParentNode"] = {
menu: {
group: "insertCommands", rank: 10,
display: {
render: function(cmd, pm) { return renderFontAwesomeIcon(cmd, pm, "fa-arrow-circle-o-left"); }
}
}
};
var pm = window.pm = new edit.ProseMirror({
place: document.querySelector('#visualEditor'),
autoInput: true,
doc: content.value,
menuBar: { float:true, content: menuItems},
commands: edit.CommandSet.default.update(updateCmd),
docFormat: "markdown"
});
var scrollStart = document.querySelector(".ProseMirror").offsetTop;
var ticking = false;
window.addEventListener("scroll", function() {
var scrollPosition = window.scrollY;
if (!ticking) {
window.requestAnimationFrame(function() {
if (!getFullScreenElement() && (scrollPosition > scrollStart)) {
document.querySelector(".ProseMirror-menubar").classList.add("scrolling");
} else {
document.querySelector(".ProseMirror-menubar").classList.remove("scrolling");
}
ticking = false;
});
}
ticking = true;
});
}
function renderFontAwesomeIcon(cmd, pm, classNames) {
var node = document.createElement("div");
node.className = "ProseMirror-icon";
var icon = document.createElement("i");
icon.setAttribute("class", "fa fa-fw " + classNames);
var active = cmd.active(pm);
if (active || cmd.spec.invert) node.classList.add("ProseMirror-menu-active");
node.appendChild(icon);
return node;
}
function getFullScreenElement() {
return document.fullscreenElement || document.mozFullScreenElement || document.webkitFullscreenElement || document.msFullscreenElement;
}
function toggleFullScreen(e) {
if (getFullScreenElement()) {
if (document.exitFullscreen) { document.exitFullscreen(); }
else if (document.msExitFullscreen) { document.msExitFullscreen(); }
else if (document.mozCancelFullScreen) { document.mozCancelFullScreen(); }
else if (document.webkitExitFullscreen) { document.webkitExitFullscreen(); }
return true;
} else {
if (e.requestFullscreen) { e.requestFullscreen(); }
else if (e.msRequestFullscreen) { e.msRequestFullscreen(); }
else if (e.mozRequestFullScreen) { e.mozRequestFullScreen(); }
else if (e.webkitRequestFullscreen) { e.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT); }
}
return false;
}
commitChanges = function() {
document.querySelector('form#documentEditor').submit();
}
{
"name": "gitblit",
"homepage": "http://gitblit.com/",
"version": "1.0.0",
"devDependencies": {
"babel-cli": "^6.4.5",
"babel-preset-es2015": "^6.3.13",
"babelify": "*",
"browserify": "^11.2.0",
"browserify-shim": "^3.8.10",
"suitcss-preprocessor": "^0.8.0",
"uglify-js": "^2.6.1"
},
"scripts": {
"build": "npm run build:js-min & npm run build:css-min",
"build:debug": "npm run build:js & npm run build:css",
"build:js": "browserify editor.dev.js > ../resources/gitblit-editor.js",
"build:js-min": "browserify editor.dev.js | uglifyjs --compress --mangle -o ../resources/gitblit-editor.min.js",
"build:css": "suitcss editor.dev.css ../resources/gitblit-editor.css",
"build:css-min": "suitcss -m editor.dev.css ../resources/gitblit-editor.min.css",
"postinstall": "cd prosemirror && npm install"
}
}

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

.ProseMirror-menubar{border-top-left-radius:inherit;border-top-right-radius:inherit;color:#666;padding:1px 6px;border-bottom:1px solid silver;background:#fff;z-index:10;box-sizing:border-box;overflow:visible}.ProseMirror-menubar.scrolling{top:47px}.ProseMirror-menuitem{display:inline-block;font-size:1.2em;text-align:center;min-width:30px;line-height:30px;box-sizing:border-box;margin:1px;border:1px solid #fff}.ProseMirror-menuitem:hover{border-radius:5px;background:#fcfcfc;border:1px solid #95a5a6;box-sizing:border-box}.ProseMirror-icon{line-height:inherit;vertical-align:middle}.ProseMirror-menubar .fa-header-x:after{font-family:Arial,Helvetica,sans-serif;font-size:65%;vertical-align:text-bottom;position:relative;top:2px}.fa-header-1:after{content:"1"}.fa-header-2:after{content:"2"}.fa-header-3:after{content:"3"}.forceHide{display:none!important}

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

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.tests;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
import java.text.MessageFormat;
import java.util.Date;
import java.util.List;
import org.eclipse.jgit.api.CloneCommand;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.MergeResult;
import org.eclipse.jgit.api.MergeCommand.FastForwardMode;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.transport.PushResult;
import org.eclipse.jgit.transport.RefSpec;
import org.eclipse.jgit.transport.RemoteRefUpdate;
import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
import org.eclipse.jgit.transport.RemoteRefUpdate.Status;
import org.eclipse.jgit.util.FileUtils;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import com.gitblit.Constants.AccessPermission;
import com.gitblit.Constants.AccessRestrictionType;
import com.gitblit.Constants.AuthorizationControl;
import com.gitblit.GitBlitException;
import com.gitblit.models.RepositoryModel;
import com.gitblit.models.TicketModel;
import com.gitblit.models.UserModel;
import com.gitblit.models.TicketModel.Change;
import com.gitblit.models.TicketModel.Field;
import com.gitblit.models.TicketModel.Reference;
import com.gitblit.tickets.ITicketService;
/**
* Creates and deletes a range of ticket references via ticket comments and commits
*/
public class TicketReferenceTest extends GitblitUnitTest {
static File workingCopy = new File(GitBlitSuite.REPOSITORIES, "working/TicketReferenceTest.git-wc");
static ITicketService ticketService;
static final String account = "TicketRefTest";
static final String password = GitBlitSuite.password;
static final String url = GitBlitSuite.gitServletUrl;
static UserModel user = null;
static RepositoryModel repo = null;
static CredentialsProvider cp = null;
static Git git = null;
@BeforeClass
public static void configure() throws Exception {
File repositoryName = new File("TicketReferenceTest.git");;
GitBlitSuite.close(repositoryName);
if (repositoryName.exists()) {
FileUtils.delete(repositoryName, FileUtils.RECURSIVE | FileUtils.RETRY);
}
repo = new RepositoryModel("TicketReferenceTest.git", null, null, null);
if (gitblit().hasRepository(repo.name)) {
gitblit().deleteRepositoryModel(repo);
}
gitblit().updateRepositoryModel(repo.name, repo, true);
user = new UserModel(account);
user.displayName = account;
user.emailAddress = account + "@example.com";
user.password = password;
cp = new UsernamePasswordCredentialsProvider(user.username, user.password);
if (gitblit().getUserModel(user.username) != null) {
gitblit().deleteUser(user.username);
}
repo.authorizationControl = AuthorizationControl.NAMED;
repo.accessRestriction = AccessRestrictionType.PUSH;
gitblit().updateRepositoryModel(repo.name, repo, false);
// grant user push permission
user.setRepositoryPermission(repo.name, AccessPermission.REWIND);
gitblit().updateUserModel(user);
ticketService = gitblit().getTicketService();
assertTrue(ticketService.deleteAll(repo));
GitBlitSuite.close(workingCopy);
if (workingCopy.exists()) {
FileUtils.delete(workingCopy, FileUtils.RECURSIVE | FileUtils.RETRY);
}
CloneCommand clone = Git.cloneRepository();
clone.setURI(MessageFormat.format("{0}/{1}", url, repo.name));
clone.setDirectory(workingCopy);
clone.setBare(false);
clone.setBranch("master");
clone.setCredentialsProvider(cp);
GitBlitSuite.close(clone.call());
git = Git.open(workingCopy);
git.getRepository().getConfig().setString("user", null, "name", user.displayName);
git.getRepository().getConfig().setString("user", null, "email", user.emailAddress);
git.getRepository().getConfig().save();
final RevCommit revCommit1 = makeCommit("initial commit");
final String initialSha = revCommit1.name();
Iterable<PushResult> results = git.push().setPushAll().setCredentialsProvider(cp).call();
GitBlitSuite.close(git);
for (PushResult result : results) {
for (RemoteRefUpdate update : result.getRemoteUpdates()) {
assertEquals(Status.OK, update.getStatus());
assertEquals(initialSha, update.getNewObjectId().name());
}
}
}
@AfterClass
public static void cleanup() throws Exception {
GitBlitSuite.close(git);
}
@Test
public void noReferencesOnTicketCreation() throws Exception {
TicketModel a = ticketService.createTicket(repo, newTicket("noReferencesOnCreation"));
assertNotNull(a);
assertFalse(a.hasReferences());
//Ensure retrieval process doesn't affect anything
a = ticketService.getTicket(repo, a.number);
assertNotNull(a);
assertFalse(a.hasReferences());
}
@Test
public void commentNoUnexpectedReference() throws Exception {
TicketModel a = ticketService.createTicket(repo, newTicket("commentNoUnexpectedReference-A"));
TicketModel b = ticketService.createTicket(repo, newTicket("commentNoUnexpectedReference-B"));
assertNotNull(ticketService.updateTicket(repo, a.number, newComment("comment for 1 - no reference")));
assertNotNull(ticketService.updateTicket(repo, a.number, newComment("comment for # - no reference")));
assertNotNull(ticketService.updateTicket(repo, a.number, newComment("comment for #999 - ignores invalid reference")));
a = ticketService.getTicket(repo, a.number);
b = ticketService.getTicket(repo, b.number);
assertFalse(a.hasReferences());
assertFalse(b.hasReferences());
}
@Test
public void commentNoSelfReference() throws Exception {
TicketModel a = ticketService.createTicket(repo, newTicket("commentNoSelfReference-A"));
final Change comment = newComment(String.format("comment for #%d - no self reference", a.number));
assertNotNull(ticketService.updateTicket(repo, a.number, comment));
a = ticketService.getTicket(repo, a.number);
assertFalse(a.hasReferences());
}
@Test
public void commentSingleReference() throws Exception {
TicketModel a = ticketService.createTicket(repo, newTicket("commentSingleReference-A"));
TicketModel b = ticketService.createTicket(repo, newTicket("commentSingleReference-B"));
final Change comment = newComment(String.format("comment for #%d - single reference", b.number));
assertNotNull(ticketService.updateTicket(repo, a.number, comment));
a = ticketService.getTicket(repo, a.number);
b = ticketService.getTicket(repo, b.number);
assertFalse(a.hasReferences());
assertTrue(b.hasReferences());
List<Reference> cRefB = b.getReferences();
assertNotNull(cRefB);
assertEquals(1, cRefB.size());
assertEquals(a.number, cRefB.get(0).ticketId.longValue());
assertEquals(comment.comment.id, cRefB.get(0).hash);
}
@Test
public void commentSelfAndOtherReference() throws Exception {
TicketModel a = ticketService.createTicket(repo, newTicket("commentSelfAndOtherReference-A"));
TicketModel b = ticketService.createTicket(repo, newTicket("commentSelfAndOtherReference-B"));
final Change comment = newComment(String.format("comment for #%d and #%d - self and other reference", a.number, b.number));
assertNotNull(ticketService.updateTicket(repo, a.number, comment));
a = ticketService.getTicket(repo, a.number);
b = ticketService.getTicket(repo, b.number);
assertFalse(a.hasReferences());
assertTrue(b.hasReferences());
List<Reference> cRefB = b.getReferences();
assertNotNull(cRefB);
assertEquals(1, cRefB.size());
assertEquals(a.number, cRefB.get(0).ticketId.longValue());
assertEquals(comment.comment.id, cRefB.get(0).hash);
}
@Test
public void commentMultiReference() throws Exception {
TicketModel a = ticketService.createTicket(repo, newTicket("commentMultiReference-A"));
TicketModel b = ticketService.createTicket(repo, newTicket("commentMultiReference-B"));
TicketModel c = ticketService.createTicket(repo, newTicket("commentMultiReference-C"));
final Change comment = newComment(String.format("comment for #%d and #%d - multi reference", b.number, c.number));
assertNotNull(ticketService.updateTicket(repo, a.number, comment));
a = ticketService.getTicket(repo, a.number);
b = ticketService.getTicket(repo, b.number);
c = ticketService.getTicket(repo, c.number);
assertFalse(a.hasReferences());
assertTrue(b.hasReferences());
assertTrue(c.hasReferences());
List<Reference> cRefB = b.getReferences();
assertNotNull(cRefB);
assertEquals(1, cRefB.size());
assertEquals(a.number, cRefB.get(0).ticketId.longValue());
assertEquals(comment.comment.id, cRefB.get(0).hash);
List<Reference> cRefC = c.getReferences();
assertNotNull(cRefC);
assertEquals(1, cRefC.size());
assertEquals(a.number, cRefC.get(0).ticketId.longValue());
assertEquals(comment.comment.id, cRefC.get(0).hash);
}
@Test
public void commitMasterNoUnexpectedReference() throws Exception {
TicketModel a = ticketService.createTicket(repo, newTicket("commentMultiReference-A"));
final String branchName = "master";
git.checkout().setCreateBranch(false).setName(branchName).call();
makeCommit("commit for 1 - no reference");
makeCommit("comment for # - no reference");
final RevCommit revCommit1 = makeCommit("comment for #999 - ignores invalid reference");
final String commit1Sha = revCommit1.name();
assertPushSuccess(commit1Sha, branchName);
a = ticketService.getTicket(repo, a.number);
assertFalse(a.hasReferences());
}
@Test
public void commitMasterSingleReference() throws Exception {
TicketModel a = ticketService.createTicket(repo, newTicket("commitMasterSingleReference-A"));
final String branchName = "master";
git.checkout().setCreateBranch(false).setName(branchName).call();
final String message = String.format("commit for #%d - single reference", a.number);
final RevCommit revCommit1 = makeCommit(message);
final String commit1Sha = revCommit1.name();
assertPushSuccess(commit1Sha, branchName);
a = ticketService.getTicket(repo, a.number);
assertTrue(a.hasReferences());
List<Reference> cRefA = a.getReferences();
assertNotNull(cRefA);
assertEquals(1, cRefA.size());
assertNull(cRefA.get(0).ticketId);
assertEquals(commit1Sha, cRefA.get(0).hash);
}
@Test
public void commitMasterMultiReference() throws Exception {
TicketModel a = ticketService.createTicket(repo, newTicket("commitMasterMultiReference-A"));
TicketModel b = ticketService.createTicket(repo, newTicket("commitMasterMultiReference-B"));
final String branchName = "master";
git.checkout().setCreateBranch(false).setName(branchName).call();
final String message = String.format("commit for #%d and #%d - multi reference", a.number, b.number);
final RevCommit revCommit1 = makeCommit(message);
final String commit1Sha = revCommit1.name();
assertPushSuccess(commit1Sha, branchName);
a = ticketService.getTicket(repo, a.number);
b = ticketService.getTicket(repo, b.number);
assertTrue(a.hasReferences());
assertTrue(b.hasReferences());
List<Reference> cRefA = a.getReferences();
assertNotNull(cRefA);
assertEquals(1, cRefA.size());
assertNull(cRefA.get(0).ticketId);
assertEquals(commit1Sha, cRefA.get(0).hash);
List<Reference> cRefB = a.getReferences();
assertNotNull(cRefB);
assertEquals(1, cRefB.size());
assertNull(cRefB.get(0).ticketId);
assertEquals(commit1Sha, cRefB.get(0).hash);
}
@Test
public void commitMasterAmendReference() throws Exception {
TicketModel a = ticketService.createTicket(repo, newTicket("commitMasterAmendReference-A"));
TicketModel b = ticketService.createTicket(repo, newTicket("commitMasterAmendReference-B"));
final String branchName = "master";
git.checkout().setCreateBranch(false).setName(branchName).call();
String message = String.format("commit before amend for #%d and #%d", a.number, b.number);
final RevCommit revCommit1 = makeCommit(message);
final String commit1Sha = revCommit1.name();
assertPushSuccess(commit1Sha, branchName);
a = ticketService.getTicket(repo, a.number);
b = ticketService.getTicket(repo, b.number);
assertTrue(a.hasReferences());
assertTrue(b.hasReferences());
List<Reference> cRefA = a.getReferences();
assertNotNull(cRefA);
assertEquals(1, cRefA.size());
assertNull(cRefA.get(0).ticketId);
assertEquals(commit1Sha, cRefA.get(0).hash);
List<Reference> cRefB = b.getReferences();
assertNotNull(cRefB);
assertEquals(1, cRefB.size());
assertNull(cRefB.get(0).ticketId);
assertEquals(commit1Sha, cRefB.get(0).hash);
//Confirm that old invalid references removed for both tickets
//and new reference added for one referenced ticket
message = String.format("commit after amend for #%d", a.number);
final String commit2Sha = amendCommit(message);
assertForcePushSuccess(commit2Sha, branchName);
a = ticketService.getTicket(repo, a.number);
b = ticketService.getTicket(repo, b.number);
assertTrue(a.hasReferences());
assertFalse(b.hasReferences());
cRefA = a.getReferences();
assertNotNull(cRefA);
assertEquals(1, cRefA.size());
assertNull(cRefA.get(0).ticketId);
assertEquals(commit2Sha, cRefA.get(0).hash);
}
@Test
public void commitPatchsetNoUnexpectedReference() throws Exception {
setPatchsetAvailable(true);
TicketModel a = ticketService.createTicket(repo, newTicket("commitPatchsetNoUnexpectedReference-A"));
String branchName = String.format("ticket/%d", a.number);
git.checkout().setCreateBranch(true).setName(branchName).call();
makeCommit("commit for 1 - no reference");
makeCommit("commit for # - no reference");
final String message = "commit for #999 - ignores invalid reference";
final RevCommit revCommit1 = makeCommit(message);
final String commit1Sha = revCommit1.name();
assertPushSuccess(commit1Sha, branchName);
a = ticketService.getTicket(repo, a.number);
assertFalse(a.hasReferences());
}
@Test
public void commitPatchsetNoSelfReference() throws Exception {
setPatchsetAvailable(true);
TicketModel a = ticketService.createTicket(repo, newTicket("commitPatchsetNoSelfReference-A"));
String branchName = String.format("ticket/%d", a.number);
git.checkout().setCreateBranch(true).setName(branchName).call();
final String message = String.format("commit for #%d - patchset self reference", a.number);
final RevCommit revCommit1 = makeCommit(message);
final String commit1Sha = revCommit1.name();
assertPushSuccess(commit1Sha, branchName);
a = ticketService.getTicket(repo, a.number);
assertFalse(a.hasReferences());
}
@Test
public void commitPatchsetSingleReference() throws Exception {
setPatchsetAvailable(true);
TicketModel a = ticketService.createTicket(repo, newTicket("commitPatchsetSingleReference-A"));
TicketModel b = ticketService.createTicket(repo, newTicket("commitPatchsetSingleReference-B"));
String branchName = String.format("ticket/%d", a.number);
git.checkout().setCreateBranch(true).setName(branchName).call();
final String message = String.format("commit for #%d - patchset single reference", b.number);
final RevCommit revCommit1 = makeCommit(message);
final String commit1Sha = revCommit1.name();
assertPushSuccess(commit1Sha, branchName);
a = ticketService.getTicket(repo, a.number);
b = ticketService.getTicket(repo, b.number);
assertFalse(a.hasReferences());
assertTrue(b.hasReferences());
List<Reference> cRefB = b.getReferences();
assertNotNull(cRefB);
assertEquals(1, cRefB.size());
assertNull(cRefB.get(0).ticketId);
assertEquals(commit1Sha, cRefB.get(0).hash);
}
@Test
public void commitPatchsetMultiReference() throws Exception {
setPatchsetAvailable(true);
TicketModel a = ticketService.createTicket(repo, newTicket("commitPatchsetMultiReference-A"));
TicketModel b = ticketService.createTicket(repo, newTicket("commitPatchsetMultiReference-B"));
TicketModel c = ticketService.createTicket(repo, newTicket("commitPatchsetMultiReference-C"));
String branchName = String.format("ticket/%d", a.number);
git.checkout().setCreateBranch(true).setName(branchName).call();
final String message = String.format("commit for #%d and #%d- patchset multi reference", b.number, c.number);
final RevCommit revCommit1 = makeCommit(message);
final String commit1Sha = revCommit1.name();
assertPushSuccess(commit1Sha, branchName);
a = ticketService.getTicket(repo, a.number);
b = ticketService.getTicket(repo, b.number);
c = ticketService.getTicket(repo, c.number);
assertFalse(a.hasReferences());
assertTrue(b.hasReferences());
assertTrue(c.hasReferences());
List<Reference> cRefB = b.getReferences();
assertNotNull(cRefB);
assertEquals(1, cRefB.size());
assertNull(cRefB.get(0).ticketId);
assertEquals(commit1Sha, cRefB.get(0).hash);
List<Reference> cRefC = c.getReferences();
assertNotNull(cRefC);
assertEquals(1, cRefC.size());
assertNull(cRefC.get(0).ticketId);
assertEquals(commit1Sha, cRefC.get(0).hash);
}
@Test
public void commitPatchsetAmendReference() throws Exception {
setPatchsetAvailable(true);
TicketModel a = ticketService.createTicket(repo, newTicket("commitPatchsetAmendReference-A"));
TicketModel b = ticketService.createTicket(repo, newTicket("commitPatchsetAmendReference-B"));
TicketModel c = ticketService.createTicket(repo, newTicket("commitPatchsetAmendReference-C"));
assertFalse(c.hasPatchsets());
String branchName = String.format("ticket/%d", c.number);
git.checkout().setCreateBranch(true).setName(branchName).call();
String message = String.format("commit before amend for #%d and #%d", a.number, b.number);
final RevCommit revCommit1 = makeCommit(message);
final String commit1Sha = revCommit1.name();
assertPushSuccess(commit1Sha, branchName);
a = ticketService.getTicket(repo, a.number);
b = ticketService.getTicket(repo, b.number);
c = ticketService.getTicket(repo, c.number);
assertTrue(a.hasReferences());
assertTrue(b.hasReferences());
assertFalse(c.hasReferences());
assertTrue(c.hasPatchsets());
assertNotNull(c.getPatchset(1, 1));
List<Reference> cRefA = a.getReferences();
assertNotNull(cRefA);
assertEquals(1, cRefA.size());
assertNull(cRefA.get(0).ticketId);
assertEquals(commit1Sha, cRefA.get(0).hash);
List<Reference> cRefB = b.getReferences();
assertNotNull(cRefB);
assertEquals(1, cRefB.size());
assertNull(cRefB.get(0).ticketId);
assertEquals(commit1Sha, cRefB.get(0).hash);
//As a new patchset is created the references will remain until deleted
message = String.format("commit after amend for #%d", a.number);
final String commit2Sha = amendCommit(message);
assertForcePushSuccess(commit2Sha, branchName);
a = ticketService.getTicket(repo, a.number);
b = ticketService.getTicket(repo, b.number);
c = ticketService.getTicket(repo, c.number);
assertTrue(a.hasReferences());
assertTrue(b.hasReferences());
assertFalse(c.hasReferences());
assertNotNull(c.getPatchset(1, 1));
assertNotNull(c.getPatchset(2, 1));
cRefA = a.getReferences();
assertNotNull(cRefA);
assertEquals(2, cRefA.size());
assertNull(cRefA.get(0).ticketId);
assertNull(cRefA.get(1).ticketId);
assertEquals(commit1Sha, cRefA.get(0).hash);
assertEquals(commit2Sha, cRefA.get(1).hash);
cRefB = b.getReferences();
assertNotNull(cRefB);
assertEquals(1, cRefB.size());
assertNull(cRefB.get(0).ticketId);
assertEquals(commit1Sha, cRefB.get(0).hash);
//Delete the original patchset and confirm old references are removed
ticketService.deletePatchset(c, c.getPatchset(1, 1), user.username);
a = ticketService.getTicket(repo, a.number);
b = ticketService.getTicket(repo, b.number);
c = ticketService.getTicket(repo, c.number);
assertTrue(a.hasReferences());
assertFalse(b.hasReferences());
assertFalse(c.hasReferences());
assertNull(c.getPatchset(1, 1));
assertNotNull(c.getPatchset(2, 1));
cRefA = a.getReferences();
assertNotNull(cRefA);
assertEquals(1, cRefA.size());
assertNull(cRefA.get(0).ticketId);
assertEquals(commit2Sha, cRefA.get(0).hash);
}
@Test
public void commitTicketBranchNoUnexpectedReference() throws Exception {
setPatchsetAvailable(false);
TicketModel a = ticketService.createTicket(repo, newTicket("commitTicketBranchNoUnexpectedReference-A"));
String branchName = String.format("ticket/%d", a.number);
git.checkout().setCreateBranch(true).setName(branchName).call();
makeCommit("commit for 1 - no reference");
makeCommit("commit for # - no reference");
final String message = "commit for #999 - ignores invalid reference";
final RevCommit revCommit1 = makeCommit(message);
final String commit1Sha = revCommit1.name();
assertPushSuccess(commit1Sha, branchName);
a = ticketService.getTicket(repo, a.number);
assertFalse(a.hasReferences());
}
@Test
public void commitTicketBranchSelfReference() throws Exception {
setPatchsetAvailable(false);
TicketModel a = ticketService.createTicket(repo, newTicket("commitTicketBranchSelfReference-A"));
String branchName = String.format("ticket/%d", a.number);
git.checkout().setCreateBranch(true).setName(branchName).call();
final String message = String.format("commit for #%d - patchset self reference", a.number);
final RevCommit revCommit1 = makeCommit(message);
final String commit1Sha = revCommit1.name();
assertPushSuccess(commit1Sha, branchName);
a = ticketService.getTicket(repo, a.number);
assertTrue(a.hasReferences());
List<Reference> cRefA = a.getReferences();
assertNotNull(cRefA);
assertEquals(1, cRefA.size());
assertNull(cRefA.get(0).ticketId);
assertEquals(commit1Sha, cRefA.get(0).hash);
}
@Test
public void commitTicketBranchSingleReference() throws Exception {
setPatchsetAvailable(false);
TicketModel a = ticketService.createTicket(repo, newTicket("commitTicketBranchSingleReference-A"));
TicketModel b = ticketService.createTicket(repo, newTicket("commitTicketBranchSingleReference-B"));
String branchName = String.format("ticket/%d", a.number);
git.checkout().setCreateBranch(true).setName(branchName).call();
final String message = String.format("commit for #%d - patchset single reference", b.number);
final RevCommit revCommit1 = makeCommit(message);
final String commit1Sha = revCommit1.name();
assertPushSuccess(commit1Sha, branchName);
a = ticketService.getTicket(repo, a.number);
b = ticketService.getTicket(repo, b.number);
assertFalse(a.hasReferences());
assertTrue(b.hasReferences());
List<Reference> cRefB = b.getReferences();
assertNotNull(cRefB);
assertEquals(1, cRefB.size());
assertNull(cRefB.get(0).ticketId);
assertEquals(commit1Sha, cRefB.get(0).hash);
}
@Test
public void commitTicketBranchMultiCommit() throws Exception {
setPatchsetAvailable(false);
TicketModel a = ticketService.createTicket(repo, newTicket("commitTicketBranchMultiCommit-A"));
TicketModel b = ticketService.createTicket(repo, newTicket("commitTicketBranchMultiCommit-B"));
String branchName = String.format("ticket/%d", a.number);
git.checkout().setCreateBranch(true).setName(branchName).call();
final String message1 = String.format("commit for #%d - patchset multi commit 1", b.number);
final RevCommit revCommit1 = makeCommit(message1);
final String commit1Sha = revCommit1.name();
final String message2 = String.format("commit for #%d - patchset multi commit 2", b.number);
final RevCommit revCommit2 = makeCommit(message2);
final String commit2Sha = revCommit2.name();
assertPushSuccess(commit2Sha, branchName);
a = ticketService.getTicket(repo, a.number);
b = ticketService.getTicket(repo, b.number);
assertFalse(a.hasReferences());
assertTrue(b.hasReferences());
List<Reference> cRefB = b.getReferences();
assertNotNull(cRefB);
assertEquals(2, cRefB.size());
assertNull(cRefB.get(0).ticketId);
assertEquals(commit1Sha, cRefB.get(1).hash);
assertEquals(commit2Sha, cRefB.get(0).hash);
}
@Test
public void commitTicketBranchMultiReference() throws Exception {
setPatchsetAvailable(false);
TicketModel a = ticketService.createTicket(repo, newTicket("commitTicketBranchMultiReference-A"));
TicketModel b = ticketService.createTicket(repo, newTicket("commitTicketBranchMultiReference-B"));
TicketModel c = ticketService.createTicket(repo, newTicket("commitTicketBranchMultiReference-C"));
String branchName = String.format("ticket/%d", a.number);
git.checkout().setCreateBranch(true).setName(branchName).call();
final String message = String.format("commit for #%d and #%d- patchset multi reference", b.number, c.number);
final RevCommit revCommit1 = makeCommit(message);
final String commit1Sha = revCommit1.name();
assertPushSuccess(commit1Sha, branchName);
a = ticketService.getTicket(repo, a.number);
b = ticketService.getTicket(repo, b.number);
c = ticketService.getTicket(repo, c.number);
assertFalse(a.hasReferences());
assertTrue(b.hasReferences());
assertTrue(c.hasReferences());
List<Reference> cRefB = b.getReferences();
assertNotNull(cRefB);
assertEquals(1, cRefB.size());
assertNull(cRefB.get(0).ticketId);
assertEquals(commit1Sha, cRefB.get(0).hash);
List<Reference> cRefC = c.getReferences();
assertNotNull(cRefC);
assertEquals(1, cRefC.size());
assertNull(cRefC.get(0).ticketId);
assertEquals(commit1Sha, cRefC.get(0).hash);
}
@Test
public void commitTicketBranchAmendReference() throws Exception {
setPatchsetAvailable(false);
TicketModel a = ticketService.createTicket(repo, newTicket("commitTicketBranchAmendReference-A"));
TicketModel b = ticketService.createTicket(repo, newTicket("commitTicketBranchAmendReference-B"));
TicketModel c = ticketService.createTicket(repo, newTicket("commitTicketBranchAmendReference-C"));
assertFalse(c.hasPatchsets());
String branchName = String.format("ticket/%d", c.number);
git.checkout().setCreateBranch(true).setName(branchName).call();
String message = String.format("commit before amend for #%d and #%d", a.number, b.number);
final RevCommit revCommit1 = makeCommit(message);
final String commit1Sha = revCommit1.name();
assertPushSuccess(commit1Sha, branchName);
a = ticketService.getTicket(repo, a.number);
b = ticketService.getTicket(repo, b.number);
c = ticketService.getTicket(repo, c.number);
assertTrue(a.hasReferences());
assertTrue(b.hasReferences());
assertFalse(c.hasReferences());
assertFalse(c.hasPatchsets());
List<Reference> cRefA = a.getReferences();
assertNotNull(cRefA);
assertEquals(1, cRefA.size());
assertNull(cRefA.get(0).ticketId);
assertEquals(commit1Sha, cRefA.get(0).hash);
List<Reference> cRefB = b.getReferences();
assertNotNull(cRefB);
assertEquals(1, cRefB.size());
assertNull(cRefB.get(0).ticketId);
assertEquals(commit1Sha, cRefB.get(0).hash);
//Confirm that old invalid references removed for both tickets
//and new reference added for one referenced ticket
message = String.format("commit after amend for #%d", a.number);
final String commit2Sha = amendCommit(message);
assertForcePushSuccess(commit2Sha, branchName);
a = ticketService.getTicket(repo, a.number);
b = ticketService.getTicket(repo, b.number);
c = ticketService.getTicket(repo, c.number);
assertTrue(a.hasReferences());
assertFalse(b.hasReferences());
assertFalse(c.hasReferences());
assertFalse(c.hasPatchsets());
cRefA = a.getReferences();
assertNotNull(cRefA);
assertEquals(1, cRefA.size());
assertNull(cRefA.get(0).ticketId);
assertEquals(commit2Sha, cRefA.get(0).hash);
}
@Test
public void commitTicketBranchDeleteNoMergeReference() throws Exception {
setPatchsetAvailable(false);
TicketModel a = ticketService.createTicket(repo, newTicket("commitTicketBranchDeleteNoMergeReference-A"));
TicketModel b = ticketService.createTicket(repo, newTicket("commitTicketBranchDeleteNoMergeReference-B"));
TicketModel c = ticketService.createTicket(repo, newTicket("commitTicketBranchDeleteNoMergeReference-C"));
assertFalse(c.hasPatchsets());
String branchName = String.format("ticket/%d", c.number);
git.checkout().setCreateBranch(true).setName(branchName).call();
String message = String.format("commit before amend for #%d and #%d", a.number, b.number);
final RevCommit revCommit1 = makeCommit(message);
final String commit1Sha = revCommit1.name();
assertPushSuccess(commit1Sha, branchName);
a = ticketService.getTicket(repo, a.number);
b = ticketService.getTicket(repo, b.number);
c = ticketService.getTicket(repo, c.number);
assertTrue(a.hasReferences());
assertTrue(b.hasReferences());
assertFalse(c.hasReferences());
List<Reference> cRefA = a.getReferences();
assertNotNull(cRefA);
assertEquals(1, cRefA.size());
assertNull(cRefA.get(0).ticketId);
assertEquals(commit1Sha, cRefA.get(0).hash);
List<Reference> cRefB = b.getReferences();
assertNotNull(cRefB);
assertEquals(1, cRefB.size());
assertNull(cRefB.get(0).ticketId);
assertEquals(commit1Sha, cRefB.get(0).hash);
//Confirm that old invalid references removed for both tickets
assertDeleteBranch(branchName);
a = ticketService.getTicket(repo, a.number);
b = ticketService.getTicket(repo, b.number);
c = ticketService.getTicket(repo, c.number);
assertFalse(a.hasReferences());
assertFalse(b.hasReferences());
assertFalse(c.hasReferences());
}
@Test
public void commitTicketBranchDeletePostMergeReference() throws Exception {
setPatchsetAvailable(false);
TicketModel a = ticketService.createTicket(repo, newTicket("commitTicketBranchDeletePostMergeReference-A"));
TicketModel b = ticketService.createTicket(repo, newTicket("commitTicketBranchDeletePostMergeReference-B"));
TicketModel c = ticketService.createTicket(repo, newTicket("commitTicketBranchDeletePostMergeReference-C"));
assertFalse(c.hasPatchsets());
String branchName = String.format("ticket/%d", c.number);
git.checkout().setCreateBranch(true).setName(branchName).call();
String message = String.format("commit before amend for #%d and #%d", a.number, b.number);
final RevCommit revCommit1 = makeCommit(message);
final String commit1Sha = revCommit1.name();
assertPushSuccess(commit1Sha, branchName);
a = ticketService.getTicket(repo, a.number);
b = ticketService.getTicket(repo, b.number);
c = ticketService.getTicket(repo, c.number);
assertTrue(a.hasReferences());
assertTrue(b.hasReferences());
assertFalse(c.hasReferences());
List<Reference> cRefA = a.getReferences();
assertNotNull(cRefA);
assertEquals(1, cRefA.size());
assertNull(cRefA.get(0).ticketId);
assertEquals(commit1Sha, cRefA.get(0).hash);
List<Reference> cRefB = b.getReferences();
assertNotNull(cRefB);
assertEquals(1, cRefB.size());
assertNull(cRefB.get(0).ticketId);
assertEquals(commit1Sha, cRefB.get(0).hash);
git.checkout().setCreateBranch(false).setName("refs/heads/master").call();
// merge the tip of the branch into master
MergeResult mergeResult = git.merge().setFastForward(FastForwardMode.NO_FF).include(revCommit1.getId()).call();
assertEquals(MergeResult.MergeStatus.MERGED, mergeResult.getMergeStatus());
// push the merged master to the origin
Iterable<PushResult> results = git.push().setCredentialsProvider(cp).setRemote("origin").call();
for (PushResult result : results) {
RemoteRefUpdate ref = result.getRemoteUpdate("refs/heads/master");
assertEquals(Status.OK, ref.getStatus());
}
//As everything has been merged no references should be changed
assertDeleteBranch(branchName);
a = ticketService.getTicket(repo, a.number);
b = ticketService.getTicket(repo, b.number);
c = ticketService.getTicket(repo, c.number);
assertTrue(a.hasReferences());
assertTrue(b.hasReferences());
assertFalse(c.hasReferences());
cRefA = a.getReferences();
assertNotNull(cRefA);
assertEquals(1, cRefA.size());
assertNull(cRefA.get(0).ticketId);
assertEquals(commit1Sha, cRefA.get(0).hash);
cRefB = b.getReferences();
assertNotNull(cRefB);
assertEquals(1, cRefB.size());
assertNull(cRefB.get(0).ticketId);
assertEquals(commit1Sha, cRefB.get(0).hash);
}
private static Change newComment(String text) {
Change change = new Change("JUnit");
change.comment(text);
return change;
}
private static Change newTicket(String title) {
Change change = new Change("JUnit");
change.setField(Field.title, title);
change.setField(Field.type, TicketModel.Type.Bug );
return change;
}
private static RevCommit makeCommit(String message) throws Exception {
File file = new File(workingCopy, "testFile.txt");
OutputStreamWriter os = new OutputStreamWriter(new FileOutputStream(file, true), Constants.CHARSET);
BufferedWriter w = new BufferedWriter(os);
w.write("// " + new Date().toString() + "\n");
w.close();
git.add().addFilepattern(file.getName()).call();
RevCommit rev = git.commit().setMessage(message).call();
return rev;
}
private static String amendCommit(String message) throws Exception {
File file = new File(workingCopy, "testFile.txt");
OutputStreamWriter os = new OutputStreamWriter(new FileOutputStream(file, true), Constants.CHARSET);
BufferedWriter w = new BufferedWriter(os);
w.write("// " + new Date().toString() + "\n");
w.close();
git.add().addFilepattern(file.getName()).call();
RevCommit rev = git.commit().setAmend(true).setMessage(message).call();
return rev.getId().name();
}
private void setPatchsetAvailable(boolean state) throws GitBlitException {
repo.acceptNewPatchsets = state;
gitblit().updateRepositoryModel(repo.name, repo, false);
}
private void assertPushSuccess(String commitSha, String branchName) throws Exception {
Iterable<PushResult> results = git.push().setRemote("origin").setCredentialsProvider(cp).call();
for (PushResult result : results) {
RemoteRefUpdate ref = result.getRemoteUpdate("refs/heads/" + branchName);
assertEquals(Status.OK, ref.getStatus());
assertEquals(commitSha, ref.getNewObjectId().name());
}
}
private void assertForcePushSuccess(String commitSha, String branchName) throws Exception {
Iterable<PushResult> results = git.push().setForce(true).setRemote("origin").setCredentialsProvider(cp).call();
for (PushResult result : results) {
RemoteRefUpdate ref = result.getRemoteUpdate("refs/heads/" + branchName);
assertEquals(Status.OK, ref.getStatus());
assertEquals(commitSha, ref.getNewObjectId().name());
}
}
private void assertDeleteBranch(String branchName) throws Exception {
RefSpec refSpec = new RefSpec()
.setSource(null)
.setDestination("refs/heads/" + branchName);
Iterable<PushResult> results = git.push().setRefSpecs(refSpec).setRemote("origin").setCredentialsProvider(cp).call();
for (PushResult result : results) {
RemoteRefUpdate ref = result.getRemoteUpdate("refs/heads/" + branchName);
assertEquals(Status.OK, ref.getStatus());
}
}
}
+3
-0
[submodule "src/main/distrib/data/gitignore"]
path = src/main/distrib/data/gitignore
url = https://github.com/github/gitignore.git
[submodule "src/main/js/prosemirror"]
path = src/main/js/prosemirror
url = https://github.com/ProseMirror/prosemirror.git

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

<!-- Build API JavaDoc jar -->
<mx:javadoc destdir="${javadoc.dir}" redirect="true">
<mx:javadoc destdir="${javadoc.dir}" charset="utf-8" encoding="utf-8" docencoding="utf-8" redirect="true">
<fileset dir="${project.src.dir}" defaultexcludes="yes">

@@ -518,2 +518,3 @@ <include name="com/gitblit/Constants.java"/>

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

@@ -1060,2 +1061,19 @@ <page name="Gitblit as a viewer" src="setup_viewer.mkd" />

<!--
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Build Gitblit UI via npm
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-->
<target name="buildUI" description="Build Gitblit UI via npm">
<exec executable="npm" dir="src/main/js/" failonerror="true" vmlauncher="false" searchpath="true" >
<arg value="install" />
</exec>
<exec executable="npm" dir="src/main/js/" failonerror="true" vmlauncher="false" searchpath="true" >
<arg value="run" />
<arg value="build" />
</exec>
</target>
</project>

@@ -36,3 +36,5 @@ ## Documentation

[[src/site/setup_scaling.mkd]]
[[src/site/setup_filestore.mkd]]
### Gitblit Tickets

@@ -39,0 +41,0 @@

+4
-1

@@ -72,6 +72,9 @@ /*

// define the repository base url
def jenkinsGitbaseurl = gitblit.getString('groovy.jenkinsGitbaseurl', "${url}/r")
// define the trigger url
def triggerUrl = jenkinsUrl + "/git/notifyCommit?url=${url}/r/${repository.name}"
def triggerUrl = jenkinsUrl + "/git/notifyCommit?url=" + jenkinsGitbaseurl + "/${repository.name}"
// trigger the build
new URL(triggerUrl).getContent()

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

import javax.servlet.http.HttpServletRequest;
import org.slf4j.Logger;

@@ -27,2 +29,3 @@ import org.slf4j.LoggerFactory;

import com.gitblit.Constants.Role;
import com.gitblit.Constants.AuthenticationType;
import com.gitblit.IStoredSettings;

@@ -78,2 +81,4 @@ import com.gitblit.manager.IRuntimeManager;

public abstract AuthenticationType getAuthenticationType();
protected void setCookie(UserModel user, char [] password) {

@@ -122,2 +127,20 @@ // create a user cookie

/**
* Used to handle requests for requests for pages requiring authentication.
* This allows authentication to occur based on the contents of the request
* itself.
*
* @param httpRequest
* @return
*/
public abstract UserModel authenticate(HttpServletRequest httpRequest);
/**
* Used to authentication user/password credentials, both for login form
* and HTTP Basic authentication processing.
*
* @param username
* @param password
* @return
*/
public abstract UserModel authenticate(String username, char[] password);

@@ -128,5 +151,5 @@

/**
* Does the user service support changes to credentials?
* Returns true if the users's credentials can be changed.
*
* @return true or false
* @return true if the authentication provider supports credential changes
* @since 1.0.0

@@ -140,3 +163,3 @@ */

* @param user
* @return true if the user service supports display name changes
* @return true if the authentication provider supports display name changes
*/

@@ -149,3 +172,3 @@ public abstract boolean supportsDisplayNameChanges();

* @param user
* @return true if the user service supports email address changes
* @return true if the authentication provider supports email address changes
*/

@@ -158,3 +181,3 @@ public abstract boolean supportsEmailAddressChanges();

* @param user
* @return true if the user service supports team membership changes
* @return true if the authentication provider supports team membership changes
*/

@@ -191,2 +214,12 @@ public abstract boolean supportsTeamMembershipChanges();

@Override
public UserModel authenticate(HttpServletRequest httpRequest) {
return null;
}
@Override
public AuthenticationType getAuthenticationType() {
return AuthenticationType.CREDENTIALS;
}
@Override

@@ -215,2 +248,7 @@ public void stop() {

@Override
public UserModel authenticate(HttpServletRequest httpRequest) {
return null;
}
@Override
public UserModel authenticate(String username, char[] password) {

@@ -226,2 +264,7 @@ return null;

@Override
public AuthenticationType getAuthenticationType() {
return null;
}
@Override
public boolean supportsCredentialChanges() {

@@ -228,0 +271,0 @@ return true;

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

import com.gitblit.Constants.RegistrantType;
import com.gitblit.GitBlitException.ForbiddenException;
import com.gitblit.GitBlitException.NotAllowedException;
import com.gitblit.GitBlitException.UnauthorizedException;
import com.gitblit.GitBlitException.UnknownRequestException;
import com.gitblit.Keys;

@@ -123,30 +119,15 @@ import com.gitblit.models.FederationModel;

try {
// credentials may not have administrator access
// or server may have disabled rpc management
refreshUsers();
if (protocolVersion > 1) {
refreshTeams();
}
allowManagement = true;
} catch (UnauthorizedException e) {
} catch (ForbiddenException e) {
} catch (NotAllowedException e) {
} catch (UnknownRequestException e) {
} catch (IOException e) {
e.printStackTrace();
// credentials may not have administrator access
// or server may have disabled rpc management
refreshUsers();
if (protocolVersion > 1) {
refreshTeams();
}
allowManagement = true;
try {
// credentials may not have administrator access
// or server may have disabled rpc administration
refreshStatus();
allowAdministration = true;
} catch (UnauthorizedException e) {
} catch (ForbiddenException e) {
} catch (NotAllowedException e) {
} catch (UnknownRequestException e) {
} catch (IOException e) {
e.printStackTrace();
}
// credentials may not have administrator access
// or server may have disabled rpc administration
refreshStatus();
allowAdministration = true;
}

@@ -153,0 +134,0 @@

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

user.accountType = AccountType.fromString(config.getString(USER, username, ACCOUNTTYPE));
if (Constants.EXTERNAL_ACCOUNT.equals(user.password) && user.accountType.isLocal()) {
user.accountType = AccountType.EXTERNAL;
}
user.disabled = config.getBoolean(USER, username, DISABLED, false);

@@ -898,0 +895,0 @@ user.organizationalUnit = config.getString(USER, username, ORGANIZATIONALUNIT);

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

public static final int LEN_SHORTLOG_REFS = 60;
public static final int LEN_FILESTORE_META_MIN = 125;
public static final int LEN_FILESTORE_META_MAX = 146;

@@ -578,3 +582,3 @@ public static final String DEFAULT_BRANCH = "default";

public static enum AuthenticationType {
PUBLIC_KEY, CREDENTIALS, COOKIE, CERTIFICATE, CONTAINER;
PUBLIC_KEY, CREDENTIALS, COOKIE, CERTIFICATE, CONTAINER, HTTPHEADER;

@@ -587,3 +591,3 @@ public boolean isStandard() {

public static enum AccountType {
LOCAL, EXTERNAL, CONTAINER, LDAP, REDMINE, SALESFORCE, WINDOWS, PAM, HTPASSWD;
LOCAL, CONTAINER, LDAP, REDMINE, SALESFORCE, WINDOWS, PAM, HTPASSWD, HTTPHEADER;

@@ -590,0 +594,0 @@ public static AccountType fromString(String value) {

@@ -25,14 +25,24 @@ /*

import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.concurrent.TimeUnit;
import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.BatchRefUpdate;
import org.eclipse.jgit.lib.NullProgressMonitor;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.ProgressMonitor;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.PostReceiveHook;

@@ -54,4 +64,13 @@ import org.eclipse.jgit.transport.PreReceiveHook;

import com.gitblit.models.RepositoryModel;
import com.gitblit.models.TicketModel;
import com.gitblit.models.UserModel;
import com.gitblit.models.TicketModel.Change;
import com.gitblit.models.TicketModel.Field;
import com.gitblit.models.TicketModel.Patchset;
import com.gitblit.models.TicketModel.Status;
import com.gitblit.models.TicketModel.TicketAction;
import com.gitblit.models.TicketModel.TicketLink;
import com.gitblit.tickets.BranchTicketService;
import com.gitblit.tickets.ITicketService;
import com.gitblit.tickets.TicketNotifier;
import com.gitblit.utils.ArrayUtils;

@@ -63,2 +82,3 @@ import com.gitblit.utils.ClientLogger;

import com.gitblit.utils.StringUtils;
import com.google.common.collect.Lists;

@@ -98,3 +118,8 @@

protected final IGitblit gitblit;
protected final ITicketService ticketService;
protected final TicketNotifier ticketNotifier;
public GitblitReceivePack(

@@ -121,2 +146,10 @@ IGitblit gitblit,

if (gitblit.getTicketService().isAcceptingTicketUpdates(repository)) {
this.ticketService = gitblit.getTicketService();
this.ticketNotifier = this.ticketService.createNotifier();
} else {
this.ticketService = null;
this.ticketNotifier = null;
}
// set advanced ref permissions

@@ -508,2 +541,100 @@ setAllowCreates(user.canCreateRef(repository));

}
//
// if there are ref update receive commands that were
// successfully processed and there is an active ticket service for the repository
// then process any referenced tickets
//
if (ticketService != null) {
List<ReceiveCommand> allUpdates = ReceiveCommand.filter(batch.getCommands(), Result.OK);
if (!allUpdates.isEmpty()) {
int ticketsProcessed = 0;
for (ReceiveCommand cmd : allUpdates) {
switch (cmd.getType()) {
case CREATE:
case UPDATE:
if (cmd.getRefName().startsWith(Constants.R_HEADS)) {
Collection<TicketModel> tickets = processReferencedTickets(cmd);
ticketsProcessed += tickets.size();
for (TicketModel ticket : tickets) {
ticketNotifier.queueMailing(ticket);
}
}
break;
case UPDATE_NONFASTFORWARD:
if (cmd.getRefName().startsWith(Constants.R_HEADS)) {
String base = JGitUtils.getMergeBase(getRepository(), cmd.getOldId(), cmd.getNewId());
List<TicketLink> deletedRefs = JGitUtils.identifyTicketsBetweenCommits(getRepository(), settings, base, cmd.getOldId().name());
for (TicketLink link : deletedRefs) {
link.isDelete = true;
}
Change deletion = new Change(user.username);
deletion.pendingLinks = deletedRefs;
ticketService.updateTicket(repository, 0, deletion);
Collection<TicketModel> tickets = processReferencedTickets(cmd);
ticketsProcessed += tickets.size();
for (TicketModel ticket : tickets) {
ticketNotifier.queueMailing(ticket);
}
}
break;
case DELETE:
//Identify if the branch has been merged
SortedMap<Integer, String> bases = new TreeMap<Integer, String>();
try {
ObjectId dObj = cmd.getOldId();
Collection<Ref> tips = getRepository().getRefDatabase().getRefs(Constants.R_HEADS).values();
for (Ref ref : tips) {
ObjectId iObj = ref.getObjectId();
String mergeBase = JGitUtils.getMergeBase(getRepository(), dObj, iObj);
if (mergeBase != null) {
int d = JGitUtils.countCommits(getRepository(), getRevWalk(), mergeBase, dObj.name());
bases.put(d, mergeBase);
//All commits have been merged into some other branch
if (d == 0) {
break;
}
}
}
if (bases.isEmpty()) {
//TODO: Handle orphan branch case
} else {
if (bases.firstKey() > 0) {
//Delete references from the remaining commits that haven't been merged
String mergeBase = bases.get(bases.firstKey());
List<TicketLink> deletedRefs = JGitUtils.identifyTicketsBetweenCommits(getRepository(),
settings, mergeBase, dObj.name());
for (TicketLink link : deletedRefs) {
link.isDelete = true;
}
Change deletion = new Change(user.username);
deletion.pendingLinks = deletedRefs;
ticketService.updateTicket(repository, 0, deletion);
}
}
} catch (IOException e) {
LOGGER.error(null, e);
}
break;
default:
break;
}
}
if (ticketsProcessed == 1) {
sendInfo("1 ticket updated");
} else if (ticketsProcessed > 1) {
sendInfo("{0} tickets updated", ticketsProcessed);
}
}
// reset the ticket caches for the repository
ticketService.resetCaches(repository);
}
}

@@ -625,2 +756,114 @@

}
/**
* Automatically closes open tickets and adds references to tickets if made in the commit message.
*
* @param cmd
*/
private Collection<TicketModel> processReferencedTickets(ReceiveCommand cmd) {
Map<Long, TicketModel> changedTickets = new LinkedHashMap<Long, TicketModel>();
final RevWalk rw = getRevWalk();
try {
rw.reset();
rw.markStart(rw.parseCommit(cmd.getNewId()));
if (!ObjectId.zeroId().equals(cmd.getOldId())) {
rw.markUninteresting(rw.parseCommit(cmd.getOldId()));
}
RevCommit c;
while ((c = rw.next()) != null) {
rw.parseBody(c);
List<TicketLink> ticketLinks = JGitUtils.identifyTicketsFromCommitMessage(getRepository(), settings, c);
if (ticketLinks == null) {
continue;
}
for (TicketLink link : ticketLinks) {
TicketModel ticket = ticketService.getTicket(repository, link.targetTicketId);
if (ticket == null) {
continue;
}
Change change = null;
String commitSha = c.getName();
String branchName = Repository.shortenRefName(cmd.getRefName());
switch (link.action) {
case Commit: {
//A commit can reference a ticket in any branch even if the ticket is closed.
//This allows developers to identify and communicate related issues
change = new Change(user.username);
change.referenceCommit(commitSha);
} break;
case Close: {
// As this isn't a patchset theres no merging taking place when closing a ticket
if (ticket.isClosed()) {
continue;
}
change = new Change(user.username);
change.setField(Field.status, Status.Fixed);
if (StringUtils.isEmpty(ticket.responsible)) {
// unassigned tickets are assigned to the closer
change.setField(Field.responsible, user.username);
}
}
default: {
//No action
} break;
}
if (change != null) {
ticket = ticketService.updateTicket(repository, ticket.number, change);
}
if (ticket != null) {
sendInfo("");
sendHeader("#{0,number,0}: {1}", ticket.number, StringUtils.trimString(ticket.title, Constants.LEN_SHORTLOG));
switch (link.action) {
case Commit: {
sendInfo("referenced by push of {0} to {1}", commitSha, branchName);
changedTickets.put(ticket.number, ticket);
} break;
case Close: {
sendInfo("closed by push of {0} to {1}", commitSha, branchName);
changedTickets.put(ticket.number, ticket);
} break;
default: { }
}
sendInfo(ticketService.getTicketUrl(ticket));
sendInfo("");
} else {
switch (link.action) {
case Commit: {
sendError("FAILED to reference ticket {0} by push of {1}", link.targetTicketId, commitSha);
} break;
case Close: {
sendError("FAILED to close ticket {0} by push of {1}", link.targetTicketId, commitSha);
} break;
default: { }
}
}
}
}
} catch (IOException e) {
LOGGER.error("Can't scan for changes to reference or close", e);
} finally {
rw.reset();
}
return changedTickets.values();
}
}

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

import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.BatchRefUpdate;

@@ -64,2 +63,4 @@ import org.eclipse.jgit.lib.NullProgressMonitor;

import com.gitblit.models.TicketModel.Status;
import com.gitblit.models.TicketModel.TicketAction;
import com.gitblit.models.TicketModel.TicketLink;
import com.gitblit.models.UserModel;

@@ -490,5 +491,23 @@ import com.gitblit.tickets.BranchTicketService;

case UPDATE:
if (cmd.getRefName().startsWith(Constants.R_HEADS)) {
Collection<TicketModel> tickets = processReferencedTickets(cmd);
ticketsProcessed += tickets.size();
for (TicketModel ticket : tickets) {
ticketNotifier.queueMailing(ticket);
}
}
break;
case UPDATE_NONFASTFORWARD:
if (cmd.getRefName().startsWith(Constants.R_HEADS)) {
Collection<TicketModel> tickets = processMergedTickets(cmd);
String base = JGitUtils.getMergeBase(getRepository(), cmd.getOldId(), cmd.getNewId());
List<TicketLink> deletedRefs = JGitUtils.identifyTicketsBetweenCommits(getRepository(), settings, base, cmd.getOldId().name());
for (TicketLink link : deletedRefs) {
link.isDelete = true;
}
Change deletion = new Change(user.username);
deletion.pendingLinks = deletedRefs;
ticketService.updateTicket(repository, 0, deletion);
Collection<TicketModel> tickets = processReferencedTickets(cmd);
ticketsProcessed += tickets.size();

@@ -610,11 +629,13 @@ for (TicketModel ticket : tickets) {

}
// check to see if this commit is already linked to a ticket
long id = identifyTicket(tipCommit, false);
if (id > 0) {
sendError("{0} has already been pushed to ticket {1,number,0}.", shortTipId, id);
if (ticket != null &&
JGitUtils.getTicketNumberFromCommitBranch(getRepository(), tipCommit) == ticket.number) {
sendError("{0} has already been pushed to ticket {1,number,0}.", shortTipId, ticket.number);
sendRejection(cmd, "everything up-to-date");
return null;
}
List<TicketLink> ticketLinks = JGitUtils.identifyTicketsFromCommitMessage(getRepository(), settings, tipCommit);
PatchsetCommand psCmd;

@@ -809,2 +830,6 @@ if (ticket == null) {

}
Change change = psCmd.getChange();
change.pendingLinks = ticketLinks;
return psCmd;

@@ -898,7 +923,7 @@ }

* Automatically closes open tickets that have been merged to their integration
* branch by a client.
* branch by a client and adds references to tickets if made in the commit message.
*
* @param cmd
*/
private Collection<TicketModel> processMergedTickets(ReceiveCommand cmd) {
private Collection<TicketModel> processReferencedTickets(ReceiveCommand cmd) {
Map<Long, TicketModel> mergedTickets = new LinkedHashMap<Long, TicketModel>();

@@ -916,101 +941,147 @@ final RevWalk rw = getRevWalk();

rw.parseBody(c);
long ticketNumber = identifyTicket(c, true);
if (ticketNumber == 0L || mergedTickets.containsKey(ticketNumber)) {
List<TicketLink> ticketLinks = JGitUtils.identifyTicketsFromCommitMessage(getRepository(), settings, c);
if (ticketLinks == null) {
continue;
}
TicketModel ticket = ticketService.getTicket(repository, ticketNumber);
if (ticket == null) {
continue;
}
String integrationBranch;
if (StringUtils.isEmpty(ticket.mergeTo)) {
// unspecified integration branch
integrationBranch = null;
} else {
// specified integration branch
integrationBranch = Constants.R_HEADS + ticket.mergeTo;
}
for (TicketLink link : ticketLinks) {
if (mergedTickets.containsKey(link.targetTicketId)) {
continue;
}
TicketModel ticket = ticketService.getTicket(repository, link.targetTicketId);
if (ticket == null) {
continue;
}
String integrationBranch;
if (StringUtils.isEmpty(ticket.mergeTo)) {
// unspecified integration branch
integrationBranch = null;
} else {
// specified integration branch
integrationBranch = Constants.R_HEADS + ticket.mergeTo;
}
Change change;
Patchset patchset = null;
String mergeSha = c.getName();
String mergeTo = Repository.shortenRefName(cmd.getRefName());
// ticket must be open and, if specified, the ref must match the integration branch
if (ticket.isClosed() || (integrationBranch != null && !integrationBranch.equals(cmd.getRefName()))) {
continue;
}
String baseRef = PatchsetCommand.getBasePatchsetBranch(ticket.number);
boolean knownPatchset = false;
Set<Ref> refs = getRepository().getAllRefsByPeeledObjectId().get(c.getId());
if (refs != null) {
for (Ref ref : refs) {
if (ref.getName().startsWith(baseRef)) {
knownPatchset = true;
break;
if (link.action == TicketAction.Commit) {
//A commit can reference a ticket in any branch even if the ticket is closed.
//This allows developers to identify and communicate related issues
change = new Change(user.username);
change.referenceCommit(mergeSha);
} else {
// ticket must be open and, if specified, the ref must match the integration branch
if (ticket.isClosed() || (integrationBranch != null && !integrationBranch.equals(cmd.getRefName()))) {
continue;
}
String baseRef = PatchsetCommand.getBasePatchsetBranch(ticket.number);
boolean knownPatchset = false;
Set<Ref> refs = getRepository().getAllRefsByPeeledObjectId().get(c.getId());
if (refs != null) {
for (Ref ref : refs) {
if (ref.getName().startsWith(baseRef)) {
knownPatchset = true;
break;
}
}
}
if (knownPatchset) {
// identify merged patchset by the patchset tip
for (Patchset ps : ticket.getPatchsets()) {
if (ps.tip.equals(mergeSha)) {
patchset = ps;
break;
}
}
if (patchset == null) {
// should not happen - unless ticket has been hacked
sendError("Failed to find the patchset for {0} in ticket {1,number,0}?!",
mergeSha, ticket.number);
continue;
}
// create a new change
change = new Change(user.username);
} else {
// new patchset pushed by user
String base = cmd.getOldId().getName();
patchset = newPatchset(ticket, base, mergeSha);
PatchsetCommand psCmd = new PatchsetCommand(user.username, patchset);
psCmd.updateTicket(c, mergeTo, ticket, null);
// create a ticket patchset ref
updateRef(psCmd.getPatchsetBranch(), c.getId(), patchset.type);
RefUpdate ru = updateRef(psCmd.getTicketBranch(), c.getId(), patchset.type);
updateReflog(ru);
// create a change from the patchset command
change = psCmd.getChange();
}
// set the common change data about the merge
change.setField(Field.status, Status.Merged);
change.setField(Field.mergeSha, mergeSha);
change.setField(Field.mergeTo, mergeTo);
if (StringUtils.isEmpty(ticket.responsible)) {
// unassigned tickets are assigned to the closer
change.setField(Field.responsible, user.username);
}
}
}
ticket = ticketService.updateTicket(repository, ticket.number, change);
if (ticket != null) {
sendInfo("");
sendHeader("#{0,number,0}: {1}", ticket.number, StringUtils.trimString(ticket.title, Constants.LEN_SHORTLOG));
String mergeSha = c.getName();
String mergeTo = Repository.shortenRefName(cmd.getRefName());
Change change;
Patchset patchset;
if (knownPatchset) {
// identify merged patchset by the patchset tip
patchset = null;
for (Patchset ps : ticket.getPatchsets()) {
if (ps.tip.equals(mergeSha)) {
patchset = ps;
switch (link.action) {
case Commit: {
sendInfo("referenced by push of {0} to {1}", c.getName(), mergeTo);
}
break;
}
}
if (patchset == null) {
// should not happen - unless ticket has been hacked
sendError("Failed to find the patchset for {0} in ticket {1,number,0}?!",
mergeSha, ticket.number);
continue;
}
case Close: {
sendInfo("closed by push of {0} to {1}", patchset, mergeTo);
mergedTickets.put(ticket.number, ticket);
}
break;
// create a new change
change = new Change(user.username);
} else {
// new patchset pushed by user
String base = cmd.getOldId().getName();
patchset = newPatchset(ticket, base, mergeSha);
PatchsetCommand psCmd = new PatchsetCommand(user.username, patchset);
psCmd.updateTicket(c, mergeTo, ticket, null);
default: {
}
}
// create a ticket patchset ref
updateRef(psCmd.getPatchsetBranch(), c.getId(), patchset.type);
RefUpdate ru = updateRef(psCmd.getTicketBranch(), c.getId(), patchset.type);
updateReflog(ru);
sendInfo(ticketService.getTicketUrl(ticket));
sendInfo("");
// create a change from the patchset command
change = psCmd.getChange();
} else {
String shortid = mergeSha.substring(0, settings.getInteger(Keys.web.shortCommitIdLength, 6));
switch (link.action) {
case Commit: {
sendError("FAILED to reference ticket {0,number,0} by push of {1}", link.targetTicketId, shortid);
}
break;
case Close: {
sendError("FAILED to close ticket {0,number,0} by push of {1}", link.targetTicketId, shortid);
} break;
default: {
}
}
}
}
// set the common change data about the merge
change.setField(Field.status, Status.Merged);
change.setField(Field.mergeSha, mergeSha);
change.setField(Field.mergeTo, mergeTo);
if (StringUtils.isEmpty(ticket.responsible)) {
// unassigned tickets are assigned to the closer
change.setField(Field.responsible, user.username);
}
ticket = ticketService.updateTicket(repository, ticket.number, change);
if (ticket != null) {
sendInfo("");
sendHeader("#{0,number,0}: {1}", ticket.number, StringUtils.trimString(ticket.title, Constants.LEN_SHORTLOG));
sendInfo("closed by push of {0} to {1}", patchset, mergeTo);
sendInfo(ticketService.getTicketUrl(ticket));
sendInfo("");
mergedTickets.put(ticket.number, ticket);
} else {
String shortid = mergeSha.substring(0, settings.getInteger(Keys.web.shortCommitIdLength, 6));
sendError("FAILED to close ticket {0,number,0} by push of {1}", ticketNumber, shortid);
}
}
} catch (IOException e) {
LOGGER.error("Can't scan for changes to close", e);
LOGGER.error("Can't scan for changes to reference or close", e);
} finally {

@@ -1023,72 +1094,6 @@ rw.reset();

/**
* Try to identify a ticket id from the commit.
*
* @param commit
* @param parseMessage
* @return a ticket id or 0
*/
private long identifyTicket(RevCommit commit, boolean parseMessage) {
// try lookup by change ref
Map<AnyObjectId, Set<Ref>> map = getRepository().getAllRefsByPeeledObjectId();
Set<Ref> refs = map.get(commit.getId());
if (!ArrayUtils.isEmpty(refs)) {
for (Ref ref : refs) {
long number = PatchsetCommand.getTicketNumber(ref.getName());
if (number > 0) {
return number;
}
}
}
if (parseMessage) {
// parse commit message looking for fixes/closes #n
String dx = "(?:fixes|closes)[\\s-]+#?(\\d+)";
String x = settings.getString(Keys.tickets.closeOnPushCommitMessageRegex, dx);
if (StringUtils.isEmpty(x)) {
x = dx;
}
try {
Pattern p = Pattern.compile(x, Pattern.CASE_INSENSITIVE);
Matcher m = p.matcher(commit.getFullMessage());
while (m.find()) {
String val = m.group(1);
return Long.parseLong(val);
}
} catch (Exception e) {
LOGGER.error(String.format("Failed to parse \"%s\" in commit %s", x, commit.getName()), e);
}
}
return 0L;
}
private int countCommits(String baseId, String tipId) {
int count = 0;
RevWalk walk = getRevWalk();
walk.reset();
walk.sort(RevSort.TOPO);
walk.sort(RevSort.REVERSE, true);
try {
RevCommit tip = walk.parseCommit(getRepository().resolve(tipId));
RevCommit base = walk.parseCommit(getRepository().resolve(baseId));
walk.markStart(tip);
walk.markUninteresting(base);
for (;;) {
RevCommit c = walk.next();
if (c == null) {
break;
}
count++;
}
} catch (IOException e) {
// Should never happen, the core receive process would have
// identified the missing object earlier before we got control.
LOGGER.error("failed to get commit count", e);
return 0;
} finally {
walk.close();
}
return count;
}
/**

@@ -1102,3 +1107,3 @@ * Creates a new patchset with metadata.

private Patchset newPatchset(TicketModel ticket, String mergeBase, String tip) {
int totalCommits = countCommits(mergeBase, tip);
int totalCommits = JGitUtils.countCommits(getRepository(), getRevWalk(), mergeBase, tip);

@@ -1105,0 +1110,0 @@ Patchset newPatchset = new Patchset();

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

import com.gitblit.auth.HtpasswdAuthProvider;
import com.gitblit.auth.HttpHeaderAuthProvider;
import com.gitblit.auth.LdapAuthProvider;

@@ -96,2 +97,3 @@ import com.gitblit.auth.PAMAuthProvider;

providerNames.put("htpasswd", HtpasswdAuthProvider.class);
providerNames.put("httpheader", HttpHeaderAuthProvider.class);
providerNames.put("ldap", LdapAuthProvider.class);

@@ -175,4 +177,8 @@ providerNames.put("pam", PAMAuthProvider.class);

/**
* Authenticate a user based on HTTP request parameters.
* Used to handle authentication for page requests.
*
* This allows authentication to occur based on the contents of the request
* itself. If no configured @{AuthenticationProvider}s authenticate succesffully,
* a request for login will be shown.
*
* Authentication by X509Certificate is tried first and then by cookie.

@@ -191,3 +197,3 @@ *

*
* Authentication by servlet container principal, X509Certificate, cookie,
* Authentication by custom HTTP header, servlet container principal, X509Certificate, cookie,
* and finally BASIC header.

@@ -205,3 +211,3 @@ *

if (!StringUtils.isEmpty(reqAuthUser)) {
logger.warn("Called servlet authenticate when request is already authenticated.");
logger.debug("Called servlet authenticate when request is already authenticated.");
return userManager.getUserModel(reqAuthUser);

@@ -327,2 +333,14 @@ }

}
// Check each configured AuthenticationProvider
for (AuthenticationProvider ap : authenticationProviders) {
UserModel authedUser = ap.authenticate(httpRequest);
if (null != authedUser) {
flagRequest(httpRequest, ap.getAuthenticationType(), authedUser.username);
logger.debug(MessageFormat.format("{0} authenticated by {1} from {2} for {3}",
authedUser.username, ap.getServiceName(), httpRequest.getRemoteAddr(),
httpRequest.getPathInfo()));
return validateAuthentication(authedUser, ap.getAuthenticationType());
}
}
return null;

@@ -457,2 +475,8 @@ }

if (username.equalsIgnoreCase(Constants.FEDERATION_USER)) {
// can not authenticate internal FEDERATION_USER at this point
// it must be routed to FederationManager
return null;
}
String usernameDecoded = StringUtils.decodeUsername(username);

@@ -459,0 +483,0 @@ String pw = new String(password);

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

});
if (files == null) {
return list;
}
for (File file : files) {

@@ -372,0 +376,0 @@ String json = com.gitblit.utils.FileUtils.readContent(file, null);

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

private final IRuntimeManager runtimeManager;
private final IRepositoryManager repositoryManager;

@@ -97,4 +99,6 @@ private final IStoredSettings settings;

FilestoreManager(
IRuntimeManager runtimeManager) {
IRuntimeManager runtimeManager,
IRepositoryManager repositoryManager) {
this.runtimeManager = runtimeManager;
this.repositoryManager = repositoryManager;
this.settings = runtimeManager.getSettings();

@@ -329,4 +333,25 @@ }

@Override
public List<FilestoreModel> getAllObjects() {
return new ArrayList<FilestoreModel>(fileCache.values());
public List<FilestoreModel> getAllObjects(UserModel user) {
final List<RepositoryModel> viewableRepositories = repositoryManager.getRepositoryModels(user);
List<String> viewableRepositoryNames = new ArrayList<String>(viewableRepositories.size());
for (RepositoryModel repository : viewableRepositories) {
viewableRepositoryNames.add(repository.name);
}
if (viewableRepositoryNames.size() == 0) {
return null;
}
final Collection<FilestoreModel> allFiles = fileCache.values();
List<FilestoreModel> userViewableFiles = new ArrayList<FilestoreModel>(allFiles.size());
for (FilestoreModel file : allFiles) {
if (file.isInRepositoryList(viewableRepositoryNames)) {
userViewableFiles.add(file);
}
}
return userViewableFiles;
}

@@ -333,0 +358,0 @@

@@ -1277,4 +1277,4 @@ /*

@Override
public List<FilestoreModel> getAllObjects() {
return filestoreManager.getAllObjects();
public List<FilestoreModel> getAllObjects(UserModel user) {
return filestoreManager.getAllObjects(user);
}

@@ -1281,0 +1281,0 @@

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

List<FilestoreModel> getAllObjects();
List<FilestoreModel> getAllObjects(UserModel user);

@@ -43,0 +43,0 @@ File getStorageFolder();

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

import java.util.NoSuchElementException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.gitblit.Constants;
/**

@@ -31,6 +35,18 @@ * A FilestoreModel represents a file stored outside a repository but referenced by the repository using a unique objectID

*/
public class FilestoreModel implements Serializable {
public class FilestoreModel implements Serializable, Comparable<FilestoreModel> {
private static final long serialVersionUID = 1L;
private static final String metaRegexText = new StringBuilder()
.append("version\\shttps://git-lfs.github.com/spec/v1\\s+")
.append("oid\\ssha256:(" + Constants.REGEX_SHA256 + ")\\s+")
.append("size\\s([0-9]+)")
.toString();
private static final Pattern metaRegex = Pattern.compile(metaRegexText);
private static final int metaRegexIndexSHA = 1;
private static final int metaRegexIndexSize = 2;
public final String oid;

@@ -48,2 +64,8 @@

public FilestoreModel(String id, long definedSize) {
oid = id;
size = definedSize;
status = Status.ReferenceOnly;
}
public FilestoreModel(String id, long expectedSize, UserModel user, String repo) {

@@ -59,2 +81,25 @@ oid = id;

/*
* Attempts to create a FilestoreModel from the given meta string
*
* @return A valid FilestoreModel if successful, otherwise null
*/
public static FilestoreModel fromMetaString(String meta) {
Matcher m = metaRegex.matcher(meta);
if (m.find()) {
try
{
final Long size = Long.parseLong(m.group(metaRegexIndexSize));
final String sha = m.group(metaRegexIndexSHA);
return new FilestoreModel(sha, size);
} catch (Exception e) {
//Fail silent - it is not a valid filestore item
}
}
return null;
}
public synchronized long getSize() {

@@ -109,13 +154,30 @@ return size;

public synchronized void addRepository(String repo) {
if (!repositories.contains(repo)) {
repositories.add(repo);
}
if (status != Status.ReferenceOnly) {
if (!repositories.contains(repo)) {
repositories.add(repo);
}
}
}
public synchronized void removeRepository(String repo) {
repositories.remove(repo);
if (status != Status.ReferenceOnly) {
repositories.remove(repo);
}
}
public synchronized boolean isInRepositoryList(List<String> repoList) {
if (status != Status.ReferenceOnly) {
for (String name : repositories) {
if (repoList.contains(name)) {
return true;
}
}
}
return false;
}
public static enum Status {
ReferenceOnly(-42),
Deleted(-30),

@@ -163,3 +225,8 @@ AuthenticationRequired(-20),

@Override
public int compareTo(FilestoreModel o) {
return this.oid.compareTo(o.oid);
}
}

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

import java.io.IOException;
import java.io.Serializable;

@@ -23,4 +24,12 @@

import org.eclipse.jgit.diff.DiffEntry.ChangeType;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevWalk;
import com.gitblit.manager.FilestoreManager;
import com.gitblit.utils.JGitUtils;
/**

@@ -39,2 +48,3 @@ * PathModel is a serializable model class that represents a file or a folder,

public final String path;
private final FilestoreModel filestoreItem;
public final long size;

@@ -45,7 +55,8 @@ public final int mode;

public boolean isParentPath;
public PathModel(String name, String path, long size, int mode, String objectId, String commitId) {
public PathModel(String name, String path, FilestoreModel filestoreItem, long size, int mode, String objectId, String commitId) {
this.name = name;
this.path = path;
this.size = size;
this.filestoreItem = filestoreItem;
this.size = (filestoreItem == null) ? size : filestoreItem.getSize();
this.mode = mode;

@@ -73,2 +84,14 @@ this.objectId = objectId;

}
public boolean isFilestoreItem() {
return filestoreItem != null;
}
public String getFilestoreOid() {
if (filestoreItem != null) {
return filestoreItem.oid;
}
return null;
}

@@ -127,5 +150,5 @@ @Override

public PathChangeModel(String name, String path, long size, int mode, String objectId,
public PathChangeModel(String name, String path, FilestoreModel filestoreItem, long size, int mode, String objectId,
String commitId, ChangeType type) {
super(name, path, size, mode, objectId, commitId);
super(name, path, filestoreItem, size, mode, objectId, commitId);
this.changeType = type;

@@ -157,14 +180,29 @@ }

public static PathChangeModel from(DiffEntry diff, String commitId) {
public static PathChangeModel from(DiffEntry diff, String commitId, Repository repository) {
PathChangeModel pcm;
FilestoreModel filestoreItem = null;
long size = 0;
if (repository != null) {
try (RevWalk revWalk = new RevWalk(repository)) {
size = revWalk.getObjectReader().getObjectSize(diff.getNewId().toObjectId(), Constants.OBJ_BLOB);
if (JGitUtils.isPossibleFilestoreItem(size)) {
filestoreItem = JGitUtils.getFilestoreItem(revWalk.getObjectReader().open(diff.getNewId().toObjectId()));
}
} catch (Exception e) {
e.printStackTrace();
}
}
if (diff.getChangeType().equals(ChangeType.DELETE)) {
pcm = new PathChangeModel(diff.getOldPath(), diff.getOldPath(), 0, diff
pcm = new PathChangeModel(diff.getOldPath(), diff.getOldPath(), filestoreItem, size, diff
.getNewMode().getBits(), diff.getOldId().name(), commitId, diff
.getChangeType());
} else if (diff.getChangeType().equals(ChangeType.RENAME)) {
pcm = new PathChangeModel(diff.getOldPath(), diff.getNewPath(), 0, diff
pcm = new PathChangeModel(diff.getOldPath(), diff.getNewPath(), filestoreItem, size, diff
.getNewMode().getBits(), diff.getNewId().name(), commitId, diff
.getChangeType());
} else {
pcm = new PathChangeModel(diff.getNewPath(), diff.getNewPath(), 0, diff
pcm = new PathChangeModel(diff.getNewPath(), diff.getNewPath(), filestoreItem, size, diff
.getNewMode().getBits(), diff.getNewId().name(), commitId, diff

@@ -171,0 +209,0 @@ .getChangeType());

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

Map<String, Change> comments = new HashMap<String, Change>();
Map<String, Change> references = new HashMap<String, Change>();
Map<Integer, Integer> latestRevisions = new HashMap<Integer, Integer>();
int latestPatchsetNumber = -1;
List<Integer> deletedPatchsets = new ArrayList<Integer>();
for (Change change : changes) {
if (change.patchset != null) {
if (change.patchset.isDeleted()) {
deletedPatchsets.add(change.patchset.number);
} else {
Integer latestRev = latestRevisions.get(change.patchset.number);
if (latestRev == null || change.patchset.rev > latestRev) {
latestRevisions.put(change.patchset.number, change.patchset.rev);
}
if (change.patchset.number > latestPatchsetNumber) {
latestPatchsetNumber = change.patchset.number;
}
}
}
}
for (Change change : changes) {
if (change.comment != null) {

@@ -126,2 +151,27 @@ if (comments.containsKey(change.comment.id)) {

}
} else if (change.patchset != null) {
//All revisions of a deleted patchset are not displayed
if (!deletedPatchsets.contains(change.patchset.number)) {
Integer latestRev = latestRevisions.get(change.patchset.number);
if ( (change.patchset.number < latestPatchsetNumber)
&& (change.patchset.rev == latestRev)) {
change.patchset.canDelete = true;
}
effectiveChanges.add(change);
}
} else if (change.reference != null){
if (references.containsKey(change.reference.toString())) {
Change original = references.get(change.reference.toString());
Change clone = copy(original);
clone.reference.deleted = change.reference.deleted;
int idx = effectiveChanges.indexOf(original);
effectiveChanges.remove(original);
effectiveChanges.add(idx, clone);
} else {
effectiveChanges.add(change);
references.put(change.reference.toString(), change);
}
} else {

@@ -135,6 +185,12 @@ effectiveChanges.add(change);

for (Change change : effectiveChanges) {
//Ensure deleted items are not included
if (!change.hasComment()) {
// ensure we do not include a deleted comment
change.comment = null;
}
if (!change.hasReference()) {
change.reference = null;
}
if (!change.hasPatchset()) {
change.patchset = null;
}
ticket.applyChange(change);

@@ -323,2 +379,11 @@ }

public boolean hasReferences() {
for (Change change : changes) {
if (change.hasReference()) {
return true;
}
}
return false;
}
public List<Attachment> getAttachments() {

@@ -334,2 +399,12 @@ List<Attachment> list = new ArrayList<Attachment>();

public List<Reference> getReferences() {
List<Reference> list = new ArrayList<Reference>();
for (Change change : changes) {
if (change.hasReference()) {
list.add(change.reference);
}
}
return list;
}
public List<Patchset> getPatchsets() {

@@ -544,4 +619,8 @@ List<Patchset> list = new ArrayList<Patchset>();

// add the change to the ticket
changes.add(change);
// add real changes to the ticket and ensure deleted changes are removed
if (change.isEmptyChange()) {
changes.remove(change);
} else {
changes.add(change);
}
}

@@ -617,2 +696,4 @@

public Reference reference;
public Map<Field, String> fields;

@@ -628,2 +709,6 @@

//Once links have been made they become a reference on the target ticket
//The ticket service handles promoting links to references
public transient List<TicketLink> pendingLinks;
public Change(String author) {

@@ -652,3 +737,3 @@ this(author, new Date());

public boolean hasPatchset() {
return patchset != null;
return patchset != null && !patchset.isDeleted();
}

@@ -663,3 +748,11 @@

}
public boolean hasReference() {
return reference != null && !reference.isDeleted();
}
public boolean hasPendingLinks() {
return pendingLinks != null && pendingLinks.size() > 0;
}
public Comment comment(String text) {

@@ -669,3 +762,26 @@ comment = new Comment(text);

// parse comment looking for ref #n
//TODO: Ideally set via settings
String x = "(?:ref|task|issue|bug)?[\\s-]*#(\\d+)";
try {
Pattern p = Pattern.compile(x, Pattern.CASE_INSENSITIVE);
Matcher m = p.matcher(text);
while (m.find()) {
String val = m.group(1);
long targetTicketId = Long.parseLong(val);
if (targetTicketId > 0) {
if (pendingLinks == null) {
pendingLinks = new ArrayList<TicketLink>();
}
pendingLinks.add(new TicketLink(targetTicketId, TicketAction.Comment));
}
}
} catch (Exception e) {
// ignore
}
try {
Pattern mentions = Pattern.compile("\\s@([A-Za-z0-9-_]+)");

@@ -683,2 +799,12 @@ Matcher m = mentions.matcher(text);

public Reference referenceCommit(String commitHash) {
reference = new Reference(commitHash);
return reference;
}
public Reference referenceTicket(long ticketId, String changeHash) {
reference = new Reference(ticketId, changeHash);
return reference;
}
public Review review(Patchset patchset, Score score, boolean addReviewer) {

@@ -854,2 +980,13 @@ if (addReviewer) {

}
/*
* Identify if this is an empty change. i.e. only an author and date is defined.
* This can occur when items have been deleted
* @returns true if the change is empty
*/
private boolean isEmptyChange() {
return ((comment == null) && (reference == null) &&
(fields == null) && (attachments == null) &&
(patchset == null) && (review == null));
}

@@ -864,2 +1001,4 @@ @Override

sb.append(MessageFormat.format(" {0} uploaded by ", patchset));
} else if (hasReference()) {
sb.append(MessageFormat.format(" referenced in {0} by ", reference));
} else {

@@ -1050,2 +1189,4 @@ sb.append(" changed by ");

public transient boolean canDelete = false;
public boolean isFF() {

@@ -1055,2 +1196,6 @@ return PatchsetType.FastForward == type;

public boolean isDeleted() {
return PatchsetType.Delete == type;
}
@Override

@@ -1121,3 +1266,111 @@ public int hashCode() {

}
public static enum TicketAction {
Commit, Comment, Patchset, Close
}
//Intentionally not serialized, links are persisted as "references"
public static class TicketLink {
public long targetTicketId;
public String hash;
public TicketAction action;
public boolean success;
public boolean isDelete;
public TicketLink(long targetTicketId, TicketAction action) {
this.targetTicketId = targetTicketId;
this.action = action;
success = false;
isDelete = false;
}
public TicketLink(long targetTicketId, TicketAction action, String hash) {
this.targetTicketId = targetTicketId;
this.action = action;
this.hash = hash;
success = false;
isDelete = false;
}
}
public static enum ReferenceType {
Undefined, Commit, Ticket;
@Override
public String toString() {
return name().toLowerCase().replace('_', ' ');
}
public static ReferenceType fromObject(Object o, ReferenceType defaultType) {
if (o instanceof ReferenceType) {
// cast and return
return (ReferenceType) o;
} else if (o instanceof String) {
// find by name
for (ReferenceType type : values()) {
String str = o.toString();
if (type.name().equalsIgnoreCase(str)
|| type.toString().equalsIgnoreCase(str)) {
return type;
}
}
} else if (o instanceof Number) {
// by ordinal
int id = ((Number) o).intValue();
if (id >= 0 && id < values().length) {
return values()[id];
}
}
return defaultType;
}
}
public static class Reference implements Serializable {
private static final long serialVersionUID = 1L;
public String hash;
public Long ticketId;
public Boolean deleted;
Reference(String commitHash) {
this.hash = commitHash;
}
Reference(long ticketId, String changeHash) {
this.ticketId = ticketId;
this.hash = changeHash;
}
public ReferenceType getSourceType(){
if (hash != null) {
if (ticketId != null) {
return ReferenceType.Ticket;
} else {
return ReferenceType.Commit;
}
}
return ReferenceType.Undefined;
}
public boolean isDeleted() {
return deleted != null && deleted;
}
@Override
public String toString() {
switch (getSourceType()) {
case Commit: return hash;
case Ticket: return ticketId.toString() + "#" + hash;
default: {} break;
}
return String.format("Unknown Reference Type");
}
}
public static class Attachment implements Serializable {

@@ -1307,3 +1560,3 @@

public static enum PatchsetType {
Proposal, FastForward, Rebase, Squash, Rebase_Squash, Amend;
Proposal, FastForward, Rebase, Squash, Rebase_Squash, Amend, Delete;

@@ -1310,0 +1563,0 @@ public boolean isRewrite() {

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

* Default is WWW-Authenticate
* @param httpRequest
* @param action
* @return authentication type header
*/
protected String getAuthenticationHeader(String action) {
protected String getAuthenticationHeader(HttpServletRequest httpRequest, String action) {
return "WWW-Authenticate";

@@ -196,3 +197,3 @@ }

httpResponse.setHeader(getAuthenticationHeader(urlRequestType), CHALLENGE);
httpResponse.setHeader(getAuthenticationHeader(httpRequest, urlRequestType), CHALLENGE);
httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED);

@@ -244,3 +245,3 @@ return;

}
httpResponse.setHeader(getAuthenticationHeader(urlRequestType), CHALLENGE);
httpResponse.setHeader(getAuthenticationHeader(httpRequest, urlRequestType), CHALLENGE);
httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED);

@@ -254,4 +255,4 @@ return;

newSession(authenticatedRequest, httpResponse);
logger.info(MessageFormat.format("ARF: {0} ({1}) authenticated", fullUrl,
HttpServletResponse.SC_CONTINUE));
logger.info(MessageFormat.format("ARF: authenticated {0} to {1} ({2})", user.username,
fullUrl, HttpServletResponse.SC_CONTINUE));
chain.doFilter(authenticatedRequest, httpResponse);

@@ -258,0 +259,0 @@ return;

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

import com.google.inject.Singleton;
import javax.servlet.ServletException;

@@ -38,2 +39,3 @@ import javax.servlet.http.HttpServlet;

import com.gitblit.Keys;
import com.gitblit.manager.IFilestoreManager;
import com.gitblit.manager.IRepositoryManager;

@@ -62,2 +64,4 @@ import com.gitblit.utils.CompressionUtils;

private IRepositoryManager repositoryManager;
private IFilestoreManager filestoreManager;

@@ -84,5 +88,6 @@ public static enum Format {

@Inject
public DownloadZipServlet(IStoredSettings settings, IRepositoryManager repositoryManager) {
public DownloadZipServlet(IStoredSettings settings, IRepositoryManager repositoryManager, IFilestoreManager filestoreManager) {
this.settings = settings;
this.repositoryManager = repositoryManager;
this.filestoreManager = filestoreManager;
}

@@ -176,18 +181,19 @@

try {
switch (format) {
case zip:
CompressionUtils.zip(r, basePath, objectId, response.getOutputStream());
CompressionUtils.zip(r, filestoreManager, basePath, objectId, response.getOutputStream());
break;
case tar:
CompressionUtils.tar(r, basePath, objectId, response.getOutputStream());
CompressionUtils.tar(r, filestoreManager, basePath, objectId, response.getOutputStream());
break;
case gz:
CompressionUtils.gz(r, basePath, objectId, response.getOutputStream());
CompressionUtils.gz(r, filestoreManager, basePath, objectId, response.getOutputStream());
break;
case xz:
CompressionUtils.xz(r, basePath, objectId, response.getOutputStream());
CompressionUtils.xz(r, filestoreManager, basePath, objectId, response.getOutputStream());
break;
case bzip2:
CompressionUtils.bzip2(r, basePath, objectId, response.getOutputStream());
CompressionUtils.bzip2(r, filestoreManager, basePath, objectId, response.getOutputStream());
break;

@@ -194,0 +200,0 @@ }

@@ -66,3 +66,3 @@ /*

public static final String REGEX_PATH = "^(.*?)/(r|git)/(.*?)/info/lfs/objects/(batch|" + Constants.REGEX_SHA256 + ")";
public static final String REGEX_PATH = "^(.*?)/(r)/(.*?)/info/lfs/objects/(batch|" + Constants.REGEX_SHA256 + ")";
public static final int REGEX_GROUP_BASE_URI = 1;

@@ -242,3 +242,6 @@ public static final int REGEX_GROUP_PREFIX = 2;

response.setStatus(responseObject.error.code);
serialize(response, responseObject.error);
if (isMetaRequest) {
serialize(response, responseObject.error);
}
}

@@ -245,0 +248,0 @@ };

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

import java.util.List;
import java.util.Set;

@@ -459,3 +460,8 @@ import javax.naming.Context;

protected void extractResources(ServletContext context, String path, File toDir) {
for (String resource : context.getResourcePaths(path)) {
Set<String> resources = context.getResourcePaths(path);
if (resources == null) {
logger.warn("There are no WAR resources to extract from {}", path);
return;
}
for (String resource : resources) {
// extract the resource to the directory if it does not exist

@@ -462,0 +468,0 @@ File f = new File(toDir, resource.substring(path.length()));

@@ -105,4 +105,4 @@ /*

/**
* Analyze the url and returns the action of the request. Return values are
* either "/git-receive-pack" or "/git-upload-pack".
* Analyze the url and returns the action of the request. Return values are:
* "/git-receive-pack", "/git-upload-pack" or "/info/lfs".
*

@@ -320,3 +320,5 @@ * @param serverUrl

* Git lfs action uses an alternative authentication header,
* dependent on the viewing method.
*
* @param httpRequest
* @param action

@@ -326,9 +328,11 @@ * @return

@Override
protected String getAuthenticationHeader(String action) {
protected String getAuthenticationHeader(HttpServletRequest httpRequest, String action) {
if (action.equals(gitLfs)) {
return "LFS-Authenticate";
if (hasContentInRequestHeader(httpRequest, "Accept", FilestoreServlet.GIT_LFS_META_MIME)) {
return "LFS-Authenticate";
}
}
return super.getAuthenticationHeader(action);
return super.getAuthenticationHeader(httpRequest, action);
}

@@ -335,0 +339,0 @@

@@ -169,19 +169,10 @@ /*

// determine repository and resource from url
String repository = "";
String repository = path;
Repository r = null;
int offset = 0;
while (r == null) {
int slash = path.indexOf('/', offset);
if (slash == -1) {
repository = path;
} else {
repository = path.substring(0, slash);
}
offset = ( slash + 1 );
int terminator = repository.length();
do {
repository = repository.substring(0, terminator);
r = repositoryManager.getRepository(repository, false);
if (repository.equals(path)) {
// either only repository in url or no repository found
break;
}
}
terminator = repository.lastIndexOf('/');
} while (r == null && terminator > -1 );

@@ -246,3 +237,10 @@ ServletContext context = request.getSession().getServletContext();

String ext = StringUtils.getFileExtension(file).toLowerCase();
String contentType = quickContentTypes.get(ext);
// We can't parse out an extension for classic "dotfiles", so make a general assumption that
// they're text files to allow presenting them in browser instead of only for download.
//
// However, that only holds for files with no other extension included, for files that happen
// to start with a dot but also include an extension, process the extension normally.
// This logic covers .gitattributes, .gitignore, .zshrc, etc., but does not cover .mongorc.js, .zshrc.bak
boolean isExtensionlessDotfile = file.charAt(0) == '.' && (file.length() == 1 || file.indexOf('.', 1) < 0);
String contentType = isExtensionlessDotfile ? "text/plain" : quickContentTypes.get(ext);

@@ -371,3 +369,3 @@ if (contentType == null) {

String pp = URLEncoder.encode(requestedPath, Constants.ENCODING);
pathEntries.add(0, new PathModel("..", pp + "/..", 0, FileMode.TREE.getBits(), null, null));
pathEntries.add(0, new PathModel("..", pp + "/..", null, 0, FileMode.TREE.getBits(), null, null));
}

@@ -374,0 +372,0 @@ }

@@ -131,3 +131,3 @@ /*

// check user access for request
if (user.canAdmin() || canAccess(user, requestType)) {
if (user.canAdmin() || !adminRequest) {
// authenticated request permitted.

@@ -157,13 +157,2 @@ // pass processing to the restricted servlet.

}
private boolean canAccess(UserModel user, RpcRequest requestType) {
switch (requestType) {
case GET_PROTOCOL:
return true;
case LIST_REPOSITORIES:
return true;
default:
return user.canAdmin();
}
}
}
}

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

import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;

@@ -35,3 +34,2 @@ import java.util.HashSet;

import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
import org.eclipse.jgit.api.errors.JGitInternalException;
import org.eclipse.jgit.dircache.DirCache;

@@ -42,4 +40,2 @@ import org.eclipse.jgit.dircache.DirCacheBuilder;

import org.eclipse.jgit.events.RefsChangedListener;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.FileMode;

@@ -51,3 +47,2 @@ import org.eclipse.jgit.lib.ObjectId;

import org.eclipse.jgit.lib.RefRename;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.RefUpdate.Result;

@@ -345,3 +340,3 @@ import org.eclipse.jgit.lib.Repository;

for (DirCacheEntry entry : getTreeEntries(db, ignorePaths)) {
for (DirCacheEntry entry : JGitUtils.getTreeEntries(db, BRANCH, ignorePaths)) {
builder.add(entry);

@@ -812,3 +807,3 @@ }

for (DirCacheEntry entry : getTreeEntries(db, ignorePaths)) {
for (DirCacheEntry entry : JGitUtils.getTreeEntries(db, BRANCH, ignorePaths)) {
builder.add(entry);

@@ -825,50 +820,6 @@ }

/**
* Returns all tree entries that do not match the ignore paths.
*
* @param db
* @param ignorePaths
* @param dcBuilder
* @throws IOException
*/
private List<DirCacheEntry> getTreeEntries(Repository db, Collection<String> ignorePaths) throws IOException {
List<DirCacheEntry> list = new ArrayList<DirCacheEntry>();
TreeWalk tw = null;
try {
ObjectId treeId = db.resolve(BRANCH + "^{tree}");
if (treeId == null) {
// branch does not exist yet, could be migrating tickets
return list;
}
tw = new TreeWalk(db);
int hIdx = tw.addTree(treeId);
tw.setRecursive(true);
while (tw.next()) {
String path = tw.getPathString();
CanonicalTreeParser hTree = null;
if (hIdx != -1) {
hTree = tw.getTree(hIdx, CanonicalTreeParser.class);
}
if (!ignorePaths.contains(path)) {
// add all other tree entries
if (hTree != null) {
final DirCacheEntry entry = new DirCacheEntry(path);
entry.setObjectId(hTree.getEntryObjectId());
entry.setFileMode(hTree.getEntryFileMode());
list.add(entry);
}
}
}
} finally {
if (tw != null) {
tw.close();
}
}
return list;
}
private boolean commitIndex(Repository db, DirCache index, String author, String message) throws IOException, ConcurrentRefUpdateException {
final boolean forceCommit = true;
boolean success = false;
ObjectId headId = db.resolve(BRANCH + "^{commit}");

@@ -878,52 +829,6 @@ if (headId == null) {

createTicketsBranch(db);
headId = db.resolve(BRANCH + "^{commit}");
}
ObjectInserter odi = db.newObjectInserter();
try {
// Create the in-memory index of the new/updated ticket
ObjectId indexTreeId = index.writeTree(odi);
// Create a commit object
PersonIdent ident = new PersonIdent(author, "gitblit@localhost");
CommitBuilder commit = new CommitBuilder();
commit.setAuthor(ident);
commit.setCommitter(ident);
commit.setEncoding(Constants.ENCODING);
commit.setMessage(message);
commit.setParentId(headId);
commit.setTreeId(indexTreeId);
// Insert the commit into the repository
ObjectId commitId = odi.insert(commit);
odi.flush();
RevWalk revWalk = new RevWalk(db);
try {
RevCommit revCommit = revWalk.parseCommit(commitId);
RefUpdate ru = db.updateRef(BRANCH);
ru.setNewObjectId(commitId);
ru.setExpectedOldObjectId(headId);
ru.setRefLogMessage("commit: " + revCommit.getShortMessage(), false);
Result rc = ru.forceUpdate();
switch (rc) {
case NEW:
case FORCED:
case FAST_FORWARD:
success = true;
break;
case REJECTED:
case LOCK_FAILURE:
throw new ConcurrentRefUpdateException(JGitText.get().couldNotLockHEAD,
ru.getRef(), rc);
default:
throw new JGitInternalException(MessageFormat.format(
JGitText.get().updatingRefFailed, BRANCH, commitId.toString(),
rc));
}
} finally {
revWalk.close();
}
} finally {
odi.close();
}
success = JGitUtils.commitIndex(db, BRANCH, index, headId, forceCommit, author, "gitblit@localhost", message);
return success;

@@ -930,0 +835,0 @@ }

@@ -51,6 +51,9 @@ /*

import com.gitblit.models.TicketModel.Patchset;
import com.gitblit.models.TicketModel.PatchsetType;
import com.gitblit.models.TicketModel.Status;
import com.gitblit.models.TicketModel.TicketLink;
import com.gitblit.tickets.TicketIndexer.Lucene;
import com.gitblit.utils.DeepCopier;
import com.gitblit.utils.DiffUtils;
import com.gitblit.utils.JGitUtils;
import com.gitblit.utils.DiffUtils.DiffStat;

@@ -1024,8 +1027,8 @@ import com.gitblit.utils.StringUtils;

/**
* Updates a ticket.
* Updates a ticket and promotes pending links into references.
*
* @param repository
* @param ticketId
* @param ticketId, or 0 to action pending links in general
* @param change
* @return the ticket model if successful
* @return the ticket model if successful, null if failure or using 0 ticketId
* @since 1.4.0

@@ -1042,24 +1045,74 @@ */

TicketKey key = new TicketKey(repository, ticketId);
ticketsCache.invalidate(key);
boolean success = commitChangeImpl(repository, ticketId, change);
boolean success = true;
TicketModel ticket = null;
if (ticketId > 0) {
TicketKey key = new TicketKey(repository, ticketId);
ticketsCache.invalidate(key);
success = commitChangeImpl(repository, ticketId, change);
if (success) {
ticket = getTicket(repository, ticketId);
ticketsCache.put(key, ticket);
indexer.index(ticket);
// call the ticket hooks
if (pluginManager != null) {
for (TicketHook hook : pluginManager.getExtensions(TicketHook.class)) {
try {
hook.onUpdateTicket(ticket, change);
} catch (Exception e) {
log.error("Failed to execute extension", e);
}
}
}
}
}
if (success) {
TicketModel ticket = getTicket(repository, ticketId);
ticketsCache.put(key, ticket);
indexer.index(ticket);
//Now that the ticket has been successfully persisted add references to this ticket from linked tickets
if (change.hasPendingLinks()) {
for (TicketLink link : change.pendingLinks) {
TicketModel linkedTicket = getTicket(repository, link.targetTicketId);
Change dstChange = null;
//Ignore if not available or self reference
if (linkedTicket != null && link.targetTicketId != ticketId) {
dstChange = new Change(change.author, change.date);
switch (link.action) {
case Comment: {
if (ticketId == 0) {
throw new RuntimeException("must specify a ticket when linking a comment!");
}
dstChange.referenceTicket(ticketId, change.comment.id);
} break;
case Commit: {
dstChange.referenceCommit(link.hash);
} break;
default: {
throw new RuntimeException(
String.format("must add persist logic for link of type %s", link.action));
}
}
}
if (dstChange != null) {
//If not deleted then remain null in journal
if (link.isDelete) {
dstChange.reference.deleted = true;
}
// call the ticket hooks
if (pluginManager != null) {
for (TicketHook hook : pluginManager.getExtensions(TicketHook.class)) {
try {
hook.onUpdateTicket(ticket, change);
} catch (Exception e) {
log.error("Failed to execute extension", e);
if (updateTicket(repository, link.targetTicketId, dstChange) != null) {
link.success = true;
}
}
}
}
return ticket;
}
return null;
return ticket;
}

@@ -1219,2 +1272,35 @@

}
/**
* Deletes a patchset from a ticket.
*
* @param ticket
* @param patchset
* the patchset to delete (should be the highest revision)
* @param userName
* the user deleting the commit
* @return the revised ticket if the deletion was successful
* @since 1.8.0
*/
public final TicketModel deletePatchset(TicketModel ticket, Patchset patchset, String userName) {
Change deletion = new Change(userName);
deletion.patchset = new Patchset();
deletion.patchset.number = patchset.number;
deletion.patchset.rev = patchset.rev;
deletion.patchset.type = PatchsetType.Delete;
//Find and delete references to tickets by the removed commits
List<TicketLink> patchsetTicketLinks = JGitUtils.identifyTicketsBetweenCommits(
repositoryManager.getRepository(ticket.repository),
settings, patchset.base, patchset.tip);
for (TicketLink link : patchsetTicketLinks) {
link.isDelete = true;
}
deletion.pendingLinks = patchsetTicketLinks;
RepositoryModel repositoryModel = repositoryManager.getRepositoryModel(ticket.repository);
TicketModel revisedTicket = updateTicket(repositoryModel, ticket.number, deletion);
return revisedTicket;
}

@@ -1221,0 +1307,0 @@ /**

@@ -320,2 +320,15 @@ /*

sb.append(HARD_BRK);
} else if (lastChange.hasReference()) {
// reference update
String type = "?";
switch (lastChange.reference.getSourceType()) {
case Commit: { type = "commit"; } break;
case Ticket: { type = "ticket"; } break;
default: { } break;
}
sb.append(MessageFormat.format("**{0}** referenced this ticket in {1} {2}", type, lastChange.toString()));
sb.append(HARD_BRK);
} else {

@@ -322,0 +335,0 @@ // general update

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

import java.io.ByteArrayOutputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

@@ -32,5 +35,7 @@ import java.io.OutputStream;

import org.apache.commons.compress.compressors.CompressorStreamFactory;
import org.apache.commons.io.IOUtils;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.MutableObjectId;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectLoader;

@@ -46,2 +51,7 @@ import org.eclipse.jgit.lib.ObjectReader;

import com.gitblit.GitBlit;
import com.gitblit.manager.IFilestoreManager;
import com.gitblit.models.FilestoreModel;
import com.gitblit.models.FilestoreModel.Status;
/**

@@ -93,3 +103,3 @@ * Collection of static methods for retrieving information from a repository.

*/
public static boolean zip(Repository repository, String basePath, String objectId,
public static boolean zip(Repository repository, IFilestoreManager filestoreManager, String basePath, String objectId,
OutputStream os) {

@@ -122,5 +132,16 @@ RevCommit commit = JGitUtils.getCommit(repository, objectId);

tw.getObjectId(id, 0);
ObjectLoader loader = repository.open(id);
ZipArchiveEntry entry = new ZipArchiveEntry(tw.getPathString());
ZipArchiveEntry entry = new ZipArchiveEntry(tw.getPathString());
entry.setSize(reader.getObjectSize(id, Constants.OBJ_BLOB));
FilestoreModel filestoreItem = null;
if (JGitUtils.isPossibleFilestoreItem(loader.getSize())) {
filestoreItem = JGitUtils.getFilestoreItem(tw.getObjectReader().open(id));
}
final long size = (filestoreItem == null) ? loader.getSize() : filestoreItem.getSize();
entry.setSize(size);
entry.setComment(commit.getName());

@@ -130,5 +151,18 @@ entry.setUnixMode(mode.getBits());

zos.putArchiveEntry(entry);
if (filestoreItem == null) {
//Copy repository stored file
loader.copyTo(zos);
} else {
//Copy filestore file
try (FileInputStream streamIn = new FileInputStream(filestoreManager.getStoragePath(filestoreItem.oid))) {
IOUtils.copyLarge(streamIn, zos);
} catch (Throwable e) {
LOGGER.error(MessageFormat.format("Failed to archive filestore item {0}", filestoreItem.oid), e);
ObjectLoader ldr = repository.open(id);
ldr.copyTo(zos);
//Handle as per other errors
throw e;
}
}
zos.closeArchiveEntry();

@@ -160,5 +194,5 @@ }

*/
public static boolean tar(Repository repository, String basePath, String objectId,
public static boolean tar(Repository repository, IFilestoreManager filestoreManager, String basePath, String objectId,
OutputStream os) {
return tar(null, repository, basePath, objectId, os);
return tar(null, repository, filestoreManager, basePath, objectId, os);
}

@@ -179,5 +213,5 @@

*/
public static boolean gz(Repository repository, String basePath, String objectId,
public static boolean gz(Repository repository, IFilestoreManager filestoreManager, String basePath, String objectId,
OutputStream os) {
return tar(CompressorStreamFactory.GZIP, repository, basePath, objectId, os);
return tar(CompressorStreamFactory.GZIP, repository, filestoreManager, basePath, objectId, os);
}

@@ -198,5 +232,5 @@

*/
public static boolean xz(Repository repository, String basePath, String objectId,
public static boolean xz(Repository repository, IFilestoreManager filestoreManager, String basePath, String objectId,
OutputStream os) {
return tar(CompressorStreamFactory.XZ, repository, basePath, objectId, os);
return tar(CompressorStreamFactory.XZ, repository, filestoreManager, basePath, objectId, os);
}

@@ -217,6 +251,6 @@

*/
public static boolean bzip2(Repository repository, String basePath, String objectId,
public static boolean bzip2(Repository repository, IFilestoreManager filestoreManager, String basePath, String objectId,
OutputStream os) {
return tar(CompressorStreamFactory.BZIP2, repository, basePath, objectId, os);
return tar(CompressorStreamFactory.BZIP2, repository, filestoreManager, basePath, objectId, os);
}

@@ -240,3 +274,3 @@

*/
private static boolean tar(String algorithm, Repository repository, String basePath, String objectId,
private static boolean tar(String algorithm, Repository repository, IFilestoreManager filestoreManager, String basePath, String objectId,
OutputStream os) {

@@ -277,2 +311,3 @@ RevCommit commit = JGitUtils.getCommit(repository, objectId);

}
tw.getObjectId(id, 0);

@@ -293,5 +328,30 @@

entry.setModTime(modified);
entry.setSize(loader.getSize());
FilestoreModel filestoreItem = null;
if (JGitUtils.isPossibleFilestoreItem(loader.getSize())) {
filestoreItem = JGitUtils.getFilestoreItem(tw.getObjectReader().open(id));
}
final long size = (filestoreItem == null) ? loader.getSize() : filestoreItem.getSize();
entry.setSize(size);
tos.putArchiveEntry(entry);
loader.copyTo(tos);
if (filestoreItem == null) {
//Copy repository stored file
loader.copyTo(tos);
} else {
//Copy filestore file
try (FileInputStream streamIn = new FileInputStream(filestoreManager.getStoragePath(filestoreItem.oid))) {
IOUtils.copyLarge(streamIn, tos);
} catch (Throwable e) {
LOGGER.error(MessageFormat.format("Failed to archive filestore item {0}", filestoreItem.oid), e);
//Handle as per other errors
throw e;
}
}
tos.closeArchiveEntry();

@@ -298,0 +358,0 @@ }

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

import org.eclipse.jgit.diff.RawText;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.util.io.NullOutputStream;

@@ -41,5 +42,5 @@

public DiffStatFormatter(String commitId) {
public DiffStatFormatter(String commitId, Repository repository) {
super(NullOutputStream.INSTANCE);
diffStat = new DiffStat(commitId);
diffStat = new DiffStat(commitId, repository);
}

@@ -46,0 +47,0 @@

@@ -160,9 +160,12 @@ /*

private final String commitId;
private final Repository repository;
public DiffStat(String commitId) {
public DiffStat(String commitId, Repository repository) {
this.commitId = commitId;
this.repository = repository;
}
public PathChangeModel addPath(DiffEntry entry) {
PathChangeModel pcm = PathChangeModel.from(entry, commitId);
PathChangeModel pcm = PathChangeModel.from(entry, commitId, repository);
paths.add(pcm);

@@ -383,3 +386,3 @@ return pcm;

case HTML:
df = new GitBlitDiffFormatter(commit.getName(), path, handler, tabLength);
df = new GitBlitDiffFormatter(commit.getName(), repository, path, handler, tabLength);
break;

@@ -553,3 +556,3 @@ case PLAIN:

RawTextComparator cmp = RawTextComparator.DEFAULT;
DiffStatFormatter df = new DiffStatFormatter(commit.getName());
DiffStatFormatter df = new DiffStatFormatter(commit.getName(), repository);
df.setRepository(repository);

@@ -556,0 +559,0 @@ df.setDiffComparator(cmp);

@@ -143,5 +143,6 @@ /*

InputStreamReader is = null;
BufferedReader reader = null;
try {
is = new InputStreamReader(new FileInputStream(file), Charset.forName("UTF-8"));
BufferedReader reader = new BufferedReader(is);
reader = new BufferedReader(is);
String line = null;

@@ -158,2 +159,10 @@ while ((line = reader.readLine()) != null) {

} finally {
if (reader != null){
try {
reader.close();
} catch (IOException ioe) {
System.err.println("Failed to close file " + file.getAbsolutePath());
ioe.printStackTrace();
}
}
if (is != null) {

@@ -160,0 +169,0 @@ try {

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

import org.eclipse.jgit.diff.RawText;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.util.RawParseUtils;

@@ -168,7 +169,7 @@

public GitBlitDiffFormatter(String commitId, String path, BinaryDiffHandler handler, int tabLength) {
public GitBlitDiffFormatter(String commitId, Repository repository, String path, BinaryDiffHandler handler, int tabLength) {
super(new DiffOutputStream());
this.os = (DiffOutputStream) getOutputStream();
this.os.setFormatter(this, handler);
this.diffStat = new DiffStat(commitId);
this.diffStat = new DiffStat(commitId, repository);
this.tabLength = tabLength;

@@ -175,0 +176,0 @@ // If we have a full commitdiff, install maxima to avoid generating a super-long diff listing that

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

// try to use reverse-proxy's context
String context = request.getContextPath();

@@ -96,9 +97,20 @@ String forwardedContext = request.getHeader("X-Forwarded-Context");

// try to use reverse-proxy's hostname
String host = request.getServerName();
String forwardedHost = request.getHeader("X-Forwarded-Host");
if (StringUtils.isEmpty(forwardedHost)) {
forwardedHost = request.getHeader("X_Forwarded_Host");
}
if (!StringUtils.isEmpty(forwardedHost)) {
host = forwardedHost;
}
// build result
StringBuilder sb = new StringBuilder();
sb.append(scheme);
sb.append("://");
sb.append(request.getServerName());
sb.append(host);
if (("http".equals(scheme) && port != 80)
|| ("https".equals(scheme) && port != 443)) {
sb.append(":" + port);
sb.append(":").append(port);
}

@@ -105,0 +117,0 @@ sb.append(context);

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

import com.gitblit.wicket.pages.DocsPage;
import com.gitblit.wicket.pages.EditFilePage;
import com.gitblit.wicket.pages.EditMilestonePage;

@@ -234,2 +235,3 @@ import com.gitblit.wicket.pages.EditRepositoryPage;

mount("/doc", DocPage.class, "r", "h", "f");
mount("/editfile", EditFilePage.class, "r", "h", "f");

@@ -236,0 +238,0 @@ // federation urls

@@ -773,2 +773,12 @@ gb.repository = repository

gb.statusChangedBy = status changed by
gb.filestoreHelp = How to use the Filestore?
gb.filestoreHelp = How to use the Filestore?
gb.editFile = edit file
gb.continueEditing = Continue Editing
gb.commitChanges = Commit Changes
gb.fileNotMergeable = Unable to commit {0}. This file can not be automatically merged.
gb.fileCommitted = Successfully committed {0}.
gb.deletePatchset = Delete Patchset {0}
gb.deletePatchsetSuccess = Deleted Patchset {0}.
gb.deletePatchsetFailure = Error deleting Patchset {0}.
gb.referencedByCommit = Referenced by commit.
gb.referencedByTicket = Referenced by ticket.

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

import org.apache.wicket.markup.html.link.BookmarkablePageLink;
import org.apache.wicket.markup.html.link.ExternalLink;
import org.apache.wicket.markup.repeater.Item;

@@ -97,5 +98,30 @@ import org.apache.wicket.markup.repeater.data.DataView;

RevCommit commit = getCommit();
add(new BookmarkablePageLink<Void>("blobLink", BlobPage.class,
WicketUtils.newPathParameter(repositoryName, objectId, blobPath)));
PathModel pathModel = null;
List<PathModel> paths = JGitUtils.getFilesInPath(getRepository(), StringUtils.getRootPath(blobPath), commit);
for (PathModel path : paths) {
if (path.path.equals(blobPath)) {
pathModel = path;
break;
}
}
if (pathModel == null) {
final String notFound = MessageFormat.format("Blame page failed to find {0} in {1} @ {2}",
blobPath, repositoryName, objectId);
logger.error(notFound);
add(new Label("annotation").setVisible(false));
add(new Label("missingBlob", missingBlob(blobPath, commit)).setEscapeModelStrings(false));
return;
}
if (pathModel.isFilestoreItem()) {
String rawUrl = JGitUtils.getLfsRepositoryUrl(getContextUrl(), repositoryName, pathModel.getFilestoreOid());
add(new ExternalLink("blobLink", rawUrl));
} else {
add(new BookmarkablePageLink<Void>("blobLink", BlobPage.class,
WicketUtils.newPathParameter(repositoryName, objectId, blobPath)));
}
add(new BookmarkablePageLink<Void>("commitLink", CommitPage.class,

@@ -139,19 +165,5 @@ WicketUtils.newObjectParameter(repositoryName, objectId)));

PathModel pathModel = null;
List<PathModel> paths = JGitUtils.getFilesInPath(getRepository(), StringUtils.getRootPath(blobPath), commit);
for (PathModel path : paths) {
if (path.path.equals(blobPath)) {
pathModel = path;
break;
}
}
if (pathModel == null) {
final String notFound = MessageFormat.format("Blame page failed to find {0} in {1} @ {2}",
blobPath, repositoryName, objectId);
logger.error(notFound);
add(new Label("annotation").setVisible(false));
add(new Label("missingBlob", missingBlob(blobPath, commit)).setEscapeModelStrings(false));
return;
}

@@ -158,0 +170,0 @@ add(new Label("missingBlob").setVisible(false));

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

<td class="hidden-phone rightAlign">
<span wicket:id="filestore" style="margin-right:20px;" class="fa fa-fw fa-external-link-square filestore-item"></span>
<span class="hidden-tablet" style="padding-right:20px;" wicket:id="diffStat"></span>

@@ -50,0 +51,0 @@ <span class="link">

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

import java.io.OutputStream;
import java.util.ArrayList;

@@ -27,5 +28,9 @@ import java.util.Arrays;

import org.apache.wicket.markup.html.link.ExternalLink;
import org.apache.wicket.markup.html.link.Link;
import org.apache.wicket.markup.repeater.Item;
import org.apache.wicket.markup.repeater.data.DataView;
import org.apache.wicket.markup.repeater.data.ListDataProvider;
import org.apache.wicket.request.target.resource.ResourceStreamRequestTarget;
import org.apache.wicket.util.resource.AbstractResourceStreamWriter;
import org.apache.wicket.util.resource.IResourceStream;
import org.eclipse.jgit.diff.DiffEntry.ChangeType;

@@ -40,2 +45,3 @@ import org.eclipse.jgit.lib.Repository;

import com.gitblit.models.SubmoduleModel;
import com.gitblit.models.UserModel;
import com.gitblit.servlet.RawServlet;

@@ -48,2 +54,3 @@ import com.gitblit.utils.DiffUtils;

import com.gitblit.wicket.CacheControl;
import com.gitblit.wicket.GitBlitWebSession;
import com.gitblit.wicket.CacheControl.LastModified;

@@ -142,2 +149,3 @@ import com.gitblit.wicket.WicketUtils;

final PathChangeModel entry = item.getModelObject();
Label changeType = new Label("changeType", "");

@@ -148,5 +156,8 @@ WicketUtils.setChangeTypeCssClass(changeType, entry.changeType);

item.add(new DiffStatPanel("diffStat", entry.insertions, entry.deletions, true));
item.add(WicketUtils.setHtmlTooltip(new Label("filestore", ""), getString("gb.filestore"))
.setVisible(entry.isFilestoreItem()));
boolean hasSubmodule = false;
String submodulePath = null;
if (entry.isTree()) {

@@ -168,3 +179,30 @@ // tree

// add relative link
item.add(new LinkPanel("pathName", "list", entry.path, "#n" + entry.objectId));
if (entry.isFilestoreItem()) {
item.add(new LinkPanel("pathName", "list", entry.path, new Link<Object>("link", null) {
private static final long serialVersionUID = 1L;
@Override
public void onClick() {
IResourceStream resourceStream = new AbstractResourceStreamWriter() {
private static final long serialVersionUID = 1L;
@Override
public void write(OutputStream output) {
UserModel user = GitBlitWebSession.get().getUser();
user = user == null ? UserModel.ANONYMOUS : user;
app().filestore().downloadBlob(entry.getFilestoreOid(), user, getRepositoryModel(), output);
}
};
getRequestCycle().setRequestTarget(new ResourceStreamRequestTarget(resourceStream, entry.path));
}}));
}
else
{
item.add(new LinkPanel("pathName", "list", entry.path, "#n" + entry.objectId));
}
}

@@ -189,8 +227,60 @@

&& !entry.changeType.equals(ChangeType.DELETE)));
item.add(new BookmarkablePageLink<Void>("view", BlobPage.class, WicketUtils
.newPathParameter(repositoryName, entry.commitId, entry.path))
.setEnabled(!entry.changeType.equals(ChangeType.DELETE)));
String rawUrl = RawServlet.asLink(getContextUrl(), repositoryName, entry.commitId, entry.path);
item.add(new ExternalLink("raw", rawUrl)
.setEnabled(!entry.changeType.equals(ChangeType.DELETE)));
if (entry.isFilestoreItem()) {
item.add(new Link<Object>("view", null) {
private static final long serialVersionUID = 1L;
@Override
public void onClick() {
IResourceStream resourceStream = new AbstractResourceStreamWriter() {
private static final long serialVersionUID = 1L;
@Override
public void write(OutputStream output) {
UserModel user = GitBlitWebSession.get().getUser();
user = user == null ? UserModel.ANONYMOUS : user;
app().filestore().downloadBlob(entry.getFilestoreOid(), user, getRepositoryModel(), output);
}
};
getRequestCycle().setRequestTarget(new ResourceStreamRequestTarget(resourceStream, entry.path));
}});
item.add(new Link<Object>("raw", null) {
private static final long serialVersionUID = 1L;
@Override
public void onClick() {
IResourceStream resourceStream = new AbstractResourceStreamWriter() {
private static final long serialVersionUID = 1L;
@Override
public void write(OutputStream output) {
UserModel user = GitBlitWebSession.get().getUser();
user = user == null ? UserModel.ANONYMOUS : user;
app().filestore().downloadBlob(entry.getFilestoreOid(), user, getRepositoryModel(), output);
}
};
getRequestCycle().setRequestTarget(new ResourceStreamRequestTarget(resourceStream, entry.path));
}});
} else {
item.add(new BookmarkablePageLink<Void>("view", BlobPage.class, WicketUtils
.newPathParameter(repositoryName, entry.commitId, entry.path))
.setEnabled(!entry.changeType.equals(ChangeType.DELETE)));
item.add(new ExternalLink("raw", RawServlet.asLink(getContextUrl(), repositoryName, entry.commitId, entry.path))
.setEnabled(!entry.changeType.equals(ChangeType.DELETE)));
}
item.add(new BookmarkablePageLink<Void>("blame", BlamePage.class, WicketUtils

@@ -197,0 +287,0 @@ .newPathParameter(repositoryName, entry.commitId, entry.path))

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

<td class="changeType"><span wicket:id="changeType">[change type]</span></td>
<td class="path"><span wicket:id="pathName">[commit path]</span></td>
<td class="path"><span wicket:id="pathName">[commit path]</span></td>
<td class="hidden-phone rightAlign">
<span wicket:id="filestore" style="margin-right:20px;" class="fa fa-fw fa-external-link-square filestore-item"></span>
<span class="hidden-tablet" style="padding-right:20px;" wicket:id="diffStat"></span>

@@ -84,0 +85,0 @@ <span class="link">

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

import java.io.OutputStream;
import java.util.ArrayList;

@@ -27,2 +28,3 @@ import java.util.Arrays;

import org.apache.wicket.markup.html.link.ExternalLink;
import org.apache.wicket.markup.html.link.Link;
import org.apache.wicket.markup.repeater.Item;

@@ -32,2 +34,5 @@ import org.apache.wicket.markup.repeater.data.DataView;

import org.apache.wicket.model.StringResourceModel;
import org.apache.wicket.request.target.resource.ResourceStreamRequestTarget;
import org.apache.wicket.util.resource.AbstractResourceStreamWriter;
import org.apache.wicket.util.resource.IResourceStream;
import org.eclipse.jgit.diff.DiffEntry.ChangeType;

@@ -41,5 +46,7 @@ import org.eclipse.jgit.lib.Repository;

import com.gitblit.models.SubmoduleModel;
import com.gitblit.models.UserModel;
import com.gitblit.servlet.RawServlet;
import com.gitblit.utils.JGitUtils;
import com.gitblit.wicket.CacheControl;
import com.gitblit.wicket.GitBlitWebSession;
import com.gitblit.wicket.CacheControl.LastModified;

@@ -170,2 +177,3 @@ import com.gitblit.wicket.WicketUtils;

final PathChangeModel entry = item.getModelObject();
Label changeType = new Label("changeType", "");

@@ -176,2 +184,4 @@ WicketUtils.setChangeTypeCssClass(changeType, entry.changeType);

item.add(new DiffStatPanel("diffStat", entry.insertions, entry.deletions, true));
item.add(WicketUtils.setHtmlTooltip(new Label("filestore", ""), getString("gb.filestore"))
.setVisible(entry.isFilestoreItem()));

@@ -203,5 +213,33 @@ boolean hasSubmodule = false;

}
item.add(new LinkPanel("pathName", "list", displayPath, BlobPage.class,
WicketUtils
.newPathParameter(repositoryName, entry.commitId, path)));
if (entry.isFilestoreItem()) {
item.add(new LinkPanel("pathName", "list", entry.path, new Link<Object>("link", null) {
private static final long serialVersionUID = 1L;
@Override
public void onClick() {
IResourceStream resourceStream = new AbstractResourceStreamWriter() {
private static final long serialVersionUID = 1L;
@Override
public void write(OutputStream output) {
UserModel user = GitBlitWebSession.get().getUser();
user = user == null ? UserModel.ANONYMOUS : user;
app().filestore().downloadBlob(entry.getFilestoreOid(), user, getRepositoryModel(), output);
}
};
getRequestCycle().setRequestTarget(new ResourceStreamRequestTarget(resourceStream, entry.path));
}}));
} else {
item.add(new LinkPanel("pathName", "list", displayPath, BlobPage.class,
WicketUtils.newPathParameter(repositoryName, entry.commitId, path)));
}
}

@@ -230,8 +268,60 @@

&& !entry.changeType.equals(ChangeType.DELETE)));
item.add(new BookmarkablePageLink<Void>("view", BlobPage.class, WicketUtils
.newPathParameter(repositoryName, entry.commitId, entry.path))
.setEnabled(!entry.changeType.equals(ChangeType.DELETE)));
String rawUrl = RawServlet.asLink(getContextUrl(), repositoryName, entry.commitId, entry.path);
item.add(new ExternalLink("raw", rawUrl)
.setEnabled(!entry.changeType.equals(ChangeType.DELETE)));
if (entry.isFilestoreItem()) {
item.add(new Link<Object>("view", null) {
private static final long serialVersionUID = 1L;
@Override
public void onClick() {
IResourceStream resourceStream = new AbstractResourceStreamWriter() {
private static final long serialVersionUID = 1L;
@Override
public void write(OutputStream output) {
UserModel user = GitBlitWebSession.get().getUser();
user = user == null ? UserModel.ANONYMOUS : user;
app().filestore().downloadBlob(entry.getFilestoreOid(), user, getRepositoryModel(), output);
}
};
getRequestCycle().setRequestTarget(new ResourceStreamRequestTarget(resourceStream, entry.path));
}});
item.add(new Link<Object>("raw", null) {
private static final long serialVersionUID = 1L;
@Override
public void onClick() {
IResourceStream resourceStream = new AbstractResourceStreamWriter() {
private static final long serialVersionUID = 1L;
@Override
public void write(OutputStream output) {
UserModel user = GitBlitWebSession.get().getUser();
user = user == null ? UserModel.ANONYMOUS : user;
app().filestore().downloadBlob(entry.getFilestoreOid(), user, getRepositoryModel(), output);
}
};
getRequestCycle().setRequestTarget(new ResourceStreamRequestTarget(resourceStream, entry.path));
}});
} else {
item.add(new BookmarkablePageLink<Void>("view", BlobPage.class, WicketUtils
.newPathParameter(repositoryName, entry.commitId, entry.path))
.setEnabled(!entry.changeType.equals(ChangeType.DELETE)));
String rawUrl = RawServlet.asLink(getContextUrl(), repositoryName, entry.commitId, entry.path);
item.add(new ExternalLink("raw", rawUrl)
.setEnabled(!entry.changeType.equals(ChangeType.DELETE)));
}
item.add(new BookmarkablePageLink<Void>("blame", BlamePage.class, WicketUtils

@@ -238,0 +328,0 @@ .newPathParameter(repositoryName, entry.commitId, entry.path))

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

<div style="float: right;" class="docnav">
<a wicket:id="blameLink"><wicket:message key="gb.blame"></wicket:message></a> | <a wicket:id="historyLink"><wicket:message key="gb.history"></wicket:message></a> | <a wicket:id="rawLink"><wicket:message key="gb.raw"></wicket:message></a>
<a wicket:id="editLink"><wicket:message key="gb.edit"></wicket:message></a> | <a wicket:id="blameLink"><wicket:message key="gb.blame"></wicket:message></a> | <a wicket:id="historyLink"><wicket:message key="gb.history"></wicket:message></a> | <a wicket:id="rawLink"><wicket:message key="gb.raw"></wicket:message></a>
</div>

@@ -28,3 +28,3 @@

<div style="float: right;" class="docnav">
<a wicket:id="blameLink"><wicket:message key="gb.blame"></wicket:message></a> | <a wicket:id="historyLink"><wicket:message key="gb.history"></wicket:message></a> | <a wicket:id="rawLink"><wicket:message key="gb.raw"></wicket:message></a>
<a wicket:id="editLink"><wicket:message key="gb.edit"></wicket:message></a> | <a wicket:id="blameLink"><wicket:message key="gb.blame"></wicket:message></a> | <a wicket:id="historyLink"><wicket:message key="gb.history"></wicket:message></a> | <a wicket:id="rawLink"><wicket:message key="gb.raw"></wicket:message></a>
</div>

@@ -31,0 +31,0 @@

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

import com.gitblit.models.UserModel;
import com.gitblit.servlet.RawServlet;

@@ -34,2 +35,3 @@ import com.gitblit.utils.BugtraqProcessor;

import com.gitblit.wicket.CacheControl;
import com.gitblit.wicket.GitBlitWebSession;
import com.gitblit.wicket.CacheControl.LastModified;

@@ -49,3 +51,5 @@ import com.gitblit.wicket.MarkupProcessor;

MarkupProcessor processor = new MarkupProcessor(app().settings(), app().xssFilter());
UserModel currentUser = (GitBlitWebSession.get().getUser() != null) ? GitBlitWebSession.get().getUser() : UserModel.ANONYMOUS;
final boolean userCanEdit = currentUser.canEdit(getRepositoryModel());
Repository r = getRepository();

@@ -91,2 +95,5 @@ RevCommit commit = JGitUtils.getCommit(r, objectId);

// document page links
fragment.add(new BookmarkablePageLink<Void>("editLink", EditFilePage.class,
WicketUtils.newPathParameter(repositoryName, objectId, documentPath))
.setEnabled(userCanEdit));
fragment.add(new BookmarkablePageLink<Void>("blameLink", BlamePage.class,

@@ -93,0 +100,0 @@ WicketUtils.newPathParameter(repositoryName, objectId, documentPath)));

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

<div style="float: right;" class="docnav">
<a wicket:id="blameLink"><wicket:message key="gb.blame"></wicket:message></a> | <a wicket:id="historyLink"><wicket:message key="gb.history"></wicket:message></a> | <a wicket:id="rawLink"><wicket:message key="gb.raw"></wicket:message></a>
<a wicket:id="editLink"><wicket:message key="gb.edit"></wicket:message></a> | <a wicket:id="blameLink"><wicket:message key="gb.blame"></wicket:message></a> | <a wicket:id="historyLink"><wicket:message key="gb.history"></wicket:message></a> | <a wicket:id="rawLink"><wicket:message key="gb.raw"></wicket:message></a>
</div>

@@ -47,3 +47,3 @@ <div class="content" wicket:id="content"></div>

<span class="hidden-phone link">
<a wicket:id="view"><wicket:message key="gb.view"></wicket:message></a> | <a wicket:id="raw"><wicket:message key="gb.raw"></wicket:message></a> | <a wicket:id="blame"><wicket:message key="gb.blame"></wicket:message></a> | <a wicket:id="history"><wicket:message key="gb.history"></wicket:message></a>
<a wicket:id="view"><wicket:message key="gb.view"></wicket:message></a> | <a wicket:id="edit"><wicket:message key="gb.edit"></wicket:message></a> | <a wicket:id="raw"><wicket:message key="gb.raw"></wicket:message></a> | <a wicket:id="blame"><wicket:message key="gb.blame"></wicket:message></a> | <a wicket:id="history"><wicket:message key="gb.history"></wicket:message></a>
</span>

@@ -50,0 +50,0 @@ </td>

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

import org.apache.wicket.markup.repeater.data.ListDataProvider;
import org.apache.wicket.model.StringResourceModel;
import org.eclipse.jgit.diff.DiffEntry.ChangeType;
import org.eclipse.jgit.lib.Repository;

@@ -35,2 +37,3 @@ import org.eclipse.jgit.revwalk.RevCommit;

import com.gitblit.models.PathModel;
import com.gitblit.models.UserModel;
import com.gitblit.servlet.RawServlet;

@@ -41,2 +44,3 @@ import com.gitblit.utils.ByteFormat;

import com.gitblit.wicket.CacheControl;
import com.gitblit.wicket.GitBlitWebSession;
import com.gitblit.wicket.CacheControl.LastModified;

@@ -60,2 +64,5 @@ import com.gitblit.wicket.MarkupProcessor;

Repository r = getRepository();
UserModel currentUser = (GitBlitWebSession.get().getUser() != null) ? GitBlitWebSession.get().getUser() : UserModel.ANONYMOUS;
final boolean userCanEdit = currentUser.canEdit(getRepositoryModel());
RevCommit head = JGitUtils.getCommit(r, objectId);

@@ -109,3 +116,8 @@ final String commitId = getBestCommitId(head);

MarkupDocument doc = item.getModelObject();
// document page links
item.add(new BookmarkablePageLink<Void>("editLink", EditFilePage.class,
WicketUtils.newPathParameter(repositoryName, commitId, doc.documentPath))
.setEnabled(userCanEdit));
// document page links
item.add(new BookmarkablePageLink<Void>("blameLink", BlamePage.class,

@@ -156,2 +168,5 @@ WicketUtils.newPathParameter(repositoryName, commitId, doc.documentPath)));

.newPathParameter(repositoryName, commitId, entry.path)));
item.add(new BookmarkablePageLink<Void>("edit", EditFilePage.class, WicketUtils
.newPathParameter(repositoryName, commitId, entry.path))
.setEnabled(userCanEdit));
String rawUrl = RawServlet.asLink(getContextUrl(), repositoryName, commitId, entry.path);

@@ -158,0 +173,0 @@ item.add(new ExternalLink("raw", rawUrl));

@@ -22,3 +22,3 @@ <!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.milestone"></wicket:message></th><td class="edit"><input class="input-large" type="text" wicket:id="name" id="name"></input></td></tr>
<tr><th><wicket:message key="gb.due"></wicket:message></th><td class="edit"><input class="input-large" type="text" wicket:id="due"></input> &nbsp;<span class="help-inline" wicket:id="dueFormat"></span></td></tr>
<tr><th><wicket:message key="gb.due"></wicket:message></th><td class="edit"><input class="input-large" type="date" wicket:id="due"></input> &nbsp;<span class="help-inline" wicket:id="dueFormat"></span></td></tr>
<tr><th><wicket:message key="gb.status"></wicket:message><span style="color:red;">*</span></th><td class="edit"><select class="input-large" wicket:id="status"></select></td></tr>

@@ -25,0 +25,0 @@ <tr><th></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="notify" /> &nbsp;<span class="help-inline"><wicket:message key="gb.notifyChangedOpenTickets"></wicket:message></span></label></td></tr>

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

import org.apache.wicket.ajax.markup.html.form.AjaxButton;
import org.apache.wicket.extensions.markup.html.form.DateTextField;
import org.apache.wicket.markup.html.basic.Label;

@@ -46,2 +45,3 @@ import org.apache.wicket.markup.html.form.Button;

import com.gitblit.wicket.GitBlitWebSession;
import com.gitblit.wicket.Html5DateField;
import com.gitblit.wicket.WicketUtils;

@@ -111,6 +111,8 @@ import com.gitblit.wicket.panels.BasePanel.JavascriptEventConfirmation;

form.add(new TextField<String>("name", nameModel));
form.add(new DateTextField("due", dueModel, "yyyy-MM-dd"));
form.add(new Html5DateField("due", dueModel, "yyyy-MM-dd"));
form.add(new Label("dueFormat", "yyyy-MM-dd"));
form.add(new CheckBox("notify", notificationModel));
addBottomScriptInline("{var e=document.createElement('input');e.type='date';if(e.type=='date'){$('[name=\"due\"]~.help-inline').hide()}}");
addBottomScript("scripts/wicketHtml5Patch.js");
List<Status> statusChoices = Arrays.asList(Status.Open, Status.Closed);

@@ -117,0 +119,0 @@ form.add(new DropDownChoice<TicketModel.Status>("status", statusModel, statusChoices));

@@ -449,3 +449,3 @@ /*

getString("gb.acceptNewTicketsDescription"),
new PropertyModel<Boolean>(repositoryModel, "acceptNewPatchsets")));
new PropertyModel<Boolean>(repositoryModel, "acceptNewTickets")));

@@ -452,0 +452,0 @@ form.add(new BooleanOption("requireApproval",

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

<div class="markdown" style="padding: 10px 0px 5px 0px;">
<span wicket:id="repositoriesMessage">[repositories message]</span>
<div style="padding: 10px 0px 5px 0px;">
<table class="filestore-status">
<tr>
<td><a wicket:id="filterByOk" href="#"><span wicket:id="statusOkIcon"></span><span wicket:id="statusOkCount"></span></a></td>
<td><a wicket:id="filterByPending" href="#"><span wicket:id="statusPendingIcon"></span><span wicket:id="statusPendingCount"></span></a></td>
<td><a wicket:id="filterByInprogress" href="#"><span wicket:id="statusInprogressIcon"></span><span wicket:id="statusInprogressCount"></span></a></td>
<td><a wicket:id="filterByError" href="#"><span wicket:id="statusErrorIcon"></span><span wicket:id="statusErrorCount"></span></a></td>
<td><a wicket:id="filterByDeleted" href="#"><span wicket:id="statusDeletedIcon"></span><span wicket:id="statusDeletedCount"></span></a></td>
<td></td>
<td><span class="fa fa-fw fa-database"></span><span wicket:id="spaceAvailable"></span></td>
</tr>
</table>
<span style="float:right"><a href="#" wicket:id="filestoreHelp"><span wicket:id="helpMessage">[help message]</span></a></span>

@@ -35,5 +45,12 @@ </div>

</table>
<!-- pager links -->
<div style="padding-bottom:5px;">
<a wicket:id="firstPageBottom"><wicket:message key="gb.pageFirst"></wicket:message></a> | <a wicket:id="prevPageBottom">&laquo; <wicket:message key="gb.pagePrevious"></wicket:message></a> | <a wicket:id="nextPageBottom"><wicket:message key="gb.pageNext"></wicket:message> &raquo;</a>
</div>
</div>
</wicket:extend>
</body>
</html>

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

import java.text.DateFormat;
import java.text.MessageFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import org.apache.commons.io.FileUtils;
import org.apache.wicket.Component;
import org.apache.wicket.PageParameters;
import org.apache.wicket.extensions.markup.html.repeater.util.SortableDataProvider;
import org.apache.wicket.markup.html.basic.Label;

@@ -30,10 +34,15 @@ import org.apache.wicket.markup.html.link.BookmarkablePageLink;

import org.apache.wicket.markup.repeater.data.DataView;
import org.apache.wicket.markup.repeater.data.ListDataProvider;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.Model;
import com.gitblit.Constants;
import com.gitblit.Keys;
import com.gitblit.models.FilestoreModel;
import com.gitblit.models.FilestoreModel.Status;
import com.gitblit.models.UserModel;
import com.gitblit.wicket.CacheControl;
import com.gitblit.wicket.FilestoreUI;
import com.gitblit.wicket.RequiresAdminRole;
import com.gitblit.wicket.GitBlitWebSession;
import com.gitblit.wicket.WicketUtils;
import com.gitblit.wicket.CacheControl.LastModified;

@@ -46,28 +55,111 @@ /**

*/
@RequiresAdminRole
@CacheControl(LastModified.ACTIVITY)
public class FilestorePage extends RootPage {
public FilestorePage() {
super();
public FilestorePage(PageParameters params) {
super(params);
setupPage("", "");
final List<FilestoreModel> files = app().filestore().getAllObjects();
int itemsPerPage = app().settings().getInteger(Keys.web.itemsPerPage, 20);
if (itemsPerPage <= 1) {
itemsPerPage = 20;
}
final int pageNumber = WicketUtils.getPage(params);
final String filter = WicketUtils.getSearchString(params);
int prevPage = Math.max(0, pageNumber - 1);
int nextPage = pageNumber + 1;
boolean hasMore = false;
final UserModel user = (GitBlitWebSession.get().getUser() == null) ? UserModel.ANONYMOUS : GitBlitWebSession.get().getUser();
final long nBytesUsed = app().filestore().getFilestoreUsedByteCount();
final long nBytesAvailable = app().filestore().getFilestoreAvailableByteCount();
List<FilestoreModel> files = app().filestore().getAllObjects(user);
String message = MessageFormat.format(getString("gb.filestoreStats"), files.size(),
FileUtils.byteCountToDisplaySize(nBytesUsed), FileUtils.byteCountToDisplaySize(nBytesAvailable) );
if (files == null) {
files = new ArrayList<FilestoreModel>();
}
Component repositoriesMessage = new Label("repositoriesMessage", message)
.setEscapeModelStrings(false).setVisible(message.length() > 0);
long nOk = 0;
long nPending = 0;
long nInprogress = 0;
long nError = 0;
long nDeleted = 0;
for (FilestoreModel file : files) {
switch (file.getStatus()) {
case Available: { nOk++;} break;
case Upload_Pending: { nPending++; } break;
case Upload_In_Progress: { nInprogress++; } break;
case Deleted: { nDeleted++; } break;
default: { nError++; } break;
}
}
BookmarkablePageLink<Void> itemOk = new BookmarkablePageLink<Void>("filterByOk", FilestorePage.class,
WicketUtils.newFilestorePageParameter(prevPage, SortBy.ok.name()));
BookmarkablePageLink<Void> itemPending = new BookmarkablePageLink<Void>("filterByPending", FilestorePage.class,
WicketUtils.newFilestorePageParameter(prevPage, SortBy.pending.name()));
BookmarkablePageLink<Void> itemInprogress = new BookmarkablePageLink<Void>("filterByInprogress", FilestorePage.class,
WicketUtils.newFilestorePageParameter(prevPage, SortBy.inprogress.name()));
BookmarkablePageLink<Void> itemError = new BookmarkablePageLink<Void>("filterByError", FilestorePage.class,
WicketUtils.newFilestorePageParameter(prevPage, SortBy.error.name()));
add(repositoriesMessage);
BookmarkablePageLink<Void> helpLink = new BookmarkablePageLink<Void>("filestoreHelp", FilestoreUsage.class);
helpLink.add(new Label("helpMessage", getString("gb.filestoreHelp")));
add(helpLink);
DataView<FilestoreModel> filesView = new DataView<FilestoreModel>("fileRow",
new ListDataProvider<FilestoreModel>(files)) {
BookmarkablePageLink<Void> itemDeleted = new BookmarkablePageLink<Void>("filterByDeleted", FilestorePage.class,
WicketUtils.newFilestorePageParameter(prevPage, SortBy.deleted.name()));
List<FilestoreModel> filteredResults = new ArrayList<FilestoreModel>(files.size());
if (filter == null) {
filteredResults = files;
} else if (filter.equals(SortBy.ok.name())) {
WicketUtils.setCssClass(itemOk, "filter-on");
for (FilestoreModel item : files) {
if (item.getStatus() == Status.Available) {
filteredResults.add(item);
}
}
} else if (filter.equals(SortBy.pending.name())) {
WicketUtils.setCssClass(itemPending, "filter-on");
for (FilestoreModel item : files) {
if (item.getStatus() == Status.Upload_Pending) {
filteredResults.add(item);
}
}
} else if (filter.equals(SortBy.inprogress.name())) {
WicketUtils.setCssClass(itemInprogress, "filter-on");
for (FilestoreModel item : files) {
if (item.getStatus() == Status.Upload_In_Progress) {
filteredResults.add(item);
}
}
} else if (filter.equals(SortBy.error.name())) {
WicketUtils.setCssClass(itemError, "filter-on");
for (FilestoreModel item : files) {
if (item.isInErrorState()) {
filteredResults.add(item);
}
}
} else if (filter.equals(SortBy.deleted.name())) {
WicketUtils.setCssClass(itemDeleted, "filter-on");
for (FilestoreModel item : files) {
if (item.getStatus() == Status.Deleted) {
filteredResults.add(item);
}
}
}
DataView<FilestoreModel> filesView = new DataView<FilestoreModel>("fileRow",
new SortableFilestoreProvider(filteredResults) , itemsPerPage) {
private static final long serialVersionUID = 1L;

@@ -105,4 +197,89 @@ private int counter;

if (filteredResults.size() < itemsPerPage) {
filesView.setCurrentPage(0);
hasMore = false;
} else {
filesView.setCurrentPage(pageNumber - 1);
hasMore = true;
}
add(filesView);
add(new BookmarkablePageLink<Void>("firstPageBottom", FilestorePage.class).setEnabled(pageNumber > 1));
add(new BookmarkablePageLink<Void>("prevPageBottom", FilestorePage.class,
WicketUtils.newFilestorePageParameter(prevPage, filter)).setEnabled(pageNumber > 1));
add(new BookmarkablePageLink<Void>("nextPageBottom", FilestorePage.class,
WicketUtils.newFilestorePageParameter(nextPage, filter)).setEnabled(hasMore));
itemOk.add(FilestoreUI.getStatusIcon("statusOkIcon", FilestoreModel.Status.Available));
itemPending.add(FilestoreUI.getStatusIcon("statusPendingIcon", FilestoreModel.Status.Upload_Pending));
itemInprogress.add(FilestoreUI.getStatusIcon("statusInprogressIcon", FilestoreModel.Status.Upload_In_Progress));
itemError.add(FilestoreUI.getStatusIcon("statusErrorIcon", FilestoreModel.Status.Error_Unknown));
itemDeleted.add(FilestoreUI.getStatusIcon("statusDeletedIcon", FilestoreModel.Status.Deleted));
itemOk.add(new Label("statusOkCount", String.valueOf(nOk)));
itemPending.add(new Label("statusPendingCount", String.valueOf(nPending)));
itemInprogress.add(new Label("statusInprogressCount", String.valueOf(nInprogress)));
itemError.add(new Label("statusErrorCount", String.valueOf(nError)));
itemDeleted.add(new Label("statusDeletedCount", String.valueOf(nDeleted)));
add(itemOk);
add(itemPending);
add(itemInprogress);
add(itemError);
add(itemDeleted);
add(new Label("spaceAvailable", String.format("%s / %s",
FileUtils.byteCountToDisplaySize(nBytesUsed),
FileUtils.byteCountToDisplaySize(nBytesAvailable))));
BookmarkablePageLink<Void> helpLink = new BookmarkablePageLink<Void>("filestoreHelp", FilestoreUsage.class);
helpLink.add(new Label("helpMessage", getString("gb.filestoreHelp")));
add(helpLink);
}
}
protected enum SortBy {
ok, pending, inprogress, error, deleted;
}
private static class SortableFilestoreProvider extends SortableDataProvider<FilestoreModel> {
private static final long serialVersionUID = 1L;
private List<FilestoreModel> list;
protected SortableFilestoreProvider(List<FilestoreModel> list) {
this.list = list;
}
@Override
public int size() {
if (list == null) {
return 0;
}
return list.size();
}
@Override
public IModel<FilestoreModel> model(FilestoreModel header) {
return new Model<FilestoreModel>(header);
}
@Override
public Iterator<FilestoreModel> iterator(int first, int count) {
Collections.sort(list, new Comparator<FilestoreModel>() {
@Override
public int compare(FilestoreModel o1, FilestoreModel o2) {
return o2.getChangedOn().compareTo(o1.getChangedOn());
}
});
return list.subList(first, first + count).iterator();
}
}
}

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

<div class="alert alert-danger">
<h3><center>Using the Filestore</center></h3>
<h3><center>Using the filestore</center></h3>
<p>
<strong>All clients intending to use the filestore must first install the <a href="https://git-lfs.github.com/">Git-LFS Client</a> and then run <code>git lfs init</code> to register the hooks globally.</strong><br/>
<i>This version of GitBlit has been verified with Git-LFS client version 0.6.0 which requires Git v1.8.2 or higher.</i>
<strong>All clients intending to use the filestore must first install the <a href="https://git-lfs.github.com/">Git-LFS Client</a> and then run <code>git lfs install</code></strong><br/>
<p>
If using password authentication it is recommended that you configure the <a href="https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage">git credential storage</a> to avoid Git-LFS asking for your password on each file<br/>
On Windows for example: <code>git config --global credential.helper wincred</code>
</p>
</p>

@@ -25,3 +28,3 @@ </div>

<p>
Just <code>git clone</code> as usual, no further action is required as GitBlit is configured to use the default Git-LFS end point <code>{repository}/info/lfs/objects/</code>.<br/>
Just <code>git clone</code> as usual, no further action is required as Gitblit is configured to use the default Git-LFS end point <code>{repository}/info/lfs/objects/</code>.<br/>
<i>If the repository uses a 3rd party Git-LFS server you will need to <a href="https://github.com/github/git-lfs/blob/master/docs/spec.md#the-server">manually configure the correct endpoints</a></i>.

@@ -43,24 +46,3 @@ </p>

<div class="alert alert-warn">
<h3><center>Limitations & Warnings</center></h3>
<p>GitBlit currently provides a server-only implementation of the opensource Git-LFS API, <a href="https://github.com/github/git-lfs/wiki/Implementations">other implementations</a> are available.<br/>
However, until <a href="https://bugs.eclipse.org/bugs/show_bug.cgi?id=470333">JGit provides Git-LFS client capabilities</a> some GitBlit features may not be fully supported when using the filestore.
Notably:
<ul>
<li>Mirroring a repository that uses Git-LFS - Only the pointer files, not the large files, are mirrored.</li>
<li>Federation - Only the pointer files, not the large files, are transfered.</li>
</ul>
</p>
</div>
<div class="alert alert-info">
<h3><center>GitBlit Configuration</center></h3>
<p>GitBlit provides the following configuration items when using the filestore:
<h4>filestore.storageFolder</h4>
<p>Defines the path on the server where filestore objects are to be saved. This defaults to <code>${baseFolder}/lfs</code></p>
<h4>filestore.maxUploadSize</h4>
<p>Defines the maximum allowable size that can be uploaded to the filestore. Once a file is uploaded it will be unaffected by later changes in this property. This defaults to <code>-1</code> indicating no limits.</p>
</p>
</div>
</div>

@@ -67,0 +49,0 @@ </div>

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

String query = "";
boolean allRepos = false;
int page = 1;

@@ -96,3 +98,4 @@ int pageSize = app().settings().getInteger(Keys.web.itemsPerPage, 50);

if (params.containsKey("allrepos")) {
allRepos = params.getAsBoolean("allrepos", false);
if (allRepos) {
repositories.addAll(availableRepositories);

@@ -138,3 +141,3 @@ }

final Model<ArrayList<String>> repositoriesModel = new Model<ArrayList<String>>(searchRepositories);
final Model<Boolean> allreposModel = new Model<Boolean>(params != null && params.containsKey("allrepos"));
final Model<Boolean> allreposModel = new Model<Boolean>(allRepos);
SessionlessForm<Void> form = new SessionlessForm<Void>("searchForm", getClass()) {

@@ -141,0 +144,0 @@

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

import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;

@@ -347,10 +346,19 @@ import java.util.List;

final List<QueryResult> results =
final List<QueryResult> allResults =
StringUtils.isEmpty(searchParam) ? query(qb, page, pageSize, sortBy, desc) : search(searchParam, page, pageSize);
int totalResults = results.size() == 0 ? 0 : results.get(0).totalResults;
buildPager(queryParam, milestoneParam, statiiParam, assignedToParam, sortBy, desc, repositoryId, page, pageSize, results.size(), totalResults);
List<QueryResult> viewableResults = new ArrayList<>(allResults.size());
for (QueryResult queryResult : allResults) {
RepositoryModel model = app().repositories().getRepositoryModel(currentUser, queryResult.repository);
if ((model != null) && (currentUser.canView(model))) {
viewableResults.add(queryResult);
}
}
int totalResults = viewableResults.size() == 0 ? 0 : viewableResults.get(0).totalResults;
buildPager(queryParam, milestoneParam, statiiParam, assignedToParam, sortBy, desc, repositoryId, page, pageSize, viewableResults.size(), totalResults);
final boolean showSwatch = app().settings().getBoolean(Keys.web.repositoryListSwatches, true);
add(new TicketListPanel("ticketList", results, showSwatch, true));
add(new TicketListPanel("ticketList", viewableResults, showSwatch, true));
}

@@ -357,0 +365,0 @@

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

<wicket:extend>
<body onload="document.getElementById('name').focus();">

@@ -23,3 +24,3 @@

<tr><th><wicket:message key="gb.milestone"></wicket:message></th><td class="edit"><input class="input-large" type="text" wicket:id="name" id="name"></input></td></tr>
<tr><th><wicket:message key="gb.due"></wicket:message></th><td class="edit"><input class="input-large" type="text" wicket:id="due"></input> &nbsp;<span class="help-inline" wicket:id="dueFormat"></span></td></tr>
<tr><th><wicket:message key="gb.due"></wicket:message></th><td class="edit"><input class="input-large" type="date" wicket:id="due"></input> &nbsp;<span class="help-inline" wicket:id="dueFormat"></span></td></tr>
</table>

@@ -37,4 +38,3 @@ </div>

</body>
</wicket:extend>
</html>

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

import org.apache.wicket.ajax.markup.html.form.AjaxButton;
import org.apache.wicket.extensions.markup.html.form.DateTextField;
import org.apache.wicket.markup.html.basic.Label;

@@ -39,2 +38,3 @@ import org.apache.wicket.markup.html.form.Button;

import com.gitblit.wicket.GitBlitWebSession;
import com.gitblit.wicket.Html5DateField;
import com.gitblit.wicket.WicketUtils;

@@ -83,5 +83,6 @@

form.add(new TextField<String>("name", nameModel));
form.add(new DateTextField("due", dueModel, "yyyy-MM-dd"));
form.add(new Html5DateField("due", dueModel, "yyyy-MM-dd"));
form.add(new Label("dueFormat", "yyyy-MM-dd"));
addBottomScriptInline("{var e=document.createElement('input');e.type='date';if(e.type=='date'){$('[name=\"due\"]~.help-inline').hide()}}");
addBottomScript("scripts/wicketHtml5Patch.js");
form.add(new AjaxButton("create") {

@@ -88,0 +89,0 @@

@@ -272,4 +272,11 @@ /*

protected void setupPage(String repositoryName, String pageName) {
//This method should only be called once in the page lifecycle.
//However, it must be called after the constructor has run, hence not in onInitialize
//It may be attempted to be called again if an info or error message is displayed.
if (get("projectTitle") != null) { return; }
String projectName = StringUtils.getFirstPathElement(repositoryName);
ProjectModel project = app().projects().getProjectModel(projectName);
if (project.isUserProject()) {

@@ -666,2 +673,3 @@ // user-as-project

@Override

@@ -674,4 +682,6 @@ protected void onBeforeRender() {

}
// setup page header and footer
setupPage(getRepositoryName(), "/ " + getPageName());
super.onBeforeRender();

@@ -678,0 +688,0 @@ }

@@ -200,5 +200,5 @@ /*

getRootPageParameters()));
if (user.canAdmin()) {
navLinks.add(new PageNavLink("gb.filestore", FilestorePage.class, getRootPageParameters()));
}
navLinks.add(new PageNavLink("gb.filestore", FilestorePage.class, getRootPageParameters()));
navLinks.add(new PageNavLink("gb.activity", ActivityPage.class, getRootPageParameters()));

@@ -205,0 +205,0 @@ if (allowLucene) {

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

session.setUser(user);
session.continueRequest();
return;

@@ -103,0 +104,0 @@ }

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

</td>
<td><span class="hidden-phone hidden-tablet aui-lozenge aui-lozenge-subtle" wicket:id="patchsetRevision">[R1]</span>
<td><span class="hidden-phone hidden-tablet" wicket:id="patchsetRevision">[R1]</span>
<span class="fa fa-fw" style="padding-left:15px;"><a wicket:id="deleteRevision" class="fa fa-fw fa-trash delete-patchset"></a></span>
<span class="hidden-tablet hidden-phone" style="padding-left:15px;"><span wicket:id="patchsetDiffStat"></span></span>

@@ -467,0 +468,0 @@ </td>

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

<td class="hidden-phone icon"><img wicket:id="pathIcon" /></td>
<td><span wicket:id="pathName"></span></td>
<td><span wicket:id="pathName"></span></td>
<td class="hidden-phone filestore"><span wicket:id="filestore" class="fa fa-fw fa-external-link-square filestore-item"></span></td>
<td class="hidden-phone size"><span wicket:id="pathSize">[path size]</span></td>

@@ -28,0 +29,0 @@ <td class="hidden-phone mode"><span wicket:id="pathPermissions">[path permissions]</span></td>

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

import java.io.OutputStream;
import java.util.List;

@@ -25,2 +26,3 @@

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

@@ -30,2 +32,5 @@ import org.apache.wicket.markup.repeater.Item;

import org.apache.wicket.markup.repeater.data.ListDataProvider;
import org.apache.wicket.request.target.resource.ResourceStreamRequestTarget;
import org.apache.wicket.util.resource.AbstractResourceStreamWriter;
import org.apache.wicket.util.resource.IResourceStream;
import org.eclipse.jgit.lib.FileMode;

@@ -37,2 +42,3 @@ import org.eclipse.jgit.lib.Repository;

import com.gitblit.models.SubmoduleModel;
import com.gitblit.models.UserModel;
import com.gitblit.servlet.RawServlet;

@@ -42,2 +48,3 @@ import com.gitblit.utils.ByteFormat;

import com.gitblit.wicket.CacheControl;
import com.gitblit.wicket.GitBlitWebSession;
import com.gitblit.wicket.CacheControl.LastModified;

@@ -78,3 +85,3 @@ import com.gitblit.wicket.WicketUtils;

}
PathModel model = new PathModel("..", parentPath, 0, FileMode.TREE.getBits(), null, objectId);
PathModel model = new PathModel("..", parentPath, null, 0, FileMode.TREE.getBits(), null, objectId);
model.isParentPath = true;

@@ -85,2 +92,3 @@ paths.add(0, model);

final String id = getBestCommitId(commit);
final ByteFormat byteFormat = new ByteFormat();

@@ -97,4 +105,8 @@ final String baseUrl = WicketUtils.getGitblitURL(getRequest());

public void populateItem(final Item<PathModel> item) {
PathModel entry = item.getModelObject();
final PathModel entry = item.getModelObject();
item.add(new Label("pathPermissions", JGitUtils.getPermissionsFromMode(entry.mode)));
item.add(WicketUtils.setHtmlTooltip(new Label("filestore", ""), getString("gb.filestore"))
.setVisible(entry.isFilestoreItem()));
if (entry.isParentPath) {

@@ -105,4 +117,3 @@ // parent .. path

item.add(new LinkPanel("pathName", null, entry.name, TreePage.class,
WicketUtils
.newPathParameter(repositoryName, id, entry.path)));
WicketUtils.newPathParameter(repositoryName, id, entry.path)));
item.add(new Label("pathLinks", ""));

@@ -167,13 +178,91 @@ } else {

item.add(new Label("pathSize", byteFormat.format(entry.size)));
item.add(new LinkPanel("pathName", "list", displayPath, BlobPage.class,
WicketUtils.newPathParameter(repositoryName, id,
path)));
// links
Fragment links = new Fragment("pathLinks", "blobLinks", this);
links.add(new BookmarkablePageLink<Void>("view", BlobPage.class,
WicketUtils.newPathParameter(repositoryName, id,
path)));
String rawUrl = RawServlet.asLink(getContextUrl(), repositoryName, id, path);
links.add(new ExternalLink("raw", rawUrl));
if (entry.isFilestoreItem()) {
item.add(new LinkPanel("pathName", "list", displayPath, new Link<Object>("link", null) {
private static final long serialVersionUID = 1L;
@Override
public void onClick() {
IResourceStream resourceStream = new AbstractResourceStreamWriter() {
private static final long serialVersionUID = 1L;
@Override
public void write(OutputStream output) {
UserModel user = GitBlitWebSession.get().getUser();
user = user == null ? UserModel.ANONYMOUS : user;
app().filestore().downloadBlob(entry.getFilestoreOid(), user, getRepositoryModel(), output);
}
};
getRequestCycle().setRequestTarget(new ResourceStreamRequestTarget(resourceStream, entry.path));
}}));
links.add(new Link<Object>("view", null) {
private static final long serialVersionUID = 1L;
@Override
public void onClick() {
IResourceStream resourceStream = new AbstractResourceStreamWriter() {
private static final long serialVersionUID = 1L;
@Override
public void write(OutputStream output) {
UserModel user = GitBlitWebSession.get().getUser();
user = user == null ? UserModel.ANONYMOUS : user;
app().filestore().downloadBlob(entry.getFilestoreOid(), user, getRepositoryModel(), output);
}
};
getRequestCycle().setRequestTarget(new ResourceStreamRequestTarget(resourceStream, entry.path));
}});
links.add(new Link<Object>("raw", null) {
private static final long serialVersionUID = 1L;
@Override
public void onClick() {
IResourceStream resourceStream = new AbstractResourceStreamWriter() {
private static final long serialVersionUID = 1L;
@Override
public void write(OutputStream output) {
UserModel user = GitBlitWebSession.get().getUser();
user = user == null ? UserModel.ANONYMOUS : user;
app().filestore().downloadBlob(entry.getFilestoreOid(), user, getRepositoryModel(), output);
}
};
getRequestCycle().setRequestTarget(new ResourceStreamRequestTarget(resourceStream, entry.path));
}});
} else {
item.add(new LinkPanel("pathName", "list", displayPath, BlobPage.class,
WicketUtils.newPathParameter(repositoryName, id,
path)));
links.add(new BookmarkablePageLink<Void>("view", BlobPage.class,
WicketUtils.newPathParameter(repositoryName, id,
path)));
String rawUrl = RawServlet.asLink(getContextUrl(), repositoryName, id, path);
links.add(new ExternalLink("raw", rawUrl));
}
links.add(new BookmarkablePageLink<Void>("blame", BlamePage.class,

@@ -180,0 +269,0 @@ WicketUtils.newPathParameter(repositoryName, id,

@@ -112,3 +112,3 @@ /*

if (tw.getPathString().equals(path)) {
matchingPath = new PathChangeModel(tw.getPathString(), tw.getPathString(), 0, tw
matchingPath = new PathChangeModel(tw.getPathString(), tw.getPathString(), null, 0, tw
.getRawMode(0), tw.getObjectId(0).getName(), commit.getId().getName(),

@@ -115,0 +115,0 @@ ChangeType.MODIFY);

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

import java.io.OutputStream;
import java.util.concurrent.Callable;
import org.apache.wicket.Component;

@@ -30,4 +33,9 @@ import org.apache.wicket.PageParameters;

import org.apache.wicket.model.Model;
import org.apache.wicket.request.target.resource.ResourceStreamRequestTarget;
import org.apache.wicket.util.resource.AbstractResourceStreamWriter;
import org.apache.wicket.util.resource.IResourceStream;
import com.gitblit.models.UserModel;
import com.gitblit.utils.StringUtils;
import com.gitblit.wicket.GitBlitWebSession;
import com.gitblit.wicket.WicketUtils;

@@ -113,2 +121,16 @@

public LinkPanel(String wicketId, String linkCssClass, String label, Link<?> link) {
super(wicketId);
this.labelModel = new Model<String>(label);
if (linkCssClass != null) {
link.add(new SimpleAttributeModifier("class", linkCssClass));
}
link.add(new Label("icon").setVisible(false));
link.add(new Label("label", labelModel));
add(link);
}
public void setNoFollow() {

@@ -115,0 +137,0 @@ Component c = get("link");

@@ -76,4 +76,4 @@ /*

public static void setHtmlTooltip(Component container, String value) {
container.add(new SimpleAttributeModifier("title", value));
public static Component setHtmlTooltip(Component container, String value) {
return container.add(new SimpleAttributeModifier("title", value));
}

@@ -421,2 +421,15 @@

public static PageParameters newFilestorePageParameter(int pageNumber, String filter) {
Map<String, String> parameterMap = new HashMap<String, String>();
if (pageNumber > 1) {
parameterMap.put("pg", String.valueOf(pageNumber));
}
if (filter != null) {
parameterMap.put("s", String.valueOf(filter));
}
return new PageParameters(parameterMap);
}
public static PageParameters newBlobDiffParameter(String repositoryName,

@@ -423,0 +436,0 @@ String baseCommitId, String commitId, String path) {

/*!
* Font Awesome 4.0.3 by @davegandy - http://fontawesome.io - @fontawesome
* Font Awesome 4.5.0 by @davegandy - http://fontawesome.io - @fontawesome
* License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License)
*/@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.0.3');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.0.3') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff?v=4.0.3') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.0.3') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.0.3#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font-family:FontAwesome;font-style:normal;font-weight:normal;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.3333333333333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.2857142857142858em;text-align:center}.fa-ul{padding-left:0;margin-left:2.142857142857143em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.142857142857143em;width:2.142857142857143em;top:.14285714285714285em;text-align:center}.fa-li.fa-lg{left:-1.8571428571428572em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:spin 2s infinite linear;-moz-animation:spin 2s infinite linear;-o-animation:spin 2s infinite linear;animation:spin 2s infinite linear}@-moz-keyframes spin{0%{-moz-transform:rotate(0deg)}100%{-moz-transform:rotate(359deg)}}@-webkit-keyframes spin{0%{-webkit-transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg)}}@-o-keyframes spin{0%{-o-transform:rotate(0deg)}100%{-o-transform:rotate(359deg)}}@-ms-keyframes spin{0%{-ms-transform:rotate(0deg)}100%{-ms-transform:rotate(359deg)}}@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(359deg)}}.fa-rotate-90{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=1);-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2);-webkit-transform:rotate(180deg);-moz-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=3);-webkit-transform:rotate(270deg);-moz-transform:rotate(270deg);-ms-transform:rotate(270deg);-o-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=0,mirror=1);-webkit-transform:scale(-1,1);-moz-transform:scale(-1,1);-ms-transform:scale(-1,1);-o-transform:scale(-1,1);transform:scale(-1,1)}.fa-flip-vertical{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2,mirror=1);-webkit-transform:scale(1,-1);-moz-transform:scale(1,-1);-ms-transform:scale(1,-1);-o-transform:scale(1,-1);transform:scale(1,-1)}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-asc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-desc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-reply-all:before{content:"\f122"}.fa-mail-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}
*/@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.5.0');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.5.0') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff2?v=4.5.0') format('woff2'),url('../fonts/fontawesome-webfont.woff?v=4.5.0') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.5.0') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.5.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=1);-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2);-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=3);-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1);-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1);-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"}

@@ -1947,2 +1947,8 @@ body {

td.filestore {
text-align: right;
width:1em;
padding-right:15px;
}
td.size {

@@ -2070,6 +2076,2 @@ text-align: right;

div.docs {
max-width: 880px;
}
div.docs ul.nav {

@@ -2359,1 +2361,52 @@ margin-bottom: 0px !important;

}
.filestore-item {
color:#815b3a;
}
.filestore-status {
display: inline;
font-size: 1.2em;
}
table.filestore-status {
border:none!important;
border-spacing: 10px 0px;
border-collapse: separate;
}
.filestore-status tr td a {
border:none!important;
margin-right:1.5em!important;
padding:0.25em;
}
.filestore-status td a:hover, .filestore-status td a.filter-on {
background-color: #eee;
border-radius:5px;
}
.filestore-status span:nth-child(2) {
font-weight:800;
margin-left:0.25em;
}
.delete-patchset {
color:#D51900;
font-size: 1.2em;
}
.ticketReference-comment {
font-family: sans-serif;
font-weight: 200;
font-size: 1em;
font-variant: normal;
text-transform: none;
}
.ticketReference-commit {
font-family: monospace;
font-weight: 200;
font-size: 1em;
font-variant: normal;
}

@@ -93,1 +93,3 @@ #

server.shutdownPort = 8081
tickets.service = com.gitblit.tickets.BranchTicketService

@@ -69,3 +69,3 @@ /*

SshKeysDispatcherTest.class, UITicketTest.class, PathUtilsTest.class, SshKerberosAuthenticationTest.class,
GravatarTest.class, FilestoreManagerTest.class, FilestoreServletTest.class })
GravatarTest.class, FilestoreManagerTest.class, FilestoreServletTest.class, TicketReferenceTest.class })
public class GitBlitSuite {

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

@@ -588,7 +588,7 @@ /*

public void testZip() throws Exception {
assertFalse(CompressionUtils.zip(null, null, null, null));
assertFalse(CompressionUtils.zip(null, null, null, null, null));
Repository repository = GitBlitSuite.getHelloworldRepository();
File zipFileA = new File(GitBlitSuite.REPOSITORIES, "helloworld.zip");
FileOutputStream fosA = new FileOutputStream(zipFileA);
boolean successA = CompressionUtils.zip(repository, null, Constants.HEAD, fosA);
boolean successA = CompressionUtils.zip(repository, null, null, Constants.HEAD, fosA);
fosA.close();

@@ -598,3 +598,3 @@

FileOutputStream fosB = new FileOutputStream(zipFileB);
boolean successB = CompressionUtils.zip(repository, "java.java", Constants.HEAD, fosB);
boolean successB = CompressionUtils.zip(repository, null, "java.java", Constants.HEAD, fosB);
fosB.close();

@@ -601,0 +601,0 @@

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