www.github.com/gitblit/gitblit.git
Advanced tools
| # 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">×</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); | ||
| } |
| node_modules |
| .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 |
+19
-1
@@ -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> |
+2
-0
@@ -36,3 +36,5 @@ ## Documentation | ||
| [[src/site/setup_scaling.mkd]] | ||
| [[src/site/setup_filestore.mkd]] | ||
| ### Gitblit Tickets | ||
@@ -39,0 +41,0 @@ |
@@ -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> <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> <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" /> <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">« <wicket:message key="gb.pagePrevious"></wicket:message></a> | <a wicket:id="nextPageBottom"><wicket:message key="gb.pageNext"></wicket:message> »</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> <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> <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