From 00afd77a2182ece3d9522d41b86b4ddd7e132288 Mon Sep 17 00:00:00 2001 From: James Moger <james.moger@gitblit.com> Date: Thu, 19 May 2011 17:13:50 -0400 Subject: [PATCH] Owner editing. Frozen status. Grouped repositories. Documentation. --- src/com/gitblit/utils/StringUtils.java | 7 docs/architecture.png | 0 src/com/gitblit/wicket/pages/RepositoriesPage.java | 155 ++++++++++++---- src/com/gitblit/wicket/pages/EditRepositoryPage.java | 66 ++++++ src/com/gitblit/wicket/RepositoryPage.java | 19 - src/com/gitblit/BuildSite.java | 2 src/com/gitblit/wicket/models/RepositoryModel.java | 8 distrib/gitblit.properties | 10 + docs/01_eclipse.mkd | 17 + docs/architecture.odg | 0 src/com/gitblit/wicket/resources/gitblit.css | 15 + docs/00_overview.mkd | 37 ++- src/com/gitblit/GitBlit.java | 32 ++ src/com/gitblit/wicket/GitBlitWebApp.properties | 4 src/com/gitblit/wicket/resources/welcome.mkd | 4 src/com/gitblit/GitBlitServlet.java | 6 src/com/gitblit/wicket/resources/cold_16x16.png | 0 docs/01_configuration.mkd | 24 +- docs/sslverify2.png | 0 src/com/gitblit/wicket/pages/RepositoriesPage.html | 70 +++++-- docs/sslverify.png | 0 src/com/gitblit/wicket/pages/EditRepositoryPage.html | 5 src/com/gitblit/Constants.java | 2 src/com/gitblit/wicket/LoginPage.html | 4 24 files changed, 363 insertions(+), 124 deletions(-) diff --git a/distrib/gitblit.properties b/distrib/gitblit.properties index e13b60c..de243d2 100644 --- a/distrib/gitblit.properties +++ b/distrib/gitblit.properties @@ -66,6 +66,16 @@ 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 diff --git a/docs/00_overview.mkd b/docs/00_overview.mkd index 63898f6..7e286c5 100644 --- a/docs/00_overview.mkd +++ b/docs/00_overview.mkd @@ -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 @@ ### 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  @@ -73,8 +80,8 @@ - [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/> diff --git a/docs/01_configuration.mkd b/docs/01_configuration.mkd index 9e8a9f0..415ad47 100644 --- a/docs/01_configuration.mkd +++ b/docs/01_configuration.mkd @@ -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 @@ 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/> diff --git a/docs/01_eclipse.mkd b/docs/01_eclipse.mkd index 93fd531..c04834c 100644 --- a/docs/01_eclipse.mkd +++ b/docs/01_eclipse.mkd @@ -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. -how to push new unshared project to new repository \ No newline at end of file + + + + +### 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" + diff --git a/docs/architecture.odg b/docs/architecture.odg index fc4ea7b..c2fc25c 100644 --- a/docs/architecture.odg +++ b/docs/architecture.odg Binary files differ diff --git a/docs/architecture.png b/docs/architecture.png index d2f3e0c..0dd7ddc 100644 --- a/docs/architecture.png +++ b/docs/architecture.png Binary files differ diff --git a/docs/sslverify.png b/docs/sslverify.png new file mode 100644 index 0000000..e987b9f --- /dev/null +++ b/docs/sslverify.png Binary files differ diff --git a/docs/sslverify2.png b/docs/sslverify2.png new file mode 100644 index 0000000..94db977 --- /dev/null +++ b/docs/sslverify2.png Binary files differ diff --git a/src/com/gitblit/BuildSite.java b/src/com/gitblit/BuildSite.java index ae85c06..e122bc2 100644 --- a/src/com/gitblit/BuildSite.java +++ b/src/com/gitblit/BuildSite.java @@ -56,7 +56,7 @@ 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 { diff --git a/src/com/gitblit/Constants.java b/src/com/gitblit/Constants.java index 9cf6a54..7e19cef 100644 --- a/src/com/gitblit/Constants.java +++ b/src/com/gitblit/Constants.java @@ -4,7 +4,7 @@ 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. diff --git a/src/com/gitblit/GitBlit.java b/src/com/gitblit/GitBlit.java index 62ff55e..51c3b45 100644 --- a/src/com/gitblit/GitBlit.java +++ b/src/com/gitblit/GitBlit.java @@ -4,6 +4,7 @@ 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 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 List<String> getAllUsernames() { - return loginService.getAllUsernames(); + List<String> names = loginService.getAllUsernames(); + Collections.sort(names); + return names; } public UserModel getUserModel(String username) { @@ -169,15 +173,28 @@ 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 { @@ -209,6 +226,7 @@ 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) { diff --git a/src/com/gitblit/GitBlitServlet.java b/src/com/gitblit/GitBlitServlet.java index ffdc1b0..17642c6 100644 --- a/src/com/gitblit/GitBlitServlet.java +++ b/src/com/gitblit/GitBlitServlet.java @@ -44,12 +44,12 @@ 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; diff --git a/src/com/gitblit/utils/StringUtils.java b/src/com/gitblit/utils/StringUtils.java index ff47a51..8b7960b 100644 --- a/src/com/gitblit/utils/StringUtils.java +++ b/src/com/gitblit/utils/StringUtils.java @@ -107,4 +107,11 @@ throw new RuntimeException(t); } } + + public static String getRootPath(String path) { + if (path.indexOf('/') > -1) { + return path.substring(0, path.indexOf('/')); + } + return ""; + } } diff --git a/src/com/gitblit/wicket/GitBlitWebApp.properties b/src/com/gitblit/wicket/GitBlitWebApp.properties index 3fe24d0..d07f0bc 100644 --- a/src/com/gitblit/wicket/GitBlitWebApp.properties +++ b/src/com/gitblit/wicket/GitBlitWebApp.properties @@ -90,4 +90,6 @@ gb.useDocsDescription = enumerates Markdown documentation in repository gb.showRemoteBranchesDescription = show remote branches gb.canAdminDescription = can administer Git:Blit server -gb.permittedUsers = permitted users \ No newline at end of file +gb.permittedUsers = permitted users +gb.isFrozen = is frozen +gb.isFrozenDescription = deny push operations \ No newline at end of file diff --git a/src/com/gitblit/wicket/LoginPage.html b/src/com/gitblit/wicket/LoginPage.html index 71421ab..037063c 100644 --- a/src/com/gitblit/wicket/LoginPage.html +++ b/src/com/gitblit/wicket/LoginPage.html @@ -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" /> diff --git a/src/com/gitblit/wicket/RepositoryPage.java b/src/com/gitblit/wicket/RepositoryPage.java index 78fd33c..e3ae635 100644 --- a/src/com/gitblit/wicket/RepositoryPage.java +++ b/src/com/gitblit/wicket/RepositoryPage.java @@ -36,7 +36,6 @@ 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 @@ } 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 @@ 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 @@ 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 @@ 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; } diff --git a/src/com/gitblit/wicket/models/RepositoryModel.java b/src/com/gitblit/wicket/models/RepositoryModel.java index 43a7ac1..2aabfb1 100644 --- a/src/com/gitblit/wicket/models/RepositoryModel.java +++ b/src/com/gitblit/wicket/models/RepositoryModel.java @@ -17,9 +17,14 @@ 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 @@ this.description = description; this.owner = owner; this.lastChange = lastchange; + this.accessRestriction = AccessRestrictionType.NONE; } } \ No newline at end of file diff --git a/src/com/gitblit/wicket/pages/EditRepositoryPage.html b/src/com/gitblit/wicket/pages/EditRepositoryPage.html index db5ab22..763d46f 100644 --- a/src/com/gitblit/wicket/pages/EditRepositoryPage.html +++ b/src/com/gitblit/wicket/pages/EditRepositoryPage.html @@ -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> diff --git a/src/com/gitblit/wicket/pages/EditRepositoryPage.java b/src/com/gitblit/wicket/pages/EditRepositoryPage.java index 8820290..56d1d55 100644 --- a/src/com/gitblit/wicket/pages/EditRepositoryPage.java +++ b/src/com/gitblit/wicket/pages/EditRepositoryPage.java @@ -3,6 +3,7 @@ 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 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 @@ } 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 @@ super.setupPage("", getString("gb.edit")); if (repositoryModel.accessRestriction.exceeds(AccessRestrictionType.NONE)) { repositoryUsers.addAll(GitBlit.self().getRepositoryUsers(repositoryModel)); + Collections.sort(repositoryUsers); } } @@ -99,16 +105,20 @@ 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(); List<String> repositoryUsers = new ArrayList<String>(); 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); } @@ -124,8 +134,9 @@ // 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 @@ 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> { diff --git a/src/com/gitblit/wicket/pages/RepositoriesPage.html b/src/com/gitblit/wicket/pages/RepositoriesPage.html index 9c27f79..d00c498 100644 --- a/src/com/gitblit/wicket/pages/RepositoriesPage.html +++ b/src/com/gitblit/wicket/pages/RepositoriesPage.html @@ -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> diff --git a/src/com/gitblit/wicket/pages/RepositoriesPage.java b/src/com/gitblit/wicket/pages/RepositoriesPage.java index 53e3c2f..14a5426 100644 --- a/src/com/gitblit/wicket/pages/RepositoriesPage.java +++ b/src/com/gitblit/wicket/pages/RepositoriesPage.java @@ -4,8 +4,11 @@ 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.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 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 @@ // 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 @@ 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 { - item.add(WicketUtils.newBlankImage("docsIcon")); - } - - switch (entry.accessRestriction) { - case NONE: - item.add(WicketUtils.newBlankImage("accessRestrictionIcon")); - break; - case PUSH: - item.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))); - break; - case VIEW: - item.add(WicketUtils.newImage("accessRestrictionIcon", "shield_16x16.png", accessRestrictionTranslations.get(entry.accessRestriction))); - break; - default: - item.add(WicketUtils.newBlankImage("accessRestrictionIcon")); + row.add(WicketUtils.newBlankImage("docsIcon")); } - item.add(new Label("repositoryOwner", entry.owner)); + if (entry.isFrozen) { + row.add(WicketUtils.newImage("frozenIcon", "cold_16x16.png", getString("gb.isFrozen"))); + } else { + row.add(WicketUtils.newClearPixel("frozenIcon").setVisible(false)); + } + switch (entry.accessRestriction) { + case NONE: + row.add(WicketUtils.newBlankImage("accessRestrictionIcon")); + break; + case PUSH: + row.add(WicketUtils.newImage("accessRestrictionIcon", "lock_go_16x16.png", accessRestrictionTranslations.get(entry.accessRestriction))); + break; + case CLONE: + row.add(WicketUtils.newImage("accessRestrictionIcon", "lock_pull_16x16.png", accessRestrictionTranslations.get(entry.accessRestriction))); + break; + case VIEW: + row.add(WicketUtils.newImage("accessRestrictionIcon", "shield_16x16.png", accessRestrictionTranslations.get(entry.accessRestriction))); + break; + default: + row.add(WicketUtils.newBlankImage("accessRestrictionIcon")); + } + + 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 @@ }; 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 @@ 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)); + } + } } diff --git a/src/com/gitblit/wicket/resources/cold_16x16.png b/src/com/gitblit/wicket/resources/cold_16x16.png new file mode 100644 index 0000000..79cb756 --- /dev/null +++ b/src/com/gitblit/wicket/resources/cold_16x16.png Binary files differ diff --git a/src/com/gitblit/wicket/resources/gitblit.css b/src/com/gitblit/wicket/resources/gitblit.css index 2d41872..36afae5 100644 --- a/src/com/gitblit/wicket/resources/gitblit.css +++ b/src/com/gitblit/wicket/resources/gitblit.css @@ -47,6 +47,10 @@ font-style: italic; } +img.inlineIcon { + padding-left: 1px; + padding-right: 1px; +} a { color: #0000cc; @@ -552,6 +556,17 @@ 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; } diff --git a/src/com/gitblit/wicket/resources/welcome.mkd b/src/com/gitblit/wicket/resources/welcome.mkd index a9248ac..769baa4 100644 --- a/src/com/gitblit/wicket/resources/welcome.mkd +++ b/src/com/gitblit/wicket/resources/welcome.mkd @@ -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) \ No newline at end of file +A quick and easy way to host or view your own [Git](http://www.git-scm.com) repositories. -- Gitblit v1.9.1