@@ -66,6 +66,16 @@ web.useClientTimezone = false | |||
web.datestampShortFormat = yyyy-MM-dd | |||
web.datetimestampLongFormat = EEEE, MMMM d, yyyy h:mm a z | |||
# Choose how to present the repositories list. | |||
# grouped = group nested/subfolder repositories together (no sorting) | |||
# flat = flat list of repositories (sorting allowed) | |||
web.repositoryListType = flat | |||
# If using a grouped repository list and there are repositories at the | |||
# root level of your repositories folder, you may specify the displayed | |||
# group name with this setting. This value is only used for web presentation. | |||
web.repositoryRootGroupName = main | |||
# Choose the diff presentation style: gitblt, gitweb, or plain | |||
web.diffStyle = gitblit | |||
@@ -1,6 +1,6 @@ | |||
## Overview | |||
Git:Blit is an open-source, integrated pure-Java stack for managing, viewing, and serving [Git](http://git-scm.com) repositories. | |||
Its designed primarily as a tool for small workgroups who want to host Git repositories on a Windows machine. | |||
Its designed primarily as a tool for small workgroups who want to host [Git](http://git-scm.com) repositories on a Windows machine. | |||
Of course, since its pure-Java it should run with any JVM on any platform, but there are already [many compelling Git solutions](https://git.wiki.kernel.org/index.php/InterfacesFrontendsAndTools) for non-Windows environments. | |||
@@ -10,46 +10,53 @@ Of course, since its pure-Java it should run with any JVM on any platform, but t | |||
### Features | |||
- Out-of-the-box integrated stack requiring minimal configuration | |||
- JGit SmartHTTP Servlet | |||
- Web and Git Servlet authentication | |||
- JGit SmartHTTP servlet | |||
- Browser and git client authentication | |||
- Four repository access control configurations | |||
- *Anonymous View, Clone & Push* | |||
- *Authenticated Push* | |||
- *Authenticated Clone & Push* | |||
- *Authenticated View, Clone & Push* | |||
- Gitweb inspired UI (mostly plain html) | |||
- Repository administration through web UI | |||
- User administration through web UI | |||
- Repositories may also be frozen (deny push) temporarily or permanently | |||
- Gitweb inspired UI | |||
- Administrators may create, edit, rename, or delete repositories through the web UI | |||
- Administrators may create, edit, rename, or delete users through the web UI | |||
- Repository Owners may edit repositories through the web UI | |||
- Automatically generates a self-signed certificate for https communications | |||
- Dates can optionally be displayed using browser's reported timezone | |||
- Author and Committer email address display can be controlled | |||
- Syntax highlighting | |||
- Customizable regular expression handling for commit messages | |||
- Single text file for server configuration | |||
- Single text file for users configuration | |||
- Simple repository stats | |||
- Simple text file for server configuration | |||
- Simple text file for users configuration | |||
- Optional integrated Ticgit | |||
- Optional integrated Markdown | |||
- Optional read-only Docs page which enumerates all Markdown files within a repository | |||
- Optional read-only Ticgit Ticket pages *(based on last MIT release bf57b032 2009-01-27)* | |||
### Limitations | |||
- HTTP/HTTPS are the only supported protocols | |||
- Access controls are not path-based, they are repository-based | |||
- Only admin users can create repositories | |||
- Only Administrators can create, rename or delete repositories | |||
- Git:Blit is a full-stack solution, its not just a webapp so at this time there is no WAR build | |||
### Todo List | |||
- Review spots where Git:Blit can cache data instead of abusing the disk | |||
- Unit testing | |||
- Ticgit activity/timeline | |||
- Ticgit query feature with paging support | |||
- Ticgit ticket change history | |||
- Implement Markdown editing | |||
- View images on Blob page | |||
- View other binary files Blob page | |||
- View other binary files on Blob page | |||
### License | |||
TBD | |||
### Architecture | |||
### Inspirations | |||
- [Gitweb](http://www.git-scm.com) | |||
- [Fossil](http://www.fossil-scm.org) | |||
## Architecture | |||
![block diagram](architecture.png "Git Blit Architecture") | |||
@@ -73,8 +80,8 @@ The following dependencies are automatically downloaded from the Apache Maven re | |||
- [JCommander](http://jcommander.org) | |||
- [BouncyCastle](http://www.bouncycastle.org) | |||
### Building | |||
Eclipse is recommended for development as the project settings are preconfigured. | |||
## Building | |||
[Eclipse](http://eclipse.org) is recommended for development as the project settings are preconfigured. | |||
1. Clone the git repository from here. | |||
2. Import the gitblit project into your Eclipse workspace.<br/> |
@@ -6,36 +6,39 @@ | |||
Open `gitblit.properties` in your favorite text editor and make sure to review and set: | |||
- *git.repositoryFolder* | |||
- *server.tempFolder* | |||
- *server.httpBindInterface* and *server.httpsBindInterface* | |||
- *server.httpBindInterface* and *server.httpsBindInterface*<br/> | |||
**NOTE:** Consider using **https** exclusively because passwords for authentication are transmitted as clear text! | |||
- *server.storePassword*<br/> | |||
**NOTE:**<br/> | |||
Its recommended to use **https** wherever possible instead of http because passwords are transmitted as clear text! | |||
**NOTE:** The certificate password AND the keystore password must match! | |||
3. Execute `gitblit.cmd` or `java -jar gitblit.jar` from a command-line | |||
4. Wait a minute or two while all dependencies are downloaded and your self-signed certificate is generated. | |||
5. Open your browser to <http://localhost> or <https://localhost> depending on your chosen configuration. | |||
6. Click the *Login* link and enter the default administrator credentials: **admin / admin**<br/> | |||
**NOTE:**<br/> | |||
Make sure to change the administrator username and/or password!! | |||
**NOTE:** Make sure to change the administrator username and/or password!! | |||
### Administering Repositories | |||
Repositories can be created, edited, and deleted through the web UI. They may also be created, edited, and deleted from the command-line using real Git or your favorite file manager and text editor. | |||
Repositories can be created, edited, renamed, and deleted through the web UI. They may also be created, edited, and deleted from the command-line using real [Git](http://git-scm.com) or your favorite file manager and text editor. | |||
All repository settings are stored within the repository `.git/config` file under the *gitblit* section. | |||
[gitblit] | |||
description = master repository | |||
owner = Joe Owner | |||
owner = james | |||
useTickets = false | |||
useDocs = true | |||
showRemoteBranches = false | |||
accessRestriction = clone | |||
isFrozen = false | |||
#### Repository Names | |||
Repository names must be unique and are case-insensitive. The name must be composed of letters, digits, or `/ _ - .`<br/> | |||
Whitespace is illegal. | |||
#### Repository Owner | |||
The *Repository Owner* has the special permission of being able to edit a repository through the web UI. The Repository Owner is not permitted to rename the repository, delete the repository, or reassign ownership to another user. | |||
### Administering Users | |||
In contrast, all users are stored in the `users.properties` file or in the file your specified in `gitblit.properties`.<br/> | |||
All users are stored in the `users.properties` file or in the file you specified in `gitblit.properties`.<br/> | |||
The format of `users.properties` follows Jetty's convention for HashRealms: | |||
username,password,role1,role2,role3... | |||
@@ -48,11 +51,12 @@ Whitespace is illegal. | |||
User passwords are CASE-SENSITIVE and may be *plain*, *md5*, or *crypt* formatted (see `gitblit.properties` -> *realm.passwordStorage*). | |||
#### User Roles | |||
There is only one actual *role* in Git:Blit and that is *#admin* which grants administrative powers to that user. Administrators automatically have access to all repositories. All other *roles* are actually repository names. If a repository is access-restricted, the user must have the repository's name within his/her roles to bypass the access restriction. This is how users are granted access to a restricted repository. | |||
There is only one actual *role* in Git:Blit and that is *#admin* which grants administrative powers to that user. Administrators automatically have access to all repositories. All other *roles* are repository names. If a repository is access-restricted, the user must have the repository's name within his/her roles to bypass the access restriction. This is how users are granted access to a restricted repository. | |||
### Creating your own Self-Signed Certificate | |||
Review the contents of the `makekeystore.cmd` or `makekeystore_jdk.cmd`script and execute it. Voila. | |||
Review the contents of the `makekeystore.cmd` or `makekeystore_jdk.cmd` script and execute it.<br/> | |||
**NOTE:** The certificate password AND the keystore password must match! | |||
### Running as a Service | |||
Review the contents of the `installService.cmd` or `installService64.cmd`, as appropriate for your JVM.<br/> |
@@ -1,5 +1,18 @@ | |||
## Eclipse Tips | |||
verifySsl | |||
### Do Not Verify Self-Signed Certificates | |||
If you are using a self-signed certificate, like the one that is automatically generated by Git:Blit, you have to tell Eclipse/EGit to ignore certificate verification errors. | |||
![sslverify](sslverify.png "http.sslVerify setting") | |||
![sslverify2](sslverify2.png "Adding http.sslVerify setting") | |||
### Pushing a New Project to a New Git:Blit Repository | |||
1. Project Root->Team->Share->Git | |||
Create a Git repository inside the project | |||
### Pushing a Git-Controlled Project to another Git:Blit Repository | |||
1. Project Root->Team->Remote->Push | |||
2. Enter the URL information of the repository | |||
3. In the Refspec dialog click the buttons named "All all branches spec" and "All all tags spec" | |||
how to push new unshared project to new repository |
@@ -56,7 +56,7 @@ public class BuildSite { | |||
String html_footer = readContent(new File(params.pageFooter)); | |||
final String links = sb.toString(); | |||
final String header = MessageFormat.format(html_header, Constants.FULL_NAME, links); | |||
final String date = new SimpleDateFormat("yyyy MMM dd").format(new Date()); | |||
final String date = new SimpleDateFormat("yyyy-MM-dd").format(new Date()); | |||
final String footer = MessageFormat.format(html_footer, "generated " + date); | |||
for (File file : markdownFiles) { | |||
try { |
@@ -4,7 +4,7 @@ public class Constants { | |||
public final static String NAME = "Git:Blit"; | |||
public final static String FULL_NAME = "Git:Blit - a Pure Java Git Server"; | |||
public final static String FULL_NAME = "Git:Blit - a Pure Java Git Solution"; | |||
// The build script extracts this exact line so be careful editing it | |||
// and only use A-Z a-z 0-9 .-_ in the string. |
@@ -4,6 +4,7 @@ import java.io.File; | |||
import java.io.IOException; | |||
import java.text.MessageFormat; | |||
import java.util.ArrayList; | |||
import java.util.Collections; | |||
import java.util.List; | |||
import javax.servlet.ServletContextEvent; | |||
@@ -21,6 +22,7 @@ import org.slf4j.LoggerFactory; | |||
import com.gitblit.Constants.AccessRestrictionType; | |||
import com.gitblit.utils.JGitUtils; | |||
import com.gitblit.utils.StringUtils; | |||
import com.gitblit.wicket.models.RepositoryModel; | |||
import com.gitblit.wicket.models.UserModel; | |||
@@ -97,7 +99,9 @@ public class GitBlit implements ServletContextListener { | |||
} | |||
public List<String> getAllUsernames() { | |||
return loginService.getAllUsernames(); | |||
List<String> names = loginService.getAllUsernames(); | |||
Collections.sort(names); | |||
return names; | |||
} | |||
public UserModel getUserModel(String username) { | |||
@@ -169,16 +173,29 @@ public class GitBlit implements ServletContextListener { | |||
model.lastChange = JGitUtils.getLastChange(r); | |||
StoredConfig config = JGitUtils.readConfig(r); | |||
if (config != null) { | |||
model.description = config.getString("gitblit", null, "description"); | |||
model.owner = config.getString("gitblit", null, "owner"); | |||
model.useTickets = config.getBoolean("gitblit", "useTickets", false); | |||
model.useDocs = config.getBoolean("gitblit", "useDocs", false); | |||
model.accessRestriction = AccessRestrictionType.fromName(config.getString("gitblit", null, "accessRestriction")); | |||
model.showRemoteBranches = config.getBoolean("gitblit", "showRemoteBranches", false); | |||
model.description = getConfig(config, "description", ""); | |||
model.owner = getConfig(config, "owner", ""); | |||
model.useTickets = getConfig(config, "useTickets", false); | |||
model.useDocs = getConfig(config, "useDocs", false); | |||
model.accessRestriction = AccessRestrictionType.fromName(getConfig(config, "accessRestriction", null)); | |||
model.showRemoteBranches = getConfig(config, "showRemoteBranches", false); | |||
model.isFrozen = getConfig(config, "isFrozen", false); | |||
} | |||
r.close(); | |||
return model; | |||
} | |||
private String getConfig(StoredConfig config, String field, String defaultValue) { | |||
String value = config.getString("gitblit", null, field); | |||
if (StringUtils.isEmpty(value)) { | |||
return defaultValue; | |||
} | |||
return value; | |||
} | |||
private boolean getConfig(StoredConfig config, String field, boolean defaultValue) { | |||
return config.getBoolean("gitblit", field, defaultValue); | |||
} | |||
public void editRepositoryModel(RepositoryModel repository, boolean isCreate) throws GitBlitException { | |||
Repository r = null; | |||
@@ -209,6 +226,7 @@ public class GitBlit implements ServletContextListener { | |||
config.setBoolean("gitblit", null, "useDocs", repository.useDocs); | |||
config.setString("gitblit", null, "accessRestriction", repository.accessRestriction.name()); | |||
config.setBoolean("gitblit", null, "showRemoteBranches", repository.showRemoteBranches); | |||
config.setBoolean("gitblit", null, "isFrozen", repository.isFrozen); | |||
try { | |||
config.save(); | |||
} catch (IOException e) { |
@@ -44,12 +44,12 @@ public class GitBlitServlet extends GitServlet { | |||
String function = url.substring(forwardSlash + 1); | |||
String query = req.getQueryString(); | |||
RepositoryModel model = GitBlit.self().getRepositoryModel(repository); | |||
if (model != null) { | |||
if (model.accessRestriction.atLeast(AccessRestrictionType.PUSH)) { | |||
if (model != null) { | |||
if (model.isFrozen || model.accessRestriction.atLeast(AccessRestrictionType.PUSH)) { | |||
boolean authorizedUser = req.isUserInRole(repository); | |||
if (function.startsWith("git-receive-pack") || (query.indexOf("service=git-receive-pack") > -1)) { | |||
// Push request | |||
if (authorizedUser) { | |||
if (!model.isFrozen && authorizedUser) { | |||
// clone-restricted or push-authorized | |||
super.service(req, rsp); | |||
return; |
@@ -107,4 +107,11 @@ public class StringUtils { | |||
throw new RuntimeException(t); | |||
} | |||
} | |||
public static String getRootPath(String path) { | |||
if (path.indexOf('/') > -1) { | |||
return path.substring(0, path.indexOf('/')); | |||
} | |||
return ""; | |||
} | |||
} |
@@ -90,4 +90,6 @@ gb.useTicketsDescription = distributed Ticgit issues | |||
gb.useDocsDescription = enumerates Markdown documentation in repository | |||
gb.showRemoteBranchesDescription = show remote branches | |||
gb.canAdminDescription = can administer Git:Blit server | |||
gb.permittedUsers = permitted users | |||
gb.permittedUsers = permitted users | |||
gb.isFrozen = is frozen | |||
gb.isFrozenDescription = deny push operations |
@@ -25,10 +25,10 @@ | |||
<form style="text-align:center;" wicket:id="loginForm"> | |||
<div> | |||
<p/> | |||
<wicket:message key="gb.username"></wicket:message> | |||
<wicket:message key="gb.username"></wicket:message> | |||
<input type="text" id="username" wicket:id="username" value=""/> | |||
<p/> | |||
<wicket:message key="gb.password"></wicket:message> | |||
<wicket:message key="gb.password"></wicket:message> | |||
<input type="password" wicket:id="password" value=""/> | |||
<p/> | |||
<input type="submit" value="Login" wicket:message="value:gb.login" /> |
@@ -36,7 +36,6 @@ import com.gitblit.wicket.models.RepositoryModel; | |||
import com.gitblit.wicket.pages.BranchesPage; | |||
import com.gitblit.wicket.pages.DocsPage; | |||
import com.gitblit.wicket.pages.LogPage; | |||
import com.gitblit.wicket.pages.RepositoriesPage; | |||
import com.gitblit.wicket.pages.SearchPage; | |||
import com.gitblit.wicket.pages.SummaryPage; | |||
import com.gitblit.wicket.pages.TagsPage; | |||
@@ -79,10 +78,8 @@ public abstract class RepositoryPage extends BasePage { | |||
} | |||
Repository r = getRepository(); | |||
if (r == null) { | |||
error(MessageFormat.format("Failed to open repository {0} for {1}!", repositoryName, getPageName()), true); | |||
} | |||
RepositoryModel model = getRepositoryModel(); | |||
// standard page links | |||
add(new BookmarkablePageLink<Void>("summary", SummaryPage.class, WicketUtils.newRepositoryParameter(repositoryName))); | |||
add(new BookmarkablePageLink<Void>("log", LogPage.class, WicketUtils.newRepositoryParameter(repositoryName))); | |||
@@ -94,12 +91,12 @@ public abstract class RepositoryPage extends BasePage { | |||
List<String> extraPageLinks = new ArrayList<String>(); | |||
// Conditionally add tickets page | |||
if (getRepositoryModel().useTickets && JGitUtils.getTicketsBranch(r) != null) { | |||
if (model.useTickets && JGitUtils.getTicketsBranch(r) != null) { | |||
extraPageLinks.add("tickets"); | |||
} | |||
// Conditionally add docs page | |||
if (getRepositoryModel().useDocs) { | |||
if (model.useDocs) { | |||
extraPageLinks.add("docs"); | |||
} | |||
@@ -150,8 +147,7 @@ public abstract class RepositoryPage extends BasePage { | |||
if (r == null) { | |||
Repository r = GitBlit.self().getRepository(repositoryName); | |||
if (r == null) { | |||
error("Can not load repository " + repositoryName); | |||
redirectToInterceptPage(new RepositoriesPage()); | |||
error("Can not load repository " + repositoryName, true); | |||
return null; | |||
} | |||
this.r = r; | |||
@@ -163,9 +159,8 @@ public abstract class RepositoryPage extends BasePage { | |||
if (m == null) { | |||
RepositoryModel model = GitBlit.self().getRepositoryModel(GitBlitWebSession.get().getUser(), repositoryName); | |||
if (model == null) { | |||
error("Unauthorized access for repository " + repositoryName); | |||
redirectToInterceptPage(new RepositoriesPage()); | |||
return null; | |||
error("Unauthorized access for repository " + repositoryName, true); | |||
return null; | |||
} | |||
m = model; | |||
} |
@@ -17,9 +17,14 @@ public class RepositoryModel implements Serializable { | |||
public boolean useTickets; | |||
public boolean useDocs; | |||
public AccessRestrictionType accessRestriction; | |||
public boolean isFrozen; | |||
public RepositoryModel() { | |||
this.name = ""; | |||
this.description = ""; | |||
this.owner = ""; | |||
this.lastChange = new Date(0); | |||
this.accessRestriction = AccessRestrictionType.NONE; | |||
} | |||
public RepositoryModel(String name, String description, String owner, Date lastchange) { | |||
@@ -27,5 +32,6 @@ public class RepositoryModel implements Serializable { | |||
this.description = description; | |||
this.owner = owner; | |||
this.lastChange = lastchange; | |||
this.accessRestriction = AccessRestrictionType.NONE; | |||
} | |||
} |
@@ -17,13 +17,14 @@ | |||
<tbody> | |||
<tr><th><wicket:message key="gb.name"></wicket:message></th><td class="edit"><input type="text" wicket:id="name" id="name" size="40" tabindex="1" /></td></tr> | |||
<tr><th><wicket:message key="gb.description"></wicket:message></th><td class="edit"><input type="text" wicket:id="description" size="40" tabindex="2" /></td></tr> | |||
<tr><th><wicket:message key="gb.owner"></wicket:message></th><td class="edit"><input type="text" wicket:id="owner" size="40" tabindex="3" /></td></tr> | |||
<tr><th><wicket:message key="gb.owner"></wicket:message></th><td class="edit"><select wicket:id="owner" tabindex="3" /></td></tr> | |||
<tr><th><wicket:message key="gb.enableTickets"></wicket:message></th><td class="edit"><input type="checkbox" wicket:id="useTickets" tabindex="4" /> <i><wicket:message key="gb.useTicketsDescription"></wicket:message></i></td></tr> | |||
<tr><th><wicket:message key="gb.enableDocs"></wicket:message></th><td class="edit"><input type="checkbox" wicket:id="useDocs" tabindex="5" /> <i><wicket:message key="gb.useDocsDescription"></wicket:message></i></td></tr> | |||
<tr><th><wicket:message key="gb.showRemoteBranches"></wicket:message></th><td class="edit"><input type="checkbox" wicket:id="showRemoteBranches" tabindex="6" /> <i><wicket:message key="gb.showRemoteBranchesDescription"></wicket:message></i></td></tr> | |||
<tr><th><wicket:message key="gb.accessRestriction"></wicket:message></th><td class="edit"><select wicket:id="accessRestriction" tabindex="7" /></td></tr> | |||
<tr><th><wicket:message key="gb.isFrozen"></wicket:message></th><td class="edit"><input type="checkbox" wicket:id="isFrozen" tabindex="8" /> <i><wicket:message key="gb.isFrozenDescription"></wicket:message></i></td></tr> | |||
<tr><th style="vertical-align: top;"><wicket:message key="gb.permittedUsers"></wicket:message></th><td style="padding:2px;"><span wicket:id="users"></span></td></tr> | |||
<tr><th></th><td class="editButton"><input type="submit" value="Save" wicket:message="value:gb.save" tabindex="8" /></td></tr> | |||
<tr><th></th><td class="editButton"><input type="submit" value="Save" wicket:message="value:gb.save" tabindex="9" /></td></tr> | |||
</tbody> | |||
</table> | |||
</form> |
@@ -3,6 +3,7 @@ package com.gitblit.wicket.pages; | |||
import java.text.MessageFormat; | |||
import java.util.ArrayList; | |||
import java.util.Arrays; | |||
import java.util.Collections; | |||
import java.util.Date; | |||
import java.util.Iterator; | |||
import java.util.List; | |||
@@ -23,13 +24,14 @@ import org.apache.wicket.model.util.ListModel; | |||
import com.gitblit.Constants.AccessRestrictionType; | |||
import com.gitblit.GitBlit; | |||
import com.gitblit.GitBlitException; | |||
import com.gitblit.Keys; | |||
import com.gitblit.utils.StringUtils; | |||
import com.gitblit.wicket.AdminPage; | |||
import com.gitblit.wicket.BasePage; | |||
import com.gitblit.wicket.GitBlitWebSession; | |||
import com.gitblit.wicket.WicketUtils; | |||
import com.gitblit.wicket.models.RepositoryModel; | |||
import com.gitblit.wicket.models.UserModel; | |||
@AdminPage | |||
public class EditRepositoryPage extends BasePage { | |||
private final boolean isCreate; | |||
@@ -51,6 +53,9 @@ public class EditRepositoryPage extends BasePage { | |||
} | |||
protected void setupPage(final RepositoryModel repositoryModel) { | |||
// ensure this user can create or edit this repository | |||
checkPermissions(repositoryModel); | |||
List<String> repositoryUsers = new ArrayList<String>(); | |||
if (isCreate) { | |||
super.setupPage("", getString("gb.newRepository")); | |||
@@ -58,6 +63,7 @@ public class EditRepositoryPage extends BasePage { | |||
super.setupPage("", getString("gb.edit")); | |||
if (repositoryModel.accessRestriction.exceeds(AccessRestrictionType.NONE)) { | |||
repositoryUsers.addAll(GitBlit.self().getRepositoryUsers(repositoryModel)); | |||
Collections.sort(repositoryUsers); | |||
} | |||
} | |||
@@ -99,10 +105,10 @@ public class EditRepositoryPage extends BasePage { | |||
error("Please select access restriction!"); | |||
return; | |||
} | |||
// save the repository | |||
GitBlit.self().editRepositoryModel(repositoryModel, isCreate); | |||
// save the repository access list | |||
if (repositoryModel.accessRestriction.exceeds(AccessRestrictionType.NONE)) { | |||
Iterator<String> users = usersPalette.getSelectedChoices(); | |||
@@ -110,6 +116,10 @@ public class EditRepositoryPage extends BasePage { | |||
while (users.hasNext()) { | |||
repositoryUsers.add(users.next()); | |||
} | |||
// ensure the owner is added to the user list | |||
if (!repositoryUsers.contains(repositoryModel.owner)) { | |||
repositoryUsers.add(repositoryModel.owner); | |||
} | |||
GitBlit.self().setRepositoryUsers(repositoryModel, repositoryUsers); | |||
} | |||
} catch (GitBlitException e) { | |||
@@ -124,8 +134,9 @@ public class EditRepositoryPage extends BasePage { | |||
// field names reflective match RepositoryModel fields | |||
form.add(new TextField<String>("name").setEnabled(isCreate)); | |||
form.add(new TextField<String>("description")); | |||
form.add(new TextField<String>("owner")); | |||
form.add(new DropDownChoice<String>("owner", GitBlit.self().getAllUsernames()).setEnabled(GitBlitWebSession.get().canAdmin())); | |||
form.add(new DropDownChoice<AccessRestrictionType>("accessRestriction", Arrays.asList(AccessRestrictionType.values()), new AccessRestrictionRenderer())); | |||
form.add(new CheckBox("isFrozen")); | |||
form.add(new CheckBox("useTickets")); | |||
form.add(new CheckBox("useDocs")); | |||
form.add(new CheckBox("showRemoteBranches")); | |||
@@ -133,6 +144,51 @@ public class EditRepositoryPage extends BasePage { | |||
add(form); | |||
} | |||
/** | |||
* Unfortunately must repeat part of AuthorizaitonStrategy here because that | |||
* mechanism does not take PageParameters into consideration, only page | |||
* instantiation. | |||
* | |||
* Repository Owners should be able to edit their repository. | |||
*/ | |||
private void checkPermissions(RepositoryModel model) { | |||
boolean authenticateAdmin = GitBlit.self().settings().getBoolean(Keys.web.authenticateAdminPages, true); | |||
boolean allowAdmin = GitBlit.self().settings().getBoolean(Keys.web.allowAdministration, true); | |||
GitBlitWebSession session = GitBlitWebSession.get(); | |||
UserModel user = session.getUser(); | |||
if (allowAdmin) { | |||
if (authenticateAdmin) { | |||
if (user == null) { | |||
// No Login Available | |||
error("Administration requires a login", true); | |||
} | |||
if (isCreate) { | |||
// Create Repository | |||
if (!user.canAdmin()) { | |||
// Only Administrators May Create | |||
error("Only an administrator may create a repository", true); | |||
} | |||
} else { | |||
// Edit Repository | |||
if (user.canAdmin()) { | |||
// Admins can edit everything | |||
return; | |||
} else { | |||
if (!model.owner.equalsIgnoreCase(user.getUsername())) { | |||
// User is not an Admin nor Owner | |||
error("Only an administrator or the owner may edit a repository", true); | |||
} | |||
} | |||
} | |||
} | |||
} else { | |||
// No Administration Permitted | |||
error("Administration is disabled", true); | |||
} | |||
} | |||
private class AccessRestrictionRenderer implements IChoiceRenderer<AccessRestrictionType> { | |||
@@ -10,30 +10,21 @@ | |||
</wicket:head> | |||
<body> | |||
<wicket:extend> | |||
<div style="text-align:center;padding-top:5px;" wicket:id="feedback">[Feedback Panel]</div> | |||
<wicket:extend> | |||
<!-- Filler div --> | |||
<div style="padding-top:18px;"></div> | |||
<div style="text-align:center;padding-bottom:5px;" wicket:id="feedback">[Feedback Panel]</div> | |||
<div class="markdown" style="padding-top:5px;" wicket:id="repositoriesMessage">[repositories message]</div> | |||
<div class="markdown" style="margin-top:-0.5em;padding-bottom:5px;" wicket:id="repositoriesMessage">[repositories message]</div> | |||
<div style="padding-top:5px;" wicket:id="adminPanel">[admin links]</div> | |||
<div wicket:id="adminPanel">[admin links]</div> | |||
<table class="repositories"> | |||
<tr> | |||
<th wicket:id="orderByRepository"><wicket:message key="gb.repository">Repository</wicket:message></th> | |||
<th wicket:id="orderByDescription"><wicket:message key="gb.description">Description</wicket:message></th> | |||
<th wicket:id="orderByOwner"><wicket:message key="gb.owner">Owner</wicket:message></th> | |||
<th></th> | |||
<th wicket:id="orderByDate"><wicket:message key="gb.lastChange">Last Change</wicket:message></th> | |||
<th></th> | |||
</tr> | |||
<tbody> | |||
<tr wicket:id="repository"> | |||
<td><div class="list" wicket:id="repositoryName">[repository name]</div></td> | |||
<td><div class="list" wicket:id="repositoryDescription">[repository description]</div></td> | |||
<td class="author"><span wicket:id="repositoryOwner">[repository owner]</span></td> | |||
<td class="icon"><img wicket:id="ticketsIcon" /><img wicket:id="docsIcon" /><img wicket:id="accessRestrictionIcon" /></td> | |||
<td><span wicket:id="repositoryLastChange">[last change]</span></td> | |||
<td class="rightAlign"><span wicket:id="repositoryLinks"></span></td> | |||
<span wicket:id="headerContent"></span> | |||
<tbody> | |||
<tr wicket:id="row"> | |||
<span wicket:id="rowContent"></span> | |||
</tr> | |||
</tbody> | |||
</table> | |||
@@ -48,6 +39,45 @@ | |||
<wicket:fragment wicket:id="repositoryAdminLinks"> | |||
<span class="link"><a wicket:id="editRepository"><wicket:message key="gb.edit">[edit]</wicket:message></a> | <a wicket:id="renameRepository"><wicket:message key="gb.rename">[rename]</wicket:message></a> | <a wicket:id="deleteRepository"><wicket:message key="gb.delete">[delete]</wicket:message></a></span> | |||
</wicket:fragment> | |||
<wicket:fragment wicket:id="repositoryOwnerLinks"> | |||
<span class="link"><a wicket:id="editRepository"><wicket:message key="gb.edit">[edit]</wicket:message></a></span> | |||
</wicket:fragment> | |||
<wicket:fragment wicket:id="flatHeader"> | |||
<tr> | |||
<th wicket:id="orderByRepository"><wicket:message key="gb.repository">Repository</wicket:message></th> | |||
<th wicket:id="orderByDescription"><wicket:message key="gb.description">Description</wicket:message></th> | |||
<th wicket:id="orderByOwner"><wicket:message key="gb.owner">Owner</wicket:message></th> | |||
<th></th> | |||
<th wicket:id="orderByDate"><wicket:message key="gb.lastChange">Last Change</wicket:message></th> | |||
<th></th> | |||
</tr> | |||
</wicket:fragment> | |||
<wicket:fragment wicket:id="groupHeader"> | |||
<tr> | |||
<th><wicket:message key="gb.repository">Repository</wicket:message></th> | |||
<th><wicket:message key="gb.description">Description</wicket:message></th> | |||
<th><wicket:message key="gb.owner">Owner</wicket:message></th> | |||
<th></th> | |||
<th><wicket:message key="gb.lastChange">Last Change</wicket:message></th> | |||
<th></th> | |||
</tr> | |||
</wicket:fragment> | |||
<wicket:fragment wicket:id="groupRow"> | |||
<td colspan="6"><span wicket:id="groupName">[group name]</span></td> | |||
</wicket:fragment> | |||
<wicket:fragment wicket:id="repositoryRow"> | |||
<td><div class="list" wicket:id="repositoryName">[repository name]</div></td> | |||
<td><div class="list" wicket:id="repositoryDescription">[repository description]</div></td> | |||
<td class="author"><span wicket:id="repositoryOwner">[repository owner]</span></td> | |||
<td style="text-align: right;padding-right:10px;"><img class="inlineIcon" wicket:id="ticketsIcon" /><img class="inlineIcon" wicket:id="docsIcon" /><img class="inlineIcon" wicket:id="frozenIcon" /><img class="inlineIcon" wicket:id="accessRestrictionIcon" /></td> | |||
<td><span wicket:id="repositoryLastChange">[last change]</span></td> | |||
<td class="rightAlign"><span wicket:id="repositoryLinks"></span></td> | |||
</wicket:fragment> | |||
</wicket:extend> | |||
</body> |
@@ -4,8 +4,11 @@ import java.io.File; | |||
import java.io.FileReader; | |||
import java.io.InputStream; | |||
import java.io.InputStreamReader; | |||
import java.util.ArrayList; | |||
import java.util.Collections; | |||
import java.util.Comparator; | |||
import java.util.Date; | |||
import java.util.HashMap; | |||
import java.util.Iterator; | |||
import java.util.List; | |||
import java.util.Map; | |||
@@ -20,6 +23,8 @@ import org.apache.wicket.markup.html.link.BookmarkablePageLink; | |||
import org.apache.wicket.markup.html.panel.Fragment; | |||
import org.apache.wicket.markup.repeater.Item; | |||
import org.apache.wicket.markup.repeater.data.DataView; | |||
import org.apache.wicket.markup.repeater.data.IDataProvider; | |||
import org.apache.wicket.markup.repeater.data.ListDataProvider; | |||
import org.apache.wicket.model.IModel; | |||
import org.apache.wicket.model.Model; | |||
import org.apache.wicket.resource.ContextRelativeResource; | |||
@@ -42,13 +47,22 @@ public class RepositoriesPage extends BasePage { | |||
public RepositoriesPage() { | |||
super(); | |||
setupPage("", ""); | |||
final boolean showAdmin; | |||
if (GitBlit.self().settings().getBoolean(Keys.web.authenticateAdminPages, true)) { | |||
boolean allowAdmin = GitBlit.self().settings().getBoolean(Keys.web.allowAdministration, false); | |||
showAdmin = allowAdmin && GitBlitWebSession.get().canAdmin(); | |||
// authentication requires state and session | |||
setStatelessHint(false); | |||
} else { | |||
showAdmin = GitBlit.self().settings().getBoolean(Keys.web.allowAdministration, false); | |||
if (GitBlit.self().settings().getBoolean(Keys.web.authenticateViewPages, false)) { | |||
// authentication requires state and session | |||
setStatelessHint(false); | |||
} else { | |||
// no authentication required, no state and no session required | |||
setStatelessHint(true); | |||
} | |||
} | |||
Fragment adminLinks = new Fragment("adminPanel", "adminLinks", this); | |||
@@ -66,7 +80,7 @@ public class RepositoriesPage extends BasePage { | |||
// Load the markdown welcome message | |||
String messageSource = GitBlit.self().settings().getString(Keys.web.repositoriesMessage, "gitblit"); | |||
String message = ""; | |||
String message = "<br/>"; | |||
if (messageSource.equalsIgnoreCase("gitblit")) { | |||
// Read default welcome message | |||
try { | |||
@@ -99,70 +113,114 @@ public class RepositoriesPage extends BasePage { | |||
add(repositoriesMessage); | |||
final Map<AccessRestrictionType, String> accessRestrictionTranslations = getAccessRestrictions(); | |||
UserModel user = GitBlitWebSession.get().getUser(); | |||
List<RepositoryModel> rows = GitBlit.self().getRepositoryModels(user); | |||
DataProvider dp = new DataProvider(rows); | |||
DataView<RepositoryModel> dataView = new DataView<RepositoryModel>("repository", dp) { | |||
final UserModel user = GitBlitWebSession.get().getUser(); | |||
List<RepositoryModel> models = GitBlit.self().getRepositoryModels(user); | |||
IDataProvider<RepositoryModel> dp; | |||
if (GitBlit.self().settings().getString(Keys.web.repositoryListType, "flat").equalsIgnoreCase("grouped")) { | |||
Map<String, List<RepositoryModel>> groups = new HashMap<String, List<RepositoryModel>>(); | |||
for (RepositoryModel model : models) { | |||
String rootPath = StringUtils.getRootPath(model.name); | |||
if (StringUtils.isEmpty(rootPath)) { | |||
rootPath = GitBlit.self().settings().getString(Keys.web.repositoryRootGroupName, " "); | |||
} | |||
if (!groups.containsKey(rootPath)) { | |||
groups.put(rootPath, new ArrayList<RepositoryModel>()); | |||
} | |||
groups.get(rootPath).add(model); | |||
} | |||
List<String> roots = new ArrayList<String>(groups.keySet()); | |||
Collections.sort(roots); | |||
List<RepositoryModel> groupedModels = new ArrayList<RepositoryModel>(); | |||
for (String root : roots) { | |||
groupedModels.add(new GroupRepositoryModel(root)); | |||
groupedModels.addAll(groups.get(root)); | |||
} | |||
dp = new ListDataProvider<RepositoryModel>(groupedModels); | |||
} else { | |||
dp = new DataProvider(models); | |||
} | |||
DataView<RepositoryModel> dataView = new DataView<RepositoryModel>("row", dp) { | |||
private static final long serialVersionUID = 1L; | |||
int counter = 0; | |||
public void populateItem(final Item<RepositoryModel> item) { | |||
final RepositoryModel entry = item.getModelObject(); | |||
if (entry instanceof GroupRepositoryModel) { | |||
Fragment row = new Fragment("rowContent", "groupRow", this); | |||
item.add(row); | |||
row.add(new Label("groupName", entry.name)); | |||
WicketUtils.setCssClass(item, "group"); | |||
return; | |||
} | |||
Fragment row = new Fragment("rowContent", "repositoryRow", this); | |||
item.add(row); | |||
if (entry.hasCommits) { | |||
// Existing repository | |||
PageParameters pp = WicketUtils.newRepositoryParameter(entry.name); | |||
item.add(new LinkPanel("repositoryName", "list", entry.name, SummaryPage.class, pp)); | |||
item.add(new LinkPanel("repositoryDescription", "list", entry.description, SummaryPage.class, pp)); | |||
row.add(new LinkPanel("repositoryName", "list", entry.name, SummaryPage.class, pp)); | |||
row.add(new LinkPanel("repositoryDescription", "list", entry.description, SummaryPage.class, pp)); | |||
} else { | |||
// New repository | |||
item.add(new Label("repositoryName", entry.name + "<span class='empty'>(empty)</span>").setEscapeModelStrings(false)); | |||
item.add(new Label("repositoryDescription", entry.description)); | |||
row.add(new Label("repositoryName", entry.name + "<span class='empty'>(empty)</span>").setEscapeModelStrings(false)); | |||
row.add(new Label("repositoryDescription", entry.description)); | |||
} | |||
if (entry.useTickets) { | |||
item.add(WicketUtils.newImage("ticketsIcon", "bug_16x16.png", getString("gb.tickets"))); | |||
row.add(WicketUtils.newImage("ticketsIcon", "bug_16x16.png", getString("gb.tickets"))); | |||
} else { | |||
item.add(WicketUtils.newBlankImage("ticketsIcon")); | |||
row.add(WicketUtils.newBlankImage("ticketsIcon")); | |||
} | |||
if (entry.useDocs) { | |||
item.add(WicketUtils.newImage("docsIcon", "book_16x16.png", getString("gb.docs"))); | |||
row.add(WicketUtils.newImage("docsIcon", "book_16x16.png", getString("gb.docs"))); | |||
} else { | |||
row.add(WicketUtils.newBlankImage("docsIcon")); | |||
} | |||
if (entry.isFrozen) { | |||
row.add(WicketUtils.newImage("frozenIcon", "cold_16x16.png", getString("gb.isFrozen"))); | |||
} else { | |||
item.add(WicketUtils.newBlankImage("docsIcon")); | |||
row.add(WicketUtils.newClearPixel("frozenIcon").setVisible(false)); | |||
} | |||
switch (entry.accessRestriction) { | |||
case NONE: | |||
item.add(WicketUtils.newBlankImage("accessRestrictionIcon")); | |||
row.add(WicketUtils.newBlankImage("accessRestrictionIcon")); | |||
break; | |||
case PUSH: | |||
item.add(WicketUtils.newImage("accessRestrictionIcon", "lock_go_16x16.png", accessRestrictionTranslations.get(entry.accessRestriction))); | |||
row.add(WicketUtils.newImage("accessRestrictionIcon", "lock_go_16x16.png", accessRestrictionTranslations.get(entry.accessRestriction))); | |||
break; | |||
case CLONE: | |||
item.add(WicketUtils.newImage("accessRestrictionIcon", "lock_pull_16x16.png", accessRestrictionTranslations.get(entry.accessRestriction))); | |||
row.add(WicketUtils.newImage("accessRestrictionIcon", "lock_pull_16x16.png", accessRestrictionTranslations.get(entry.accessRestriction))); | |||
break; | |||
case VIEW: | |||
item.add(WicketUtils.newImage("accessRestrictionIcon", "shield_16x16.png", accessRestrictionTranslations.get(entry.accessRestriction))); | |||
row.add(WicketUtils.newImage("accessRestrictionIcon", "shield_16x16.png", accessRestrictionTranslations.get(entry.accessRestriction))); | |||
break; | |||
default: | |||
item.add(WicketUtils.newBlankImage("accessRestrictionIcon")); | |||
row.add(WicketUtils.newBlankImage("accessRestrictionIcon")); | |||
} | |||
item.add(new Label("repositoryOwner", entry.owner)); | |||
row.add(new Label("repositoryOwner", entry.owner)); | |||
String lastChange = TimeUtils.timeAgo(entry.lastChange); | |||
Label lastChangeLabel = new Label("repositoryLastChange", lastChange); | |||
item.add(lastChangeLabel); | |||
row.add(lastChangeLabel); | |||
WicketUtils.setCssClass(lastChangeLabel, TimeUtils.timeAgoCss(entry.lastChange)); | |||
boolean showOwner = user != null && user.getUsername().equalsIgnoreCase(entry.owner); | |||
if (showAdmin) { | |||
Fragment repositoryLinks = new Fragment("repositoryLinks", "repositoryAdminLinks", this); | |||
repositoryLinks.add(new BookmarkablePageLink<Void>("editRepository", EditRepositoryPage.class, WicketUtils.newRepositoryParameter(entry.name))); | |||
repositoryLinks.add(new BookmarkablePageLink<Void>("renameRepository", EditRepositoryPage.class, WicketUtils.newRepositoryParameter(entry.name)).setEnabled(false)); | |||
repositoryLinks.add(new BookmarkablePageLink<Void>("deleteRepository", EditRepositoryPage.class, WicketUtils.newRepositoryParameter(entry.name)).setEnabled(false)); | |||
item.add(repositoryLinks); | |||
row.add(repositoryLinks); | |||
} else if (showOwner) { | |||
Fragment repositoryLinks = new Fragment("repositoryLinks", "repositoryOwnerLinks", this); | |||
repositoryLinks.add(new BookmarkablePageLink<Void>("editRepository", EditRepositoryPage.class, WicketUtils.newRepositoryParameter(entry.name))); | |||
row.add(repositoryLinks); | |||
} else { | |||
item.add(new Label("repositoryLinks")); | |||
row.add(new Label("repositoryLinks")); | |||
} | |||
WicketUtils.setAlternatingBackground(item, counter); | |||
counter++; | |||
@@ -170,10 +228,20 @@ public class RepositoriesPage extends BasePage { | |||
}; | |||
add(dataView); | |||
add(newSort("orderByRepository", SortBy.repository, dp, dataView)); | |||
add(newSort("orderByDescription", SortBy.description, dp, dataView)); | |||
add(newSort("orderByOwner", SortBy.owner, dp, dataView)); | |||
add(newSort("orderByDate", SortBy.date, dp, dataView)); | |||
if (dp instanceof SortableDataProvider<?>) { | |||
// add sortable header | |||
SortableDataProvider<?> sdp = (SortableDataProvider<?>) dp; | |||
Fragment fragment = new Fragment("headerContent", "flatHeader", this); | |||
fragment.add(newSort("orderByRepository", SortBy.repository, sdp, dataView)); | |||
fragment.add(newSort("orderByDescription", SortBy.description, sdp, dataView)); | |||
fragment.add(newSort("orderByOwner", SortBy.owner, sdp, dataView)); | |||
fragment.add(newSort("orderByDate", SortBy.date, sdp, dataView)); | |||
add(fragment); | |||
} else { | |||
// not sortable | |||
Fragment fragment = new Fragment("headerContent", "groupHeader", this); | |||
add(fragment); | |||
} | |||
} | |||
protected enum SortBy { | |||
@@ -258,4 +326,13 @@ public class RepositoriesPage extends BasePage { | |||
return list.subList(first, first + count).iterator(); | |||
} | |||
} | |||
private class GroupRepositoryModel extends RepositoryModel { | |||
private static final long serialVersionUID = 1L; | |||
GroupRepositoryModel(String name) { | |||
super(name, "", "", new Date(0)); | |||
} | |||
} | |||
} |
@@ -47,6 +47,10 @@ pre, code, pre.prettyprint, pre.plainprint { | |||
font-style: italic; | |||
} | |||
img.inlineIcon { | |||
padding-left: 1px; | |||
padding-right: 1px; | |||
} | |||
a { | |||
color: #0000cc; | |||
@@ -552,6 +556,17 @@ tr th.wicket_orderDown a {background-image: url(arrow_down.png); } | |||
tr th.wicket_orderUp a { background-image: url(arrow_up.png); } | |||
tr th.wicket_orderNone a { background-image: url(arrow_off.png); } | |||
tr.group { | |||
background-color: #E66C2C; | |||
} | |||
tr.group td { | |||
font-weight: bold; | |||
border-bottom: 1px solid orange; | |||
color: white; | |||
background-color: #E66C2C; | |||
} | |||
tr.light { | |||
background-color: #ffffff; | |||
} |
@@ -1,5 +1,3 @@ | |||
## Welcome to Git:Blit | |||
A quick and easy way to host or view your own Git repositories. | |||
Built with [JGit](http://eclipse.org/jgit), [Wicket](http://wicket.apache.org), [WicketStuff GoogleCharts](https://github.com/wicketstuff/core/wiki/GoogleCharts), [MarkdownPapers](http://markdown.tautua.org), [Jetty](http://eclipse.org/jetty), [SLF4J](http://www.slf4j.org), [Log4j](http://logging.apache.org/log4j), [google-code-prettify](http://code.google.com/p/google-code-prettify), [JCommander](http://jcommander.org), [BouncyCastle](http://www.bouncycastle.org), [JavaService](http://forge.ow2.org/projects/javaservice), and most icons courtesy of [FatCow Hosting](http://www.fatcow.com/free-icons) | |||
A quick and easy way to host or view your own [Git](http://www.git-scm.com) repositories. |