From: James Moger Date: Sat, 25 Jun 2011 12:57:29 +0000 (-0400) Subject: Big push for first release. X-Git-Tag: v0.5.0~4 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=85c2e6eb34215e2242e388a8f8b7173a14b96ad3;p=gitblit.git Big push for first release. * Build script overhaul including building & publishing GO, WAR, Docs, and Site. * Restored JGit 0.12.1 dependency and backported Blame. Got tired of waiting for JGit 1.0.0 Maven artifacts. * Changed Summary Page layout * Optional cookie authentication * Added icons for log, tags, and branches panels. * Show last commit author and short message on branches panel. * Unit testing. * Documentation. --- diff --git a/.classpath b/.classpath index 28f56954..73cfac42 100644 --- a/.classpath +++ b/.classpath @@ -2,6 +2,7 @@ + @@ -49,35 +50,45 @@ - - + - + - + - + - + - + - + - + - + - + - + - + + + + + + + + + + + + diff --git a/NOTICE b/NOTICE index b45f30b8..bb9aa12c 100644 --- a/NOTICE +++ b/NOTICE @@ -119,6 +119,14 @@ JUnit http://junit.org +--------------------------------------------------------------------------- +ant-googlecode +--------------------------------------------------------------------------- + ant-googlecode, released under the + New BSD License + + http://code.google.com/p/ant-googlecode + --------------------------------------------------------------------------- Fancybox image viewer --------------------------------------------------------------------------- diff --git a/README.MKD b/README.MKD index bb272b38..a588cb44 100644 --- a/README.MKD +++ b/README.MKD @@ -1,20 +1,18 @@ Gitblit ================= -Gitblit is an open source, pure Java Git solution for creating, viewing, and serving [Git](http://git-scm.com) repositories.
+Gitblit is an open source, pure Java Git solution for managing, viewing, and serving [Git](http://git-scm.com) repositories.
More information about Gitblit can be found [here](http://gitblit.com). License ------- -Gitblit is distributed under the terms of the Apache Software Foundation -license, version 2.0. The text of the license is included in the file LICENSE in the root -of the project. +Gitblit is distributed under the terms of the Apache Software Foundation license, version 2.0. The text of the license is included in the file LICENSE in the root of the project. -Java/Application server requirements +Java Runtime Requirement ------------------------------------ -Gitblit requires at least Java 1.6. +Gitblit requires at Java 6 Runtime Environment (JRE) or a Java 6 Development Kit (JDK). Getting help ------------ diff --git a/build.xml b/build.xml index ee01de7d..9a00822b 100644 --- a/build.xml +++ b/build.xml @@ -1,14 +1,27 @@ - + + + + - + + + + + + + @@ -43,15 +56,18 @@ - - Building Gitblit ${gb.version} - + - - + + + @@ -67,8 +83,7 @@ - - + @@ -87,10 +102,31 @@ + + + + + + + Building Gitblit GO ${gb.version} + + + + + + + + + + + - - + @@ -102,10 +138,200 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Building Gitblit WAR ${gb.version} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Building Gitblit Website ${gb.version} + - - - + + + @@ -130,8 +356,8 @@ - - + + @@ -139,29 +365,29 @@ - + - + - + - - + + @@ -174,7 +400,7 @@ - + @@ -192,96 +418,10 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -293,100 +433,83 @@ + - - - - - - - - - - - - - - - + + + + - + - - - - - - - - - - + + - - - - - - - - - - - + Uploading Gitblit ${gb.version} binaries - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + - - - + + + + Uploading Gitblit ${gb.version} website + - + + + + + + + + + \ No newline at end of file diff --git a/distrib/gitblit.properties b/distrib/gitblit.properties index e41f12ce..4b5d3af0 100644 --- a/distrib/gitblit.properties +++ b/distrib/gitblit.properties @@ -2,9 +2,6 @@ # Git Servlet Settings # -# Allow push/pull over http/https with JGit servlet. -git.enableGitServlet = true - # Base folder for repositories # Changing this value requires a server restart. # Use forward slashes even on Windows!! @@ -18,6 +15,9 @@ git.repositoriesFolder = git # c:/gitrepos/libraries/myotherlibrary.git git.searchRepositoriesSubfolders = true +# Allow push/pull over http/https with JGit servlet. +git.enableGitServlet = true + # # Authentication Settings # @@ -30,11 +30,15 @@ web.authenticateViewPages = false # Changing this value requires a server restart. web.authenticateAdminPages = true -# Either a simple user realm file to authenticate users -# OR a fully qualified class name that implements the ILoginService interface. +# Allow Gitblit to store a cookie in the user's browser for automatic +# authentication. The cookie is generated by the user service. +web.allowCookieAuthentication = true + +# Either the path to a simple user properties file +# OR a fully qualified class name that implements the IUserService interface. # Any custom implementation must have a public default constructor. # Changing this value requires a server restart. -realm.realmFile = users.properties +realm.userService = users.properties # How to store passwords. # Valid values are plain or md5. Default is md5. @@ -60,7 +64,7 @@ web.allowAdministration = true # Allow dyanamic zip downloads. web.allowZipDownloads = true -# Default number of entries to include in RSS/Atom Syndication links +# Default number of entries to include in RSS Syndication links web.syndicationEntries = 25 # This is the message display above the repositories table. @@ -69,7 +73,9 @@ web.syndicationEntries = 25 web.repositoriesMessage = gitblit # Use the client timezone when formatting dates. -# This uses AJAX to determine the browser's timezone. +# This uses AJAX to determine the browser's timezone and may require more +# server overhead because a Wicket session is created. All Gitblit pages +# attempt to be stateless, if possible. # Changing this value requires a server restart. web.useClientTimezone = false @@ -118,8 +124,10 @@ web.generateActivityGraph = true # Value must exceed 0 else default of 20 is used web.summaryCommitCount = 16 -# The number of tags/heads to display on the summary page -# Value must exceed 0 else default of 5 is used +# The number of tags/branches to display on the summary page. +# -1 = all tags/branches +# 0 = hide tags/branches +# N = N tags/branches web.summaryRefsCount = 5 # The number of items to show on a page before showing the first, prev, next @@ -144,7 +152,7 @@ web.binaryExtensions = jar pdf tar.gz zip # Aggressive heap management will run the garbage collector on every generated # page. This slows down page generation a little but improves heap consumption. -web.aggressiveHeapManagement = true +web.aggressiveHeapManagement = false # Run the webapp in debug mode # Changing this value requires a server restart. @@ -171,7 +179,7 @@ regex.myrepository.bug = \\b(Bug:)(\\s*[#]?|-){0,1}(\\d+)\\b!!!This is to ensure authenticity of dependencies and to keep the Gitblit distribution svelte. +Screenshots -### Gitblit Features -- JGit SmartHTTP servlet -- Browser and git client authentication -- Four repository access control configurations and a Read-Only flag -
    -
  • ![anonymous](blank.png) *Anonymous View, Clone & Push*
  • -
  • ![push](lock_go_16x16.png) *Authenticated Push*
  • -
  • ![clone](lock_pull_16x16.png) *Authenticated Clone & Push*
  • -
  • ![view](shield_16x16.png) *Authenticated View, Clone & Push*
  • -
  • ![freeze](cold_16x16.png) Freeze repository (i.e. deny push, make read-only) -
-- 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 -- Git-notes support -- Branch metrics (uses Google Charts) -- HEAD and branch RSS feeds -- Blame annotations view -- Dates can optionally be displayed using the browser's reported timezone -- Display of Author and Committer email addresses can be disabled -- Case-insensitive searching of commit messages, authors, or committers -- Dynamic zip downloads feature -- Markdown file view support -- Syntax highlighting for popular source code types -- Customizable regular expression substitution for commit messages (i.e. bug or code review link integration) -- Single text file for users configuration -- Optional utility pages -
    -
  • ![docs](book_16x16.png) Docs page which enumerates all Markdown files within a repository
  • -
  • ![tickets](bug_16x16.png) Ticgit ticket pages *(based on last MIT release bf57b032 2009-01-27)*
  • -
+Gitblit is an open-source, pure Java stack for managing, viewing, and serving [Git][git] repositories.
+Its designed primarily as a tool for small workgroups who want to host centralized repositories. -### Gitblit-Go Features -- Out-of-the-box integrated stack requiring minimal configuration -- Automatically generates a self-signed certificate for https communications -- Single text file for server configuration +Gitblit is available in two variations: +
    +
  • *Gitblit GO* - a complete & integrated pure Java stack

    + This is what you should download if you want to go from zero to Git in less than 5 mins.
    + Gitblit GO is like a mashup of Apache httpd, [Git][git], and Gitweb with simplified configuration and maintenance.
    + All dependencies are downloaded on first execution.

    +

  • *Gitblit WAR* - a traditional WAR distribution

    + This is what you should download if you want to deploy Gitblit into your own servlet container (e.g. Tomcat, Jetty, etc).
    + All dependencies are bundled. +

-### Limitations -- HTTP/HTTPS are the only supported protocols -- Access controls are not path-based, they are repository-based -- Only Administrators can create, rename or delete repositories +### Java Runtime Requirement -### Caveats -- Gitblit may eat your data. Use at your own risk. -- Gitblit may have security holes. Patches welcome. :) +Gitblit requires a Java 6 Runtime Environment (JRE) or a Java 6 Development Kit (JDK). + +### Current Release -### Todo List -- Code documentation -- Unit testing -- Update Build.java to JGit 1.0.0, when its released +%VERSION% ([go](http://code.google.com/p/gitblit/downloads/detail?name=%GO%)|[war](http://code.google.com/p/gitblit/downloads/detail?name=%WAR%)) based on [%JGIT%][jgit] with Blame backport   (*%BUILDDATE%*) -### Idea List -- Consider clone remote repository feature -- Stronger Ticgit read-only integration - - activity/timeline - - query feature with paging support - - change history -- Ticgit write integration -- Blob page improvements - - view images - - view other binary files (pdf, doc, etc) -- Markdown editing feature +issues & binaries @ [Google Code][googlecode]
+sources @ [Github][gitbltsrc] ### License Gitblit is distributed under the terms of the [Apache Software Foundation license, version 2.0][apachelicense] -### Inspirations -- [Gitweb](http://www.git-scm.com) -- [Fossil](http://www.fossil-scm.org) - -## Architecture - -![block diagram](architecture.png "Gitblit Architecture") - -### Bundled Dependencies -The following dependencies are bundled with Gitblit. - -- [google-code-prettify](http://code.google.com/p/google-code-prettify) (Apache 2.0) -- [JavaService](http://forge.ow2.org/projects/javaservice) (BSD and LGPL) -- magnifying glass search icon courtesy of [Gnome](http://gnome.org) (Creative Commons CC-BY) -- modified Git logo originally designed by [Henrik Nyh](http://henrik.nyh.se/2007/06/alternative-git-logo-and-favicon) -- other icons courtesy of [FatCow Hosting](http://www.fatcow.com/free-icons) (Creative Commons CC-BY) - -### Downloaded Dependencies -The following dependencies are automatically downloaded by Gitblit-Go (or already bundled with the WAR) from the Apache Maven repository and from the Eclipse Maven repository when Gitblit is launched for the first time. - -- [JGit][jgit] (EDL 1.0) -- [Wicket](http://wicket.apache.org) (Apache 2.0) -- [WicketStuff GoogleCharts](https://github.com/wicketstuff/core/wiki/GoogleCharts) (Apache 2.0) -- [MarkdownPapers](http://markdown.tautua.org) (Apache 2.0) -- [Jetty](http://eclipse.org/jetty) (Apache 2.0, EPL 1.0) -- [SLF4J](http://www.slf4j.org) (MIT/X11) -- [Log4j](http://logging.apache.org/log4j) (Apache 2.0) -- [JCommander](http://jcommander.org) (Apache 2.0) -- [BouncyCastle](http://www.bouncycastle.org) (MIT/X11) -- [JSch - Java Secure Channel](http://www.jcraft.com/jsch) (BSD) -- [Rome](http://rome.dev.java.net) (Apache 1.1) -- [jdom](http://www.jdom.org) (Apache-style JDOM license) - -### Other Build Dependencies -- [Fancybox image viewer](http://fancybox.net) (MIT and GPL dual-licensed) -- [JUnit](http://junit.org) (Common Public License) -- [commons-net](http://commons.apache.org/net) (Apache 2.0) - -## Building from Source -[Eclipse](http://eclipse.org) is recommended for development as the project settings are preconfigured. - -Additionally, [Google CodePro AnalytiX](http://code.google.com/javadevtools), [eclipse-cs](http://eclipse-cs.sourceforge.net), [FindBugs](http://findbugs.sourceforge.net), and [EclEmma](http://www.eclemma.org) are recommended development tools. - -1. Clone the git repository from [Github][gitbltsrc]. -2. Import the gitblit project into your Eclipse workspace.
-*There will be lots of build errors.* -3. Using Ant, execute the `build.xml` script in the project root.
-*This will download all necessary build dependencies and will also generate the Keys class for accessing settings.* -4. Select your gitblit project root and **Refresh** the project, this should correct all build problems. -5. Using JUnit, execute the `com.gitblit.tests.GitBlitSuite` test suite.
-*This will clone some repositories from the web and run through the unit tests.* -5. Review the settings in `gitblit.properties` in your project root. - - By default, the *git.repositoriesFolder* points to the repositories cloned by the test suite.
- - If running on Linux you may have to change the served port(s) to > 1024 unless you are developing as the root user. -6. Execute the *com.gitblit.Launcher* class to start Gitblit. - - -## Contributing -Patches welcome in any form. - -Contributions must be your own original work and must licensed under the [Apache License, Version 2.0][apachelicense], the same license used by Gitblit. - [jgit]: http://eclipse.org/jgit "Eclipse JGit Site" [git]: http://git-scm.com "Official Git Site" -[gitbltsrc]: http://somewhere.com "gitblit git repository" +[gitbltsrc]: http://github.com/gitblit "gitblit git repository" +[googlecode]: http://code.google.com/p/gitblit "gitblit project management" [apachelicense]: http://www.apache.org/licenses/LICENSE-2.0 "Apache License, Version 2.0" \ No newline at end of file diff --git a/docs/00_setup.mkd b/docs/00_setup.mkd deleted file mode 100644 index 02816e7f..00000000 --- a/docs/00_setup.mkd +++ /dev/null @@ -1,101 +0,0 @@ -## Gitblit-Go Setup and Configuration - -1. Download and unzip [Gitblit-Go %VERSION%](http://gitblit.com/%GO%).
-*Its best to eliminate spaces in the path name as that can cause troubleshooting headaches.* -2. The server itself is configured through a simple text file.
-Open `gitblit.properties` in your favorite text editor and make sure to review and set: - - *git.repositoryFolder* - - *server.tempFolder* - - *server.httpBindInterface* and *server.httpsBindInterface*
-**NOTE:** Consider using **https** exclusively because passwords for authentication are transmitted as clear text! - - *server.storePassword*
-**NOTE:** If you manually generate an ssl certificate, 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 or depending on your chosen configuration. -6. Click the *Login* link and enter the default administrator credentials: **admin / admin**
-**NOTE:** Make sure to change the administrator username and/or password!! - -### Administering Repositories -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 = james - useTickets = false - useDocs = true - showRemoteBranches = false - accessRestriction = clone - isFrozen = false - showReadme = false - -#### Repository Names -Repository names must be unique and are CASE-SENSITIVE ON CASE-SENSITIVE FILESYSTEMS. The name must be composed of letters, digits, or `/ _ - .`
-Whitespace is illegal. - -Repositories can be grouped within subfolders. e.g. *libraries/mycoollib.git* and *libraries/myotherlib.git* - -All created repositories are *bare* and will automatically have *.git* appended to the name at creation time, if not already specified. - -#### 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 -All users are stored in the `users.properties` file or in the file you specified in `gitblit.properties`.
-The format of `users.properties` follows Jetty's convention for HashRealms: - - username,password,role1,role2,role3... - -#### Usernames -Usernames must be unique and are case-insensitive.
-Whitespace is illegal. - -#### Passwords -User passwords are CASE-SENSITIVE and may be *plain* or *md5* formatted (see `gitblit.properties` -> *realm.passwordStorage*). - -#### User Roles -There is only one actual *role* in Gitblit 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.
-**NOTE:** If you manually generate an ssl certificate, 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 installed Java Virtual Machine.
-Set the *JVM* variable in the script to the location of your Java Virtual Machine, add any necessary start parameters, and execute the script. - -#### Command-Line Parameters - --tempFolder Server temp folder - --repositoriesFolder Git Repositories Folder - --realmFile Users Realm Hash File - --useNio Use NIO Connector else use Socket Connector. - --httpPort HTTP port for to serve. (port <= 0 will disable this connector) - --httpsPort HTTPS port to serve. (port <= 0 will disable this connector) - --storePassword Password for SSL (https) keystore. - --shutdownPort Port for Shutdown Monitor to listen on. (port <= 0 will disable this monitor) - -**Example** - - java -jar gitblit.jar --realmFile c:\myrealm.txt --storePassword something - -## Client Setup and Configuration -### Https with Self-Signed Certificates -You must tell Git not to verify the self-signed certificate in order to perform any remote Git operations. - -- Eclipse/EGit - 1. Window->Preferences->Team->Git->Configuration - 2. Click the *New Entry* button - 3.
Key = *http.sslVerify*       
-       Value = *false*
-- Command-line Git ([Git-Config Manual Page](http://www.kernel.org/pub/software/scm/git/docs/git-config.html)) -
git config --global --bool --add http.sslVerify false
- -### Cloning an Access Restricted Repository -- Eclipse/Egit
Nothing special to configure, EGit figures out everything. -
https://yourserver/git/your/repository
-- Command-line Git
*My testing indicates that your username must be embedded in the url. YMMV.* -
https://username@yourserver/git/your/repository
- \ No newline at end of file diff --git a/docs/01_faq.mkd b/docs/01_faq.mkd deleted file mode 100644 index 552c89d2..00000000 --- a/docs/01_faq.mkd +++ /dev/null @@ -1,96 +0,0 @@ -## Troubleshooting - -### Eclipse/Egit/Git complains that it "can't open upload pack"? -There are a few ways this can occur: - -1. You are using https with a self-signed certificate and you **did not** configure *http.sslVerify=false* - 1. Window->Preferences->Team->Git->Configuration - 2. Click the *New Entry* button - 3.
Key = *http.sslVerify*       
-       Value = *false*
-2. The repository is clone-restricted and you don't have access. -3. The repository is clone-restricted and your password changed. -4. A regression in Gitblit. :( - -### Why can't I access Gitblit-Go from another machine? -Please check *server.httpBindInterface* and *server.httpsBindInterface* in `gitblit.properties`. - -### How do I run Gitblit-Go on port 80 or 443 in Linux? -Linux requires root permissions to serve on ports < 1024.
-Run the server as *root* (security concern) or change the ports you are serving to 8080 (http) and/or 8443 (https). - -## General Interest Questions - -### Gitblit? What kind of name is that? -It's a phonetic play on [bitblt][bitblt] which is an image processing operation meaning *bit-block transfer*. - -### Why use Gitblit? -It's a small tool that allows you to easily manage shared repositories and doesn't require alot of setup or git kung-foo. - -### Who is the target user for Gitblit? -Small workgroups that require centralized repositories. - -Gitblit is not meant to be a social coding resource like [Github](http://github.com) or [Bitbucket](http://bitbucket.com) with 100s or 1000s of users. Gitblit is designed to fulfill the same function as your centralized Subversion or CVS server. - -### Why does Gitblit exist? -As a Java developer I prefer that as much of my tooling as possible is Java.
-Originally, I was going to use [Mercurial](http://mercurial.selenic.com) but... - -- MercurialEclipse [shells to Python, writes to System.out, and captures System.in](http://mercurial.808500.n3.nabble.com/Hg4J-Mercurial-pure-Java-library-tp2693090p2694555.html)
-Parsing command-line output is fragile and suboptimal.
Unfortunately this is necessary because Mercurial is an application, not a library. -- Mercurial HTTP/HTTPS needs to run as CGI through Apache/IIS/etc, as mod_python through Apache, or served with a built-in http server.
-This requires setup and maintenance of multiple, mixed 3rd party components. - -Gitblit eliminates all that complication with its 100% Java stack and simple single configuration file. - -### Do I need real Git? -No. Gitblit is based on [JGit][jgit] which is a pure Java implementation of the [Git version control system][git].
-Everything you need for Gitblit is either in the zip distribution file or automatically downloaded on execution. - -### Can I run Gitblit in conjunction with my existing Git tooling? -Yes. - -### Do I need a JDK or can I use a JRE? -Gitblit will run just fine with a JRE. Gitblit can optionally use `keytool` from the JDK to generate self-signed certificates, but normally Gitblit uses [BouncyCastle][bouncycastle] for that need. - -### Does Gitblit use a database to store its data? -No. Gitblit stores its repository configuration information within the `.git/config` file and its user information in `users.properties` or whatever filename is configured in `gitblit.properties`. - -### Can I manually edit users.properties, gitblit.properties, or .git/config? -Yes. You can manually manipulate all of them and (most) changes will be immediately available to Gitblit.
Exceptions to this are noted in `gitblit.properties`. - -*NOTE:* Care must be taken to preserve the relationship between user roles and repository names.
Please see the [setup](/setup.html) page for details. - -### Can I restrict access to paths within a repository? -No. Access restrictions apply to the repository as a whole. - -Gitblit's simple authentication and authorization mechanism can be used to facilitate one or more of the [workflows outlined here](http://progit.org/book/ch5-1.html). Should you require more fine-grained access controls you might consider using [gitolite](https://github.com/sitaramc/gitolite). - -### Can I authenticate users against XYZ? -Yes. The login service is pluggable. You may write your own authentication module by implementing the *ILoginService* interface. Set the fully qualified classname as the *realm.realmFile* property. - -### Why doesn't Gitblit support SSH? -Gitblit could integrate [Apache Mina][mina] to provide SSH access. However, doing so violates Gitblit's first design principle: [KISS](http://en.wikipedia.org/wiki/KISS_principle).
-SSH support requires creating, exchanging, and managing SSH keys (arguably not more complicated than managing users). While this is possible, JGit's SmartHTTP implementation is a simpler and universal transport mechanism. - -You might consider running [Gerrit](http://gerrit.googlecode.org) which does integrate [Apache Mina][mina] and supports SSH or you might consider serving [Git][git] on Linux which would offer real SSH support and also allow use of [many other compelling Git solutions](https://git.wiki.kernel.org/index.php/InterfacesFrontendsAndTools). - -### What types of Search does Gitblit support? -Gitblit supports case-insensitive searches of *commit message* (default), *author*, and *committer*.
- -To search by *author* or *committer* use the following syntax in the search box: - - author: james - committer: james - -Alternatively, you could enable the search type dropdown list in your `gitblit.properties` file. - -### Can Gitblit be translated? - -Yes. Most messages are localized to a standard Java properties file. - -[bitblt]: http://en.wikipedia.org/wiki/Bit_blit "Wikipedia Bitblt" -[jgit]: http://eclipse.org/jgit "Eclipse JGit Site" -[git]: http://git-scm.com "Official Git Site" -[mina]: http://mina.apache.org "Apache Mina" -[bouncycastle]: http://bouncycastle.org "The Legion of the Bouncy Castle" \ No newline at end of file diff --git a/docs/01_features.mkd b/docs/01_features.mkd new file mode 100644 index 00000000..e31d9596 --- /dev/null +++ b/docs/01_features.mkd @@ -0,0 +1,64 @@ +## Gitblit Features +- JGit SmartHTTP servlet +- Browser and git client authentication +- Four repository access control configurations with a Read-Only control flag +
    +
  • ![anonymous](blank.png) *Anonymous View, Clone & Push*
  • +
  • ![push](lock_go_16x16.png) *Authenticated Push*
  • +
  • ![clone](lock_pull_16x16.png) *Authenticated Clone & Push*
  • +
  • ![view](shield_16x16.png) *Authenticated View, Clone & Push*
  • +
  • ![freeze](cold_16x16.png) Freeze repository (i.e. deny push, make read-only) +
+- Gitweb inspired web 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 +- Git-notes support +- Branch metrics (uses Google Charts) +- HEAD and Branch RSS feeds +- Blame annotations view +- Dates can optionally be displayed using the browser's reported timezone +- Display of Author and Committer email addresses can be disabled +- Case-insensitive searching of commit messages, authors, or committers +- Dynamic zip downloads feature +- Markdown file view support +- Syntax highlighting for popular source code types +- Customizable regular expression substitution for commit messages (i.e. bug or code review link integration) +- Single text file for users configuration +- Optional utility pages +
    +
  • ![docs](book_16x16.png) Docs page which enumerates all Markdown files within a repository
  • +
  • ![tickets](bug_16x16.png) Ticgit ticket pages *(based on last MIT release bf57b032 2009-01-27)*
  • +
+ +## Gitblit GO Features +- Out-of-the-box integrated stack requiring minimal configuration +- Automatically generates a self-signed certificate for https communications +- Single text file for configuring server and gitblit + +## Limitations +- HTTP/HTTPS are the only supported protocols +- Access controls are not path-based, they are repository-based +- Only Administrators can create, rename or delete repositories +- Only Administrators can create, modify or delete users + +### Caveats +- Gitblit may eat your data. Use at your own risk. +- Gitblit may have security holes. Patches welcome. :) + +## Todo List +- Code documentation +- Unit testing +- Update to JGit 1.0.0 when JGit team provides Maven artifacts + +### Under Consideration +- Clone remote repository feature +- Blob page improvements + - view images + - view other binary files (pdf, doc, etc) +- Markdown editing feature +- Stronger Ticgit read-only integration + - activity/timeline + - query feature with paging support + - change history +- Ticgit write integration \ No newline at end of file diff --git a/docs/01_releases.mkd b/docs/01_releases.mkd deleted file mode 100644 index 7c0c2040..00000000 --- a/docs/01_releases.mkd +++ /dev/null @@ -1 +0,0 @@ -## Release History diff --git a/docs/01_setup.mkd b/docs/01_setup.mkd new file mode 100644 index 00000000..8ad7c5ed --- /dev/null +++ b/docs/01_setup.mkd @@ -0,0 +1,129 @@ +## Gitblit WAR Setup + +1. Download [Gitblit WAR %VERSION%](http://code.google.com/p/gitblit/downloads/detail?name=%WAR%) to the webapps folder of your servlet container.
+2. You may have to manually extract the WAR (zip file) to a folder within your webapps folder. Manual extraction depends on if your servlet container is configured to automatically deploy WAR files. +3. Copy the `WEB-INF/users.properties` file to a location outside the webapps folder but accessible by your servlet container. +4. The Gitblit webapp is configured through its `web.xml` file.
+Open `web.xml` in your favorite text editor and make sure to review and set: + - <context-parameter> *git.repositoryFolder* (set the full path to your repositories folder) + - <context-parameter> *realm.userService* (set the full path to `users.properties`) +5. You may have to restart your servlet container. +6. Open your browser to or whatever the url should be. +7. Click the *Login* link and enter the default administrator credentials: **admin / admin**
+**NOTE:** Make sure to change the administrator username and/or password!! + +## Gitblit GO Setup + +1. Download and unzip [Gitblit GO %VERSION%](http://code.google.com/p/gitblit/downloads/detail?name=%GO%).
+*Its best to eliminate spaces in the path name.* +2. The server itself is configured through a simple text file.
+Open `gitblit.properties` in your favorite text editor and make sure to review and set: + - *git.repositoryFolder* (path my be relative or absolute) + - *server.tempFolder* (path my be relative or absolute) + - *server.httpBindInterface* and *server.httpsBindInterface*
+**NOTE:** Consider using **https** exclusively because passwords for authentication are transmitted as clear text! + - *server.storePassword*
+**NOTE:** If you manually generate an ssl certificate, 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 or depending on your chosen configuration. +6. Click the *Login* link and enter the default administrator credentials: **admin / admin**
+**NOTE:** Make sure to change the administrator username and/or password!! + +### Creating your own Self-Signed Certificate +Gitblit GO automatically generates an ssl certificate for you that contains generic, non-personalized information. + +Should you want to include more personal or server-specific information in your self-signed certificate you will have to generate a new one. + +Review the contents of the `makekeystore.cmd` or `makekeystore_jdk.cmd` script and execute it.
+**NOTE:** If you manually generate an ssl certificate, the certificate password AND the keystore password must match! + +### Running as a Windows Service +Review the contents of the `installService.cmd` or `installService64.cmd`, as appropriate for your installed Java Virtual Machine.
+Set the *JVM* variable in the script to the location of your Java Virtual Machine, add any necessary start parameters, and execute the script. + +#### Command-Line Parameters +Command-Line parameters override the values in `gitblit.properties` at runtime. + + --repositoriesFolder Git Repositories Folder + --userService Authentication and Authorization Service (filename or fully qualified classname) + --useNio Use NIO Connector else use Socket Connector. + --httpPort HTTP port for to serve. (port <= 0 will disable this connector) + --httpsPort HTTPS port to serve. (port <= 0 will disable this connector) + --storePassword Password for SSL (https) keystore. + --shutdownPort Port for Shutdown Monitor to listen on. (port <= 0 will disable this monitor) + --tempFolder Folder for server to extract built-in webapp + +**Example** + + java -jar gitblit.jar --userService c:\myrealm.properties --storePassword something + +## Gitblit Configuration + +### Administering Repositories +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 = james + useTickets = false + useDocs = true + showRemoteBranches = false + accessRestriction = clone + isFrozen = false + showReadme = false + +#### Repository Names +Repository names must be unique and are CASE-SENSITIVE ON CASE-SENSITIVE FILESYSTEMS. The name must be composed of letters, digits, or `/ _ - .`
+Whitespace is illegal. + +Repositories can be grouped within subfolders. e.g. *libraries/mycoollib.git* and *libraries/myotherlib.git* + +All repositories created with Gitblit are *bare* and will automatically have *.git* appended to the name at creation time, if not already specified. + +#### 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 +All users are stored in the `users.properties` file or in the file you specified in `gitblit.properties`.
+The format of `users.properties` follows Jetty's convention for HashRealms: + + username,password,role1,role2,role3... + +#### Usernames +Usernames must be unique and are case-insensitive.
+Whitespace is illegal. + +#### Passwords +User passwords are CASE-SENSITIVE and may be *plain* or *md5* formatted (see `gitblit.properties` -> *realm.passwordStorage*). + +#### User Roles +There is only one actual *role* in Gitblit 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. + +## Authentication and Authorization Customization +Instead of maintaining a `users.properties` file, you may want to integrate Gitblit into an existing environment. + +You may use your own custom *com.gitblit.IUserService* implementation by specifying its fully qualified classname in the *realm.userService* setting.
+ +Your user service class must be on Gitblit's classpath and must have a public default constructor. + +## Client Setup and Configuration +### Https with Self-Signed Certificates +You must tell Git not to verify the self-signed certificate in order to perform any remote Git operations. + +- Eclipse/EGit + 1. Window->Preferences->Team->Git->Configuration + 2. Click the *New Entry* button + 3.
Key = *http.sslVerify*       
+       Value = *false*
+- Command-line Git ([Git-Config Manual Page](http://www.kernel.org/pub/software/scm/git/docs/git-config.html)) +
git config --global --bool --add http.sslVerify false
+ +### Cloning an Access Restricted Repository +- Eclipse/Egit
Nothing special to configure, EGit figures out everything. +
https://yourserver/git/your/repository
+- Command-line Git
*My testing indicates that your username must be embedded in the url. YMMV.* +
https://username@yourserver/git/your/repository
+ \ No newline at end of file diff --git a/docs/02_faq.mkd b/docs/02_faq.mkd new file mode 100644 index 00000000..7958f307 --- /dev/null +++ b/docs/02_faq.mkd @@ -0,0 +1,109 @@ +## Troubleshooting + +### Eclipse/Egit/Git complains that it "can't open upload pack"? +There are a few ways this can occur: + +1. You are using https with a self-signed certificate and you **did not** configure *http.sslVerify=false* + 1. Window->Preferences->Team->Git->Configuration + 2. Click the *New Entry* button + 3.
Key = *http.sslVerify*       
+       Value = *false*
+2. The repository is clone-restricted and you don't have access. +3. The repository is clone-restricted and your password changed. +4. A regression in Gitblit. :( + +### Why can't I access Gitblit GO from another machine? +Please check *server.httpBindInterface* and *server.httpsBindInterface* in `gitblit.properties`, you may be binding only to localhost. + +And of course ensure that any firewall you may have running either has an exception for your ports or for the running process. + +### How do I run Gitblit GO on port 80 or 443 in Linux? +Linux requires root permissions to serve on ports < 1024.
+Run the server as *root* (security concern) or change the ports you are serving to 8080 (http) and/or 8443 (https). + +### Gitblit GO does not list my repositories?! +Confirm that the value *git.repositoriesFolder* in `gitblit.properties` actually points to your repositories folder. + +### Gitblit WAR does not list my repositories?! +Confirm that the <context-param> *git.repositoriesFolder* value in your `web.xml` file actually points to your repositories folder. + +### Gitblit WAR will not authenticate any users?! +Confirm that the <context-param> *realm.userService* value in your `web.xml` file actually points to a `users.properties` file. + +## General Interest Questions + +### Gitblit? What kind of name is that? +It's a phonetic play on [bitblt][bitblt] which is an image processing operation meaning *bit-block transfer*. + +### Why use Gitblit? +It's a small tool that allows you to easily manage shared repositories and doesn't require alot of setup or git kung-foo. + +### Who is the target user for Gitblit? +Small workgroups that require centralized repositories. + +Gitblit is not meant to be a social coding resource like [Github](http://github.com) or [Bitbucket](http://bitbucket.com) with 100s or 1000s of users. Gitblit is designed to fulfill the same function as your centralized Subversion or CVS server. + +### Why does Gitblit exist when there is Git and Gitweb? +As a Java developer I prefer that as much of my tooling as possible is Java.
+Originally, I was going to use [Mercurial](http://mercurial.selenic.com) but... + +- MercurialEclipse [shells to Python, writes to System.out, and captures System.in](http://mercurial.808500.n3.nabble.com/Hg4J-Mercurial-pure-Java-library-tp2693090p2694555.html)
+Parsing command-line output is fragile and suboptimal.
Unfortunately this is necessary because Mercurial is an application, not a library. +- Mercurial HTTP/HTTPS needs to run as CGI through Apache/IIS/etc, as mod_python through Apache, or served with a built-in http server.
+This requires setup and maintenance of multiple, mixed 3rd party components. + +Gitblit eliminates all that complication with its 100% Java stack and simple single configuration file. + +Additionally, Git and Gitweb do not offer repository creation or user management. + +### Do I need real Git? +No. Gitblit is based on [JGit][jgit] which is a pure Java implementation of the [Git version control system][git].
+Everything you need for Gitblit (except Java) is either bundled in the distribution file or automatically downloaded on execution. + +### Can I run Gitblit in conjunction with my existing Git tooling? +Yes. + +### Do I need a JDK or can I use a JRE? +Gitblit will run just fine with a JRE. Gitblit can optionally use `keytool` from the JDK to generate self-signed certificates, but normally Gitblit uses [BouncyCastle][bouncycastle] for that need. + +### Does Gitblit use a database to store its data? +No. Gitblit stores its repository configuration information within the `.git/config` file and its user information in `users.properties` or whatever filename is configured in `gitblit.properties`. + +### Can I manually edit users.properties, gitblit.properties, or .git/config? +Yes. You can manually manipulate all of them and (most) changes will be immediately available to Gitblit.
Exceptions to this are noted in `gitblit.properties`. + +*NOTE:* Care must be taken to preserve the relationship between user roles and repository names.
Please see the *User Roles* section of the [setup](/setup.html) page for details. + +### Can I restrict access to paths within a repository? +No. Access restrictions apply to the repository as a whole. + +Gitblit's simple authentication and authorization mechanism can be used to facilitate one or more of the [workflows outlined here](http://progit.org/book/ch5-1.html). Should you require more fine-grained access controls you might consider using [gitolite](https://github.com/sitaramc/gitolite). + +### Can I authenticate users against XYZ? +Yes. The user service is pluggable. You may write your own user service by implementing the *com.gitblit.IUserService* interface. Set the fully qualified classname as the *realm.userService* property. + +### Why doesn't Gitblit support SSH? +Gitblit could integrate [Apache Mina][mina] to provide SSH access. However, doing so violates Gitblit's first design principle: [KISS](http://en.wikipedia.org/wiki/KISS_principle).
+SSH support requires creating, exchanging, and managing SSH keys (arguably not more complicated than managing users). While this is possible, JGit's SmartHTTP implementation is a simpler and universal transport mechanism. + +You might consider running [Gerrit](http://gerrit.googlecode.org) which does integrate [Apache Mina][mina] and supports SSH or you might consider serving [Git][git] on Linux which would offer real SSH support and also allow use of [many other compelling Git solutions](https://git.wiki.kernel.org/index.php/InterfacesFrontendsAndTools). + +### What types of Search does Gitblit support? +Gitblit supports case-insensitive searches of *commit message* (default), *author*, and *committer*.
+ +To search by *author* or *committer* use the following syntax in the search box: + + author: james + committer: james + +Alternatively, you could enable the search type dropdown list in your `gitblit.properties` file. + +### Can Gitblit be translated? + +Yes. Most messages are localized to a standard Java properties file. + +[bitblt]: http://en.wikipedia.org/wiki/Bit_blit "Wikipedia Bitblt" +[jgit]: http://eclipse.org/jgit "Eclipse JGit Site" +[git]: http://git-scm.com "Official Git Site" +[mina]: http://mina.apache.org "Apache Mina" +[bouncycastle]: http://bouncycastle.org "The Legion of the Bouncy Castle" \ No newline at end of file diff --git a/docs/02_properties.mkd b/docs/02_properties.mkd deleted file mode 100644 index 3a1dec17..00000000 --- a/docs/02_properties.mkd +++ /dev/null @@ -1,4 +0,0 @@ -## gitblit.properties -
-%PROPERTIES%
-
\ No newline at end of file diff --git a/docs/03_properties.mkd b/docs/03_properties.mkd new file mode 100644 index 00000000..3a1dec17 --- /dev/null +++ b/docs/03_properties.mkd @@ -0,0 +1,4 @@ +## gitblit.properties +
+%PROPERTIES%
+
\ No newline at end of file diff --git a/docs/04_design.mkd b/docs/04_design.mkd new file mode 100644 index 00000000..244b35e3 --- /dev/null +++ b/docs/04_design.mkd @@ -0,0 +1,69 @@ +## Design Principles +1. [Keep It Simple, Stupid](http://en.wikipedia.org/wiki/KISS_principle) +2. Offer useful features for serving Git repositories. If feature is complex, refer to #1. +3. All dependencies must be retrievable from a publicly accessible [Maven](http://maven.apache.org) repository.
This is to ensure authenticity of dependencies, to keep the Gitblit GO distribution svelte, and to automate the setup of developer environments. + +## Architecture + +![block diagram](architecture.png "Gitblit Architecture") + +### Bundled Dependencies +The following dependencies are bundled with Gitblit. + +- [google-code-prettify](http://code.google.com/p/google-code-prettify) (Apache 2.0) +- [JavaService](http://forge.ow2.org/projects/javaservice) (BSD and LGPL) +- magnifying glass search icon courtesy of [Gnome](http://gnome.org) (Creative Commons CC-BY) +- modified Git logo originally designed by [Henrik Nyh](http://henrik.nyh.se/2007/06/alternative-git-logo-and-favicon) +- other icons courtesy of [FatCow Hosting](http://www.fatcow.com/free-icons) (Creative Commons CC-BY) + +### Downloaded Dependencies +The following dependencies are automatically downloaded by Gitblit GO (or already bundled with the WAR) from the Apache Maven repository and from the Eclipse Maven repository when Gitblit is launched for the first time. + +- [JGit][jgit] (EDL 1.0) +- [Wicket](http://wicket.apache.org) (Apache 2.0) +- [WicketStuff GoogleCharts](https://github.com/wicketstuff/core/wiki/GoogleCharts) (Apache 2.0) +- [MarkdownPapers](http://markdown.tautua.org) (Apache 2.0) +- [Jetty](http://eclipse.org/jetty) (Apache 2.0, EPL 1.0) +- [SLF4J](http://www.slf4j.org) (MIT/X11) +- [Log4j](http://logging.apache.org/log4j) (Apache 2.0) +- [JCommander](http://jcommander.org) (Apache 2.0) +- [BouncyCastle](http://www.bouncycastle.org) (MIT/X11) +- [JSch - Java Secure Channel](http://www.jcraft.com/jsch) (BSD) +- [Rome](http://rome.dev.java.net) (Apache 1.1) +- [jdom](http://www.jdom.org) (Apache-style JDOM license) + +### Other Build Dependencies +- [Fancybox image viewer](http://fancybox.net) (MIT and GPL dual-licensed) +- [JUnit](http://junit.org) (Common Public License) +- [commons-net](http://commons.apache.org/net) (Apache 2.0) +- [ant-googlecode](http://code.google.com/p/ant-googlecode) (New BSD) + +## Building from Source +[Eclipse](http://eclipse.org) is recommended for development as the project settings are preconfigured. + +Additionally, [Google CodePro AnalytiX](http://code.google.com/javadevtools), [eclipse-cs](http://eclipse-cs.sourceforge.net), [FindBugs](http://findbugs.sourceforge.net), and [EclEmma](http://www.eclemma.org) are recommended development tools. + +1. Clone the git repository from [Github][gitbltsrc]. +2. Import the gitblit project into your Eclipse workspace.
+*There will be lots of build errors.* +3. Using Ant, execute the `build.xml` script in the project root.
+*This will download all necessary build dependencies and will also generate the Keys class for accessing settings.* +4. Select your gitblit project root and **Refresh** the project, this should correct all build problems. +5. Using JUnit, execute the `com.gitblit.tests.GitBlitSuite` test suite.
+*This will clone some repositories from the web and run through the unit tests.* +5. Review the settings in `gitblit.properties` in your project root. + - By default, the *git.repositoriesFolder* points to the repositories cloned by the test suite.
+ - If running on Linux you may have to change the served port(s) to > 1024 unless you are developing as the root user. +6. Execute the *com.gitblit.Launcher* class to start Gitblit. + + +## Contributing +Patches welcome in any form. + +Contributions must be your own original work and must licensed under the [Apache License, Version 2.0][apachelicense], the same license used by Gitblit. + +[jgit]: http://eclipse.org/jgit "Eclipse JGit Site" +[git]: http://git-scm.com "Official Git Site" +[gitbltsrc]: http://github.com/gitblit "gitblit git repository" +[googlecode]: http://code.google.com/p/gitblit "gitblit project management" +[apachelicense]: http://www.apache.org/licenses/LICENSE-2.0 "Apache License, Version 2.0" \ No newline at end of file diff --git a/docs/04_releases.mkd b/docs/04_releases.mkd new file mode 100644 index 00000000..109041d2 --- /dev/null +++ b/docs/04_releases.mkd @@ -0,0 +1,9 @@ +## Release History + +### Current Release +%VERSION% ([go](http://code.google.com/p/gitblit/downloads/detail?name=%GO%)|[war](http://code.google.com/p/gitblit/downloads/detail?name=%WAR%)) based on [%JGIT%][jgit] with Blame backport   (*%BUILDDATE%*) + +### Older Releases +none + +[jgit]: http://eclipse.org/jgit "Eclipse JGit Site" \ No newline at end of file diff --git a/resources/commit_changes_16x16.png b/resources/commit_changes_16x16.png new file mode 100644 index 00000000..257cfee3 Binary files /dev/null and b/resources/commit_changes_16x16.png differ diff --git a/resources/gitblit.css b/resources/gitblit.css index 29474188..08d147ac 100644 --- a/resources/gitblit.css +++ b/resources/gitblit.css @@ -56,6 +56,11 @@ img.inlineIcon { padding-right: 1px; } +img.overview { + float:right; + border:1px solid #CCCCCC; +} + a { color: #0000cc; } diff --git a/src/com/gitblit/Build.java b/src/com/gitblit/Build.java index 90224f08..b6c485a3 100644 --- a/src/com/gitblit/Build.java +++ b/src/com/gitblit/Build.java @@ -86,10 +86,10 @@ public class Build { downloadFromApache(MavenObject.JSCH, BuildType.COMPILETIME); downloadFromApache(MavenObject.ROME, BuildType.COMPILETIME); downloadFromApache(MavenObject.JDOM, BuildType.COMPILETIME); - + downloadFromEclipse(MavenObject.JGIT, BuildType.COMPILETIME); downloadFromEclipse(MavenObject.JGIT_HTTP, BuildType.COMPILETIME); - + // needed for site publishing downloadFromApache(MavenObject.COMMONSNET, BuildType.RUNTIME); } @@ -401,18 +401,17 @@ public class Build { "e528f593b19b04d500992606f58b87fcfded8883", "d0ffadd0a4ab909d94a577b5aad43c13b617ddcb"); - public static final MavenObject COMMONSNET = new MavenObject("commons-net", "commons-net", "commons-net", - "1.4.0", 181000, 0, 0, "eb47e8cad2dd7f92fd7e77df1d1529cae87361f7", - "", - ""); - - public static final MavenObject ROME = new MavenObject("rome", "rome", "rome", - "0.9", 208000, 196000, 407000, "dee2705dd01e79a5a96a17225f5a1ae30470bb18", + public static final MavenObject COMMONSNET = new MavenObject("commons-net", "commons-net", + "commons-net", "1.4.0", 181000, 0, 0, "eb47e8cad2dd7f92fd7e77df1d1529cae87361f7", + "", ""); + + public static final MavenObject ROME = new MavenObject("rome", "rome", "rome", "0.9", + 208000, 196000, 407000, "dee2705dd01e79a5a96a17225f5a1ae30470bb18", "226f851dc44fd94fe70b9c471881b71f88949cbf", "8d7d867b97eeb3a9196c3926da550ad042941c1b"); - public static final MavenObject JDOM = new MavenObject("jdom", "org/jdom", "jdom", - "1.1", 153000, 235000, 445000, "1d04c0f321ea337f3661cf7ede8f4c6f653a8fdd", + public static final MavenObject JDOM = new MavenObject("jdom", "org/jdom", "jdom", "1.1", + 153000, 235000, 445000, "1d04c0f321ea337f3661cf7ede8f4c6f653a8fdd", "a7ed425c4c46605b8f2bf2ee118c1609682f4f2c", "f3df91edccba2f07a0fced70887c2f7b7836cb75"); diff --git a/src/com/gitblit/BuildThumbnails.java b/src/com/gitblit/BuildThumbnails.java new file mode 100644 index 00000000..4f2b2ab8 --- /dev/null +++ b/src/com/gitblit/BuildThumbnails.java @@ -0,0 +1,134 @@ +/* + * Copyright 2011 gitblit.com. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.gitblit; + +import java.awt.Dimension; +import java.awt.Image; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FilenameFilter; +import java.io.IOException; +import java.util.Iterator; + +import javax.imageio.ImageIO; +import javax.imageio.ImageReader; +import javax.imageio.stream.ImageInputStream; + +import com.beust.jcommander.JCommander; +import com.beust.jcommander.Parameter; +import com.beust.jcommander.ParameterException; +import com.beust.jcommander.Parameters; + +public class BuildThumbnails { + + public static void main(String[] args) { + Params params = new Params(); + JCommander jc = new JCommander(params); + try { + jc.parse(args); + } catch (ParameterException t) { + System.err.println(t.getMessage()); + jc.usage(); + } + createImageThumbnail(params.sourceFolder, params.destinationFolder, params.maximumDimension); + } + + public static void createImageThumbnail(String sourceFolder, String destinationFolder, + int maxDimension) { + if (maxDimension <= 0) + return; + File source = new File(sourceFolder); + File destination = new File(destinationFolder); + destination.mkdirs(); + File[] sourceFiles = source.listFiles(new FilenameFilter() { + @Override + public boolean accept(File dir, String name) { + return name.toLowerCase().endsWith(".png"); + } + }); + + for (File sourceFile : sourceFiles) { + File destinationFile = new File(destination, sourceFile.getName()); + try { + Dimension sz = getImageDimensions(sourceFile); + int w = 0; + int h = 0; + if (sz.width > maxDimension) { + // Scale to Width + w = maxDimension; + float f = maxDimension; + h = (int) ((f / sz.width) * sz.height); // normalize height + } else if (sz.height > maxDimension) { + // Scale to Height + h = maxDimension; + float f = maxDimension; + w = (int) ((f / sz.height) * sz.width); // normalize width + } else { + // No thumbnail + return; + } + System.out.println("Generating thumbnail for " + sourceFile.getName() + " as (" + w + + "," + h + ")"); + BufferedImage image = ImageIO.read(sourceFile); + Image scaledImage = image.getScaledInstance(w, h, BufferedImage.SCALE_SMOOTH); + BufferedImage destImage = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB); + destImage.createGraphics().drawImage(scaledImage, 0, 0, null); + FileOutputStream fos = new FileOutputStream(destinationFile); + ImageIO.write(destImage, "png", fos); + fos.flush(); + fos.getFD().sync(); + fos.close(); + } catch (Throwable t) { + t.printStackTrace(); + } + } + } + + public static Dimension getImageDimensions(File file) throws IOException { + ImageInputStream in = ImageIO.createImageInputStream(file); + try { + final Iterator readers = ImageIO.getImageReaders(in); + if (readers.hasNext()) { + ImageReader reader = readers.next(); + try { + reader.setInput(in); + return new Dimension(reader.getWidth(0), reader.getHeight(0)); + } finally { + reader.dispose(); + } + } + } finally { + if (in != null) + in.close(); + } + return null; + } + + @Parameters(separators = " ") + private static class Params { + + @Parameter(names = { "--sourceFolder" }, description = "Source folder for raw images", required = true) + public String sourceFolder; + + @Parameter(names = { "--destinationFolder" }, description = "Destination folder for thumbnails", required = true) + public String destinationFolder; + + @Parameter(names = { "--maximumDimension" }, description = "Maximum width or height for thumbnail", required = true) + public int maximumDimension; + + } +} diff --git a/src/com/gitblit/BuildWebXml.java b/src/com/gitblit/BuildWebXml.java index 557c6a85..8e957df5 100644 --- a/src/com/gitblit/BuildWebXml.java +++ b/src/com/gitblit/BuildWebXml.java @@ -24,6 +24,12 @@ import java.util.ArrayList; import java.util.List; import java.util.Vector; +import com.beust.jcommander.JCommander; +import com.beust.jcommander.Parameter; +import com.beust.jcommander.ParameterException; +import com.beust.jcommander.Parameters; +import com.gitblit.utils.StringUtils; + public class BuildWebXml { private static final String PARAMS = ""; @@ -34,9 +40,21 @@ public class BuildWebXml { private static final String PARAM_PATTERN = "\n\t\n\t\t{0}\n\t\t{1}\n\t\n"; public static void main(String[] args) throws Exception { + Params params = new Params(); + JCommander jc = new JCommander(params); + try { + jc.parse(args); + } catch (ParameterException t) { + System.err.println(t.getMessage()); + jc.usage(); + } + generateWebXml(params); + } + + private static void generateWebXml(Params params) throws Exception { // Read the current Gitblit properties BufferedReader propertiesReader = new BufferedReader(new FileReader(new File( - "distrib/gitblit.properties"))); + params.propertiesFile))); Vector settings = new Vector(); List comments = new ArrayList(); @@ -68,11 +86,11 @@ public class BuildWebXml { for (String comment : setting.comments) { parameters.append(MessageFormat.format(COMMENT_PATTERN, comment)); } - parameters.append(MessageFormat.format(PARAM_PATTERN, setting.name, setting.value)); + parameters.append(MessageFormat.format(PARAM_PATTERN, setting.name, StringUtils.escapeForHtml(setting.value, false))); } // Read the prototype web.xml file - File webxml = new File("src/WEB-INF/web.xml"); + File webxml = new File(params.sourceFile); char[] buffer = new char[(int) webxml.length()]; FileReader webxmlReader = new FileReader(webxml); webxmlReader.read(buffer); @@ -90,7 +108,7 @@ public class BuildWebXml { sb.append(webXmlContent.substring(idx + PARAMS.length())); // Save the merged web.xml to the war build folder - FileOutputStream os = new FileOutputStream(new File("war/WEB-INF/web.xml"), false); + FileOutputStream os = new FileOutputStream(new File(params.destinationFile), false); os.write(sb.toString().getBytes()); os.close(); } @@ -110,4 +128,18 @@ public class BuildWebXml { this.comments = new ArrayList(comments); } } + + @Parameters(separators = " ") + private static class Params { + + @Parameter(names = { "--sourceFile" }, description = "Source web.xml file", required = true) + public String sourceFile; + + @Parameter(names = { "--propertiesFile" }, description = "Properties settings file", required = true) + public String propertiesFile; + + @Parameter(names = { "--destinationFile" }, description = "Destination web.xml file", required = true) + public String destinationFile; + + } } diff --git a/src/com/gitblit/Constants.java b/src/com/gitblit/Constants.java index b874a7b0..d410d35f 100644 --- a/src/com/gitblit/Constants.java +++ b/src/com/gitblit/Constants.java @@ -36,9 +36,9 @@ public class Constants { public static final String GIT_PATH = "/git/"; public static final String ZIP_PATH = "/zip/"; - + public static final String SYNDICATION_PATH = "/feed/"; - + public static final String BORDER = "***********************************************************"; public static enum AccessRestrictionType { diff --git a/src/com/gitblit/FileLoginService.java b/src/com/gitblit/FileLoginService.java deleted file mode 100644 index e239efc4..00000000 --- a/src/com/gitblit/FileLoginService.java +++ /dev/null @@ -1,372 +0,0 @@ -/* - * Copyright 2011 gitblit.com. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.gitblit; - -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; -import java.text.MessageFormat; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Properties; -import java.util.Set; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.gitblit.models.UserModel; -import com.gitblit.utils.StringUtils; - -public class FileLoginService extends FileSettings implements ILoginService { - - private final Logger logger = LoggerFactory.getLogger(FileLoginService.class); - - public FileLoginService(File realmFile) { - super(realmFile.getAbsolutePath()); - } - - @Override - public UserModel authenticate(String username, char[] password) { - Properties allUsers = read(); - String userInfo = allUsers.getProperty(username); - if (StringUtils.isEmpty(userInfo)) { - return null; - } - UserModel returnedUser = null; - UserModel user = getUserModel(username); - if (user.password.startsWith(StringUtils.MD5_TYPE)) { - String md5 = StringUtils.MD5_TYPE + StringUtils.getMD5(new String(password)); - if (user.password.equalsIgnoreCase(md5)) { - returnedUser = user; - } - } else if (user.password.equals(new String(password))) { - returnedUser = user; - } - return returnedUser; - } - - @Override - public UserModel getUserModel(String username) { - Properties allUsers = read(); - String userInfo = allUsers.getProperty(username); - if (userInfo == null) { - return null; - } - UserModel model = new UserModel(username); - String[] userValues = userInfo.split(","); - model.password = userValues[0]; - for (int i = 1; i < userValues.length; i++) { - String role = userValues[i]; - switch (role.charAt(0)) { - case '#': - // Permissions - if (role.equalsIgnoreCase(Constants.ADMIN_ROLE)) { - model.canAdmin = true; - } - break; - default: - model.addRepository(role); - } - } - return model; - } - - @Override - public boolean updateUserModel(UserModel model) { - return updateUserModel(model.username, model); - } - - @Override - public boolean updateUserModel(String username, UserModel model) { - try { - Properties allUsers = read(); - ArrayList roles = new ArrayList(model.repositories); - - // Permissions - if (model.canAdmin) { - roles.add(Constants.ADMIN_ROLE); - } - - StringBuilder sb = new StringBuilder(); - sb.append(model.password); - sb.append(','); - for (String role : roles) { - sb.append(role); - sb.append(','); - } - // trim trailing comma - sb.setLength(sb.length() - 1); - allUsers.remove(username); - allUsers.put(model.username, sb.toString()); - - write(allUsers); - return true; - } catch (Throwable t) { - logger.error(MessageFormat.format("Failed to update user model {0}!", model.username), - t); - } - return false; - } - - @Override - public boolean deleteUserModel(UserModel model) { - return deleteUser(model.username); - } - - @Override - public boolean deleteUser(String username) { - try { - // Read realm file - Properties allUsers = read(); - allUsers.remove(username); - write(allUsers); - return true; - } catch (Throwable t) { - logger.error(MessageFormat.format("Failed to delete user {0}!", username), t); - } - return false; - } - - @Override - public List getAllUsernames() { - Properties allUsers = read(); - List list = new ArrayList(allUsers.stringPropertyNames()); - return list; - } - - @Override - public List getUsernamesForRole(String role) { - List list = new ArrayList(); - try { - Properties allUsers = read(); - for (String username : allUsers.stringPropertyNames()) { - String value = allUsers.getProperty(username); - String[] values = value.split(","); - // skip first value (password) - for (int i = 1; i < values.length; i++) { - String r = values[i]; - if (r.equalsIgnoreCase(role)) { - list.add(username); - break; - } - } - } - } catch (Throwable t) { - logger.error(MessageFormat.format("Failed to get usernames for role {0}!", role), t); - } - return list; - } - - @Override - public boolean setUsernamesForRole(String role, List usernames) { - try { - Set specifiedUsers = new HashSet(usernames); - Set needsAddRole = new HashSet(specifiedUsers); - Set needsRemoveRole = new HashSet(); - - // identify users which require add and remove role - Properties allUsers = read(); - for (String username : allUsers.stringPropertyNames()) { - String value = allUsers.getProperty(username); - String[] values = value.split(","); - // skip first value (password) - for (int i = 1; i < values.length; i++) { - String r = values[i]; - if (r.equalsIgnoreCase(role)) { - // user has role, check against revised user list - if (specifiedUsers.contains(username)) { - needsAddRole.remove(username); - } else { - // remove role from user - needsRemoveRole.add(username); - } - break; - } - } - } - - // add roles to users - for (String user : needsAddRole) { - String userValues = allUsers.getProperty(user); - userValues += "," + role; - allUsers.put(user, userValues); - } - - // remove role from user - for (String user : needsRemoveRole) { - String[] values = allUsers.getProperty(user).split(","); - String password = values[0]; - StringBuilder sb = new StringBuilder(); - sb.append(password); - sb.append(','); - List revisedRoles = new ArrayList(); - // skip first value (password) - for (int i = 1; i < values.length; i++) { - String value = values[i]; - if (!value.equalsIgnoreCase(role)) { - revisedRoles.add(value); - sb.append(value); - sb.append(','); - } - } - sb.setLength(sb.length() - 1); - - // update properties - allUsers.put(user, sb.toString()); - } - - // persist changes - write(allUsers); - return true; - } catch (Throwable t) { - logger.error(MessageFormat.format("Failed to set usernames for role {0}!", role), t); - } - return false; - } - - @Override - public boolean renameRole(String oldRole, String newRole) { - try { - Properties allUsers = read(); - Set needsRenameRole = new HashSet(); - - // identify users which require role rename - for (String username : allUsers.stringPropertyNames()) { - String value = allUsers.getProperty(username); - String[] roles = value.split(","); - // skip first value (password) - for (int i = 1; i < roles.length; i++) { - String r = roles[i]; - if (r.equalsIgnoreCase(oldRole)) { - needsRenameRole.remove(username); - break; - } - } - } - - // rename role for identified users - for (String user : needsRenameRole) { - String userValues = allUsers.getProperty(user); - String[] values = userValues.split(","); - String password = values[0]; - StringBuilder sb = new StringBuilder(); - sb.append(password); - sb.append(','); - List revisedRoles = new ArrayList(); - revisedRoles.add(newRole); - // skip first value (password) - for (int i = 1; i < values.length; i++) { - String value = values[i]; - if (!value.equalsIgnoreCase(oldRole)) { - revisedRoles.add(value); - sb.append(value); - sb.append(','); - } - } - sb.setLength(sb.length() - 1); - - // update properties - allUsers.put(user, sb.toString()); - } - - // persist changes - write(allUsers); - return true; - } catch (Throwable t) { - logger.error( - MessageFormat.format("Failed to rename role {0} to {1}!", oldRole, newRole), t); - } - return false; - } - - @Override - public boolean deleteRole(String role) { - try { - Properties allUsers = read(); - Set needsDeleteRole = new HashSet(); - - // identify users which require role rename - for (String username : allUsers.stringPropertyNames()) { - String value = allUsers.getProperty(username); - String[] roles = value.split(","); - // skip first value (password) - for (int i = 1; i < roles.length; i++) { - String r = roles[i]; - if (r.equalsIgnoreCase(role)) { - needsDeleteRole.remove(username); - break; - } - } - } - - // delete role for identified users - for (String user : needsDeleteRole) { - String userValues = allUsers.getProperty(user); - String[] values = userValues.split(","); - String password = values[0]; - StringBuilder sb = new StringBuilder(); - sb.append(password); - sb.append(','); - List revisedRoles = new ArrayList(); - // skip first value (password) - for (int i = 1; i < values.length; i++) { - String value = values[i]; - if (!value.equalsIgnoreCase(role)) { - revisedRoles.add(value); - sb.append(value); - sb.append(','); - } - } - sb.setLength(sb.length() - 1); - - // update properties - allUsers.put(user, sb.toString()); - } - - // persist changes - write(allUsers); - return true; - } catch (Throwable t) { - logger.error(MessageFormat.format("Failed to delete role {0}!", role), t); - } - return false; - } - - private void write(Properties properties) throws IOException { - // Update realm file - File realmFileCopy = new File(propertiesFile.getAbsolutePath() + ".tmp"); - FileWriter writer = new FileWriter(realmFileCopy); - properties - .store(writer, - "# Gitblit realm file format: username=password,\\#permission,repository1,repository2..."); - writer.close(); - if (realmFileCopy.exists() && realmFileCopy.length() > 0) { - if (propertiesFile.delete()) { - if (!realmFileCopy.renameTo(propertiesFile)) { - throw new IOException(MessageFormat.format("Failed to rename {0} to {1}!", - realmFileCopy.getAbsolutePath(), propertiesFile.getAbsolutePath())); - } - } else { - throw new IOException(MessageFormat.format("Failed to delete (0)!", - propertiesFile.getAbsolutePath())); - } - } else { - throw new IOException(MessageFormat.format("Failed to save {0}!", - realmFileCopy.getAbsolutePath())); - } - } -} diff --git a/src/com/gitblit/FileSettings.java b/src/com/gitblit/FileSettings.java index e213e80f..1e654222 100644 --- a/src/com/gitblit/FileSettings.java +++ b/src/com/gitblit/FileSettings.java @@ -45,7 +45,7 @@ public class FileSettings extends IStoredSettings { Properties props = new Properties(); is = new FileInputStream(propertiesFile); props.load(is); - + // load properties after we have successfully read file properties.clear(); properties.putAll(props); @@ -67,6 +67,10 @@ public class FileSettings extends IStoredSettings { return properties; } + protected long lastRead() { + return lastread; + } + @Override public String toString() { return propertiesFile.getAbsolutePath(); diff --git a/src/com/gitblit/FileUserService.java b/src/com/gitblit/FileUserService.java new file mode 100644 index 00000000..01a50be1 --- /dev/null +++ b/src/com/gitblit/FileUserService.java @@ -0,0 +1,423 @@ +/* + * Copyright 2011 gitblit.com. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.gitblit; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.gitblit.models.UserModel; +import com.gitblit.utils.StringUtils; + +public class FileUserService extends FileSettings implements IUserService { + + private final Logger logger = LoggerFactory.getLogger(FileUserService.class); + + private final Map cookies = new ConcurrentHashMap(); + + public FileUserService(File realmFile) { + super(realmFile.getAbsolutePath()); + } + + @Override + public boolean supportsCookies() { + return true; + } + + @Override + public char[] getCookie(UserModel model) { + Properties allUsers = super.read(); + String value = allUsers.getProperty(model.username); + String[] roles = value.split(","); + String password = roles[0]; + String cookie = StringUtils.getSHA1(model.username + password); + return cookie.toCharArray(); + } + + @Override + public UserModel authenticate(char[] cookie) { + String hash = new String(cookie); + if (StringUtils.isEmpty(hash)) { + return null; + } + read(); + UserModel model = null; + if (cookies.containsKey(hash)) { + String username = cookies.get(hash); + model = getUserModel(username); + } + return model; + } + + @Override + public UserModel authenticate(String username, char[] password) { + Properties allUsers = read(); + String userInfo = allUsers.getProperty(username); + if (StringUtils.isEmpty(userInfo)) { + return null; + } + UserModel returnedUser = null; + UserModel user = getUserModel(username); + if (user.password.startsWith(StringUtils.MD5_TYPE)) { + String md5 = StringUtils.MD5_TYPE + StringUtils.getMD5(new String(password)); + if (user.password.equalsIgnoreCase(md5)) { + returnedUser = user; + } + } else if (user.password.equals(new String(password))) { + returnedUser = user; + } + return returnedUser; + } + + @Override + public UserModel getUserModel(String username) { + Properties allUsers = read(); + String userInfo = allUsers.getProperty(username); + if (userInfo == null) { + return null; + } + UserModel model = new UserModel(username); + String[] userValues = userInfo.split(","); + model.password = userValues[0]; + for (int i = 1; i < userValues.length; i++) { + String role = userValues[i]; + switch (role.charAt(0)) { + case '#': + // Permissions + if (role.equalsIgnoreCase(Constants.ADMIN_ROLE)) { + model.canAdmin = true; + } + break; + default: + model.addRepository(role); + } + } + return model; + } + + @Override + public boolean updateUserModel(UserModel model) { + return updateUserModel(model.username, model); + } + + @Override + public boolean updateUserModel(String username, UserModel model) { + try { + Properties allUsers = read(); + ArrayList roles = new ArrayList(model.repositories); + + // Permissions + if (model.canAdmin) { + roles.add(Constants.ADMIN_ROLE); + } + + StringBuilder sb = new StringBuilder(); + sb.append(model.password); + sb.append(','); + for (String role : roles) { + sb.append(role); + sb.append(','); + } + // trim trailing comma + sb.setLength(sb.length() - 1); + allUsers.remove(username); + allUsers.put(model.username, sb.toString()); + + write(allUsers); + return true; + } catch (Throwable t) { + logger.error(MessageFormat.format("Failed to update user model {0}!", model.username), + t); + } + return false; + } + + @Override + public boolean deleteUserModel(UserModel model) { + return deleteUser(model.username); + } + + @Override + public boolean deleteUser(String username) { + try { + // Read realm file + Properties allUsers = read(); + allUsers.remove(username); + write(allUsers); + return true; + } catch (Throwable t) { + logger.error(MessageFormat.format("Failed to delete user {0}!", username), t); + } + return false; + } + + @Override + public List getAllUsernames() { + Properties allUsers = read(); + List list = new ArrayList(allUsers.stringPropertyNames()); + return list; + } + + @Override + public List getUsernamesForRepository(String role) { + List list = new ArrayList(); + try { + Properties allUsers = read(); + for (String username : allUsers.stringPropertyNames()) { + String value = allUsers.getProperty(username); + String[] values = value.split(","); + // skip first value (password) + for (int i = 1; i < values.length; i++) { + String r = values[i]; + if (r.equalsIgnoreCase(role)) { + list.add(username); + break; + } + } + } + } catch (Throwable t) { + logger.error(MessageFormat.format("Failed to get usernames for role {0}!", role), t); + } + return list; + } + + @Override + public boolean setUsernamesForRepository(String role, List usernames) { + try { + Set specifiedUsers = new HashSet(usernames); + Set needsAddRole = new HashSet(specifiedUsers); + Set needsRemoveRole = new HashSet(); + + // identify users which require add and remove role + Properties allUsers = read(); + for (String username : allUsers.stringPropertyNames()) { + String value = allUsers.getProperty(username); + String[] values = value.split(","); + // skip first value (password) + for (int i = 1; i < values.length; i++) { + String r = values[i]; + if (r.equalsIgnoreCase(role)) { + // user has role, check against revised user list + if (specifiedUsers.contains(username)) { + needsAddRole.remove(username); + } else { + // remove role from user + needsRemoveRole.add(username); + } + break; + } + } + } + + // add roles to users + for (String user : needsAddRole) { + String userValues = allUsers.getProperty(user); + userValues += "," + role; + allUsers.put(user, userValues); + } + + // remove role from user + for (String user : needsRemoveRole) { + String[] values = allUsers.getProperty(user).split(","); + String password = values[0]; + StringBuilder sb = new StringBuilder(); + sb.append(password); + sb.append(','); + List revisedRoles = new ArrayList(); + // skip first value (password) + for (int i = 1; i < values.length; i++) { + String value = values[i]; + if (!value.equalsIgnoreCase(role)) { + revisedRoles.add(value); + sb.append(value); + sb.append(','); + } + } + sb.setLength(sb.length() - 1); + + // update properties + allUsers.put(user, sb.toString()); + } + + // persist changes + write(allUsers); + return true; + } catch (Throwable t) { + logger.error(MessageFormat.format("Failed to set usernames for role {0}!", role), t); + } + return false; + } + + @Override + public boolean renameRepositoryRole(String oldRole, String newRole) { + try { + Properties allUsers = read(); + Set needsRenameRole = new HashSet(); + + // identify users which require role rename + for (String username : allUsers.stringPropertyNames()) { + String value = allUsers.getProperty(username); + String[] roles = value.split(","); + // skip first value (password) + for (int i = 1; i < roles.length; i++) { + String r = roles[i]; + if (r.equalsIgnoreCase(oldRole)) { + needsRenameRole.remove(username); + break; + } + } + } + + // rename role for identified users + for (String user : needsRenameRole) { + String userValues = allUsers.getProperty(user); + String[] values = userValues.split(","); + String password = values[0]; + StringBuilder sb = new StringBuilder(); + sb.append(password); + sb.append(','); + List revisedRoles = new ArrayList(); + revisedRoles.add(newRole); + // skip first value (password) + for (int i = 1; i < values.length; i++) { + String value = values[i]; + if (!value.equalsIgnoreCase(oldRole)) { + revisedRoles.add(value); + sb.append(value); + sb.append(','); + } + } + sb.setLength(sb.length() - 1); + + // update properties + allUsers.put(user, sb.toString()); + } + + // persist changes + write(allUsers); + return true; + } catch (Throwable t) { + logger.error( + MessageFormat.format("Failed to rename role {0} to {1}!", oldRole, newRole), t); + } + return false; + } + + @Override + public boolean deleteRepositoryRole(String role) { + try { + Properties allUsers = read(); + Set needsDeleteRole = new HashSet(); + + // identify users which require role rename + for (String username : allUsers.stringPropertyNames()) { + String value = allUsers.getProperty(username); + String[] roles = value.split(","); + // skip first value (password) + for (int i = 1; i < roles.length; i++) { + String r = roles[i]; + if (r.equalsIgnoreCase(role)) { + needsDeleteRole.remove(username); + break; + } + } + } + + // delete role for identified users + for (String user : needsDeleteRole) { + String userValues = allUsers.getProperty(user); + String[] values = userValues.split(","); + String password = values[0]; + StringBuilder sb = new StringBuilder(); + sb.append(password); + sb.append(','); + List revisedRoles = new ArrayList(); + // skip first value (password) + for (int i = 1; i < values.length; i++) { + String value = values[i]; + if (!value.equalsIgnoreCase(role)) { + revisedRoles.add(value); + sb.append(value); + sb.append(','); + } + } + sb.setLength(sb.length() - 1); + + // update properties + allUsers.put(user, sb.toString()); + } + + // persist changes + write(allUsers); + return true; + } catch (Throwable t) { + logger.error(MessageFormat.format("Failed to delete role {0}!", role), t); + } + return false; + } + + private void write(Properties properties) throws IOException { + // Update realm file + File realmFileCopy = new File(propertiesFile.getAbsolutePath() + ".tmp"); + FileWriter writer = new FileWriter(realmFileCopy); + properties + .store(writer, + "# Gitblit realm file format: username=password,\\#permission,repository1,repository2..."); + writer.close(); + if (realmFileCopy.exists() && realmFileCopy.length() > 0) { + if (propertiesFile.delete()) { + if (!realmFileCopy.renameTo(propertiesFile)) { + throw new IOException(MessageFormat.format("Failed to rename {0} to {1}!", + realmFileCopy.getAbsolutePath(), propertiesFile.getAbsolutePath())); + } + } else { + throw new IOException(MessageFormat.format("Failed to delete (0)!", + propertiesFile.getAbsolutePath())); + } + } else { + throw new IOException(MessageFormat.format("Failed to save {0}!", + realmFileCopy.getAbsolutePath())); + } + } + + @Override + protected synchronized Properties read() { + long lastRead = lastRead(); + Properties allUsers = super.read(); + if (lastRead != lastRead()) { + // reload hash cache + cookies.clear(); + for (String username : allUsers.stringPropertyNames()) { + String value = allUsers.getProperty(username); + String[] roles = value.split(","); + String password = roles[0]; + cookies.put(StringUtils.getSHA1(username + password), username); + } + } + return allUsers; + } +} diff --git a/src/com/gitblit/GitBlit.java b/src/com/gitblit/GitBlit.java index 1fa8b60f..9b661171 100644 --- a/src/com/gitblit/GitBlit.java +++ b/src/com/gitblit/GitBlit.java @@ -27,7 +27,9 @@ import java.util.Map.Entry; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; +import javax.servlet.http.Cookie; +import org.apache.wicket.protocol.http.WebResponse; import org.eclipse.jgit.errors.RepositoryNotFoundException; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.StoredConfig; @@ -55,7 +57,7 @@ public class GitBlit implements ServletContextListener { private boolean exportAll = true; - private ILoginService loginService; + private IUserService userService; private IStoredSettings storedSettings; @@ -105,44 +107,81 @@ public class GitBlit implements ServletContextListener { return cloneUrls; } - public void setLoginService(ILoginService loginService) { - logger.info("Setting up login service " + loginService.toString()); - this.loginService = loginService; + public void setUserService(IUserService userService) { + logger.info("Setting up user service " + userService.toString()); + this.userService = userService; } public UserModel authenticate(String username, char[] password) { - if (loginService == null) { + if (userService == null) { return null; } - return loginService.authenticate(username, password); + return userService.authenticate(username, password); + } + + public UserModel authenticate(Cookie[] cookies) { + if (userService == null) { + return null; + } + if (userService.supportsCookies()) { + if (cookies != null && cookies.length > 0) { + for (Cookie cookie : cookies) { + if (cookie.getName().equals(Constants.NAME)) { + String value = cookie.getValue(); + return userService.authenticate(value.toCharArray()); + } + } + } + } + return null; + } + + public void setCookie(WebResponse response, UserModel user) { + if (userService == null) { + return; + } + if (userService.supportsCookies()) { + Cookie userCookie; + if (user == null) { + // clear cookie for logout + userCookie = new Cookie(Constants.NAME, ""); + } else { + // set cookie for login + char[] cookie = userService.getCookie(user); + userCookie = new Cookie(Constants.NAME, new String(cookie)); + userCookie.setMaxAge(Integer.MAX_VALUE); + } + userCookie.setPath("/"); + response.addCookie(userCookie); + } } public List getAllUsernames() { - List names = new ArrayList(loginService.getAllUsernames()); + List names = new ArrayList(userService.getAllUsernames()); Collections.sort(names); return names; } public boolean deleteUser(String username) { - return loginService.deleteUser(username); + return userService.deleteUser(username); } public UserModel getUserModel(String username) { - UserModel user = loginService.getUserModel(username); + UserModel user = userService.getUserModel(username); return user; } public List getRepositoryUsers(RepositoryModel repository) { - return loginService.getUsernamesForRole(repository.name); + return userService.getUsernamesForRepository(repository.name); } public boolean setRepositoryUsers(RepositoryModel repository, List repositoryUsers) { - return loginService.setUsernamesForRole(repository.name, repositoryUsers); + return userService.setUsernamesForRepository(repository.name, repositoryUsers); } public void editUserModel(String username, UserModel user, boolean isCreate) throws GitBlitException { - if (!loginService.updateUserModel(username, user)) { + if (!userService.updateUserModel(username, user)) { throw new GitBlitException(isCreate ? "Failed to add user!" : "Failed to update user!"); } } @@ -181,6 +220,9 @@ public class GitBlit implements ServletContextListener { public RepositoryModel getRepositoryModel(UserModel user, String repositoryName) { RepositoryModel model = getRepositoryModel(repositoryName); + if (model == null) { + return null; + } if (model.accessRestriction.atLeast(AccessRestrictionType.VIEW)) { if (user != null && user.canAccessRepository(model.name)) { return model; @@ -261,7 +303,7 @@ public class GitBlit implements ServletContextListener { repository.name)); } // rename the roles - if (!loginService.renameRole(repositoryName, repository.name)) { + if (!userService.renameRepositoryRole(repositoryName, repository.name)) { throw new GitBlitException(MessageFormat.format( "Failed to rename repository permissions ''{0}'' to ''{1}''.", repositoryName, repository.name)); @@ -309,7 +351,7 @@ public class GitBlit implements ServletContextListener { File folder = new File(repositoriesFolder, repositoryName); if (folder.exists() && folder.isDirectory()) { FileUtils.delete(folder, FileUtils.RECURSIVE); - if (loginService.deleteRole(repositoryName)) { + if (userService.deleteRepositoryRole(repositoryName)) { return true; } } @@ -360,13 +402,13 @@ public class GitBlit implements ServletContextListener { repositoriesFolder = new File(settings.getString(Keys.git.repositoriesFolder, "git")); logger.info("Git repositories folder " + repositoriesFolder.getAbsolutePath()); repositoryResolver = new FileResolver(repositoriesFolder, exportAll); - String realm = settings.getString(Keys.realm.realmFile, "users.properties"); - ILoginService loginService = null; + String realm = settings.getString(Keys.realm.userService, "users.properties"); + IUserService loginService = null; try { // Check to see if this "file" is a login service class Class realmClass = Class.forName(realm); - if (ILoginService.class.isAssignableFrom(realmClass)) { - loginService = (ILoginService) realmClass.newInstance(); + if (IUserService.class.isAssignableFrom(realmClass)) { + loginService = (IUserService) realmClass.newInstance(); } } catch (Throwable t) { // Not a login service class OR other issue @@ -380,9 +422,9 @@ public class GitBlit implements ServletContextListener { MessageFormat.format("COULD NOT CREATE REALM FILE {0}!", realmFile), x); } } - loginService = new FileLoginService(realmFile); + loginService = new FileUserService(realmFile); } - setLoginService(loginService); + setUserService(loginService); } @Override diff --git a/src/com/gitblit/GitBlitServer.java b/src/com/gitblit/GitBlitServer.java index 80a46902..02cc54a0 100644 --- a/src/com/gitblit/GitBlitServer.java +++ b/src/com/gitblit/GitBlitServer.java @@ -111,7 +111,7 @@ public class GitBlitServer { * Start Server. */ private static void start(Params params) { - FileSettings settings = params.FILESETTINGS; + FileSettings settings = Params.FILESETTINGS; logger = LoggerFactory.getLogger(GitBlitServer.class); logger.info(Constants.BORDER); @@ -194,15 +194,15 @@ public class GitBlitServer { sessionManager.setSecureCookies(params.port <= 0 && params.securePort > 0); rootContext.getSessionHandler().setSessionManager(sessionManager); - // Ensure there is a defined Login Service - String realmUsers = params.realmFile; + // Ensure there is a defined User Service + String realmUsers = params.userService; if (StringUtils.isEmpty(realmUsers)) { - logger.error(MessageFormat.format("PLEASE SPECIFY {0}!!", Keys.realm.realmFile)); + logger.error(MessageFormat.format("PLEASE SPECIFY {0}!!", Keys.realm.userService)); return; } - + // Override settings - settings.overrideSetting(Keys.realm.realmFile, params.realmFile); + settings.overrideSetting(Keys.realm.userService, params.userService); settings.overrideSetting(Keys.git.repositoriesFolder, params.repositoriesFolder); // Set the server's contexts @@ -342,7 +342,7 @@ public class GitBlitServer { @Parameter(names = { "--stop" }, description = "Stop Server") public Boolean stop = false; - @Parameter(names = { "--tempFolder" }, description = "Server temp folder") + @Parameter(names = { "--tempFolder" }, description = "Folder for server to extract built-in webapp") public String temp = FILESETTINGS.getString(Keys.server.tempFolder, "temp"); /* @@ -355,8 +355,8 @@ public class GitBlitServer { /* * Authentication Parameters */ - @Parameter(names = { "--realmFile" }, description = "Users Realm Hash File") - public String realmFile = FILESETTINGS.getString(Keys.realm.realmFile, "users.properties"); + @Parameter(names = { "--userService" }, description = "Authentication and Authorization Service (filename or fully qualified classname)") + public String userService = FILESETTINGS.getString(Keys.realm.userService, "users.properties"); /* * JETTY Parameters diff --git a/src/com/gitblit/ILoginService.java b/src/com/gitblit/ILoginService.java deleted file mode 100644 index 0e706cf3..00000000 --- a/src/com/gitblit/ILoginService.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2011 gitblit.com. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.gitblit; - -import java.util.List; - -import com.gitblit.models.UserModel; - -public interface ILoginService { - - UserModel authenticate(String username, char[] password); - - UserModel getUserModel(String username); - - boolean updateUserModel(UserModel model); - - boolean updateUserModel(String username, UserModel model); - - boolean deleteUserModel(UserModel model); - - boolean deleteUser(String username); - - List getAllUsernames(); - - List getUsernamesForRole(String role); - - boolean setUsernamesForRole(String role, List usernames); - - boolean renameRole(String oldRole, String newRole); - - boolean deleteRole(String role); - - String toString(); -} diff --git a/src/com/gitblit/IStoredSettings.java b/src/com/gitblit/IStoredSettings.java index 6fcb437e..e220a81c 100644 --- a/src/com/gitblit/IStoredSettings.java +++ b/src/com/gitblit/IStoredSettings.java @@ -27,7 +27,7 @@ import com.gitblit.utils.StringUtils; public abstract class IStoredSettings { protected final Logger logger; - + protected final Properties overrides = new Properties(); public IStoredSettings(Class clazz) { @@ -35,7 +35,7 @@ public abstract class IStoredSettings { } protected abstract Properties read(); - + private Properties getSettings() { Properties props = read(); props.putAll(overrides); @@ -110,7 +110,7 @@ public abstract class IStoredSettings { } return strings; } - + public void overrideSetting(String key, String value) { overrides.put(key, value); } diff --git a/src/com/gitblit/IUserService.java b/src/com/gitblit/IUserService.java new file mode 100644 index 00000000..d0d0105a --- /dev/null +++ b/src/com/gitblit/IUserService.java @@ -0,0 +1,53 @@ +/* + * Copyright 2011 gitblit.com. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.gitblit; + +import java.util.List; + +import com.gitblit.models.UserModel; + +public interface IUserService { + + boolean supportsCookies(); + + char[] getCookie(UserModel model); + + UserModel authenticate(char[] cookie); + + UserModel authenticate(String username, char[] password); + + UserModel getUserModel(String username); + + boolean updateUserModel(UserModel model); + + boolean updateUserModel(String username, UserModel model); + + boolean deleteUserModel(UserModel model); + + boolean deleteUser(String username); + + List getAllUsernames(); + + List getUsernamesForRepository(String role); + + boolean setUsernamesForRepository(String role, List usernames); + + boolean renameRepositoryRole(String oldRole, String newRole); + + boolean deleteRepositoryRole(String role); + + String toString(); +} diff --git a/src/com/gitblit/SyndicationServlet.java b/src/com/gitblit/SyndicationServlet.java index 66dc467a..998949ad 100644 --- a/src/com/gitblit/SyndicationServlet.java +++ b/src/com/gitblit/SyndicationServlet.java @@ -62,7 +62,7 @@ public class SyndicationServlet extends HttpServlet { } return url.toString(); } - + public static String getTitle(String repository, String objectId) { String id = objectId; if (!StringUtils.isEmpty(id)) { diff --git a/src/com/gitblit/Thumbnailer.java b/src/com/gitblit/Thumbnailer.java deleted file mode 100644 index 5976f25e..00000000 --- a/src/com/gitblit/Thumbnailer.java +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright 2011 gitblit.com. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.gitblit; - -import java.awt.Dimension; -import java.awt.Image; -import java.awt.image.BufferedImage; -import java.io.File; -import java.io.FileOutputStream; -import java.io.FilenameFilter; -import java.io.IOException; -import java.util.Iterator; - -import javax.imageio.ImageIO; -import javax.imageio.ImageReader; -import javax.imageio.stream.ImageInputStream; - -import com.beust.jcommander.JCommander; -import com.beust.jcommander.Parameter; -import com.beust.jcommander.ParameterException; -import com.beust.jcommander.Parameters; - -public class Thumbnailer { - - public static void main(String[] args) { - Params params = new Params(); - JCommander jc = new JCommander(params); - try { - jc.parse(args); - } catch (ParameterException t) { - System.err.println(t.getMessage()); - jc.usage(); - } - createImageThumbnail(params.sourceFolder, params.destinationFolder, params.maximumDimension); - } - - public static void createImageThumbnail(String sourceFolder, String destinationFolder, - int maxDimension) { - if (maxDimension <= 0) - return; - File source = new File(sourceFolder); - File destination = new File(destinationFolder); - destination.mkdirs(); - File[] sourceFiles = source.listFiles(new FilenameFilter() { - @Override - public boolean accept(File dir, String name) { - return name.toLowerCase().endsWith(".png"); - } - }); - - for (File sourceFile : sourceFiles) { - File destinationFile = new File(destination, sourceFile.getName()); - try { - Dimension sz = getImageDimensions(sourceFile); - int w = 0; - int h = 0; - if (sz.width > maxDimension) { - // Scale to Width - w = maxDimension; - float f = maxDimension; - h = (int) ((f / sz.width) * sz.height); // normalize height - } else if (sz.height > maxDimension) { - // Scale to Height - h = maxDimension; - float f = maxDimension; - w = (int) ((f / sz.height) * sz.width); // normalize width - } else { - // No thumbnail - return; - } - System.out.println("Generating thumbnail for " + sourceFile.getName() + " as (" + w - + "," + h + ")"); - BufferedImage image = ImageIO.read(sourceFile); - Image scaledImage = image.getScaledInstance(w, h, BufferedImage.SCALE_SMOOTH); - BufferedImage destImage = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB); - destImage.createGraphics().drawImage(scaledImage, 0, 0, null); - FileOutputStream fos = new FileOutputStream(destinationFile); - ImageIO.write(destImage, "png", fos); - fos.flush(); - fos.getFD().sync(); - fos.close(); - } catch (Throwable t) { - t.printStackTrace(); - } - } - } - - public static Dimension getImageDimensions(File file) throws IOException { - ImageInputStream in = ImageIO.createImageInputStream(file); - try { - final Iterator readers = ImageIO.getImageReaders(in); - if (readers.hasNext()) { - ImageReader reader = readers.next(); - try { - reader.setInput(in); - return new Dimension(reader.getWidth(0), reader.getHeight(0)); - } finally { - reader.dispose(); - } - } - } finally { - if (in != null) - in.close(); - } - return null; - } - - @Parameters(separators = " ") - private static class Params { - - @Parameter(names = { "--sourceFolder" }, description = "Source folder for raw images", required = true) - public String sourceFolder; - - @Parameter(names = { "--destinationFolder" }, description = "Destination folder for thumbnails", required = true) - public String destinationFolder; - - @Parameter(names = { "--maxDimension" }, description = "Maximum width or height for thumbnail", required = true) - public int maximumDimension; - - } -} diff --git a/src/com/gitblit/WebXmlSettings.java b/src/com/gitblit/WebXmlSettings.java index 0ff2a3e7..dff5700e 100644 --- a/src/com/gitblit/WebXmlSettings.java +++ b/src/com/gitblit/WebXmlSettings.java @@ -20,20 +20,28 @@ import java.util.Properties; import javax.servlet.ServletContext; +import com.gitblit.utils.StringUtils; + public class WebXmlSettings extends IStoredSettings { private final Properties properties = new Properties(); - + public WebXmlSettings(ServletContext context) { super(WebXmlSettings.class); Enumeration keys = context.getInitParameterNames(); while (keys.hasMoreElements()) { String key = keys.nextElement().toString(); String value = context.getInitParameter(key); - properties.put(key, value); + properties.put(key, decodeValue(value)); + logger.debug(key + "=" + properties.getProperty(key)); } } + private String decodeValue(String value) { + // Decode escaped backslashes and HTML entities + return StringUtils.decodeFromHtml(value).replace("\\\\", "\\"); + } + @Override protected Properties read() { return properties; diff --git a/src/com/gitblit/models/UserModel.java b/src/com/gitblit/models/UserModel.java index 29647088..fd355fbb 100644 --- a/src/com/gitblit/models/UserModel.java +++ b/src/com/gitblit/models/UserModel.java @@ -43,7 +43,7 @@ public class UserModel implements Principal, Serializable { } @Override - public String getName() { + public String getName() { return username; } diff --git a/src/com/gitblit/utils/JGitUtils.java b/src/com/gitblit/utils/JGitUtils.java index 5656efb3..1c607ca7 100644 --- a/src/com/gitblit/utils/JGitUtils.java +++ b/src/com/gitblit/utils/JGitUtils.java @@ -402,12 +402,12 @@ public class JGitUtils { public static List getFilesInCommit(Repository r, RevCommit commit) { List list = new ArrayList(); - RevWalk rw = new RevWalk(r); + RevWalk rw = new RevWalk(r); try { if (commit == null) { ObjectId object = r.resolve(Constants.HEAD); commit = rw.parseCommit(object); - } + } if (commit.getParentCount() == 0) { TreeWalk tw = new TreeWalk(r); @@ -441,7 +441,7 @@ public class JGitUtils { } catch (Throwable t) { LOGGER.error("failed to determine files in commit!", t); } finally { - rw.dispose(); + rw.dispose(); } return list; } @@ -526,6 +526,9 @@ public class JGitUtils { public static List getRevLog(Repository r, String objectId, String path, int offset, int maxCount) { List list = new ArrayList(); + if (maxCount == 0) { + return list; + } if (!hasCommits(r)) { return list; } @@ -591,6 +594,9 @@ public class JGitUtils { final SearchType type, int offset, int maxCount) { final String lcValue = value.toLowerCase(); List list = new ArrayList(); + if (maxCount == 0) { + return list; + } if (!hasCommits(r)) { return list; } @@ -677,6 +683,9 @@ public class JGitUtils { private static List getRefs(Repository r, String refs, boolean fullName, int maxCount) { List list = new ArrayList(); + if (maxCount == 0) { + return list; + } try { Map map = r.getRefDatabase().getRefs(refs); RevWalk rw = new RevWalk(r); diff --git a/src/com/gitblit/utils/StringUtils.java b/src/com/gitblit/utils/StringUtils.java index b53b5e15..219699fc 100644 --- a/src/com/gitblit/utils/StringUtils.java +++ b/src/com/gitblit/utils/StringUtils.java @@ -58,6 +58,11 @@ public class StringUtils { return retStr.toString(); } + public static String decodeFromHtml(String inStr) { + return inStr.replace("&", "&").replace("<", "<").replace(">", ">") + .replace(""", "\"").replace(" ", " "); + } + public static String encodeURL(String inStr) { StringBuffer retStr = new StringBuffer(); int i = 0; @@ -165,7 +170,7 @@ public class StringUtils { } return sb.toString(); } - + public static String getRootPath(String path) { if (path.indexOf('/') > -1) { return path.substring(0, path.lastIndexOf('/')); diff --git a/src/com/gitblit/wicket/pages/BasePage.java b/src/com/gitblit/wicket/pages/BasePage.java index 5a0eb90f..06d54837 100644 --- a/src/com/gitblit/wicket/pages/BasePage.java +++ b/src/com/gitblit/wicket/pages/BasePage.java @@ -19,13 +19,17 @@ import java.util.LinkedHashMap; import java.util.Map; import java.util.TimeZone; +import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import org.apache.wicket.PageParameters; import org.apache.wicket.RestartResponseAtInterceptPageException; +import org.apache.wicket.RestartResponseException; import org.apache.wicket.markup.html.WebPage; import org.apache.wicket.markup.html.basic.Label; import org.apache.wicket.markup.html.panel.FeedbackPanel; +import org.apache.wicket.protocol.http.WebRequest; +import org.apache.wicket.protocol.http.WebResponse; import org.apache.wicket.protocol.http.servlet.ServletWebRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -34,6 +38,7 @@ import com.gitblit.Constants; import com.gitblit.Constants.AccessRestrictionType; import com.gitblit.GitBlit; import com.gitblit.Keys; +import com.gitblit.models.UserModel; import com.gitblit.wicket.GitBlitWebSession; import com.gitblit.wicket.WicketUtils; import com.gitblit.wicket.panels.LinkPanel; @@ -45,14 +50,40 @@ public abstract class BasePage extends WebPage { public BasePage() { super(); logger = LoggerFactory.getLogger(getClass()); + loginByCookie(); } public BasePage(PageParameters params) { super(params); logger = LoggerFactory.getLogger(getClass()); + loginByCookie(); + } + + private void loginByCookie() { + if (!GitBlit.getBoolean(Keys.web.allowCookieAuthentication, false)) { + return; + } + UserModel user = null; + + // Grab cookie from Browser Session + Cookie[] cookies = ((WebRequest) getRequestCycle().getRequest()).getCookies(); + if (cookies != null && cookies.length > 0) { + user = GitBlit.self().authenticate(cookies); + } + + // Login the user + if (user != null) { + // Set the user into the session + GitBlitWebSession.get().setUser(user); + + // Set Cookie + WebResponse response = (WebResponse) getRequestCycle().getResponse(); + GitBlit.self().setCookie(response, user); + } } protected void setupPage(String repositoryName, String pageName) { + if (repositoryName != null && repositoryName.trim().length() > 0) { add(new Label("title", getServerName() + " - " + repositoryName)); } else { @@ -122,7 +153,7 @@ public abstract class BasePage extends WebPage { HttpServletRequest req = servletWebRequest.getHttpServletRequest(); return req.getServerName(); } - + public void warn(String message, Throwable t) { logger.warn(message, t); } @@ -131,7 +162,7 @@ public abstract class BasePage extends WebPage { logger.error(message); if (redirect) { GitBlitWebSession.get().cacheErrorMessage(message); - throw new RestartResponseAtInterceptPageException(getApplication().getHomePage()); + throw new RestartResponseException(getApplication().getHomePage()); } else { super.error(message); } @@ -141,9 +172,18 @@ public abstract class BasePage extends WebPage { logger.error(message, t); if (redirect) { GitBlitWebSession.get().cacheErrorMessage(message); - throw new RestartResponseAtInterceptPageException(getApplication().getHomePage()); + throw new RestartResponseException(getApplication().getHomePage()); } else { super.error(message); } } + + public void authenticationError(String message) { + logger.error(message); + if (GitBlitWebSession.get().isLoggedIn()) { + error(message, true); + } else { + throw new RestartResponseAtInterceptPageException(LoginPage.class); + } + } } diff --git a/src/com/gitblit/wicket/pages/CommitPage.java b/src/com/gitblit/wicket/pages/CommitPage.java index a34917b6..3e3dcb8b 100644 --- a/src/com/gitblit/wicket/pages/CommitPage.java +++ b/src/com/gitblit/wicket/pages/CommitPage.java @@ -128,8 +128,8 @@ public class CommitPage extends RepositoryPage { SearchType.AUTHOR)); item.add(WicketUtils.createTimestampLabel("authorDate", entry.notesRef .getAuthorIdent().getWhen(), getTimeZone())); - item.add(new Label("noteContent", GitBlit.self().processCommitMessage(repositoryName, entry.content)) - .setEscapeModelStrings(false)); + item.add(new Label("noteContent", GitBlit.self().processCommitMessage( + repositoryName, entry.content)).setEscapeModelStrings(false)); } }; add(notesView.setVisible(notes.size() > 0)); diff --git a/src/com/gitblit/wicket/pages/DocsPage.java b/src/com/gitblit/wicket/pages/DocsPage.java index 2f899bbe..40518b5c 100644 --- a/src/com/gitblit/wicket/pages/DocsPage.java +++ b/src/com/gitblit/wicket/pages/DocsPage.java @@ -64,8 +64,8 @@ public class DocsPage extends RepositoryPage { .newPathParameter(repositoryName, entry.commitId, entry.path))); item.add(new BookmarkablePageLink("raw", RawPage.class, WicketUtils .newPathParameter(repositoryName, entry.commitId, entry.path))); - item.add(new BookmarkablePageLink("blame", BlamePage.class, - WicketUtils.newPathParameter(repositoryName, entry.commitId, entry.path))); + item.add(new BookmarkablePageLink("blame", BlamePage.class, WicketUtils + .newPathParameter(repositoryName, entry.commitId, entry.path))); item.add(new BookmarkablePageLink("history", HistoryPage.class, WicketUtils .newPathParameter(repositoryName, entry.commitId, entry.path))); WicketUtils.setAlternatingBackground(item, counter); diff --git a/src/com/gitblit/wicket/pages/EditUserPage.java b/src/com/gitblit/wicket/pages/EditUserPage.java index 63916276..8f68ac28 100644 --- a/src/com/gitblit/wicket/pages/EditUserPage.java +++ b/src/com/gitblit/wicket/pages/EditUserPage.java @@ -130,7 +130,8 @@ public class EditUserPage extends BasePage { String type = GitBlit.getString(Keys.realm.passwordStorage, "md5"); if (type.equalsIgnoreCase("md5")) { // store MD5 digest of password - userModel.password = StringUtils.MD5_TYPE + StringUtils.getMD5(userModel.password); + userModel.password = StringUtils.MD5_TYPE + + StringUtils.getMD5(userModel.password); } } diff --git a/src/com/gitblit/wicket/pages/LogPage.java b/src/com/gitblit/wicket/pages/LogPage.java index 2cd787c7..c012538f 100644 --- a/src/com/gitblit/wicket/pages/LogPage.java +++ b/src/com/gitblit/wicket/pages/LogPage.java @@ -27,7 +27,7 @@ public class LogPage extends RepositoryPage { super(params); addSyndicationDiscoveryLink(); - + int pageNumber = WicketUtils.getPage(params); int prevPage = Math.max(0, pageNumber - 1); int nextPage = pageNumber + 1; diff --git a/src/com/gitblit/wicket/pages/LoginPage.java b/src/com/gitblit/wicket/pages/LoginPage.java index 971ba327..6ee72db3 100644 --- a/src/com/gitblit/wicket/pages/LoginPage.java +++ b/src/com/gitblit/wicket/pages/LoginPage.java @@ -15,7 +15,10 @@ */ package com.gitblit.wicket.pages; +import javax.servlet.http.Cookie; + import org.apache.wicket.PageParameters; +import org.apache.wicket.RestartResponseException; import org.apache.wicket.markup.html.WebPage; import org.apache.wicket.markup.html.basic.Label; import org.apache.wicket.markup.html.form.PasswordTextField; @@ -24,6 +27,8 @@ import org.apache.wicket.markup.html.form.TextField; import org.apache.wicket.markup.html.panel.FeedbackPanel; import org.apache.wicket.model.IModel; import org.apache.wicket.model.Model; +import org.apache.wicket.protocol.http.WebRequest; +import org.apache.wicket.protocol.http.WebResponse; import com.gitblit.Constants; import com.gitblit.GitBlit; @@ -42,8 +47,11 @@ public class LoginPage extends WebPage { // If we are already logged in because user directly accessed // the login url, redirect to the home page if (GitBlitWebSession.get().isLoggedIn()) { - setRedirect(true); - setResponsePage(getApplication().getHomePage()); + throw new RestartResponseException(getApplication().getHomePage()); + } + + if (GitBlit.getBoolean(Keys.web.allowCookieAuthentication, false)) { + loginByCookie(); } add(new Label("title", GitBlit.getString(Keys.web.siteName, Constants.NAME))); @@ -72,11 +80,30 @@ public class LoginPage extends WebPage { add(loginForm); } + private void loginByCookie() { + UserModel user = null; + + // Grab cookie from Browser Session + Cookie[] cookies = ((WebRequest) getRequestCycle().getRequest()).getCookies(); + if (cookies != null && cookies.length > 0) { + user = GitBlit.self().authenticate(cookies); + } + + // Login the user + loginUser(user); + } + private void loginUser(UserModel user) { if (user != null) { // Set the user into the session GitBlitWebSession.get().setUser(user); + // Set Cookie + if (GitBlit.getBoolean(Keys.web.allowCookieAuthentication, false)) { + WebResponse response = (WebResponse) getRequestCycle().getResponse(); + GitBlit.self().setCookie(response, user); + } + if (!continueToOriginalDestination()) { // Redirect to home page setResponsePage(getApplication().getHomePage()); diff --git a/src/com/gitblit/wicket/pages/LogoutPage.java b/src/com/gitblit/wicket/pages/LogoutPage.java index 05beab3c..b049e8e0 100644 --- a/src/com/gitblit/wicket/pages/LogoutPage.java +++ b/src/com/gitblit/wicket/pages/LogoutPage.java @@ -16,11 +16,16 @@ package com.gitblit.wicket.pages; import org.apache.wicket.markup.html.WebPage; +import org.apache.wicket.protocol.http.WebResponse; + +import com.gitblit.GitBlit; +import com.gitblit.wicket.GitBlitWebSession; public class LogoutPage extends WebPage { public LogoutPage() { - getSession().invalidate(); + GitBlitWebSession.get().invalidate(); + GitBlit.self().setCookie(((WebResponse) getResponse()), null); setRedirect(true); setResponsePage(getApplication().getHomePage()); } diff --git a/src/com/gitblit/wicket/pages/RepositoryPage.java b/src/com/gitblit/wicket/pages/RepositoryPage.java index 00ed7554..22d3323a 100644 --- a/src/com/gitblit/wicket/pages/RepositoryPage.java +++ b/src/com/gitblit/wicket/pages/RepositoryPage.java @@ -15,6 +15,7 @@ */ package com.gitblit.wicket.pages; +import java.io.Serializable; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; @@ -205,7 +206,7 @@ public abstract class RepositoryPage extends BasePage { RepositoryModel model = GitBlit.self().getRepositoryModel( GitBlitWebSession.get().getUser(), repositoryName); if (model == null) { - error("Unauthorized access for repository " + repositoryName, true); + authenticationError("Unauthorized access for repository " + repositoryName); return null; } m = model; @@ -333,7 +334,9 @@ public abstract class RepositoryPage extends BasePage { return WicketUtils.newPathParameter(repositoryName, objectId, path); } - private static class PageRegistration { + private static class PageRegistration implements Serializable { + private static final long serialVersionUID = 1L; + final String translationKey; final Class pageClass; @@ -343,7 +346,7 @@ public abstract class RepositoryPage extends BasePage { } } - private static class SearchForm extends StatelessForm { + private static class SearchForm extends StatelessForm implements Serializable { private static final long serialVersionUID = 1L; private final String repositoryName; diff --git a/src/com/gitblit/wicket/pages/SummaryPage.html b/src/com/gitblit/wicket/pages/SummaryPage.html index bbf89798..35ad3477 100644 --- a/src/com/gitblit/wicket/pages/SummaryPage.html +++ b/src/com/gitblit/wicket/pages/SummaryPage.html @@ -32,15 +32,11 @@
[commits panel]
- -
-
[branches panel]
-
- -
-
[tags panel]
-
+
[tags panel]
+ + +
[branches panel]
diff --git a/src/com/gitblit/wicket/pages/SummaryPage.java b/src/com/gitblit/wicket/pages/SummaryPage.java index e31375c0..39b8a97e 100644 --- a/src/com/gitblit/wicket/pages/SummaryPage.java +++ b/src/com/gitblit/wicket/pages/SummaryPage.java @@ -58,18 +58,11 @@ public class SummaryPage extends RepositoryPage { public SummaryPage(PageParameters params) { super(params); - int numCommitsDef = 20; - int numRefsDef = 5; - - int numberCommits = GitBlit.getInteger(Keys.web.summaryCommitCount, numCommitsDef); + int numberCommits = GitBlit.getInteger(Keys.web.summaryCommitCount, 20); if (numberCommits <= 0) { - numberCommits = numCommitsDef; - } - - int numberRefs = GitBlit.getInteger(Keys.web.summaryRefsCount, numRefsDef); - if (numberRefs <= 0) { - numberRefs = numRefsDef; + numberCommits = 20; } + int numberRefs = GitBlit.getInteger(Keys.web.summaryRefsCount, 5); Repository r = getRepository(); List metrics = null; @@ -78,7 +71,7 @@ public class SummaryPage extends RepositoryPage { metrics = MetricUtils.getDateMetrics(r, null, true, null); metricsTotal = metrics.remove(0); } - + addSyndicationDiscoveryLink(); // repository description @@ -121,7 +114,7 @@ public class SummaryPage extends RepositoryPage { add(WicketUtils.newClearPixel("accessRestrictionIcon").setVisible(false)); } StringBuilder sb = new StringBuilder(); - sb.append(WicketUtils.getHostURL(getRequestCycle().getRequest())); + sb.append(WicketUtils.getHostURL(getRequestCycle().getRequest())); sb.append(Constants.GIT_PATH); sb.append(repositoryName); repositoryUrls.add(sb.toString()); diff --git a/src/com/gitblit/wicket/panels/BranchesPanel.html b/src/com/gitblit/wicket/panels/BranchesPanel.html index 7e87067a..c58f42ef 100644 --- a/src/com/gitblit/wicket/panels/BranchesPanel.html +++ b/src/com/gitblit/wicket/panels/BranchesPanel.html @@ -8,14 +8,15 @@ -
[branches header]
+
[branches header]
- + + diff --git a/src/com/gitblit/wicket/panels/BranchesPanel.java b/src/com/gitblit/wicket/panels/BranchesPanel.java index 302b48dd..8e58d673 100644 --- a/src/com/gitblit/wicket/panels/BranchesPanel.java +++ b/src/com/gitblit/wicket/panels/BranchesPanel.java @@ -27,18 +27,20 @@ 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.model.StringResourceModel; -import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.Repository; import com.gitblit.SyndicationServlet; import com.gitblit.models.RefModel; import com.gitblit.models.RepositoryModel; import com.gitblit.utils.JGitUtils; +import com.gitblit.utils.JGitUtils.SearchType; import com.gitblit.utils.StringUtils; import com.gitblit.wicket.WicketUtils; import com.gitblit.wicket.pages.BranchesPage; +import com.gitblit.wicket.pages.CommitPage; import com.gitblit.wicket.pages.LogPage; import com.gitblit.wicket.pages.MetricsPage; +import com.gitblit.wicket.pages.SearchPage; import com.gitblit.wicket.pages.SummaryPage; import com.gitblit.wicket.pages.TreePage; @@ -90,11 +92,24 @@ public class BranchesPanel extends BasePanel { entry.displayName, 28), LogPage.class, WicketUtils.newObjectParameter( model.name, entry.getName()))); - // only show branch type on the branches page - boolean remote = entry.getName().startsWith(Constants.R_REMOTES); - item.add(new Label("branchType", remote ? getString("gb.remote") - : getString("gb.local")).setVisible(maxCount <= 0)); - + String author = entry.getAuthorIdent().getName(); + LinkPanel authorLink = new LinkPanel("branchAuthor", "list", author, + SearchPage.class, WicketUtils.newSearchParameter(model.name, entry.getName(), + author, SearchType.AUTHOR)); + setPersonSearchTooltip(authorLink, author, SearchType.AUTHOR); + item.add(authorLink); + + // short message + String shortMessage = entry.getShortMessage(); + String trimmedMessage = StringUtils.trimShortLog(shortMessage); + LinkPanel shortlog = new LinkPanel("branchLog", "list subject", + trimmedMessage, CommitPage.class, WicketUtils.newObjectParameter( + model.name, entry.getName())); + if (!shortMessage.equals(trimmedMessage)) { + WicketUtils.setHtmlTooltip(shortlog, shortMessage); + } + item.add(shortlog); + if (maxCount <= 0) { Fragment fragment = new Fragment("branchLinks", "branchPageLinks", this); fragment.add(new BookmarkablePageLink("log", LogPage.class, WicketUtils @@ -103,8 +118,9 @@ public class BranchesPanel extends BasePanel { .newObjectParameter(model.name, entry.getName()))); fragment.add(new BookmarkablePageLink("metrics", MetricsPage.class, WicketUtils.newObjectParameter(model.name, entry.getName()))); - fragment.add(new ExternalLink("syndication", SyndicationServlet.asLink(getRequest() - .getRelativePathPrefixToContextRoot(), model.name, entry.getName(), 0))); + fragment.add(new ExternalLink("syndication", SyndicationServlet.asLink( + getRequest().getRelativePathPrefixToContextRoot(), model.name, + entry.getName(), 0))); item.add(fragment); } else { Fragment fragment = new Fragment("branchLinks", "branchPanelLinks", this); diff --git a/src/com/gitblit/wicket/panels/LogPanel.html b/src/com/gitblit/wicket/panels/LogPanel.html index 1ca92851..712a6628 100644 --- a/src/com/gitblit/wicket/panels/LogPanel.html +++ b/src/com/gitblit/wicket/panels/LogPanel.html @@ -8,8 +8,7 @@ -
[log header]
- +
[log header]
[branch date] [branch name][branch type][branch author][branch log]
diff --git a/src/com/gitblit/wicket/panels/SearchPanel.java b/src/com/gitblit/wicket/panels/SearchPanel.java index 759040e6..d118790c 100644 --- a/src/com/gitblit/wicket/panels/SearchPanel.java +++ b/src/com/gitblit/wicket/panels/SearchPanel.java @@ -74,7 +74,7 @@ public class SearchPanel extends BasePanel { // header add(new CommitHeaderPanel("commitHeader", repositoryName, commit)); - + add(new Label("searchString", value)); add(new Label("searchType", searchType.toString())); diff --git a/src/com/gitblit/wicket/panels/TagsPanel.html b/src/com/gitblit/wicket/panels/TagsPanel.html index 481d8e81..86eedd60 100644 --- a/src/com/gitblit/wicket/panels/TagsPanel.html +++ b/src/com/gitblit/wicket/panels/TagsPanel.html @@ -8,7 +8,7 @@ -
[tags header]
+
[tags header]
diff --git a/src/com/gitblit/wicket/panels/TagsPanel.java b/src/com/gitblit/wicket/panels/TagsPanel.java index 95bc8575..58cb4586 100644 --- a/src/com/gitblit/wicket/panels/TagsPanel.java +++ b/src/com/gitblit/wicket/panels/TagsPanel.java @@ -90,13 +90,10 @@ public class TagsPanel extends BasePanel { item.add(new LinkPanel("tagName", "list name", entry.displayName, linkClass, WicketUtils.newObjectParameter(repositoryName, entry .getReferencedObjectId().getName()))); - String message; - if (maxCount > 0) { - message = StringUtils.trimString(entry.getShortMessage(), 40); - } else { - // workaround for RevTag returning a lengthy shortlog. :( - message = StringUtils.trimShortLog(entry.getShortMessage()); - } + + // workaround for RevTag returning a lengthy shortlog. :( + String message = StringUtils.trimShortLog(entry.getShortMessage()); + if (linkClass.equals(BlobPage.class)) { // Blob Tag Object item.add(WicketUtils.newImage("tagIcon", "file_16x16.png")); diff --git a/src/org/eclipse/jgit/api/BlameCommand.java b/src/org/eclipse/jgit/api/BlameCommand.java new file mode 100644 index 00000000..400d94bc --- /dev/null +++ b/src/org/eclipse/jgit/api/BlameCommand.java @@ -0,0 +1,227 @@ +/* + * Copyright (C) 2011, GitHub Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.eclipse.jgit.api; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; + +import org.eclipse.jgit.api.errors.JGitInternalException; +import org.eclipse.jgit.blame.BlameGenerator; +import org.eclipse.jgit.blame.BlameResult; +import org.eclipse.jgit.diff.DiffAlgorithm; +import org.eclipse.jgit.diff.RawText; +import org.eclipse.jgit.diff.RawTextComparator; +import org.eclipse.jgit.dircache.DirCache; +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Repository; + +/** + * Blame command for building a {@link BlameResult} for a file path. + */ +public class BlameCommand extends GitCommand { + + private String path; + + private DiffAlgorithm diffAlgorithm; + + private RawTextComparator textComparator; + + private ObjectId startCommit; + + private Collection reverseEndCommits; + + private Boolean followFileRenames; + + /** + * @param repo + */ + public BlameCommand(Repository repo) { + super(repo); + } + + /** + * Set file path + * + * @param filePath + * @return this command + */ + public BlameCommand setFilePath(String filePath) { + this.path = filePath; + return this; + } + + /** + * Set diff algorithm + * + * @param diffAlgorithm + * @return this command + */ + public BlameCommand setDiffAlgorithm(DiffAlgorithm diffAlgorithm) { + this.diffAlgorithm = diffAlgorithm; + return this; + } + + /** + * Set raw text comparator + * + * @param textComparator + * @return this command + */ + public BlameCommand setTextComparator(RawTextComparator textComparator) { + this.textComparator = textComparator; + return this; + } + + /** + * Set start commit id + * + * @param commit + * @return this command + */ + public BlameCommand setStartCommit(AnyObjectId commit) { + this.startCommit = commit.toObjectId(); + return this; + } + + /** + * Enable (or disable) following file renames. + *

+ * If true renames are followed using the standard FollowFilter behavior + * used by RevWalk (which matches {@code git log --follow} in the C + * implementation). This is not the same as copy/move detection as + * implemented by the C implementation's of {@code git blame -M -C}. + * + * @param follow + * enable following. + * @return {@code this} + */ + public BlameCommand setFollowFileRenames(boolean follow) { + followFileRenames = Boolean.valueOf(follow); + return this; + } + + /** + * Configure the command to compute reverse blame (history of deletes). + * + * @param start + * oldest commit to traverse from. The result file will be loaded + * from this commit's tree. + * @param end + * most recent commit to stop traversal at. Usually an active + * branch tip, tag, or HEAD. + * @return {@code this} + * @throws IOException + * the repository cannot be read. + */ + public BlameCommand reverse(AnyObjectId start, AnyObjectId end) + throws IOException { + return reverse(start, Collections.singleton(end.toObjectId())); + } + + /** + * Configure the generator to compute reverse blame (history of deletes). + * + * @param start + * oldest commit to traverse from. The result file will be loaded + * from this commit's tree. + * @param end + * most recent commits to stop traversal at. Usually an active + * branch tip, tag, or HEAD. + * @return {@code this} + * @throws IOException + * the repository cannot be read. + */ + public BlameCommand reverse(AnyObjectId start, Collection end) + throws IOException { + startCommit = start.toObjectId(); + reverseEndCommits = new ArrayList(end); + return this; + } + + /** + * Generate a list of lines with information about when the lines were + * introduced into the file path. + * + * @return list of lines + */ + public BlameResult call() throws JGitInternalException { + checkCallable(); + BlameGenerator gen = new BlameGenerator(repo, path); + try { + if (diffAlgorithm != null) + gen.setDiffAlgorithm(diffAlgorithm); + if (textComparator != null) + gen.setTextComparator(textComparator); + if (followFileRenames != null) + gen.setFollowFileRenames(followFileRenames.booleanValue()); + + if (reverseEndCommits != null) + gen.reverse(startCommit, reverseEndCommits); + else if (startCommit != null) + gen.push(null, startCommit); + else { + gen.push(null, repo.resolve(Constants.HEAD)); + if (!repo.isBare()) { + DirCache dc = repo.readDirCache(); + int entry = dc.findEntry(path); + if (0 <= entry) + gen.push(null, dc.getEntry(entry).getObjectId()); + + File inTree = new File(repo.getWorkTree(), path); + if (inTree.isFile()) + gen.push(null, new RawText(inTree)); + } + } + return gen.computeBlameResult(); + } catch (IOException e) { + throw new JGitInternalException(e.getMessage(), e); + } finally { + gen.release(); + } + } +} diff --git a/src/org/eclipse/jgit/blame/BlameGenerator.java b/src/org/eclipse/jgit/blame/BlameGenerator.java new file mode 100644 index 00000000..286f4c1f --- /dev/null +++ b/src/org/eclipse/jgit/blame/BlameGenerator.java @@ -0,0 +1,961 @@ +/* + * Copyright (C) 2011, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.blame; + +import static org.eclipse.jgit.lib.Constants.OBJ_BLOB; + +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; + +import org.eclipse.jgit.blame.Candidate.BlobCandidate; +import org.eclipse.jgit.blame.Candidate.ReverseCandidate; +import org.eclipse.jgit.blame.ReverseWalk.ReverseCommit; +import org.eclipse.jgit.diff.DiffAlgorithm; +import org.eclipse.jgit.diff.DiffEntry; +import org.eclipse.jgit.diff.DiffEntry.ChangeType; +import org.eclipse.jgit.diff.EditList; +import org.eclipse.jgit.diff.HistogramDiff; +import org.eclipse.jgit.diff.RawText; +import org.eclipse.jgit.diff.RawTextComparator; +import org.eclipse.jgit.diff.RenameDetector; +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.MutableObjectId; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectLoader; +import org.eclipse.jgit.lib.ObjectReader; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevFlag; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.treewalk.TreeWalk; +import org.eclipse.jgit.treewalk.filter.PathFilter; +import org.eclipse.jgit.treewalk.filter.TreeFilter; + +/** + * Generate author information for lines based on introduction to the file. + *

+ * Applications that want a simple one-shot computation of blame for a file + * should use {@link #computeBlameResult()} to prepare the entire result in one + * method call. This may block for significant time as the history of the + * repository must be traversed until information is gathered for every line. + *

+ * Applications that want more incremental update behavior may use either the + * raw {@link #next()} streaming approach supported by this class, or construct + * a {@link BlameResult} using {@link BlameResult#create(BlameGenerator)} and + * incrementally construct the result with {@link BlameResult#computeNext()}. + *

+ * This class is not thread-safe. + *

+ * An instance of BlameGenerator can only be used once. To blame multiple files + * the application must create a new BlameGenerator. + *

+ * During blame processing there are two files involved: + *

    + *
  • result - The file whose lines are being examined. This is the revision + * the user is trying to view blame/annotation information alongside of.
  • + *
  • source - The file that was blamed with supplying one or more lines of + * data into result. The source may be a different file path (due to copy or + * rename). Source line numbers may differ from result line numbers due to lines + * being added/removed in intermediate revisions.
  • + *
+ *

+ * The blame algorithm is implemented by initially assigning responsibility for + * all lines of the result to the starting commit. A difference against the + * commit's ancestor is computed, and responsibility is passed to the ancestor + * commit for any lines that are common. The starting commit is blamed only for + * the lines that do not appear in the ancestor, if any. The loop repeats using + * the ancestor, until there are no more lines to acquire information on, or the + * file's creation point is discovered in history. + */ +public class BlameGenerator { + private final Repository repository; + + private final PathFilter resultPath; + + private final MutableObjectId idBuf; + + /** Revision pool used to acquire commits from. */ + private RevWalk revPool; + + /** Indicates the commit has already been processed. */ + private RevFlag SEEN; + + private ObjectReader reader; + + private TreeWalk treeWalk; + + private DiffAlgorithm diffAlgorithm = new HistogramDiff(); + + private RawTextComparator textComparator = RawTextComparator.DEFAULT; + + private RenameDetector renameDetector; + + /** Potential candidates, sorted by commit time descending. */ + private Candidate queue; + + /** Number of lines that still need to be discovered. */ + private int remaining; + + /** Blame is currently assigned to this source. */ + private Candidate currentSource; + + /** + * Create a blame generator for the repository and path + * + * @param repository + * repository to access revision data from. + * @param path + * initial path of the file to start scanning. + */ + public BlameGenerator(Repository repository, String path) { + this.repository = repository; + this.resultPath = PathFilter.create(path); + + idBuf = new MutableObjectId(); + setFollowFileRenames(true); + initRevPool(false); + + remaining = -1; + } + + private void initRevPool(boolean reverse) { + if (queue != null) + throw new IllegalStateException(); + + if (revPool != null) + revPool.release(); + + if (reverse) + revPool = new ReverseWalk(getRepository()); + else + revPool = new RevWalk(getRepository()); + + revPool.setRetainBody(true); + SEEN = revPool.newFlag("SEEN"); + reader = revPool.getObjectReader(); + treeWalk = new TreeWalk(reader); + } + + /** @return repository being scanned for revision history. */ + public Repository getRepository() { + return repository; + } + + /** @return path file path being processed. */ + public String getResultPath() { + return resultPath.getPath(); + } + + /** + * Difference algorithm to use when comparing revisions. + * + * @param algorithm + * @return {@code this} + */ + public BlameGenerator setDiffAlgorithm(DiffAlgorithm algorithm) { + diffAlgorithm = algorithm; + return this; + } + + /** + * Text comparator to use when comparing revisions. + * + * @param comparator + * @return {@code this} + */ + public BlameGenerator setTextComparator(RawTextComparator comparator) { + textComparator = comparator; + return this; + } + + /** + * Enable (or disable) following file renames, on by default. + *

+ * If true renames are followed using the standard FollowFilter behavior + * used by RevWalk (which matches {@code git log --follow} in the C + * implementation). This is not the same as copy/move detection as + * implemented by the C implementation's of {@code git blame -M -C}. + * + * @param follow + * enable following. + * @return {@code this} + */ + public BlameGenerator setFollowFileRenames(boolean follow) { + if (follow) + renameDetector = new RenameDetector(getRepository()); + else + renameDetector = null; + return this; + } + + /** + * Obtain the RenameDetector if {@code setFollowFileRenames(true)}. + * + * @return the rename detector, allowing the application to configure its + * settings for rename score and breaking behavior. + */ + public RenameDetector getRenameDetector() { + return renameDetector; + } + + /** + * Push a candidate blob onto the generator's traversal stack. + *

+ * Candidates should be pushed in history order from oldest-to-newest. + * Applications should push the starting commit first, then the index + * revision (if the index is interesting), and finally the working tree + * copy (if the working tree is interesting). + * + * @param description + * description of the blob revision, such as "Working Tree". + * @param contents + * contents of the file. + * @return {@code this} + * @throws IOException + * the repository cannot be read. + */ + public BlameGenerator push(String description, byte[] contents) + throws IOException { + return push(description, new RawText(contents)); + } + + /** + * Push a candidate blob onto the generator's traversal stack. + *

+ * Candidates should be pushed in history order from oldest-to-newest. + * Applications should push the starting commit first, then the index + * revision (if the index is interesting), and finally the working tree copy + * (if the working tree is interesting). + * + * @param description + * description of the blob revision, such as "Working Tree". + * @param contents + * contents of the file. + * @return {@code this} + * @throws IOException + * the repository cannot be read. + */ + public BlameGenerator push(String description, RawText contents) + throws IOException { + if (description == null) + // XXX description = JGitText.get().blameNotCommittedYet; + description = "blame not committed yet"; + BlobCandidate c = new BlobCandidate(description, resultPath); + c.sourceText = contents; + c.regionList = new Region(0, 0, contents.size()); + remaining = contents.size(); + push(c); + return this; + } + + /** + * Push a candidate object onto the generator's traversal stack. + *

+ * Candidates should be pushed in history order from oldest-to-newest. + * Applications should push the starting commit first, then the index + * revision (if the index is interesting), and finally the working tree copy + * (if the working tree is interesting). + * + * @param description + * description of the blob revision, such as "Working Tree". + * @param id + * may be a commit or a blob. + * @return {@code this} + * @throws IOException + * the repository cannot be read. + */ + public BlameGenerator push(String description, AnyObjectId id) + throws IOException { + ObjectLoader ldr = reader.open(id); + if (ldr.getType() == OBJ_BLOB) { + if (description == null) + // XXX description = JGitText.get().blameNotCommittedYet; + description = "blame not committed yet"; + BlobCandidate c = new BlobCandidate(description, resultPath); + c.sourceBlob = id.toObjectId(); + c.sourceText = new RawText(ldr.getCachedBytes(Integer.MAX_VALUE)); + c.regionList = new Region(0, 0, c.sourceText.size()); + remaining = c.sourceText.size(); + push(c); + return this; + } + + RevCommit commit = revPool.parseCommit(id); + if (!find(commit, resultPath)) + return this; + + Candidate c = new Candidate(commit, resultPath); + c.sourceBlob = idBuf.toObjectId(); + c.loadText(reader); + c.regionList = new Region(0, 0, c.sourceText.size()); + remaining = c.sourceText.size(); + push(c); + return this; + } + + /** + * Configure the generator to compute reverse blame (history of deletes). + *

+ * This method is expensive as it immediately runs a RevWalk over the + * history spanning the expression {@code start..end} (end being more recent + * than start) and then performs the equivalent operation as + * {@link #push(String, AnyObjectId)} to begin blame traversal from the + * commit named by {@code start} walking forwards through history until + * {@code end} blaming line deletions. + *

+ * A reverse blame may produce multiple sources for the same result line, + * each of these is a descendant commit that removed the line, typically + * this occurs when the same deletion appears in multiple side branches such + * as due to a cherry-pick. Applications relying on reverse should use + * {@link BlameResult} as it filters these duplicate sources and only + * remembers the first (oldest) deletion. + * + * @param start + * oldest commit to traverse from. The result file will be loaded + * from this commit's tree. + * @param end + * most recent commit to stop traversal at. Usually an active + * branch tip, tag, or HEAD. + * @return {@code this} + * @throws IOException + * the repository cannot be read. + */ + public BlameGenerator reverse(AnyObjectId start, AnyObjectId end) + throws IOException { + return reverse(start, Collections.singleton(end.toObjectId())); + } + + /** + * Configure the generator to compute reverse blame (history of deletes). + *

+ * This method is expensive as it immediately runs a RevWalk over the + * history spanning the expression {@code start..end} (end being more recent + * than start) and then performs the equivalent operation as + * {@link #push(String, AnyObjectId)} to begin blame traversal from the + * commit named by {@code start} walking forwards through history until + * {@code end} blaming line deletions. + *

+ * A reverse blame may produce multiple sources for the same result line, + * each of these is a descendant commit that removed the line, typically + * this occurs when the same deletion appears in multiple side branches such + * as due to a cherry-pick. Applications relying on reverse should use + * {@link BlameResult} as it filters these duplicate sources and only + * remembers the first (oldest) deletion. + * + * @param start + * oldest commit to traverse from. The result file will be loaded + * from this commit's tree. + * @param end + * most recent commits to stop traversal at. Usually an active + * branch tip, tag, or HEAD. + * @return {@code this} + * @throws IOException + * the repository cannot be read. + */ + public BlameGenerator reverse(AnyObjectId start, + Collection end) throws IOException { + initRevPool(true); + + ReverseCommit result = (ReverseCommit) revPool.parseCommit(start); + if (!find(result, resultPath)) + return this; + + revPool.markUninteresting(result); + for (ObjectId id : end) + revPool.markStart(revPool.parseCommit(id)); + + while (revPool.next() != null) { + // just pump the queue + } + + ReverseCandidate c = new ReverseCandidate(result, resultPath); + c.sourceBlob = idBuf.toObjectId(); + c.loadText(reader); + c.regionList = new Region(0, 0, c.sourceText.size()); + remaining = c.sourceText.size(); + push(c); + return this; + } + + /** + * Execute the generator in a blocking fashion until all data is ready. + * + * @return the complete result. Null if no file exists for the given path. + * @throws IOException + * the repository cannot be read. + */ + public BlameResult computeBlameResult() throws IOException { + try { + BlameResult r = BlameResult.create(this); + if (r != null) + r.computeAll(); + return r; + } finally { + release(); + } + } + + /** + * Step the blame algorithm one iteration. + * + * @return true if the generator has found a region's source. The getSource* + * and {@link #getResultStart()}, {@link #getResultEnd()} methods + * can be used to inspect the region found. False if there are no + * more regions to describe. + * @throws IOException + * repository cannot be read. + */ + public boolean next() throws IOException { + // If there is a source still pending, produce the next region. + if (currentSource != null) { + Region r = currentSource.regionList; + Region n = r.next; + remaining -= r.length; + if (n != null) { + currentSource.regionList = n; + return true; + } + + if (currentSource.queueNext != null) + return result(currentSource.queueNext); + + currentSource = null; + } + + // If there are no lines remaining, the entire result is done, + // even if there are revisions still available for the path. + if (remaining == 0) + return done(); + + for (;;) { + Candidate n = pop(); + if (n == null) + return done(); + + int pCnt = n.getParentCount(); + if (pCnt == 1) { + if (processOne(n)) + return true; + + } else if (1 < pCnt) { + if (processMerge(n)) + return true; + + } else if (n instanceof ReverseCandidate) { + // Do not generate a tip of a reverse. The region + // survives and should not appear to be deleted. + + } else /* if (pCnt == 0) */{ + // Root commit, with at least one surviving region. + // Assign the remaining blame here. + return result(n); + } + } + } + + private boolean done() { + release(); + return false; + } + + private boolean result(Candidate n) throws IOException { + if (n.sourceCommit != null) + revPool.parseBody(n.sourceCommit); + currentSource = n; + return true; + } + + private boolean reverseResult(Candidate parent, Candidate source) + throws IOException { + // On a reverse blame present the application the parent + // (as this is what did the removals), however the region + // list to enumerate is the source's surviving list. + Candidate res = parent.copy(parent.sourceCommit); + res.regionList = source.regionList; + return result(res); + } + + private Candidate pop() { + Candidate n = queue; + if (n != null) { + queue = n.queueNext; + n.queueNext = null; + } + return n; + } + + private void push(BlobCandidate toInsert) { + Candidate c = queue; + if (c != null) { + c.regionList = null; + toInsert.parent = c; + } + queue = toInsert; + } + + private void push(Candidate toInsert) { + // Mark sources to ensure they get discarded (above) if + // another path to the same commit. + toInsert.add(SEEN); + + // Insert into the queue using descending commit time, so + // the most recent commit will pop next. + int time = toInsert.getTime(); + Candidate n = queue; + if (n == null || time >= n.getTime()) { + toInsert.queueNext = n; + queue = toInsert; + return; + } + + for (Candidate p = n;; p = n) { + n = p.queueNext; + if (n == null || time >= n.getTime()) { + toInsert.queueNext = n; + p.queueNext = toInsert; + return; + } + } + } + + private boolean processOne(Candidate n) throws IOException { + RevCommit parent = n.getParent(0); + if (parent == null) + return split(n.getNextCandidate(0), n); + if (parent.has(SEEN)) + return false; + revPool.parseHeaders(parent); + + if (find(parent, n.sourcePath)) { + if (idBuf.equals(n.sourceBlob)) { + // The common case of the file not being modified in + // a simple string-of-pearls history. Blame parent. + n.sourceCommit = parent; + push(n); + return false; + } + + Candidate next = n.create(parent, n.sourcePath); + next.sourceBlob = idBuf.toObjectId(); + next.loadText(reader); + return split(next, n); + } + + if (n.sourceCommit == null) + return result(n); + + DiffEntry r = findRename(parent, n.sourceCommit, n.sourcePath); + if (r == null) + return result(n); + + if (0 == r.getOldId().prefixCompare(n.sourceBlob)) { + // A 100% rename without any content change can also + // skip directly to the parent. + n.sourceCommit = parent; + n.sourcePath = PathFilter.create(r.getOldPath()); + push(n); + return false; + } + + Candidate next = n.create(parent, PathFilter.create(r.getOldPath())); + next.sourceBlob = r.getOldId().toObjectId(); + next.renameScore = r.getScore(); + next.loadText(reader); + return split(next, n); + } + + private boolean split(Candidate parent, Candidate source) + throws IOException { + EditList editList = diffAlgorithm.diff(textComparator, + parent.sourceText, source.sourceText); + if (editList.isEmpty()) { + // Ignoring whitespace (or some other special comparator) can + // cause non-identical blobs to have an empty edit list. In + // a case like this push the parent alone. + parent.regionList = source.regionList; + push(parent); + return false; + } + + parent.takeBlame(editList, source); + if (parent.regionList != null) + push(parent); + if (source.regionList != null) { + if (source instanceof ReverseCandidate) + return reverseResult(parent, source); + return result(source); + } + return false; + } + + private boolean processMerge(Candidate n) throws IOException { + int pCnt = n.getParentCount(); + + for (int pIdx = 0; pIdx < pCnt; pIdx++) { + RevCommit parent = n.getParent(pIdx); + if (parent.has(SEEN)) + continue; + revPool.parseHeaders(parent); + } + + // If any single parent exactly matches the merge, follow only + // that one parent through history. + ObjectId[] ids = null; + for (int pIdx = 0; pIdx < pCnt; pIdx++) { + RevCommit parent = n.getParent(pIdx); + if (parent.has(SEEN)) + continue; + if (!find(parent, n.sourcePath)) + continue; + if (!(n instanceof ReverseCandidate) && idBuf.equals(n.sourceBlob)) { + n.sourceCommit = parent; + push(n); + return false; + } + if (ids == null) + ids = new ObjectId[pCnt]; + ids[pIdx] = idBuf.toObjectId(); + } + + // If rename detection is enabled, search for any relevant names. + DiffEntry[] renames = null; + if (renameDetector != null) { + renames = new DiffEntry[pCnt]; + for (int pIdx = 0; pIdx < pCnt; pIdx++) { + RevCommit parent = n.getParent(pIdx); + if (parent.has(SEEN)) + continue; + if (ids != null && ids[pIdx] != null) + continue; + + DiffEntry r = findRename(parent, n.sourceCommit, n.sourcePath); + if (r == null) + continue; + + if (n instanceof ReverseCandidate) { + if (ids == null) + ids = new ObjectId[pCnt]; + ids[pCnt] = r.getOldId().toObjectId(); + } else if (0 == r.getOldId().prefixCompare(n.sourceBlob)) { + // A 100% rename without any content change can also + // skip directly to the parent. Note this bypasses an + // earlier parent that had the path (above) but did not + // have an exact content match. For performance reasons + // we choose to follow the one parent over trying to do + // possibly both parents. + n.sourceCommit = parent; + n.sourcePath = PathFilter.create(r.getOldPath()); + push(n); + return false; + } + + renames[pIdx] = r; + } + } + + // Construct the candidate for each parent. + Candidate[] parents = new Candidate[pCnt]; + for (int pIdx = 0; pIdx < pCnt; pIdx++) { + RevCommit parent = n.getParent(pIdx); + if (parent.has(SEEN)) + continue; + + Candidate p; + if (renames != null && renames[pIdx] != null) { + p = n.create(parent, + PathFilter.create(renames[pIdx].getOldPath())); + p.renameScore = renames[pIdx].getScore(); + p.sourceBlob = renames[pIdx].getOldId().toObjectId(); + } else if (ids != null && ids[pIdx] != null) { + p = n.create(parent, n.sourcePath); + p.sourceBlob = ids[pIdx]; + } else { + continue; + } + + EditList editList; + if (n instanceof ReverseCandidate + && p.sourceBlob.equals(n.sourceBlob)) { + // This special case happens on ReverseCandidate forks. + p.sourceText = n.sourceText; + editList = new EditList(0); + } else { + p.loadText(reader); + editList = diffAlgorithm.diff(textComparator, + p.sourceText, n.sourceText); + } + + if (editList.isEmpty()) { + // Ignoring whitespace (or some other special comparator) can + // cause non-identical blobs to have an empty edit list. In + // a case like this push the parent alone. + if (n instanceof ReverseCandidate) { + parents[pIdx] = p; + continue; + } + + p.regionList = n.regionList; + push(p); + return false; + } + + p.takeBlame(editList, n); + + // Only remember this parent candidate if there is at least + // one region that was blamed on the parent. + if (p.regionList != null) { + // Reverse blame requires inverting the regions. This puts + // the regions the parent deleted from us into the parent, + // and retains the common regions to look at other parents + // for deletions. + if (n instanceof ReverseCandidate) { + Region r = p.regionList; + p.regionList = n.regionList; + n.regionList = r; + } + + parents[pIdx] = p; + } + } + + if (n instanceof ReverseCandidate) { + // On a reverse blame report all deletions found in the children, + // and pass on to them a copy of our region list. + Candidate resultHead = null; + Candidate resultTail = null; + + for (int pIdx = 0; pIdx < pCnt; pIdx++) { + Candidate p = parents[pIdx]; + if (p == null) + continue; + + if (p.regionList != null) { + Candidate r = p.copy(p.sourceCommit); + if (resultTail != null) { + resultTail.queueNext = r; + resultTail = r; + } else { + resultHead = r; + resultTail = r; + } + } + + if (n.regionList != null) { + p.regionList = n.regionList.deepCopy(); + push(p); + } + } + + if (resultHead != null) + return result(resultHead); + return false; + } + + // Push any parents that are still candidates. + for (int pIdx = 0; pIdx < pCnt; pIdx++) { + if (parents[pIdx] != null) + push(parents[pIdx]); + } + + if (n.regionList != null) + return result(n); + return false; + } + + /** + * Get the revision blamed for the current region. + *

+ * The source commit may be null if the line was blamed to an uncommitted + * revision, such as the working tree copy, or during a reverse blame if the + * line survives to the end revision (e.g. the branch tip). + * + * @return current revision being blamed. + */ + public RevCommit getSourceCommit() { + return currentSource.sourceCommit; + } + + /** @return current author being blamed. */ + public PersonIdent getSourceAuthor() { + return currentSource.getAuthor(); + } + + /** @return current committer being blamed. */ + public PersonIdent getSourceCommitter() { + RevCommit c = getSourceCommit(); + return c != null ? c.getCommitterIdent() : null; + } + + /** @return path of the file being blamed. */ + public String getSourcePath() { + return currentSource.sourcePath.getPath(); + } + + /** @return rename score if a rename occurred in {@link #getSourceCommit}. */ + public int getRenameScore() { + return currentSource.renameScore; + } + + /** + * @return first line of the source data that has been blamed for the + * current region. This is line number of where the region was added + * during {@link #getSourceCommit()} in file + * {@link #getSourcePath()}. + */ + public int getSourceStart() { + return currentSource.regionList.sourceStart; + } + + /** + * @return one past the range of the source data that has been blamed for + * the current region. This is line number of where the region was + * added during {@link #getSourceCommit()} in file + * {@link #getSourcePath()}. + */ + public int getSourceEnd() { + Region r = currentSource.regionList; + return r.sourceStart + r.length; + } + + /** + * @return first line of the result that {@link #getSourceCommit()} has been + * blamed for providing. Line numbers use 0 based indexing. + */ + public int getResultStart() { + return currentSource.regionList.resultStart; + } + + /** + * @return one past the range of the result that {@link #getSourceCommit()} + * has been blamed for providing. Line numbers use 0 based indexing. + * Because a source cannot be blamed for an empty region of the + * result, {@link #getResultEnd()} is always at least one larger + * than {@link #getResultStart()}. + */ + public int getResultEnd() { + Region r = currentSource.regionList; + return r.resultStart + r.length; + } + + /** + * @return number of lines in the current region being blamed to + * {@link #getSourceCommit()}. This is always the value of the + * expression {@code getResultEnd() - getResultStart()}, but also + * {@code getSourceEnd() - getSourceStart()}. + */ + public int getRegionLength() { + return currentSource.regionList.length; + } + + /** + * @return complete contents of the source file blamed for the current + * output region. This is the contents of {@link #getSourcePath()} + * within {@link #getSourceCommit()}. The source contents is + * temporarily available as an artifact of the blame algorithm. Most + * applications will want the result contents for display to users. + */ + public RawText getSourceContents() { + return currentSource.sourceText; + } + + /** + * @return complete file contents of the result file blame is annotating. + * This value is accessible only after being configured and only + * immediately before the first call to {@link #next()}. Returns + * null if the path does not exist. + * @throws IOException + * repository cannot be read. + * @throws IllegalStateException + * {@link #next()} has already been invoked. + */ + public RawText getResultContents() throws IOException { + return queue != null ? queue.sourceText : null; + } + + /** Release the current blame session. */ + public void release() { + revPool.release(); + queue = null; + currentSource = null; + } + + private boolean find(RevCommit commit, PathFilter path) throws IOException { + treeWalk.setFilter(path); + treeWalk.reset(commit.getTree()); + while (treeWalk.next()) { + if (path.isDone(treeWalk)) { + if (treeWalk.getFileMode(0).getObjectType() != OBJ_BLOB) + return false; + treeWalk.getObjectId(idBuf, 0); + return true; + } + + if (treeWalk.isSubtree()) + treeWalk.enterSubtree(); + } + return false; + } + + private DiffEntry findRename(RevCommit parent, RevCommit commit, + PathFilter path) throws IOException { + if (renameDetector == null) + return null; + + treeWalk.setFilter(TreeFilter.ANY_DIFF); + treeWalk.reset(parent.getTree(), commit.getTree()); + renameDetector.addAll(DiffEntry.scan(treeWalk)); + for (DiffEntry ent : renameDetector.compute()) { + if (isRename(ent) && ent.getNewPath().equals(path.getPath())) + return ent; + } + return null; + } + + private static boolean isRename(DiffEntry ent) { + return ent.getChangeType() == ChangeType.RENAME + || ent.getChangeType() == ChangeType.COPY; + } +} diff --git a/src/org/eclipse/jgit/blame/BlameResult.java b/src/org/eclipse/jgit/blame/BlameResult.java new file mode 100644 index 00000000..d7a958fe --- /dev/null +++ b/src/org/eclipse/jgit/blame/BlameResult.java @@ -0,0 +1,356 @@ +/* + * Copyright (C) 2011, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.blame; + +import java.io.IOException; + +import org.eclipse.jgit.diff.RawText; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.revwalk.RevCommit; + +/** + * Collects line annotations for inspection by applications. + *

+ * A result is usually updated incrementally as the BlameGenerator digs back + * further through history. Applications that want to lay annotations down text + * to the original source file in a viewer may find the BlameResult structure an + * easy way to acquire the information, at the expense of keeping tables in + * memory tracking every line of the result file. + *

+ * This class is not thread-safe. + *

+ * During blame processing there are two files involved: + *

    + *
  • result - The file whose lines are being examined. This is the revision + * the user is trying to view blame/annotation information alongside of.
  • + *
  • source - The file that was blamed with supplying one or more lines of + * data into result. The source may be a different file path (due to copy or + * rename). Source line numbers may differ from result line numbers due to lines + * being added/removed in intermediate revisions.
  • + *
+ */ +public class BlameResult { + /** + * Construct a new BlameResult for a generator. + * + * @param gen + * the generator the result will consume records from. + * @return the new result object. null if the generator cannot find the path + * it starts from. + * @throws IOException + * the repository cannot be read. + */ + public static BlameResult create(BlameGenerator gen) throws IOException { + String path = gen.getResultPath(); + RawText contents = gen.getResultContents(); + if (contents == null) { + gen.release(); + return null; + } + return new BlameResult(gen, path, contents); + } + + private final String resultPath; + + private final RevCommit[] sourceCommits; + + private final PersonIdent[] sourceAuthors; + + private final PersonIdent[] sourceCommitters; + + private final String[] sourcePaths; + + /** Warning: these are actually 1-based. */ + private final int[] sourceLines; + + private RawText resultContents; + + private BlameGenerator generator; + + private int lastLength; + + BlameResult(BlameGenerator bg, String path, RawText text) { + generator = bg; + resultPath = path; + resultContents = text; + + int cnt = text.size(); + sourceCommits = new RevCommit[cnt]; + sourceAuthors = new PersonIdent[cnt]; + sourceCommitters = new PersonIdent[cnt]; + sourceLines = new int[cnt]; + sourcePaths = new String[cnt]; + } + + /** @return path of the file this result annotates. */ + public String getResultPath() { + return resultPath; + } + + /** @return contents of the result file, available for display. */ + public RawText getResultContents() { + return resultContents; + } + + /** Throw away the {@link #getResultContents()}. */ + public void discardResultContents() { + resultContents = null; + } + + /** + * Check if the given result line has been annotated yet. + * + * @param idx + * line to read data of, 0 based. + * @return true if the data has been annotated, false otherwise. + */ + public boolean hasSourceData(int idx) { + return sourceLines[idx] != 0; + } + + /** + * Check if the given result line has been annotated yet. + * + * @param start + * first index to examine. + * @param end + * last index to examine. + * @return true if the data has been annotated, false otherwise. + */ + public boolean hasSourceData(int start, int end) { + for (; start < end; start++) + if (sourceLines[start] == 0) + return false; + return true; + } + + /** + * Get the commit that provided the specified line of the result. + *

+ * The source commit may be null if the line was blamed to an uncommitted + * revision, such as the working tree copy, or during a reverse blame if the + * line survives to the end revision (e.g. the branch tip). + * + * @param idx + * line to read data of, 0 based. + * @return commit that provided line {@code idx}. May be null. + */ + public RevCommit getSourceCommit(int idx) { + return sourceCommits[idx]; + } + + /** + * Get the author that provided the specified line of the result. + * + * @param idx + * line to read data of, 0 based. + * @return author that provided line {@code idx}. May be null. + */ + public PersonIdent getSourceAuthor(int idx) { + return sourceAuthors[idx]; + } + + /** + * Get the committer that provided the specified line of the result. + * + * @param idx + * line to read data of, 0 based. + * @return committer that provided line {@code idx}. May be null. + */ + public PersonIdent getSourceCommitter(int idx) { + return sourceCommitters[idx]; + } + + /** + * Get the file path that provided the specified line of the result. + * + * @param idx + * line to read data of, 0 based. + * @return source file path that provided line {@code idx}. + */ + public String getSourcePath(int idx) { + return sourcePaths[idx]; + } + + /** + * Get the corresponding line number in the source file. + * + * @param idx + * line to read data of, 0 based. + * @return matching line number in the source file. + */ + public int getSourceLine(int idx) { + return sourceLines[idx] - 1; + } + + /** + * Compute all pending information. + * + * @throws IOException + * the repository cannot be read. + */ + public void computeAll() throws IOException { + BlameGenerator gen = generator; + if (gen == null) + return; + + try { + while (gen.next()) + loadFrom(gen); + } finally { + gen.release(); + generator = null; + } + } + + /** + * Compute the next available segment and return the first index. + *

+ * Computes one segment and returns to the caller the first index that is + * available. After return the caller can also inspect {@link #lastLength()} + * to determine how many lines of the result were computed. + * + * @return index that is now available. -1 if no more are available. + * @throws IOException + * the repository cannot be read. + */ + public int computeNext() throws IOException { + BlameGenerator gen = generator; + if (gen == null) + return -1; + + if (gen.next()) { + loadFrom(gen); + lastLength = gen.getRegionLength(); + return gen.getResultStart(); + } else { + gen.release(); + generator = null; + return -1; + } + } + + /** @return length of the last segment found by {@link #computeNext()}. */ + public int lastLength() { + return lastLength; + } + + /** + * Compute until the entire range has been populated. + * + * @param start + * first index to examine. + * @param end + * last index to examine. + * @throws IOException + * the repository cannot be read. + */ + public void computeRange(int start, int end) throws IOException { + BlameGenerator gen = generator; + if (gen == null) + return; + + while (start < end) { + if (hasSourceData(start, end)) + return; + + if (!gen.next()) { + gen.release(); + generator = null; + return; + } + + loadFrom(gen); + + // If the result contains either end of our current range bounds, + // update the bounds to avoid scanning that section during the + // next loop iteration. + + int resLine = gen.getResultStart(); + int resEnd = gen.getResultEnd(); + + if (resLine <= start && start < resEnd) + start = resEnd; + + if (resLine <= end && end < resEnd) + end = resLine; + } + } + + @Override + public String toString() { + StringBuilder r = new StringBuilder(); + r.append("BlameResult: "); + r.append(getResultPath()); + return r.toString(); + } + + private void loadFrom(BlameGenerator gen) { + RevCommit srcCommit = gen.getSourceCommit(); + PersonIdent srcAuthor = gen.getSourceAuthor(); + PersonIdent srcCommitter = gen.getSourceCommitter(); + String srcPath = gen.getSourcePath(); + int srcLine = gen.getSourceStart(); + int resLine = gen.getResultStart(); + int resEnd = gen.getResultEnd(); + + for (; resLine < resEnd; resLine++) { + // Reverse blame can generate multiple results for the same line. + // Favor the first one selected, as this is the oldest and most + // likely to be nearest to the inquiry made by the user. + if (sourceLines[resLine] != 0) + continue; + + sourceCommits[resLine] = srcCommit; + sourceAuthors[resLine] = srcAuthor; + sourceCommitters[resLine] = srcCommitter; + sourcePaths[resLine] = srcPath; + + // Since sourceLines is 1-based to permit hasSourceData to use 0 to + // mean the line has not been annotated yet, pre-increment instead + // of the traditional post-increment when making the assignment. + sourceLines[resLine] = ++srcLine; + } + } +} diff --git a/src/org/eclipse/jgit/blame/Candidate.java b/src/org/eclipse/jgit/blame/Candidate.java new file mode 100644 index 00000000..5f20ce95 --- /dev/null +++ b/src/org/eclipse/jgit/blame/Candidate.java @@ -0,0 +1,386 @@ +/* + * Copyright (C) 2011, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.blame; + +import java.io.IOException; + +import org.eclipse.jgit.blame.ReverseWalk.ReverseCommit; +import org.eclipse.jgit.diff.Edit; +import org.eclipse.jgit.diff.EditList; +import org.eclipse.jgit.diff.RawText; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectLoader; +import org.eclipse.jgit.lib.ObjectReader; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevFlag; +import org.eclipse.jgit.treewalk.filter.PathFilter; + +/** + * A source that may have supplied some (or all) of the result file. + *

+ * Candidates are kept in a queue by BlameGenerator, allowing the generator to + * perform a parallel search down the parents of any merges that are discovered + * during the history traversal. Each candidate retains a {@link #regionList} + * describing sections of the result file the candidate has taken responsibility + * for either directly or indirectly through its history. Actual blame from this + * region list will be assigned to the candidate when its ancestor commit(s) are + * themselves converted into Candidate objects and the ancestor's candidate uses + * {@link #takeBlame(EditList, Candidate)} to accept responsibility for sections + * of the result. + */ +class Candidate { + /** Next candidate in the candidate queue. */ + Candidate queueNext; + + /** Commit being considered (or blamed, depending on state). */ + RevCommit sourceCommit; + + /** Path of the candidate file in {@link #sourceCommit}. */ + PathFilter sourcePath; + + /** Unique name of the candidate blob in {@link #sourceCommit}. */ + ObjectId sourceBlob; + + /** Complete contents of the file in {@link #sourceCommit}. */ + RawText sourceText; + + /** + * Chain of regions this candidate may be blamed for. + *

+ * This list is always kept sorted by resultStart order, making it simple to + * merge-join with the sorted EditList during blame assignment. + */ + Region regionList; + + /** + * Score assigned to the rename to this candidate. + *

+ * Consider the history "A<-B<-C". If the result file S in C was renamed to + * R in B, the rename score for this rename will be held in this field by + * the candidate object for B. By storing the score with B, the application + * can see what the rename score was as it makes the transition from C/S to + * B/R. This may seem backwards since it was C that performed the rename, + * but the application doesn't learn about path R until B. + */ + int renameScore; + + Candidate(RevCommit commit, PathFilter path) { + sourceCommit = commit; + sourcePath = path; + } + + int getParentCount() { + return sourceCommit.getParentCount(); + } + + RevCommit getParent(int idx) { + return sourceCommit.getParent(idx); + } + + Candidate getNextCandidate(@SuppressWarnings("unused") int idx) { + return null; + } + + void add(RevFlag flag) { + sourceCommit.add(flag); + } + + int getTime() { + return sourceCommit.getCommitTime(); + } + + PersonIdent getAuthor() { + return sourceCommit.getAuthorIdent(); + } + + Candidate create(RevCommit commit, PathFilter path) { + return new Candidate(commit, path); + } + + Candidate copy(RevCommit commit) { + Candidate r = create(commit, sourcePath); + r.sourceBlob = sourceBlob; + r.sourceText = sourceText; + r.regionList = regionList; + r.renameScore = renameScore; + return r; + } + + void loadText(ObjectReader reader) throws IOException { + ObjectLoader ldr = reader.open(sourceBlob, Constants.OBJ_BLOB); + sourceText = new RawText(ldr.getCachedBytes(Integer.MAX_VALUE)); + } + + void takeBlame(EditList editList, Candidate child) { + blame(editList, this, child); + } + + private static void blame(EditList editList, Candidate a, Candidate b) { + Region r = b.clearRegionList(); + Region aTail = null; + Region bTail = null; + + for (int eIdx = 0; eIdx < editList.size();) { + // If there are no more regions left, neither side has any + // more responsibility for the result. Remaining edits can + // be safely ignored. + if (r == null) + return; + + Edit e = editList.get(eIdx); + + // Edit ends before the next candidate region. Skip the edit. + if (e.getEndB() <= r.sourceStart) { + eIdx++; + continue; + } + + // Next candidate region starts before the edit. Assign some + // of the blame onto A, but possibly split and also on B. + if (r.sourceStart < e.getBeginB()) { + int d = e.getBeginB() - r.sourceStart; + if (r.length <= d) { + // Pass the blame for this region onto A. + Region next = r.next; + r.sourceStart = e.getBeginA() - d; + aTail = add(aTail, a, r); + r = next; + continue; + } + + // Split the region and assign some to A, some to B. + aTail = add(aTail, a, r.splitFirst(e.getBeginA() - d, d)); + r.slideAndShrink(d); + } + + // At this point e.getBeginB() <= r.sourceStart. + + // An empty edit on the B side isn't relevant to this split, + // as it does not overlap any candidate region. + if (e.getLengthB() == 0) { + eIdx++; + continue; + } + + // If the region ends before the edit, blame on B. + int rEnd = r.sourceStart + r.length; + if (rEnd <= e.getEndB()) { + Region next = r.next; + bTail = add(bTail, b, r); + r = next; + if (rEnd == e.getEndB()) + eIdx++; + continue; + } + + // This region extends beyond the edit. Blame the first + // half of the region on B, and process the rest after. + int len = e.getEndB() - r.sourceStart; + bTail = add(bTail, b, r.splitFirst(r.sourceStart, len)); + r.slideAndShrink(len); + eIdx++; + } + + if (r == null) + return; + + // For any remaining region, pass the blame onto A after shifting + // the source start to account for the difference between the two. + Edit e = editList.get(editList.size() - 1); + int endB = e.getEndB(); + int d = endB - e.getEndA(); + if (aTail == null) + a.regionList = r; + else + aTail.next = r; + do { + if (endB <= r.sourceStart) + r.sourceStart -= d; + r = r.next; + } while (r != null); + } + + private static Region add(Region aTail, Candidate a, Region n) { + // If there is no region on the list, use only this one. + if (aTail == null) { + a.regionList = n; + n.next = null; + return n; + } + + // If the prior region ends exactly where the new region begins + // in both the result and the source, combine these together into + // one contiguous region. This occurs when intermediate commits + // have inserted and deleted lines in the middle of a region. Try + // to report this region as a single region to the application, + // rather than in fragments. + if (aTail.resultStart + aTail.length == n.resultStart + && aTail.sourceStart + aTail.length == n.sourceStart) { + aTail.length += n.length; + return aTail; + } + + // Append the region onto the end of the list. + aTail.next = n; + n.next = null; + return n; + } + + private Region clearRegionList() { + Region r = regionList; + regionList = null; + return r; + } + + @Override + public String toString() { + StringBuilder r = new StringBuilder(); + r.append("Candidate["); + r.append(sourcePath.getPath()); + if (sourceCommit != null) + r.append(" @ ").append(sourceCommit.abbreviate(6).name()); + if (regionList != null) + r.append(" regions:").append(regionList); + r.append("]"); + return r.toString(); + } + + /** + * Special candidate type used for reverse blame. + *

+ * Reverse blame inverts the commit history graph to follow from a commit to + * its descendant children, rather than the normal history direction of + * child to parent. These types require a {@link ReverseCommit} which keeps + * children pointers, allowing reverse navigation of history. + */ + static final class ReverseCandidate extends Candidate { + ReverseCandidate(ReverseCommit commit, PathFilter path) { + super(commit, path); + } + + @Override + int getParentCount() { + return ((ReverseCommit) sourceCommit).getChildCount(); + } + + @Override + RevCommit getParent(int idx) { + return ((ReverseCommit) sourceCommit).getChild(idx); + } + + @Override + int getTime() { + // Invert the timestamp so newer dates sort older. + return -sourceCommit.getCommitTime(); + } + + @Override + Candidate create(RevCommit commit, PathFilter path) { + return new ReverseCandidate((ReverseCommit) commit, path); + } + + @Override + public String toString() { + return "Reverse" + super.toString(); + } + } + + /** + * Candidate loaded from a file source, and not a commit. + *

+ * The {@link Candidate#sourceCommit} field is always null on this type of + * candidate. Instead history traversal follows the single {@link #parent} + * field to discover the next Candidate. Often this is a normal Candidate + * type that has a valid sourceCommit. + */ + static final class BlobCandidate extends Candidate { + /** + * Next candidate to pass blame onto. + *

+ * When computing the differences that this candidate introduced to the + * file content, the parent's sourceText is used as the base. + */ + Candidate parent; + + /** Author name to refer to this blob with. */ + String description; + + BlobCandidate(String name, PathFilter path) { + super(null, path); + description = name; + } + + @Override + int getParentCount() { + return parent != null ? 1 : 0; + } + + @Override + RevCommit getParent(int idx) { + return null; + } + + @Override + Candidate getNextCandidate(int idx) { + return parent; + } + + @Override + void add(RevFlag flag) { + // Do nothing, sourceCommit is null. + } + + @Override + int getTime() { + return Integer.MAX_VALUE; + } + + @Override + PersonIdent getAuthor() { + return new PersonIdent(description, null); + } + } +} diff --git a/src/org/eclipse/jgit/blame/Region.java b/src/org/eclipse/jgit/blame/Region.java new file mode 100644 index 00000000..9ea346b8 --- /dev/null +++ b/src/org/eclipse/jgit/blame/Region.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2011, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.blame; + +/** + * Region of the result that still needs to be computed. + *

+ * Regions are held in a singly-linked-list by {@link Candidate} using the + * {@link Candidate#regionList} field. The list is kept in sorted order by + * {@link #resultStart}. + */ +class Region { + /** Next entry in the region linked list. */ + Region next; + + /** First position of this region in the result file blame is computing. */ + int resultStart; + + /** First position in the {@link Candidate} that owns this Region. */ + int sourceStart; + + /** Length of the region, always >= 1. */ + int length; + + Region(int rs, int ss, int len) { + resultStart = rs; + sourceStart = ss; + length = len; + } + + /** + * Copy the entire result region, but at a new source position. + * + * @param newSource + * the new source position. + * @return the same result region, but offset for a new source. + */ + Region copy(int newSource) { + return new Region(resultStart, newSource, length); + } + + /** + * Split the region, assigning a new source position to the first half. + * + * @param newSource + * the new source position. + * @param newLen + * length of the new region. + * @return the first half of the region, at the new source. + */ + Region splitFirst(int newSource, int newLen) { + return new Region(resultStart, newSource, newLen); + } + + /** + * Edit this region to remove the first {@code d} elements. + * + * @param d + * number of elements to remove from the start of this region. + */ + void slideAndShrink(int d) { + resultStart += d; + sourceStart += d; + length -= d; + } + + Region deepCopy() { + Region head = new Region(resultStart, sourceStart, length); + Region tail = head; + for (Region n = next; n != null; n = n.next) { + Region q = new Region(n.resultStart, n.sourceStart, n.length); + tail.next = q; + tail = q; + } + return head; + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder(); + Region r = this; + do { + if (r != this) + buf.append(','); + buf.append(r.resultStart); + buf.append('-'); + buf.append(r.resultStart + r.length); + r = r.next; + } while (r != null); + return buf.toString(); + } +} diff --git a/src/org/eclipse/jgit/blame/ReverseWalk.java b/src/org/eclipse/jgit/blame/ReverseWalk.java new file mode 100644 index 00000000..5b59804c --- /dev/null +++ b/src/org/eclipse/jgit/blame/ReverseWalk.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2011, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.blame; + +import java.io.IOException; + +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; + +final class ReverseWalk extends RevWalk { + ReverseWalk(Repository repo) { + super(repo); + } + + @Override + public ReverseCommit next() throws MissingObjectException, + IncorrectObjectTypeException, IOException { + ReverseCommit c = (ReverseCommit) super.next(); + if (c == null) + return null; + for (int pIdx = 0; pIdx < c.getParentCount(); pIdx++) + ((ReverseCommit) c.getParent(pIdx)).addChild(c); + return c; + } + + @Override + protected RevCommit createCommit(AnyObjectId id) { + return new ReverseCommit(id); + } + + static final class ReverseCommit extends RevCommit { + private static final ReverseCommit[] NO_CHILDREN = {}; + + private ReverseCommit[] children = NO_CHILDREN; + + ReverseCommit(AnyObjectId id) { + super(id); + } + + void addChild(ReverseCommit c) { + // Always put the most recent child onto the front of the list. + // This works correctly because our ReverseWalk parent (above) + // runs in COMMIT_TIME_DESC order. Older commits will be popped + // later and should go in front of the children list so they are + // visited first by BlameGenerator when considering candidates. + + int cnt = children.length; + if (cnt == 0) + children = new ReverseCommit[] { c }; + else if (cnt == 1) + children = new ReverseCommit[] { c, children[0] }; + else { + ReverseCommit[] n = new ReverseCommit[1 + cnt]; + n[0] = c; + System.arraycopy(children, 0, n, 1, cnt); + children = n; + } + } + + int getChildCount() { + return children.length; + } + + ReverseCommit getChild(final int nth) { + return children[nth]; + } + } +} diff --git a/tests/com/gitblit/tests/DiffUtilsTest.java b/tests/com/gitblit/tests/DiffUtilsTest.java index 84353c19..ff6f2328 100644 --- a/tests/com/gitblit/tests/DiffUtilsTest.java +++ b/tests/com/gitblit/tests/DiffUtilsTest.java @@ -35,7 +35,7 @@ public class DiffUtilsTest extends TestCase { assertTrue(DiffOutputType.forName("gitblit").equals(DiffOutputType.GITBLIT)); assertTrue(DiffOutputType.forName(null) == null); } - + public void testParentCommitDiff() throws Exception { Repository repository = GitBlitSuite.getHelloworldRepository(); RevCommit commit = JGitUtils.getCommit(repository, @@ -107,10 +107,11 @@ public class DiffUtilsTest extends TestCase { String expected = "- system.out.println(\"Hello World\");\n+ System.out.println(\"Hello World\""; assertTrue(patch.indexOf(expected) > -1); } - + public void testBlame() throws Exception { Repository repository = GitBlitSuite.getHelloworldRepository(); - List lines = DiffUtils.blame(repository, "java.java", "1d0c2933a4ae69c362f76797d42d6bd182d05176"); + List lines = DiffUtils.blame(repository, "java.java", + "1d0c2933a4ae69c362f76797d42d6bd182d05176"); repository.close(); assertTrue(lines.size() > 0); assertTrue(lines.get(0).commitId.equals("c6d31dccf5cc75e8e46299fc62d38f60ec6d41e0")); diff --git a/tests/com/gitblit/tests/GitBlitSuite.java b/tests/com/gitblit/tests/GitBlitSuite.java index c9e383e7..31e29c96 100644 --- a/tests/com/gitblit/tests/GitBlitSuite.java +++ b/tests/com/gitblit/tests/GitBlitSuite.java @@ -24,7 +24,7 @@ import junit.framework.TestSuite; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.storage.file.FileRepository; -import com.gitblit.FileLoginService; +import com.gitblit.FileUserService; import com.gitblit.FileSettings; import com.gitblit.GitBlit; import com.gitblit.GitBlitException; @@ -45,6 +45,7 @@ public class GitBlitSuite extends TestSetup { suite.addTestSuite(ByteFormatTest.class); suite.addTestSuite(MarkdownUtilsTest.class); suite.addTestSuite(JGitUtilsTest.class); + suite.addTestSuite(SyndicationUtilsTest.class); suite.addTestSuite(DiffUtilsTest.class); suite.addTestSuite(MetricUtilsTest.class); suite.addTestSuite(TicgitUtilsTest.class); @@ -72,8 +73,8 @@ public class GitBlitSuite extends TestSetup { protected void setUp() throws Exception { FileSettings settings = new FileSettings("distrib/gitblit.properties"); GitBlit.self().configureContext(settings); - FileLoginService loginService = new FileLoginService(new File("distrib/users.properties")); - GitBlit.self().setLoginService(loginService); + FileUserService loginService = new FileUserService(new File("distrib/users.properties")); + GitBlit.self().setUserService(loginService); if (REPOSITORIES.exists() || REPOSITORIES.mkdirs()) { cloneOrFetch("helloworld.git", "https://github.com/git/hello-world.git"); diff --git a/tests/com/gitblit/tests/GitBlitTest.java b/tests/com/gitblit/tests/GitBlitTest.java index 13705f1d..22297683 100644 --- a/tests/com/gitblit/tests/GitBlitTest.java +++ b/tests/com/gitblit/tests/GitBlitTest.java @@ -55,7 +55,7 @@ public class GitBlitTest extends TestCase { model.addRepository(repository); assertTrue("Admin can't access repository!", model.canAccessRepository(repository)); } - + public void testAccessRestrictionTypes() throws Exception { assertTrue(AccessRestrictionType.PUSH.exceeds(AccessRestrictionType.NONE)); assertTrue(AccessRestrictionType.CLONE.exceeds(AccessRestrictionType.PUSH)); @@ -72,7 +72,7 @@ public class GitBlitTest extends TestCase { assertFalse(AccessRestrictionType.NONE.atLeast(AccessRestrictionType.PUSH)); assertFalse(AccessRestrictionType.PUSH.atLeast(AccessRestrictionType.CLONE)); assertFalse(AccessRestrictionType.CLONE.atLeast(AccessRestrictionType.VIEW)); - + assertTrue(AccessRestrictionType.PUSH.toString().equals("PUSH")); assertTrue(AccessRestrictionType.CLONE.toString().equals("CLONE")); assertTrue(AccessRestrictionType.VIEW.toString().equals("VIEW")); @@ -82,50 +82,50 @@ public class GitBlitTest extends TestCase { assertTrue(AccessRestrictionType.fromName("clone").equals(AccessRestrictionType.CLONE)); assertTrue(AccessRestrictionType.fromName("view").equals(AccessRestrictionType.VIEW)); } - + public void testFileSettings() throws Exception { FileSettings settings = new FileSettings("distrib/gitblit.properties"); assertTrue(settings.getBoolean("missing", true) == true); assertTrue(settings.getString("missing", "default").equals("default")); assertTrue(settings.getInteger("missing", 10) == 10); assertTrue(settings.getInteger("realm.realmFile", 5) == 5); - + assertTrue(settings.getBoolean("git.enableGitServlet", false) == true); - assertTrue(settings.getString("realm.realmFile", null).equals("users.properties")); + assertTrue(settings.getString("realm.userService", null).equals("users.properties")); assertTrue(settings.getInteger("realm.minPasswordLength", 0) == 5); List mdExtensions = settings.getStrings("web.markdownExtensions"); assertTrue(mdExtensions.size() > 0); assertTrue(mdExtensions.contains("md")); - + List keys = settings.getAllKeys("server"); assertTrue(keys.size() > 0); assertTrue(keys.contains("server.httpsPort")); } - + public void testGitblitSettings() throws Exception { // These are already tested by above test method. assertTrue(GitBlit.getBoolean("missing", true) == true); assertTrue(GitBlit.getString("missing", "default").equals("default")); assertTrue(GitBlit.getInteger("missing", 10) == 10); - assertTrue(GitBlit.getInteger("realm.realmFile", 5) == 5); - + assertTrue(GitBlit.getInteger("realm.userService", 5) == 5); + assertTrue(GitBlit.getBoolean("git.enableGitServlet", false) == true); - assertTrue(GitBlit.getString("realm.realmFile", null).equals("users.properties")); + assertTrue(GitBlit.getString("realm.userService", null).equals("users.properties")); assertTrue(GitBlit.getInteger("realm.minPasswordLength", 0) == 5); List mdExtensions = GitBlit.getStrings("web.markdownExtensions"); assertTrue(mdExtensions.size() > 0); assertTrue(mdExtensions.contains("md")); - + List keys = GitBlit.getAllKeys("server"); assertTrue(keys.size() > 0); assertTrue(keys.contains("server.httpsPort")); } - - public void testAuthentication() throws Exception { + + public void testAuthentication() throws Exception { assertTrue(GitBlit.self().authenticate("admin", "admin".toCharArray()) != null); } - - public void testRepositories() throws Exception { + + public void testRepositories() throws Exception { assertTrue(GitBlit.self().getRepository("missing") == null); assertTrue(GitBlit.self().getRepositoryModel("missing") == null); } diff --git a/tests/com/gitblit/tests/JGitUtilsTest.java b/tests/com/gitblit/tests/JGitUtilsTest.java index 19a48474..daf0cfee 100644 --- a/tests/com/gitblit/tests/JGitUtilsTest.java +++ b/tests/com/gitblit/tests/JGitUtilsTest.java @@ -138,6 +138,7 @@ public class JGitUtilsTest extends TestCase { public void testBranches() throws Exception { Repository repository = GitBlitSuite.getJGitRepository(); + assertTrue(JGitUtils.getLocalBranches(repository, true, 0).size() == 0); for (RefModel model : JGitUtils.getLocalBranches(repository, true, -1)) { assertTrue(model.getName().startsWith(Constants.R_HEADS)); assertTrue(model.equals(model)); @@ -160,6 +161,7 @@ public class JGitUtilsTest extends TestCase { public void testTags() throws Exception { Repository repository = GitBlitSuite.getJGitRepository(); + assertTrue(JGitUtils.getTags(repository, true, 5).size() == 5); for (RefModel model : JGitUtils.getTags(repository, true, -1)) { if (model.getObjectId().getName().equals("d28091fb2977077471138fe97da1440e0e8ae0da")) { assertTrue("Not an annotated tag!", model.isAnnotatedTag()); @@ -276,6 +278,7 @@ public class JGitUtilsTest extends TestCase { } public void testRevlog() throws Exception { + assertTrue(JGitUtils.getRevLog(null, 0).size() == 0); List commits = JGitUtils.getRevLog(null, 10); assertTrue(commits.size() == 0); @@ -306,6 +309,7 @@ public class JGitUtilsTest extends TestCase { } public void testSearchRevlogs() throws Exception { + assertTrue(JGitUtils.searchRevlogs(null, null, "java", SearchType.COMMIT, 0, 0).size() == 0); List results = JGitUtils.searchRevlogs(null, null, "java", SearchType.COMMIT, 0, 3); assertTrue(results.size() == 0); diff --git a/tests/com/gitblit/tests/StringUtilsTest.java b/tests/com/gitblit/tests/StringUtilsTest.java index b0d9a1ff..1bf0de4a 100644 --- a/tests/com/gitblit/tests/StringUtilsTest.java +++ b/tests/com/gitblit/tests/StringUtilsTest.java @@ -36,6 +36,12 @@ public class StringUtilsTest extends TestCase { String output = "this
is
a
test

of

line

breaking"; assertTrue(StringUtils.breakLinesForHtml(input).equals(output)); } + + public void testEncodeUrl() throws Exception { + String input = "test /"; + String output = "test%20%2F"; + assertTrue(StringUtils.encodeURL(input).equals(output)); + } public void testEscapeForHtml() throws Exception { String input = "& < > \" \t"; @@ -44,6 +50,12 @@ public class StringUtilsTest extends TestCase { assertTrue(StringUtils.escapeForHtml(input, false).equals(outputNoChange)); assertTrue(StringUtils.escapeForHtml(input, true).equals(outputChange)); } + + public void testDecodeForHtml() throws Exception { + String input = "& < > ""; + String output = "& < > \""; + assertTrue(StringUtils.decodeFromHtml(input).equals(output)); + } public void testFlattenStrings() throws Exception { String[] strings = { "A", "B", "C", "D" }; @@ -70,6 +82,11 @@ public class StringUtilsTest extends TestCase { assertTrue(StringUtils.getSHA1("blob 16\000what is up, doc?").equals( "bd9dbf5aae1a3862dd1526723246b20206e5fc37")); } + + public void testMD5() throws Exception { + assertTrue(StringUtils.getMD5("blob 16\000what is up, doc?").equals( + "77fb8d95331f0d557472f6776d3aedf6")); + } public void testRootPath() throws Exception { String input = "/nested/path/to/repository"; @@ -77,7 +94,7 @@ public class StringUtilsTest extends TestCase { assertTrue(StringUtils.getRootPath(input).equals(output)); assertTrue(StringUtils.getRootPath("repository").equals("")); } - + public void testStringsFromValue() throws Exception { List strings = StringUtils.getStringsFromValue("A B C D"); assertTrue(strings.size() == 4); diff --git a/tests/com/gitblit/tests/SyndicationUtilsTest.java b/tests/com/gitblit/tests/SyndicationUtilsTest.java new file mode 100644 index 00000000..ab518041 --- /dev/null +++ b/tests/com/gitblit/tests/SyndicationUtilsTest.java @@ -0,0 +1,42 @@ +/* + * Copyright 2011 gitblit.com. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.gitblit.tests; + +import java.io.ByteArrayOutputStream; +import java.util.List; + +import junit.framework.TestCase; + +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; + +import com.gitblit.utils.JGitUtils; +import com.gitblit.utils.SyndicationUtils; + +public class SyndicationUtilsTest extends TestCase { + + public void testSyndication() throws Exception { + Repository repository = GitBlitSuite.getHelloworldRepository(); + List commits = JGitUtils.getRevLog(repository, 1); + ByteArrayOutputStream os = new ByteArrayOutputStream(); + SyndicationUtils.toRSS("http://localhost", "Title", "Description", "Repository", commits, os); + String feed = os.toString(); + os.close(); + assertTrue(feed.length() > 100); + assertTrue(feed.indexOf("Title") > -1); + assertTrue(feed.indexOf("Description") > -1); + } +} \ No newline at end of file diff --git a/tools/ant-googlecode-0.0.3.jar b/tools/ant-googlecode-0.0.3.jar new file mode 100644 index 00000000..452fa84f Binary files /dev/null and b/tools/ant-googlecode-0.0.3.jar differ