diff options
233 files changed, 6841 insertions, 2272 deletions
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt index b45853d696..7c9fcbf6df 100644 --- a/Documentation/access-control.txt +++ b/Documentation/access-control.txt @@ -10,7 +10,7 @@ be granted to individual users. System Groups ------------- -Gerrit comes with 3 system groups, with special access privileges +Gerrit comes with 4 system groups, with special access privileges and membership management. The identity of these groups is set in the `system_config` table within the database, so the groups can be renamed after installation if desired. @@ -65,6 +65,21 @@ cause it to become approved or rejected. Registered users are always permitted to make and publish comments on any change in any project they have `Read Access` to. +Project Owners +~~~~~~~~~~~~~~ + +Access rights assigned to this group are always evaluated within the +context of a project and are resolved to access rights for all users +which own the project. + +By assigning access rights to this group on a parent project Gerrit +administrators can define a set of default access rights for project +owners. Child projects inherit these access rights where they are +resolved to the users that own the child project. +Having default access rights for projects owners assigned on a parent +project may avoid the need to initially configure access rights for +newly created child projects. + Account Groups -------------- diff --git a/Documentation/cmd-create-project.txt b/Documentation/cmd-create-project.txt index 3da01c4015..f698d5c7ea 100644 --- a/Documentation/cmd-create-project.txt +++ b/Documentation/cmd-create-project.txt @@ -13,10 +13,13 @@ SYNOPSIS [--branch <REF>] \ [\--owner <GROUP> ...] \ [\--parent <NAME>] \ +[\--permissions-only] \ [\--description <DESC>] \ [\--submit-type <TYPE>] \ +[\--use-content-merge] \ [\--use-contributor-agreements] \ [\--use-signed-off-by] +[\--empty-commit] DESCRIPTION ----------- @@ -68,6 +71,11 @@ repository.*.createGroup will be used. If they don't exist, through. If not specified, the parent is set to the default project `\-- All Projects \--`. +\--permissions-only:: + Create the project only to serve as a parent for other + projects. The new project's Git repository will not be + initialized, and cannot be cloned. + \--description:: Initial description of the project. If not specified, no description is stored. @@ -89,6 +97,13 @@ Description values containing spaces should be quoted in single quotes Defaults to MERGE_IF_NECESSARY. For more details see link:project-setup.html#submit_type[Change Submit Actions]. +\--use-content-merge:: + If enabled, Gerrit will try to perform a 3-way merge of text + file content when a file has been modified by both the + destination branch and the change being submitted. This + option only takes effect if submit type is not + FAST_FORWARD_ONLY. Disabled by default. + \--use-contributor-agreements:: If enabled, authors must complete a contributor agreement on the site before pushing any commits or changes to this @@ -99,6 +114,10 @@ link:project-setup.html#submit_type[Change Submit Actions]. from either the author or the uploader in the commit message. Disabled by default. +\--empty-commit: + Creates an initial empty commit for the Git repository of the + project that is newly created. + EXAMPLES -------- diff --git a/Documentation/cmd-query.txt b/Documentation/cmd-query.txt index 76a9b7dc34..9047202505 100644 --- a/Documentation/cmd-query.txt +++ b/Documentation/cmd-query.txt @@ -11,7 +11,7 @@ SYNOPSIS 'ssh' -p <port> <host> 'gerrit query' \ [\--format {TEXT | JSON}] \ [\--current-patch-set] \ -[\--patch-sets] \ +[\--patch-sets|--all-approvals] \ [\--] \ <query> \ [limit:<n>] \ @@ -47,6 +47,12 @@ OPTIONS the \--current-patch-set flag then the current patch set information will be output twice, once in each field. +\--all-approvals:: + Include information about all patch sets along with the + approval information for each patch set. If combined with + the \--current-patch-set flag then the current patch set + information will be output twice, once in each field. + limit:<n>:: Maximum number of results to return. This is actually a query operator, and not a command line option. If more diff --git a/Documentation/cmd-stream-events.txt b/Documentation/cmd-stream-events.txt index aae908a2f7..fb54f67edb 100644 --- a/Documentation/cmd-stream-events.txt +++ b/Documentation/cmd-stream-events.txt @@ -42,7 +42,7 @@ SCHEMA ------ The JSON messages consist of nested objects referencing the *change*, *patchset*, *account* involved, and other attributes as appropriate. -The currently supported message types are *patchset-added*, +The currently supported message types are *patchset-created*, *comment-added*, *change-merged*, and *change-abandoned*. Note that any field may be missing in the JSON messages, so consumers of @@ -50,9 +50,9 @@ this JSON stream should deal with that appropriately. Events ~~~~~~ -Patchset Added -^^^^^^^^^^^^^^ -type:: "patchset-added" +Patchset Created +^^^^^^^^^^^^^^^^ +type:: "patchset-created" change:: link:json.html#change[change attribute] @@ -102,6 +102,15 @@ author:: link:json.html#account[account attribute] comment:: Comment text author had written +Ref Updated +^^^^^^^^^^^ +type:: "ref-updated" + +submitter:: link:json.html#account[account attribute] + +refUpdate:: link:json.html#refupdate[refupdate attribute] + + SEE ALSO -------- diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt index ebd1c7f017..864092eda3 100644 --- a/Documentation/config-gerrit.txt +++ b/Documentation/config-gerrit.txt @@ -56,6 +56,19 @@ from the user's account object in LDAP. The user's group membership is also pulled from LDAP, making any LDAP groups that a user is a member of available as groups in Gerrit. + +* `CLIENT_SSL_CERT_LDAP` ++ +This authentication type is actually kind of SSO. Gerrit will configure +Jetty's SSL channel to request client's SSL certificate. For this +authentication to work a Gerrit administrator has to import the root +certificate of the trust chain used to issue the client's certificate +into the <review-site>/etc/keystore. +After the authentication is done Gerrit will obtain basic user +registration (name and email) from LDAP, and some group memberships. +Therefore, the "_LDAP" suffix in the name of this authentication type. +This authentication type can only be used under hosted daemon mode, and +the httpd.listenUrl must use https:// as the protocol. ++ * `LDAP` + Gerrit prompts the user to enter a username and a password, which @@ -578,6 +591,27 @@ repository data manipulation. + Default on JGit is 128 file descriptors on all platforms. +[[core.streamFileThreshold]]core.streamFileThreshold:: ++ +Largest object size, in bytes, that JGit will allocate as a +contiguous byte array. Any file revision larger than this threshold +will have to be streamed, typically requiring the use of temporary +files under '$GIT_DIR/objects' to implement psuedo-random access +during delta decompression. ++ +Servers with very high traffic should set this to be larger than +the size of their common big files. For example a server managing +the Android platform typically has to deal with ~10-12 MiB XML +files, so `15 m` would be a reasonable setting in that environment. +Setting this too high may cause the JVM to run out of heap space +when handling very big binary files, such as device firmware or +CD-ROM ISO images. ++ +Default is 50 MiB on all platforms. Prior to Gerrit 2.1.6, +this value was effectively 2047 MiB. ++ +Common unit suffixes of 'k', 'm', or 'g' are supported. + [[core.packedGitMmap]]core.packedGitMmap:: + When true, JGit will use `mmap()` rather than `malloc()+read()` @@ -797,6 +831,11 @@ By default unset, as the git daemon must be configured externally by the system administrator, and might not even be running on the same host as Gerrit. +[[gerrit.replicateOnStartup]]gerrit.replicateOnStartup:: ++ +If true, replicates to all remotes on startup to ensure they are +in-sync with this server. By default, true. + [[gitweb]]Section gitweb ~~~~~~~~~~~~~~~~~~~~~~~~ @@ -1079,8 +1118,9 @@ By default, 5 minutes. ~~~~~~~~~~~~~~~~~~~~ LDAP integration is only enabled if `auth.type` was set to -`HTTP_LDAP` or `LDAP`. See above for a detailed description of -the auth.type settings and their implications. +`HTTP_LDAP`, `LDAP` or `CLIENT_SSL_CERT_LDAP`. See above for a +detailed description of the auth.type settings and their +implications. An example LDAP configuration follows, and then discussion of the parameters introduced here. Suitable defaults for most @@ -1315,6 +1355,53 @@ Common examples: safe = true ---- + +[[pack]]Section pack +~~~~~~~~~~~~~~~~~~~~ +Global settings controlling how Gerrit Code Review creates pack +streams for Git clients running clone, fetch, or pull. Most of these +variables are per-client request, and thus should be carefully set +given the expected concurrent request load and available CPU and +memory resources. + +[[pack.deltacompression]]pack.deltacompression:: ++ +If true, delta compression between objects is enabled. This may +result in a smaller overall transfer for the client, but requires +more server memory and CPU time. ++ +False (off) by default, matching Gerrit Code Review 2.1.4. + +[[pack.threads]]pack.threads:: ++ +Maximum number of threads to use for delta compression (if enabled). +This is per-client request. If set to 0 then the number of CPUs is +auto-detected and one thread per CPU is used, per client request. ++ +By default, 1. + + +[[receive]]Section receive +~~~~~~~~~~~~~~~~~~~~~~~~~~ +Sets the group of users allowed to execute 'receive-pack' on the +server, 'receive-pack' is what runs on the server during a user's +push or repo upload command. + +---- +[receive] + allowGroup = GROUP_ALLOWED_TO_EXECUTE + allowGroup = YET_ANOTHER_GROUP_ALLOWED_TO_EXECUTE +---- + +[[receive.allowGroup]]receive.allowGroup:: ++ +Name of the groups of users that are allowed to execute +'receive-pack' on the server. One or more groups can be set. ++ +If no groups are added, any user will be allowed to execute +'receive-pack' on the server. + + [[repository]]Section repository ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Repositories in this sense are the same as projects. @@ -1516,6 +1603,41 @@ pool by a simple FIFO scheduling system. + By default, 1 plus the number of CPUs available to the JVM. +[[sshd.maxAuthTries]]sshd.maxAuthTries:: ++ +Maximum number of authentication attempts before the server +disconnects the client. Each public key that a client has loaded +into its local agent counts as one auth request. Users can work +around the server's limit by loading less keys into their agent, +or selecting a specific key in their `~/.ssh/config` file with +the `IdentityFile` option. ++ +By default, 6. + +[[sshd.loginGraceTime]]sshd.loginGraceTime:: ++ +Time in seconds that a client has to authenticate before the server +automatically terminates their connection. Values should use common +unit suffixes to express their setting: ++ +* s, sec, second, seconds +* m, min, minute, minutes +* h, hr, hour, hours +* d, day, days + ++ +By default, 2 minutes. + +[[sshd.maxConnectionsPerUser]]sshd.maxConnectionsPerUser:: ++ +Maximum number of concurrent SSH sessions that a user account +may open at one time. This is the number of distinct SSH logins +the each user may have active at one time, and is not related to +the number of commands a user may issue over a single connection. +If set to 0, there is no limit. ++ +By default, 64. + [[sshd.cipher]]sshd.cipher:: + Available ciphers. To permit multiple ciphers, specify multiple @@ -1650,6 +1772,28 @@ reasonable timeout value. + Defaults to 0 seconds, wait indefinitely. + +[[upload]]Section upload +~~~~~~~~~~~~~~~~~~~~~~~~~~ +Sets the group of users allowed to execute 'upload-pack' on the +server, 'upload-pack' is what runs on the server during a user's +fetch, clone or repo sync command. + +---- +[upload] + allowGroup = GROUP_ALLOWED_TO_EXECUTE + allowGroup = YET_ANOTHER_GROUP_ALLOWED_TO_EXECUTE +---- + +[[upload.allowGroup]]upload.allowGroup:: ++ +Name of the groups of users that are allowed to execute 'upload-pack' +on the server. One or more groups can be set. ++ +If no groups are added, any user will be allowed to execute +'upload-pack' on the server. + + [[user]] Section user ~~~~~~~~~~~~~~~~~~~~~ diff --git a/Documentation/config-hooks.txt b/Documentation/config-hooks.txt index e271ba8155..fd2ae82b93 100644 --- a/Documentation/config-hooks.txt +++ b/Documentation/config-hooks.txt @@ -66,6 +66,15 @@ Called whenever a change has been restored. change-restored --change <change id> --change-url <change url> --project <project name> --branch <branch> --restorer <restorer> --reason <reason> ==== +ref-updated +~~~~~~~~~~~ + +Called whenever a ref has been updated. + +==== + ref-updated --oldrev <old rev> --newrev <new rev> --refname <ref name> --project <project name> --submitter <submitter> +==== + Configuration Settings ---------------------- diff --git a/Documentation/config-mail.txt b/Documentation/config-mail.txt new file mode 100644 index 0000000000..168bbfe169 --- /dev/null +++ b/Documentation/config-mail.txt @@ -0,0 +1,172 @@ +Gerrit Code Review - Mail Templates +=================================== + +Gerrit uses velocity templates for the bulk of the standard mails it sends out. +There are builtin default templates which are used if they are not overridden. +These defaults are also provided as examples so that administrators may copy +them and easily modify them to tweak their contents. + + +Template Locations and Extensions: +---------------------------------- + +The default example templates reside under: `'$site_path'/etc/mail` and are +terminated with the double extension `.vm.example`. Modifying these example +files will have no effect on the behavior of Gerrit. However, copying an +example template to an equivalently named file without the `.example` extension +and modifying it will allow an administrator to customize the template. + + +Supported Mail Templates: +------------------------- + +Each mail that Gerrit sends out is controlled by at least one template, these +are listed below. Change emails are influenced by two additional templates, +one to set the subject line, and one to set the footer which gets appended to +all the change emails (see `ChangeSubject.vm` and `ChangeFooter.vm` below.) + +Abandoned.vm +~~~~~~~~~~~~ + +The `Abandoned.vm` template will determine the contents of the email related +to a change being abandoned. It is a `ChangeEmail`: see `ChangeSubject.vm` and +`ChangeFooter.vm`. + +ChangeFooter.vm +~~~~~~~~~~~~~~~ + +The `ChangeFooter.vm` template will determine the contents of the footer +text that will be appended to emails related to changes (all `ChangeEmails)`. + +ChangeSubject.vm +~~~~~~~~~~~~~~~~ + +The `ChangeSubject.vm` template will determine the contents of the email +subject line for ALL emails related to changes. + +Comment.vm +~~~~~~~~~~ + +The `Comment.vm` template will determine the contents of the email related to +a user submitting comments on changes. It is a `ChangeEmail`: see + +Merged.vm +~~~~~~~~~ + +The `Merged.vm` template will determine the contents of the email related to +a change successfully merged to the head. It is a `ChangeEmail`: see +`ChangeSubject.vm` and `ChangeFooter.vm`. + +MergeFail.vm +~~~~~~~~~~~~ + +The `MergeFail.vm` template will determine the contents of the email related +to a failure upon attempting to merge a change to the head. It is a + +NewChange.vm +~~~~~~~~~~~~ + +The `NewChange.vm` template will determine the contents of the email related +to a user submitting a new change for review. It is a `ChangeEmail`: see +`ChangeSubject.vm` and `ChangeFooter.vm`. + +RegisterNewEmail.vm +~~~~~~~~~~~~~~~~~~~ + +The `RegisterNewEmail.vm` template will determine the contents of the email +related to registering new email accounts. + +ReplacePatchSet.vm +~~~~~~~~~~~~~~~~~~ + +The `ReplacePatchSet.vm` template will determine the contents of the email +related to a user submitting a new patchset for a change. It is a +`ChangeEmail`: see `ChangeSubject.vm` and `ChangeFooter.vm`. + + +Mail Variables and Methods +-------------------------- + +Mail templates can access and display objects currently made available to them +via the velocity context. While the base objects are documented here, it is +possible to call public methods on these objects from templates. Those methods +are not documented here since they could change with every release. As these +templates are meant to be modified only by a qualified sysadmin, it is accepted +that writing templates for Gerrit emails is likely to require some basic +knowledge of the class structure to be useful. Browsing the source code might +be necessary for anything more than a minor formatting change. + +Warning +~~~~~~~ + +Be aware that modifying templates can cause them to fail to parse and therefor +not send out the actual email, or worse, calling methods on the available +objects could have internal side effects which would adversely affect the +health of your Gerrit server and/or data. + +All OutgoingEmails +~~~~~~~~~~~~~~~~~~ + +All outgoing emails have the following variables available to them: + +$email:: ++ +A reference to the class constructing the current `OutgoingEmail`. With this +reference it is possible to call any public method on the OutgoingEmail class +or the current child class inherited from it. + +$messageClass:: ++ +A String containing the messageClass + +$StringUtils:: ++ +A reference to the Apache `StringUtils` class. This can be very useful for +formatting strings. + +Change Emails +~~~~~~~~~~~~~ + +All change related emails have the following additional variables available to them: + +$change:: ++ +A reference to the current `Change` object + +$changeId:: ++ +Id of the current change (a `Change.Key`) + +$coverLetter:: ++ +The text of the `ChangeMessage` + +$branch:: ++ +A reference to the branch of this change (a `Branch.NameKey`) + +$fromName:: ++ +The name of the from user + +$projectName:: ++ +The name of this change's project + +$patchSet:: ++ +A reference to the current `PatchSet` + +$patchSetInfo:: ++ +A reference to the current `PatchSetInfo` + + +See Also +-------- + +* link:http://velocity.apache.org/[velocity] + +GERRIT +------ +Part of link:index.html[Gerrit Code Review] diff --git a/Documentation/config-replication.txt b/Documentation/config-replication.txt index abb6a9791c..1e20d2b9a2 100644 --- a/Documentation/config-replication.txt +++ b/Documentation/config-replication.txt @@ -42,8 +42,8 @@ different hosts: url = mirror1.us.some.org:/pub/git/${name}.git url = mirror2.us.some.org:/pub/git/${name}.git url = mirror3.us.some.org:/pub/git/${name}.git - push = +refs/heads/* - push = +refs/tags/* + push = +refs/heads/*:refs/heads/* + push = +refs/tags/*:refs/tags/* threads = 3 authGroup = Public Mirror Group authGroup = Second Public Mirror Group @@ -142,6 +142,19 @@ This is a Gerrit specific extension to the Git remote block. + By default, 15 seconds. +[[remote.name.replicationRetry]]remote.<name>.replicationRetry:: ++ +Number of minutes to wait before scheduling a remote push operation +previously failed due to an offline remote server. ++ +If a remote push operation fails because a remote server was +offline, all push operations to the same destination URL are +blocked, and the remote push is continuously retried. ++ +This is a Gerrit specific extension to the Git remote block. ++ +By default, 1 minute. + [[remote.name.threads]]remote.<name>.threads:: + Number of worker threads to dedicate to pushing to the repositories diff --git a/Documentation/index.txt b/Documentation/index.txt index 1e13e8d8f5..e419455204 100644 --- a/Documentation/index.txt +++ b/Documentation/index.txt @@ -31,6 +31,7 @@ Configuration * link:config-sso.html[Single Sign-On Systems] * link:config-apache2.html[Apache 2 Reverse Proxy] * link:config-hooks.html[Hooks] +* link:config-mail.html[Mail Templates] Developer Documentation ----------------------- diff --git a/Documentation/json.txt b/Documentation/json.txt index 1c9a808dbe..99b158daae 100644 --- a/Documentation/json.txt +++ b/Documentation/json.txt @@ -107,6 +107,19 @@ was added or last updated. by:: Reviewer of the patch set in <<account,account attribute>>. +[[refupdate]] +refupdate +-------- +Information about a ref that was updated. + +oldRev:: The old value of the ref, prior to the update. + +newRev:: The new value the ref was updated to. + +project:: Project path in Gerrit + +refName:: Ref name within project. + SEE ALSO -------- diff --git a/Documentation/licenses.txt b/Documentation/licenses.txt index 2c123f8841..25194a98ae 100644 --- a/Documentation/licenses.txt +++ b/Documentation/licenses.txt @@ -30,6 +30,7 @@ Apache Log4J <<apache2,Apache License 2.0>> Apache MINA <<apache2,Apache License 2.0>> Apache Tomact Servlet API <<apache2,Apache License 2.0>> Apache SSHD <<apache2,Apache License 2.0>>, see also <<sshd,NOTICE>> +Apache Velocity <<apache2,Apache License 2.0>> Apache Xerces <<apache2,Apache License 2.0>> OpenID4Java <<apache2,Apache License 2.0>> Neko HTML <<apache2,Apache License 2.0>> diff --git a/Documentation/pgm-daemon.txt b/Documentation/pgm-daemon.txt index 228c60f859..0b2e72f153 100644 --- a/Documentation/pgm-daemon.txt +++ b/Documentation/pgm-daemon.txt @@ -49,7 +49,7 @@ OPTIONS Run in slave mode, permitting only read operations by clients. Commands which modify state such as link:cmd-receive-pack.html[recieve-pack] (creates new changes - or updates existing ones) or link:cmd-approve.html[approve] + or updates existing ones) or link:cmd-review.html[review] (sets approve marks) are disabled. + This option automatically implies '\--disable-httpd \--enable-sshd'. diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt index c62e894017..5b2f81e632 100644 --- a/Documentation/user-search.txt +++ b/Documentation/user-search.txt @@ -87,7 +87,8 @@ Changes where 'SHA1' is one of the patch sets of the change. [[project]] project:'PROJECT':: + -Changes occuring in 'PROJECT'. +Changes occuring in 'PROJECT'. If 'PROJECT' starts with `^` it +matches project names by regular expression. [[branch]] branch:'BRANCH':: @@ -98,6 +99,9 @@ prefix. This operator is a shorthand for 'refs:'. Searching for 'branch:master' really means 'ref:refs/heads/master', and searching for 'branch:refs/heads/master' is the same as searching for 'ref:refs/heads/refs/heads/master'. ++ +If 'BRANCH' starts with `^` it matches branch names by regular +expression patterns. [[topic]] topic:'TOPIC':: @@ -105,6 +109,9 @@ topic:'TOPIC':: Changes whose designated topic at upload was 'TOPIC'. This is often combined with 'branch:' and 'project:' operators to select all related changes in a series. ++ +If 'TOPIC' starts with `^` it matches topic names by regular +expression patterns. [[ref]] ref:'REF':: @@ -112,6 +119,9 @@ ref:'REF':: Changes where the destination branch is exactly the given 'REF' name. Since 'REF' is absolute from the top of the repository it must start with 'refs/'. ++ +If 'REF' starts with `^` it matches reference names by regular +expression patterns. [[tr]][[bug]] tr:'ID', bug:'ID':: @@ -130,12 +140,29 @@ Matches changes where the approval score 'VALUE' has been set during a review. See <<labels,labels>> below for more detail on the format of the argument. +[[message]] +message:'MESSAGE':: ++ +Changes that matches 'MESSAGE' arbitrary string in body commit messages. + [[file]] file:\^'REGEX':: + Matches any change where REGEX matches a file that was affected by the change. The regular expression pattern must start with -'\^'. For example, to match all XML files use `file:^.*\.xml$`. +`\^`. For example, to match all XML files use `file:^.*\.xml$`. ++ +The `\^` required at the beginning of the regular expression not only +denotes a regular expression, but it also has the usual meaning of +anchoring the match to the start of the string. To match all Java +files, use `file:^.*\.java`. ++ +The entire regular expression pattern, including the `\^` character, +should be double quoted when using more complex construction (like +ones using a bracket expression). For example, to match all XML +files named like 'name1.xml', 'name2.xml', and 'name3.xml' use +`\file:"\^name[1-3].xml"`. ++ Currently this operator is only available on a watched project and may not be used in the search bar. diff --git a/gerrit-common/pom.xml b/gerrit-common/pom.xml index adac3a8058..af03191364 100644 --- a/gerrit-common/pom.xml +++ b/gerrit-common/pom.xml @@ -22,7 +22,7 @@ limitations under the License. <parent> <groupId>com.google.gerrit</groupId> <artifactId>gerrit-parent</artifactId> - <version>2.1.4-SNAPSHOT</version> + <version>2.1-SNAPSHOT</version> </parent> <artifactId>gerrit-common</artifactId> diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ListBranchesResult.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ListBranchesResult.java index 56c31ad6b4..7341c47425 100644 --- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ListBranchesResult.java +++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ListBranchesResult.java @@ -19,35 +19,33 @@ import com.google.gerrit.reviewdb.Branch; import java.util.List; /** - * It holds list of branches and boolean to indicate - * if it is allowed to add new branches. + * It holds list of branches and boolean to indicate if it is allowed to add new + * branches. */ public final class ListBranchesResult { + protected boolean noRepository; protected boolean canAdd; - protected List<Branch> branches; protected ListBranchesResult() { } - public ListBranchesResult(final List<Branch> branches, boolean canAdd) { + public ListBranchesResult(List<Branch> branches, boolean canAdd, + boolean noRepository) { this.branches = branches; this.canAdd = canAdd; + this.noRepository = noRepository; } - public boolean getCanAdd() { - return canAdd; + public boolean getNoRepository() { + return noRepository; } - public void setCanAdd(boolean canAdd) { - this.canAdd = canAdd; + public boolean getCanAdd() { + return canAdd; } public List<Branch> getBranches() { return branches; } - - public void setBranches(List<Branch> branches) { - this.branches = branches; - } } diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java index 048d4408cb..e24405fe53 100644 --- a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java +++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java @@ -34,10 +34,16 @@ public class PatchScript { NONE, DIFF, IMG } + public static enum FileMode { + FILE, SYMLINK, GITLINK + } + protected Change.Key changeId; protected ChangeType changeType; protected String oldName; protected String newName; + protected FileMode oldMode; + protected FileMode newMode; protected List<String> header; protected AccountDiffPreference diffPrefs; protected SparseFileContent a; @@ -51,7 +57,8 @@ public class PatchScript { protected boolean intralineDifference; public PatchScript(final Change.Key ck, final ChangeType ct, final String on, - final String nn, final List<String> h, final AccountDiffPreference dp, + final String nn, final FileMode om, final FileMode nm, + final List<String> h, final AccountDiffPreference dp, final SparseFileContent ca, final SparseFileContent cb, final List<Edit> e, final DisplayMethod ma, final DisplayMethod mb, final CommentDetail cd, final List<Patch> hist, final boolean hf, @@ -60,6 +67,8 @@ public class PatchScript { changeType = ct; oldName = on; newName = nn; + oldMode = om; + newMode = nm; header = h; diffPrefs = dp; a = ca; @@ -88,6 +97,14 @@ public class PatchScript { return displayMethodB; } + public FileMode getFileModeA() { + return oldMode; + } + + public FileMode getFileModeB() { + return newMode; + } + public List<String> getPatchHeader() { return header; } diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ReviewerResult.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ReviewerResult.java index 19772fcf49..678ec79a7e 100644 --- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ReviewerResult.java +++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ReviewerResult.java @@ -50,6 +50,9 @@ public class ReviewerResult { /** Name supplied does not match to a registered account. */ ACCOUNT_NOT_FOUND, + /** The account is inactive. */ + ACCOUNT_INACTIVE, + /** The account is not permitted to see the change. */ CHANGE_NOT_VISIBLE, diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/SuggestService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/SuggestService.java index 164df439fb..9dae169830 100644 --- a/gerrit-common/src/main/java/com/google/gerrit/common/data/SuggestService.java +++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/SuggestService.java @@ -28,7 +28,7 @@ public interface SuggestService extends RemoteJsonService { void suggestProjectNameKey(String query, int limit, AsyncCallback<List<Project.NameKey>> callback); - void suggestAccount(String query, int limit, + void suggestAccount(String query, Boolean enabled, int limit, AsyncCallback<List<AccountInfo>> callback); void suggestAccountGroup(String query, int limit, diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/errors/InactiveAccountException.java b/gerrit-common/src/main/java/com/google/gerrit/common/errors/InactiveAccountException.java new file mode 100644 index 0000000000..6ae5eb6d5d --- /dev/null +++ b/gerrit-common/src/main/java/com/google/gerrit/common/errors/InactiveAccountException.java @@ -0,0 +1,26 @@ +// Copyright (C) 2010 The Android Open Source Project +// +// 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.google.gerrit.common.errors; + +/** Error indicating the account is currently inactive. */ +public class InactiveAccountException extends Exception { + private static final long serialVersionUID = 1L; + + public static final String MESSAGE = "Account Inactive: "; + + public InactiveAccountException(String who) { + super(MESSAGE + who); + } +} diff --git a/gerrit-gwtdebug/pom.xml b/gerrit-gwtdebug/pom.xml index c3a07a4178..88246ac97b 100644 --- a/gerrit-gwtdebug/pom.xml +++ b/gerrit-gwtdebug/pom.xml @@ -22,7 +22,7 @@ limitations under the License. <parent> <groupId>com.google.gerrit</groupId> <artifactId>gerrit-parent</artifactId> - <version>2.1.4-SNAPSHOT</version> + <version>2.1-SNAPSHOT</version> </parent> <artifactId>gerrit-gwtdebug</artifactId> diff --git a/gerrit-gwtui/pom.xml b/gerrit-gwtui/pom.xml index 29c5b01cfd..9a27d12bea 100644 --- a/gerrit-gwtui/pom.xml +++ b/gerrit-gwtui/pom.xml @@ -22,7 +22,7 @@ limitations under the License. <parent> <groupId>com.google.gerrit</groupId> <artifactId>gerrit-parent</artifactId> - <version>2.1.4-SNAPSHOT</version> + <version>2.1-SNAPSHOT</version> </parent> <artifactId>gerrit-gwtui</artifactId> diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java index 281c72b696..81a9dd5df3 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java @@ -16,6 +16,7 @@ package com.google.gerrit.client; import com.google.gerrit.common.data.AccountInfo; import com.google.gerrit.reviewdb.Account; +import com.google.gerrit.reviewdb.AccountGeneralPreferences; import com.google.gwt.i18n.client.DateTimeFormat; import java.util.Date; @@ -24,13 +25,29 @@ import java.util.Date; public class FormatUtil { private static final long ONE_YEAR = 182L * 24 * 60 * 60 * 1000; - private static final DateTimeFormat sTime = - DateTimeFormat.getShortTimeFormat(); - private static final DateTimeFormat sDate = DateTimeFormat.getFormat("MMM d"); - private static final DateTimeFormat mDate = - DateTimeFormat.getMediumDateFormat(); - private static final DateTimeFormat dtfmt = - DateTimeFormat.getFormat(mDate.getPattern() + " " + sTime.getPattern()); + private static DateTimeFormat sTime = DateTimeFormat.getShortTimeFormat(); + private static DateTimeFormat sDate = DateTimeFormat.getFormat("MMM d"); + private static DateTimeFormat mDate = DateTimeFormat.getMediumDateFormat(); + private static DateTimeFormat dtfmt; + + public static void setPreferences(AccountGeneralPreferences pref) { + if (pref == null) { + if (Gerrit.isSignedIn()) { + pref = Gerrit.getUserAccount().getGeneralPreferences(); + } else { + pref = new AccountGeneralPreferences(); + pref.resetToDefaults(); + } + } + + String fmt_sTime = pref.getTimeFormat().getFormat(); + String fmt_mDate = pref.getDateFormat().getLongFormat(); + + sTime = DateTimeFormat.getFormat(fmt_sTime); + sDate = DateTimeFormat.getFormat(pref.getDateFormat().getShortFormat()); + mDate = DateTimeFormat.getFormat(fmt_mDate); + dtfmt = DateTimeFormat.getFormat(fmt_mDate + " " + fmt_sTime); + } /** Format a date using a really short format. */ public static String shortFormat(Date dt) { @@ -38,6 +55,7 @@ public class FormatUtil { return ""; } + ensureInited(); final Date now = new Date(); dt = new Date(dt.getTime()); if (mDate.format(now).equals(mDate.format(dt))) { @@ -62,9 +80,16 @@ public class FormatUtil { if (dt == null) { return ""; } + ensureInited(); return dtfmt.format(new Date(dt.getTime())); } + private static void ensureInited() { + if (dtfmt == null) { + setPreferences(null); + } + } + /** Format an account as a name and email address. */ public static String nameEmail(final Account acct) { return nameEmail(new AccountInfo(acct)); diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java index 7bee0af174..d5ccdf9e10 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java @@ -481,6 +481,7 @@ public class Gerrit implements EntryPoint { switch (cfg.getAuthType()) { case HTTP: case HTTP_LDAP: + case CLIENT_SSL_CERT_LDAP: break; case OPENID: @@ -526,6 +527,7 @@ public class Gerrit implements EntryPoint { if (siteFooter != null) { siteFooter.setVisible(p.isShowSiteHeader()); } + FormatUtil.setPreferences(myAccount.getGeneralPreferences()); } } diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java index 3f263d23fd..b32a32d061 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.java @@ -46,6 +46,8 @@ public interface GerritConstants extends Constants { String nameAlreadyUsedBody(); String noSuchAccountTitle(); + String inactiveAccountBody(); + String menuAll(); String menuAllOpen(); String menuAllMerged(); diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties index 83a06e47de..33630150d3 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritConstants.properties @@ -29,6 +29,8 @@ notFoundBody = The page you requested was not found. nameAlreadyUsedBody = The name is already in use. noSuchAccountTitle = Code Review - Unknown User +inactiveAccountBody = This user is currently inactive. + menuAll = All menuAllOpen = Open menuAllMerged = Merged diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java index dd684790d8..ca9f248fcd 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java @@ -67,6 +67,7 @@ public interface GerritCss extends CssResource { String commentPanelHeader(); String commentPanelLast(); String commentPanelMessage(); + String commentPanelMenuBar(); String commentPanelSummary(); String commentPanelSummaryCell(); String complexHeader(); @@ -105,6 +106,7 @@ public interface GerritCss extends CssResource { String fileLineCONTEXT(); String fileLineDELETE(); String fileLineINSERT(); + String fileLineMode(); String fileLineNone(); String filePathCell(); String gerritTopMenu(); diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchPanel.java index 2d3d2e8cfb..db5094d6df 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchPanel.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchPanel.java @@ -15,14 +15,11 @@ package com.google.gerrit.client; import com.google.gerrit.client.changes.QueryScreen; +import com.google.gerrit.client.ui.HintTextBox; import com.google.gerrit.common.PageLinks; import com.google.gerrit.reviewdb.Change; -import com.google.gwt.event.dom.client.BlurEvent; -import com.google.gwt.event.dom.client.BlurHandler; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; -import com.google.gwt.event.dom.client.FocusEvent; -import com.google.gwt.event.dom.client.FocusHandler; import com.google.gwt.event.dom.client.KeyCodes; import com.google.gwt.event.dom.client.KeyPressEvent; import com.google.gwt.event.dom.client.KeyPressHandler; @@ -35,7 +32,7 @@ import com.google.gwtexpui.globalkey.client.KeyCommand; import com.google.gwtexpui.globalkey.client.NpTextBox; class SearchPanel extends Composite { - private final NpTextBox searchBox; + private final HintTextBox searchBox; private HandlerRegistration regFocus; SearchPanel() { @@ -43,39 +40,14 @@ class SearchPanel extends Composite { initWidget(body); setStyleName(Gerrit.RESOURCES.css().searchPanel()); - searchBox = new NpTextBox(); + searchBox = new HintTextBox(); searchBox.setVisibleLength(70); - searchBox.setText(Gerrit.C.searchHint()); - searchBox.addStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint()); - searchBox.addFocusHandler(new FocusHandler() { - @Override - public void onFocus(FocusEvent event) { - if (Gerrit.C.searchHint().equals(searchBox.getText())) { - searchBox.setText(""); - searchBox.removeStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint()); - } - } - }); - searchBox.addBlurHandler(new BlurHandler() { - @Override - public void onBlur(BlurEvent event) { - if ("".equals(searchBox.getText())) { - searchBox.setText(Gerrit.C.searchHint()); - searchBox.addStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint()); - } - } - }); + searchBox.setHintText(Gerrit.C.searchHint()); searchBox.addKeyPressHandler(new KeyPressHandler() { @Override public void onKeyPress(final KeyPressEvent event) { - switch (event.getCharCode()) { - case KeyCodes.KEY_ENTER: - doSearch(); - break; - case KeyCodes.KEY_ESCAPE: - searchBox.setText(""); - searchBox.setFocus(false); - break; + if (event.getCharCode() == KeyCodes.KEY_ENTER) { + doSearch(); } } }); @@ -93,13 +65,7 @@ class SearchPanel extends Composite { } void setText(final String query) { - if (query == null || query.equals("")) { - searchBox.setText(Gerrit.C.searchHint()); - searchBox.addStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint()); - } else { - searchBox.setText(query); - searchBox.removeStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint()); - } + searchBox.setText(query); } @Override @@ -129,7 +95,7 @@ class SearchPanel extends Composite { private void doSearch() { final String query = searchBox.getText().trim(); - if (query.length() == 0 || Gerrit.C.searchHint().equals(query)) { + if ("".equals(query)) { return; } diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java index 3756bedf73..276fc27d19 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java @@ -25,10 +25,12 @@ public interface AccountConstants extends Constants { String accountId(); String maximumPageSizeFieldLabel(); + String dateFormatLabel(); String contextWholeFile(); String showSiteHeader(); String useFlashClipboard(); String copySelfOnEmails(); + String displayPatchSetsInReverseOrder(); String buttonSaveChanges(); String tabAccountSummary(); @@ -81,6 +83,10 @@ public interface AccountConstants extends Constants { String buttonWatchProject(); String defaultProjectName(); String defaultFilter(); + String buttonBrowseProjects(); + String projects(); + String projectsClose(); + String projectListOpen(); String watchedProjectName(); String watchedProjectFilter(); String watchedProjectColumnEmailNotifications(); diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties index 29ee14af9a..a5a652f34e 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties @@ -7,8 +7,10 @@ accountId = Account ID showSiteHeader = Show Site Header useFlashClipboard = Use Flash Clipboard Widget copySelfOnEmails = CC Me On Comments I Write +displayPatchSetsInReverseOrder = Display Patch Sets In Reverse Order defaultContextFieldLabel = Default Context: maximumPageSizeFieldLabel = Maximum Page Size: +dateFormatLabel = Date/Time Format: contextWholeFile = Whole File buttonSaveChanges = Save Changes @@ -62,6 +64,10 @@ sshJavaAppletNotAvailable = Open Key Unavailable: Java not enabled buttonWatchProject = Watch defaultProjectName = Project Name defaultFilter = branch:name, or other search expression +projects = All Watchable Projects +projectsClose = Close +buttonBrowseProjects = Browse +projectListOpen = Watch Selected project watchedProjectName = Project Name watchedProjectFilter = Only If watchedProjectColumnEmailNotifications = Email Notifications diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelFull.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelFull.java index 3742beda92..b7a00ad506 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelFull.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelFull.java @@ -15,7 +15,7 @@ package com.google.gerrit.client.account; import com.google.gerrit.client.Gerrit; -import com.google.gerrit.client.ui.TextSaveButtonListener; +import com.google.gerrit.client.ui.OnEditEnabler; import com.google.gerrit.reviewdb.Account; import com.google.gerrit.reviewdb.ContactInformation; import com.google.gwt.user.client.ui.Grid; @@ -80,11 +80,11 @@ class ContactPanelFull extends ContactPanelShort { infoSecure.getCellFormatter().addStyleName(0, 1, Gerrit.RESOURCES.css().topmost()); infoSecure.getCellFormatter().addStyleName(3, 0, Gerrit.RESOURCES.css().bottomheader()); - final TextSaveButtonListener sbl = new TextSaveButtonListener(save); - addressTxt.addKeyPressHandler(sbl); - countryTxt.addKeyPressHandler(sbl); - phoneTxt.addKeyPressHandler(sbl); - faxTxt.addKeyPressHandler(sbl); + final OnEditEnabler sbl = new OnEditEnabler(save); + sbl.listenTo(addressTxt); + sbl.listenTo(countryTxt); + sbl.listenTo(phoneTxt); + sbl.listenTo(faxTxt); } @Override diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java index 21c9163f69..8fe4d5fe6f 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java @@ -16,7 +16,7 @@ package com.google.gerrit.client.account; import com.google.gerrit.client.Gerrit; import com.google.gerrit.client.rpc.GerritCallback; -import com.google.gerrit.client.ui.TextSaveButtonListener; +import com.google.gerrit.client.ui.OnEditEnabler; import com.google.gerrit.reviewdb.Account; import com.google.gerrit.reviewdb.AccountExternalId; import com.google.gerrit.reviewdb.ContactInformation; @@ -120,9 +120,8 @@ class ContactPanelShort extends Composite { doSave(); } }); + new OnEditEnabler(save, nameTxt); - final TextSaveButtonListener sbl = new TextSaveButtonListener(save); - nameTxt.addKeyPressHandler(sbl); emailPick.addChangeHandler(new ChangeHandler() { @Override public void onChange(final ChangeEvent event) { diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGroupsScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGroupsScreen.java index b9fff040d4..437d438fb6 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGroupsScreen.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGroupsScreen.java @@ -26,7 +26,7 @@ public class MyGroupsScreen extends SettingsScreen { @Override protected void onInitUI() { super.onInitUI(); - groups = new GroupTable(false /* do not hyperlink to admin */); + groups = new GroupTable(true /* hyperlink to admin */); add(groups); } diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java index bb81f3f85f..3f32195eed 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java @@ -26,18 +26,25 @@ import com.google.gwt.event.dom.client.ChangeEvent; import com.google.gwt.event.dom.client.ChangeHandler; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.i18n.client.DateTimeFormat; import com.google.gwt.i18n.client.LocaleInfo; import com.google.gwt.user.client.ui.Button; import com.google.gwt.user.client.ui.CheckBox; +import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.Grid; import com.google.gwt.user.client.ui.ListBox; import com.google.gwtjsonrpc.client.VoidResult; +import java.util.Date; + public class MyPreferencesScreen extends SettingsScreen { private CheckBox showSiteHeader; private CheckBox useFlashClipboard; private CheckBox copySelfOnEmails; + private CheckBox displayPatchSetsInReverseOrder; private ListBox maximumPageSize; + private ListBox dateFormat; + private ListBox timeFormat; private Button save; @Override @@ -66,21 +73,51 @@ public class MyPreferencesScreen extends SettingsScreen { copySelfOnEmails = new CheckBox(Util.C.copySelfOnEmails()); copySelfOnEmails.addClickHandler(onClickSave); + displayPatchSetsInReverseOrder = new CheckBox(Util.C.displayPatchSetsInReverseOrder()); + displayPatchSetsInReverseOrder.addClickHandler(onClickSave); + maximumPageSize = new ListBox(); for (final short v : PAGESIZE_CHOICES) { maximumPageSize.addItem(Util.M.rowsPerPage(v), String.valueOf(v)); } maximumPageSize.addChangeHandler(onChangeSave); + Date now = new Date(); + dateFormat = new ListBox(); + for (AccountGeneralPreferences.DateFormat fmt : AccountGeneralPreferences.DateFormat + .values()) { + StringBuilder r = new StringBuilder(); + r.append(DateTimeFormat.getFormat(fmt.getShortFormat()).format(now)); + r.append(" ; "); + r.append(DateTimeFormat.getFormat(fmt.getLongFormat()).format(now)); + dateFormat.addItem(r.toString(), fmt.name()); + } + dateFormat.addChangeHandler(onChangeSave); + + timeFormat = new ListBox(); + for (AccountGeneralPreferences.TimeFormat fmt : AccountGeneralPreferences.TimeFormat + .values()) { + StringBuilder r = new StringBuilder(); + r.append(DateTimeFormat.getFormat(fmt.getFormat()).format(now)); + timeFormat.addItem(r.toString(), fmt.name()); + } + timeFormat.addChangeHandler(onChangeSave); + + FlowPanel dateTimePanel = new FlowPanel(); + final int labelIdx, fieldIdx; if (LocaleInfo.getCurrentLocale().isRTL()) { labelIdx = 1; fieldIdx = 0; + dateTimePanel.add(timeFormat); + dateTimePanel.add(dateFormat); } else { labelIdx = 0; fieldIdx = 1; + dateTimePanel.add(dateFormat); + dateTimePanel.add(timeFormat); } - final Grid formGrid = new Grid(4, 2); + final Grid formGrid = new Grid(6, 2); int row = 0; formGrid.setText(row, labelIdx, ""); @@ -95,10 +132,18 @@ public class MyPreferencesScreen extends SettingsScreen { formGrid.setWidget(row, fieldIdx, copySelfOnEmails); row++; + formGrid.setText(row, labelIdx, ""); + formGrid.setWidget(row, fieldIdx, displayPatchSetsInReverseOrder); + row++; + formGrid.setText(row, labelIdx, Util.C.maximumPageSizeFieldLabel()); formGrid.setWidget(row, fieldIdx, maximumPageSize); row++; + formGrid.setText(row, labelIdx, Util.C.dateFormatLabel()); + formGrid.setWidget(row, fieldIdx, dateTimePanel); + row++; + add(formGrid); save = new Button(Util.C.buttonSaveChanges()); @@ -126,21 +171,40 @@ public class MyPreferencesScreen extends SettingsScreen { showSiteHeader.setEnabled(on); useFlashClipboard.setEnabled(on); copySelfOnEmails.setEnabled(on); + displayPatchSetsInReverseOrder.setEnabled(on); maximumPageSize.setEnabled(on); + dateFormat.setEnabled(on); + timeFormat.setEnabled(on); } private void display(final AccountGeneralPreferences p) { showSiteHeader.setValue(p.isShowSiteHeader()); useFlashClipboard.setValue(p.isUseFlashClipboard()); copySelfOnEmails.setValue(p.isCopySelfOnEmails()); + displayPatchSetsInReverseOrder.setValue(p.isDisplayPatchSetsInReverseOrder()); setListBox(maximumPageSize, DEFAULT_PAGESIZE, p.getMaximumPageSize()); + setListBox(dateFormat, AccountGeneralPreferences.DateFormat.STD, // + p.getDateFormat()); + setListBox(timeFormat, AccountGeneralPreferences.TimeFormat.HHMM_12, // + p.getTimeFormat()); } private void setListBox(final ListBox f, final short defaultValue, final short currentValue) { + setListBox(f, String.valueOf(defaultValue), String.valueOf(currentValue)); + } + + private <T extends Enum<?>> void setListBox(final ListBox f, + final T defaultValue, final T currentValue) { + setListBox(f, defaultValue.name(), // + currentValue != null ? currentValue.name() : ""); + } + + private void setListBox(final ListBox f, final String defaultValue, + final String currentValue) { final int n = f.getItemCount(); for (int i = 0; i < n; i++) { - if (Short.parseShort(f.getValue(i)) == currentValue) { + if (f.getValue(i).equals(currentValue)) { f.setSelectedIndex(i); return; } @@ -158,12 +222,33 @@ public class MyPreferencesScreen extends SettingsScreen { return defaultValue; } + private <T extends Enum<?>> T getListBox(final ListBox f, + final T defaultValue, T[] all) { + final int idx = f.getSelectedIndex(); + if (0 <= idx) { + String v = f.getValue(idx); + for (T t : all) { + if (t.name().equals(v)) { + return t; + } + } + } + return defaultValue; + } + private void doSave() { final AccountGeneralPreferences p = new AccountGeneralPreferences(); p.setShowSiteHeader(showSiteHeader.getValue()); p.setUseFlashClipboard(useFlashClipboard.getValue()); p.setCopySelfOnEmails(copySelfOnEmails.getValue()); + p.setDisplayPatchSetsInReverseOrder(displayPatchSetsInReverseOrder.getValue()); p.setMaximumPageSize(getListBox(maximumPageSize, DEFAULT_PAGESIZE)); + p.setDateFormat(getListBox(dateFormat, + AccountGeneralPreferences.DateFormat.STD, + AccountGeneralPreferences.DateFormat.values())); + p.setTimeFormat(getListBox(timeFormat, + AccountGeneralPreferences.TimeFormat.HHMM_12, + AccountGeneralPreferences.TimeFormat.values())); enable(false); save.setEnabled(false); diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchedProjectsScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchedProjectsScreen.java index 28099bf08f..224ff93050 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchedProjectsScreen.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchedProjectsScreen.java @@ -17,179 +17,303 @@ package com.google.gerrit.client.account; import com.google.gerrit.client.Gerrit; import com.google.gerrit.client.rpc.GerritCallback; import com.google.gerrit.client.rpc.ScreenLoadCallback; -import com.google.gerrit.client.ui.FancyFlexTable; -import com.google.gerrit.client.ui.ProjectLink; +import com.google.gerrit.client.ui.HintTextBox; import com.google.gerrit.client.ui.ProjectNameSuggestOracle; +import com.google.gerrit.client.ui.RPCSuggestOracle; +import com.google.gerrit.client.ui.ProjectsTable; +import com.google.gerrit.common.PageLinks; import com.google.gerrit.common.data.AccountProjectWatchInfo; -import com.google.gerrit.reviewdb.AccountProjectWatch; -import com.google.gerrit.reviewdb.Change.Status; -import com.google.gwt.event.dom.client.BlurEvent; -import com.google.gwt.event.dom.client.BlurHandler; +import com.google.gerrit.reviewdb.Project; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; -import com.google.gwt.event.dom.client.FocusEvent; -import com.google.gwt.event.dom.client.FocusHandler; import com.google.gwt.event.dom.client.KeyCodes; import com.google.gwt.event.dom.client.KeyPressEvent; import com.google.gwt.event.dom.client.KeyPressHandler; +import com.google.gwt.event.logical.shared.ResizeEvent; +import com.google.gwt.event.logical.shared.ResizeHandler; import com.google.gwt.event.logical.shared.SelectionEvent; import com.google.gwt.event.logical.shared.SelectionHandler; -import com.google.gwt.user.client.DOM; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.user.client.Window; import com.google.gwt.user.client.ui.Button; -import com.google.gwt.user.client.ui.CheckBox; import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.Grid; -import com.google.gwt.user.client.ui.Label; -import com.google.gwt.user.client.ui.SuggestBox; -import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter; import com.google.gwt.user.client.ui.HTMLTable.CellFormatter; +import com.google.gwt.user.client.ui.PopupPanel; +import com.google.gwt.user.client.ui.ScrollPanel; +import com.google.gwt.user.client.ui.SuggestBox; import com.google.gwt.user.client.ui.SuggestOracle.Suggestion; -import com.google.gwtexpui.globalkey.client.NpTextBox; -import com.google.gwtjsonrpc.client.VoidResult; +import com.google.gwtexpui.globalkey.client.GlobalKey; +import com.google.gwtexpui.globalkey.client.HidePopupPanelCommand; +import com.google.gwtexpui.user.client.PluginSafeDialogBox; -import java.util.HashSet; import java.util.List; -public class MyWatchedProjectsScreen extends SettingsScreen { - private WatchTable watches; - +public class MyWatchedProjectsScreen extends SettingsScreen implements + ResizeHandler { private Button addNew; - private NpTextBox nameBox; + private HintTextBox nameBox; private SuggestBox nameTxt; - private NpTextBox filterTxt; + private HintTextBox filterTxt; + private MyWatchesTable watchesTab; + private Button browse; + private PluginSafeDialogBox popup; + private Button close; + private ProjectsTable projectsTab; private Button delSel; + + private PopupPanel.PositionCallback popupPosition; + private HandlerRegistration regWindowResize; + + private int preferredPopupWidth = -1; + private boolean submitOnSelection; + private boolean firstPopupLoad = true; + private boolean popingUp; + + private ScrollPanel sp; @Override protected void onInitUI() { super.onInitUI(); + createWidgets(); - { - nameBox = new NpTextBox(); - nameTxt = new SuggestBox(new ProjectNameSuggestOracle(), nameBox); - nameBox.setVisibleLength(50); - nameBox.setText(Util.C.defaultProjectName()); - nameBox.addStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint()); - nameBox.addFocusHandler(new FocusHandler() { - @Override - public void onFocus(FocusEvent event) { - if (Util.C.defaultProjectName().equals(nameBox.getText())) { - nameBox.setText(""); - nameBox.removeStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint()); - } - } - }); - nameBox.addBlurHandler(new BlurHandler() { - @Override - public void onBlur(BlurEvent event) { - if ("".equals(nameBox.getText())) { - nameBox.setText(Util.C.defaultProjectName()); - nameBox.addStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint()); - } + + /* top table */ + + final Grid grid = new Grid(2, 2); + grid.setStyleName(Gerrit.RESOURCES.css().infoBlock()); + grid.setText(0, 0, Util.C.watchedProjectName()); + grid.setWidget(0, 1, nameTxt); + + grid.setText(1, 0, Util.C.watchedProjectFilter()); + grid.setWidget(1, 1, filterTxt); + + final CellFormatter fmt = grid.getCellFormatter(); + fmt.addStyleName(0, 0, Gerrit.RESOURCES.css().topmost()); + fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().topmost()); + fmt.addStyleName(0, 0, Gerrit.RESOURCES.css().header()); + fmt.addStyleName(1, 0, Gerrit.RESOURCES.css().header()); + fmt.addStyleName(1, 0, Gerrit.RESOURCES.css().bottomheader()); + + final FlowPanel fp = new FlowPanel(); + fp.setStyleName(Gerrit.RESOURCES.css().addWatchPanel()); + fp.add(grid); + fp.add(addNew); + fp.add(browse); + add(fp); + + + /* bottom table */ + + add(watchesTab); + add(delSel); + + + /* popup */ + + final FlowPanel pfp = new FlowPanel(); + sp = new ScrollPanel(projectsTab); + pfp.add(sp); + pfp.add(close); + popup.setWidget(pfp); + + popupPosition = new PopupPanel.PositionCallback() { + public void setPosition(int offsetWidth, int offsetHeight) { + if (preferredPopupWidth == -1) { + preferredPopupWidth = offsetWidth; } - }); - nameBox.addKeyPressHandler(new KeyPressHandler() { - @Override - public void onKeyPress(KeyPressEvent event) { - submitOnSelection = false; - if (event.getCharCode() == KeyCodes.KEY_ENTER) { - if (nameTxt.isSuggestionListShowing()) { - submitOnSelection = true; - } else { - doAddNew(); - } - } + int top = grid.getAbsoluteTop() - 50; // under page header + + // Try to place it to the right of everything else, but not + // right justified + int left = 5 + Math.max( + grid.getAbsoluteLeft() + grid.getOffsetWidth(), + watchesTab.getAbsoluteLeft() + watchesTab.getOffsetWidth() ); + + if (top + offsetHeight > Window.getClientHeight()) { + top = Window.getClientHeight() - offsetHeight; } - }); - nameTxt.addSelectionHandler(new SelectionHandler<Suggestion>() { - @Override - public void onSelection(SelectionEvent<Suggestion> event) { - if (submitOnSelection) { - submitOnSelection = false; - doAddNew(); - } + if (left + offsetWidth > Window.getClientWidth()) { + left = Window.getClientWidth() - offsetWidth; } - }); - - filterTxt = new NpTextBox(); - filterTxt.setVisibleLength(50); - filterTxt.setText(Util.C.defaultFilter()); - filterTxt.addStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint()); - filterTxt.addFocusHandler(new FocusHandler() { - @Override - public void onFocus(FocusEvent event) { - if (Util.C.defaultFilter().equals(filterTxt.getText())) { - filterTxt.setText(""); - filterTxt.removeStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint()); - } + + if (top < 0) { + sp.setHeight((sp.getOffsetHeight() + top) + "px"); + top = 0; } - }); - filterTxt.addBlurHandler(new BlurHandler() { - @Override - public void onBlur(BlurEvent event) { - if ("".equals(filterTxt.getText())) { - filterTxt.setText(Util.C.defaultFilter()); - filterTxt.addStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint()); - } + if (left < 0) { + sp.setWidth((sp.getOffsetWidth() + left) + "px"); + left = 0; } - }); - filterTxt.addKeyPressHandler(new KeyPressHandler() { - @Override - public void onKeyPress(KeyPressEvent event) { - if (event.getCharCode() == KeyCodes.KEY_ENTER) { + + popup.setPopupPosition(left, top); + } + }; + } + + @Override + public void onResize(final ResizeEvent event) { + sp.setSize("100%","100%"); + + // For some reason keeping track of preferredWidth keeps the width better, + // but using 100% for height works better. + popup.setHeight("100%"); + popupPosition.setPosition(preferredPopupWidth, popup.getOffsetHeight()); + } + + protected void createWidgets() { + nameBox = new HintTextBox(); + nameTxt = new SuggestBox(new ProjectNameSuggestOracle(), nameBox); + nameBox.setVisibleLength(50); + nameBox.setHintText(Util.C.defaultProjectName()); + nameBox.addKeyPressHandler(new KeyPressHandler() { + @Override + public void onKeyPress(KeyPressEvent event) { + submitOnSelection = false; + + if (event.getCharCode() == KeyCodes.KEY_ENTER) { + if (nameTxt.isSuggestionListShowing()) { + submitOnSelection = true; + } else { doAddNew(); } } - }); + } + }); + nameTxt.addSelectionHandler(new SelectionHandler<Suggestion>() { + @Override + public void onSelection(SelectionEvent<Suggestion> event) { + if (submitOnSelection) { + submitOnSelection = false; + doAddNew(); + } + } + }); - addNew = new Button(Util.C.buttonWatchProject()); - addNew.addClickHandler(new ClickHandler() { - @Override - public void onClick(final ClickEvent event) { + filterTxt = new HintTextBox(); + filterTxt.setVisibleLength(50); + filterTxt.setHintText(Util.C.defaultFilter()); + filterTxt.addKeyPressHandler(new KeyPressHandler() { + @Override + public void onKeyPress(KeyPressEvent event) { + if (event.getCharCode() == KeyCodes.KEY_ENTER) { doAddNew(); } - }); - - final Grid grid = new Grid(2, 2); - grid.setStyleName(Gerrit.RESOURCES.css().infoBlock()); - grid.setText(0, 0, Util.C.watchedProjectName()); - grid.setWidget(0, 1, nameTxt); - - grid.setText(1, 0, Util.C.watchedProjectFilter()); - grid.setWidget(1, 1, filterTxt); - - final CellFormatter fmt = grid.getCellFormatter(); - fmt.addStyleName(0, 0, Gerrit.RESOURCES.css().topmost()); - fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().topmost()); - fmt.addStyleName(0, 0, Gerrit.RESOURCES.css().header()); - fmt.addStyleName(1, 0, Gerrit.RESOURCES.css().header()); - fmt.addStyleName(1, 0, Gerrit.RESOURCES.css().bottomheader()); - - final FlowPanel fp = new FlowPanel(); - fp.setStyleName(Gerrit.RESOURCES.css().addWatchPanel()); - fp.add(grid); - fp.add(addNew); - add(fp); - } + } + }); + + addNew = new Button(Util.C.buttonWatchProject()); + addNew.addClickHandler(new ClickHandler() { + @Override + public void onClick(final ClickEvent event) { + doAddNew(); + } + }); + + projectsTab = new ProjectsTable() { + { + keysNavigation.add(new OpenKeyCommand(0, 'o', Util.C.projectListOpen())); + keysNavigation.add(new OpenKeyCommand(0, KeyCodes.KEY_ENTER, + Util.C.projectListOpen())); + } + + @Override + protected void movePointerTo(final int row, final boolean scroll) { + super.movePointerTo(row, scroll); - watches = new WatchTable(); - add(watches); + // prevent user input from being overwritten by simply poping up + if (! popingUp || "".equals(nameBox.getText()) ) { + nameBox.setText(getRowItem(row).getName()); + } + } + + @Override + protected void onOpenRow(final int row) { + super.onOpenRow(row); + nameBox.setText(getRowItem(row).getName()); + doAddNew(); + } + }; + projectsTab.setSavePointerId(PageLinks.SETTINGS_PROJECTS); + + close = new Button(Util.C.projectsClose()); + close.addClickHandler(new ClickHandler() { + @Override + public void onClick(final ClickEvent event) { + closePopup(); + } + }); + + popup = new PluginSafeDialogBox(); + popup.setModal(false); + popup.setText(Util.C.projects()); + + browse = new Button(Util.C.buttonBrowseProjects()); + browse.addClickHandler(new ClickHandler() { + @Override + public void onClick(final ClickEvent event) { + displayPopup(); + } + }); + + watchesTab = new MyWatchesTable(); delSel = new Button(Util.C.buttonDeleteSshKey()); delSel.addClickHandler(new ClickHandler() { @Override public void onClick(final ClickEvent event) { - watches.deleteChecked(); + watchesTab.deleteChecked(); } }); - add(delSel); } - void doAddNew() { + @Override + protected void onLoad() { + super.onLoad(); + populateWatches(); + } + + @Override + protected void onUnload() { + super.onUnload(); + closePopup(); + } + + protected void displayPopup() { + popingUp = true; + if (firstPopupLoad) { // For sizing/positioning, delay display until loaded + populateProjects(); + } else { + popup.setPopupPositionAndShow(popupPosition); + + GlobalKey.dialog(popup); + GlobalKey.addApplication(popup, new HidePopupPanelCommand(0, + KeyCodes.KEY_ESCAPE, popup)); + projectsTab.setRegisterKeys(true); + + projectsTab.finishDisplay(); + + if (regWindowResize == null) { + regWindowResize = Window.addResizeHandler(this); + } + + popingUp = false; + } + } + + protected void closePopup() { + popup.hide(); + if (regWindowResize != null) { + regWindowResize.removeHandler(); + regWindowResize = null; + } + } + + protected void doAddNew() { final String projectName = nameTxt.getText(); - if (projectName == null || projectName.length() == 0 - || Util.C.defaultProjectName().equals(projectName)) { + if ("".equals(projectName)) { return; } @@ -211,7 +335,7 @@ public class MyWatchedProjectsScreen extends SettingsScreen { filterTxt.setEnabled(true); nameTxt.setText(""); - watches.insertWatch(result); + watchesTab.insertWatch(result); } @Override @@ -225,190 +349,27 @@ public class MyWatchedProjectsScreen extends SettingsScreen { }); } - @Override - protected void onLoad() { - super.onLoad(); - Util.ACCOUNT_SVC - .myProjectWatch(new ScreenLoadCallback<List<AccountProjectWatchInfo>>( - this) { - public void preDisplay(final List<AccountProjectWatchInfo> result) { - watches.display(result); - } - }); - } - - private class WatchTable extends FancyFlexTable<AccountProjectWatchInfo> { - WatchTable() { - table.setWidth(""); - table.insertRow(1); - table.setText(0, 2, Util.C.watchedProjectName()); - table.setText(0, 3, Util.C.watchedProjectColumnEmailNotifications()); - - final FlexCellFormatter fmt = table.getFlexCellFormatter(); - fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().iconHeader()); - fmt.addStyleName(0, 2, Gerrit.RESOURCES.css().dataHeader()); - fmt.addStyleName(0, 3, Gerrit.RESOURCES.css().dataHeader()); - fmt.setRowSpan(0, 0, 2); - fmt.setRowSpan(0, 1, 2); - fmt.setRowSpan(0, 2, 2); - DOM.setElementProperty(fmt.getElement(0, 3), "align", "center"); - - fmt.setColSpan(0, 3, 3); - table.setText(1, 0, Util.C.watchedProjectColumnNewChanges()); - table.setText(1, 1, Util.C.watchedProjectColumnAllComments()); - table.setText(1, 2, Util.C.watchedProjectColumnSubmittedChanges()); - fmt.addStyleName(1, 0, Gerrit.RESOURCES.css().dataHeader()); - fmt.addStyleName(1, 1, Gerrit.RESOURCES.css().dataHeader()); - fmt.addStyleName(1, 2, Gerrit.RESOURCES.css().dataHeader()); - } - - void deleteChecked() { - final HashSet<AccountProjectWatch.Key> ids = - new HashSet<AccountProjectWatch.Key>(); - for (int row = 1; row < table.getRowCount(); row++) { - final AccountProjectWatchInfo k = getRowItem(row); - if (k != null && ((CheckBox) table.getWidget(row, 1)).getValue()) { - ids.add(k.getWatch().getKey()); - } - } - if (!ids.isEmpty()) { - Util.ACCOUNT_SVC.deleteProjectWatches(ids, - new GerritCallback<VoidResult>() { - public void onSuccess(final VoidResult result) { - for (int row = 1; row < table.getRowCount();) { - final AccountProjectWatchInfo k = getRowItem(row); - if (k != null && ids.contains(k.getWatch().getKey())) { - table.removeRow(row); - } else { - row++; - } - } - } - }); + protected void populateWatches() { + Util.ACCOUNT_SVC.myProjectWatch( + new ScreenLoadCallback<List<AccountProjectWatchInfo>>(this) { + @Override + public void preDisplay(final List<AccountProjectWatchInfo> result) { + watchesTab.display(result); } - } + }); + } - void insertWatch(final AccountProjectWatchInfo k) { - final String newName = k.getProject().getName(); - int row = 1; - for (; row < table.getRowCount(); row++) { - final AccountProjectWatchInfo i = getRowItem(row); - if (i != null && i.getProject().getName().compareTo(newName) >= 0) { - break; + protected void populateProjects() { + Util.PROJECT_SVC.visibleProjects( + new GerritCallback<List<Project>>() { + @Override + public void onSuccess(final List<Project> result) { + projectsTab.display(result); + if (firstPopupLoad) { // Display was delayed until table was loaded + firstPopupLoad = false; + displayPopup(); } } - - table.insertRow(row); - applyDataRowStyle(row); - populate(row, k); - } - - void display(final List<AccountProjectWatchInfo> result) { - while (2 < table.getRowCount()) - table.removeRow(table.getRowCount() - 1); - - for (final AccountProjectWatchInfo k : result) { - final int row = table.getRowCount(); - table.insertRow(row); - applyDataRowStyle(row); - populate(row, k); - } - } - - void populate(final int row, final AccountProjectWatchInfo k) { - final FlowPanel fp = new FlowPanel(); - fp.add(new ProjectLink(k.getProject().getNameKey(), Status.NEW)); - if (k.getWatch().getFilter() != null) { - Label filter = new Label(k.getWatch().getFilter()); - filter.setStyleName(Gerrit.RESOURCES.css().watchedProjectFilter()); - fp.add(filter); - } - - table.setWidget(row, 1, new CheckBox()); - table.setWidget(row, 2, fp); - { - final CheckBox notifyNewChanges = new CheckBox(); - notifyNewChanges.addClickHandler(new ClickHandler() { - @Override - public void onClick(final ClickEvent event) { - final boolean oldVal = k.getWatch().isNotifyNewChanges(); - k.getWatch().setNotifyNewChanges(notifyNewChanges.getValue()); - Util.ACCOUNT_SVC.updateProjectWatch(k.getWatch(), - new GerritCallback<VoidResult>() { - public void onSuccess(final VoidResult result) { - } - - @Override - public void onFailure(final Throwable caught) { - k.getWatch().setNotifyNewChanges(oldVal); - notifyNewChanges.setValue(oldVal); - super.onFailure(caught); - } - }); - } - }); - notifyNewChanges.setValue(k.getWatch().isNotifyNewChanges()); - table.setWidget(row, 3, notifyNewChanges); - } - { - final CheckBox notifyAllComments = new CheckBox(); - notifyAllComments.addClickHandler(new ClickHandler() { - @Override - public void onClick(final ClickEvent event) { - final boolean oldVal = k.getWatch().isNotifyAllComments(); - k.getWatch().setNotifyAllComments(notifyAllComments.getValue()); - Util.ACCOUNT_SVC.updateProjectWatch(k.getWatch(), - new GerritCallback<VoidResult>() { - public void onSuccess(final VoidResult result) { - } - - @Override - public void onFailure(final Throwable caught) { - k.getWatch().setNotifyAllComments(oldVal); - notifyAllComments.setValue(oldVal); - super.onFailure(caught); - } - }); - } - }); - notifyAllComments.setValue(k.getWatch().isNotifyAllComments()); - table.setWidget(row, 4, notifyAllComments); - } - { - final CheckBox notifySubmittedChanges = new CheckBox(); - notifySubmittedChanges.addClickHandler(new ClickHandler() { - @Override - public void onClick(final ClickEvent event) { - final boolean oldVal = k.getWatch().isNotifySubmittedChanges(); - k.getWatch().setNotifySubmittedChanges( - notifySubmittedChanges.getValue()); - Util.ACCOUNT_SVC.updateProjectWatch(k.getWatch(), - new GerritCallback<VoidResult>() { - public void onSuccess(final VoidResult result) { - } - - @Override - public void onFailure(final Throwable caught) { - k.getWatch().setNotifySubmittedChanges(oldVal); - notifySubmittedChanges.setValue(oldVal); - super.onFailure(caught); - } - }); - } - }); - notifySubmittedChanges - .setValue(k.getWatch().isNotifySubmittedChanges()); - table.setWidget(row, 5, notifySubmittedChanges); - } - - final FlexCellFormatter fmt = table.getFlexCellFormatter(); - fmt.addStyleName(row, 1, Gerrit.RESOURCES.css().iconCell()); - fmt.addStyleName(row, 2, Gerrit.RESOURCES.css().dataCell()); - fmt.addStyleName(row, 3, Gerrit.RESOURCES.css().dataCell()); - fmt.addStyleName(row, 4, Gerrit.RESOURCES.css().dataCell()); - fmt.addStyleName(row, 5, Gerrit.RESOURCES.css().dataCell()); - - setRowItem(row, k); - } + }); } } diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchesTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchesTable.java new file mode 100644 index 0000000000..be808fa57e --- /dev/null +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchesTable.java @@ -0,0 +1,178 @@ +// Copyright (C) 2010 The Android Open Source Project +// +// 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.google.gerrit.client.account; + +import com.google.gerrit.client.Gerrit; +import com.google.gerrit.client.rpc.GerritCallback; +import com.google.gerrit.client.ui.FancyFlexTable; +import com.google.gerrit.client.ui.ProjectLink; +import com.google.gerrit.common.data.AccountProjectWatchInfo; +import com.google.gerrit.reviewdb.AccountProjectWatch; +import com.google.gerrit.reviewdb.Change.Status; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.ui.CheckBox; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.Label; +import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter; +import com.google.gwtjsonrpc.client.VoidResult; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class MyWatchesTable extends FancyFlexTable<AccountProjectWatchInfo> { + + public MyWatchesTable() { + table.setWidth(""); + table.insertRow(1); + table.setText(0, 2, Util.C.watchedProjectName()); + table.setText(0, 3, Util.C.watchedProjectColumnEmailNotifications()); + + final FlexCellFormatter fmt = table.getFlexCellFormatter(); + fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().iconHeader()); + fmt.addStyleName(0, 2, Gerrit.RESOURCES.css().dataHeader()); + fmt.addStyleName(0, 3, Gerrit.RESOURCES.css().dataHeader()); + fmt.setRowSpan(0, 0, 2); + fmt.setRowSpan(0, 1, 2); + fmt.setRowSpan(0, 2, 2); + DOM.setElementProperty(fmt.getElement(0, 3), "align", "center"); + + fmt.setColSpan(0, 3, 3); + table.setText(1, 0, Util.C.watchedProjectColumnNewChanges()); + table.setText(1, 1, Util.C.watchedProjectColumnAllComments()); + table.setText(1, 2, Util.C.watchedProjectColumnSubmittedChanges()); + fmt.addStyleName(1, 0, Gerrit.RESOURCES.css().dataHeader()); + fmt.addStyleName(1, 1, Gerrit.RESOURCES.css().dataHeader()); + fmt.addStyleName(1, 2, Gerrit.RESOURCES.css().dataHeader()); + } + + public void deleteChecked() { + final Set<AccountProjectWatch.Key> ids = getCheckedIds(); + if (!ids.isEmpty()) { + Util.ACCOUNT_SVC.deleteProjectWatches(ids, + new GerritCallback<VoidResult>() { + public void onSuccess(final VoidResult result) { + remove(ids); + } + }); + } + } + + protected void remove(Set<AccountProjectWatch.Key> ids) { + for (int row = 1; row < table.getRowCount();) { + final AccountProjectWatchInfo k = getRowItem(row); + if (k != null && ids.contains(k.getWatch().getKey())) { + table.removeRow(row); + } else { + row++; + } + } + } + + protected Set<AccountProjectWatch.Key> getCheckedIds() { + final Set<AccountProjectWatch.Key> ids = + new HashSet<AccountProjectWatch.Key>(); + for (int row = 1; row < table.getRowCount(); row++) { + final AccountProjectWatchInfo k = getRowItem(row); + if (k != null && ((CheckBox) table.getWidget(row, 1)).getValue()) { + ids.add(k.getWatch().getKey()); + } + } + return ids; + } + + public void insertWatch(final AccountProjectWatchInfo k) { + final String newName = k.getProject().getName(); + int row = 1; + for (; row < table.getRowCount(); row++) { + final AccountProjectWatchInfo i = getRowItem(row); + if (i != null && i.getProject().getName().compareTo(newName) >= 0) { + break; + } + } + + table.insertRow(row); + applyDataRowStyle(row); + populate(row, k); + } + + public void display(final List<AccountProjectWatchInfo> result) { + while (2 < table.getRowCount()) + table.removeRow(table.getRowCount() - 1); + + for (final AccountProjectWatchInfo k : result) { + final int row = table.getRowCount(); + table.insertRow(row); + applyDataRowStyle(row); + populate(row, k); + } + } + + protected void populate(final int row, final AccountProjectWatchInfo info) { + final FlowPanel fp = new FlowPanel(); + fp.add(new ProjectLink(info.getProject().getNameKey(), Status.NEW)); + if (info.getWatch().getFilter() != null) { + Label filter = new Label(info.getWatch().getFilter()); + filter.setStyleName(Gerrit.RESOURCES.css().watchedProjectFilter()); + fp.add(filter); + } + + table.setWidget(row, 1, new CheckBox()); + table.setWidget(row, 2, fp); + + addNotifyButton(AccountProjectWatch.Type.NEW_CHANGES, info, row, 3); + addNotifyButton(AccountProjectWatch.Type.COMMENTS, info, row, 4); + addNotifyButton(AccountProjectWatch.Type.SUBMITS, info, row, 5); + + final FlexCellFormatter fmt = table.getFlexCellFormatter(); + fmt.addStyleName(row, 1, Gerrit.RESOURCES.css().iconCell()); + fmt.addStyleName(row, 2, Gerrit.RESOURCES.css().dataCell()); + fmt.addStyleName(row, 3, Gerrit.RESOURCES.css().dataCell()); + fmt.addStyleName(row, 4, Gerrit.RESOURCES.css().dataCell()); + fmt.addStyleName(row, 5, Gerrit.RESOURCES.css().dataCell()); + + setRowItem(row, info); + } + + protected void addNotifyButton(final AccountProjectWatch.Type type, + final AccountProjectWatchInfo info, final int row, final int col) { + final CheckBox cbox = new CheckBox(); + + cbox.addClickHandler(new ClickHandler() { + @Override + public void onClick(final ClickEvent event) { + final boolean oldVal = info.getWatch().isNotify(type); + info.getWatch().setNotify(type, cbox.getValue()); + Util.ACCOUNT_SVC.updateProjectWatch(info.getWatch(), + new GerritCallback<VoidResult>() { + public void onSuccess(final VoidResult result) { + } + + @Override + public void onFailure(final Throwable caught) { + info.getWatch().setNotify(type, oldVal); + cbox.setValue(oldVal); + super.onFailure(caught); + } + }); + } + }); + + cbox.setValue(info.getWatch().isNotify(type)); + table.setWidget(row, col, cbox); + } +} diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/NewAgreementScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/NewAgreementScreen.java index a0aeced39f..15b0e4ec86 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/NewAgreementScreen.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/NewAgreementScreen.java @@ -18,8 +18,8 @@ import com.google.gerrit.client.ErrorDialog; import com.google.gerrit.client.Gerrit; import com.google.gerrit.client.rpc.GerritCallback; import com.google.gerrit.client.ui.AccountScreen; +import com.google.gerrit.client.ui.OnEditEnabler; import com.google.gerrit.client.ui.SmallHeading; -import com.google.gerrit.client.ui.TextSaveButtonListener; import com.google.gerrit.common.PageLinks; import com.google.gerrit.common.data.AgreementInfo; import com.google.gerrit.reviewdb.AccountAgreement; @@ -144,7 +144,7 @@ public class NewAgreementScreen extends AccountScreen { }); finalGroup.add(submit); formBody.add(finalGroup); - new TextSaveButtonListener(yesIAgreeBox, submit); + new OnEditEnabler(submit, yesIAgreeBox); final FormPanel form = new FormPanel(); form.add(formBody); diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/UsernameField.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/UsernameField.java index 93ccb7431a..73e784bd6b 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/UsernameField.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/UsernameField.java @@ -17,7 +17,7 @@ package com.google.gerrit.client.account; import com.google.gerrit.client.ErrorDialog; import com.google.gerrit.client.Gerrit; import com.google.gerrit.client.rpc.GerritCallback; -import com.google.gerrit.client.ui.TextSaveButtonListener; +import com.google.gerrit.client.ui.OnEditEnabler; import com.google.gerrit.common.errors.InvalidUserNameException; import com.google.gerrit.reviewdb.Account; import com.google.gwt.event.dom.client.ClickEvent; @@ -73,7 +73,7 @@ class UsernameField extends Composite { doSetUserName(); } }); - new TextSaveButtonListener(userNameTxt, setUserName); + new OnEditEnabler(setUserName, userNameTxt); userNameLbl.setVisible(false); body.add(userNameLbl); diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/Util.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/Util.java index 5ed79e6716..59e439d57c 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/Util.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/Util.java @@ -16,6 +16,8 @@ package com.google.gerrit.client.account; import com.google.gerrit.common.data.AccountSecurity; import com.google.gerrit.common.data.AccountService; +import com.google.gerrit.common.data.ProjectAdminService; +import com.google.gerrit.reviewdb.Project; import com.google.gwt.core.client.GWT; import com.google.gwtjsonrpc.client.JsonUtil; @@ -24,6 +26,7 @@ public class Util { public static final AccountMessages M = GWT.create(AccountMessages.class); public static final AccountService ACCOUNT_SVC; public static final AccountSecurity ACCOUNT_SEC; + public static final ProjectAdminService PROJECT_SVC; static { ACCOUNT_SVC = GWT.create(AccountService.class); @@ -31,5 +34,8 @@ public class Util { ACCOUNT_SEC = GWT.create(AccountSecurity.class); JsonUtil.bind(ACCOUNT_SEC, "rpc/AccountSecurity"); + + PROJECT_SVC = GWT.create(ProjectAdminService.class); + JsonUtil.bind(PROJECT_SVC, "rpc/ProjectAdminService"); } } diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessRightEditor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessRightEditor.java new file mode 100644 index 0000000000..762cc47141 --- /dev/null +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessRightEditor.java @@ -0,0 +1,408 @@ +// Copyright (C) 2010 The Android Open Source Project +// +// 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.google.gerrit.client.admin; + +import com.google.gerrit.client.Gerrit; +import com.google.gerrit.client.rpc.GerritCallback; +import com.google.gerrit.client.ui.AccountGroupSuggestOracle; +import com.google.gerrit.client.ui.HintTextBox; +import com.google.gerrit.client.ui.RPCSuggestOracle; +import com.google.gerrit.common.data.ApprovalType; +import com.google.gerrit.common.data.ProjectDetail; +import com.google.gerrit.reviewdb.ApprovalCategory; +import com.google.gerrit.reviewdb.ApprovalCategoryValue; +import com.google.gerrit.reviewdb.AccountGroup; +import com.google.gerrit.reviewdb.Project; +import com.google.gerrit.reviewdb.RefRight; +import com.google.gwt.event.dom.client.BlurEvent; +import com.google.gwt.event.dom.client.BlurHandler; +import com.google.gwt.event.dom.client.ChangeEvent; +import com.google.gwt.event.dom.client.ChangeHandler; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.event.dom.client.FocusEvent; +import com.google.gwt.event.dom.client.FocusHandler; +import com.google.gwt.event.dom.client.KeyCodes; +import com.google.gwt.event.dom.client.KeyPressEvent; +import com.google.gwt.event.dom.client.KeyPressHandler; +import com.google.gwt.event.logical.shared.ValueChangeEvent; +import com.google.gwt.event.logical.shared.ValueChangeHandler; +import com.google.gwt.event.logical.shared.HasValueChangeHandlers; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.Composite; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.Grid; +import com.google.gwt.user.client.ui.ListBox; +import com.google.gwt.user.client.ui.SuggestBox; + +public class AccessRightEditor extends Composite + implements HasValueChangeHandlers<ProjectDetail> { + private Project.NameKey projectKey; + private ListBox catBox; + private HintTextBox nameTxt; + private SuggestBox nameSug; + private HintTextBox referenceTxt; + private ListBox topBox; + private ListBox botBox; + private Button addBut; + private Button clearBut; + + public AccessRightEditor(final Project.NameKey key) { + projectKey = key; + + initWidgets(); + initCategories(); + + final Grid grid = new Grid(5, 2); + grid.setText(0, 0, Util.C.columnApprovalCategory() + ":"); + grid.setWidget(0, 1, catBox); + + grid.setText(1, 0, Util.C.columnGroupName() + ":"); + grid.setWidget(1, 1, nameSug); + + grid.setText(2, 0, Util.C.columnRefName() + ":"); + grid.setWidget(2, 1, referenceTxt); + + grid.setText(3, 0, Util.C.columnRightRange() + ":"); + grid.setWidget(3, 1, topBox); + + grid.setText(4, 0, ""); + grid.setWidget(4, 1, botBox); + + FlowPanel fp = new FlowPanel(); + fp.setStyleName(Gerrit.RESOURCES.css().addSshKeyPanel()); + + fp.add(grid); + fp.add(addBut); + fp.add(clearBut); + initWidget(fp); + } + + protected void initWidgets() { + catBox = new ListBox(); + catBox.addChangeHandler(new ChangeHandler() { + @Override + public void onChange(final ChangeEvent event) { + updateCategorySelection(); + } + }); + + nameTxt = new HintTextBox(); + nameSug = new SuggestBox(new RPCSuggestOracle( + new AccountGroupSuggestOracle()), nameTxt); + nameTxt.setVisibleLength(50); + nameTxt.setHintText(Util.C.defaultAccountGroupName()); + + referenceTxt = new HintTextBox(); + referenceTxt.setVisibleLength(50); + referenceTxt.setText(""); + referenceTxt.addKeyPressHandler(new KeyPressHandler() { + @Override + public void onKeyPress(KeyPressEvent event) { + if (event.getCharCode() == KeyCodes.KEY_ENTER) { + doAddNewRight(); + } + } + }); + + topBox = new ListBox(); + botBox = new ListBox(); + + addBut = new Button(Util.C.buttonAddProjectRight()); + addBut.addClickHandler(new ClickHandler() { + @Override + public void onClick(final ClickEvent event) { + doAddNewRight(); + } + }); + + clearBut = new Button(Util.C.buttonClearProjectRight()); + clearBut.addClickHandler(new ClickHandler() { + @Override + public void onClick(final ClickEvent event) { + clear(); + } + }); + } + + protected void initCategories() { + for (final ApprovalType at : Gerrit.getConfig().getApprovalTypes() + .getApprovalTypes()) { + final ApprovalCategory c = at.getCategory(); + catBox.addItem(c.getName(), c.getId().get()); + } + for (final ApprovalType at : Gerrit.getConfig().getApprovalTypes() + .getActionTypes()) { + final ApprovalCategory c = at.getCategory(); + if (Gerrit.getConfig().getWildProject().equals(projectKey) + && ApprovalCategory.OWN.equals(c.getId())) { + // Giving out control of the WILD_PROJECT to other groups beyond + // Administrators is dangerous. Having control over WILD_PROJECT + // is about the same as having Administrator access as users are + // able to affect grants in all projects on the system. + // + continue; + } + catBox.addItem(c.getName(), c.getId().get()); + } + + if (catBox.getItemCount() > 0) { + catBox.setSelectedIndex(0); + updateCategorySelection(); + } + } + + public void enableForm(final boolean on) { + final boolean canAdd = on && catBox.getItemCount() > 0; + addBut.setEnabled(canAdd); + clearBut.setEnabled(canAdd); + nameTxt.setEnabled(canAdd); + referenceTxt.setEnabled(canAdd); + catBox.setEnabled(canAdd); + topBox.setEnabled(canAdd); + botBox.setEnabled(canAdd); + } + + public void clear() { + setCat(null); + setName(""); + setReference(""); + } + + public void load(final RefRight right, final AccountGroup group) { + final ApprovalType atype = + Gerrit.getConfig().getApprovalTypes().getApprovalType( + right.getApprovalCategoryId()); + + setCat(atype != null ? atype.getCategory().getName() + : right.getApprovalCategoryId().get() ); + + setName(group.getName()); + setReference(right.getRefPatternForDisplay()); + + setRange(atype.getCategory().isRange() ? atype.getValue(right.getMinValue()) + : null, atype.getValue(right.getMaxValue()) ); + } + + protected void doAddNewRight() { + final ApprovalType at = getApprovalType(); + ApprovalCategoryValue min = getMin(at); + ApprovalCategoryValue max = getMax(at); + + if (at == null || min == null || max == null) { + return; + } + + final String groupName = nameSug.getText(); + if ("".equals(groupName) + || Util.C.defaultAccountGroupName().equals(groupName)) { + return; + } + + final String refPattern = referenceTxt.getText(); + + addBut.setEnabled(false); + Util.PROJECT_SVC.addRight(projectKey, at.getCategory().getId(), + groupName, refPattern, min.getValue(), max.getValue(), + new GerritCallback<ProjectDetail>() { + public void onSuccess(final ProjectDetail result) { + addBut.setEnabled(true); + nameSug.setText(""); + referenceTxt.setText(""); + ValueChangeEvent.fire(AccessRightEditor.this, result); + } + + @Override + public void onFailure(final Throwable caught) { + addBut.setEnabled(true); + super.onFailure(caught); + } + }); + } + + protected void updateCategorySelection() { + final ApprovalType at = getApprovalType(); + + if (at == null || at.getValues().isEmpty()) { + topBox.setEnabled(false); + botBox.setEnabled(false); + referenceTxt.setEnabled(false); + addBut.setEnabled(false); + clearBut.setEnabled(false); + return; + } + + updateRanges(at); + } + + protected void updateRanges(final ApprovalType at) { + ApprovalCategoryValue min = null, max = null, last = null; + + topBox.clear(); + botBox.clear(); + + for(final ApprovalCategoryValue v : at.getValues()) { + final int nval = v.getValue(); + final String vStr = String.valueOf(nval); + + String nStr = vStr + ": " + v.getName(); + if (nval > 0) { + nStr = "+" + nStr; + } + + topBox.addItem(nStr, vStr); + botBox.addItem(nStr, vStr); + + if (min == null || nval < 0) { + min = v; + } else if (max == null && nval > 0) { + max = v; + } + last = v; + } + + if (max == null) { + max = last; + } + + if (ApprovalCategory.READ.equals(at.getCategory().getId())) { + // Special case; for READ the most logical range is just + // +1 READ, so assume that as the default for both. + min = max; + } + + if (! at.getCategory().isRange()) { + max = null; + } + + setRange(min, max); + } + + protected void setCat(final String cat) { + if (cat == null) { + catBox.setSelectedIndex(0); + } else { + setSelectedText(catBox, cat); + } + updateCategorySelection(); + } + + protected void setName(final String name) { + nameTxt.setText(name); + } + + protected void setReference(final String ref) { + referenceTxt.setText(ref); + } + + protected void setRange(final ApprovalCategoryValue min, + final ApprovalCategoryValue max) { + if (min == null || max == null) { + botBox.setVisible(false); + if (max != null) { + setSelectedValue(topBox, "" + max.getValue()); + return; + } + } else { + botBox.setVisible(true); + setSelectedValue(botBox, "" + max.getValue()); + } + setSelectedValue(topBox, "" + min.getValue()); + } + + private ApprovalType getApprovalType() { + int idx = catBox.getSelectedIndex(); + if (idx < 0) { + return null; + } + return Gerrit.getConfig().getApprovalTypes().getApprovalType( + new ApprovalCategory.Id(catBox.getValue(idx))); + } + + public ApprovalCategoryValue getMin(ApprovalType at) { + final ApprovalCategoryValue top = getTop(at); + final ApprovalCategoryValue bot = getBot(at); + if (bot == null) { + for (final ApprovalCategoryValue v : at.getValues()) { + if (0 <= v.getValue() && v.getValue() <= top.getValue()) { + return v; + } + } + return at.getMin(); + } + + if (top.getValue() > bot.getValue()) { + return bot; + } + return top; + } + + public ApprovalCategoryValue getMax(ApprovalType at) { + final ApprovalCategoryValue top = getTop(at); + final ApprovalCategoryValue bot = getBot(at); + if (bot == null || bot.getValue() < top.getValue()) { + return top; + } + return bot; + } + + protected ApprovalCategoryValue getTop(ApprovalType at) { + int idx = topBox.getSelectedIndex(); + if (idx < 0) { + return null; + } + return at.getValue(Short.parseShort(topBox.getValue(idx))); + } + + protected ApprovalCategoryValue getBot(ApprovalType at) { + int idx = botBox.getSelectedIndex(); + if (idx < 0 || ! botBox.isVisible()) { + return null; + } + return at.getValue(Short.parseShort(botBox.getValue(idx))); + } + + public HandlerRegistration addValueChangeHandler( + final ValueChangeHandler<ProjectDetail> handler) { + return addHandler(handler, ValueChangeEvent.getType()); + } + + public static boolean setSelectedText(ListBox box, String text) { + if (text == null) { + return false; + } + for (int i =0 ; i < box.getItemCount(); i++) { + if (text.equals(box.getItemText(i))) { + box.setSelectedIndex(i); + return true; + } + } + return false; + } + + public static boolean setSelectedValue(ListBox box, String value) { + if (value == null) { + return false; + } + for (int i =0 ; i < box.getItemCount(); i++) { + if (value.equals(box.getValue(i))) { + box.setSelectedIndex(i); + return true; + } + } + return false; + } +} diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupScreen.java index dd76875200..389f22b7b5 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupScreen.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupScreen.java @@ -22,8 +22,9 @@ import com.google.gerrit.client.ui.AccountGroupSuggestOracle; import com.google.gerrit.client.ui.AccountScreen; import com.google.gerrit.client.ui.AddMemberBox; import com.google.gerrit.client.ui.FancyFlexTable; +import com.google.gerrit.client.ui.OnEditEnabler; +import com.google.gerrit.client.ui.RPCSuggestOracle; import com.google.gerrit.client.ui.SmallHeading; -import com.google.gerrit.client.ui.TextSaveButtonListener; import com.google.gerrit.common.data.AccountInfoCache; import com.google.gerrit.common.data.GroupDetail; import com.google.gerrit.reviewdb.Account; @@ -134,7 +135,7 @@ public class AccountGroupScreen extends AccountScreen { groupNamePanel.add(saveName); add(groupNamePanel); - new TextSaveButtonListener(groupNameTxt, saveName); + new OnEditEnabler(saveName, groupNameTxt); } private void initOwner() { @@ -143,7 +144,8 @@ public class AccountGroupScreen extends AccountScreen { ownerTxtBox = new NpTextBox(); ownerTxtBox.setVisibleLength(60); - ownerTxt = new SuggestBox(new AccountGroupSuggestOracle(), ownerTxtBox); + ownerTxt = new SuggestBox(new RPCSuggestOracle( + new AccountGroupSuggestOracle()), ownerTxtBox); ownerPanel.add(ownerTxt); saveOwner = new Button(Util.C.buttonChangeGroupOwner()); @@ -165,7 +167,7 @@ public class AccountGroupScreen extends AccountScreen { ownerPanel.add(saveOwner); add(ownerPanel); - new TextSaveButtonListener(ownerTxtBox, saveOwner); + new OnEditEnabler(saveOwner, ownerTxtBox); } private void initDescription() { @@ -194,7 +196,7 @@ public class AccountGroupScreen extends AccountScreen { vp.add(saveDesc); add(vp); - new TextSaveButtonListener(descTxt, saveDesc); + new OnEditEnabler(saveDesc, descTxt); } private void initGroupType() { @@ -223,6 +225,7 @@ public class AccountGroupScreen extends AccountScreen { case HTTP_LDAP: case LDAP: case LDAP_BIND: + case CLIENT_SSL_CERT_LDAP: break; default: return; diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java index 20de3e336b..6f27ce5e99 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java @@ -31,14 +31,17 @@ public interface AdminConstants extends Constants { String buttonChangeGroupType(); String buttonSelectGroup(); String buttonAddProjectRight(); + String buttonClearProjectRight(); String buttonSaveChanges(); + String useContentMerge(); String useContributorAgreements(); String useSignedOffBy(); + String requireChangeID(); String headingOwner(); String headingParentProjectName(); String headingDescription(); - String headingSubmitType(); + String headingProjectOptions(); String headingGroupType(); String headingMembers(); String headingExternalGroup(); @@ -87,4 +90,5 @@ public interface AdminConstants extends Constants { String noGroupSelected(); String errorNoMatchingGroups(); + String errorNoGitRepository(); } diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties index 28917f1b8f..0874c37b74 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties @@ -12,14 +12,17 @@ buttonChangeGroupOwner = Change Owner buttonChangeGroupType = Change Type buttonSelectGroup = Select buttonAddProjectRight = Add Access Right +buttonClearProjectRight = Clear Form buttonSaveChanges = Save Changes +useContentMerge = Automatically resolve conflicts useContributorAgreements = Require a valid contributor agreement to upload useSignedOffBy = Require <a href="http://gerrit.googlecode.com/svn/documentation/2.0/user-signedoffby.html#Signed-off-by" target="_blank"><code>Signed-off-by</code></a> in commit message +requireChangeID = Require <a href="http://gerrit.googlecode.com/svn/documentation/2.0/user-changeid.html" target="_blank"><code>Change-Id</code></a> in commit message headingOwner = Owners headingParentProjectName = Rights Inherit From headingDescription = Description -headingSubmitType = Change Submit Action +headingProjectOptions = Project Options headingGroupType = Group Type headingMembers = Members headingExternalGroup = Selected External Group @@ -68,3 +71,4 @@ projectAdminTabAccess = Access noGroupSelected = (No group selected) errorNoMatchingGroups = No Matching Groups +errorNoGitRepository = No Git Repository diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java index 0586c8483d..9c51b77375 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java @@ -19,11 +19,16 @@ import com.google.gerrit.client.Gerrit; import com.google.gerrit.client.rpc.GerritCallback; import com.google.gerrit.client.rpc.ScreenLoadCallback; import com.google.gerrit.client.ui.AccountScreen; +import com.google.gerrit.client.ui.OnEditEnabler; import com.google.gerrit.client.ui.SmallHeading; import com.google.gerrit.common.PageLinks; import com.google.gerrit.reviewdb.AccountGroup; +import com.google.gwt.event.dom.client.BlurEvent; +import com.google.gwt.event.dom.client.BlurHandler; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.event.dom.client.FocusEvent; +import com.google.gwt.event.dom.client.FocusHandler; import com.google.gwt.event.dom.client.KeyCodes; import com.google.gwt.event.dom.client.KeyPressEvent; import com.google.gwt.event.dom.client.KeyPressHandler; @@ -78,14 +83,36 @@ public class GroupListScreen extends AccountScreen { fp.add(addTxt); addNew = new Button(Util.C.buttonCreateGroup()); + addNew.setEnabled(false); addNew.addClickHandler(new ClickHandler() { @Override public void onClick(final ClickEvent event) { doCreateGroup(); } }); + addNew.addFocusHandler(new FocusHandler() { + @Override + public void onFocus(FocusEvent event) { + // unregister the keys for the 'groups' table so that pressing ENTER + // when the 'addNew' button has the focus triggers the button (if the + // keys for the 'groups' table would not be unregistered the 'addNew' + // button would not be triggered on ENTER but the group which is + // selected in the 'groups' table would be opened) + groups.setRegisterKeys(false); + } + }); + addNew.addBlurHandler(new BlurHandler() { + @Override + public void onBlur(BlurEvent event) { + // re-register the keys for the 'groups' table when the 'addNew' button + // gets blurred + groups.setRegisterKeys(true); + } + }); fp.add(addNew); add(fp); + + new OnEditEnabler(addNew, addTxt); } @Override @@ -100,10 +127,17 @@ public class GroupListScreen extends AccountScreen { return; } + addNew.setEnabled(false); Util.GROUP_SVC.createGroup(newName, new GerritCallback<AccountGroup.Id>() { public void onSuccess(final AccountGroup.Id result) { History.newItem(Dispatcher.toAccountGroup(result)); } + + @Override + public void onFailure(Throwable caught) { + super.onFailure(caught); + addNew.setEnabled(true); + } }); } } diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.java index 14449f9cb8..5b863c76f0 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.java @@ -18,7 +18,6 @@ import com.google.gerrit.client.Dispatcher; import com.google.gerrit.client.Gerrit; import com.google.gerrit.client.rpc.GerritCallback; import com.google.gerrit.client.rpc.ScreenLoadCallback; -import com.google.gerrit.client.ui.AccountGroupSuggestOracle; import com.google.gerrit.client.ui.FancyFlexTable; import com.google.gerrit.client.ui.Hyperlink; import com.google.gerrit.client.ui.SmallHeading; @@ -27,31 +26,18 @@ import com.google.gerrit.common.data.GerritConfig; import com.google.gerrit.common.data.InheritedRefRight; import com.google.gerrit.common.data.ProjectDetail; import com.google.gerrit.reviewdb.AccountGroup; -import com.google.gerrit.reviewdb.ApprovalCategory; import com.google.gerrit.reviewdb.ApprovalCategoryValue; import com.google.gerrit.reviewdb.Project; import com.google.gerrit.reviewdb.RefRight; -import com.google.gwt.event.dom.client.BlurEvent; -import com.google.gwt.event.dom.client.BlurHandler; -import com.google.gwt.event.dom.client.ChangeEvent; -import com.google.gwt.event.dom.client.ChangeHandler; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; -import com.google.gwt.event.dom.client.FocusEvent; -import com.google.gwt.event.dom.client.FocusHandler; -import com.google.gwt.event.dom.client.KeyCodes; -import com.google.gwt.event.dom.client.KeyPressEvent; -import com.google.gwt.event.dom.client.KeyPressHandler; +import com.google.gwt.event.logical.shared.ValueChangeEvent; +import com.google.gwt.event.logical.shared.ValueChangeHandler; import com.google.gwt.user.client.ui.Button; import com.google.gwt.user.client.ui.CheckBox; -import com.google.gwt.user.client.ui.FlowPanel; -import com.google.gwt.user.client.ui.Grid; -import com.google.gwt.user.client.ui.ListBox; import com.google.gwt.user.client.ui.Panel; -import com.google.gwt.user.client.ui.SuggestBox; import com.google.gwt.user.client.ui.VerticalPanel; import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter; -import com.google.gwtexpui.globalkey.client.NpTextBox; import com.google.gwtexpui.safehtml.client.SafeHtml; import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder; @@ -65,14 +51,7 @@ public class ProjectAccessScreen extends ProjectScreen { private RightsTable rights; private Button delRight; - private Button addRight; - private ListBox catBox; - private ListBox rangeMinBox; - private ListBox rangeMaxBox; - private NpTextBox nameTxtBox; - private SuggestBox nameTxt; - private NpTextBox referenceTxt; - private FlowPanel addPanel; + private AccessRightEditor rightEditor; public ProjectAccessScreen(final Project.NameKey toShow) { super(toShow); @@ -99,14 +78,7 @@ public class ProjectAccessScreen extends ProjectScreen { private void enableForm(final boolean on) { delRight.setEnabled(on); - - final boolean canAdd = on && catBox.getItemCount() > 0; - addRight.setEnabled(canAdd); - nameTxtBox.setEnabled(canAdd); - referenceTxt.setEnabled(canAdd); - catBox.setEnabled(canAdd); - rangeMinBox.setEnabled(canAdd); - rangeMaxBox.setEnabled(canAdd); + rightEditor.enableForm(on); } private void initParent() { @@ -119,102 +91,6 @@ public class ProjectAccessScreen extends ProjectScreen { } private void initRights() { - addPanel = new FlowPanel(); - addPanel.setStyleName(Gerrit.RESOURCES.css().addSshKeyPanel()); - - final Grid addGrid = new Grid(5, 2); - - catBox = new ListBox(); - rangeMinBox = new ListBox(); - rangeMaxBox = new ListBox(); - - catBox.addChangeHandler(new ChangeHandler() { - @Override - public void onChange(final ChangeEvent event) { - updateCategorySelection(); - } - }); - for (final ApprovalType at : Gerrit.getConfig().getApprovalTypes() - .getApprovalTypes()) { - final ApprovalCategory c = at.getCategory(); - catBox.addItem(c.getName(), c.getId().get()); - } - for (final ApprovalType at : Gerrit.getConfig().getApprovalTypes() - .getActionTypes()) { - final ApprovalCategory c = at.getCategory(); - if (Gerrit.getConfig().getWildProject().equals(getProjectKey()) - && ApprovalCategory.OWN.equals(c.getId())) { - // Giving out control of the WILD_PROJECT to other groups beyond - // Administrators is dangerous. Having control over WILD_PROJECT - // is about the same as having Administrator access as users are - // able to affect grants in all projects on the system. - // - continue; - } - catBox.addItem(c.getName(), c.getId().get()); - } - - addGrid.setText(0, 0, Util.C.columnApprovalCategory() + ":"); - addGrid.setWidget(0, 1, catBox); - - nameTxtBox = new NpTextBox(); - nameTxt = new SuggestBox(new AccountGroupSuggestOracle(), nameTxtBox); - nameTxtBox.setVisibleLength(50); - nameTxtBox.setText(Util.C.defaultAccountGroupName()); - nameTxtBox.addStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint()); - nameTxtBox.addFocusHandler(new FocusHandler() { - @Override - public void onFocus(FocusEvent event) { - if (Util.C.defaultAccountGroupName().equals(nameTxtBox.getText())) { - nameTxtBox.setText(""); - nameTxtBox.removeStyleName(Gerrit.RESOURCES.css() - .inputFieldTypeHint()); - } - } - }); - nameTxtBox.addBlurHandler(new BlurHandler() { - @Override - public void onBlur(BlurEvent event) { - if ("".equals(nameTxtBox.getText())) { - nameTxtBox.setText(Util.C.defaultAccountGroupName()); - nameTxtBox.addStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint()); - } - } - }); - addGrid.setText(1, 0, Util.C.columnGroupName() + ":"); - addGrid.setWidget(1, 1, nameTxt); - - referenceTxt = new NpTextBox(); - referenceTxt.setVisibleLength(50); - referenceTxt.setText(""); - referenceTxt.addKeyPressHandler(new KeyPressHandler() { - @Override - public void onKeyPress(KeyPressEvent event) { - if (event.getCharCode() == KeyCodes.KEY_ENTER) { - doAddNewRight(); - } - } - }); - - addGrid.setText(2, 0, Util.C.columnRefName() + ":"); - addGrid.setWidget(2, 1, referenceTxt); - - addGrid.setText(3, 0, Util.C.columnRightRange() + ":"); - addGrid.setWidget(3, 1, rangeMinBox); - - addGrid.setText(4, 0, ""); - addGrid.setWidget(4, 1, rangeMaxBox); - - addRight = new Button(Util.C.buttonAddProjectRight()); - addRight.addClickHandler(new ClickHandler() { - @Override - public void onClick(final ClickEvent event) { - doAddNewRight(); - } - }); - addPanel.add(addGrid); - addPanel.add(addRight); - rights = new RightsTable(); delRight = new Button(Util.C.buttonDeleteGroupMembers()); @@ -226,15 +102,18 @@ public class ProjectAccessScreen extends ProjectScreen { } }); + rightEditor = new AccessRightEditor(getProjectKey()); + rightEditor.addValueChangeHandler(new ValueChangeHandler<ProjectDetail>() { + @Override + public void onValueChange(ValueChangeEvent<ProjectDetail> event) { + display(event.getValue()); + } + }); + add(new SmallHeading(Util.C.headingAccessRights())); add(rights); add(delRight); - add(addPanel); - - if (catBox.getItemCount() > 0) { - catBox.setSelectedIndex(0); - updateCategorySelection(); - } + add(rightEditor); } void display(final ProjectDetail result) { @@ -253,7 +132,7 @@ public class ProjectAccessScreen extends ProjectScreen { rights.display(result.groups, result.rights); - addPanel.setVisible(result.canModifyAccess); + rightEditor.setVisible(result.canModifyAccess); delRight.setVisible(rights.getCanDelete()); } @@ -270,144 +149,9 @@ public class ProjectAccessScreen extends ProjectScreen { } } - private void doAddNewRight() { - int idx = catBox.getSelectedIndex(); - final ApprovalType at; - ApprovalCategoryValue min, max; - if (idx < 0) { - return; - } - at = - Gerrit.getConfig().getApprovalTypes().getApprovalType( - new ApprovalCategory.Id(catBox.getValue(idx))); - if (at == null) { - return; - } - - idx = rangeMinBox.getSelectedIndex(); - if (idx < 0) { - return; - } - min = at.getValue(Short.parseShort(rangeMinBox.getValue(idx))); - if (min == null) { - return; - } - - if (at.getCategory().isRange()) { - idx = rangeMaxBox.getSelectedIndex(); - if (idx < 0) { - return; - } - max = at.getValue(Short.parseShort(rangeMaxBox.getValue(idx))); - if (max == null) { - return; - } - } else { - // If its not a range, the maximum box was disabled. Use the min - // value as the max, and select the min from the category values. - // - max = min; - min = at.getMin(); - for (ApprovalCategoryValue v : at.getValues()) { - if (0 <= v.getValue() && v.getValue() <= max.getValue()) { - min = v; - break; - } - } - } - - final String groupName = nameTxt.getText(); - if ("".equals(groupName) - || Util.C.defaultAccountGroupName().equals(groupName)) { - return; - } - - final String refPattern = referenceTxt.getText(); - - if (min.getValue() > max.getValue()) { - // If the user selects it backwards in the web UI, help them out - // by reversing the order to what we would expect. - // - final ApprovalCategoryValue newMin = max; - final ApprovalCategoryValue newMax = min; - min = newMin; - max = newMax; - } - - addRight.setEnabled(false); - Util.PROJECT_SVC.addRight(getProjectKey(), at.getCategory().getId(), - groupName, refPattern, min.getValue(), max.getValue(), - new GerritCallback<ProjectDetail>() { - public void onSuccess(final ProjectDetail result) { - addRight.setEnabled(true); - nameTxt.setText(""); - referenceTxt.setText(""); - display(result); - } - - @Override - public void onFailure(final Throwable caught) { - addRight.setEnabled(true); - super.onFailure(caught); - } - }); - } - - private void updateCategorySelection() { - final int idx = catBox.getSelectedIndex(); - final ApprovalType at; - if (idx >= 0) { - at = - Gerrit.getConfig().getApprovalTypes().getApprovalType( - new ApprovalCategory.Id(catBox.getValue(idx))); - } else { - at = null; - } - - if (at == null || at.getValues().isEmpty()) { - rangeMinBox.setEnabled(false); - rangeMaxBox.setEnabled(false); - referenceTxt.setEnabled(false); - addRight.setEnabled(false); - return; - } - - int curIndex = 0, minIndex = -1, maxIndex = -1; - rangeMinBox.clear(); - rangeMaxBox.clear(); - for (final ApprovalCategoryValue v : at.getValues()) { - final String vStr = String.valueOf(v.getValue()); - String nStr = vStr + ": " + v.getName(); - if (v.getValue() > 0) { - nStr = "+" + nStr; - } - - rangeMinBox.addItem(nStr, vStr); - rangeMaxBox.addItem(nStr, vStr); - - if (v.getValue() < 0) { - minIndex = curIndex; - } - if (maxIndex < 0 && v.getValue() > 0) { - maxIndex = curIndex; - } - - curIndex++; - } - if (ApprovalCategory.READ.equals(at.getCategory().getId())) { - // Special case; for READ the most logical range is just - // +1 READ, so assume that as the default for both. - minIndex = maxIndex; - } - rangeMinBox.setSelectedIndex(minIndex >= 0 ? minIndex : 0); - rangeMaxBox.setSelectedIndex(maxIndex >= 0 ? maxIndex : curIndex - 1); - rangeMaxBox.setVisible(at.getCategory().isRange()); - - addRight.setEnabled(true); - } - private class RightsTable extends FancyFlexTable<RefRight> { boolean canDelete; + Map<AccountGroup.Id, AccountGroup> groups; RightsTable() { table.setWidth(""); @@ -422,6 +166,13 @@ public class ProjectAccessScreen extends ProjectScreen { fmt.addStyleName(0, 3, Gerrit.RESOURCES.css().dataHeader()); fmt.addStyleName(0, 4, Gerrit.RESOURCES.css().dataHeader()); fmt.addStyleName(0, 5, Gerrit.RESOURCES.css().dataHeader()); + + table.addClickHandler(new ClickHandler() { + @Override + public void onClick(final ClickEvent event) { + onOpenRow(table.getCellForEvent(event).getRowIndex()); + } + }); } HashSet<RefRight.Key> getRefRightIdsChecked() { @@ -436,8 +187,9 @@ public class ProjectAccessScreen extends ProjectScreen { return refRightIds; } - void display(final Map<AccountGroup.Id, AccountGroup> groups, + void display(final Map<AccountGroup.Id, AccountGroup> grps, final List<InheritedRefRight> refRights) { + groups = grps; canDelete = false; while (1 < table.getRowCount()) @@ -447,12 +199,17 @@ public class ProjectAccessScreen extends ProjectScreen { final int row = table.getRowCount(); table.insertRow(row); applyDataRowStyle(row); - populate(row, groups, r); + populate(row, r); + } + } + protected void onOpenRow(final int row) { + if (row > 0) { + RefRight right = getRowItem(row); + rightEditor.load(right, groups.get(right.getAccountGroupId())); } } - void populate(final int row, final Map<AccountGroup.Id, AccountGroup> groups, - final InheritedRefRight r) { + void populate(final int row, final InheritedRefRight r) { final GerritConfig config = Gerrit.getConfig(); final RefRight right = r.getRight(); final ApprovalType ar = @@ -467,14 +224,12 @@ public class ProjectAccessScreen extends ProjectScreen { canDelete = true; } - if (ar != null) { - table.setText(row, 2, ar.getCategory().getName()); - } else { - table.setText(row, 2, right.getApprovalCategoryId().get()); - } + table.setText(row, 2, ar != null ? ar.getCategory().getName() + : right.getApprovalCategoryId().get() ); if (group != null) { - table.setText(row, 3, group.getName()); + table.setWidget(row, 3, new Hyperlink(group.getName(), Dispatcher + .toAccountGroup(group.getId()))); } else { table.setText(row, 3, Util.M.deletedGroup(right.getAccountGroupId() .get())); diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java index 8bff3e5ff5..77c431a1da 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java @@ -20,18 +20,15 @@ import com.google.gerrit.client.Gerrit; import com.google.gerrit.client.rpc.GerritCallback; import com.google.gerrit.client.rpc.ScreenLoadCallback; import com.google.gerrit.client.ui.FancyFlexTable; +import com.google.gerrit.client.ui.HintTextBox; import com.google.gerrit.common.data.GitwebLink; import com.google.gerrit.common.data.ListBranchesResult; import com.google.gerrit.common.errors.InvalidNameException; import com.google.gerrit.common.errors.InvalidRevisionException; import com.google.gerrit.reviewdb.Branch; import com.google.gerrit.reviewdb.Project; -import com.google.gwt.event.dom.client.BlurEvent; -import com.google.gwt.event.dom.client.BlurHandler; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; -import com.google.gwt.event.dom.client.FocusEvent; -import com.google.gwt.event.dom.client.FocusHandler; import com.google.gwt.event.dom.client.KeyCodes; import com.google.gwt.event.dom.client.KeyPressEvent; import com.google.gwt.event.dom.client.KeyPressHandler; @@ -43,8 +40,8 @@ import com.google.gwt.user.client.ui.CheckBox; import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.Grid; import com.google.gwt.user.client.ui.HTML; +import com.google.gwt.user.client.ui.Label; import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter; -import com.google.gwtexpui.globalkey.client.NpTextBox; import com.google.gwtjsonrpc.client.RemoteJsonException; import java.util.HashSet; @@ -55,8 +52,8 @@ public class ProjectBranchesScreen extends ProjectScreen { private BranchesTable branches; private Button delBranch; private Button addBranch; - private NpTextBox nameTxtBox; - private NpTextBox irevTxtBox; + private HintTextBox nameTxtBox; + private HintTextBox irevTxtBox; private FlowPanel addPanel; public ProjectBranchesScreen(final Project.NameKey toShow) { @@ -68,10 +65,22 @@ public class ProjectBranchesScreen extends ProjectScreen { super.onLoad(); Util.PROJECT_SVC.listBranches(getProjectKey(), new ScreenLoadCallback<ListBranchesResult>(this) { + @Override public void preDisplay(final ListBranchesResult result) { - enableForm(true); - display(result.getBranches()); - addPanel.setVisible(result.getCanAdd()); + if (result.getNoRepository()) { + branches.setVisible(false); + addPanel.setVisible(false); + delBranch.setVisible(false); + + Label no = new Label(Util.C.errorNoGitRepository()); + no.setStyleName(Gerrit.RESOURCES.css().smallHeading()); + add(no); + + } else { + enableForm(true); + display(result.getBranches()); + addPanel.setVisible(result.getCanAdd()); + } } }); } @@ -97,28 +106,9 @@ public class ProjectBranchesScreen extends ProjectScreen { final Grid addGrid = new Grid(2, 2); - nameTxtBox = new NpTextBox(); + nameTxtBox = new HintTextBox(); nameTxtBox.setVisibleLength(50); - nameTxtBox.setText(Util.C.defaultBranchName()); - nameTxtBox.addStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint()); - nameTxtBox.addFocusHandler(new FocusHandler() { - @Override - public void onFocus(FocusEvent event) { - if (Util.C.defaultBranchName().equals(nameTxtBox.getText())) { - nameTxtBox.setText(""); - nameTxtBox.removeStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint()); - } - } - }); - nameTxtBox.addBlurHandler(new BlurHandler() { - @Override - public void onBlur(BlurEvent event) { - if ("".equals(nameTxtBox.getText())) { - nameTxtBox.setText(Util.C.defaultBranchName()); - nameTxtBox.addStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint()); - } - } - }); + nameTxtBox.setHintText(Util.C.defaultBranchName()); nameTxtBox.addKeyPressHandler(new KeyPressHandler() { @Override public void onKeyPress(KeyPressEvent event) { @@ -130,28 +120,9 @@ public class ProjectBranchesScreen extends ProjectScreen { addGrid.setText(0, 0, Util.C.columnBranchName() + ":"); addGrid.setWidget(0, 1, nameTxtBox); - irevTxtBox = new NpTextBox(); + irevTxtBox = new HintTextBox(); irevTxtBox.setVisibleLength(50); - irevTxtBox.setText(Util.C.defaultRevisionSpec()); - irevTxtBox.addStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint()); - irevTxtBox.addFocusHandler(new FocusHandler() { - @Override - public void onFocus(FocusEvent event) { - if (Util.C.defaultRevisionSpec().equals(irevTxtBox.getText())) { - irevTxtBox.setText(""); - irevTxtBox.removeStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint()); - } - } - }); - irevTxtBox.addBlurHandler(new BlurHandler() { - @Override - public void onBlur(BlurEvent event) { - if ("".equals(irevTxtBox.getText())) { - irevTxtBox.setText(Util.C.defaultRevisionSpec()); - irevTxtBox.addStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint()); - } - } - }); + irevTxtBox.setHintText(Util.C.defaultRevisionSpec()); irevTxtBox.addKeyPressHandler(new KeyPressHandler() { @Override public void onKeyPress(KeyPressEvent event) { @@ -190,13 +161,13 @@ public class ProjectBranchesScreen extends ProjectScreen { private void doAddNewBranch() { String branchName = nameTxtBox.getText(); - if ("".equals(branchName) || Util.C.defaultBranchName().equals(branchName)) { + if ("".equals(branchName)) { nameTxtBox.setFocus(true); return; } String rev = irevTxtBox.getText(); - if ("".equals(rev) || Util.C.defaultRevisionSpec().equals(rev)) { + if ("".equals(rev)) { irevTxtBox.setText("HEAD"); DeferredCommand.addCommand(new Command() { @Override diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java index d308fa6c1a..22fd8d9935 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java @@ -17,16 +17,15 @@ package com.google.gerrit.client.admin; import com.google.gerrit.client.Gerrit; import com.google.gerrit.client.rpc.GerritCallback; import com.google.gerrit.client.rpc.ScreenLoadCallback; +import com.google.gerrit.client.ui.OnEditEnabler; import com.google.gerrit.client.ui.SmallHeading; -import com.google.gerrit.client.ui.TextSaveButtonListener; import com.google.gerrit.common.data.ProjectDetail; import com.google.gerrit.reviewdb.Project; +import com.google.gerrit.reviewdb.Project.SubmitType; import com.google.gwt.event.dom.client.ChangeEvent; import com.google.gwt.event.dom.client.ChangeHandler; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; -import com.google.gwt.event.logical.shared.ValueChangeEvent; -import com.google.gwt.event.logical.shared.ValueChangeHandler; import com.google.gwt.user.client.ui.Button; import com.google.gwt.user.client.ui.CheckBox; import com.google.gwt.user.client.ui.ListBox; @@ -37,8 +36,10 @@ import com.google.gwtexpui.globalkey.client.NpTextArea; public class ProjectInfoScreen extends ProjectScreen { private Project project; - private Panel submitTypePanel; + private Panel projectOptionsPanel; + private CheckBox requireChangeID; private ListBox submitType; + private CheckBox useContentMerge; private Panel agreementsPanel; private CheckBox useContributorAgreements; @@ -47,6 +48,8 @@ public class ProjectInfoScreen extends ProjectScreen { private NpTextArea descTxt; private Button saveProject; + private OnEditEnabler saveEnabler; + public ProjectInfoScreen(final Project.NameKey toShow) { super(toShow); } @@ -64,7 +67,7 @@ public class ProjectInfoScreen extends ProjectScreen { }); initDescription(); - initSubmitType(); + initProjectOptions(); initAgreements(); add(saveProject); } @@ -81,7 +84,6 @@ public class ProjectInfoScreen extends ProjectScreen { result.canModifyAgreements || result.canModifyDescription || result.canModifyMergeType); - saveProject.setEnabled(false); display(result); } }); @@ -90,11 +92,11 @@ public class ProjectInfoScreen extends ProjectScreen { private void enableForm(final boolean canModifyAgreements, final boolean canModifyDescription, final boolean canModifyMergeType) { submitType.setEnabled(canModifyMergeType); + useContentMerge.setEnabled(canModifyMergeType); descTxt.setEnabled(canModifyDescription); useContributorAgreements.setEnabled(canModifyAgreements); useSignedOffBy.setEnabled(canModifyAgreements); - saveProject.setEnabled( - canModifyAgreements || canModifyDescription || canModifyMergeType); + requireChangeID.setEnabled(canModifyMergeType); } private void initDescription() { @@ -107,12 +109,13 @@ public class ProjectInfoScreen extends ProjectScreen { vp.add(descTxt); add(vp); - new TextSaveButtonListener(descTxt, saveProject); + saveEnabler = new OnEditEnabler(saveProject); + saveEnabler.listenTo(descTxt); } - private void initSubmitType() { - submitTypePanel = new VerticalPanel(); - submitTypePanel.add(new SmallHeading(Util.C.headingSubmitType())); + private void initProjectOptions() { + projectOptionsPanel = new VerticalPanel(); + projectOptionsPanel.add(new SmallHeading(Util.C.headingProjectOptions())); submitType = new ListBox(); for (final Project.SubmitType type : Project.SubmitType.values()) { @@ -120,46 +123,66 @@ public class ProjectInfoScreen extends ProjectScreen { } submitType.addChangeHandler(new ChangeHandler() { @Override - public void onChange(final ChangeEvent event) { - saveProject.setEnabled(true); + public void onChange(ChangeEvent event) { + setEnabledForUseContentMerge(); } }); - submitTypePanel.add(submitType); - add(submitTypePanel); + saveEnabler.listenTo(submitType); + projectOptionsPanel.add(submitType); + + useContentMerge = new CheckBox(Util.C.useContentMerge(), true); + saveEnabler.listenTo(useContentMerge); + projectOptionsPanel.add(useContentMerge); + + requireChangeID = new CheckBox(Util.C.requireChangeID(), true); + saveEnabler.listenTo(requireChangeID); + projectOptionsPanel.add(requireChangeID); + + add(projectOptionsPanel); } - private void initAgreements() { - final ValueChangeHandler<Boolean> onChangeSave = - new ValueChangeHandler<Boolean>() { - @Override - public void onValueChange(ValueChangeEvent<Boolean> event) { - saveProject.setEnabled(true); - } - }; + /** + * Enables the {@link #useContentMerge} checkbox if the selected submit type + * allows the usage of content merge. + * If the submit type (currently only 'Fast Forward Only') does not allow + * content merge the useContentMerge checkbox gets disabled. + */ + private void setEnabledForUseContentMerge() { + if (SubmitType.FAST_FORWARD_ONLY.equals(Project.SubmitType + .valueOf(submitType.getValue(submitType.getSelectedIndex())))) { + useContentMerge.setEnabled(false); + useContentMerge.setValue(false); + } else { + useContentMerge.setEnabled(true); + } + } + private void initAgreements() { agreementsPanel = new VerticalPanel(); agreementsPanel.add(new SmallHeading(Util.C.headingAgreements())); useContributorAgreements = new CheckBox(Util.C.useContributorAgreements()); - useContributorAgreements.addValueChangeHandler(onChangeSave); + saveEnabler.listenTo(useContributorAgreements); agreementsPanel.add(useContributorAgreements); useSignedOffBy = new CheckBox(Util.C.useSignedOffBy(), true); - useSignedOffBy.addValueChangeHandler(onChangeSave); + saveEnabler.listenTo(useSignedOffBy); agreementsPanel.add(useSignedOffBy); add(agreementsPanel); } private void setSubmitType(final Project.SubmitType newSubmitType) { + int index = -1; if (submitType != null) { for (int i = 0; i < submitType.getItemCount(); i++) { if (newSubmitType.name().equals(submitType.getValue(i))) { - submitType.setSelectedIndex(i); - return; + index = i; + break; } } - submitType.setSelectedIndex(-1); + submitType.setSelectedIndex(index); + setEnabledForUseContentMerge(); } } @@ -168,7 +191,7 @@ public class ProjectInfoScreen extends ProjectScreen { final boolean isall = Gerrit.getConfig().getWildProject().equals(project.getNameKey()); - submitTypePanel.setVisible(!isall); + projectOptionsPanel.setVisible(!isall); agreementsPanel.setVisible(!isall); useContributorAgreements.setVisible(Gerrit.getConfig() .isUseContributorAgreements()); @@ -176,20 +199,25 @@ public class ProjectInfoScreen extends ProjectScreen { descTxt.setText(project.getDescription()); useContributorAgreements.setValue(project.isUseContributorAgreements()); useSignedOffBy.setValue(project.isUseSignedOffBy()); + useContentMerge.setValue(project.isUseContentMerge()); + requireChangeID.setValue(project.isRequireChangeID()); setSubmitType(project.getSubmitType()); + + saveProject.setEnabled(false); } private void doSave() { project.setDescription(descTxt.getText().trim()); project.setUseContributorAgreements(useContributorAgreements.getValue()); project.setUseSignedOffBy(useSignedOffBy.getValue()); + project.setUseContentMerge(useContentMerge.getValue()); + project.setRequireChangeID(requireChangeID.getValue()); if (submitType.getSelectedIndex() >= 0) { project.setSubmitType(Project.SubmitType.valueOf(submitType .getValue(submitType.getSelectedIndex()))); } enableForm(false, false, false); - saveProject.setEnabled(false); Util.PROJECT_SVC.changeProjectSettings(project, new GerritCallback<ProjectDetail>() { diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java index e676718ee3..758f4dea46 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java @@ -18,23 +18,18 @@ import com.google.gerrit.client.Dispatcher; import com.google.gerrit.client.Gerrit; import com.google.gerrit.client.rpc.ScreenLoadCallback; import com.google.gerrit.client.ui.Hyperlink; -import com.google.gerrit.client.ui.NavigationTable; +import com.google.gerrit.client.ui.ProjectsTable; import com.google.gerrit.client.ui.Screen; import com.google.gerrit.client.ui.SmallHeading; import com.google.gerrit.common.PageLinks; import com.google.gerrit.reviewdb.Project; -import com.google.gwt.event.dom.client.ClickEvent; -import com.google.gwt.event.dom.client.ClickHandler; -import com.google.gwt.event.dom.client.KeyCodes; import com.google.gwt.user.client.History; import com.google.gwt.user.client.ui.VerticalPanel; -import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter; -import com.google.gwt.user.client.ui.HTMLTable.Cell; import java.util.List; public class ProjectListScreen extends Screen { - private ProjectTable projects; + private ProjectsTable projects; @Override protected void onLoad() { @@ -53,7 +48,26 @@ public class ProjectListScreen extends Screen { super.onInitUI(); setPageTitle(Util.C.projectListTitle()); - projects = new ProjectTable(); + projects = new ProjectsTable() { + @Override + protected void onOpenRow(final int row) { + History.newItem(link(getRowItem(row))); + } + + private String link(final Project item) { + return Dispatcher.toProjectAdmin(item.getNameKey(), ProjectScreen.INFO); + } + + @Override + protected void populate(final int row, final Project k) { + table.setWidget(row, 1, new Hyperlink(k.getName(), link(k))); + table.setText(row, 2, k.getDescription()); + + setRowItem(row, k); + } + }; + projects.setSavePointerId(PageLinks.ADMIN_PROJECTS); + add(projects); final VerticalPanel fp = new VerticalPanel(); @@ -66,70 +80,4 @@ public class ProjectListScreen extends Screen { super.registerKeys(); projects.setRegisterKeys(true); } - - private class ProjectTable extends NavigationTable<Project> { - ProjectTable() { - setSavePointerId(PageLinks.ADMIN_PROJECTS); - keysNavigation.add(new PrevKeyCommand(0, 'k', Util.C.projectListPrev())); - keysNavigation.add(new NextKeyCommand(0, 'j', Util.C.projectListNext())); - keysNavigation.add(new OpenKeyCommand(0, 'o', Util.C.projectListOpen())); - keysNavigation.add(new OpenKeyCommand(0, KeyCodes.KEY_ENTER, Util.C - .projectListOpen())); - - table.setText(0, 1, Util.C.columnProjectName()); - table.setText(0, 2, Util.C.columnProjectDescription()); - table.addClickHandler(new ClickHandler() { - @Override - public void onClick(final ClickEvent event) { - final Cell cell = table.getCellForEvent(event); - if (cell != null && cell.getCellIndex() != 1 - && getRowItem(cell.getRowIndex()) != null) { - movePointerTo(cell.getRowIndex()); - } - } - }); - - final FlexCellFormatter fmt = table.getFlexCellFormatter(); - fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().dataHeader()); - fmt.addStyleName(0, 2, Gerrit.RESOURCES.css().dataHeader()); - } - - @Override - protected Object getRowItemKey(final Project item) { - return item.getNameKey(); - } - - @Override - protected void onOpenRow(final int row) { - History.newItem(link(getRowItem(row))); - } - - private String link(final Project item) { - return Dispatcher.toProjectAdmin(item.getNameKey(), ProjectScreen.INFO); - } - - void display(final List<Project> result) { - while (1 < table.getRowCount()) - table.removeRow(table.getRowCount() - 1); - - for (final Project k : result) { - final int row = table.getRowCount(); - table.insertRow(row); - applyDataRowStyle(row); - populate(row, k); - } - } - - void populate(final int row, final Project k) { - table.setWidget(row, 1, new Hyperlink(k.getName(), link(k))); - table.setText(row, 2, k.getDescription()); - - final FlexCellFormatter fmt = table.getFlexCellFormatter(); - fmt.addStyleName(row, 1, Gerrit.RESOURCES.css().dataCell()); - fmt.addStyleName(row, 1, Gerrit.RESOURCES.css().cPROJECT()); - fmt.addStyleName(row, 2, Gerrit.RESOURCES.css().dataCell()); - - setRowItem(row, k); - } - } } diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/openid.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/openid.css index 07133918fe..1e475ee6c1 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/openid.css +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/openid.css @@ -23,7 +23,6 @@ } .logo { - width: 98%; text-align: right; } diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/userpass/UserPassSignInDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/userpass/UserPassSignInDialog.java index 46e16bb05d..c4709a5c3c 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/userpass/UserPassSignInDialog.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/userpass/UserPassSignInDialog.java @@ -149,6 +149,7 @@ public class UserPassSignInDialog extends SignInDialog { buttons.add(login); close = new Button(); + DOM.setStyleAttribute(close.getElement(), "marginLeft", "45px"); close.setText(Gerrit.C.signInDialogClose()); close.addClickHandler(new ClickHandler() { @Override @@ -226,7 +227,7 @@ public class UserPassSignInDialog extends SignInDialog { @Override public void onFailure(final Throwable caught) { super.onFailure(caught); - enable(false); + enable(true); } }); } diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ApprovalTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ApprovalTable.java index f850a33eda..23dd635719 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ApprovalTable.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ApprovalTable.java @@ -188,6 +188,10 @@ public class ApprovalTable extends Composite { r.append(Util.M.accountNotFound(e.getName())); break; + case ACCOUNT_INACTIVE: + r.append(Util.M.accountInactive(e.getName())); + break; + case CHANGE_NOT_VISIBLE: r.append(Util.M.changeNotVisibleTo(e.getName())); break; diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java index 6c68ba92fd..312cc106c0 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java @@ -49,6 +49,7 @@ public interface ChangeMessages extends Messages { String changeQueryPageTitle(String query); String accountNotFound(String who); + String accountInactive(String who); String changeNotVisibleTo(String who); String anonymousDownload(String protocol); diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties index a4a1c3165b..625a9ecb8b 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties @@ -1,6 +1,6 @@ accountDashboardTitle = Code Review Dashboard for {0} changesStartedBy = Started by {0} -changesReviewableBy = Reviewable by {0} +changesReviewableBy = Review Requests for {0} changesOpenInProject = Open Changes In {0} changesMergedInProject = Merged Changes In {0} changesAbandonedInProject = Abandoned Changes In {0} @@ -30,6 +30,7 @@ changeQueryWindowTitle = {0} changeQueryPageTitle = Search for {0} accountNotFound = {0} is not a registered user. +accountInactive = {0} is not an active user. changeNotVisibleTo = {0} cannot access the change. anonymousDownload = Anonymous {0} diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java index 83870c0a54..4db387f9ce 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java @@ -29,9 +29,9 @@ import com.google.gerrit.common.data.ChangeInfo; import com.google.gerrit.common.data.ToggleStarRequest; import com.google.gerrit.reviewdb.Account; import com.google.gerrit.reviewdb.Change; +import com.google.gerrit.reviewdb.Change.Status; import com.google.gerrit.reviewdb.ChangeMessage; import com.google.gerrit.reviewdb.PatchSet; -import com.google.gerrit.reviewdb.Change.Status; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; import com.google.gwt.event.dom.client.KeyPressEvent; @@ -39,6 +39,7 @@ import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.i18n.client.LocaleInfo; import com.google.gwt.user.client.ui.DisclosurePanel; import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.HorizontalPanel; import com.google.gwt.user.client.ui.Image; import com.google.gwt.user.client.ui.Label; import com.google.gwt.user.client.ui.Panel; @@ -115,7 +116,6 @@ public class ChangeScreen extends Screen { super.registerKeys(); regNavigation = GlobalKey.add(this, keysNavigation); regAction = GlobalKey.add(this, keysAction); - patchSetsBlock.setRegisterKeys(true); } public void refresh() { @@ -125,6 +125,11 @@ public class ChangeScreen extends Screen { protected void preDisplay(final ChangeDetail r) { display(r); } + + @Override + protected void postDisplay() { + patchSetsBlock.setRegisterKeys(true); + } }); } @@ -178,7 +183,7 @@ public class ChangeScreen extends Screen { dependencies = new ChangeTable() { { - table.setWidth("98%"); + table.setWidth("auto"); } }; dependsOn = new ChangeTable.Section(Util.C.changeScreenDependsOn()); @@ -188,7 +193,6 @@ public class ChangeScreen extends Screen { dependenciesPanel = new DisclosurePanel(Util.C.changeScreenDependencies()); dependenciesPanel.setContent(dependencies); - dependenciesPanel.setWidth("95%"); add(dependenciesPanel); patchSetsBlock = new PatchSetsBlock(this); @@ -217,7 +221,12 @@ public class ChangeScreen extends Screen { setPageTitle(titleBuf.toString()); } - void display(final ChangeDetail detail) { + void update(final ChangeDetail detail) { + display(detail); + patchSetsBlock.setRegisterKeys(true); + } + + private void display(final ChangeDetail detail) { displayTitle(detail.getChange().getKey(), detail.getChange().getSubject()); if (starChange != null) { @@ -263,15 +272,17 @@ public class ChangeScreen extends Screen { private void addComments(final ChangeDetail detail) { comments.clear(); - final Label hdr = new Label(Util.C.changeScreenComments()); - hdr.setStyleName(Gerrit.RESOURCES.css().blockHeader()); - comments.add(hdr); - final AccountInfoCache accts = detail.getAccounts(); final List<ChangeMessage> msgList = detail.getMessages(); + + HorizontalPanel title = new HorizontalPanel(); + title.setWidth("100%"); + title.add(new Label(Util.C.changeScreenComments())); if (msgList.size() > 1) { - comments.add(messagesMenuBar()); + title.add(messagesMenuBar()); } + title.setStyleName(Gerrit.RESOURCES.css().blockHeader()); + comments.add(title); final long AGE = 7 * 24 * 60 * 60 * 1000L; final Timestamp aged = new Timestamp(System.currentTimeMillis() - AGE); @@ -307,24 +318,22 @@ public class ChangeScreen extends Screen { comments.add(cp); } - if (msgList.size() > 1) { - comments.add(messagesMenuBar()); - } comments.setVisible(msgList.size() > 0); } private LinkMenuBar messagesMenuBar() { final Panel c = comments; - final LinkMenuBar m = new LinkMenuBar(); - m.addItem(Util.C.messageExpandRecent(), new ExpandAllCommand(c, true) { + final LinkMenuBar menuBar = new LinkMenuBar(); + menuBar.addItem(Util.C.messageExpandRecent(), new ExpandAllCommand(c, true) { @Override protected void expand(final CommentPanel w) { w.setOpen(w.isRecent()); } }); - m.addItem(Util.C.messageExpandAll(), new ExpandAllCommand(c, true)); - m.addItem(Util.C.messageCollapseAll(), new ExpandAllCommand(c, false)); - return m; + menuBar.addItem(Util.C.messageExpandAll(), new ExpandAllCommand(c, true)); + menuBar.addItem(Util.C.messageCollapseAll(), new ExpandAllCommand(c, false)); + menuBar.addStyleName(Gerrit.RESOURCES.css().commentPanelMenuBar()); + return menuBar; } private void toggleStar() { diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java index 6a57feb165..248e57de72 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java @@ -25,8 +25,9 @@ import com.google.gerrit.common.data.GitwebLink; import com.google.gerrit.common.data.PatchSetDetail; import com.google.gerrit.reviewdb.Account; import com.google.gerrit.reviewdb.AccountGeneralPreferences; +import com.google.gerrit.reviewdb.AccountGeneralPreferences.DownloadCommand; +import com.google.gerrit.reviewdb.AccountGeneralPreferences.DownloadScheme; import com.google.gerrit.reviewdb.ApprovalCategory; -import com.google.gerrit.reviewdb.Branch; import com.google.gerrit.reviewdb.Change; import com.google.gerrit.reviewdb.ChangeMessage; import com.google.gerrit.reviewdb.Patch; @@ -34,8 +35,6 @@ import com.google.gerrit.reviewdb.PatchSet; import com.google.gerrit.reviewdb.PatchSetInfo; import com.google.gerrit.reviewdb.Project; import com.google.gerrit.reviewdb.UserIdentity; -import com.google.gerrit.reviewdb.AccountGeneralPreferences.DownloadCommand; -import com.google.gerrit.reviewdb.AccountGeneralPreferences.DownloadScheme; import com.google.gwt.core.client.GWT; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; @@ -48,9 +47,9 @@ import com.google.gwt.user.client.ui.Button; import com.google.gwt.user.client.ui.DisclosurePanel; import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.Grid; +import com.google.gwt.user.client.ui.HTMLTable.CellFormatter; import com.google.gwt.user.client.ui.InlineLabel; import com.google.gwt.user.client.ui.Panel; -import com.google.gwt.user.client.ui.HTMLTable.CellFormatter; import com.google.gwtexpui.clippy.client.CopyableLabel; import java.util.Collections; @@ -167,7 +166,6 @@ class PatchSetComplexDisclosurePanel extends ComplexDisclosurePanel implements O } private void displayDownload() { - final Branch.NameKey branchKey = changeDetail.getChange().getDest(); final Project.NameKey projectKey = changeDetail.getChange().getProject(); final String projectName = projectKey.get(); final CopyableLabel copyLabel = new CopyableLabel(""); @@ -405,7 +403,7 @@ class PatchSetComplexDisclosurePanel extends ComplexDisclosurePanel implements O new AbandonChangeDialog(patchSet.getId(), new AsyncCallback<ChangeDetail>() { public void onSuccess(ChangeDetail result) { - changeScreen.display(result); + changeScreen.update(result); } public void onFailure(Throwable caught) { @@ -425,7 +423,7 @@ class PatchSetComplexDisclosurePanel extends ComplexDisclosurePanel implements O new RestoreChangeDialog(patchSet.getId(), new AsyncCallback<ChangeDetail>() { public void onSuccess(ChangeDetail result) { - changeScreen.display(result); + changeScreen.update(result); } public void onFailure(Throwable caught) { @@ -516,7 +514,7 @@ class PatchSetComplexDisclosurePanel extends ComplexDisclosurePanel implements O new SubmitFailureDialog(result, msg).center(); } } - changeScreen.display(result); + changeScreen.update(result); } public PatchSet getPatchSet() { diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetsBlock.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetsBlock.java index b0f06a4e3e..edfeb235e6 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetsBlock.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetsBlock.java @@ -16,6 +16,7 @@ package com.google.gerrit.client.changes; import com.google.gerrit.client.Gerrit; import com.google.gerrit.common.data.ChangeDetail; +import com.google.gerrit.reviewdb.AccountGeneralPreferences; import com.google.gerrit.reviewdb.PatchSet; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; @@ -30,8 +31,10 @@ import com.google.gwtexpui.globalkey.client.GlobalKey; import com.google.gwtexpui.globalkey.client.KeyCommand; import com.google.gwtexpui.globalkey.client.KeyCommandSet; +import java.util.Collections; import java.util.HashMap; import java.util.List; +import java.util.Map; /** * Composite that displays the patch sets of a change. This composite ensures @@ -39,7 +42,7 @@ import java.util.List; */ public class PatchSetsBlock extends Composite { - private final HashMap<PatchSet.Id, PatchSetComplexDisclosurePanel> patchSetPanels = + private final Map<PatchSet.Id, PatchSetComplexDisclosurePanel> patchSetPanels = new HashMap<PatchSet.Id, PatchSetComplexDisclosurePanel>(); private final ChangeScreen parent; @@ -72,6 +75,14 @@ public class PatchSetsBlock extends Composite { currentPatchSetId = currps.getId(); patchSets = detail.getPatchSets(); + if (Gerrit.isSignedIn()) { + final AccountGeneralPreferences p = + Gerrit.getUserAccount().getGeneralPreferences(); + if (p.isDisplayPatchSetsInReverseOrder()) { + Collections.reverse(patchSets); + } + } + for (final PatchSet ps : patchSets) { if (ps == currps) { add(new PatchSetComplexDisclosurePanel(parent, detail, detail diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchTable.java index 34695ddaaa..33b37c8b24 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchTable.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchTable.java @@ -17,10 +17,12 @@ package com.google.gerrit.client.changes; import com.google.gerrit.client.Gerrit; import com.google.gerrit.client.patches.PatchScreen; import com.google.gerrit.client.ui.InlineHyperlink; +import com.google.gerrit.client.ui.ListenableAccountDiffPreference; import com.google.gerrit.client.ui.NavigationTable; import com.google.gerrit.client.ui.PatchLink; import com.google.gerrit.common.data.PatchSetDetail; import com.google.gerrit.reviewdb.Patch; +import com.google.gerrit.reviewdb.Patch.ChangeType; import com.google.gerrit.reviewdb.Patch.Key; import com.google.gerrit.reviewdb.Patch.PatchType; import com.google.gwt.core.client.GWT; @@ -53,16 +55,22 @@ public class PatchTable extends Composite { private MyTable myTable; private String savePointerId; private List<Patch> patchList; + private ListenableAccountDiffPreference listenablePrefs; private List<ClickHandler> clickHandlers; private boolean active; private boolean registerKeys; - public PatchTable() { + public PatchTable(ListenableAccountDiffPreference prefs) { + listenablePrefs = prefs; myBody = new FlowPanel(); initWidget(myBody); } + public PatchTable() { + this(new ListenableAccountDiffPreference()); + } + public int indexOf(Patch.Key patch) { for (int i = 0; i < patchList.size(); i++) { if (patchList.get(i).getKey().equals(patch)) { @@ -166,9 +174,13 @@ public class PatchTable extends Composite { * @return a link to the previous file in this patch set, or null. */ public InlineHyperlink getPreviousPatchLink(int index, PatchScreen.Type patchType) { - if (0 < index) - return createLink(index - 1, patchType, SafeHtml.asis(Util.C + for(index--; index > -1; index--) { + InlineHyperlink link = createLink(index, patchType, SafeHtml.asis(Util.C .prevPatchLinkIcon()), null); + if (link != null) { + return link; + } + } return null; } @@ -176,9 +188,13 @@ public class PatchTable extends Composite { * @return a link to the next file in this patch set, or null. */ public InlineHyperlink getNextPatchLink(int index, PatchScreen.Type patchType) { - if (index < patchList.size() - 1) - return createLink(index + 1, patchType, null, SafeHtml.asis(Util.C + for(index++; index < patchList.size(); index++) { + InlineHyperlink link = createLink(index, patchType, null, SafeHtml.asis(Util.C .nextPatchLinkIcon())); + if (link != null) { + return link; + } + } return null; } @@ -192,6 +208,14 @@ public class PatchTable extends Composite { private PatchLink createLink(int index, PatchScreen.Type patchType, SafeHtml before, SafeHtml after) { Patch patch = patchList.get(index); + if (( listenablePrefs.get().isSkipDeleted() && + patch.getChangeType().equals(ChangeType.DELETED) ) + || + ( listenablePrefs.get().isSkipUncommented() && + patch.getCommentCount() == 0 ) ) { + return null; + } + Key thisKey = patch.getKey(); PatchLink link; if (patchType == PatchScreen.Type.SIDE_BY_SIDE @@ -240,6 +264,14 @@ public class PatchTable extends Composite { } } + public ListenableAccountDiffPreference getPreferences() { + return listenablePrefs; + } + + public void setPreferences(ListenableAccountDiffPreference prefs) { + listenablePrefs = prefs; + } + private class MyTable extends NavigationTable<Patch> { private static final int C_PATH = 2; private static final int C_DRAFT = 3; @@ -756,5 +788,4 @@ public class PatchTable extends Composite { return System.currentTimeMillis() - start > 200; } } - } diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css index b184064153..c8f16d0afc 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css @@ -23,7 +23,7 @@ @external .gwt-TextBox; @external .gwt-Hyperlink; @external .gwt-CheckBox; -@external .gwt-DisclosurePanel, .header; +@external .gwt-DisclosurePanel, .header, .content; @external .gwt-InlineLabel; @external .gwt-InlineHyperlink; @external .gwt-RadioButton; @@ -69,7 +69,6 @@ font-size: 11pt; padding-left: 5px; padding-right: 5px; - width: 98%; } .version, @@ -199,6 +198,9 @@ padding-left: 0.5em; padding-right: 0.5em; } +.commentPanelMenuBar { + float: right; +} .commentPanelMessage p { margin-top: 0px; margin-bottom: 0px; @@ -674,6 +676,9 @@ background: #eeeeee; border-bottom: 1px solid #eeeeee; } +.fileLineMode { + font-weight: bold; +} .fileLineDELETE, .fileLineDELETE .wdc { background: #ffeeee; @@ -753,6 +758,10 @@ margin-bottom: 10px; } +.gwt-DisclosurePanel .content { + margin-left: 10px; +} + .changeScreenDescription { white-space: pre; font-family: mono-font; @@ -764,8 +773,7 @@ } .changeComments { - margin-left: 0.5em; - margin-right: 0.5em; + padding-top: 1em; width: 60em; } diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/AbstractPatchContentTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/AbstractPatchContentTable.java index 050dcb85d0..6452e08624 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/AbstractPatchContentTable.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/AbstractPatchContentTable.java @@ -30,8 +30,12 @@ import com.google.gerrit.prettify.common.SparseFileContent; import com.google.gerrit.reviewdb.Patch; import com.google.gerrit.reviewdb.PatchLineComment; import com.google.gerrit.reviewdb.PatchSet; +import com.google.gwt.event.dom.client.BlurEvent; +import com.google.gwt.event.dom.client.BlurHandler; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.event.dom.client.FocusEvent; +import com.google.gwt.event.dom.client.FocusHandler; import com.google.gwt.event.dom.client.KeyCodes; import com.google.gwt.event.dom.client.KeyPressEvent; import com.google.gwt.event.shared.HandlerRegistration; @@ -52,7 +56,7 @@ import java.util.ArrayList; import java.util.List; public abstract class AbstractPatchContentTable extends NavigationTable<Object> - implements CommentEditorContainer { + implements CommentEditorContainer, FocusHandler, BlurHandler { protected PatchTable fileList; protected AccountInfoCache accountCache = AccountInfoCache.empty(); protected Patch.Key patchKey; @@ -62,6 +66,8 @@ public abstract class AbstractPatchContentTable extends NavigationTable<Object> private final KeyCommandSet keysComment; private HandlerRegistration regComment; + private final KeyCommandSet keysOpenByEnter; + private HandlerRegistration regOpenByEnter; protected AbstractPatchContentTable() { keysNavigation.add(new PrevKeyCommand(0, 'k', PatchUtil.C.linePrev())); @@ -72,8 +78,8 @@ public abstract class AbstractPatchContentTable extends NavigationTable<Object> keysNavigation.add(new NextCommentCmd(0, 'N', PatchUtil.C.commentNext())); keysAction.add(new OpenKeyCommand(0, 'o', PatchUtil.C.expandComment())); - keysAction.add(new OpenKeyCommand(0, KeyCodes.KEY_ENTER, PatchUtil.C - .expandComment())); + keysOpenByEnter = new KeyCommandSet(Gerrit.C.sectionNavigation()); + keysOpenByEnter.add(new OpenKeyCommand(0, KeyCodes.KEY_ENTER, PatchUtil.C.expandComment())); if (Gerrit.isSignedIn()) { keysAction.add(new InsertCommentCommand(0, 'c', PatchUtil.C @@ -86,8 +92,6 @@ public abstract class AbstractPatchContentTable extends NavigationTable<Object> keysComment = new KeyCommandSet(PatchUtil.C.commentEditorSet()); keysComment.add(new NoOpKeyCommand(KeyCommand.M_CTRL, 's', PatchUtil.C .commentSaveDraft())); - keysComment.add(new NoOpKeyCommand(KeyCommand.M_CTRL, 'd', PatchUtil.C - .commentDiscard())); keysComment.add(new NoOpKeyCommand(0, KeyCodes.KEY_ESCAPE, PatchUtil.C .commentCancelEdit())); } else { @@ -149,6 +153,13 @@ public abstract class AbstractPatchContentTable extends NavigationTable<Object> regComment.removeHandler(); regComment = null; } + + if (on && keysOpenByEnter != null && regOpenByEnter == null) { + regOpenByEnter = GlobalKey.add(this, keysOpenByEnter); + } else if (!on && regOpenByEnter != null) { + regOpenByEnter.removeHandler(); + regOpenByEnter = null; + } } public void display(final Patch.Key k, final PatchSet.Id a, @@ -157,9 +168,6 @@ public abstract class AbstractPatchContentTable extends NavigationTable<Object> idSideA = a; idSideB = b; - final String pathName = patchKey.get(); - int ext = pathName.lastIndexOf('.'); - render(s); } @@ -408,6 +416,8 @@ public abstract class AbstractPatchContentTable extends NavigationTable<Object> } final CommentEditorPanel ed = new CommentEditorPanel(newComment); + ed.addFocusHandler(this); + ed.addBlurHandler(this); boolean isCommentRow = false; boolean needInsert = false; if (row < table.getRowCount()) { @@ -522,6 +532,8 @@ public abstract class AbstractPatchContentTable extends NavigationTable<Object> final PatchLineComment line, final boolean isLast) { if (line.getStatus() == PatchLineComment.Status.DRAFT) { final CommentEditorPanel plc = new CommentEditorPanel(line); + plc.addFocusHandler(this); + plc.addBlurHandler(this); table.setWidget(row, col, plc); styleLastCommentCell(row, col); @@ -529,6 +541,8 @@ public abstract class AbstractPatchContentTable extends NavigationTable<Object> final AccountInfo author = accountCache.get(line.getAuthor()); final PublishedCommentPanel panel = new PublishedCommentPanel(author, line); + panel.addFocusHandler(this); + panel.addBlurHandler(this); table.setWidget(row, col, panel); styleLastCommentCell(row, col); @@ -544,6 +558,29 @@ public abstract class AbstractPatchContentTable extends NavigationTable<Object> styleCommentRow(row); } + @Override + public void onFocus(FocusEvent event) { + // when the comment panel gets focused (actually when a button inside the + // comment panel gets focused) we have to unregister the key binding for + // ENTER that expands/collapses the comment panel, if we don't do this the + // focused button in the comment panel cannot be triggered by pressing ENTER + // since ENTER would then be already consumed by this key binding + if (regOpenByEnter != null) { + regOpenByEnter.removeHandler(); + regOpenByEnter = null; + } + } + + @Override + public void onBlur(BlurEvent event) { + // when the comment panel gets blurred (actually when a button inside the + // comment panel gets blurred) we have to re-register the key binding for + // ENTER that expands/collapses the comment panel + if (keysOpenByEnter != null && regOpenByEnter == null) { + regOpenByEnter = GlobalKey.add(this, keysOpenByEnter); + } + } + private void styleCommentRow(final int row) { final CellFormatter fmt = table.getCellFormatter(); final Element iconCell = fmt.getElement(row, 0); @@ -705,11 +742,11 @@ public abstract class AbstractPatchContentTable extends NavigationTable<Object> reply = new Button(PatchUtil.C.buttonReply()); reply.addClickHandler(this); - getButtonPanel().add(reply); + addButton(reply); replyDone = new Button(PatchUtil.C.buttonReplyDone()); replyDone.addClickHandler(this); - getButtonPanel().add(replyDone); + addButton(replyDone); } @Override @@ -738,18 +775,18 @@ public abstract class AbstractPatchContentTable extends NavigationTable<Object> final PatchLineComment newComment = newComment(); newComment.setMessage(message); - enable(false); + enableButtons(false); PatchUtil.DETAIL_SVC.saveDraft(newComment, new GerritCallback<PatchLineComment>() { public void onSuccess(final PatchLineComment result) { - enable(true); + enableButtons(true); notifyDraftDelta(1); createEditor(result).setOpen(false); } @Override public void onFailure(Throwable caught) { - enable(true); + enableButtons(true); super.onFailure(caught); } }); @@ -775,13 +812,5 @@ public abstract class AbstractPatchContentTable extends NavigationTable<Object> newComment.setSide(comment.getSide()); return newComment; } - - private void enable(boolean on) { - for (Widget w : getButtonPanel()) { - if (w instanceof Button) { - ((Button) w).setEnabled(on); - } - } - } } } diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommentEditorPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommentEditorPanel.java index 73a3ec2279..98f0c95997 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommentEditorPanel.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommentEditorPanel.java @@ -102,18 +102,6 @@ public class CommentEditorPanel extends CommentPanel implements ClickHandler, event.preventDefault(); onSave(NULL_CALLBACK); return; - - case 'd': - case 'D': - event.preventDefault(); - if (isNew()) { - onDiscard(); - } else if (Window.confirm(PatchUtil.C.confirmDiscard())) { - onDiscard(); - } else { - text.setFocus(true); - } - return; } } @@ -125,22 +113,22 @@ public class CommentEditorPanel extends CommentPanel implements ClickHandler, edit = new Button(); edit.setText(PatchUtil.C.buttonEdit()); edit.addClickHandler(this); - getButtonPanel().add(edit); + addButton(edit); save = new Button(); save.setText(PatchUtil.C.buttonSave()); save.addClickHandler(this); - getButtonPanel().add(save); + addButton(save); cancel = new Button(); cancel.setText(PatchUtil.C.buttonCancel()); cancel.addClickHandler(this); - getButtonPanel().add(cancel); + addButton(cancel); discard = new Button(); discard.setText(PatchUtil.C.buttonDiscard()); discard.addClickHandler(this); - getButtonPanel().add(discard); + addButton(discard); setOpen(true); if (isNew()) { diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/HistoryTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/HistoryTable.java index f7f99e1e45..cb85c2448e 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/HistoryTable.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/HistoryTable.java @@ -38,7 +38,7 @@ class HistoryTable extends FancyFlexTable<Patch> { HistoryTable(final PatchScreen parent) { setStyleName(Gerrit.RESOURCES.css().patchHistoryTable()); screen = parent; - table.setWidth("98%"); + table.setWidth("auto"); table.addStyleName(Gerrit.RESOURCES.css().changeTable()); } diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/NavLinks.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/NavLinks.java index 6aae855f55..b8c52d8cb6 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/NavLinks.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/NavLinks.java @@ -30,16 +30,31 @@ import com.google.gwtexpui.globalkey.client.KeyCommandSet; import com.google.gwtexpui.safehtml.client.SafeHtml; class NavLinks extends Composite { + public enum Nav { + PREV (0, '[', PatchUtil.C.previousFileHelp(), 0), + NEXT (2, ']', PatchUtil.C.nextFileHelp(), 1); + + public int col; // Table Cell column to display link in + public int key; // key code shortcut to activate link + public String help; // help string for '?' popup + public int cmd; // index into cmds array + + Nav(int c, int k, String h, int i) { + this.col = c; + this.key = k; + this.help = h; + this.cmd = i; + } + } + + private final Change.Id changeId; private final KeyCommandSet keys; private final Grid table; - private InlineHyperlink prev; - private InlineHyperlink next; - - private KeyCommand prevKey; - private KeyCommand nextKey; + private KeyCommand cmds[] = new KeyCommand[2]; NavLinks(KeyCommandSet kcs, Change.Id forChange) { + changeId = forChange; keys = kcs; table = new Grid(1, 3); initWidget(table); @@ -50,56 +65,49 @@ class NavLinks extends Composite { fmt.setHorizontalAlignment(0, 1, HasHorizontalAlignment.ALIGN_CENTER); fmt.setHorizontalAlignment(0, 2, HasHorizontalAlignment.ALIGN_RIGHT); - final ChangeLink up = new ChangeLink("", forChange); + final ChangeLink up = new ChangeLink("", changeId); SafeHtml.set(up, SafeHtml.asis(Util.C.upToChangeIconLink())); table.setWidget(0, 1, up); } void display(int patchIndex, PatchScreen.Type type, PatchTable fileList) { if (fileList != null) { - prev = fileList.getPreviousPatchLink(patchIndex, type); - next = fileList.getNextPatchLink(patchIndex, type); + setupNav(Nav.PREV, fileList.getPreviousPatchLink(patchIndex, type)); + setupNav(Nav.NEXT, fileList.getNextPatchLink(patchIndex, type)); } else { - prev = null; - next = null; + setupNav(Nav.PREV, null); + setupNav(Nav.NEXT, null); } + } - if (prev != null) { - if (keys != null && prevKey == null) { - prevKey = new KeyCommand(0, '[', PatchUtil.C.previousFileHelp()) { - @Override - public void onKeyPress(KeyPressEvent event) { - prev.go(); - } - }; - keys.add(prevKey); - } - table.setWidget(0, 0, prev); + protected void setupNav(final Nav nav, final InlineHyperlink link) { + + /* setup the cells */ + if (link != null) { + table.setWidget(0, nav.col, link); } else { - if (keys != null && prevKey != null) { - keys.remove(prevKey); - prevKey = null; - } - table.clearCell(0, 0); + table.clearCell(0, nav.col); } - if (next != null) { - if (keys != null && nextKey == null) { - nextKey = new KeyCommand(0, ']', PatchUtil.C.nextFileHelp()) { + /* setup the keys */ + if (keys != null) { + + if (cmds[nav.cmd] != null) { + keys.remove(cmds[nav.cmd]); + } + + if (link != null) { + cmds[nav.cmd] = new KeyCommand(0, nav.key, nav.help) { @Override public void onKeyPress(KeyPressEvent event) { - next.go(); + link.go(); } }; - keys.add(nextKey); + } else { + cmds[nav.cmd] = new UpToChangeCommand(changeId, 0, nav.key); } - table.setWidget(0, 2, next); - } else { - if (keys != null && nextKey != null) { - keys.remove(nextKey); - nextKey = null; - } - table.clearCell(0, 2); + + keys.add(cmds[nav.cmd]); } } } diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.java index 3b7c445373..93e8deb44a 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.java @@ -24,7 +24,6 @@ public interface PatchConstants extends Constants { String buttonSave(); String buttonCancel(); String buttonDiscard(); - String confirmDiscard(); String noDifference(); String patchHeaderOld(); @@ -46,7 +45,6 @@ public interface PatchConstants extends Constants { String commentEditorSet(); String commentInsert(); String commentSaveDraft(); - String commentDiscard(); String commentCancelEdit(); String whitespaceIGNORE_NONE(); @@ -62,4 +60,7 @@ public interface PatchConstants extends Constants { String buttonReplyDone(); String cannedReplyDone(); + + String fileTypeSymlink(); + String fileTypeGitlink(); } diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.properties index 9826aba61f..90def1d307 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.properties +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.properties @@ -7,7 +7,6 @@ buttonEdit = Edit buttonSave = Save buttonCancel = Cancel buttonDiscard = Discard -confirmDiscard = Discard this comment? noDifference = No Differences patchHeaderOld = Old Version @@ -28,7 +27,6 @@ expandComment = Expand or collapse comment commentEditorSet = Comment Editing commentInsert = Create a new inline comment commentSaveDraft = Save draft comment -commentDiscard = Discard draft comment commentCancelEdit = Cancel comment edit whitespaceIGNORE_NONE=None @@ -41,3 +39,6 @@ nextFileHelp = Next file reviewed = Reviewed download = (Download) + +fileTypeSymlink = Type: Symbolic Link +fileTypeGitlink = Type: Git Commit in Subproject diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScreen.java index 238a201d2b..812a74ee72 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScreen.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScreen.java @@ -17,15 +17,13 @@ package com.google.gerrit.client.patches; import com.google.gerrit.client.Dispatcher; import com.google.gerrit.client.Gerrit; import com.google.gerrit.client.RpcStatus; -import com.google.gerrit.client.changes.ChangeScreen; import com.google.gerrit.client.changes.CommitMessageBlock; import com.google.gerrit.client.changes.PatchTable; import com.google.gerrit.client.changes.Util; import com.google.gerrit.client.rpc.GerritCallback; import com.google.gerrit.client.rpc.ScreenLoadCallback; -import com.google.gerrit.client.ui.InlineHyperlink; +import com.google.gerrit.client.ui.ListenableAccountDiffPreference; import com.google.gerrit.client.ui.Screen; -import com.google.gerrit.common.PageLinks; import com.google.gerrit.common.data.PatchScript; import com.google.gerrit.common.data.PatchSetDetail; import com.google.gerrit.prettify.client.ClientSideFormatter; @@ -137,17 +135,12 @@ public abstract class PatchScreen extends Screen implements /** The index of the file we are currently looking at among the fileList */ private int patchIndex; + private ListenableAccountDiffPreference prefs; /** Keys that cause an action on this screen */ private KeyCommandSet keysNavigation; private HandlerRegistration regNavigation; - /** Link to the screen for the previous file, null if not applicable */ - private InlineHyperlink previousFileLink; - - /** Link to the screen for the next file, null if not applicable */ - private InlineHyperlink nextFileLink; - /** * How this patch should be displayed in the patch screen. */ @@ -175,14 +168,17 @@ public abstract class PatchScreen extends Screen implements idSideB = diffSideB != null ? diffSideB : id.getParentKey(); this.patchIndex = patchIndex; - settingsPanel = new PatchScriptSettingsPanel(); - settingsPanel - .addValueChangeHandler(new ValueChangeHandler<AccountDiffPreference>() { + prefs = fileList != null ? fileList.getPreferences() : + new ListenableAccountDiffPreference(); + prefs.addValueChangeHandler( + new ValueChangeHandler<AccountDiffPreference>() { @Override public void onValueChange(ValueChangeEvent<AccountDiffPreference> event) { update(event.getValue()); } }); + + settingsPanel = new PatchScriptSettingsPanel(prefs); settingsPanel.getReviewedCheckBox().addValueChangeHandler( new ValueChangeHandler<Boolean>() { @Override @@ -252,8 +248,9 @@ public abstract class PatchScreen extends Screen implements protected void onInitUI() { super.onInitUI(); + final Change.Id ck = patchKey.getParentKey().getParentKey(); keysNavigation = new KeyCommandSet(Gerrit.C.sectionNavigation()); - keysNavigation.add(new UpToChangeCommand(0, 'u', PatchUtil.C.upToChange())); + keysNavigation.add(new UpToChangeCommand(ck, 0, 'u')); keysNavigation.add(new FileListCmd(0, 'f', PatchUtil.C.fileList())); historyTable = new HistoryTable(this); @@ -333,11 +330,9 @@ public abstract class PatchScreen extends Screen implements public void onSuccess(PatchSetDetail result) { patchSetDetail = result; if (fileList == null) { - fileList = new PatchTable(); + fileList = new PatchTable(prefs); fileList.display(result); patchIndex = fileList.indexOf(patchKey); - topNav.display(patchIndex, getPatchScreenType(), fileList); - bottomNav.display(patchIndex, getPatchScreenType(), fileList); } refresh(true); } @@ -360,6 +355,10 @@ public abstract class PatchScreen extends Screen implements public void registerKeys() { super.registerKeys(); contentTable.setRegisterKeys(contentTable.isVisible()); + if (regNavigation != null) { + regNavigation.removeHandler(); + regNavigation = null; + } regNavigation = GlobalKey.add(this, keysNavigation); } @@ -391,6 +390,7 @@ public abstract class PatchScreen extends Screen implements } private void onResult(final PatchScript script, final boolean isFirst) { + final Change.Key cid = script.getChangeId(); final String path = PatchTable.getDisplayFileName(patchKey); String fileName = path; @@ -411,6 +411,7 @@ public abstract class PatchScreen extends Screen implements new GerritCallback<PatchSetDetail>() { @Override public void onSuccess(PatchSetDetail result) { + commitMessageBlock.setVisible(true); commitMessageBlock.display(result.getInfo().getMessage()); } }); @@ -450,6 +451,11 @@ public abstract class PatchScreen extends Screen implements settingsPanel.setEnabled(true); lastScript = script; + if (fileList != null) { + topNav.display(patchIndex, getPatchScreenType(), fileList); + bottomNav.display(patchIndex, getPatchScreenType(), fileList); + } + // Mark this file reviewed as soon we display the diff screen if (Gerrit.isSignedIn() && isFirst) { settingsPanel.getReviewedCheckBox().setValue(true); @@ -473,18 +479,6 @@ public abstract class PatchScreen extends Screen implements diffSideB = patchSetId; } - public class UpToChangeCommand extends KeyCommand { - public UpToChangeCommand(int mask, int key, String help) { - super(mask, key, help); - } - - @Override - public void onKeyPress(final KeyPressEvent event) { - final Change.Id ck = patchKey.getParentKey().getParentKey(); - Gerrit.display(PageLinks.toChange(ck), new ChangeScreen(ck)); - } - } - public class FileListCmd extends KeyCommand { public FileListCmd(int mask, int key, String help) { super(mask, key, help); @@ -494,7 +488,7 @@ public abstract class PatchScreen extends Screen implements public void onKeyPress(final KeyPressEvent event) { if (fileList == null || fileList.isAttached()) { final PatchSet.Id psid = patchKey.getParentKey(); - fileList = new PatchTable(); + fileList = new PatchTable(prefs); fileList.setSavePointerId("PatchTable " + psid); Util.DETAIL_SVC.patchSetDetail(psid, new GerritCallback<PatchSetDetail>() { diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.java index 758b0f0ad0..3550229f6f 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.java @@ -17,6 +17,7 @@ package com.google.gerrit.client.patches; import com.google.gerrit.client.Gerrit; import com.google.gerrit.client.account.Util; import com.google.gerrit.client.rpc.GerritCallback; +import com.google.gerrit.client.ui.ListenableAccountDiffPreference; import com.google.gerrit.client.ui.NpIntTextBox; import com.google.gerrit.reviewdb.AccountDiffPreference; import com.google.gerrit.reviewdb.AccountDiffPreference.Whitespace; @@ -48,7 +49,7 @@ public class PatchScriptSettingsPanel extends Composite implements interface MyUiBinder extends UiBinder<Widget, PatchScriptSettingsPanel> { } - private AccountDiffPreference value; + private ListenableAccountDiffPreference listenablePrefs; private boolean enableIntralineDifference = true; private boolean enableSmallFileFeatures = true; @@ -80,6 +81,13 @@ public class PatchScriptSettingsPanel extends Composite implements CheckBox reviewed; @UiField + CheckBox skipDeleted; + + @UiField + CheckBox skipUncommented; + + + @UiField Button update; /** @@ -96,7 +104,8 @@ public class PatchScriptSettingsPanel extends Composite implements */ private int setEnabledCounter; - public PatchScriptSettingsPanel() { + public PatchScriptSettingsPanel(ListenableAccountDiffPreference prefs) { + listenablePrefs = prefs; initWidget(uiBinder.createAndBindUi(this)); initIgnoreWhitespace(ignoreWhitespace); initContext(context); @@ -115,11 +124,7 @@ public class PatchScriptSettingsPanel extends Composite implements tabWidth.addKeyPressHandler(onEnter); colWidth.addKeyPressHandler(onEnter); - if (Gerrit.isSignedIn() && Gerrit.getAccountDiffPreference() != null) { - setValue(Gerrit.getAccountDiffPreference()); - } else { - setValue(AccountDiffPreference.createDefault(null)); - } + display(); } @Override @@ -147,7 +152,7 @@ public class PatchScriptSettingsPanel extends Composite implements public void setEnableSmallFileFeatures(final boolean on) { enableSmallFileFeatures = on; if (enableSmallFileFeatures) { - syntaxHighlighting.setValue(value.isSyntaxHighlighting()); + syntaxHighlighting.setValue(getValue().isSyntaxHighlighting()); } else { syntaxHighlighting.setValue(false); } @@ -157,7 +162,7 @@ public class PatchScriptSettingsPanel extends Composite implements public void setEnableIntralineDifference(final boolean on) { enableIntralineDifference = on; if (enableIntralineDifference) { - intralineDifference.setValue(value.isIntralineDifference()); + intralineDifference.setValue(getValue().isIntralineDifference()); } else { intralineDifference.setValue(false); } @@ -178,10 +183,16 @@ public class PatchScriptSettingsPanel extends Composite implements } public AccountDiffPreference getValue() { - return value; + return listenablePrefs.get(); } public void setValue(final AccountDiffPreference dp) { + listenablePrefs.set(dp); + display(); + } + + protected void display() { + final AccountDiffPreference dp = getValue(); setIgnoreWhitespace(dp.getIgnoreWhitespace()); if (enableSmallFileFeatures) { syntaxHighlighting.setValue(dp.isSyntaxHighlighting()); @@ -195,8 +206,8 @@ public class PatchScriptSettingsPanel extends Composite implements intralineDifference.setValue(dp.isIntralineDifference()); whitespaceErrors.setValue(dp.isShowWhitespaceErrors()); showTabs.setValue(dp.isShowTabs()); - - value = dp; + skipDeleted.setValue(dp.isSkipDeleted()); + skipUncommented.setValue(dp.isSkipUncommented()); } @UiHandler("update") @@ -205,7 +216,7 @@ public class PatchScriptSettingsPanel extends Composite implements } private void update() { - AccountDiffPreference dp = new AccountDiffPreference(value); + AccountDiffPreference dp = new AccountDiffPreference(getValue()); dp.setIgnoreWhitespace(getIgnoreWhitespace()); dp.setContext(getContext()); dp.setTabSize(tabWidth.getIntValue()); @@ -214,9 +225,10 @@ public class PatchScriptSettingsPanel extends Composite implements dp.setIntralineDifference(intralineDifference.getValue()); dp.setShowWhitespaceErrors(whitespaceErrors.getValue()); dp.setShowTabs(showTabs.getValue()); + dp.setSkipDeleted(skipDeleted.getValue()); + dp.setSkipUncommented(skipUncommented.getValue()); - value = dp; - fireEvent(new ValueChangeEvent<AccountDiffPreference>(dp) {}); + listenablePrefs.set(dp); if (Gerrit.isSignedIn()) { persistDiffPreferences(); @@ -225,10 +237,11 @@ public class PatchScriptSettingsPanel extends Composite implements private void persistDiffPreferences() { setEnabled(false); - Util.ACCOUNT_SVC.changeDiffPreferences(value, new GerritCallback<VoidResult>() { + Util.ACCOUNT_SVC.changeDiffPreferences(getValue(), + new GerritCallback<VoidResult>() { @Override public void onSuccess(VoidResult result) { - Gerrit.setAccountDiffPreference(value); + Gerrit.setAccountDiffPreference(getValue()); setEnabled(true); } @@ -267,7 +280,7 @@ public class PatchScriptSettingsPanel extends Composite implements if (0 <= sel) { return Whitespace.valueOf(ignoreWhitespace.getValue(sel)); } - return value.getIgnoreWhitespace(); + return getValue().getIgnoreWhitespace(); } private void setIgnoreWhitespace(Whitespace s) { @@ -285,7 +298,7 @@ public class PatchScriptSettingsPanel extends Composite implements if (0 <= sel) { return Short.parseShort(context.getValue(sel)); } - return (short) value.getContext(); + return (short) getValue().getContext(); } private void setContext(int ctx) { diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.ui.xml index 7bbc8fe3ab..f0f1b4dcaa 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.ui.xml +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.ui.xml @@ -115,12 +115,28 @@ limitations under the License. </g:CheckBox> </td> + <td rowspan='2'> + <g:CheckBox + ui:field='skipUncommented' + text='Skip Uncommented Files' + tabIndex='9'> + <ui:attribute name='text'/> + </g:CheckBox> + <br/> + <g:CheckBox + ui:field='skipDeleted' + text='Skip Deleted Files' + tabIndex='10'> + <ui:attribute name='text'/> + </g:CheckBox> + </td> + <td valign='bottom' rowspan='2'> <g:Button ui:field='update' text='Update' styleName='{style.updateButton}' - tabIndex='9'> + tabIndex='11'> <ui:attribute name='text'/> </g:Button> </td> @@ -129,7 +145,7 @@ limitations under the License. <g:CheckBox ui:field='reviewed' text='Reviewed' - tabIndex='10'> + tabIndex='12'> <ui:attribute name='text'/> </g:CheckBox> </td> diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/SideBySideTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/SideBySideTable.java index 1bc4dd5c14..2939e714f2 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/SideBySideTable.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/SideBySideTable.java @@ -22,6 +22,7 @@ import static com.google.gerrit.client.patches.PatchLine.Type.REPLACE; import com.google.gerrit.client.Gerrit; import com.google.gerrit.common.data.CommentDetail; import com.google.gerrit.common.data.PatchScript; +import com.google.gerrit.common.data.PatchScript.FileMode; import com.google.gerrit.prettify.common.EditList; import com.google.gerrit.prettify.common.SparseHtmlFile; import com.google.gerrit.reviewdb.Patch; @@ -81,6 +82,14 @@ public class SideBySideTable extends AbstractPatchContentTable { appendHeader(script, nc); lines.add(null); + if(script.getFileModeA()!=FileMode.FILE||script.getFileModeB()!=FileMode.FILE){ + openLine(nc); + appendModeLine(nc, script.getFileModeA()); + appendModeLine(nc, script.getFileModeB()); + closeLine(nc); + lines.add(null); + } + int lastB = 0; final boolean ignoreWS = script.isIgnoreWhitespace(); for (final EditList.Hunk hunk : script.getHunks()) { @@ -153,6 +162,29 @@ public class SideBySideTable extends AbstractPatchContentTable { } } + private void appendModeLine(final SafeHtmlBuilder nc, final FileMode mode) { + nc.openTd(); + nc.setStyleName(Gerrit.RESOURCES.css().lineNumber()); + nc.nbsp(); + nc.closeTd(); + + nc.openTd(); + nc.addStyleName(Gerrit.RESOURCES.css().fileLine()); + nc.addStyleName(Gerrit.RESOURCES.css().fileLineMode()); + switch(mode){ + case FILE: + nc.nbsp(); + break; + case SYMLINK: + nc.append(PatchUtil.C.fileTypeSymlink()); + break; + case GITLINK: + nc.append(PatchUtil.C.fileTypeGitlink()); + break; + } + nc.closeTd(); + } + @Override public void display(final CommentDetail cd) { if (cd.isEmpty()) { diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UpToChangeCommand.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UpToChangeCommand.java new file mode 100644 index 0000000000..709065d3d9 --- /dev/null +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UpToChangeCommand.java @@ -0,0 +1,36 @@ +// Copyright (C) 2010 The Android Open Source Project +// +// 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.google.gerrit.client.patches; + +import com.google.gerrit.client.Gerrit; +import com.google.gerrit.client.changes.ChangeScreen; +import com.google.gerrit.common.PageLinks; +import com.google.gerrit.reviewdb.Change; +import com.google.gwt.event.dom.client.KeyPressEvent; +import com.google.gwtexpui.globalkey.client.KeyCommand; + +class UpToChangeCommand extends KeyCommand { + private final Change.Id changeId; + + UpToChangeCommand(Change.Id changeId, int mask, int key) { + super(mask, key, PatchUtil.C.upToChange()); + this.changeId = changeId; + } + + @Override + public void onKeyPress(final KeyPressEvent event) { + Gerrit.display(PageLinks.toChange(changeId), new ChangeScreen(changeId)); + } +} diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/GerritCallback.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/GerritCallback.java index fa028d75ba..f29742ad92 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/GerritCallback.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/GerritCallback.java @@ -17,6 +17,7 @@ package com.google.gerrit.client.rpc; import com.google.gerrit.client.ErrorDialog; import com.google.gerrit.client.Gerrit; import com.google.gerrit.client.NotSignedInDialog; +import com.google.gerrit.common.errors.InactiveAccountException; import com.google.gerrit.common.errors.NameAlreadyUsedException; import com.google.gerrit.common.errors.NoSuchAccountException; import com.google.gerrit.common.errors.NoSuchEntityException; @@ -37,6 +38,9 @@ public abstract class GerritCallback<T> implements AsyncCallback<T> { } else if (isNoSuchEntity(caught)) { new ErrorDialog(Gerrit.C.notFoundBody()).center(); + } else if (isInactiveAccount(caught)) { + new ErrorDialog(Gerrit.C.inactiveAccountBody()).center(); + } else if (isNoSuchAccount(caught)) { final String msg = caught.getMessage(); final String who = msg.substring(NoSuchAccountException.MESSAGE.length()); @@ -71,6 +75,11 @@ public abstract class GerritCallback<T> implements AsyncCallback<T> { && caught.getMessage().equals(NoSuchEntityException.MESSAGE); } + protected static boolean isInactiveAccount(final Throwable caught) { + return caught instanceof RemoteJsonException + && caught.getMessage().startsWith(InactiveAccountException.MESSAGE); + } + private static boolean isNoSuchAccount(final Throwable caught) { return caught instanceof RemoteJsonException && caught.getMessage().startsWith(NoSuchAccountException.MESSAGE); diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountSuggestOracle.java index 441878f41a..bcf6438ec8 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountSuggestOracle.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountSuggestOracle.java @@ -30,7 +30,8 @@ public class AccountSuggestOracle extends HighlightSuggestOracle { public void onRequestSuggestions(final Request req, final Callback callback) { RpcStatus.hide(new Runnable() { public void run() { - SuggestUtil.SVC.suggestAccount(req.getQuery(), req.getLimit(), + SuggestUtil.SVC.suggestAccount(req.getQuery(), Boolean.TRUE, + req.getLimit(), new GerritCallback<List<AccountInfo>>() { public void onSuccess(final List<AccountInfo> result) { final ArrayList<AccountSuggestion> r = diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AddMemberBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AddMemberBox.java index b5e36558bf..b2d25e439f 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AddMemberBox.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AddMemberBox.java @@ -16,12 +16,10 @@ package com.google.gerrit.client.ui; import com.google.gerrit.client.Gerrit; import com.google.gerrit.client.admin.Util; -import com.google.gwt.event.dom.client.BlurEvent; -import com.google.gwt.event.dom.client.BlurHandler; +import com.google.gerrit.client.ui.HintTextBox; +import com.google.gerrit.client.ui.RPCSuggestOracle; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; -import com.google.gwt.event.dom.client.FocusEvent; -import com.google.gwt.event.dom.client.FocusHandler; import com.google.gwt.event.dom.client.KeyCodes; import com.google.gwt.event.dom.client.KeyPressEvent; import com.google.gwt.event.dom.client.KeyPressHandler; @@ -32,42 +30,23 @@ import com.google.gwt.user.client.ui.Composite; import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.SuggestBox; import com.google.gwt.user.client.ui.SuggestOracle.Suggestion; -import com.google.gwtexpui.globalkey.client.NpTextBox; public class AddMemberBox extends Composite { private final FlowPanel addPanel; private final Button addMember; - private final NpTextBox nameTxtBox; + private final HintTextBox nameTxtBox; private final SuggestBox nameTxt; private boolean submitOnSelection; public AddMemberBox() { addPanel = new FlowPanel(); addMember = new Button(Util.C.buttonAddGroupMember()); - nameTxtBox = new NpTextBox(); - nameTxt = new SuggestBox(new AccountSuggestOracle(), nameTxtBox); + nameTxtBox = new HintTextBox(); + nameTxt = new SuggestBox(new RPCSuggestOracle( + new AccountSuggestOracle()), nameTxtBox); nameTxtBox.setVisibleLength(50); - nameTxtBox.setText(Util.C.defaultAccountName()); - nameTxtBox.addStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint()); - nameTxtBox.addFocusHandler(new FocusHandler() { - @Override - public void onFocus(final FocusEvent event) { - if (Util.C.defaultAccountName().equals(nameTxtBox.getText())) { - nameTxtBox.setText(""); - nameTxtBox.removeStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint()); - } - } - }); - nameTxtBox.addBlurHandler(new BlurHandler() { - @Override - public void onBlur(final BlurEvent event) { - if ("".equals(nameTxtBox.getText())) { - nameTxtBox.setText(Util.C.defaultAccountName()); - nameTxtBox.addStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint()); - } - } - }); + nameTxtBox.setHintText(Util.C.defaultAccountName()); nameTxtBox.addKeyPressHandler(new KeyPressHandler() { @Override public void onKeyPress(KeyPressEvent event) { @@ -108,10 +87,7 @@ public class AddMemberBox extends Composite { public String getText() { String s = nameTxtBox.getText(); - if (s == null || s.equals(Util.C.defaultAccountName())) { - s = ""; - } - return s; + return s == null ? "" : s; } public void setEnabled(boolean enabled) { diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentPanel.java index b23bdba5a3..06c17e5c5b 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentPanel.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentPanel.java @@ -17,12 +17,20 @@ package com.google.gerrit.client.ui; import com.google.gerrit.client.FormatUtil; import com.google.gerrit.client.Gerrit; import com.google.gerrit.common.data.AccountInfo; +import com.google.gwt.event.dom.client.BlurEvent; +import com.google.gwt.event.dom.client.BlurHandler; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; import com.google.gwt.event.dom.client.DoubleClickEvent; import com.google.gwt.event.dom.client.DoubleClickHandler; +import com.google.gwt.event.dom.client.FocusEvent; +import com.google.gwt.event.dom.client.FocusHandler; +import com.google.gwt.event.dom.client.HasBlurHandlers; import com.google.gwt.event.dom.client.HasDoubleClickHandlers; +import com.google.gwt.event.dom.client.HasFocusHandlers; +import com.google.gwt.event.shared.HandlerManager; import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.user.client.ui.Button; import com.google.gwt.user.client.ui.Composite; import com.google.gwt.user.client.ui.FlexTable; import com.google.gwt.user.client.ui.FlowPanel; @@ -37,8 +45,10 @@ import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder; import java.util.Date; -public class CommentPanel extends Composite implements HasDoubleClickHandlers { +public class CommentPanel extends Composite implements HasDoubleClickHandlers, + HasFocusHandlers, FocusHandler, HasBlurHandlers, BlurHandler { private static final int SUMMARY_LENGTH = 75; + private final HandlerManager handlerManager = new HandlerManager(this); private final FlexTable header; private final InlineLabel messageSummary; private final FlowPanel content; @@ -132,7 +142,39 @@ public class CommentPanel extends Composite implements HasDoubleClickHandlers { } } - protected Panel getButtonPanel() { + /** + * Registers a {@link FocusHandler} for this comment panel. + * The comment panel is considered as being focused whenever any button in the + * comment panel gets focused. + * + * @param handler the focus handler to be registered + */ + @Override + public HandlerRegistration addFocusHandler(final FocusHandler handler) { + return handlerManager.addHandler(FocusEvent.getType(), handler); + } + + /** + * Registers a {@link BlurHandler} for this comment panel. + * The comment panel is considered as being blurred whenever any button in the + * comment panel gets blurred. + * + * @param handler the blur handler to be registered + */ + @Override + public HandlerRegistration addBlurHandler(final BlurHandler handler) { + return handlerManager.addHandler(BlurEvent.getType(), handler); + } + + protected void addButton(final Button button) { + // register focus and blur handler for each button, so that we can fire + // focus and blur events for the comment panel + button.addFocusHandler(this); + button.addBlurHandler(this); + getButtonPanel().add(button); + } + + private Panel getButtonPanel() { if (buttons == null) { buttons = new FlowPanel(); buttons.setStyleName(Gerrit.RESOURCES.css().commentPanelButtons()); @@ -141,6 +183,26 @@ public class CommentPanel extends Composite implements HasDoubleClickHandlers { return buttons; } + @Override + public void onFocus(final FocusEvent event) { + // a button was focused -> fire focus event for the comment panel + handlerManager.fireEvent(event); + } + + @Override + public void onBlur(final BlurEvent event) { + // a button was blurred -> fire blur event for the comment panel + handlerManager.fireEvent(event); + } + + protected void enableButtons(final boolean on) { + for (Widget w : getButtonPanel()) { + if (w instanceof Button) { + ((Button) w).setEnabled(on); + } + } + } + private static String summarize(final String message) { if (message.length() < SUMMARY_LENGTH) { return message; diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ComplexDisclosurePanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ComplexDisclosurePanel.java index c303e1cf45..c93969db87 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ComplexDisclosurePanel.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ComplexDisclosurePanel.java @@ -38,7 +38,8 @@ public class ComplexDisclosurePanel extends Composite implements // other modification of its header. We're stuck with injecting // into the DOM directly. // - main = new DisclosurePanel(text, isOpen); + main = new DisclosurePanel(text); + main.setOpen(isOpen); final Element headerParent; { final Element table = main.getElement(); diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/HintTextBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/HintTextBox.java new file mode 100644 index 0000000000..6575b61d2d --- /dev/null +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/HintTextBox.java @@ -0,0 +1,211 @@ +// Copyright (C) 2010 The Android Open Source Project +// +// 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.google.gerrit.client.ui; + +import com.google.gerrit.client.Gerrit; + +import com.google.gwt.event.dom.client.BlurEvent; +import com.google.gwt.event.dom.client.BlurHandler; +import com.google.gwt.event.dom.client.FocusEvent; +import com.google.gwt.event.dom.client.FocusHandler; +import com.google.gwt.event.dom.client.KeyCodes; +import com.google.gwt.event.dom.client.KeyDownEvent; +import com.google.gwt.event.dom.client.KeyDownHandler; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwtexpui.globalkey.client.NpTextBox; +import com.google.gwt.user.client.ui.SuggestBox; +import com.google.gwt.user.client.ui.Widget; + + +public class HintTextBox extends NpTextBox { + private HandlerRegistration hintFocusHandler; + private HandlerRegistration hintBlurHandler; + private HandlerRegistration keyDownHandler; + + private String hintText; + private String hintStyleName = Gerrit.RESOURCES.css().inputFieldTypeHint(); + + private String prevText; + + private boolean hintOn; + private boolean isFocused; + + + public String getText() { + if (hintOn) { + return ""; + } + return super.getText(); + } + + public void setText(String text) { + focusHint(); + + super.setText(text); + prevText = text; + + if (! isFocused) { + blurHint(); + } + } + + public String getHintText() { + return hintText; + } + + public void setHintText(String text) { + if (text == null) { + if (hintText == null) { // was not set, still not set, no change. + return; + } + + // Clearing a previously set Hint + hintFocusHandler.removeHandler(); + hintFocusHandler = null; + hintBlurHandler.removeHandler(); + hintBlurHandler = null; + keyDownHandler.removeHandler(); + keyDownHandler = null; + hintText = null; + focusHint(); + + return; + } + + // Setting Hints + + if (hintText == null) { // first time (was not already set) + hintText = text; + + hintFocusHandler = addFocusHandler(new FocusHandler() { + @Override + public void onFocus(FocusEvent event) { + focusHint(); + prevText = getText(); + isFocused = true; + } + }); + + hintBlurHandler = addBlurHandler(new BlurHandler() { + @Override + public void onBlur(BlurEvent event) { + blurHint(); + isFocused = false; + } + }); + + /* + * There seems to be a strange bug (at least on firefox 3.5.9 ubuntu) with + * the textbox under the following circumstances: + * 1) The field is not focused with BText in it. + * 2) The field receives focus and a focus listener changes the text to FText + * 3) The ESC key is pressed and the value of the field has not changed + * (ever) from FText + * 4) BUG: The text value gets reset to BText! + * + * A counter to this bug seems to be to force setFocus(false) on ESC. + */ + + /* Chrome does not create a KeyPressEvent on ESC, so use KeyDownEvents */ + keyDownHandler = addKeyDownHandler(new KeyDownHandler() { + @Override + public void onKeyDown(final KeyDownEvent event) { + onKey(event.getNativeKeyCode()); + } + }); + + } else { // Changing an already set Hint + + focusHint(); + hintText = text; + } + + if (! isFocused) { + blurHint(); + } + } + + private void onKey(int key) { + if (key == KeyCodes.KEY_ESCAPE) { + setText(prevText); + + Widget p = getParent(); + if (p instanceof SuggestBox) { + // Since the text was changed, ensure that the SuggestBox is + // aware of this change so that it will refresh properly on + // the next keystroke. Without this, if the first keystroke + // recreates the same string as before ESC was pressed, the + // SuggestBox will think that the string has not changed, and + // it will not yet provide any Suggestions. + ((SuggestBox)p).showSuggestionList(); + + // The suggestion list lingers if we don't hide it. + ((SuggestBox)p).hideSuggestionList(); + } + + setFocus(false); + } + } + + public void setHintStyleName(String styleName) { + if (hintStyleName != null && hintOn) { + removeStyleName(hintStyleName); + } + + hintStyleName = styleName; + + if (styleName != null && hintOn) { + addStyleName(styleName); + } + } + + public String getHintStyleName() { + return hintStyleName; + } + + protected void blurHint() { + if (! hintOn && getHintText() != null && "".equals(super.getText())) { + hintOn = true; + super.setText(getHintText()); + if (getHintStyleName() != null) { + addStyleName(getHintStyleName()); + } + } + } + + protected void focusHint() { + if (hintOn) { + super.setText(""); + if (getHintStyleName() != null) { + removeStyleName(getHintStyleName()); + } + hintOn = false; + } + } + + public void setFocus(boolean focus) { + super.setFocus(focus); + + if (focus != isFocused) { + if (focus) { + focusHint(); + } else { + blurHint(); + } + } + + isFocused = focus; + } +} diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ListenableAccountDiffPreference.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ListenableAccountDiffPreference.java new file mode 100644 index 0000000000..195adf5e8b --- /dev/null +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ListenableAccountDiffPreference.java @@ -0,0 +1,30 @@ +// Copyright (C) 2010 The Android Open Source Project +// +// 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.google.gerrit.client.ui; + +import com.google.gerrit.client.Gerrit; +import com.google.gerrit.reviewdb.AccountDiffPreference; + +public class ListenableAccountDiffPreference + extends ListenableValue<AccountDiffPreference> { + + public ListenableAccountDiffPreference() { + if (Gerrit.isSignedIn() && Gerrit.getAccountDiffPreference() != null) { + set(Gerrit.getAccountDiffPreference()); + } else { + set(AccountDiffPreference.createDefault(null)); + } + } +} diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ListenableValue.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ListenableValue.java new file mode 100644 index 0000000000..6dad875b60 --- /dev/null +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ListenableValue.java @@ -0,0 +1,48 @@ +// Copyright (C) 2010 The Android Open Source Project +// +// 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.google.gerrit.client.ui; + +import com.google.gwt.event.logical.shared.HasValueChangeHandlers; +import com.google.gwt.event.logical.shared.ValueChangeEvent; +import com.google.gwt.event.logical.shared.ValueChangeHandler; +import com.google.gwt.event.shared.GwtEvent; +import com.google.gwt.event.shared.HandlerManager; +import com.google.gwt.event.shared.HandlerRegistration; + + +public class ListenableValue<T> implements HasValueChangeHandlers<T> { + + private HandlerManager manager = new HandlerManager(this); + + private T value; + + public T get() { + return value; + } + + public void set(final T value) { + this.value = value; + fireEvent(new ValueChangeEvent<T>(value) {}); + } + + public void fireEvent(GwtEvent<?> event) { + manager.fireEvent(event); + } + + public HandlerRegistration addValueChangeHandler( + ValueChangeHandler<T> handler) { + return manager.addHandler(ValueChangeEvent.getType(), handler); + } +} diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/OnEditEnabler.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/OnEditEnabler.java new file mode 100644 index 0000000000..932fb5e274 --- /dev/null +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/OnEditEnabler.java @@ -0,0 +1,169 @@ +// Copyright (C) 2010 The Android Open Source Project +// +// 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.google.gerrit.client.ui; + +import com.google.gwt.event.dom.client.ChangeEvent; +import com.google.gwt.event.dom.client.ChangeHandler; +import com.google.gwt.event.dom.client.FocusEvent; +import com.google.gwt.event.dom.client.FocusHandler; +import com.google.gwt.event.dom.client.KeyDownEvent; +import com.google.gwt.event.dom.client.KeyDownHandler; +import com.google.gwt.event.dom.client.KeyPressEvent; +import com.google.gwt.event.dom.client.KeyPressHandler; +import com.google.gwt.event.dom.client.MouseUpEvent; +import com.google.gwt.event.dom.client.MouseUpHandler; + +import com.google.gwt.event.logical.shared.ValueChangeEvent; +import com.google.gwt.event.logical.shared.ValueChangeHandler; +import com.google.gwt.event.shared.GwtEvent; +import com.google.gwt.user.client.Command; +import com.google.gwt.user.client.DeferredCommand; +import com.google.gwt.user.client.ui.CheckBox; +import com.google.gwt.user.client.ui.FocusWidget; +import com.google.gwt.user.client.ui.ListBox; +import com.google.gwt.user.client.ui.TextBoxBase; + +import java.util.HashMap; +import java.util.Map; + + +/** Enables a FocusWidget (e.g. a Button) if an edit is detected from any + * registered input widget. + */ +public class OnEditEnabler implements KeyPressHandler, KeyDownHandler, + MouseUpHandler, ChangeHandler, ValueChangeHandler { + + private final FocusWidget widget; + private Map<TextBoxBase, String> strings = new HashMap<TextBoxBase, String>(); + + + // The first parameter to the contructors must be the FocusWidget to enable, + // subsequent parameters are widgets to listenTo. + + public OnEditEnabler(final FocusWidget w, final TextBoxBase tb) { + this(w); + listenTo(tb); + } + + public OnEditEnabler(final FocusWidget w, final ListBox lb) { + this(w); + listenTo(lb); + } + + public OnEditEnabler(final FocusWidget w, final CheckBox cb) { + this(w); + listenTo(cb); + } + + public OnEditEnabler(final FocusWidget w) { + widget = w; + } + + + // Register input widgets to be listened to + + public void listenTo(final TextBoxBase tb) { + strings.put(tb, tb.getText()); + tb.addKeyPressHandler(this); + + // Is there another way to capture middle button X11 pastes in browsers + // which do not yet support ONPASTE events (Firefox)? + tb.addMouseUpHandler(this); + + // Resetting the "original text" on focus ensures that we are + // up to date with non-user updates of the text (calls to + // setText()...) and also up to date with user changes which + // occured after enabling "widget". + tb.addFocusHandler(new FocusHandler() { + @Override + public void onFocus(FocusEvent event) { + strings.put(tb, tb.getText()); + } + }); + + // CTRL-V Pastes in Chrome seem only detectable via BrowserEvents or + // KeyDownEvents, the latter is better. + tb.addKeyDownHandler(this); + } + + public void listenTo(final ListBox lb) { + lb.addChangeHandler(this); + } + + public void listenTo(final CheckBox cb) { + cb.addValueChangeHandler(this); + } + + + // Handlers + + @Override + public void onKeyPress(final KeyPressEvent e) { + on(e); + } + + @Override + public void onKeyDown(final KeyDownEvent e) { + on(e); + } + + @Override + public void onMouseUp(final MouseUpEvent e) { + on(e); + } + + @Override + public void onChange(final ChangeEvent e) { + on(e); + } + + @Override + public void onValueChange(final ValueChangeEvent e) { + on(e); + } + + private void on(final GwtEvent e) { + if (widget.isEnabled() || + ! (e.getSource() instanceof FocusWidget) || + ! ((FocusWidget) e.getSource()).isEnabled() ) { + return; + } + + if (e.getSource() instanceof TextBoxBase) { + onTextBoxBase((TextBoxBase) e.getSource()); + } else { + // For many widgets, we can assume that a change is an edit. If + // a widget does not work that way, it should be special cased + // above. + widget.setEnabled(true); + } + } + + private void onTextBoxBase(final TextBoxBase tb) { + // The text appears to not get updated until the handlers complete. + DeferredCommand.add(new Command() { + @Override + public void execute() { + String orig = strings.get(tb); + if (orig == null) { + orig = ""; + } + if (! orig.equals(tb.getText())) { + widget.setEnabled(true); + } + } + }); + } +} diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectConstants.java new file mode 100644 index 0000000000..8b13392e1a --- /dev/null +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectConstants.java @@ -0,0 +1,25 @@ +// Copyright (C) 2010 The Android Open Source Project +// +// 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.google.gerrit.client.ui; + +import com.google.gwt.i18n.client.Constants; + +public interface ProjectConstants extends Constants { + String projectName(); + String projectDescription(); + String projectListOpen(); + String projectListPrev(); + String projectListNext(); +} diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectConstants.properties new file mode 100644 index 0000000000..15de117e9a --- /dev/null +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectConstants.properties @@ -0,0 +1,5 @@ +projectName = Project Name +projectDescription = Project Description +projectListOpen = Select project +projectListPrev = Previous project +projectListNext = Next project diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectsTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectsTable.java new file mode 100644 index 0000000000..96089b902b --- /dev/null +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectsTable.java @@ -0,0 +1,122 @@ +// Copyright (C) 2010 The Android Open Source Project +// +// 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.google.gerrit.client.ui; + +import com.google.gerrit.client.Gerrit; +import com.google.gerrit.client.ui.NavigationTable; +import com.google.gerrit.reviewdb.Project; +import com.google.gwt.event.dom.client.KeyCodes; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter; + +import java.util.List; + +public class ProjectsTable extends NavigationTable<Project> { + + public ProjectsTable() { + keysNavigation.add(new PrevKeyCommand(0, 'k', Util.C.projectListPrev())); + keysNavigation.add(new NextKeyCommand(0, 'j', Util.C.projectListNext())); + keysNavigation.add(new OpenKeyCommand(0, 'o', Util.C.projectListOpen())); + keysNavigation.add(new OpenKeyCommand(0, KeyCodes.KEY_ENTER, + Util.C.projectListOpen())); + + table.setText(0, 1, Util.C.projectName()); + table.setText(0, 2, Util.C.projectDescription()); + + final FlexCellFormatter fmt = table.getFlexCellFormatter(); + fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().dataHeader()); + fmt.addStyleName(0, 2, Gerrit.RESOURCES.css().dataHeader()); + } + + protected MyFlexTable createFlexTable() { + MyFlexTable table = new MyFlexTable() { + @Override + public void onBrowserEvent(final Event event) { + switch (DOM.eventGetType(event)) { + case Event.ONCLICK: { + // Find out which cell was actually clicked. + final Element td = getEventTargetCell(event); + if (td == null) { + break; + } + final int row = rowOf(td); + if (getRowItem(row) != null) { + ProjectsTable.this.movePointerTo(row); + return; + } + break; + } + case Event.ONDBLCLICK: { + // Find out which cell was actually clicked. + Element td = getEventTargetCell(event); + if (td == null) { + return; + } + onOpenRow(rowOf(td)); + return; + } + } + super.onBrowserEvent(event); + } + }; + + table.sinkEvents(Event.ONDBLCLICK | Event.ONCLICK); + return table; + } + + @Override + protected Object getRowItemKey(final Project item) { + return item.getNameKey(); + } + + @Override + protected void onOpenRow(final int row) { + if (row > 0) { + movePointerTo(row); + } + } + + public void display(final List<Project> projects) { + while (1 < table.getRowCount()) + table.removeRow(table.getRowCount() - 1); + + for (final Project k : projects) + insert(table.getRowCount(), k); + + finishDisplay(); + } + + protected void insert(final int row, final Project k) { + table.insertRow(row); + + applyDataRowStyle(row); + + final FlexCellFormatter fmt = table.getFlexCellFormatter(); + fmt.addStyleName(row, 1, Gerrit.RESOURCES.css().dataCell()); + fmt.addStyleName(row, 1, Gerrit.RESOURCES.css().cPROJECT()); + fmt.addStyleName(row, 2, Gerrit.RESOURCES.css().dataCell()); + + populate(row, k); + } + + protected void populate(final int row, final Project k) { + table.setText(row, 1, k.getName()); + table.setText(row, 2, k.getDescription()); + + setRowItem(row, k); + } +} diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/RPCSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/RPCSuggestOracle.java new file mode 100644 index 0000000000..cc3d510ef7 --- /dev/null +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/RPCSuggestOracle.java @@ -0,0 +1,59 @@ +// Copyright (C) 2010 The Android Open Source Project +// +// 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.google.gerrit.client.ui; + +import com.google.gwt.user.client.ui.SuggestOracle; +import com.google.gwt.user.client.ui.SuggestOracle.Callback; +import com.google.gwt.user.client.ui.SuggestOracle.Request; +import com.google.gwt.user.client.ui.SuggestOracle.Response; + +/** This class will proxy SuggestOracle requests to another SuggestOracle + * while keeping track of the latest request. Any repsonse that belongs + * to a request which is not the latest request will be dropped to prevent + * invalid deliveries. + */ + +public class RPCSuggestOracle extends SuggestOracle { + + private SuggestOracle oracle; + private SuggestOracle.Request request; + private SuggestOracle.Callback callback; + private SuggestOracle.Callback myCallback = new SuggestOracle.Callback() { + public void onSuggestionsReady(SuggestOracle.Request req, + SuggestOracle.Response response) { + if (request == req) { + callback.onSuggestionsReady(req, response); + request = null; + callback = null; + } + } + }; + + + public RPCSuggestOracle(SuggestOracle ora) { + oracle = ora; + } + + public void requestSuggestions(SuggestOracle.Request req, + SuggestOracle.Callback cb) { + request = req; + callback = cb; + oracle.requestSuggestions(req, myCallback); + } + + public boolean isDisplayStringHTML() { + return oracle.isDisplayStringHTML(); + } +} diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/TextSaveButtonListener.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/TextSaveButtonListener.java deleted file mode 100644 index 63270df8fa..0000000000 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/TextSaveButtonListener.java +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (C) 2008 The Android Open Source Project -// -// 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.google.gerrit.client.ui; - -import com.google.gwt.event.dom.client.KeyCodes; -import com.google.gwt.event.dom.client.KeyPressEvent; -import com.google.gwt.event.dom.client.KeyPressHandler; -import com.google.gwt.user.client.ui.FocusWidget; -import com.google.gwt.user.client.ui.TextBoxBase; - -/** Enables an action (e.g. a Button) if the text box is modified. */ -public class TextSaveButtonListener implements KeyPressHandler { - private final FocusWidget descAction; - - public TextSaveButtonListener(final FocusWidget action) { - descAction = action; - } - - public TextSaveButtonListener(final TextBoxBase text, final FocusWidget action) { - this(action); - text.addKeyPressHandler(this); - } - - @Override - public void onKeyPress(final KeyPressEvent e) { - if (descAction.isEnabled()) { - // Do nothing, its already enabled. - } else if (e.isControlKeyDown() || e.isAltKeyDown() || e.isMetaKeyDown()) { - switch (e.getCharCode()) { - case 'v': - case 'x': - on(e); - break; - } - } else { - switch (e.getCharCode()) { - case KeyCodes.KEY_UP: - case KeyCodes.KEY_DOWN: - case KeyCodes.KEY_LEFT: - case KeyCodes.KEY_RIGHT: - case KeyCodes.KEY_HOME: - case KeyCodes.KEY_END: - case KeyCodes.KEY_PAGEUP: - case KeyCodes.KEY_PAGEDOWN: - case KeyCodes.KEY_ALT: - case KeyCodes.KEY_CTRL: - case KeyCodes.KEY_SHIFT: - break; - default: - on(e); - break; - } - } - } - - private void on(final KeyPressEvent e) { - descAction.setEnabled(((TextBoxBase) e.getSource()).isEnabled()); - } -} diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Util.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Util.java new file mode 100644 index 0000000000..ebed764472 --- /dev/null +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Util.java @@ -0,0 +1,21 @@ +// Copyright (C) 2010 The Android Open Source Project +// +// 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.google.gerrit.client.ui; + +import com.google.gwt.core.client.GWT; + +public class Util { + public static final ProjectConstants C = GWT.create(ProjectConstants.class); +} diff --git a/gerrit-httpd/pom.xml b/gerrit-httpd/pom.xml index b02b142562..39b2788736 100644 --- a/gerrit-httpd/pom.xml +++ b/gerrit-httpd/pom.xml @@ -22,7 +22,7 @@ limitations under the License. <parent> <groupId>com.google.gerrit</groupId> <artifactId>gerrit-parent</artifactId> - <version>2.1.4-SNAPSHOT</version> + <version>2.1-SNAPSHOT</version> </parent> <artifactId>gerrit-httpd</artifactId> diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ChangeQueryServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ChangeQueryServlet.java index b79971c03a..97debbceb1 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ChangeQueryServlet.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ChangeQueryServlet.java @@ -70,6 +70,7 @@ public class ChangeQueryServlet extends HttpServlet { p.setIncludeCurrentPatchSet(get(req, "current-patch-set", false)); p.setIncludePatchSets(get(req, "patch-sets", false)); + p.setIncludeApprovals(get(req, "all-approvals", false)); p.setOutput(rsp.getOutputStream(), format); p.query(get(req, "q", "status:open")); } diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectDigestFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectDigestFilter.java index 9d9daec2f2..f67f12fd37 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectDigestFilter.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectDigestFilter.java @@ -133,7 +133,7 @@ class ProjectDigestFilter implements Filter { } final AccountState who = accountCache.getByUsername(username); - if (who == null) { + if (who == null || ! who.getAccount().isActive()) { rsp.sendError(SC_UNAUTHORIZED); return false; } diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectServlet.java index 3f7f68d527..09e2cce8ef 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectServlet.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectServlet.java @@ -23,6 +23,7 @@ import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.config.CanonicalWebUrl; import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.git.ReceiveCommits; +import com.google.gerrit.server.git.TransferConfig; import com.google.gerrit.server.git.VisibleRefFilter; import com.google.gerrit.server.project.NoSuchProjectException; import com.google.gerrit.server.project.ProjectControl; @@ -40,6 +41,7 @@ import org.eclipse.jgit.http.server.resolver.ServiceNotAuthorizedException; import org.eclipse.jgit.http.server.resolver.ServiceNotEnabledException; import org.eclipse.jgit.http.server.resolver.UploadPackFactory; import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.storage.pack.PackConfig; import org.eclipse.jgit.transport.ReceivePack; import org.eclipse.jgit.transport.UploadPack; import org.slf4j.Logger; @@ -175,19 +177,26 @@ public class ProjectServlet extends GitServlet { static class Upload implements UploadPackFactory { private final Provider<ReviewDb> db; + private final PackConfig packConfig; @Inject - Upload(final Provider<ReviewDb> db) { + Upload(final Provider<ReviewDb> db, final TransferConfig tc) { this.db = db; + this.packConfig = tc.getPackConfig(); } @Override public UploadPack create(HttpServletRequest req, Repository repo) - throws ServiceNotEnabledException { + throws ServiceNotEnabledException, ServiceNotAuthorizedException { + ProjectControl pc = getProjectControl(req); + if (!pc.canRunUploadPack()) { + throw new ServiceNotAuthorizedException(); + } + // The Resolver above already checked READ access for us. // - ProjectControl pc = getProjectControl(req); UploadPack up = new UploadPack(repo); + up.setPackConfig(packConfig); if (!pc.allRefsAreVisible()) { up.setRefFilter(new VisibleRefFilter(repo, pc, db.get())); } @@ -207,6 +216,10 @@ public class ProjectServlet extends GitServlet { public ReceivePack create(HttpServletRequest req, Repository db) throws ServiceNotEnabledException, ServiceNotAuthorizedException { final ProjectControl pc = getProjectControl(req); + if (!pc.canRunReceivePack()) { + throw new ServiceNotAuthorizedException(); + } + if (pc.getCurrentUser() instanceof IdentifiedUser) { final IdentifiedUser user = (IdentifiedUser) pc.getCurrentUser(); final ReceiveCommits rc = factory.create(pc, db); diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java index 93b6d09a0d..cc2e144742 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java @@ -19,6 +19,7 @@ import static com.google.inject.Scopes.SINGLETON; import com.google.gerrit.common.data.GerritConfig; import com.google.gerrit.httpd.auth.become.BecomeAnyAccountLoginServlet; import com.google.gerrit.httpd.auth.container.HttpAuthModule; +import com.google.gerrit.httpd.auth.container.HttpsClientSslCertModule; import com.google.gerrit.httpd.auth.ldap.LdapAuthModule; import com.google.gerrit.httpd.auth.openid.OpenIdModule; import com.google.gerrit.httpd.gitweb.GitWebModule; @@ -101,6 +102,10 @@ public class WebModule extends FactoryModule { install(new HttpAuthModule()); break; + case CLIENT_SSL_CERT_LDAP: + install(new HttpsClientSslCertModule()); + break; + case LDAP: case LDAP_BIND: install(new LdapAuthModule()); diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertAuthFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertAuthFilter.java new file mode 100644 index 0000000000..381daa8c87 --- /dev/null +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertAuthFilter.java @@ -0,0 +1,94 @@ +// Copyright (C) 2010 The Android Open Source Project +// +// 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.google.gerrit.httpd.auth.container; + +import com.google.gerrit.httpd.WebSession; +import com.google.gerrit.server.account.AccountException; +import com.google.gerrit.server.account.AccountManager; +import com.google.gerrit.server.account.AuthRequest; +import com.google.gerrit.server.account.AuthResult; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.security.cert.X509Certificate; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + +@Singleton +class HttpsClientSslCertAuthFilter implements Filter { + + private static final Pattern REGEX_USERID = Pattern.compile("CN=([^,]*),.*"); + private static final Logger log = + LoggerFactory.getLogger(HttpsClientSslCertAuthFilter.class); + + private final Provider<WebSession> webSession; + private final AccountManager accountManager; + + @Inject + HttpsClientSslCertAuthFilter(final Provider<WebSession> webSession, + final AccountManager accountManager) { + this.webSession = webSession; + this.accountManager = accountManager; + } + + @Override + public void destroy() { + } + + @Override + public void doFilter(ServletRequest req, ServletResponse rsp, + FilterChain chain) throws IOException, ServletException { + X509Certificate[] certs = (X509Certificate[]) req.getAttribute("javax.servlet.request.X509Certificate"); + if (certs == null || certs.length == 0) { + throw new ServletException( + "Couldn't get the attribute javax.servlet.request.X509Certificate from the request"); + } + String name = certs[0].getSubjectDN().getName(); + Matcher m = REGEX_USERID.matcher(name); + String userName; + if (m.matches()) { + userName = m.group(1); + } else { + throw new ServletException("Couldn't extract username from your certificate"); + } + final AuthRequest areq = AuthRequest.forUser(userName); + final AuthResult arsp; + try { + arsp = accountManager.authenticate(areq); + } catch (AccountException e) { + String err = "Unable to authenticate user \"" + userName + "\""; + log.error(err, e); + throw new ServletException(err, e); + } + webSession.get().login(arsp, true); + chain.doFilter(req, rsp); + } + + @Override + public void init(FilterConfig arg0) throws ServletException { + } +} diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertModule.java new file mode 100644 index 0000000000..f0976f3ed5 --- /dev/null +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertModule.java @@ -0,0 +1,25 @@ +// Copyright (C) 2010 The Android Open Source Project +// +// 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.google.gerrit.httpd.auth.container; + +import com.google.inject.servlet.ServletModule; + +/** Servlets and support related to CLIENT_SSL_CERT_LDAP authentication. */ +public class HttpsClientSslCertModule extends ServletModule { + @Override + protected void configureServlets() { + filter("/").through(HttpsClientSslCertAuthFilter.class); + } +} diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/ldap/UserPassAuthServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/ldap/UserPassAuthServiceImpl.java index a59d013188..e4577c1fc6 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/ldap/UserPassAuthServiceImpl.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/ldap/UserPassAuthServiceImpl.java @@ -19,6 +19,7 @@ import com.google.gerrit.common.auth.userpass.UserPassAuthService; import com.google.gerrit.httpd.WebSession; import com.google.gerrit.server.account.AccountException; import com.google.gerrit.server.account.AccountManager; +import com.google.gerrit.server.account.AccountUserNameException; import com.google.gerrit.server.account.AuthRequest; import com.google.gerrit.server.account.AuthResult; import com.google.gwt.user.client.rpc.AsyncCallback; @@ -55,6 +56,12 @@ class UserPassAuthServiceImpl implements UserPassAuthService { final AuthResult res; try { res = accountManager.authenticate(req); + } catch (AccountUserNameException e) { + // entered user name and password were correct, but user name could not be + // set for the newly created account and this is why the login fails, + // error screen with error message should be shown to the user + callback.onFailure(e); + return; } catch (AccountException e) { result.success = false; callback.onSuccess(result); diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitWebServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitWebServlet.java index a704476471..8991ea9b06 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitWebServlet.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitWebServlet.java @@ -344,11 +344,14 @@ class GitWebServlet extends HttpServlet { rsp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); return; } - - rsp.setHeader("Expires", "Fri, 01 Jan 1980 00:00:00 GMT"); - rsp.setHeader("Pragma", "no-cache"); - rsp.setHeader("Cache-Control", "no-cache, must-revalidate"); - exec(req, rsp, project, repo); + try { + rsp.setHeader("Expires", "Fri, 01 Jan 1980 00:00:00 GMT"); + rsp.setHeader("Pragma", "no-cache"); + rsp.setHeader("Cache-Control", "no-cache, must-revalidate"); + exec(req, rsp, project, repo); + } finally { + repo.close(); + } } private static Map<String, String> getParameters(final HttpServletRequest req) diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/CatServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/CatServlet.java index 3ecfa627c7..832ae991e5 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/CatServlet.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/CatServlet.java @@ -33,14 +33,16 @@ import eu.medsea.mimeutil.MimeType; import org.eclipse.jgit.errors.RepositoryNotFoundException; 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.Repository; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.util.NB; -import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.security.MessageDigest; @@ -162,45 +164,50 @@ public class CatServlet extends HttpServlet { return; } - final byte[] blobData; + final ObjectLoader blobLoader; final RevCommit fromCommit; final String suffix; final String path = patchKey.getFileName(); try { - final RevWalk rw = new RevWalk(repo); - final RevCommit c; - final TreeWalk tw; - - c = rw.parseCommit(ObjectId.fromString(patchSet.getRevision().get())); - if (side == 0) { - fromCommit = c; - suffix = "new"; - - } else if (1 <= side && side - 1 < c.getParentCount()) { - fromCommit = rw.parseCommit(c.getParent(side - 1)); - if (c.getParentCount() == 1) { - suffix = "old"; + final ObjectReader reader = repo.newObjectReader(); + try { + final RevWalk rw = new RevWalk(reader); + final RevCommit c; + final TreeWalk tw; + + c = rw.parseCommit(ObjectId.fromString(patchSet.getRevision().get())); + if (side == 0) { + fromCommit = c; + suffix = "new"; + + } else if (1 <= side && side - 1 < c.getParentCount()) { + fromCommit = rw.parseCommit(c.getParent(side - 1)); + if (c.getParentCount() == 1) { + suffix = "old"; + } else { + suffix = "old" + side; + } + } else { - suffix = "old" + side; + rsp.sendError(HttpServletResponse.SC_NOT_FOUND); + return; } - } else { - rsp.sendError(HttpServletResponse.SC_NOT_FOUND); - return; - } - - tw = TreeWalk.forPath(repo, path, fromCommit.getTree()); - if (tw == null) { - rsp.sendError(HttpServletResponse.SC_NOT_FOUND); - return; - } + tw = TreeWalk.forPath(reader, path, fromCommit.getTree()); + if (tw == null) { + rsp.sendError(HttpServletResponse.SC_NOT_FOUND); + return; + } - if (tw.getFileMode(0).getObjectType() == Constants.OBJ_BLOB) { - blobData = repo.openBlob(tw.getObjectId(0)).getCachedBytes(); + if (tw.getFileMode(0).getObjectType() == Constants.OBJ_BLOB) { + blobLoader = reader.open(tw.getObjectId(0), Constants.OBJ_BLOB); - } else { - rsp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); - return; + } else { + rsp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + return; + } + } finally { + reader.release(); } } catch (IOException e) { getServletContext().log("Cannot read repository", e); @@ -214,14 +221,20 @@ public class CatServlet extends HttpServlet { repo.close(); } + final byte[] raw = + blobLoader.isLarge() ? null : blobLoader.getCachedBytes(); final long when = fromCommit.getCommitTime() * 1000L; - MimeType contentType = registry.getMimeType(path, blobData); - final byte[] outData; - if (registry.isSafeInline(contentType)) { - outData = blobData; + rsp.setDateHeader("Last-Modified", when); + rsp.setDateHeader("Expires", 0L); + rsp.setHeader("Pragma", "no-cache"); + rsp.setHeader("Cache-Control", "no-cache, must-revalidate"); + + OutputStream out; + ZipOutputStream zo; - } else { + final MimeType contentType = registry.getMimeType(path, raw); + if (!registry.isSafeInline(contentType)) { // The content may not be safe to transmit inline, as a browser might // interpret it as HTML or JavaScript hosted by this site. Such code // might then run in the site's security domain, and may be able to use @@ -230,31 +243,38 @@ public class CatServlet extends HttpServlet { // Usually, wrapping the content into a ZIP file forces the browser to // save the content to the local system instead. // - final ByteArrayOutputStream zip = new ByteArrayOutputStream(); - final ZipOutputStream zo = new ZipOutputStream(zip); + + rsp.setContentType(ZIP.toString()); + rsp.setHeader("Content-Disposition", "attachment; filename=\"" + + safeFileName(path, suffix) + ".zip" + "\""); + + zo = new ZipOutputStream(rsp.getOutputStream()); + final ZipEntry e = new ZipEntry(safeFileName(path, rand(req, suffix))); e.setComment(fromCommit.name() + ":" + path); - e.setSize(blobData.length); + e.setSize(blobLoader.getSize()); e.setTime(when); zo.putNextEntry(e); - zo.write(blobData); - zo.closeEntry(); - zo.close(); + out = zo; + + } else { + rsp.setContentType(contentType.toString()); + rsp.setHeader("Content-Length", "" + blobLoader.getSize()); - outData = zip.toByteArray(); - contentType = ZIP; + out = rsp.getOutputStream(); + zo = null; + } - rsp.setHeader("Content-Disposition", "attachment; filename=\"" - + safeFileName(path, suffix) + ".zip" + "\""); + if (raw != null) { + out.write(raw); + } else { + blobLoader.copyTo(out); } - rsp.setContentType(contentType.toString()); - rsp.setContentLength(outData.length); - rsp.setDateHeader("Last-Modified", when); - rsp.setDateHeader("Expires", 0L); - rsp.setHeader("Pragma", "no-cache"); - rsp.setHeader("Cache-Control", "no-cache, must-revalidate"); - rsp.getOutputStream().write(outData); + if (zo != null) { + zo.closeEntry(); + } + out.close(); } private static String safeFileName(String fileName, final String suffix) { diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestServiceImpl.java index f9721f8647..864c550ded 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestServiceImpl.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestServiceImpl.java @@ -33,6 +33,7 @@ import com.google.inject.Provider; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; class SuggestServiceImpl extends BaseServiceImplementation implements SuggestService { @@ -74,8 +75,8 @@ class SuggestServiceImpl extends BaseServiceImplementation implements }); } - public void suggestAccount(final String query, final int limit, - final AsyncCallback<List<AccountInfo>> callback) { + public void suggestAccount(final String query, final Boolean active, + final int limit, final AsyncCallback<List<AccountInfo>> callback) { run(callback, new Action<List<AccountInfo>>() { public List<AccountInfo> run(final ReviewDb db) throws OrmException { final String a = query; @@ -86,12 +87,12 @@ class SuggestServiceImpl extends BaseServiceImplementation implements final LinkedHashMap<Account.Id, AccountInfo> r = new LinkedHashMap<Account.Id, AccountInfo>(); for (final Account p : db.accounts().suggestByFullName(a, b, n)) { - r.put(p.getId(), new AccountInfo(p)); + addSuggestion(r, p, new AccountInfo(p), active); } if (r.size() < n) { for (final Account p : db.accounts().suggestByPreferredEmail(a, b, n - r.size())) { - r.put(p.getId(), new AccountInfo(p)); + addSuggestion(r, p, new AccountInfo(p), active); } } if (r.size() < n) { @@ -101,7 +102,7 @@ class SuggestServiceImpl extends BaseServiceImplementation implements final Account p = accountCache.get(e.getAccountId()).getAccount(); final AccountInfo info = new AccountInfo(p); info.setPreferredEmail(e.getEmailAddress()); - r.put(e.getAccountId(), info); + addSuggestion(r, p, info, active); } } } @@ -110,6 +111,13 @@ class SuggestServiceImpl extends BaseServiceImplementation implements }); } + private void addSuggestion(Map map, Account account, AccountInfo info, + Boolean active) { + if (active == null || active == account.isActive()) { + map.put(account.getId(), info); + } + } + public void suggestAccountGroup(final String query, final int limit, final AsyncCallback<List<AccountGroupName>> callback) { run(callback, new Action<List<AccountGroupName>>() { diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/GroupAdminServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/GroupAdminServiceImpl.java index 4e4f82e7f8..870d77ce91 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/GroupAdminServiceImpl.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/GroupAdminServiceImpl.java @@ -16,6 +16,7 @@ package com.google.gerrit.httpd.rpc.account; import com.google.gerrit.common.data.GroupAdminService; import com.google.gerrit.common.data.GroupDetail; +import com.google.gerrit.common.errors.InactiveAccountException; import com.google.gerrit.common.errors.NameAlreadyUsedException; import com.google.gerrit.common.errors.NoSuchAccountException; import com.google.gerrit.common.errors.NoSuchEntityException; @@ -221,6 +222,9 @@ class GroupAdminServiceImpl extends BaseServiceImplementation implements } final Account a = findAccount(nameOrEmail); + if (!a.isActive()) { + throw new Failure(new InactiveAccountException(a.getFullName())); + } if (!control.canAdd(a.getId())) { throw new Failure(new NoSuchEntityException()); } diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/IncludedInDetailFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/IncludedInDetailFactory.java index ec0f2255a4..7d6f764777 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/IncludedInDetailFactory.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/IncludedInDetailFactory.java @@ -80,22 +80,26 @@ class IncludedInDetailFactory extends Handler<IncludedInDetail> { repoManager.openRepository(control.getProject().getName()); try { final RevWalk rw = new RevWalk(repo); - rw.setRetainBody(false); - - final RevCommit rev; try { - rev = rw.parseCommit(ObjectId.fromString(patch.getRevision().get())); - } catch (IncorrectObjectTypeException err) { - throw new InvalidRevisionException(); - } catch (MissingObjectException err) { - throw new InvalidRevisionException(); + rw.setRetainBody(false); + + final RevCommit rev; + try { + rev = rw.parseCommit(ObjectId.fromString(patch.getRevision().get())); + } catch (IncorrectObjectTypeException err) { + throw new InvalidRevisionException(); + } catch (MissingObjectException err) { + throw new InvalidRevisionException(); + } + + detail = new IncludedInDetail(); + detail.setBranches(includedIn(repo, rw, rev, Constants.R_HEADS)); + detail.setTags(includedIn(repo, rw, rev, Constants.R_TAGS)); + + return detail; + } finally { + rw.release(); } - - detail = new IncludedInDetail(); - detail.setBranches(includedIn(repo, rw, rev, Constants.R_HEADS)); - detail.setTags(includedIn(repo, rw, rev, Constants.R_TAGS)); - - return detail; } finally { repo.close(); } diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/AddReviewer.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/AddReviewer.java index ae36388dc9..9d508e2fcf 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/AddReviewer.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/AddReviewer.java @@ -94,6 +94,11 @@ class AddReviewer extends Handler<ReviewerResult> { ReviewerResult.Error.Type.ACCOUNT_NOT_FOUND, nameOrEmail)); continue; } + if (!account.isActive()) { + result.addError(new ReviewerResult.Error( + ReviewerResult.Error.Type.ACCOUNT_INACTIVE, nameOrEmail)); + continue; + } final IdentifiedUser user = identifiedUserFactory.create(account.getId()); if (!control.forUser(user).isVisible()) { diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptBuilder.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptBuilder.java index 0f557ee630..730d2699da 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptBuilder.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptBuilder.java @@ -24,7 +24,6 @@ import com.google.gerrit.reviewdb.Change; import com.google.gerrit.reviewdb.Patch; import com.google.gerrit.reviewdb.PatchLineComment; import com.google.gerrit.reviewdb.AccountDiffPreference.Whitespace; -import com.google.gerrit.reviewdb.Patch.PatchType; import com.google.gerrit.server.FileTypeRegistry; import com.google.gerrit.server.patch.PatchListEntry; import com.google.gerrit.server.patch.Text; @@ -40,7 +39,7 @@ import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.lib.ObjectId; -import org.eclipse.jgit.lib.ObjectLoader; +import org.eclipse.jgit.lib.ObjectReader; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevTree; import org.eclipse.jgit.revwalk.RevWalk; @@ -64,6 +63,7 @@ class PatchScriptBuilder { }; private Repository db; + private ObjectReader reader; private Change change; private AccountDiffPreference diffPrefs; private boolean againstParent; @@ -112,16 +112,17 @@ class PatchScriptBuilder { PatchScript toPatchScript(final PatchListEntry content, final boolean intralineDifference, final CommentDetail comments, final List<Patch> history) throws IOException { - if (content.getPatchType() == PatchType.N_WAY) { - // For a diff --cc format we don't support converting it into - // a patch script. Instead treat everything as a file header. - // - return new PatchScript(change.getKey(), content.getChangeType(), content - .getOldName(), content.getNewName(), content.getHeaderLines(), - diffPrefs, a.dst, b.dst, Collections.<Edit> emptyList(), - a.displayMethod, b.displayMethod, comments, history, false, false); + reader = db.newObjectReader(); + try { + return build(content, intralineDifference, comments, history); + } finally { + reader.release(); } + } + private PatchScript build(final PatchListEntry content, + final boolean intralineDifference, final CommentDetail comments, + final List<Patch> history) throws IOException { a.path = oldName(content); b.path = newName(content); @@ -167,9 +168,9 @@ class PatchScriptBuilder { } return new PatchScript(change.getKey(), content.getChangeType(), content - .getOldName(), content.getNewName(), content.getHeaderLines(), - diffPrefs, a.dst, b.dst, edits, a.displayMethod, b.displayMethod, - comments, history, hugeFile, intralineDifference); + .getOldName(), content.getNewName(), a.fileMode, b.fileMode, content + .getHeaderLines(), diffPrefs, a.dst, b.dst, edits, a.displayMethod, + b.displayMethod, comments, history, hugeFile, intralineDifference); } private static String oldName(final PatchListEntry entry) { @@ -318,14 +319,14 @@ class PatchScriptBuilder { for (final EditList.Hunk hunk : list.getHunks()) { while (hunk.next()) { if (hunk.isContextLine()) { - final String lineA = a.src.getLine(hunk.getCurA()); + final String lineA = a.src.getString(hunk.getCurA()); a.dst.addLine(hunk.getCurA(), lineA); if (ignoredWhitespace) { // If we ignored whitespace in some form, also get the line // from b when it does not exactly match the line from a. // - final String lineB = b.src.getLine(hunk.getCurB()); + final String lineB = b.src.getString(hunk.getCurB()); if (!lineA.equals(lineB)) { b.dst.addLine(hunk.getCurB(), lineB); } @@ -355,6 +356,7 @@ class PatchScriptBuilder { Text src; MimeType mimeType = MimeUtil2.UNKNOWN_MIME_TYPE; DisplayMethod displayMethod = DisplayMethod.DIFF; + PatchScript.FileMode fileMode = PatchScript.FileMode.FILE; final SparseFileContent dst = new SparseFileContent(); int size() { @@ -362,7 +364,7 @@ class PatchScriptBuilder { } void addLine(int line) { - dst.addLine(line, src.getLine(line)); + dst.addLine(line, src.getString(line)); } void resolve(final Side other, final ObjectId within) throws IOException { @@ -377,7 +379,7 @@ class PatchScriptBuilder { displayMethod = DisplayMethod.NONE; } else { id = within; - src = Text.forCommit(db, within); + src = Text.forCommit(db, reader, within); srcContent = src.getContent(); if (src == Text.EMPTY) { mode = FileMode.MISSING; @@ -399,14 +401,7 @@ class PatchScriptBuilder { srcContent = other.srcContent; } else if (mode.getObjectType() == Constants.OBJ_BLOB) { - final ObjectLoader ldr = db.openObject(id); - if (ldr == null) { - throw new MissingObjectException(id, Constants.TYPE_BLOB); - } - srcContent = ldr.getCachedBytes(); - if (ldr.getType() != Constants.OBJ_BLOB) { - throw new IncorrectObjectTypeException(id, Constants.TYPE_BLOB); - } + srcContent = Text.asByteArray(db.open(id, Constants.OBJ_BLOB)); } else { srcContent = Text.NO_BYTES; @@ -443,6 +438,12 @@ class PatchScriptBuilder { } dst.setSize(size()); dst.setPath(path); + + if (mode == FileMode.SYMLINK) { + fileMode = PatchScript.FileMode.SYMLINK; + } else if (mode == FileMode.GITLINK) { + fileMode = PatchScript.FileMode.GITLINK; + } } catch (IOException err) { throw new IOException("Cannot read " + within.name() + ":" + path, err); } @@ -450,12 +451,12 @@ class PatchScriptBuilder { private TreeWalk find(final ObjectId within) throws MissingObjectException, IncorrectObjectTypeException, CorruptObjectException, IOException { - if (path == null) { + if (path == null || within == null) { return null; } - final RevWalk rw = new RevWalk(db); + final RevWalk rw = new RevWalk(reader); final RevTree tree = rw.parseTree(within); - return TreeWalk.forPath(db, path, tree); + return TreeWalk.forPath(reader, path, tree); } } } diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/AddBranch.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/AddBranch.java index 0472dd4c4f..d4698c296a 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/AddBranch.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/AddBranch.java @@ -14,6 +14,7 @@ package com.google.gerrit.httpd.rpc.project; +import com.google.gerrit.common.ChangeHookRunner; import com.google.gerrit.common.data.ListBranchesResult; import com.google.gerrit.common.errors.InvalidNameException; import com.google.gerrit.common.errors.InvalidRevisionException; @@ -58,6 +59,7 @@ class AddBranch extends Handler<ListBranchesResult> { private final IdentifiedUser identifiedUser; private final GitRepositoryManager repoManager; private final ReplicationQueue replication; + private final ChangeHookRunner hooks; private final Project.NameKey projectName; private final String branchName; @@ -69,6 +71,7 @@ class AddBranch extends Handler<ListBranchesResult> { final IdentifiedUser identifiedUser, final GitRepositoryManager repoManager, final ReplicationQueue replication, + final ChangeHookRunner hooks, @Assisted Project.NameKey projectName, @Assisted("branchName") String branchName, @@ -78,6 +81,7 @@ class AddBranch extends Handler<ListBranchesResult> { this.identifiedUser = identifiedUser; this.repoManager = repoManager; this.replication = replication; + this.hooks = hooks; this.projectName = projectName; this.branchName = branchName; @@ -136,11 +140,9 @@ class AddBranch extends Handler<ListBranchesResult> { case NEW: case NO_CHANGE: replication.scheduleUpdate(name.getParentKey(), refname); + hooks.doRefUpdatedHook(name, u, identifiedUser.getAccount()); break; default: { - final String msg = - "Cannot create branch " + name + ": " + result.name(); - log.error(msg); throw new IOException(result.name()); } } diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/DeleteBranches.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/DeleteBranches.java index e8c8904e16..fffc1261ae 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/DeleteBranches.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/DeleteBranches.java @@ -14,9 +14,11 @@ package com.google.gerrit.httpd.rpc.project; +import com.google.gerrit.common.ChangeHookRunner; import com.google.gerrit.httpd.rpc.Handler; import com.google.gerrit.reviewdb.Branch; import com.google.gerrit.reviewdb.Project; +import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.git.ReplicationQueue; import com.google.gerrit.server.project.NoSuchProjectException; @@ -46,6 +48,8 @@ class DeleteBranches extends Handler<Set<Branch.NameKey>> { private final ProjectControl.Factory projectControlFactory; private final GitRepositoryManager repoManager; private final ReplicationQueue replication; + private final IdentifiedUser identifiedUser; + private final ChangeHookRunner hooks; private final Project.NameKey projectName; private final Set<Branch.NameKey> toRemove; @@ -54,11 +58,15 @@ class DeleteBranches extends Handler<Set<Branch.NameKey>> { DeleteBranches(final ProjectControl.Factory projectControlFactory, final GitRepositoryManager repoManager, final ReplicationQueue replication, + final IdentifiedUser identifiedUser, + final ChangeHookRunner hooks, @Assisted Project.NameKey name, @Assisted Set<Branch.NameKey> toRemove) { this.projectControlFactory = projectControlFactory; this.repoManager = repoManager; this.replication = replication; + this.identifiedUser = identifiedUser; + this.hooks = hooks; this.projectName = name; this.toRemove = toRemove; @@ -85,8 +93,9 @@ class DeleteBranches extends Handler<Set<Branch.NameKey>> { for (final Branch.NameKey branchKey : toRemove) { final String refname = branchKey.get(); final RefUpdate.Result result; + final RefUpdate u; try { - final RefUpdate u = r.updateRef(refname); + u = r.updateRef(refname); u.setForceUpdate(true); result = u.delete(); } catch (IOException e) { @@ -101,6 +110,7 @@ class DeleteBranches extends Handler<Set<Branch.NameKey>> { case FORCED: deleted.add(branchKey); replication.scheduleUpdate(projectName, refname); + hooks.doRefUpdatedHook(branchKey, u, identifiedUser.getAccount()); break; case REJECTED_CURRENT_BRANCH: diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/DeleteRefRights.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/DeleteRefRights.java index 92154c4cbd..ae8b98b2cd 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/DeleteRefRights.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/DeleteRefRights.java @@ -23,7 +23,6 @@ import com.google.gerrit.server.project.NoSuchProjectException; import com.google.gerrit.server.project.NoSuchRefException; import com.google.gerrit.server.project.ProjectCache; import com.google.gerrit.server.project.ProjectControl; -import com.google.gerrit.server.project.RefControl; import com.google.gwtorm.client.OrmException; import com.google.inject.Inject; import com.google.inject.assistedinject.Assisted; @@ -71,8 +70,12 @@ class DeleteRefRights extends Handler<ProjectDetail> { if (!projectName.equals(k.getProjectNameKey())) { throw new IllegalArgumentException("All keys must be from same project"); } - if (!projectControl.controlForRef(k.getRefPattern()).isOwner()) { - throw new NoSuchRefException(k.getRefPattern()); + String refPattern = k.getRefPattern(); + if (refPattern.startsWith("-")) { + refPattern = refPattern.substring(1); + } + if (!projectControl.controlForRef(refPattern).isOwner()) { + throw new NoSuchRefException(refPattern); } } diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ListBranches.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ListBranches.java index c02af37ccf..7b22de097f 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ListBranches.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ListBranches.java @@ -60,15 +60,20 @@ class ListBranches extends Handler<ListBranchesResult> { } @Override - public ListBranchesResult call() throws NoSuchProjectException, - RepositoryNotFoundException { + public ListBranchesResult call() throws NoSuchProjectException { final ProjectControl pctl = projectControlFactory.validateFor( // projectName, // ProjectControl.OWNER | ProjectControl.VISIBLE); final List<Branch> branches = new ArrayList<Branch>(); Branch headBranch = null; - final Repository db = repoManager.openRepository(projectName.get()); + + final Repository db; + try { + db = repoManager.openRepository(projectName.get()); + } catch (RepositoryNotFoundException noGitRepository) { + return new ListBranchesResult(branches, false, true); + } try { final Map<String, Ref> all = db.getAllRefs(); @@ -139,7 +144,7 @@ class ListBranches extends Handler<ListBranchesResult> { if (headBranch != null) { branches.add(0, headBranch); } - return new ListBranchesResult(branches, pctl.canAddRefs()); + return new ListBranchesResult(branches, pctl.canAddRefs(), false); } private Branch createBranch(final String name) { diff --git a/gerrit-launcher/pom.xml b/gerrit-launcher/pom.xml index 1000777281..3c9c979618 100644 --- a/gerrit-launcher/pom.xml +++ b/gerrit-launcher/pom.xml @@ -22,7 +22,7 @@ limitations under the License. <parent> <groupId>com.google.gerrit</groupId> <artifactId>gerrit-parent</artifactId> - <version>2.1.4-SNAPSHOT</version> + <version>2.1-SNAPSHOT</version> </parent> <artifactId>gerrit-launcher</artifactId> diff --git a/gerrit-main/pom.xml b/gerrit-main/pom.xml index 10cb14aec7..a46aa4ff8a 100644 --- a/gerrit-main/pom.xml +++ b/gerrit-main/pom.xml @@ -22,7 +22,7 @@ limitations under the License. <parent> <groupId>com.google.gerrit</groupId> <artifactId>gerrit-parent</artifactId> - <version>2.1.4-SNAPSHOT</version> + <version>2.1-SNAPSHOT</version> </parent> <artifactId>gerrit-main</artifactId> diff --git a/gerrit-patch-commonsnet/pom.xml b/gerrit-patch-commonsnet/pom.xml index df64d13874..5bbae03533 100644 --- a/gerrit-patch-commonsnet/pom.xml +++ b/gerrit-patch-commonsnet/pom.xml @@ -22,7 +22,7 @@ limitations under the License. <parent> <groupId>com.google.gerrit</groupId> <artifactId>gerrit-parent</artifactId> - <version>2.1.4-SNAPSHOT</version> + <version>2.1-SNAPSHOT</version> </parent> <artifactId>gerrit-patch-commonsnet</artifactId> diff --git a/gerrit-patch-jgit/pom.xml b/gerrit-patch-jgit/pom.xml index 68a08c3d16..f30eacede2 100644 --- a/gerrit-patch-jgit/pom.xml +++ b/gerrit-patch-jgit/pom.xml @@ -22,7 +22,7 @@ limitations under the License. <parent> <groupId>com.google.gerrit</groupId> <artifactId>gerrit-parent</artifactId> - <version>2.1.4-SNAPSHOT</version> + <version>2.1-SNAPSHOT</version> </parent> <artifactId>gerrit-patch-jgit</artifactId> diff --git a/gerrit-patch-jgit/src/main/java/org/eclipse/jgit/lib/WindowCacheStatAccessor.java b/gerrit-patch-jgit/src/main/java/org/eclipse/jgit/storage/file/WindowCacheStatAccessor.java index 7d12e47e3a..7e29536139 100644 --- a/gerrit-patch-jgit/src/main/java/org/eclipse/jgit/lib/WindowCacheStatAccessor.java +++ b/gerrit-patch-jgit/src/main/java/org/eclipse/jgit/storage/file/WindowCacheStatAccessor.java @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package org.eclipse.jgit.lib; +package org.eclipse.jgit.storage.file; // Hack to obtain visibility to package level methods only. // These aren't yet part of the public JGit API. diff --git a/gerrit-pgm/pom.xml b/gerrit-pgm/pom.xml index 66c5349799..f658f52153 100644 --- a/gerrit-pgm/pom.xml +++ b/gerrit-pgm/pom.xml @@ -22,7 +22,7 @@ limitations under the License. <parent> <groupId>com.google.gerrit</groupId> <artifactId>gerrit-parent</artifactId> - <version>2.1.4-SNAPSHOT</version> + <version>2.1-SNAPSHOT</version> </parent> <artifactId>gerrit-pgm</artifactId> diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Init.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Init.java index 445c10aad6..b10e1db7b4 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Init.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Init.java @@ -52,7 +52,6 @@ import com.google.inject.spi.Message; import org.kohsuke.args4j.Option; -import java.io.Console; import java.io.File; import java.io.IOException; import java.util.ArrayList; diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ScanTrackingIds.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ScanTrackingIds.java index e62fe40050..16c72d4859 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ScanTrackingIds.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ScanTrackingIds.java @@ -140,8 +140,12 @@ public class ScanTrackingIds extends SiteProgram { private RevCommit parse(final Repository git, PatchSet ps) throws MissingObjectException, IncorrectObjectTypeException, IOException { - return new RevWalk(git).parseCommit(ObjectId.fromString(ps.getRevision() - .get())); + RevWalk rw = new RevWalk(git); + try { + return rw.parseCommit(ObjectId.fromString(ps.getRevision().get())); + } finally { + rw.release(); + } } private Change next() { diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java index e9d0f2e995..7ab8d30254 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java @@ -19,7 +19,9 @@ import static java.util.concurrent.TimeUnit.SECONDS; import com.google.gerrit.launcher.GerritLauncher; import com.google.gerrit.lifecycle.LifecycleListener; +import com.google.gerrit.reviewdb.AuthType; import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.config.ConfigUtil; import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.config.SitePaths; import com.google.inject.Inject; @@ -139,6 +141,7 @@ public class JettyServer { final URI[] listenUrls = listenURLs(cfg); final boolean reuseAddress = cfg.getBoolean("httpd", "reuseaddress", true); final int acceptors = cfg.getInt("httpd", "acceptorThreads", 2); + final AuthType authType = ConfigUtil.getEnum(cfg, "auth", null, "type", AuthType.OPENID); reverseProxy = true; final Connector[] connectors = new Connector[listenUrls.length]; @@ -147,11 +150,17 @@ public class JettyServer { final int defaultPort; final SelectChannelConnector c; + if (AuthType.CLIENT_SSL_CERT_LDAP.equals(authType) && ! "https".equals(u.getScheme())) { + throw new IllegalArgumentException("Protocol '" + u.getScheme() + + "' " + " not supported in httpd.listenurl '" + u + + "' when auth.type = '" + AuthType.CLIENT_SSL_CERT_LDAP.name() + + "'; only 'https' is supported"); + } + if ("http".equals(u.getScheme())) { reverseProxy = false; defaultPort = 80; c = new SelectChannelConnector(); - } else if ("https".equals(u.getScheme())) { final SslSelectChannelConnector ssl = new SslSelectChannelConnector(); final File keystore = getFile(cfg, "sslkeystore", "etc/keystore"); @@ -164,6 +173,10 @@ public class JettyServer { ssl.setKeyPassword(password); ssl.setTrustPassword(password); + if (AuthType.CLIENT_SSL_CERT_LDAP.equals(authType)) { + ssl.setNeedClientAuth(true); + } + reverseProxy = false; defaultPort = 443; c = ssl; diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitContainer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitContainer.java index ef95d87b7f..7063f5440f 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitContainer.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitContainer.java @@ -14,7 +14,6 @@ package com.google.gerrit.pgm.init; -import static com.google.gerrit.pgm.init.InitUtil.copy; import static com.google.gerrit.pgm.init.InitUtil.die; import static com.google.gerrit.pgm.init.InitUtil.username; @@ -24,10 +23,14 @@ import com.google.gerrit.server.config.SitePaths; import com.google.inject.Inject; import com.google.inject.Singleton; +import org.eclipse.jgit.storage.file.LockFile; +import org.eclipse.jgit.util.FS; + import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; +import java.io.OutputStream; /** Initialize the {@code container} configuration section. */ @Singleton @@ -84,7 +87,39 @@ class InitContainer implements InitStep { System.err.format("Copying gerrit.war to %s", siteWar.getPath()); System.err.println(); } - copy(siteWar, new FileInputStream(myWar)); + + FileInputStream in = new FileInputStream(myWar); + try { + siteWar.getParentFile().mkdirs(); + + LockFile lf = new LockFile(siteWar, FS.DETECTED); + if (!lf.lock()) { + throw new IOException("Cannot lock " + siteWar); + } + + try { + final OutputStream out = lf.getOutputStream(); + try { + final byte[] tmp = new byte[4096]; + for (;;) { + int n = in.read(tmp); + if (n < 0) { + break; + } + out.write(tmp, 0, n); + } + } finally { + out.close(); + } + if (!lf.commit()) { + throw new IOException("Cannot commit " + siteWar); + } + } finally { + lf.unlock(); + } + } finally { + in.close(); + } } } } diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitFlags.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitFlags.java index 5ca7e4131f..992c616cbe 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitFlags.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitFlags.java @@ -19,7 +19,8 @@ import com.google.inject.Inject; import com.google.inject.Singleton; import org.eclipse.jgit.errors.ConfigInvalidException; -import org.eclipse.jgit.lib.FileBasedConfig; +import org.eclipse.jgit.storage.file.FileBasedConfig; +import org.eclipse.jgit.util.FS; import java.io.IOException; @@ -40,8 +41,8 @@ public class InitFlags { @Inject InitFlags(final SitePaths site) throws IOException, ConfigInvalidException { - cfg = new FileBasedConfig(site.gerrit_config); - sec = new FileBasedConfig(site.secure_config); + cfg = new FileBasedConfig(site.gerrit_config, FS.DETECTED); + sec = new FileBasedConfig(site.secure_config, FS.DETECTED); cfg.load(); sec.load(); diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitUtil.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitUtil.java index 17f014673e..1382f65067 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitUtil.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitUtil.java @@ -17,19 +17,23 @@ package com.google.gerrit.pgm.init; import com.google.gerrit.pgm.util.Die; import org.eclipse.jgit.lib.Constants; -import org.eclipse.jgit.lib.FileBasedConfig; -import org.eclipse.jgit.lib.LockFile; +import org.eclipse.jgit.storage.file.FileBasedConfig; +import org.eclipse.jgit.storage.file.LockFile; +import org.eclipse.jgit.util.FS; +import org.eclipse.jgit.util.IO; import org.eclipse.jgit.util.SystemReader; -import java.io.OutputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.net.InetAddress; import java.net.URI; import java.net.URISyntaxException; import java.net.UnknownHostException; +import java.nio.ByteBuffer; +import java.util.Arrays; /** Utility functions to help initialize a site. */ class InitUtil { @@ -42,25 +46,41 @@ class InitUtil { } static void savePublic(final FileBasedConfig sec) throws IOException { - sec.save(); + if (modified(sec)) { + sec.save(); + } } static void saveSecure(final FileBasedConfig sec) throws IOException { - final byte[] out = Constants.encode(sec.toText()); - final File path = sec.getFile(); - final LockFile lf = new LockFile(path); - if (!lf.lock()) { - throw new IOException("Cannot lock " + path); + if (modified(sec)) { + final byte[] out = Constants.encode(sec.toText()); + final File path = sec.getFile(); + final LockFile lf = new LockFile(path, FS.DETECTED); + if (!lf.lock()) { + throw new IOException("Cannot lock " + path); + } + try { + chmod(0600, new File(path.getParentFile(), path.getName() + ".lock")); + lf.write(out); + if (!lf.commit()) { + throw new IOException("Cannot commit write to " + path); + } + } finally { + lf.unlock(); + } } + } + + private static boolean modified(FileBasedConfig cfg) throws IOException { + byte[] curVers; try { - chmod(0600, new File(path.getParentFile(), path.getName() + ".lock")); - lf.write(out); - if (!lf.commit()) { - throw new IOException("Cannot commit write to " + path); - } - } finally { - lf.unlock(); + curVers = IO.readFully(cfg.getFile()); + } catch (FileNotFoundException notFound) { + return true; } + + byte[] newVers = Constants.encode(cfg.toText()); + return !Arrays.equals(curVers, newVers); } static void mkdir(final File path) { @@ -144,7 +164,8 @@ class InitUtil { final String name) throws IOException { final InputStream in = open(sibling, name); if (in != null) { - copy(dst, in); + ByteBuffer buf = IO.readWholeStream(in, 8192); + copy(dst, buf); } } @@ -165,34 +186,41 @@ class InitUtil { return in; } - static void copy(final File dst, final InputStream in) + static void copy(final File dst, final ByteBuffer buf) throws FileNotFoundException, IOException { + // If the file already has the content we want to put there, + // don't attempt to overwrite the file. + // try { - dst.getParentFile().mkdirs(); - LockFile lf = new LockFile(dst); - if (!lf.lock()) { - throw new IOException("Cannot lock " + dst); + if (buf.equals(ByteBuffer.wrap(IO.readFully(dst)))) { + return; } - try { + } catch (FileNotFoundException notFound) { + // Fall through and write the file. + } - final OutputStream out = lf.getOutputStream(); - try { - final byte[] buf = new byte[4096]; - int n; - while (0 < (n = in.read(buf))) { - out.write(buf, 0, n); - } - } finally { - out.close(); - } - if (!lf.commit()) { - throw new IOException("Cannot commit " + dst); + dst.getParentFile().mkdirs(); + LockFile lf = new LockFile(dst, FS.DETECTED); + if (!lf.lock()) { + throw new IOException("Cannot lock " + dst); + } + try { + final OutputStream out = lf.getOutputStream(); + try { + final byte[] tmp = new byte[4096]; + while (0 < buf.remaining()) { + int n = Math.min(buf.remaining(), tmp.length); + buf.get(tmp, 0, n); + out.write(tmp, 0, n); } } finally { - lf.unlock(); + out.close(); + } + if (!lf.commit()) { + throw new IOException("Cannot commit " + dst); } } finally { - in.close(); + lf.unlock(); } } diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java index 74e754830c..dae08934ad 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java @@ -25,11 +25,13 @@ import static com.google.gerrit.pgm.init.InitUtil.version; import com.google.gerrit.pgm.Init; import com.google.gerrit.pgm.util.ConsoleUI; import com.google.gerrit.server.config.SitePaths; +import com.google.gerrit.server.mail.OutgoingEmail; import com.google.inject.Binding; import com.google.inject.Inject; import com.google.inject.Injector; import com.google.inject.TypeLiteral; +import java.io.File; import java.util.ArrayList; import java.util.List; @@ -66,6 +68,7 @@ public class SitePathInitializer { mkdir(site.etc_dir); mkdir(site.lib_dir); mkdir(site.logs_dir); + mkdir(site.mail_dir); mkdir(site.static_dir); for (InitStep step : steps) { @@ -82,11 +85,27 @@ public class SitePathInitializer { extract(site.gerrit_sh, Init.class, "gerrit.sh"); chmod(0755, site.gerrit_sh); + extractMailExample("Abandoned.vm"); + extractMailExample("ChangeFooter.vm"); + extractMailExample("ChangeSubject.vm"); + extractMailExample("Comment.vm"); + extractMailExample("Merged.vm"); + extractMailExample("MergeFail.vm"); + extractMailExample("NewChange.vm"); + extractMailExample("RegisterNewEmail.vm"); + extractMailExample("ReplacePatchSet.vm"); + if (!ui.isBatch()) { System.err.println(); } } + private void extractMailExample(String orig) throws Exception { + File ex = new File(site.mail_dir, orig + ".example"); + extract(ex, OutgoingEmail.class, orig); + chmod(0444, ex); + } + private static List<InitStep> stepsOf(final Injector injector) { final ArrayList<InitStep> r = new ArrayList<InitStep>(); for (Binding<InitStep> b : all(injector)) { diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_x.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_x.java index fad587884f..9f62fc5faa 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_x.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_x.java @@ -26,7 +26,7 @@ import com.google.inject.Inject; import com.google.inject.Singleton; import org.eclipse.jgit.errors.ConfigInvalidException; -import org.eclipse.jgit.lib.FileBasedConfig; +import org.eclipse.jgit.storage.file.FileBasedConfig; import java.io.File; import java.io.FileInputStream; diff --git a/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/InitTestCase.java b/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/InitTestCase.java index 2964f50fb0..4d7370bfb7 100644 --- a/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/InitTestCase.java +++ b/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/InitTestCase.java @@ -21,6 +21,6 @@ import java.io.IOException; public abstract class InitTestCase extends LocalDiskRepositoryTestCase { protected File newSitePath() throws IOException { - return new File(createWorkRepository().getWorkDir(), "test_site"); + return new File(createWorkRepository().getWorkTree(), "test_site"); } } diff --git a/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_xTest.java b/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_xTest.java index 72b02d5dd6..0018558108 100644 --- a/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_xTest.java +++ b/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_xTest.java @@ -24,7 +24,8 @@ import com.google.gerrit.pgm.util.ConsoleUI; import com.google.gerrit.server.config.SitePaths; import org.eclipse.jgit.errors.ConfigInvalidException; -import org.eclipse.jgit.lib.FileBasedConfig; +import org.eclipse.jgit.storage.file.FileBasedConfig; +import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.IO; import java.io.File; @@ -50,7 +51,9 @@ public class UpgradeFrom2_0_xTest extends InitTestCase { } } - FileBasedConfig old = new FileBasedConfig(new File(p, "gerrit.config")); + FileBasedConfig old = + new FileBasedConfig(new File(p, "gerrit.config"), FS.DETECTED); + old.setString("ldap", null, "username", "ldap.user"); old.setString("ldap", null, "password", "ldap.s3kr3t"); @@ -84,8 +87,8 @@ public class UpgradeFrom2_0_xTest extends InitTestCase { new String(IO.readFully(new File(site.etc_dir, n)), "UTF-8")); } - FileBasedConfig cfg = new FileBasedConfig(site.gerrit_config); - FileBasedConfig sec = new FileBasedConfig(site.secure_config); + FileBasedConfig cfg = new FileBasedConfig(site.gerrit_config, FS.DETECTED); + FileBasedConfig sec = new FileBasedConfig(site.secure_config, FS.DETECTED); cfg.load(); sec.load(); diff --git a/gerrit-prettify/pom.xml b/gerrit-prettify/pom.xml index 99eece6076..060ffdda0d 100644 --- a/gerrit-prettify/pom.xml +++ b/gerrit-prettify/pom.xml @@ -22,7 +22,7 @@ limitations under the License. <parent> <groupId>com.google.gerrit</groupId> <artifactId>gerrit-parent</artifactId> - <version>2.1.4-SNAPSHOT</version> + <version>2.1-SNAPSHOT</version> </parent> <artifactId>gerrit-prettify</artifactId> diff --git a/gerrit-reviewdb/pom.xml b/gerrit-reviewdb/pom.xml index 5626739adf..d81b068b0c 100644 --- a/gerrit-reviewdb/pom.xml +++ b/gerrit-reviewdb/pom.xml @@ -22,7 +22,7 @@ limitations under the License. <parent> <groupId>com.google.gerrit</groupId> <artifactId>gerrit-parent</artifactId> - <version>2.1.4-SNAPSHOT</version> + <version>2.1-SNAPSHOT</version> </parent> <artifactId>gerrit-reviewdb</artifactId> diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Account.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Account.java index f428a22790..43b7b17ba7 100644 --- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Account.java +++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Account.java @@ -129,6 +129,10 @@ public final class Account { @Column(id = 6, name = Column.NONE) protected AccountGeneralPreferences generalPreferences; + /** Is this user active */ + @Column(id = 7) + protected boolean inactive; + /** <i>computed</i> the username selected from the identities. */ protected String userName; @@ -198,6 +202,14 @@ public final class Account { contactFiledOn = new Timestamp(System.currentTimeMillis()); } + public boolean isActive() { + return ! inactive; + } + + public void setActive(boolean active) { + inactive = ! active; + } + /** @return the computed user name for this account */ public String getUserName() { return userName; diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountDiffPreference.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountDiffPreference.java index 4a3dd18120..38a23593b1 100644 --- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountDiffPreference.java +++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountDiffPreference.java @@ -96,6 +96,12 @@ public class AccountDiffPreference { @Column(id = 9) protected short context; + @Column(id = 10) + protected boolean skipDeleted; + + @Column(id = 11) + protected boolean skipUncommented; + protected AccountDiffPreference() { } @@ -112,6 +118,8 @@ public class AccountDiffPreference { this.showWhitespaceErrors = p.showWhitespaceErrors; this.intralineDifference = p.intralineDifference; this.showTabs = p.showTabs; + this.skipDeleted = p.skipDeleted; + this.skipUncommented = p.skipUncommented; this.context = p.context; } @@ -185,4 +193,20 @@ public class AccountDiffPreference { assert 0 <= context || context == WHOLE_FILE_CONTEXT; this.context = context; } + + public boolean isSkipDeleted() { + return skipDeleted; + } + + public void setSkipDeleted(boolean skip) { + skipDeleted = skip; + } + + public boolean isSkipUncommented() { + return skipUncommented; + } + + public void setSkipUncommented(boolean skip) { + skipUncommented = skip; + } } diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGeneralPreferences.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGeneralPreferences.java index c69e785513..b90129cd82 100644 --- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGeneralPreferences.java +++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGeneralPreferences.java @@ -35,6 +35,51 @@ public final class AccountGeneralPreferences { REPO_DOWNLOAD, PULL, CHECKOUT, CHERRY_PICK, FORMAT_PATCH; } + public static enum DateFormat { + /** US style dates: Apr 27, Feb 14, 2010 */ + STD("MMM d", "MMM d, yyyy"), + + /** US style dates: 04/27, 02/14/10 */ + US("MM/dd", "MM/dd/yy"), + + /** ISO style dates: 2010-02-14 */ + ISO("MM-dd", "yyyy-MM-dd"); + + private final String shortFormat; + private final String longFormat; + + DateFormat(String shortFormat, String longFormat) { + this.shortFormat = shortFormat; + this.longFormat = longFormat; + } + + public String getShortFormat() { + return shortFormat; + } + + public String getLongFormat() { + return longFormat; + } + } + + public static enum TimeFormat { + /** 12-hour clock: 1:15 am, 2:13 pm */ + HHMM_12("h:mm a"), + + /** 24-hour clock: 01:15, 14:13 */ + HHMM_24("HH:mm"); + + private final String format; + + TimeFormat(String format) { + this.format = format; + } + + public String getFormat() { + return format; + } + } + /** Number of changes to show in a screen. */ @Column(id = 2) protected short maximumPageSize; @@ -59,6 +104,19 @@ public final class AccountGeneralPreferences { @Column(id = 7) protected boolean copySelfOnEmail; + @Column(id = 8, length = 10, notNull = false) + protected String dateFormat; + + @Column(id = 9, length = 10, notNull = false) + protected String timeFormat; + + /** + * If true display the patch sets in the ChangeScreen in reverse order + * (show latest patch set on top). + */ + @Column(id = 10) + protected boolean displayPatchSetsInReverseOrder; + public AccountGeneralPreferences() { } @@ -124,12 +182,45 @@ public final class AccountGeneralPreferences { copySelfOnEmail = includeSelfOnEmail; } + public boolean isDisplayPatchSetsInReverseOrder() { + return displayPatchSetsInReverseOrder; + } + + public void setDisplayPatchSetsInReverseOrder(final boolean displayPatchSetsInReverseOrder) { + this.displayPatchSetsInReverseOrder = displayPatchSetsInReverseOrder; + } + + public DateFormat getDateFormat() { + if (dateFormat == null) { + return DateFormat.STD; + } + return DateFormat.valueOf(dateFormat); + } + + public void setDateFormat(DateFormat fmt) { + dateFormat = fmt.name(); + } + + public TimeFormat getTimeFormat() { + if (timeFormat == null) { + return TimeFormat.HHMM_12; + } + return TimeFormat.valueOf(timeFormat); + } + + public void setTimeFormat(TimeFormat fmt) { + timeFormat = fmt.name(); + } + public void resetToDefaults() { maximumPageSize = DEFAULT_PAGESIZE; showSiteHeader = true; useFlashClipboard = true; copySelfOnEmail = false; + displayPatchSetsInReverseOrder = false; downloadUrl = null; downloadCommand = null; + dateFormat = null; + timeFormat = null; } } diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountProjectWatch.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountProjectWatch.java index 52bef2ba9c..6713d8f8f6 100644 --- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountProjectWatch.java +++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountProjectWatch.java @@ -20,6 +20,11 @@ import com.google.gwtorm.client.StringKey; /** An {@link Account} interested in a {@link Project}. */ public final class AccountProjectWatch { + + public enum Type { + NEW_CHANGES, SUBMITS, COMMENTS + } + public static final String FILTER_ALL = "*"; public static class Key extends CompoundKey<Account.Id> { @@ -142,4 +147,24 @@ public final class AccountProjectWatch { public void setNotifySubmittedChanges(final boolean a) { notifySubmittedChanges = a; } + + public boolean isNotify(final Type type) { + switch(type) { + case NEW_CHANGES: return notifySubmittedChanges; + case SUBMITS: return notifyNewChanges; + case COMMENTS: return notifyAllComments; + } + return false; + } + + public void setNotify(final Type type, final boolean v) { + switch(type) { + case NEW_CHANGES: notifySubmittedChanges = v; + break; + case SUBMITS: notifyNewChanges = v; + break; + case COMMENTS: notifyAllComments = v; + break; + } + } } diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AuthType.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AuthType.java index 46b435c6fe..5d69e21da8 100644 --- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AuthType.java +++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AuthType.java @@ -40,6 +40,21 @@ public enum AuthType { HTTP_LDAP, /** + * Login via client SSL certificate. + * <p> + * This authentication type is actually kind of SSO. Gerrit will configure + * Jetty's SSL channel to request client's SSL certificate. For this + * authentication to work a Gerrit administrator has to import the root + * certificate of the trust chain used to issue the client's certificate + * into the <review-site>/etc/keystore. + * <p> + * After the authentication is done Gerrit will obtain basic user + * registration (name and email) from LDAP, and some group memberships. + * Therefore, the "_LDAP" suffix in the name of this authentication type. + */ + CLIENT_SSL_CERT_LDAP, + + /** * Login collects username and password through a web form, and binds to LDAP. * <p> * Unlike {@link #HTTP_LDAP}, Gerrit presents a sign-in dialog to the user and diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Patch.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Patch.java index 3a922fb12f..0153755a4b 100644 --- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Patch.java +++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Patch.java @@ -136,29 +136,7 @@ public final class Patch { * the only information it can display is the old and new file content * hashes. */ - BINARY('B'), - - /** - * Difference of three or more textual contents. - * - * <p> - * Git can produce an n-way unified diff, showing how a merge conflict was - * resolved when two or more conflicting branches were merged together in a - * single merge commit. - * - * <p> - * This type of patch can only appear if there are two or more - * {@link PatchSetAncestor} entities for the same parent {@link PatchSet}, - * as that denotes that the patch set is a merge commit. - * - * <p> - * Gerrit can only render an N_WAY file in a PatchScreen.Unified view, as it - * does not have code to split the n-way unified diff into multiple edit - * lists, one per pre-image. However, a logical way to display this format - * would be an n-way table, with n+1 columns displayed (n pre-images, +1 - * post-image). - */ - N_WAY('N'); + BINARY('B'); private final char code; diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Project.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Project.java index e37c78d663..409547a9bd 100644 --- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Project.java +++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Project.java @@ -99,6 +99,12 @@ public final class Project { @Column(id = 6, notNull = false, name = "parent_name") protected NameKey parent; + @Column(id = 7) + protected boolean requireChangeID; + + @Column(id = 8) + protected boolean useContentMerge; + protected Project() { } @@ -136,10 +142,26 @@ public final class Project { return useSignedOffBy; } + public boolean isUseContentMerge() { + return useContentMerge; + } + + public boolean isRequireChangeID() { + return requireChangeID; + } + public void setUseSignedOffBy(final boolean sbo) { useSignedOffBy = sbo; } + public void setUseContentMerge(final boolean cm) { + useContentMerge = cm; + } + + public void setRequireChangeID(final boolean cid) { + requireChangeID = cid; + } + public SubmitType getSubmitType() { return SubmitType.forCode(submitType); } @@ -152,6 +174,8 @@ public final class Project { description = update.description; useContributorAgreements = update.useContributorAgreements; useSignedOffBy = update.useSignedOffBy; + useContentMerge = update.useContentMerge; + requireChangeID = update.requireChangeID; submitType = update.submitType; } diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/RefRight.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/RefRight.java index ec70051aed..97ee219946 100644 --- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/RefRight.java +++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/RefRight.java @@ -96,6 +96,10 @@ public final class RefRight { return refPattern.get(); } + public void setGroupId(AccountGroup.Id groupId) { + this.groupId = groupId; + } + @Override public com.google.gwtorm.client.Key<?>[] members() { return new com.google.gwtorm.client.Key<?>[] {refPattern, categoryId, @@ -119,6 +123,13 @@ public final class RefRight { this.key = key; } + public RefRight(final RefRight refRight, final AccountGroup.Id groupId) { + this(new RefRight.Key(refRight.getKey().projectName, + refRight.getKey().refPattern, refRight.getKey().categoryId, groupId)); + setMinValue(refRight.getMinValue()); + setMaxValue(refRight.getMaxValue()); + } + public RefRight.Key getKey() { return key; } diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/SystemConfig.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/SystemConfig.java index f9b6a2dfbf..6ff23ed7ea 100644 --- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/SystemConfig.java +++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/SystemConfig.java @@ -14,7 +14,6 @@ package com.google.gerrit.reviewdb; -import com.google.gerrit.reviewdb.AccountGroup.Id; import com.google.gwtorm.client.Column; import com.google.gwtorm.client.StringKey; @@ -83,6 +82,10 @@ public final class SystemConfig { @Column(id = 8) public AccountGroup.Id batchUsersGroupId; + /** Identity of the owner group, which permits any project owner. */ + @Column(id = 9) + public AccountGroup.Id ownerGroupId; + protected SystemConfig() { } } diff --git a/gerrit-server/pom.xml b/gerrit-server/pom.xml index db8093cc20..0a9ece3eda 100644 --- a/gerrit-server/pom.xml +++ b/gerrit-server/pom.xml @@ -22,7 +22,7 @@ limitations under the License. <parent> <groupId>com.google.gerrit</groupId> <artifactId>gerrit-parent</artifactId> - <version>2.1.4-SNAPSHOT</version> + <version>2.1-SNAPSHOT</version> </parent> <artifactId>gerrit-server</artifactId> @@ -34,6 +34,16 @@ limitations under the License. <dependencies> <dependency> + <groupId>log4j</groupId> + <artifactId>log4j</artifactId> + </dependency> + + <dependency> + <groupId>org.apache.velocity</groupId> + <artifactId>velocity</artifactId> + </dependency> + + <dependency> <groupId>org.eclipse.jgit</groupId> <artifactId>org.eclipse.jgit</artifactId> </dependency> diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java index d29bc23964..c7c51b6cf1 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java +++ b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java @@ -19,6 +19,7 @@ import com.google.gerrit.common.data.ApprovalTypes; import com.google.gerrit.reviewdb.Account; import com.google.gerrit.reviewdb.ApprovalCategory; import com.google.gerrit.reviewdb.ApprovalCategoryValue; +import com.google.gerrit.reviewdb.Branch; import com.google.gerrit.reviewdb.Change; import com.google.gerrit.reviewdb.PatchSet; import com.google.gerrit.reviewdb.Project; @@ -35,6 +36,7 @@ import com.google.gerrit.server.events.ChangeRestoreEvent; import com.google.gerrit.server.events.CommentAddedEvent; import com.google.gerrit.server.events.EventFactory; import com.google.gerrit.server.events.PatchSetCreatedEvent; +import com.google.gerrit.server.events.RefUpdatedEvent; import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.git.WorkQueue; import com.google.gerrit.server.project.ProjectCache; @@ -45,6 +47,8 @@ import com.google.inject.Singleton; import org.eclipse.jgit.errors.RepositoryNotFoundException; import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.RefUpdate; import org.eclipse.jgit.lib.Repository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -96,6 +100,9 @@ public class ChangeHookRunner { /** Filename of the change abandoned hook. */ private final File changeRestoredHook; + /** Filename of the ref updated hook. */ + private final File refUpdatedHook; + /** Repository Manager. */ private final GitRepositoryManager repoManager; @@ -141,6 +148,7 @@ public class ChangeHookRunner { changeMergedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "changeMergedHook", "change-merged")).getPath()); changeAbandonedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "changeAbandonedHook", "change-abandoned")).getPath()); changeRestoredHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "changeRestoredHook", "change-restored")).getPath()); + refUpdatedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "refUpdatedHook", "ref-updated")).getPath()); } public void addChangeListener(ChangeListener listener, IdentifiedUser user) { @@ -172,7 +180,16 @@ public class ChangeHookRunner { * @return Repository or null. */ private Repository openRepository(final Change change) { - Project.NameKey name = change.getProject(); + return openRepository(change.getProject()); + } + + /** + * Get the Repository for the given project name, or null on error. + * + * @param name Project to get repo for, + * @return Repository or null. + */ + private Repository openRepository(final Project.NameKey name) { try { return repoManager.openRepository(name.get()); } catch (RepositoryNotFoundException err) { @@ -335,6 +352,44 @@ public class ChangeHookRunner { runHook(openRepository(change), changeRestoredHook, args); } + /** + * Fire the Ref Updated Hook + * @param project The project the ref update occured on + * @param refUpdate An actual RefUpdate object + * @param account The gerrit user who moved the ref + */ + public void doRefUpdatedHook(final Branch.NameKey refName, final RefUpdate refUpdate, final Account account) { + doRefUpdatedHook(refName, refUpdate.getOldObjectId(), refUpdate.getNewObjectId(), account); + } + + /** + * Fire the Ref Updated Hook + * @param refName The Branch.NameKey of the ref that was updated + * @param oldId The ref's old id + * @param newId The ref's new id + * @param account The gerrit user who moved the ref + */ + public void doRefUpdatedHook(final Branch.NameKey refName, final ObjectId oldId, final ObjectId newId, final Account account) { + final RefUpdatedEvent event = new RefUpdatedEvent(); + + if (account != null) { + event.submitter = eventFactory.asAccountAttribute(account); + } + event.refUpdate = eventFactory.asRefUpdateAttribute(oldId, newId, refName); + fireEvent(refName, event); + + final List<String> args = new ArrayList<String>(); + addArg(args, "--oldrev", event.refUpdate.oldRev); + addArg(args, "--newrev", event.refUpdate.newRev); + addArg(args, "--refname", event.refUpdate.refName); + addArg(args, "--project", event.refUpdate.project); + if (account != null) { + addArg(args, "--submitter", getDisplayName(account)); + } + + runHook(openRepository(refName.getParentKey()), refUpdatedHook, args); + } + private void fireEvent(final Change change, final ChangeEvent event) { for (ChangeListenerHolder holder : listeners.values()) { if (isVisibleTo(change, holder.user)) { @@ -343,6 +398,14 @@ public class ChangeHookRunner { } } + private void fireEvent(Branch.NameKey branchName, final ChangeEvent event) { + for (ChangeListenerHolder holder : listeners.values()) { + if (isVisibleTo(branchName, holder.user)) { + holder.listener.onChangeEvent(event); + } + } + } + private boolean isVisibleTo(Change change, IdentifiedUser user) { final ProjectState pe = projectCache.get(change.getProject()); if (pe == null) { @@ -352,6 +415,15 @@ public class ChangeHookRunner { return pc.controlFor(change).isVisible(); } + private boolean isVisibleTo(Branch.NameKey branchName, IdentifiedUser user) { + final ProjectState pe = projectCache.get(branchName.getParentKey()); + if (pe == null) { + return false; + } + final ProjectControl pc = pe.controlFor(user); + return pc.controlForRef(branchName).isVisible(); + } + /** * Create an ApprovalAttribute for the given approval suitable for serialization to JSON. * @param approval diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/CollectionsUtil.java b/gerrit-server/src/main/java/com/google/gerrit/common/CollectionsUtil.java new file mode 100644 index 0000000000..6e635cb2c2 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/common/CollectionsUtil.java @@ -0,0 +1,43 @@ +// Copyright (C) 2010 The Android Open Source Project +// +// 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.google.gerrit.common; + +import java.util.Collection; + +/** Utilities for manipulating Collections . */ +public class CollectionsUtil { + /** + * Checks if any of the elements in the first collection can be found in the + * second collection. + * + * @param findAnyOfThese which elements to look for. + * @param inThisCollection where to look for them. + * @param <E> type of the elements in question. + * @return {@code true} if any of the elements in {@code findAnyOfThese} can + * be found in {@code inThisCollection}, {@code false} otherwise. + */ + public static <E> boolean isAnyIncludedIn(Collection<E> findAnyOfThese, + Collection<E> inThisCollection) { + for (E findThisItem : findAnyOfThese) { + if (inThisCollection.contains(findThisItem)) { + return true; + } + } + return false; + } + + private CollectionsUtil() { + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ReplicationUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/ReplicationUser.java index 32933241d8..5c44bfe58c 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/ReplicationUser.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/ReplicationUser.java @@ -17,7 +17,6 @@ package com.google.gerrit.server; import com.google.gerrit.reviewdb.AccountGroup; import com.google.gerrit.reviewdb.AccountProjectWatch; import com.google.gerrit.reviewdb.Change; -import com.google.gerrit.reviewdb.Project.NameKey; import com.google.gerrit.server.config.AuthConfig; import com.google.inject.Inject; import com.google.inject.assistedinject.Assisted; @@ -47,13 +46,7 @@ public class ReplicationUser extends CurrentUser { effectiveGroups = EVERYTHING_VISIBLE; } else if (authGroups.isEmpty()) { - // Only include the registered groups if no specific groups - // were provided. This allows an administrator to configure - // a replication user with a narrower view of the system than - // all other users, such as when replicating from an internal - // company server to a public open source distribution site. - // - effectiveGroups = authConfig.getRegisteredGroups(); + effectiveGroups = Collections.emptySet(); } else { effectiveGroups = copy(authGroups); diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java index 24ddd27f39..5cb8f36bf1 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java @@ -100,7 +100,7 @@ public class AccountManager { * @param who identity of the user, with any details we received about them. * @return the result of authenticating the user. * @throws AccountException the account does not exist, and cannot be created, - * or exists, but cannot be located. + * or exists, but cannot be located, or is inactive. */ public AuthResult authenticate(AuthRequest who) throws AccountException { who = realm.authenticate(who); @@ -114,9 +114,14 @@ public class AccountManager { // return create(db, who); - } else { - // Account exists, return the identity to the caller. - // + } else { // Account exists + + Account act = db.accounts().get(id.getAccountId()); + if (act == null || !act.isActive()) { + throw new AccountException("Authentication error, account inactive"); + } + + // return the identity to the caller. update(db, who, id); return new AuthResult(id.getAccountId(), key, false); } @@ -285,13 +290,18 @@ public class AccountManager { try { changeUserNameFactory.create(db, user, who.getUserName()).call(); } catch (NameAlreadyUsedException e) { - log.error("Cannot assign user name \"" + who.getUserName() - + "\" to account " + newId + "; name already in use."); + final String message = + "Cannot assign user name \"" + who.getUserName() + "\" to account " + + newId + "; name already in use."; + handleSettingUserNameFailure(db, account, extId, message, e, false); } catch (InvalidUserNameException e) { - log.error("Cannot assign user name \"" + who.getUserName() - + "\" to account " + newId + "; name does not conform."); + final String message = + "Cannot assign user name \"" + who.getUserName() + "\" to account " + + newId + "; name does not conform."; + handleSettingUserNameFailure(db, account, extId, message, e, false); } catch (OrmException e) { - log.error("Cannot assign user name", e); + final String message = "Cannot assign user name"; + handleSettingUserNameFailure(db, account, extId, message, e, true); } } @@ -300,6 +310,49 @@ public class AccountManager { return new AuthResult(newId, extId.getKey(), true); } + /** + * This method handles an exception that occurred during the setting of the + * user name for a newly created account. If the realm does not allow the user + * to set a user name manually this method deletes the newly created account + * and throws an {@link AccountUserNameException}. In any case the error + * message is logged. + * + * @param db the database + * @param account the newly created account + * @param extId the newly created external id + * @param errorMessage the error message + * @param e the exception that occurred during the setting of the user name + * for the new account + * @param logException flag that decides whether the exception should be + * included into the log + * @throws AccountUserNameException thrown if the realm does not allow the + * user to manually set the user name + * @throws OrmException thrown if cleaning the database failed + */ + private void handleSettingUserNameFailure(final ReviewDb db, + final Account account, final AccountExternalId extId, + final String errorMessage, final Exception e, final boolean logException) + throws AccountUserNameException, OrmException { + if (logException) { + log.error(errorMessage, e); + } else { + log.error(errorMessage); + } + if (!realm.allowsEdit(Account.FieldName.USER_NAME)) { + // setting the given user name has failed, but the realm does not + // allow the user to manually set a user name, + // this means we would end with an account without user name + // (without 'username:<USERNAME>' entry in + // account_external_ids table), + // such an account cannot be used for uploading changes, + // this is why the best we can do here is to fail early and cleanup + // the database + db.accounts().delete(Collections.singleton(account)); + db.accountExternalIds().delete(Collections.singleton(extId)); + throw new AccountUserNameException(errorMessage, e); + } + } + private static AccountExternalId createId(final Account.Id newId, final AuthRequest who) { final String ext = who.getExternalId(); diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountUserNameException.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountUserNameException.java new file mode 100644 index 0000000000..1cf8be80e4 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountUserNameException.java @@ -0,0 +1,28 @@ +// Copyright (C) 2010 The Android Open Source Project +// +// 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.google.gerrit.server.account; + +/** + * Thrown by {@link AccountManager} if the user name for a newly created account + * could not be set and the realm does not allow the user to set a user name + * manually. + */ +public class AccountUserNameException extends AccountException { + private static final long serialVersionUID = 1L; + + public AccountUserNameException(final String message, final Throwable why) { + super(message, why); + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/Helper.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/Helper.java index 6f6a4d440f..675202cc4d 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/Helper.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/Helper.java @@ -33,7 +33,9 @@ import java.util.List; import java.util.Properties; import java.util.Set; +import javax.naming.CompositeName; import javax.naming.Context; +import javax.naming.Name; import javax.naming.NamingEnumeration; import javax.naming.NamingException; import javax.naming.directory.Attribute; @@ -196,8 +198,9 @@ import javax.net.ssl.SSLSocketFactory; // Recursively identify the groups it is a member of. // try { + final Name compositeGroupName = new CompositeName().add(groupDN); final Attribute in = - ctx.getAttributes(groupDN).get(schema.accountMemberField); + ctx.getAttributes(compositeGroupName).get(schema.accountMemberField); if (in != null) { final NamingEnumeration<?> groups = in.getAll(); while (groups.hasMore()) { diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java index e66746da9d..6396431876 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java @@ -146,6 +146,7 @@ public class AuthConfig { case HTTP_LDAP: case LDAP: case LDAP_BIND: + case CLIENT_SSL_CERT_LDAP: // Its safe to assume yes for an HTTP authentication type, as the // only way in is through some external system that the admin trusts // diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigUtil.java index 73320ade7a..927e88eeec 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigUtil.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigUtil.java @@ -21,6 +21,7 @@ import com.google.gerrit.reviewdb.AccountGroupName; import com.google.gerrit.reviewdb.ReviewDb; import com.google.gwtorm.client.OrmException; import com.google.gwtorm.client.SchemaFactory; + import org.eclipse.jgit.lib.Config; import org.slf4j.Logger; @@ -31,6 +32,8 @@ import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class ConfigUtil { /** @@ -226,7 +229,7 @@ public class ConfigUtil { /** * Parse a numerical time unit, such as "1 minute", from a string. * - * @param s the string to parse. + * @param valueString the string to parse. * @param defaultValue default value to return if no value was set in the * configuration file. * @param wantUnit the units of {@code defaultValue} and the return value, as @@ -235,26 +238,16 @@ public class ConfigUtil { * @return the setting, or {@code defaultValue} if not set, expressed in * {@code units}. */ - public static long getTimeUnit(String s, long defaultValue, TimeUnit wantUnit) { - final String valueString = s; - final String unitName; - final int sp = s.indexOf(' '); - if (sp > 0) { - unitName = s.substring(sp + 1).trim(); - s = s.substring(0, sp); - } else { - final char last = s.charAt(s.length() - 1); - if ('0' <= last && last <= '9') { - unitName = ""; - } else { - unitName = String.valueOf(last); - s = s.substring(0, s.length() - 1).trim(); - } - } - if (s.length() == 0) { + public static long getTimeUnit(final String valueString, long defaultValue, + TimeUnit wantUnit) { + Matcher m = Pattern.compile("^([1-9][0-9]*)\\s*(.*)$").matcher(valueString); + if (!m.matches()) { return defaultValue; } + String digits = m.group(1); + String unitName = m.group(2).trim(); + TimeUnit inputUnit; int inputMul; @@ -299,7 +292,7 @@ public class ConfigUtil { } try { - return wantUnit.convert(Long.parseLong(s) * inputMul, inputUnit); + return wantUnit.convert(Long.parseLong(digits) * inputMul, inputUnit); } catch (NumberFormatException nfe) { throw notTimeUnit(valueString); } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java index fa2aaad0a4..f77550e446 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java @@ -17,6 +17,7 @@ package com.google.gerrit.server.config; import static com.google.inject.Scopes.SINGLETON; import com.google.gerrit.common.data.ApprovalTypes; +import com.google.gerrit.lifecycle.LifecycleListener; import com.google.gerrit.lifecycle.LifecycleModule; import com.google.gerrit.reviewdb.AccountGroup; import com.google.gerrit.reviewdb.AuthType; @@ -37,6 +38,7 @@ import com.google.gerrit.server.account.GroupCacheImpl; import com.google.gerrit.server.account.Realm; import com.google.gerrit.server.auth.ldap.LdapModule; import com.google.gerrit.server.cache.CachePool; +import com.google.gerrit.server.config.SitePaths; import com.google.gerrit.server.events.EventFactory; import com.google.gerrit.server.git.ChangeMergeQueue; import com.google.gerrit.server.git.GitRepositoryManager; @@ -46,6 +48,7 @@ import com.google.gerrit.server.git.PushAllProjectsOp; import com.google.gerrit.server.git.PushReplication; import com.google.gerrit.server.git.ReloadSubmitQueueOp; import com.google.gerrit.server.git.ReplicationQueue; +import com.google.gerrit.server.git.TransferConfig; import com.google.gerrit.server.git.WorkQueue; import com.google.gerrit.server.mail.EmailSender; import com.google.gerrit.server.mail.FromAddressGenerator; @@ -53,25 +56,66 @@ import com.google.gerrit.server.mail.FromAddressGeneratorProvider; import com.google.gerrit.server.mail.SmtpEmailSender; import com.google.gerrit.server.patch.PatchListCacheImpl; import com.google.gerrit.server.patch.PatchSetInfoFactory; +import com.google.gerrit.server.project.AccessControlModule; import com.google.gerrit.server.project.ChangeControl; import com.google.gerrit.server.project.ProjectCacheImpl; import com.google.gerrit.server.project.ProjectControl; import com.google.gerrit.server.project.ProjectState; +import com.google.gerrit.server.project.RefControl; import com.google.gerrit.server.tools.ToolsCatalog; import com.google.gerrit.server.util.IdGenerator; import com.google.gerrit.server.workflow.FunctionState; import com.google.inject.Inject; import com.google.inject.TypeLiteral; +import org.apache.velocity.app.Velocity; +import org.apache.velocity.runtime.RuntimeConstants; + import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.PersonIdent; +import java.util.Properties; import java.util.Set; + /** Starts global state with standard dependencies. */ public class GerritGlobalModule extends FactoryModule { private final AuthType loginType; + public static class VelocityLifecycle implements LifecycleListener { + private final SitePaths site; + + @Inject + VelocityLifecycle(final SitePaths site) { + this.site = site; + } + + @Override + public void start() { + String rl = "resource.loader"; + String pkg = "org.apache.velocity.runtime.resource.loader"; + Properties p = new Properties(); + + p.setProperty(rl, "file, class"); + p.setProperty("file." + rl + ".class", pkg + ".FileResourceLoader"); + p.setProperty("file." + rl + ".path", site.mail_dir.getAbsolutePath()); + p.setProperty("class." + rl + ".class", pkg + ".ClasspathResourceLoader"); + p.setProperty(RuntimeConstants.RUNTIME_LOG_LOGSYSTEM_CLASS, + "org.apache.velocity.runtime.log.SimpleLog4JLogSystem" ); + p.setProperty("runtime.log.logsystem.log4j.category", "velocity"); + + try { + Velocity.init(p); + } catch(Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public void stop() { + } + } + @Inject GerritGlobalModule(final AuthConfig authConfig, @GerritServerConfig final Config config) { @@ -84,6 +128,7 @@ public class GerritGlobalModule extends FactoryModule { case HTTP_LDAP: case LDAP: case LDAP_BIND: + case CLIENT_SSL_CERT_LDAP: install(new LdapModule()); break; @@ -94,10 +139,6 @@ public class GerritGlobalModule extends FactoryModule { bind(Project.NameKey.class).annotatedWith(WildProjectName.class) .toProvider(WildProjectNameProvider.class).in(SINGLETON); - bind(new TypeLiteral<Set<AccountGroup.Id>>(){}).annotatedWith(ProjectCreatorGroups.class) - .toProvider(ProjectCreatorGroupsProvider.class).in(SINGLETON); - bind(new TypeLiteral<Set<AccountGroup.Id>>(){}).annotatedWith(ProjectOwnerGroups.class) - .toProvider(ProjectOwnerGroupsProvider.class).in(SINGLETON); bind(ApprovalTypes.class).toProvider(ApprovalTypesProvider.class).in( SINGLETON); bind(EmailExpander.class).toProvider(EmailExpanderProvider.class).in( @@ -114,15 +155,18 @@ public class GerritGlobalModule extends FactoryModule { install(GroupCacheImpl.module()); install(PatchListCacheImpl.module()); install(ProjectCacheImpl.module()); + install(new AccessControlModule()); factory(AccountInfoCacheFactory.Factory.class); factory(ProjectState.Factory.class); + factory(RefControl.Factory.class); bind(GitRepositoryManager.class).to(LocalDiskRepositoryManager.class); bind(FileTypeRegistry.class).to(MimeUtilFileTypeRegistry.class); bind(WorkQueue.class); bind(ToolsCatalog.class); bind(EventFactory.class); + bind(TransferConfig.class); bind(ReplicationQueue.class).to(PushReplication.class).in(SINGLETON); factory(PushAllProjectsOp.Factory.class); @@ -147,6 +191,7 @@ public class GerritGlobalModule extends FactoryModule { listener().to(LocalDiskRepositoryManager.Lifecycle.class); listener().to(CachePool.Lifecycle.class); listener().to(WorkQueue.Lifecycle.class); + listener().to(VelocityLifecycle.class); } }); } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritServerConfigProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritServerConfigProvider.java index 6592ac774d..92a2614702 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritServerConfigProvider.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritServerConfigProvider.java @@ -20,7 +20,8 @@ import com.google.inject.ProvisionException; import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.lib.Config; -import org.eclipse.jgit.lib.FileBasedConfig; +import org.eclipse.jgit.storage.file.FileBasedConfig; +import org.eclipse.jgit.util.FS; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,7 +41,7 @@ class GerritServerConfigProvider implements Provider<Config> { @Override public Config get() { - FileBasedConfig cfg = new FileBasedConfig(site.gerrit_config); + FileBasedConfig cfg = new FileBasedConfig(site.gerrit_config, FS.DETECTED); if (!cfg.getFile().exists()) { log.info("No " + site.gerrit_config.getAbsolutePath() @@ -57,7 +58,7 @@ class GerritServerConfigProvider implements Provider<Config> { } if (site.secure_config.exists()) { - cfg = new FileBasedConfig(cfg, site.secure_config); + cfg = new FileBasedConfig(cfg, site.secure_config, FS.DETECTED); try { cfg.load(); } catch (IOException e) { diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GitReceivePackGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitReceivePackGroups.java new file mode 100644 index 0000000000..35ea9e6c2f --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitReceivePackGroups.java @@ -0,0 +1,37 @@ +// Copyright (C) 2010 The Android Open Source Project +// +// 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.google.gerrit.server.config; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.google.inject.BindingAnnotation; + +import java.lang.annotation.Retention; + +/** + * Used to populate the groups of users that are allowed to run + * receive-pack on the server. + * + * Gerrit.config example: + * + * <pre> + * [receive] + * allowGroup = RECEIVE_GROUP_ALLOWED + * </pre> + */ +@Retention(RUNTIME) +@BindingAnnotation +public @interface GitReceivePackGroups { +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GitReceivePackGroupsProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitReceivePackGroupsProvider.java new file mode 100644 index 0000000000..9af6d629b6 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitReceivePackGroupsProvider.java @@ -0,0 +1,42 @@ +// Copyright (C) 2010 The Android Open Source Project +// +// 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.google.gerrit.server.config; + +import com.google.gerrit.reviewdb.AccountGroup; +import com.google.gerrit.reviewdb.ReviewDb; +import com.google.gwtorm.client.SchemaFactory; +import com.google.inject.Inject; + +import org.eclipse.jgit.lib.Config; + +import java.util.Collections; +import java.util.HashSet; + +public class GitReceivePackGroupsProvider extends GroupSetProvider { + @Inject + public GitReceivePackGroupsProvider(@GerritServerConfig Config config, + AuthConfig authConfig, SchemaFactory<ReviewDb> db) { + super(config, db, "receive", null, "allowGroup"); + + // If no group was set, default to "registered users" + // + if (groupIds.isEmpty()) { + HashSet<AccountGroup.Id> all = new HashSet<AccountGroup.Id>(); + all.addAll(authConfig.getRegisteredGroups()); + all.removeAll(authConfig.getAnonymousGroups()); + groupIds = Collections.unmodifiableSet(all); + } + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GitUploadPackGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitUploadPackGroups.java new file mode 100644 index 0000000000..fa8ccb7244 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitUploadPackGroups.java @@ -0,0 +1,37 @@ +// Copyright (C) 2010 The Android Open Source Project +// +// 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.google.gerrit.server.config; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.google.inject.BindingAnnotation; + +import java.lang.annotation.Retention; + +/** + * Used to populate the groups of users that are allowed to run + * upload-pack on the server. + * + * Gerrit.config example: + * + * <pre> + * [upload] + * allowGroup = UPLOAD_GROUP_ALLOWED + * </pre> + */ +@Retention(RUNTIME) +@BindingAnnotation +public @interface GitUploadPackGroups { +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GitUploadPackGroupsProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitUploadPackGroupsProvider.java new file mode 100644 index 0000000000..bfb09a51bc --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitUploadPackGroupsProvider.java @@ -0,0 +1,42 @@ +// Copyright (C) 2010 The Android Open Source Project +// +// 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.google.gerrit.server.config; + +import com.google.gerrit.reviewdb.AccountGroup; +import com.google.gerrit.reviewdb.ReviewDb; +import com.google.gwtorm.client.SchemaFactory; +import com.google.inject.Inject; + +import org.eclipse.jgit.lib.Config; + +import java.util.Collections; +import java.util.HashSet; + +public class GitUploadPackGroupsProvider extends GroupSetProvider { + @Inject + public GitUploadPackGroupsProvider(@GerritServerConfig Config config, + AuthConfig authConfig, SchemaFactory<ReviewDb> db) { + super(config, db, "upload", null, "allowGroup"); + + // If no group was set, default to "registered users" and "anonymous" + // + if (groupIds.isEmpty()) { + HashSet<AccountGroup.Id> all = new HashSet<AccountGroup.Id>(); + all.addAll(authConfig.getRegisteredGroups()); + all.addAll(authConfig.getAnonymousGroups()); + groupIds = Collections.unmodifiableSet(all); + } + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GroupSetProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GroupSetProvider.java new file mode 100644 index 0000000000..373fdb56e1 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GroupSetProvider.java @@ -0,0 +1,50 @@ +// Copyright (C) 2010 The Android Open Source Project +// +// 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.google.gerrit.server.config; + +import static com.google.gerrit.server.config.ConfigUtil.groupsFor; +import static java.util.Collections.unmodifiableSet; + +import com.google.gerrit.reviewdb.AccountGroup; +import com.google.gerrit.reviewdb.ReviewDb; +import com.google.gwtorm.client.SchemaFactory; +import com.google.inject.Inject; +import com.google.inject.Provider; + +import org.eclipse.jgit.lib.Config; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Set; + +public abstract class GroupSetProvider implements + Provider<Set<AccountGroup.Id>> { + private static final Logger log = + LoggerFactory.getLogger(GroupSetProvider.class); + + protected Set<AccountGroup.Id> groupIds; + + @Inject + protected GroupSetProvider(@GerritServerConfig Config config, + SchemaFactory<ReviewDb> db, String section, String subsection, String name) { + String[] groupNames = config.getStringList(section, subsection, name); + groupIds = unmodifiableSet(groupsFor(db, groupNames, log)); + } + + @Override + public Set<AccountGroup.Id> get() { + return groupIds; + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/MasterNodeStartup.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/MasterNodeStartup.java index d2c50056a5..26c76c5878 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/config/MasterNodeStartup.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/MasterNodeStartup.java @@ -20,6 +20,8 @@ import com.google.gerrit.server.git.PushAllProjectsOp; import com.google.gerrit.server.git.ReloadSubmitQueueOp; import com.google.inject.Inject; +import org.eclipse.jgit.lib.Config; + import java.util.concurrent.TimeUnit; /** Configuration for a master node in a cluster of servers. */ @@ -32,17 +34,24 @@ public class MasterNodeStartup extends LifecycleModule { static class OnStart implements LifecycleListener { private final PushAllProjectsOp.Factory pushAll; private final ReloadSubmitQueueOp.Factory submit; + private final boolean replicateOnStartup; @Inject OnStart(final PushAllProjectsOp.Factory pushAll, - final ReloadSubmitQueueOp.Factory submit) { + final ReloadSubmitQueueOp.Factory submit, + final @GerritServerConfig Config cfg) { this.pushAll = pushAll; this.submit = submit; + + replicateOnStartup = cfg.getBoolean("gerrit", "replicateOnStartup", true); } @Override public void start() { - pushAll.create(null).start(30, TimeUnit.SECONDS); + if (replicateOnStartup) { + pushAll.create(null).start(30, TimeUnit.SECONDS); + } + submit.create().start(15, TimeUnit.SECONDS); } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectCreatorGroupsProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectCreatorGroupsProvider.java index 616e691149..381c914238 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectCreatorGroupsProvider.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectCreatorGroupsProvider.java @@ -14,19 +14,14 @@ package com.google.gerrit.server.config; -import com.google.gerrit.reviewdb.AccountGroup; import com.google.gerrit.reviewdb.ReviewDb; import com.google.gerrit.reviewdb.SystemConfig; import com.google.gwtorm.client.SchemaFactory; import com.google.inject.Inject; -import com.google.inject.Provider; import org.eclipse.jgit.lib.Config; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.util.Collections; -import java.util.Set; /** * Provider of the group(s) which are allowed to create new projects. Currently @@ -39,27 +34,14 @@ import java.util.Set; * createGroup = Administrators * </pre> */ -public class ProjectCreatorGroupsProvider implements - Provider<Set<AccountGroup.Id>> { - private static final Logger log = - LoggerFactory.getLogger(ProjectCreatorGroupsProvider.class); - - private final Set<AccountGroup.Id> groupIds; - +public class ProjectCreatorGroupsProvider extends GroupSetProvider { @Inject - ProjectCreatorGroupsProvider(@GerritServerConfig final Config config, - SchemaFactory<ReviewDb> db, final SystemConfig systemConfig) { - String[] names = config.getStringList("repository", "*", "createGroup"); - Set<AccountGroup.Id> createGroups = ConfigUtil.groupsFor(db, names, log); + public ProjectCreatorGroupsProvider(@GerritServerConfig final Config config, + final SystemConfig systemConfig, final SchemaFactory<ReviewDb> db) { + super(config, db, "repository", "*", "createGroup"); - if (createGroups.isEmpty()) { + if (groupIds.isEmpty()) { groupIds = Collections.singleton(systemConfig.adminGroupId); - } else { - groupIds = createGroups; } } - - public Set<AccountGroup.Id> get() { - return groupIds; - } } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java index 30234c0bfb..c457d734a6 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java @@ -18,11 +18,8 @@ import com.google.gerrit.reviewdb.AccountGroup; import com.google.gerrit.reviewdb.ReviewDb; import com.google.gwtorm.client.SchemaFactory; import com.google.inject.Inject; -import com.google.inject.Provider; import org.eclipse.jgit.lib.Config; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.util.Set; @@ -37,28 +34,15 @@ import java.util.Set; * ownerGroup = Administrators * </pre> */ -public class ProjectOwnerGroupsProvider implements - Provider<Set<AccountGroup.Id>> { - private static final Logger log = - LoggerFactory.getLogger(ProjectOwnerGroupsProvider.class); - - private final Set<AccountGroup.Id> groupIds; - +public class ProjectOwnerGroupsProvider extends GroupSetProvider { @Inject - ProjectOwnerGroupsProvider(@GerritServerConfig final Config config, - SchemaFactory<ReviewDb> db, - @ProjectCreatorGroups Set<AccountGroup.Id> creatorGroups) { - String[] names = config.getStringList("repository", "*", "ownerGroup"); - Set<AccountGroup.Id> ownerGroups = ConfigUtil.groupsFor(db, names, log); + public ProjectOwnerGroupsProvider( + @ProjectCreatorGroups final Set<AccountGroup.Id> creatorGroups, + @GerritServerConfig final Config config, final SchemaFactory<ReviewDb> db) { + super(config, db, "repository", "*", "ownerGroup"); - if (ownerGroups.isEmpty()) { + if (groupIds.isEmpty()) { groupIds = creatorGroups; - } else { - groupIds = ownerGroups; } } - - public Set<AccountGroup.Id> get() { - return groupIds; - } } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java index 1faa672a95..c3a5fb7abb 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java @@ -28,6 +28,7 @@ public final class SitePaths { public final File etc_dir; public final File lib_dir; public final File logs_dir; + public final File mail_dir; public final File hooks_dir; public final File static_dir; @@ -61,6 +62,7 @@ public final class SitePaths { etc_dir = new File(site_path, "etc"); lib_dir = new File(site_path, "lib"); logs_dir = new File(site_path, "logs"); + mail_dir = new File(etc_dir, "mail"); hooks_dir = new File(site_path, "hooks"); static_dir = new File(site_path, "static"); diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java index 3ad397e0b0..573e6bb250 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java @@ -17,6 +17,7 @@ package com.google.gerrit.server.events; import com.google.gerrit.common.data.ApprovalType; import com.google.gerrit.common.data.ApprovalTypes; import com.google.gerrit.reviewdb.Account; +import com.google.gerrit.reviewdb.Branch; import com.google.gerrit.reviewdb.Change; import com.google.gerrit.reviewdb.PatchSet; import com.google.gerrit.reviewdb.PatchSetApproval; @@ -28,8 +29,11 @@ import com.google.inject.Provider; import com.google.inject.Singleton; import com.google.inject.internal.Nullable; +import org.eclipse.jgit.lib.ObjectId; + import java.util.ArrayList; import java.util.Collection; +import java.util.Map; @Singleton public class EventFactory { @@ -67,6 +71,23 @@ public class EventFactory { } /** + * Create a RefUpdateAttribute for the given old ObjectId, new ObjectId, and + * branch that is suitable for serialization to JSON. + * + * @param refUpdate + * @param refName + * @return object suitable for serialization to JSON + */ + public RefUpdateAttribute asRefUpdateAttribute(final ObjectId oldId, final ObjectId newId, final Branch.NameKey refName) { + RefUpdateAttribute ru = new RefUpdateAttribute(); + ru.newRev = newId != null ? newId.getName() : ObjectId.zeroId().getName(); + ru.oldRev = oldId != null ? oldId.getName() : ObjectId.zeroId().getName(); + ru.project = refName.getParentKey().get(); + ru.refName = refName.getShortName(); + return ru; + } + + /** * Extend the existing ChangeAttribute with additional fields. * * @param a @@ -89,10 +110,19 @@ public class EventFactory { } public void addPatchSets(ChangeAttribute a, Collection<PatchSet> ps) { + addPatchSets(a, ps, null); + } + + public void addPatchSets(ChangeAttribute ca, Collection<PatchSet> ps, + Map<PatchSet.Id,Collection<PatchSetApproval>> approvals) { if (!ps.isEmpty()) { - a.patchSets = new ArrayList<PatchSetAttribute>(ps.size()); + ca.patchSets = new ArrayList<PatchSetAttribute>(ps.size()); for (PatchSet p : ps) { - a.patchSets.add(asPatchSetAttribute(p)); + PatchSetAttribute psa = asPatchSetAttribute(p); + if (approvals != null) { + addApprovals(psa, p.getId(), approvals); + } + ca.patchSets.add(psa); } } } @@ -120,6 +150,14 @@ public class EventFactory { return p; } + public void addApprovals(PatchSetAttribute p, PatchSet.Id id, + Map<PatchSet.Id,Collection<PatchSetApproval>> all) { + Collection<PatchSetApproval> list = all.get(id); + if (list != null) { + addApprovals(p, list); + } + } + public void addApprovals(PatchSetAttribute p, Collection<PatchSetApproval> list) { if (!list.isEmpty()) { diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/RefUpdateAttribute.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/RefUpdateAttribute.java new file mode 100644 index 0000000000..e4d715a3f0 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/RefUpdateAttribute.java @@ -0,0 +1,22 @@ +// Copyright (C) 2010 The Android Open Source Project +// +// 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.google.gerrit.server.events; + +public class RefUpdateAttribute { + public String oldRev; + public String newRev; + public String refName; + public String project; +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/RefUpdatedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/RefUpdatedEvent.java new file mode 100644 index 0000000000..f90bc81018 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/RefUpdatedEvent.java @@ -0,0 +1,21 @@ +// Copyright (C) 2010 The Android Open Source Project +// +// 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.google.gerrit.server.events; + +public class RefUpdatedEvent extends ChangeEvent { + public final String type = "ref-updated"; + public AccountAttribute submitter; + public RefUpdateAttribute refUpdate; +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/GitProjectImporter.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/GitProjectImporter.java index 676e4b68e6..33661ab46b 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/GitProjectImporter.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/GitProjectImporter.java @@ -102,6 +102,8 @@ public class GitProjectImporter { p.setSubmitType(SubmitType.MERGE_IF_NECESSARY); p.setUseContributorAgreements(false); p.setUseSignedOffBy(false); + p.setUseContentMerge(false); + p.setRequireChangeID(false); db.projects().insert(Collections.singleton(p)); } else if (f.isDirectory()) { diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java index e8df230e00..c644ca8f45 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java @@ -23,12 +23,12 @@ import com.google.inject.Singleton; import org.eclipse.jgit.errors.RepositoryNotFoundException; import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.Constants; -import org.eclipse.jgit.lib.LockFile; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.RepositoryCache; -import org.eclipse.jgit.lib.WindowCache; -import org.eclipse.jgit.lib.WindowCacheConfig; import org.eclipse.jgit.lib.RepositoryCache.FileKey; +import org.eclipse.jgit.storage.file.LockFile; +import org.eclipse.jgit.storage.file.WindowCache; +import org.eclipse.jgit.storage.file.WindowCacheConfig; import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.IO; import org.eclipse.jgit.util.RawParseUtils; @@ -135,25 +135,29 @@ public class LocalDiskRepositoryManager implements GitRepositoryManager { public String getProjectDescription(final String name) throws RepositoryNotFoundException, IOException { final Repository e = openRepository(name); - final File d = new File(e.getDirectory(), "description"); - - String description; try { - description = RawParseUtils.decode(IO.readFully(d)); - } catch (FileNotFoundException err) { - return null; - } + final File d = new File(e.getDirectory(), "description"); - if (description != null) { - description = description.trim(); - if (description.isEmpty()) { - description = null; + String description; + try { + description = RawParseUtils.decode(IO.readFully(d)); + } catch (FileNotFoundException err) { + return null; } - if (UNNAMED.equals(description)) { - description = null; + + if (description != null) { + description = description.trim(); + if (description.isEmpty()) { + description = null; + } + if (UNNAMED.equals(description)) { + description = null; + } } + return description; + } finally { + e.close(); } - return description; } public void setProjectDescription(final String name, final String description) { @@ -164,21 +168,24 @@ public class LocalDiskRepositoryManager implements GitRepositoryManager { final LockFile f; e = openRepository(name); - f = new LockFile(new File(e.getDirectory(), "description")); - if (f.lock()) { - String d = description; - if (d != null) { - d = d.trim(); - if (d.length() > 0) { - d += "\n"; + try { + f = new LockFile(new File(e.getDirectory(), "description"), FS.DETECTED); + if (f.lock()) { + String d = description; + if (d != null) { + d = d.trim(); + if (d.length() > 0) { + d += "\n"; + } + } else { + d = ""; } - } else { - d = ""; + f.write(Constants.encode(d)); + f.commit(); } - f.write(Constants.encode(d)); - f.commit(); + } finally { + e.close(); } - e.close(); } catch (RepositoryNotFoundException e) { log.error("Cannot update description for " + name, e); } catch (IOException e) { diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java index a5b739e166..2efb1f4db1 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java @@ -56,9 +56,10 @@ import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.errors.RepositoryNotFoundException; import org.eclipse.jgit.lib.AnyObjectId; -import org.eclipse.jgit.lib.Commit; +import org.eclipse.jgit.lib.CommitBuilder; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectInserter; import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.RefUpdate; @@ -76,6 +77,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; +import java.io.UnsupportedEncodingException; import java.sql.Timestamp; import java.util.ArrayList; import java.util.Collection; @@ -202,6 +204,9 @@ public class MergeOp { try { mergeImpl(); } finally { + if (rw != null) { + rw.release(); + } if (db != null) { db.close(); } @@ -267,7 +272,7 @@ public class MergeOp { branchTip = null; } - for (final Ref r : rw.getRepository().getAllRefs().values()) { + for (final Ref r : db.getAllRefs().values()) { if (r.getName().startsWith(Constants.R_HEADS) || r.getName().startsWith(Constants.R_TAGS)) { try { @@ -431,7 +436,18 @@ public class MergeOp { } private void mergeOneCommit(final CodeReviewCommit n) throws MergeException { - final Merger m = MergeStrategy.SIMPLE_TWO_WAY_IN_CORE.newMerger(db); + final ThreeWayMerger m; + if (destProject.isUseContentMerge()) { + // Settings for this project allow us to try and + // automatically resolve conflicts within files if needed. + // Use ResolveMerge and instruct to operate in core. + m = MergeStrategy.RESOLVE.newMerger(db, true); + } else { + // No auto conflict resolving allowed. If any of the + // affected files was modified, merge will fail. + m = MergeStrategy.SIMPLE_TWO_WAY_IN_CORE.newMerger(db); + } + try { if (m.merge(new AnyObjectId[] {mergeTip, n})) { writeMergeCommit(m, n); @@ -547,15 +563,14 @@ public class MergeOp { authorIdent = myIdent; } - final Commit mergeCommit = new Commit(db); + final CommitBuilder mergeCommit = new CommitBuilder(); mergeCommit.setTreeId(m.getResultTreeId()); - mergeCommit.setParentIds(new ObjectId[] {mergeTip, n}); + mergeCommit.setParentIds(mergeTip, n); mergeCommit.setAuthor(authorIdent); mergeCommit.setCommitter(myIdent); mergeCommit.setMessage(msgbuf.toString()); - final ObjectId id = m.getObjectWriter().writeCommit(mergeCommit); - mergeTip = (CodeReviewCommit) rw.parseCommit(id); + mergeTip = (CodeReviewCommit) rw.parseCommit(commit(m, mergeCommit)); } private void markCleanMerges() throws MergeException { @@ -602,7 +617,17 @@ public class MergeOp { final CodeReviewCommit n = toMerge.remove(0); final ThreeWayMerger m; - m = MergeStrategy.SIMPLE_TWO_WAY_IN_CORE.newMerger(db); + if (destProject.isUseContentMerge()) { + // Settings for this project allow us to try and + // automatically resolve conflicts within files if needed. + // Use ResolveMerge and instruct to operate in core. + m = MergeStrategy.RESOLVE.newMerger(db, true); + } else { + // No auto conflict resolving allowed. If any of the + // affected files was modified, merge will fail. + m = MergeStrategy.SIMPLE_TWO_WAY_IN_CORE.newMerger(db); + } + try { if (mergeTip == null) { // The branch is unborn. Take a fast-forward resolution to @@ -789,14 +814,14 @@ public class MergeOp { log.error("Can't read approval records for " + n.patchsetId, e); } - final Commit mergeCommit = new Commit(db); + final CommitBuilder mergeCommit = new CommitBuilder(); mergeCommit.setTreeId(m.getResultTreeId()); - mergeCommit.setParentIds(new ObjectId[] {mergeTip}); + mergeCommit.setParentId(mergeTip); mergeCommit.setAuthor(n.getAuthorIdent()); mergeCommit.setCommitter(toCommitterIdent(submitAudit)); mergeCommit.setMessage(msgbuf.toString()); - final ObjectId id = m.getObjectWriter().writeCommit(mergeCommit); + final ObjectId id = commit(m, mergeCommit); final CodeReviewCommit newCommit = (CodeReviewCommit) rw.parseCommit(id); newCommit.copyFrom(n); newCommit.statusCode = CommitMergeStatus.CLEAN_PICK; @@ -806,6 +831,18 @@ public class MergeOp { setRefLogIdent(submitAudit); } + private ObjectId commit(final Merger m, final CommitBuilder mergeCommit) + throws IOException, UnsupportedEncodingException { + ObjectInserter oi = m.getObjectInserter(); + try { + ObjectId id = oi.insert(mergeCommit); + oi.flush(); + return id; + } finally { + oi.release(); + } + } + private boolean contains(List<FooterLine> footers, FooterKey key, String val) { for (final FooterLine line : footers) { if (line.matches(key) && val.equals(line.getValue())) { @@ -844,6 +881,13 @@ public class MergeOp { case FAST_FORWARD: replication.scheduleUpdate(destBranch.getParentKey(), branchUpdate .getName()); + + Account account = null; + final PatchSetApproval submitter = getSubmitter(mergeTip.patchsetId); + if (submitter != null) { + account = accountCache.get(submitter.getAccountId()).getAccount(); + } + hooks.doRefUpdatedHook(destBranch, branchUpdate, account); break; default: diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/PushOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/PushOp.java index ac53830097..1c100dfc9a 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/PushOp.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/PushOp.java @@ -78,6 +78,13 @@ class PushOp implements ProjectRunnable { private Repository db; + /** + * It indicates if the current instance is in fact retrying to push. + */ + private boolean retrying; + + private boolean canceled; + @Inject PushOp(final GitRepositoryManager grm, final SchemaFactory<ReviewDb> s, final PushReplication.ReplicationConfig p, final RemoteConfig c, @@ -90,6 +97,22 @@ class PushOp implements ProjectRunnable { uri = u; } + public boolean isRetrying() { + return retrying; + } + + public void setToRetry() { + retrying = true; + } + + public void cancel() { + canceled = true; + } + + public boolean wasCanceled() { + return canceled; + } + URIish getURI() { return uri; } @@ -103,45 +126,73 @@ class PushOp implements ProjectRunnable { } } + public Set<String> getRefs() { + final Set<String> refs; + + if (mirror) { + refs = new HashSet<String>(1); + refs.add(MIRROR_ALL); + } else { + refs = delta; + } + + return refs; + } + + public void addRefs(Set<String> refs) { + if (!mirror) { + for (String ref : refs) { + addRef(ref); + } + } + } + public void run() { - try { - // Lock the queue, and remove ourselves, so we can't be modified once - // we start replication (instead a new instance, with the same URI, is - // created and scheduled for a future point in time.) - // - pool.notifyStarting(this); - db = repoManager.openRepository(projectName.get()); - runImpl(); - } catch (RepositoryNotFoundException e) { - log.error("Cannot replicate " + projectName + "; " + e.getMessage()); - - } catch (NoRemoteRepositoryException e) { - log.error("Cannot replicate to " + uri + "; repository not found"); - - } catch (NotSupportedException e) { - log.error("Cannot replicate to " + uri, e); - - } catch (TransportException e) { - final Throwable cause = e.getCause(); - if (cause instanceof JSchException - && cause.getMessage().startsWith("UnknownHostKey:")) { - log.error("Cannot replicate to " + uri + ": " + cause.getMessage()); - } else { + // Lock the queue, and remove ourselves, so we can't be modified once + // we start replication (instead a new instance, with the same URI, is + // created and scheduled for a future point in time.) + // + pool.notifyStarting(this); + + // It should only verify if it was canceled after calling notifyStarting, + // since the canceled flag would be set locking the queue. + if (!canceled) { + try { + db = repoManager.openRepository(projectName.get()); + runImpl(); + } catch (RepositoryNotFoundException e) { + log.error("Cannot replicate " + projectName + "; " + e.getMessage()); + + } catch (NoRemoteRepositoryException e) { + log.error("Cannot replicate to " + uri + "; repository not found"); + + } catch (NotSupportedException e) { log.error("Cannot replicate to " + uri, e); - } - } catch (IOException e) { - log.error("Cannot replicate to " + uri, e); + } catch (TransportException e) { + final Throwable cause = e.getCause(); + if (cause instanceof JSchException + && cause.getMessage().startsWith("UnknownHostKey:")) { + log.error("Cannot replicate to " + uri + ": " + cause.getMessage()); + } else { + log.error("Cannot replicate to " + uri, e); + } + + // The remote push operation should be retried. + pool.reschedule(this); + } catch (IOException e) { + log.error("Cannot replicate to " + uri, e); - } catch (RuntimeException e) { - log.error("Unexpected error during replication to " + uri, e); + } catch (RuntimeException e) { + log.error("Unexpected error during replication to " + uri, e); - } catch (Error e) { - log.error("Unexpected error during replication to " + uri, e); + } catch (Error e) { + log.error("Unexpected error during replication to " + uri, e); - } finally { - if (db != null) { - db.close(); + } finally { + if (db != null) { + db.close(); + } } } } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/PushReplication.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/PushReplication.java index f26bf65301..382a7390d1 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/PushReplication.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/PushReplication.java @@ -37,7 +37,7 @@ import com.jcraft.jsch.Session; import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.lib.Config; -import org.eclipse.jgit.lib.FileBasedConfig; +import org.eclipse.jgit.storage.file.FileBasedConfig; import org.eclipse.jgit.transport.OpenSshConfig; import org.eclipse.jgit.transport.RefSpec; import org.eclipse.jgit.transport.RemoteConfig; @@ -55,7 +55,6 @@ import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; @@ -129,7 +128,8 @@ public class PushReplication implements ReplicationQueue { private List<ReplicationConfig> allConfigs(final SitePaths site) throws ConfigInvalidException, IOException { - final FileBasedConfig cfg = new FileBasedConfig(site.replication_config); + final FileBasedConfig cfg = + new FileBasedConfig(site.replication_config, FS.DETECTED); if (!cfg.getFile().exists()) { log.warn("No " + cfg.getFile() + "; not replicating"); @@ -306,6 +306,7 @@ public class PushReplication implements ReplicationQueue { static class ReplicationConfig { private final RemoteConfig remote; private final int delay; + private final int retryDelay; private final WorkQueue.Executor pool; private final Map<URIish, PushOp> pending = new HashMap<URIish, PushOp>(); private final PushOp.Factory opFactory; @@ -317,6 +318,7 @@ public class PushReplication implements ReplicationQueue { remote = rc; delay = Math.max(0, getInt(rc, cfg, "replicationdelay", 15)); + retryDelay = Math.max(0, getInt(rc, cfg, "replicationretry", 1)); final int poolSize = Math.max(0, getInt(rc, cfg, "threads", 1)); final String poolName = "ReplicateTo-" + rc.getName(); @@ -382,6 +384,96 @@ public class PushReplication implements ReplicationQueue { } } + /** + * It schedules again a PushOp instance. + * <p> + * It is assumed to be previously scheduled and found a + * transport exception. It will schedule it as a push + * operation to be retried after the minutes count + * determined by class attribute retryDelay. + * <p> + * In case the PushOp instance to be scheduled has same + * URI than one also pending for retry, it adds to the one + * pending the refs list of the parameter instance. + * <p> + * In case the PushOp instance to be scheduled has same + * URI than one pending, but not pending for retry, it + * indicates the one pending should be canceled when it + * starts executing, removes it from pending list, and + * adds its refs to the parameter instance. The parameter + * instance is scheduled for retry. + * <p> + * Notice all operations to indicate a PushOp should be + * canceled, or it is retrying, or remove/add it from/to + * pending Map should be protected by the lock on pending + * Map class instance attribute. + * + * @param pushOp The PushOp instance to be scheduled. + */ + void reschedule(final PushOp pushOp) { + try { + if (!controlFor(pushOp.getProjectNameKey()).isVisible()) { + return; + } + } catch (NoSuchProjectException e1) { + log.error("Internal error: project " + pushOp.getProjectNameKey() + + " not found during replication"); + return; + } + + // It locks access to pending variable. + synchronized (pending) { + PushOp pendingPushOp = pending.get(pushOp.getURI()); + + if (pendingPushOp != null) { + // There is one PushOp instance already pending to same URI. + + if (pendingPushOp.isRetrying()) { + // The one pending is one already retrying, so it should + // maintain it and add to it the refs of the one passed + // as parameter to the method. + + // This scenario would happen if a PushOp has started running + // and then before it failed due transport exception, another + // one to same URI started. The first one would fail and would + // be rescheduled, being present in pending list. When the + // second one fails, it will also be rescheduled and then, + // here, find out replication to its URI is already pending + // for retry (blocking). + pendingPushOp.addRefs(pushOp.getRefs()); + + } else { + // The one pending is one that is NOT retrying, it was just + // scheduled believing no problem would happen. The one pending + // should be canceled, and this is done by setting its canceled + // flag, removing it from pending list, and adding its refs to + // the pushOp instance that should then, later, in this method, + // be scheduled for retry. + + // Notice that the PushOp found pending will start running and, + // when notifying it is starting (with pending lock protection), + // it will see it was canceled and then it will do nothing with + // pending list and it will not execute its run implementation. + + pendingPushOp.cancel(); + pending.remove(pendingPushOp); + + pushOp.addRefs(pendingPushOp.getRefs()); + } + } + + if (pendingPushOp == null || !pendingPushOp.isRetrying()) { + // The PushOp method param instance should be scheduled for retry. + // Remember when retrying it should be used different delay. + + pushOp.setToRetry(); + + pending.put(pushOp.getURI(), pushOp); + pool.schedule(pushOp, retryDelay, TimeUnit.MINUTES); + } + } + } + ProjectControl controlFor(final Project.NameKey project) throws NoSuchProjectException { return projectControlFactory.controlFor(project); @@ -389,7 +481,9 @@ public class PushReplication implements ReplicationQueue { void notifyStarting(final PushOp op) { synchronized (pending) { - pending.remove(op.getURI()); + if (!op.wasCanceled()) { + pending.remove(op.getURI()); + } } } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java index a3140ec8a6..8b6f39782c 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java @@ -55,9 +55,12 @@ import com.google.gwtorm.client.OrmException; import com.google.inject.Inject; import com.google.inject.assistedinject.Assisted; +import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.lib.AbbreviatedObjectId; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectReader; import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.RefUpdate; @@ -225,11 +228,33 @@ public class ReceiveCommits implements PreReceiveHook, PostReceiveHook { /** Determine if the user can upload commits. */ public Capable canUpload() { - if (!projectControl.canUploadToAtLeastOneRef()) { + if (!projectControl.canPushToAtLeastOneRef()) { String reqName = project.getName(); return new Capable("Upload denied for project '" + reqName + "'"); } + // Don't permit receive-pack to be executed if a refs/for/branch_name + // reference exists in the destination repository. These block the + // client from being able to even send us a pack file, as it is very + // unlikely the user passed the --force flag and the new commit is + // probably not going to fast-forward the branch. + // + Map<String, Ref> blockingFors; + try { + blockingFors = repo.getRefDatabase().getRefs("refs/for/"); + } catch (IOException err) { + String projName = project.getName(); + log.warn("Cannot scan refs in '" + projName + "'", err); + return new Capable("Server process cannot read '" + projName + "'"); + } + if (!blockingFors.isEmpty()) { + String projName = project.getName(); + log.error("Repository '" + projName + + "' needs the following refs removed to receive changes: " + + blockingFors.keySet()); + return new Capable("One or more refs/for/ names blocks change upload"); + } + if (project.isUseContributorAgreements()) { try { return verifyActiveContributorAgreement(); @@ -277,6 +302,8 @@ public class ReceiveCommits implements PreReceiveHook, PostReceiveHook { // Change refs are scheduled when they are created. // replication.scheduleUpdate(project.getNameKey(), c.getRefName()); + Branch.NameKey destBranch = new Branch.NameKey(project.getNameKey(), c.getRefName()); + hooks.doRefUpdatedHook(destBranch, c.getOldId(), c.getNewId(), currentUser.getAccount()); } } } @@ -537,18 +564,11 @@ public class ReceiveCommits implements PreReceiveHook, PostReceiveHook { } private void parseRewind(final ReceiveCommand cmd) { - final RevObject oldObject, newObject; - try { - oldObject = rp.getRevWalk().parseAny(cmd.getOldId()); - } catch (IOException err) { - log.error("Invalid object " + cmd.getOldId().name() + " for " - + cmd.getRefName() + " forced update", err); - reject(cmd, "invalid object"); - return; - } - + RevCommit newObject; try { - newObject = rp.getRevWalk().parseAny(cmd.getNewId()); + newObject = rp.getRevWalk().parseCommit(cmd.getNewId()); + } catch (IncorrectObjectTypeException notCommit) { + newObject = null; } catch (IOException err) { log.error("Invalid object " + cmd.getNewId().name() + " for " + cmd.getRefName() + " forced update", err); @@ -557,9 +577,14 @@ public class ReceiveCommits implements PreReceiveHook, PostReceiveHook { } RefControl ctl = projectControl.controlForRef(cmd.getRefName()); - if (oldObject instanceof RevCommit && newObject instanceof RevCommit - && ctl.canForceUpdate()) { + if (newObject != null) { validateNewCommits(ctl, cmd); + if (cmd.getResult() != ReceiveCommand.Result.NOT_ATTEMPTED) { + return; + } + } + + if (ctl.canForceUpdate()) { // Let the core receive process handle it } else { cmd.setResult(ReceiveCommand.Result.REJECTED_NONFASTFORWARD); @@ -1061,19 +1086,30 @@ public class ReceiveCommits implements PreReceiveHook, PostReceiveHook { if (priorPatchSet.equals(ps.getId()) && c.getTree() == prior.getTree()) { rp.getRevWalk().parseBody(prior); final boolean messageEq = - c.getFullMessage().equals(prior.getFullMessage()); + eq(c.getFullMessage(), prior.getFullMessage()); final boolean parentsEq = parentsEqual(c, prior); + final boolean authorEq = authorEqual(c, prior); - if (messageEq && parentsEq) { + if (messageEq && parentsEq && authorEq) { reject(request.cmd, "no changes made"); return null; } else { - rp.sendMessage("(W) " + c.abbreviate(repo, 6).name() + ":" // - + " no files changed, but" // - + (!messageEq ? " message updated" : "") // - + (!messageEq && !parentsEq ? " and" : "") // - + (!parentsEq ? " was rebased" : "") // - ); + ObjectReader reader = rp.getRevWalk().getObjectReader(); + StringBuilder msg = new StringBuilder(); + msg.append("(W) "); + msg.append(reader.abbreviate(c).name()); + msg.append(":"); + msg.append(" no files changed"); + if (!authorEq) { + msg.append(", author changed"); + } + if (!messageEq) { + msg.append(", message updated"); + } + if (!parentsEq) { + msg.append(", was rebased"); + } + rp.sendMessage(msg.toString()); } } } catch (IOException e) { @@ -1256,6 +1292,30 @@ public class ReceiveCommits implements PreReceiveHook, PostReceiveHook { return true; } + static boolean authorEqual(RevCommit a, RevCommit b) { + PersonIdent aAuthor = a.getAuthorIdent(); + PersonIdent bAuthor = b.getAuthorIdent(); + + if (aAuthor == null && bAuthor == null) { + return true; + } else if (aAuthor == null || bAuthor == null) { + return false; + } + + return eq(aAuthor.getName(), bAuthor.getName()) + && eq(aAuthor.getEmailAddress(), bAuthor.getEmailAddress()); + } + + static boolean eq(String a, String b) { + if (a == null && b == null) { + return true; + } else if (a == null || b == null) { + return false; + } else { + return a.equals(b); + } + } + private void insertDummyApproval(final ReplaceResult result, final Account.Id forAccount, final ApprovalCategory.Id catId, final ReviewDb db) throws OrmException { @@ -1402,12 +1462,38 @@ public class ReceiveCommits implements PreReceiveHook, PostReceiveHook { } } + if (project.isRequireChangeID()) { + final List<String> idList = c.getFooterLines(CHANGE_ID); + if (idList.isEmpty()) { + reject(cmd, "missing Change-Id in commit message "); + return false; + } + + if (idList.size() > 1) { + reject(cmd, "multiple Change-Id lines in commit message "); + return false; + } + + final String v = idList.get(idList.size() - 1).trim(); + if (!v.matches("^I[0-9a-f]{8,}.*$")) { + reject(cmd, "invalid Change-Id line format in commit message "); + return false; + } + } + return true; } private void warnMalformedMessage(RevCommit c) { + ObjectReader reader = rp.getRevWalk().getObjectReader(); if (65 < c.getShortMessage().length()) { - rp.sendMessage("(W) " + c.abbreviate(repo, 6).name() + AbbreviatedObjectId id; + try { + id = reader.abbreviate(c); + } catch (IOException err) { + id = c.abbreviate(6); + } + rp.sendMessage("(W) " + id.name() // + ": commit subject >65 characters; use shorter first paragraph"); } @@ -1422,7 +1508,13 @@ public class ReceiveCommits implements PreReceiveHook, PostReceiveHook { } if (0 < longLineCnt && 33 < longLineCnt * 100 / nonEmptyCnt) { - rp.sendMessage("(W) " + c.abbreviate(repo, 6).name() + AbbreviatedObjectId id; + try { + id = reader.abbreviate(c); + } catch (IOException err) { + id = c.abbreviate(6); + } + rp.sendMessage("(W) " + id.name() // + ": commit message lines >70 characters; manually wrap lines"); } } @@ -1462,10 +1554,6 @@ public class ReceiveCommits implements PreReceiveHook, PostReceiveHook { final PatchSet.Id psi = doReplace(req); if (psi != null) { closeChange(req.cmd, psi, req.newCommit); - } else { - log.warn("Replacement of Change-Id " + req.ontoChange - + " with commit " + req.newCommit.name() - + " did not import the new patch set."); } } } catch (IOException e) { diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/TransferConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/TransferConfig.java index c976e7fcf3..de4130dca5 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/TransferConfig.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/TransferConfig.java @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.gerrit.sshd; +package com.google.gerrit.server.git; import com.google.gerrit.server.config.ConfigUtil; import com.google.gerrit.server.config.GerritServerConfig; @@ -20,21 +20,32 @@ import com.google.inject.Inject; import com.google.inject.Singleton; import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.storage.pack.PackConfig; import java.util.concurrent.TimeUnit; @Singleton public class TransferConfig { private final int timeout; + private final PackConfig packConfig; @Inject TransferConfig(@GerritServerConfig final Config cfg) { timeout = (int) ConfigUtil.getTimeUnit(cfg, "transfer", null, "timeout", // 0, TimeUnit.SECONDS); + + packConfig = new PackConfig(); + packConfig.setDeltaCompress(false); + packConfig.setThreads(1); + packConfig.fromConfig(cfg); } /** @return configured timeout, in seconds. 0 if the timeout is infinite. */ public int getTimeout() { return timeout; } + + public PackConfig getPackConfig() { + return packConfig; + } } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java index 05da2bede0..ffc3fd8e19 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java @@ -118,16 +118,20 @@ public class VisibleRefFilter implements RefFilter { private void addVisibleTags(final Map<String, Ref> result, final List<Ref> tags) { final RevWalk rw = new RevWalk(db); - final RevFlag VISIBLE = rw.newFlag("VISIBLE"); - final List<RevCommit> starts; + try { + final RevFlag VISIBLE = rw.newFlag("VISIBLE"); + final List<RevCommit> starts; - rw.carry(VISIBLE); - starts = lookupVisibleCommits(result, rw, VISIBLE); + rw.carry(VISIBLE); + starts = lookupVisibleCommits(result, rw, VISIBLE); - for (Ref tag : tags) { - if (isTagVisible(rw, VISIBLE, starts, tag)) { - result.put(tag.getName(), tag); + for (Ref tag : tags) { + if (isTagVisible(rw, VISIBLE, starts, tag)) { + result.put(tag.getName(), tag); + } } + } finally { + rw.release(); } } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/AbandonedSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/AbandonedSender.java index d2b5c29c3c..e67984978a 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/AbandonedSender.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/AbandonedSender.java @@ -30,7 +30,7 @@ public class AbandonedSender extends ReplyToChangeSender { } @Override - protected void init() { + protected void init() throws EmailException { super.init(); ccAllApprovals(); @@ -39,10 +39,7 @@ public class AbandonedSender extends ReplyToChangeSender { } @Override - protected void formatChange() { - appendText(getNameFor(fromId)); - appendText(" has abandoned change " + change.getKey().abbreviate() + ":\n"); - appendText("\n"); - formatCoverLetter(); + protected void formatChange() throws EmailException { + appendText(velocifyFile("Abandoned.vm")); } } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/AddReviewerSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/AddReviewerSender.java index be62ba070a..e5437cfac0 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/AddReviewerSender.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/AddReviewerSender.java @@ -32,7 +32,7 @@ public class AddReviewerSender extends NewChangeSender { } @Override - protected void init() { + protected void init() throws EmailException { super.init(); ccExistingReviewers(); diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java index b3ddaeb92b..71712af953 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java @@ -54,7 +54,6 @@ public abstract class ChangeEmail extends OutgoingEmail { private ProjectState projectState; protected ChangeData changeData; - private boolean inFooter; protected ChangeEmail(EmailArguments ea, final Change c, final String mc) { super(ea, mc); @@ -76,31 +75,9 @@ public abstract class ChangeEmail extends OutgoingEmail { } /** Format the message body by calling {@link #appendText(String)}. */ - protected void format() { + protected void format() throws EmailException { formatChange(); - if (getChangeUrl() != null) { - openFooter(); - appendText("To view visit "); - appendText(getChangeUrl()); - appendText("\n"); - } - if (getSettingsUrl() != null) { - openFooter(); - appendText("To unsubscribe, visit "); - appendText(getSettingsUrl()); - appendText("\n"); - } - - if (inFooter) { - appendText("\n"); - } else { - openFooter(); - } - appendText("Gerrit-MessageType: " + messageClass + "\n"); - appendText("Gerrit-Project: " + projectName + "\n"); - appendText("Gerrit-Branch: " + change.getDest().getShortName() + "\n"); - appendText("Gerrit-Owner: " + getNameEmailFor(change.getOwner()) + "\n"); - + appendText(velocifyFile("ChangeFooter.vm")); try { HashSet<Account.Id> reviewers = new HashSet<Account.Id>(); for (PatchSetApproval p : args.db.get().patchSetApprovals().byChange( @@ -121,11 +98,10 @@ public abstract class ChangeEmail extends OutgoingEmail { } /** Format the message body by calling {@link #appendText(String)}. */ - protected abstract void formatChange(); + protected abstract void formatChange() throws EmailException; /** Setup the message headers and envelope (TO, CC, BCC). */ - protected void init() { - super.init(); + protected void init() throws EmailException { if (args.projectCache != null) { projectState = args.projectCache.get(change.getProject()); projectName = @@ -151,6 +127,8 @@ public abstract class ChangeEmail extends OutgoingEmail { } } + super.init(); + if (changeMessage != null && changeMessage.getWrittenOn() != null) { setHeader("Date", new Date(changeMessage.getWrittenOn().getTime())); } @@ -159,27 +137,21 @@ public abstract class ChangeEmail extends OutgoingEmail { setListIdHeader(); setChangeUrlHeader(); setCommitIdHeader(); - - inFooter = false; } - private void setListIdHeader() { + private void setListIdHeader() throws EmailException { // Set a reasonable list id so that filters can be used to sort messages - // - final StringBuilder listid = new StringBuilder(); - listid.append("gerrit-"); - listid.append(projectName.replace('/', '-')); - listid.append("@"); - listid.append(getGerritHost()); - - final String listidStr = listid.toString(); - setHeader("Mailing-List", "list " + listidStr); - setHeader("List-Id", "<" + listidStr.replace('@', '.') + ">"); + setVHeader("Mailing-List", "list $email.listId"); + setVHeader("List-Id", "<$email.listId.replace('@', '.')>"); if (getSettingsUrl() != null) { - setHeader("List-Unsubscribe", "<" + getSettingsUrl() + ">"); + setVHeader("List-Unsubscribe", "<$email.settingsUrl>"); } } + public String getListId() throws EmailException { + return velocify("gerrit-$projectName.replace('/', '-')@$email.gerritHost"); + } + private void setChangeUrlHeader() { final String u = getChangeUrl(); if (u != null) { @@ -195,27 +167,12 @@ public abstract class ChangeEmail extends OutgoingEmail { } } - private void setChangeSubjectHeader() { - final StringBuilder subj = new StringBuilder(); - subj.append("["); - subj.append(change.getDest().getShortName()); - subj.append("] "); - subj.append("Change "); - subj.append(change.getKey().abbreviate()); - subj.append(": ("); - subj.append(projectName); - subj.append(") "); - if (change.getSubject().length() > 60) { - subj.append(change.getSubject().substring(0, 60)); - subj.append("..."); - } else { - subj.append(change.getSubject()); - } - setHeader("Subject", subj.toString()); + private void setChangeSubjectHeader() throws EmailException { + setHeader("Subject", velocifyFile("ChangeSubject.vm")); } /** Get a link to the change; null if the server doesn't know its own address. */ - protected String getChangeUrl() { + public String getChangeUrl() { if (change != null && getGerritUrl() != null) { final StringBuilder r = new StringBuilder(); r.append(getGerritUrl()); @@ -225,25 +182,9 @@ public abstract class ChangeEmail extends OutgoingEmail { return null; } - protected String getChangeMessageThreadId() { - final StringBuilder r = new StringBuilder(); - r.append('<'); - r.append("gerrit"); - r.append('.'); - r.append(change.getCreatedOn().getTime()); - r.append('.'); - r.append(change.getKey().get()); - r.append('@'); - r.append(getGerritHost()); - r.append('>'); - return r.toString(); - } - - private void openFooter() { - if (!inFooter) { - inFooter = true; - appendText("-- \n"); - } + public String getChangeMessageThreadId() throws EmailException { + return velocify("<gerrit.${change.createdOn.time}.$change.key.get()" + + "@$email.gerritHost>"); } /** Format the sender's "cover letter", {@link #getCoverLetter()}. */ @@ -256,7 +197,7 @@ public abstract class ChangeEmail extends OutgoingEmail { } /** Get the text of the "cover letter", from {@link ChangeMessage}. */ - protected String getCoverLetter() { + public String getCoverLetter() { if (changeMessage != null) { final String txt = changeMessage.getMessage(); if (txt != null) { @@ -268,34 +209,41 @@ public abstract class ChangeEmail extends OutgoingEmail { /** Format the change message and the affected file list. */ protected void formatChangeDetail() { + appendText(getChangeDetail()); + } + + /** Create the change message and the affected file list. */ + public String getChangeDetail() { + StringBuilder detail = new StringBuilder(); + if (patchSetInfo != null) { - appendText(patchSetInfo.getMessage().trim()); - appendText("\n"); + detail.append(patchSetInfo.getMessage().trim() + "\n"); } else { - appendText(change.getSubject().trim()); - appendText("\n"); + detail.append(change.getSubject().trim() + "\n"); } if (patchSet != null) { - appendText("---\n"); + detail.append("---\n"); PatchList patchList = getPatchList(); for (PatchListEntry p : patchList.getPatches()) { if (Patch.COMMIT_MSG.equals(p.getNewName())) { continue; } - appendText(p.getChangeType().getCode() + " " + p.getNewName() + "\n"); + detail.append(p.getChangeType().getCode() + " " + p.getNewName() + "\n"); } - appendText(MessageFormat.format("" // + detail.append(MessageFormat.format("" // + "{0,choice,0#0 files|1#1 file|1<{0} files} changed, " // + "{1,choice,0#0 insertions|1#1 insertion|1<{1} insertions}(+), " // + "{2,choice,0#0 deletions|1#1 deletion|1<{2} deletions}(-)" // + "\n", patchList.getPatches().size() - 1, // patchList.getInsertions(), // patchList.getDeletions())); - appendText("\n"); + detail.append("\n"); } + return detail.toString(); } + /** Get the patch list corresponding to this patch set. */ protected PatchList getPatchList() { if (patchSet != null) { @@ -441,4 +389,17 @@ public abstract class ChangeEmail extends OutgoingEmail { || projectState.controlFor(args.identifiedUserFactory.create(to)) .controlFor(change).isVisible(); } + + @Override + protected void setupVelocityContext() { + super.setupVelocityContext(); + velocityContext.put("change", change); + velocityContext.put("changeId", change.getKey()); + velocityContext.put("coverLetter", getCoverLetter()); + velocityContext.put("branch", change.getDest()); + velocityContext.put("fromName", getNameFor(fromId)); + velocityContext.put("projectName", projectName); + velocityContext.put("patchSet", patchSet); + velocityContext.put("patchSetInfo", patchSetInfo); + } } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java index e501a3e690..0425121eb7 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java @@ -61,7 +61,7 @@ public class CommentSender extends ReplyToChangeSender { } @Override - protected void init() { + protected void init() throws EmailException { super.init(); ccAllApprovals(); @@ -70,20 +70,13 @@ public class CommentSender extends ReplyToChangeSender { } @Override - protected void formatChange() { - if (!"".equals(getCoverLetter()) || !inlineComments.isEmpty()) { - appendText("Comments on Patch Set " + patchSet.getPatchSetId() + ":\n"); - appendText("\n"); - formatCoverLetter(); - formatInlineComments(); - if (getChangeUrl() != null) { - appendText("To respond, visit " + getChangeUrl() + "\n"); - appendText("\n"); - } - } + public void formatChange() throws EmailException { + appendText(velocifyFile("Comment.vm")); } - private void formatInlineComments() { + public String getInlineComments() { + StringBuilder cmts = new StringBuilder(); + final Repository repo = getRepository(); try { final PatchList patchList = repo != null ? getPatchList() : null; @@ -96,13 +89,13 @@ public class CommentSender extends ReplyToChangeSender { final short side = c.getSide(); if (!pk.equals(currentFileKey)) { - appendText("....................................................\n"); + cmts.append("....................................................\n"); if (Patch.COMMIT_MSG.equals(pk.get())) { - appendText("Commit Message\n"); + cmts.append("Commit Message\n"); } else { - appendText("File "); - appendText(pk.get()); - appendText("\n"); + cmts.append("File "); + cmts.append(pk.get()); + cmts.append("\n"); } currentFileKey = pk; @@ -118,26 +111,27 @@ public class CommentSender extends ReplyToChangeSender { } } - appendText("Line " + lineNbr); + cmts.append("Line " + lineNbr); if (currentFileData != null) { try { final String lineStr = currentFileData.getLine(side, lineNbr); - appendText(": "); - appendText(lineStr); + cmts.append(": "); + cmts.append(lineStr); } catch (Throwable cce) { // Don't quote the line if we can't safely convert it. } } - appendText("\n"); + cmts.append("\n"); - appendText(c.getMessage().trim()); - appendText("\n\n"); + cmts.append(c.getMessage().trim()); + cmts.append("\n\n"); } } finally { if (repo != null) { repo.close(); } } + return cmts.toString(); } private Repository getRepository() { diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java index ea57cde1e5..18bfe976c7 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java @@ -40,7 +40,7 @@ public class CreateChangeSender extends NewChangeSender { } @Override - protected void init() { + protected void init() throws EmailException { super.init(); bccWatchers(); diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java index 8a7ffd6b70..f2ab9fa4a9 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java @@ -20,6 +20,7 @@ import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.IdentifiedUser.GenericFactory; import com.google.gerrit.server.account.AccountCache; import com.google.gerrit.server.config.CanonicalWebUrl; +import com.google.gerrit.server.config.SitePaths; import com.google.gerrit.server.config.WildProjectName; import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.patch.PatchListCache; @@ -47,6 +48,7 @@ class EmailArguments { final ChangeQueryBuilder.Factory queryBuilder; final Provider<ChangeQueryRewriter> queryRewriter; final Provider<ReviewDb> db; + final SitePaths site; @Inject EmailArguments(GitRepositoryManager server, ProjectCache projectCache, @@ -57,7 +59,8 @@ class EmailArguments { @CanonicalWebUrl @Nullable Provider<String> urlProvider, @WildProjectName Project.NameKey wildProject, ChangeQueryBuilder.Factory queryBuilder, - Provider<ChangeQueryRewriter> queryRewriter, Provider<ReviewDb> db) { + Provider<ChangeQueryRewriter> queryRewriter, Provider<ReviewDb> db, + SitePaths site) { this.server = server; this.projectCache = projectCache; this.accountCache = accountCache; @@ -71,5 +74,6 @@ class EmailArguments { this.queryBuilder = queryBuilder; this.queryRewriter = queryRewriter; this.db = db; + this.site = site; } } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergeFailSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergeFailSender.java index 00750ef306..19fb48b640 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergeFailSender.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergeFailSender.java @@ -26,27 +26,18 @@ public class MergeFailSender extends ReplyToChangeSender { @Inject public MergeFailSender(EmailArguments ea, @Assisted Change c) { - super(ea, c, "comment"); + super(ea, c, "merge-failed"); } @Override - protected void init() { + protected void init() throws EmailException { super.init(); ccExistingReviewers(); } @Override - protected void formatChange() { - appendText("Change " + change.getKey().abbreviate()); - if (patchSetInfo != null && patchSetInfo.getAuthor() != null - && patchSetInfo.getAuthor().getName() != null) { - appendText(" by "); - appendText(patchSetInfo.getAuthor().getName()); - } - appendText(" FAILED to submit to "); - appendText(change.getDest().getShortName()); - appendText(".\n\n"); - formatCoverLetter(); + protected void formatChange() throws EmailException { + appendText(velocifyFile("MergeFail.vm")); } } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java index caf19e472e..40f479075f 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java @@ -51,7 +51,7 @@ public class MergedSender extends ReplyToChangeSender { } @Override - protected void init() { + protected void init() throws EmailException { super.init(); ccAllApprovals(); @@ -61,58 +61,47 @@ public class MergedSender extends ReplyToChangeSender { } @Override - protected void formatChange() { - appendText("Change " + change.getKey().abbreviate()); - if (patchSetInfo != null && patchSetInfo.getAuthor() != null - && patchSetInfo.getAuthor().getName() != null) { - appendText(" by "); - appendText(patchSetInfo.getAuthor().getName()); - } - appendText(" submitted to "); - appendText(dest.getShortName()); - appendText(":\n\n"); - formatChangeDetail(); - formatApprovals(); + protected void formatChange() throws EmailException { + appendText(velocifyFile("Merged.vm")); } - private void formatApprovals() { - if (patchSet != null) { - try { - final Map<Account.Id, Map<ApprovalCategory.Id, PatchSetApproval>> pos = - new HashMap<Account.Id, Map<ApprovalCategory.Id, PatchSetApproval>>(); - - final Map<Account.Id, Map<ApprovalCategory.Id, PatchSetApproval>> neg = - new HashMap<Account.Id, Map<ApprovalCategory.Id, PatchSetApproval>>(); - - for (PatchSetApproval ca : args.db.get().patchSetApprovals() - .byPatchSet(patchSet.getId())) { - if (ca.getValue() > 0) { - insert(pos, ca); - } else if (ca.getValue() < 0) { - insert(neg, ca); - } + public String getApprovals() { + try { + final Map<Account.Id, Map<ApprovalCategory.Id, PatchSetApproval>> pos = + new HashMap<Account.Id, Map<ApprovalCategory.Id, PatchSetApproval>>(); + + final Map<Account.Id, Map<ApprovalCategory.Id, PatchSetApproval>> neg = + new HashMap<Account.Id, Map<ApprovalCategory.Id, PatchSetApproval>>(); + + for (PatchSetApproval ca : args.db.get().patchSetApprovals() + .byPatchSet(patchSet.getId())) { + if (ca.getValue() > 0) { + insert(pos, ca); + } else if (ca.getValue() < 0) { + insert(neg, ca); } - - format("Approvals", pos); - format("Objections", neg); - } catch (OrmException err) { - // Don't list the approvals } + + return format("Approvals", pos) + format("Objections", neg); + } catch (OrmException err) { + // Don't list the approvals } + return ""; } - private void format(final String type, + private String format(final String type, final Map<Account.Id, Map<ApprovalCategory.Id, PatchSetApproval>> list) { + StringBuilder txt = new StringBuilder(); if (list.isEmpty()) { - return; + return ""; } - appendText(type + ":\n"); + txt.append(type + ":\n"); for (final Map.Entry<Account.Id, Map<ApprovalCategory.Id, PatchSetApproval>> ent : list .entrySet()) { final Map<ApprovalCategory.Id, PatchSetApproval> l = ent.getValue(); - appendText(" "); - appendText(getNameFor(ent.getKey())); - appendText(": "); + txt.append(" "); + txt.append(getNameFor(ent.getKey())); + txt.append(": "); boolean first = true; for (ApprovalType at : approvalTypes.getApprovalTypes()) { final PatchSetApproval ca = l.get(at.getCategory().getId()); @@ -123,24 +112,25 @@ public class MergedSender extends ReplyToChangeSender { if (first) { first = false; } else { - appendText("; "); + txt.append("; "); } final ApprovalCategoryValue v = at.getValue(ca); if (v != null) { - appendText(v.getName()); + txt.append(v.getName()); } else { - appendText(at.getCategory().getName()); - appendText("="); + txt.append(at.getCategory().getName()); + txt.append("="); if (ca.getValue() > 0) { - appendText("+"); + txt.append("+"); } - appendText("" + ca.getValue()); + txt.append("" + ca.getValue()); } } - appendText("\n"); + txt.append("\n"); } - appendText("\n"); + txt.append("\n"); + return txt.toString(); } private void insert( diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/NewChangeSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/NewChangeSender.java index dc8c2c2b33..9e78cabf30 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/NewChangeSender.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/NewChangeSender.java @@ -20,6 +20,7 @@ import com.google.gerrit.server.ssh.SshInfo; import com.jcraft.jsch.HostKey; +import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; @@ -46,7 +47,7 @@ public abstract class NewChangeSender extends ChangeEmail { } @Override - protected void init() { + protected void init() throws EmailException { super.init(); setHeader("Message-ID", getChangeMessageThreadId()); @@ -57,74 +58,19 @@ public abstract class NewChangeSender extends ChangeEmail { } @Override - protected void formatChange() { - formatSalutation(); - formatChangeDetail(); - - appendText("\n"); - appendText(" " + getPullUrl() + "\n"); + protected void formatChange() throws EmailException { + appendText(velocifyFile("NewChange.vm")); } - private void formatSalutation() { - final String changeUrl = getChangeUrl(); - + public List<String> getReviewerNames() { if (reviewers.isEmpty()) { - formatDest(); - if (changeUrl != null) { - appendText("\n"); - appendText(" " + changeUrl + "\n"); - appendText("\n"); - } - appendText("\n"); - - } else { - appendText("Hello"); - for (final Iterator<Account.Id> i = reviewers.iterator(); i.hasNext();) { - appendText(" "); - appendText(getNameFor(i.next())); - appendText(","); - } - appendText("\n"); - appendText("\n"); - - appendText("I'd like you to do a code review."); - if (changeUrl != null) { - appendText(" Please visit\n"); - appendText("\n"); - appendText(" " + changeUrl + "\n"); - appendText("\n"); - appendText("to review the following change:\n"); - } - appendText("\n"); - - formatDest(); - appendText("\n"); + return null; } - } - - private void formatDest() { - appendText("Change " + change.getKey().abbreviate()); - appendText(" for "); - appendText(change.getDest().getShortName()); - appendText(" in "); - appendText(projectName); - appendText(":\n"); - } - - private String getPullUrl() { - final String host = getSshHost(); - if (host == null) { - return ""; + List<String> names = new ArrayList<String>(); + for (Account.Id id : reviewers) { + names.add(getNameFor(id)); } - - final StringBuilder r = new StringBuilder(); - r.append("git pull ssh://"); - r.append(host); - r.append("/"); - r.append(projectName); - r.append(" "); - r.append(patchSet.getRefName()); - return r.toString(); + return names; } public String getSshHost() { diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java index 2136c658c4..02e500082a 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java @@ -19,10 +19,15 @@ import com.google.gerrit.reviewdb.UserIdentity; import com.google.gerrit.server.account.AccountState; import com.google.gerrit.server.mail.EmailHeader.AddressList; +import org.apache.commons.lang.StringUtils; +import org.apache.velocity.VelocityContext; +import org.apache.velocity.app.Velocity; +import org.apache.velocity.exception.ResourceNotFoundException; import org.eclipse.jgit.util.SystemReader; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.StringWriter; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; @@ -47,6 +52,7 @@ public abstract class OutgoingEmail { private final List<Address> smtpRcptTo = new ArrayList<Address>(); private Address smtpFromAddress; private StringBuilder body; + protected VelocityContext velocityContext; protected final EmailArguments args; protected Account.Id fromId; @@ -113,10 +119,12 @@ public abstract class OutgoingEmail { } /** Format the message body by calling {@link #appendText(String)}. */ - protected abstract void format(); + protected abstract void format() throws EmailException; /** Setup the message headers and envelope (TO, CC, BCC). */ - protected void init() { + protected void init() throws EmailException { + setupVelocityContext(); + smtpFromAddress = args.fromAddressGenerator.from(fromId); setHeader("Date", new Date()); headers.put("From", new EmailHeader.AddressList(smtpFromAddress)); @@ -140,25 +148,31 @@ public abstract class OutgoingEmail { body = new StringBuilder(); if (fromId != null && args.fromAddressGenerator.isGenericAddress(fromId)) { - final Account account = args.accountCache.get(fromId).getAccount(); - final String name = account.getFullName(); - final String email = account.getPreferredEmail(); - - if ((name != null && !name.isEmpty()) - || (email != null && !email.isEmpty())) { - body.append("From"); - if (name != null && !name.isEmpty()) { - body.append(" ").append(name); - } - if (email != null && !email.isEmpty()) { - body.append(" <").append(email).append(">"); - } - body.append(":\n\n"); + appendText(getFromLine()); + } + } + + protected String getFromLine() { + final Account account = args.accountCache.get(fromId).getAccount(); + final String name = account.getFullName(); + final String email = account.getPreferredEmail(); + StringBuilder f = new StringBuilder(); + + if ((name != null && !name.isEmpty()) + || (email != null && !email.isEmpty())) { + f.append("From"); + if (name != null && !name.isEmpty()) { + f.append(" ").append(name); + } + if (email != null && !email.isEmpty()) { + f.append(" <").append(email).append(">"); } + f.append(":\n\n"); } + return f.toString(); } - protected String getGerritHost() { + public String getGerritHost() { if (getGerritUrl() != null) { try { return new URL(getGerritUrl()).getHost(); @@ -184,10 +198,16 @@ public abstract class OutgoingEmail { return null; } - protected String getGerritUrl() { + public String getGerritUrl() { return args.urlProvider.get(); } + /** Set a header in the outgoing message using a template. */ + protected void setVHeader(final String name, final String value) throws + EmailException { + setHeader(name, velocify(value)); + } + /** Set a header in the outgoing message. */ protected void setHeader(final String name, final String value) { headers.put(name, new EmailHeader.String(value)); @@ -221,7 +241,7 @@ public abstract class OutgoingEmail { return name; } - protected String getNameEmailFor(Account.Id accountId) { + public String getNameEmailFor(Account.Id accountId) { AccountState who = args.accountCache.get(accountId); String name = who.getAccount().getFullName(); String email = who.getAccount().getPreferredEmail(); @@ -310,9 +330,40 @@ public abstract class OutgoingEmail { private Address toAddress(final Account.Id id) { final Account a = args.accountCache.get(id).getAccount(); final String e = a.getPreferredEmail(); - if (e == null) { + if (!a.isActive() || e == null) { return null; } return new Address(a.getFullName(), e); } + + protected void setupVelocityContext() { + velocityContext = new VelocityContext(); + + velocityContext.put("email", this); + velocityContext.put("messageClass", messageClass); + velocityContext.put("StringUtils", StringUtils.class); + } + + protected String velocify(String template) throws EmailException { + try { + StringWriter w = new StringWriter(); + Velocity.evaluate(velocityContext, w, "OutgoingEmail", template); + return w.toString(); + } catch(Exception e) { + throw new EmailException("Velocity template " + template, e); + } + } + + protected String velocifyFile(String name) throws EmailException { + if (!Velocity.resourceExists(name)) { + name = "com/google/gerrit/server/mail/" + name; + } + try { + StringWriter w = new StringWriter(); + Velocity.mergeTemplate(name, "UTF-8", velocityContext, w); + return w.toString(); + } catch(Exception e) { + throw new EmailException("Velocity template " + name + ".\n", e); + } + } } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RegisterNewEmailSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RegisterNewEmailSender.java index 9b201fd4b5..2c779994b6 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RegisterNewEmailSender.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RegisterNewEmailSender.java @@ -40,7 +40,7 @@ public class RegisterNewEmailSender extends OutgoingEmail { } @Override - protected void init() { + protected void init() throws EmailException { super.init(); setHeader("Subject", "[Gerrit Code Review] Email Verification"); add(RecipientType.TO, new Address(addr)); @@ -52,39 +52,8 @@ public class RegisterNewEmailSender extends OutgoingEmail { } @Override - protected void format() { - final StringBuilder url = new StringBuilder(); - url.append(getGerritUrl()); - url.append("#VE,"); - url.append(getEmailRegistrationToken()); - - appendText("Welcome to Gerrit Code Review at "); - appendText(getGerritHost()); - appendText(".\n"); - - appendText("\n"); - appendText("To add a verified email address to your user account, please\n"); - appendText("click on the following link:\n"); - appendText("\n"); - appendText(url.toString()); - appendText("\n"); - - appendText("\n"); - appendText("If you have received this mail in error," - + " you do not need to take any\n"); - appendText("action to cancel the account." - + " The account will not be activated, and\n"); - appendText("you will not receive any further emails.\n"); - - appendText("\n"); - appendText("If clicking the link above does not work," - + " copy and paste the URL in a\n"); - appendText("new browser window instead.\n"); - - appendText("\n"); - appendText("This is a send-only email address." - + " Replies to this message will not\n"); - appendText("be read or answered.\n"); + protected void format() throws EmailException { + appendText(velocifyFile("RegisterNewEmail.vm")); } public String getEmailRegistrationToken() { diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplacePatchSetSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplacePatchSetSender.java index 841aa355e0..0c8ca9ae2b 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplacePatchSetSender.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplacePatchSetSender.java @@ -22,6 +22,7 @@ import com.google.inject.assistedinject.Assisted; import com.jcraft.jsch.HostKey; +import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; @@ -53,7 +54,7 @@ public class ReplacePatchSetSender extends ReplyToChangeSender { } @Override - protected void init() { + protected void init() throws EmailException { super.init(); if (fromId != null) { @@ -67,77 +68,19 @@ public class ReplacePatchSetSender extends ReplyToChangeSender { } @Override - protected void formatChange() { - formatSalutation(); - formatChangeDetail(); - - appendText("\n"); - appendText(" " + getPullUrl() + "\n"); + protected void formatChange() throws EmailException { + appendText(velocifyFile("ReplacePatchSet.vm")); } - private void formatSalutation() { - final String changeUrl = getChangeUrl(); - + public List<String> getReviewerNames() { if (reviewers.isEmpty()) { - formatDest(); - if (changeUrl != null) { - appendText("\n"); - appendText(" " + changeUrl + "\n"); - appendText("\n"); - } - appendText("\n"); - - } else { - appendText("Hello"); - for (final Iterator<Account.Id> i = reviewers.iterator(); i.hasNext();) { - appendText(" "); - appendText(getNameFor(i.next())); - appendText(","); - } - appendText("\n"); - appendText("\n"); - - appendText("I'd like you to reexamine change " - + change.getKey().abbreviate() + "."); - if (changeUrl != null) { - appendText(" Please visit\n"); - appendText("\n"); - appendText(" " + changeUrl + "\n"); - appendText("\n"); - appendText("to look at patch set " + patchSet.getPatchSetId()); - appendText(":\n"); - } - appendText("\n"); - - formatDest(); - appendText("\n"); + return null; } - } - - private void formatDest() { - appendText("Change " + change.getKey().abbreviate()); - appendText(" (patch set " + patchSet.getPatchSetId() + ")"); - appendText(" for "); - appendText(change.getDest().getShortName()); - appendText(" in "); - appendText(projectName); - appendText(":\n"); - } - - private String getPullUrl() { - final String host = getSshHost(); - if (host == null) { - return ""; + List<String> names = new ArrayList<String>(); + for (Account.Id id : reviewers) { + names.add(getNameFor(id)); } - - final StringBuilder r = new StringBuilder(); - r.append("git pull ssh://"); - r.append(host); - r.append("/"); - r.append(projectName); - r.append(" "); - r.append(patchSet.getRefName()); - return r.toString(); + return names; } public String getSshHost() { diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplyToChangeSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplyToChangeSender.java index 05d2753dee..4c3ed76465 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplyToChangeSender.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplyToChangeSender.java @@ -23,7 +23,7 @@ public abstract class ReplyToChangeSender extends ChangeEmail { } @Override - protected void init() { + protected void init() throws EmailException { super.init(); final String threadId = getChangeMessageThreadId(); diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/CharText.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/CharText.java index fd6208633b..2a0cd2f5c2 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/CharText.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/CharText.java @@ -16,11 +16,11 @@ package com.google.gerrit.server.patch; import org.eclipse.jgit.diff.Sequence; -class CharText implements Sequence { +class CharText extends Sequence { private final String content; CharText(Text text, int s, int e) { - content = text.getLines(s, e, false /* keep LF */); + content = text.getString(s, e, false /* keep LF */); } char charAt(int idx) { @@ -41,11 +41,6 @@ class CharText implements Sequence { } @Override - public boolean equals(int a, Sequence other, int b) { - return content.charAt(a) == ((CharText) other).content.charAt(b); - } - - @Override public int size() { return content.length(); } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/CharTextComparator.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/CharTextComparator.java new file mode 100644 index 0000000000..811931380e --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/CharTextComparator.java @@ -0,0 +1,29 @@ +// Copyright (C) 2010 The Android Open Source Project +// +// 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.google.gerrit.server.patch; + +import org.eclipse.jgit.diff.SequenceComparator; + +class CharTextComparator extends SequenceComparator<CharText> { + @Override + public boolean equals(CharText a, int ai, CharText b, int bi) { + return a.charAt(ai) == b.charAt(bi); + } + + @Override + public int hash(CharText seq, int ptr) { + return seq.charAt(ptr); + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchFile.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchFile.java index 22be063ebb..4feccd05a5 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchFile.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchFile.java @@ -23,7 +23,7 @@ import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.MissingObjectException; 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.Repository; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevTree; @@ -49,29 +49,34 @@ public class PatchFile { this.repo = repo; this.entry = patchList.get(fileName); - final RevWalk rw = new RevWalk(repo); - final RevCommit bCommit = rw.parseCommit(patchList.getNewId()); + final ObjectReader reader = repo.newObjectReader(); + try { + final RevWalk rw = new RevWalk(reader); + final RevCommit bCommit = rw.parseCommit(patchList.getNewId()); - if (Patch.COMMIT_MSG.equals(fileName)) { - if (patchList.isAgainstParent()) { - a = Text.EMPTY; - } else { - a = Text.forCommit(repo, patchList.getOldId()); - } - b = Text.forCommit(repo, bCommit); + if (Patch.COMMIT_MSG.equals(fileName)) { + if (patchList.isAgainstParent()) { + a = Text.EMPTY; + } else { + a = Text.forCommit(repo, reader, patchList.getOldId()); + } + b = Text.forCommit(repo, reader, bCommit); - aTree = null; - bTree = null; + aTree = null; + bTree = null; - } else { - if (patchList.getOldId() != null) { - aTree = rw.parseTree(patchList.getOldId()); } else { - final RevCommit p = bCommit.getParent(0); - rw.parseHeaders(p); - aTree = p.getTree(); + if (patchList.getOldId() != null) { + aTree = rw.parseTree(patchList.getOldId()); + } else { + final RevCommit p = bCommit.getParent(0); + rw.parseHeaders(p); + aTree = p.getTree(); + } + bTree = bCommit.getTree(); } - bTree = bCommit.getTree(); + } finally { + reader.release(); } } @@ -93,13 +98,13 @@ public class PatchFile { if (a == null) { a = load(aTree, entry.getOldName()); } - return a.getLine(line - 1); + return a.getString(line - 1); case 1: if (b == null) { b = load(bTree, entry.getNewName()); } - return b.getLine(line - 1); + return b.getString(line - 1); default: throw new NoSuchEntityException(); @@ -119,11 +124,6 @@ public class PatchFile { if (tw.getFileMode(0).getObjectType() != Constants.OBJ_BLOB) { return Text.EMPTY; } - final ObjectId id = tw.getObjectId(0); - final ObjectLoader ldr = repo.openObject(id); - if (ldr == null) { - throw new MissingObjectException(id, Constants.TYPE_BLOB); - } - return new Text(ldr.getCachedBytes()); + return new Text(repo.open(tw.getObjectId(0), Constants.OBJ_BLOB)); } } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java index bb96237226..e5c3b72e35 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java @@ -84,17 +84,16 @@ import org.eclipse.jgit.diff.Edit; import org.eclipse.jgit.diff.EditList; import org.eclipse.jgit.diff.MyersDiff; import org.eclipse.jgit.diff.RawText; -import org.eclipse.jgit.diff.RawTextIgnoreAllWhitespace; -import org.eclipse.jgit.diff.RawTextIgnoreTrailingWhitespace; -import org.eclipse.jgit.diff.RawTextIgnoreWhitespaceChange; -import org.eclipse.jgit.diff.RenameDetector; +import org.eclipse.jgit.diff.RawTextComparator; import org.eclipse.jgit.diff.ReplaceEdit; +import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectInserter; import org.eclipse.jgit.lib.ObjectLoader; -import org.eclipse.jgit.lib.ObjectWriter; +import org.eclipse.jgit.lib.ObjectReader; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.patch.FileHeader; import org.eclipse.jgit.patch.FileHeader.PatchType; @@ -187,81 +186,85 @@ public class PatchListCacheImpl implements PatchListCache { final Repository repo) throws IOException { // TODO(jeffschu) correctly handle merge commits - RawText.Factory rawTextFactory; + RawTextComparator cmp; switch (key.getWhitespace()) { case IGNORE_ALL_SPACE: - rawTextFactory = RawTextIgnoreAllWhitespace.FACTORY; + cmp = RawTextComparator.WS_IGNORE_ALL; break; case IGNORE_SPACE_AT_EOL: - rawTextFactory = RawTextIgnoreTrailingWhitespace.FACTORY; + cmp = RawTextComparator.WS_IGNORE_TRAILING; break; case IGNORE_SPACE_CHANGE: - rawTextFactory = RawTextIgnoreWhitespaceChange.FACTORY; + cmp = RawTextComparator.WS_IGNORE_CHANGE; break; case IGNORE_NONE: default: - rawTextFactory = RawText.FACTORY; + cmp = RawTextComparator.DEFAULT; break; } - final RevWalk rw = new RevWalk(repo); - final RevCommit b = rw.parseCommit(key.getNewId()); - final RevObject a = aFor(key, repo, rw, b); + final ObjectReader reader = repo.newObjectReader(); + try { + final RevWalk rw = new RevWalk(reader); + final RevCommit b = rw.parseCommit(key.getNewId()); + final RevObject a = aFor(key, repo, rw, b); - if (a == null) { - // This is a merge commit, compared to its ancestor. - // - final PatchListEntry[] entries = new PatchListEntry[1]; - entries[0] = newCommitMessage(rawTextFactory, repo, null, b); - return new PatchList(a, b, computeIntraline, true, entries); - } + if (a == null) { + // This is a merge commit, compared to its ancestor. + // + final PatchListEntry[] entries = new PatchListEntry[1]; + entries[0] = newCommitMessage(cmp, repo, reader, null, b); + return new PatchList(a, b, computeIntraline, true, entries); + } - final boolean againstParent = - b.getParentCount() > 0 && b.getParent(0) == a; - - RevCommit aCommit; - RevTree aTree; - if (a instanceof RevCommit) { - aCommit = (RevCommit) a; - aTree = aCommit.getTree(); - } else if (a instanceof RevTree) { - aCommit = null; - aTree = (RevTree) a; - } else { - throw new IOException("Unexpected type: " + a.getClass()); - } + final boolean againstParent = + b.getParentCount() > 0 && b.getParent(0) == a; + + RevCommit aCommit; + RevTree aTree; + if (a instanceof RevCommit) { + aCommit = (RevCommit) a; + aTree = aCommit.getTree(); + } else if (a instanceof RevTree) { + aCommit = null; + aTree = (RevTree) a; + } else { + throw new IOException("Unexpected type: " + a.getClass()); + } - RevTree bTree = b.getTree(); - - final TreeWalk walk = new TreeWalk(repo); - walk.reset(); - walk.setRecursive(true); - walk.addTree(aTree); - walk.addTree(bTree); - walk.setFilter(TreeFilter.ANY_DIFF); - - DiffFormatter df = new DiffFormatter(DisabledOutputStream.INSTANCE); - df.setRepository(repo); - df.setRawTextFactory(rawTextFactory); - - RenameDetector rd = new RenameDetector(repo); - rd.addAll(DiffEntry.scan(walk)); - List<DiffEntry> diffEntries = rd.compute(); - - final int cnt = diffEntries.size(); - final PatchListEntry[] entries = new PatchListEntry[1 + cnt]; - entries[0] = newCommitMessage(rawTextFactory, repo, // - againstParent ? null : aCommit, b); - for (int i = 0; i < cnt; i++) { - FileHeader fh = df.createFileHeader(diffEntries.get(i)); - entries[1 + i] = newEntry(repo, aTree, bTree, fh); + RevTree bTree = b.getTree(); + + final TreeWalk walk = new TreeWalk(reader); + walk.reset(); + walk.setRecursive(true); + walk.addTree(aTree); + walk.addTree(bTree); + walk.setFilter(TreeFilter.ANY_DIFF); + + DiffFormatter df = new DiffFormatter(DisabledOutputStream.INSTANCE); + df.setRepository(repo); + df.setDiffComparator(cmp); + df.setDetectRenames(true); + List<DiffEntry> diffEntries = df.scan(aTree, bTree); + + final int cnt = diffEntries.size(); + final PatchListEntry[] entries = new PatchListEntry[1 + cnt]; + entries[0] = newCommitMessage(cmp, repo, reader, // + againstParent ? null : aCommit, b); + for (int i = 0; i < cnt; i++) { + FileHeader fh = df.toFileHeader(diffEntries.get(i)); + entries[1 + i] = newEntry(reader, aTree, bTree, fh); + } + return new PatchList(a, b, computeIntraline, againstParent, entries); + } finally { + reader.release(); } - return new PatchList(a, b, computeIntraline, againstParent, entries); } private PatchListEntry newCommitMessage( - final RawText.Factory rawTextFactory, final Repository repo, - final RevCommit aCommit, final RevCommit bCommit) throws IOException { + final RawTextComparator cmp, final Repository db, + final ObjectReader reader, final RevCommit aCommit, + final RevCommit bCommit) throws IOException { StringBuilder hdr = new StringBuilder(); hdr.append("diff --git"); @@ -280,18 +283,18 @@ public class PatchListCacheImpl implements PatchListCache { } hdr.append("+++ b/" + Patch.COMMIT_MSG + "\n"); - Text aText = aCommit != null ? Text.forCommit(repo, aCommit) : Text.EMPTY; - Text bText = Text.forCommit(repo, bCommit); + Text aText = aCommit != null ? Text.forCommit(db, reader, aCommit) : Text.EMPTY; + Text bText = Text.forCommit(db, reader, bCommit); byte[] rawHdr = hdr.toString().getBytes("UTF-8"); - RawText aRawText = rawTextFactory.create(aText.getContent()); - RawText bRawText = rawTextFactory.create(bText.getContent()); - EditList edits = new MyersDiff(aRawText, bRawText).getEdits(); + RawText aRawText = new RawText(aText.getContent()); + RawText bRawText = new RawText(bText.getContent()); + EditList edits = MyersDiff.INSTANCE.diff(cmp, aRawText, bRawText); FileHeader fh = new FileHeader(rawHdr, edits, PatchType.UNIFIED); - return newEntry(repo, aText, bText, edits, null, null, fh); + return newEntry(reader, aText, bText, edits, null, null, fh); } - private PatchListEntry newEntry(Repository repo, RevTree aTree, + private PatchListEntry newEntry(ObjectReader reader, RevTree aTree, RevTree bTree, FileHeader fileHeader) throws IOException { final FileMode oldMode = fileHeader.getOldMode(); final FileMode newMode = fileHeader.getNewMode(); @@ -320,10 +323,10 @@ public class PatchListCacheImpl implements PatchListCache { return new PatchListEntry(fileHeader, edits); } - return newEntry(repo, null, null, edits, aTree, bTree, fileHeader); + return newEntry(reader, null, null, edits, aTree, bTree, fileHeader); } - private PatchListEntry newEntry(Repository repo, Text aContent, + private PatchListEntry newEntry(ObjectReader reader, Text aContent, Text bContent, List<Edit> edits, RevTree aTree, RevTree bTree, FileHeader fileHeader) throws IOException { for (int i = 0; i < edits.size(); i++) { @@ -332,8 +335,8 @@ public class PatchListCacheImpl implements PatchListCache { if (e.getType() == Edit.Type.REPLACE) { if (aContent == null) { edits = new ArrayList<Edit>(edits); - aContent = read(repo, fileHeader.getOldName(), aTree); - bContent = read(repo, fileHeader.getNewName(), bTree); + aContent = read(reader, fileHeader.getOldPath(), aTree); + bContent = read(reader, fileHeader.getNewPath(), bTree); combineLineEdits(edits, aContent, bContent); i = -1; // restart the entire scan after combining lines. continue; @@ -341,8 +344,9 @@ public class PatchListCacheImpl implements PatchListCache { CharText a = new CharText(aContent, e.getBeginA(), e.getEndA()); CharText b = new CharText(bContent, e.getBeginB(), e.getEndB()); + CharTextComparator cmp = new CharTextComparator(); - List<Edit> wordEdits = new MyersDiff(a, b).getEdits(); + List<Edit> wordEdits = MyersDiff.INSTANCE.diff(cmp, a, b); // Combine edits that are really close together. If they are // just a few characters apart we tend to get better results @@ -407,11 +411,11 @@ public class PatchListCacheImpl implements PatchListCache { // such that the edges of each text is identical. Fix by // by dropping out that incorrectly replaced region. // - while (ab < ae && bb < be && a.equals(ab, b, bb)) { + while (ab < ae && bb < be && cmp.equals(a, ab, b, bb)) { ab++; bb++; } - while (ab < ae && bb < be && a.equals(ae - 1, b, be - 1)) { + while (ab < ae && bb < be && cmp.equals(a, ae - 1, b, be - 1)) { ae--; be--; } @@ -429,7 +433,7 @@ public class PatchListCacheImpl implements PatchListCache { int nb = lf + 1; int p = 0; while (p < ae - ab) { - if (a.equals(ab + p, a, ab + p)) + if (cmp.equals(a, ab + p, a, ab + p)) p++; else break; @@ -443,12 +447,12 @@ public class PatchListCacheImpl implements PatchListCache { } if (aShift) { while (0 < ab && ab < ae && a.charAt(ab - 1) != '\n' - && a.equals(ab - 1, a, ae - 1)) { + && cmp.equals(a, ab - 1, a, ae - 1)) { ab--; ae--; } if (!a.isLineStart(ab) || !a.contains(ab, ae, '\n')) { - while (ab < ae && ae < a.size() && a.equals(ab, a, ae)) { + while (ab < ae && ae < a.size() && cmp.equals(a, ab, a, ae)) { ab++; ae++; if (a.charAt(ae - 1) == '\n') { @@ -465,7 +469,7 @@ public class PatchListCacheImpl implements PatchListCache { int nb = lf + 1; int p = 0; while (p < be - bb) { - if (b.equals(bb + p, b, bb + p)) + if (cmp.equals(b, bb + p, b, bb + p)) p++; else break; @@ -479,12 +483,12 @@ public class PatchListCacheImpl implements PatchListCache { } if (bShift) { while (0 < bb && bb < be && b.charAt(bb - 1) != '\n' - && b.equals(bb - 1, b, be - 1)) { + && cmp.equals(b, bb - 1, b, be - 1)) { bb--; be--; } if (!b.isLineStart(bb) || !b.contains(bb, be, '\n')) { - while (bb < be && be < b.size() && b.equals(bb, b, be)) { + while (bb < be && be < b.size() && cmp.equals(b, bb, b, be)) { bb++; be++; if (b.charAt(be - 1) == '\n') { @@ -551,7 +555,7 @@ public class PatchListCacheImpl implements PatchListCache { private static boolean isBlankLineGap(Text a, int b, int e) { for (; b < e; b++) { - if (!BLANK_LINE_RE.matcher(a.getLine(b)).matches()) { + if (!BLANK_LINE_RE.matcher(a.getString(b)).matches()) { return false; } } @@ -559,7 +563,7 @@ public class PatchListCacheImpl implements PatchListCache { } private static boolean isControlBlockStart(Text a, int idx) { - final String l = a.getLine(idx); + final String l = a.getString(idx); return CONTROL_BLOCK_START_RE.matcher(l).find(); } @@ -590,17 +594,19 @@ public class PatchListCacheImpl implements PatchListCache { return b < e; } - private static Text read(Repository repo, String path, RevTree tree) + private static Text read(ObjectReader reader, String path, RevTree tree) throws IOException { - TreeWalk tw = TreeWalk.forPath(repo, path, tree); + TreeWalk tw = TreeWalk.forPath(reader, path, tree); if (tw == null || tw.getFileMode(0).getObjectType() != Constants.OBJ_BLOB) { return Text.EMPTY; } - ObjectLoader ldr = repo.openObject(tw.getObjectId(0)); - if (ldr == null) { + ObjectLoader ldr; + try { + ldr = reader.open(tw.getObjectId(0), Constants.OBJ_BLOB); + } catch (MissingObjectException notFound) { return Text.EMPTY; } - return new Text(ldr.getCachedBytes()); + return new Text(ldr); } private static RevObject aFor(final PatchListKey key, @@ -625,7 +631,14 @@ public class PatchListCacheImpl implements PatchListCache { } private static ObjectId emptyTree(final Repository repo) throws IOException { - return new ObjectWriter(repo).writeCanonicalTree(new byte[0]); + ObjectInserter oi = repo.newObjectInserter(); + try { + ObjectId id = oi.insert(Constants.OBJ_TREE, new byte[] {}); + oi.flush(); + return id; + } finally { + oi.release(); + } } } } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListEntry.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListEntry.java index fd7da082c5..fac8ec4350 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListEntry.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListEntry.java @@ -69,19 +69,19 @@ public class PatchListEntry { switch (changeType) { case DELETED: oldName = null; - newName = hdr.getOldName(); + newName = hdr.getOldPath(); break; case ADDED: case MODIFIED: oldName = null; - newName = hdr.getNewName(); + newName = hdr.getNewPath(); break; case COPIED: case RENAMED: - oldName = hdr.getOldName(); - newName = hdr.getNewName(); + oldName = hdr.getOldPath(); + newName = hdr.getNewPath(); break; default: @@ -286,21 +286,17 @@ public class PatchListEntry { private static PatchType toPatchType(final FileHeader hdr) { PatchType pt; - if (hdr instanceof CombinedFileHeader) { - pt = Patch.PatchType.N_WAY; - } else { - switch (hdr.getPatchType()) { - case UNIFIED: - pt = Patch.PatchType.UNIFIED; - break; - case GIT_BINARY: - case BINARY: - pt = Patch.PatchType.BINARY; - break; - default: - throw new IllegalArgumentException("Unsupported type " - + hdr.getPatchType()); - } + switch (hdr.getPatchType()) { + case UNIFIED: + pt = Patch.PatchType.UNIFIED; + break; + case GIT_BINARY: + case BINARY: + pt = Patch.PatchType.BINARY; + break; + default: + throw new IllegalArgumentException("Unsupported type " + + hdr.getPatchType()); } if (pt != PatchType.BINARY) { diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java index 961717297b..840e76f971 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java @@ -79,9 +79,13 @@ public class PatchSetInfoFactory { final String projectName = projectKey.get(); repo = repoManager.openRepository(projectName); final RevWalk rw = new RevWalk(repo); - final RevCommit src = - rw.parseCommit(ObjectId.fromString(patchSet.getRevision().get())); - return get(src, patchSetId); + try { + final RevCommit src = + rw.parseCommit(ObjectId.fromString(patchSet.getRevision().get())); + return get(src, patchSetId); + } finally { + rw.release(); + } } catch (OrmException e) { throw new PatchSetInfoNotAvailableException(e); } catch (IOException e) { diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/Text.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/Text.java index 860e4b9512..97f53fc09a 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/Text.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/Text.java @@ -15,11 +15,16 @@ package com.google.gerrit.server.patch; import org.eclipse.jgit.diff.RawText; +import org.eclipse.jgit.errors.LargeObjectException; +import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.lib.AnyObjectId; +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.RevWalk; +import org.eclipse.jgit.storage.pack.PackConfig; import org.eclipse.jgit.util.RawParseUtils; import org.mozilla.universalchardet.UniversalDetector; import org.slf4j.Logger; @@ -34,13 +39,14 @@ import java.text.SimpleDateFormat; public class Text extends RawText { private static final Logger log = LoggerFactory.getLogger(Text.class); private static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1"); + private static final int bigFileThreshold = PackConfig.DEFAULT_BIG_FILE_THRESHOLD; public static final byte[] NO_BYTES = {}; public static final Text EMPTY = new Text(NO_BYTES); - public static Text forCommit(Repository db, AnyObjectId commitId) - throws IOException { - RevWalk rw = new RevWalk(db); + public static Text forCommit(Repository db, ObjectReader reader, + AnyObjectId commitId) throws IOException { + RevWalk rw = new RevWalk(reader); RevCommit c; if (commitId instanceof RevCommit) { c = (RevCommit) commitId; @@ -56,7 +62,7 @@ public class Text extends RawText { RevCommit p = c.getParent(0); rw.parseBody(p); b.append("Parent: "); - b.append(p.abbreviate(db, 8).name()); + b.append(reader.abbreviate(p, 8).name()); b.append(" ("); b.append(p.getShortMessage()); b.append(")\n"); @@ -67,7 +73,7 @@ public class Text extends RawText { RevCommit p = c.getParent(i); rw.parseBody(p); b.append(i == 0 ? "Merge Of: " : " "); - b.append(p.abbreviate(db, 8).name()); + b.append(reader.abbreviate(p, 8).name()); b.append(" ("); b.append(p.getShortMessage()); b.append(")\n"); @@ -103,8 +109,9 @@ public class Text extends RawText { } } - public static String asString(byte[] content, String encoding) { - return new String(content, charset(content, encoding)); + public static byte[] asByteArray(ObjectLoader ldr) + throws MissingObjectException, LargeObjectException, IOException { + return ldr.getCachedBytes(bigFileThreshold); } private static Charset charset(byte[] content, String encoding) { @@ -136,39 +143,20 @@ public class Text extends RawText { super(r); } - public byte[] getContent() { - return content; - } - - public String getLine(final int i) { - return getLines(i, i + 1, true); + public Text(ObjectLoader ldr) throws MissingObjectException, + LargeObjectException, IOException { + this(asByteArray(ldr)); } - public String getLines(final int begin, final int end, boolean dropLF) { - if (begin == end) { - return ""; - } - - final int s = getLineStart(begin); - int e = getLineEnd(end - 1); - if (dropLF && content[e - 1] == '\n') { - e--; - } - return decode(s, e); + public byte[] getContent() { + return content; } - private String decode(final int s, int e) { + @Override + protected String decode(final int s, int e) { if (charset == null) { charset = charset(content, null); } return RawParseUtils.decode(charset, content, s, e); } - - private int getLineStart(final int i) { - return lines.get(i + 1); - } - - private int getLineEnd(final int i) { - return lines.get(i + 2); - } } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/AccessControlModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/AccessControlModule.java new file mode 100644 index 0000000000..1e2e7f4382 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/AccessControlModule.java @@ -0,0 +1,54 @@ +// Copyright (C) 2010 The Android Open Source Project +// +// 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.google.gerrit.server.project; + +import static com.google.inject.Scopes.SINGLETON; + +import com.google.gerrit.reviewdb.AccountGroup; +import com.google.gerrit.server.config.FactoryModule; +import com.google.gerrit.server.config.GitReceivePackGroups; +import com.google.gerrit.server.config.GitReceivePackGroupsProvider; +import com.google.gerrit.server.config.GitUploadPackGroups; +import com.google.gerrit.server.config.GitUploadPackGroupsProvider; +import com.google.gerrit.server.config.ProjectCreatorGroups; +import com.google.gerrit.server.config.ProjectCreatorGroupsProvider; +import com.google.gerrit.server.config.ProjectOwnerGroups; +import com.google.gerrit.server.config.ProjectOwnerGroupsProvider; +import com.google.inject.TypeLiteral; + +import java.util.Set; + +public class AccessControlModule extends FactoryModule { + @Override + protected void configure() { + bind(new TypeLiteral<Set<AccountGroup.Id>>() {}) // + .annotatedWith(ProjectCreatorGroups.class) // + .toProvider(ProjectCreatorGroupsProvider.class).in(SINGLETON); + + bind(new TypeLiteral<Set<AccountGroup.Id>>() {}) // + .annotatedWith(ProjectOwnerGroups.class) // + .toProvider(ProjectOwnerGroupsProvider.class).in(SINGLETON); + + bind(new TypeLiteral<Set<AccountGroup.Id>>() {}) // + .annotatedWith(GitUploadPackGroups.class) // + .toProvider(GitUploadPackGroupsProvider.class).in(SINGLETON); + + bind(new TypeLiteral<Set<AccountGroup.Id>>() {}) // + .annotatedWith(GitReceivePackGroups.class) // + .toProvider(GitReceivePackGroupsProvider.class).in(SINGLETON); + + factory(ProjectControl.AssistedFactory.class); + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java index ff788dd089..25778a6fb8 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java @@ -14,6 +14,7 @@ package com.google.gerrit.server.project; +import static com.google.gerrit.common.CollectionsUtil.*; import com.google.gerrit.reviewdb.AccountGroup; import com.google.gerrit.reviewdb.ApprovalCategory; import com.google.gerrit.reviewdb.Branch; @@ -22,8 +23,11 @@ import com.google.gerrit.reviewdb.Project; import com.google.gerrit.reviewdb.RefRight; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.ReplicationUser; +import com.google.gerrit.server.config.GitReceivePackGroups; +import com.google.gerrit.server.config.GitUploadPackGroups; import com.google.inject.Inject; import com.google.inject.Provider; +import com.google.inject.assistedinject.Assisted; import java.util.HashSet; import java.util.Set; @@ -93,10 +97,25 @@ public class ProjectControl { } } + interface AssistedFactory { + ProjectControl create(CurrentUser who, ProjectState ps); + } + + private final Set<AccountGroup.Id> uploadGroups; + private final Set<AccountGroup.Id> receiveGroups; + + private final RefControl.Factory refControlFactory; private final CurrentUser user; private final ProjectState state; - ProjectControl(final CurrentUser who, final ProjectState ps) { + @Inject + ProjectControl(@GitUploadPackGroups Set<AccountGroup.Id> uploadGroups, + @GitReceivePackGroups Set<AccountGroup.Id> receiveGroups, + final RefControl.Factory refControlFactory, + @Assisted CurrentUser who, @Assisted ProjectState ps) { + this.uploadGroups = uploadGroups; + this.receiveGroups = receiveGroups; + this.refControlFactory = refControlFactory; user = who; state = ps; } @@ -118,7 +137,7 @@ public class ProjectControl { } public RefControl controlForRef(String refName) { - return new RefControl(this, refName); + return refControlFactory.create(this, refName); } public CurrentUser getCurrentUser() { @@ -169,33 +188,25 @@ public class ProjectControl { } /** @return true if the user can upload to at least one reference */ - public boolean canUploadToAtLeastOneRef() { - return canPerformOnAnyRef(ApprovalCategory.READ, (short) 2); + public boolean canPushToAtLeastOneRef() { + return canPerformOnAnyRef(ApprovalCategory.READ, (short) 2) + || canPerformOnAnyRef(ApprovalCategory.PUSH_HEAD, (short) 1) + || canPerformOnAnyRef(ApprovalCategory.PUSH_TAG, (short) 1); } + // TODO (anatol.pomazau): Try to merge this method with similar RefRightsForPattern#canPerform private boolean canPerformOnAnyRef(ApprovalCategory.Id actionId, short requireValue) { final Set<AccountGroup.Id> groups = user.getEffectiveGroups(); - int val = Integer.MIN_VALUE; - - for (final RefRight pr : state.getLocalRights(actionId)) { - if (groups.contains(pr.getAccountGroupId())) { - val = Math.max(pr.getMaxValue(), val); - } - } - if (val >= requireValue) { - return true; - } - if (actionId.canInheritFromWildProject()) { - for (final RefRight pr : state.getInheritedRights(actionId)) { - if (groups.contains(pr.getAccountGroupId())) { - val = Math.max(pr.getMaxValue(), val); - } + for (final RefRight pr : state.getAllRights(actionId, true)) { + if (groups.contains(pr.getAccountGroupId()) + && pr.getMaxValue() >= requireValue) { + return true; } } - return val >= requireValue; + return false; } private boolean canPerformOnAllRefs(ApprovalCategory.Id actionId, @@ -220,14 +231,17 @@ public class ProjectControl { private Set<String> allRefPatterns(ApprovalCategory.Id actionId) { final Set<String> all = new HashSet<String>(); - for (final RefRight pr : state.getLocalRights(actionId)) { + for (final RefRight pr : state.getAllRights(actionId, true)) { all.add(pr.getRefPattern()); } - if (actionId.canInheritFromWildProject()) { - for (final RefRight pr : state.getInheritedRights(actionId)) { - all.add(pr.getRefPattern()); - } - } return all; } + + public boolean canRunUploadPack() { + return isAnyIncludedIn(uploadGroups, user.getEffectiveGroups()); + } + + public boolean canRunReceivePack() { + return isAnyIncludedIn(receiveGroups, user.getEffectiveGroups()); + } } diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java index 330fc6efa8..567ecfeeb7 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java @@ -28,6 +28,8 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; import java.util.List; import java.util.Set; @@ -40,6 +42,7 @@ public class ProjectState { private final AnonymousUser anonymousUser; private final Project.NameKey wildProject; private final ProjectCache projectCache; + private final ProjectControl.AssistedFactory projectControlFactory; private final Project project; private final Collection<RefRight> localRights; @@ -51,11 +54,13 @@ public class ProjectState { protected ProjectState(final AnonymousUser anonymousUser, final ProjectCache projectCache, @WildProjectName final Project.NameKey wildProject, + final ProjectControl.AssistedFactory projectControlFactory, @Assisted final Project project, @Assisted final Collection<RefRight> rights) { this.anonymousUser = anonymousUser; this.projectCache = projectCache; this.wildProject = wildProject; + this.projectControlFactory = projectControlFactory; this.project = project; this.localRights = rights; @@ -134,16 +139,59 @@ public class ProjectState { } /** - * Get the rights this project inherits from the wild project. + * Utility class that is needed to filter overridden refrights + */ + private static class Grant { + final AccountGroup.Id group; + final String pattern; + + private Grant(AccountGroup.Id group, String pattern) { + this.group = group; + this.pattern = pattern; + } + + @Override + public boolean equals(Object o) { + Grant grant = (Grant) o; + return group.equals(grant.group) && pattern.equals(grant.pattern); + } + + @Override + public int hashCode() { + int result = group.hashCode(); + result = 31 * result + pattern.hashCode(); + return result; + } + } + + /** + * Get the rights this project has and inherits from the wild project. * * @param action the category requested. + * @param dropOverridden whether to remove inherited permissions in case if we have a + * local one that matches (action,group,ref) * @return immutable collection of rights for the requested category. */ - public Collection<RefRight> getInheritedRights(ApprovalCategory.Id action) { + public Collection<RefRight> getAllRights(ApprovalCategory.Id action, boolean dropOverridden) { + Collection<RefRight> rights = new LinkedList<RefRight>(getLocalRights(action)); if (action.canInheritFromWildProject()) { - return filter(getInheritedRights(), action); + rights.addAll(filter(getInheritedRights(), action)); + } + if (dropOverridden) { + Set<Grant> grants = new HashSet<Grant>(); + Iterator<RefRight> iter = rights.iterator(); + while (iter.hasNext()) { + RefRight right = iter.next(); + + Grant grant = new Grant(right.getAccountGroupId(), right.getRefPattern()); + if (grants.contains(grant)) { + iter.remove(); + } else { + grants.add(grant); + } + } } - return Collections.emptyList(); + return Collections.unmodifiableCollection(rights); } /** Is this the special wild project which manages inherited rights? */ @@ -160,7 +208,7 @@ public class ProjectState { } public ProjectControl controlFor(final CurrentUser user) { - return new ProjectControl(user, this); + return projectControlFactory.create(user, this); } private static Collection<RefRight> filter(Collection<RefRight> all, diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java index 46fd90e7c7..7a27835460 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java @@ -32,8 +32,11 @@ import com.google.gerrit.common.data.ParamertizedString; import com.google.gerrit.reviewdb.AccountGroup; import com.google.gerrit.reviewdb.ApprovalCategory; import com.google.gerrit.reviewdb.RefRight; +import com.google.gerrit.reviewdb.SystemConfig; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.IdentifiedUser; +import com.google.inject.Inject; +import com.google.inject.assistedinject.Assisted; import dk.brics.automaton.RegExp; @@ -50,6 +53,7 @@ import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -60,13 +64,22 @@ import java.util.regex.Pattern; /** Manages access control for Git references (aka branches, tags). */ public class RefControl { + public interface Factory { + RefControl create(ProjectControl projectControl, String ref); + } + + private final SystemConfig systemConfig; private final ProjectControl projectControl; private final String refName; private Boolean canForgeAuthor; private Boolean canForgeCommitter; - RefControl(final ProjectControl projectControl, String ref) { + @Inject + protected RefControl(final SystemConfig systemConfig, + @Assisted final ProjectControl projectControl, + @Assisted String ref) { + this.systemConfig = systemConfig; if (isRE(ref)) { ref = shortestExample(ref); @@ -302,38 +315,35 @@ public class RefControl { * @param groups The groups of the user * @return The allowed value for this ref for all the specified groups */ - public int allowedValueForRef(Set<AccountGroup.Id> groups) { - int val = Integer.MIN_VALUE; + private boolean allowedValueForRef(Set<AccountGroup.Id> groups, short level) { for (RefRight right : rights) { - if (groups.contains(right.getAccountGroupId())) { - val = Math.max(right.getMaxValue(), val); + if (groups.contains(right.getAccountGroupId()) + && right.getMaxValue() >= level) { + return true; } } - return val; + return false; } } boolean canPerform(ApprovalCategory.Id actionId, short level) { final Set<AccountGroup.Id> groups = getCurrentUser().getEffectiveGroups(); - int val = Integer.MIN_VALUE; List<RefRight> allRights = new ArrayList<RefRight>(); - allRights.addAll(getLocalRights(actionId)); - - if (actionId.canInheritFromWildProject()) { - allRights.addAll(getInheritedRights(actionId)); - } + allRights.addAll(getAllRights(actionId)); SortedMap<String, RefRightsForPattern> perPatternRights = sortedRightsByPattern(allRights); for (RefRightsForPattern right : perPatternRights.values()) { - val = Math.max(val, right.allowedValueForRef(groups)); - if (val >= level || right.containsExclusive()) { - return val >= level; + if (right.allowedValueForRef(groups, level)) { + return true; + } + if (right.containsExclusive() && !actionId.equals(OWN)) { + break; } } - return val >= level; + return false; } /** @@ -464,12 +474,9 @@ public class RefControl { return rights; } - private List<RefRight> getLocalRights(ApprovalCategory.Id actionId) { - return filter(getProjectState().getLocalRights(actionId)); - } - - private List<RefRight> getInheritedRights(ApprovalCategory.Id actionId) { - return filter(getProjectState().getInheritedRights(actionId)); + private List<RefRight> getAllRights(ApprovalCategory.Id actionId) { + final List<RefRight> allRefRights = filter(getProjectState().getAllRights(actionId, true)); + return resolveOwnerGroups(allRefRights); } /** @@ -484,8 +491,7 @@ public class RefControl { */ public List<RefRight> getApplicableRights(final ApprovalCategory.Id id) { List<RefRight> l = new ArrayList<RefRight>(); - l.addAll(getLocalRights(id)); - l.addAll(getInheritedRights(id)); + l.addAll(getAllRights(id)); SortedMap<String, RefRightsForPattern> perPatternRights = sortedRightsByPattern(l); List<RefRight> applicable = new ArrayList<RefRight>(); @@ -498,6 +504,47 @@ public class RefControl { return Collections.unmodifiableList(applicable); } + /** + * Resolves all refRights which assign privileges to the 'Project Owners' + * group. All other refRights stay unchanged. + * + * @param refRights refRights to be resolved + * @return the resolved refRights + */ + private List<RefRight> resolveOwnerGroups(final List<RefRight> refRights) { + final List<RefRight> resolvedRefRights = + new ArrayList<RefRight>(refRights.size()); + for (final RefRight refRight : refRights) { + resolvedRefRights.addAll(resolveOwnerGroups(refRight)); + } + return resolvedRefRights; + } + + /** + * Checks if the given refRight assigns privileges to the 'Project Owners' + * group. + * If yes, resolves the 'Project Owners' group to the concrete groups that + * own the project and creates new refRights for the concrete owner groups + * which are returned. + * If no, the given refRight is returned unchanged. + * + * @param refRight refRight + * @return the resolved refRights + */ + private Set<RefRight> resolveOwnerGroups(final RefRight refRight) { + final Set<RefRight> resolvedRefRights = new HashSet<RefRight>(); + if (refRight.getAccountGroupId().equals(systemConfig.ownerGroupId)) { + for (final AccountGroup.Id ownerGroup : getProjectState().getOwners()) { + if (!ownerGroup.equals(systemConfig.ownerGroupId)) { + resolvedRefRights.add(new RefRight(refRight, ownerGroup)); + } + } + } else { + resolvedRefRights.add(refRight); + } + return resolvedRefRights; + } + private List<RefRight> filter(Collection<RefRight> all) { List<RefRight> mine = new ArrayList<RefRight>(all.size()); for (RefRight right : all) { diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BranchPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BranchPredicate.java index ae48fdc6f9..1083d6ae30 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BranchPredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BranchPredicate.java @@ -28,7 +28,7 @@ class BranchPredicate extends OperatorPredicate<ChangeData> { BranchPredicate(Provider<ReviewDb> dbProvider, String branch) { super(ChangeQueryBuilder.FIELD_BRANCH, branch); this.dbProvider = dbProvider; - this.shortName = new Branch.NameKey(null, branch).getShortName(); + this.shortName = branch; } @Override @@ -37,7 +37,8 @@ class BranchPredicate extends OperatorPredicate<ChangeData> { if (change == null) { return false; } - return shortName.equals(change.getDest().getShortName()); + return change.getDest().get().startsWith(Branch.R_HEADS) + && shortName.equals(change.getDest().getShortName()); } @Override diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java index 930b2d6960..b4965f040e 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java @@ -32,13 +32,16 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; public class ChangeData { private final Change.Id legacyId; private Change change; private Collection<PatchSet> patches; private Collection<PatchSetApproval> approvals; + private Map<PatchSet.Id,Collection<PatchSetApproval>> approvalsMap; private Collection<PatchSetApproval> currentApprovals; private String[] currentFiles; private Collection<PatchLineComment> comments; @@ -175,6 +178,23 @@ public class ChangeData { return approvals; } + public Map<PatchSet.Id,Collection<PatchSetApproval>> approvalsMap(Provider<ReviewDb> db) + throws OrmException { + if (approvalsMap == null) { + Collection<PatchSetApproval> all = approvals(db); + approvalsMap = new HashMap<PatchSet.Id,Collection<PatchSetApproval>>(all.size()); + for (PatchSetApproval psa : all) { + Collection<PatchSetApproval> c = approvalsMap.get(psa.getPatchSetId()); + if (c == null) { + c = new ArrayList<PatchSetApproval>(); + approvalsMap.put(psa.getPatchSetId(), c); + } + c.add(psa); + } + } + return approvalsMap; + } + public Collection<PatchLineComment> comments(Provider<ReviewDb> db) throws OrmException { if (comments == null) { diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java index eccb2e8239..5cd05a8ee2 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java @@ -27,6 +27,7 @@ import com.google.gerrit.server.account.AccountResolver; import com.google.gerrit.server.account.GroupCache; import com.google.gerrit.server.config.AuthConfig; import com.google.gerrit.server.config.WildProjectName; +import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.patch.PatchListCache; import com.google.gerrit.server.project.ChangeControl; import com.google.gerrit.server.query.IntPredicate; @@ -75,6 +76,7 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData> { public static final String FIELD_HAS = "has"; public static final String FIELD_LABEL = "label"; public static final String FIELD_LIMIT = "limit"; + public static final String FIELD_MESSAGE = "message"; public static final String FIELD_OWNER = "owner"; public static final String FIELD_PROJECT = "project"; public static final String FIELD_REF = "ref"; @@ -102,6 +104,7 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData> { final ApprovalTypes approvalTypes; final Project.NameKey wildProjectName; final PatchListCache patchListCache; + final GitRepositoryManager repoManager; @Inject Arguments(Provider<ReviewDb> dbProvider, @@ -112,7 +115,8 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData> { AccountResolver accountResolver, GroupCache groupCache, AuthConfig authConfig, ApprovalTypes approvalTypes, @WildProjectName Project.NameKey wildProjectName, - PatchListCache patchListCache) { + PatchListCache patchListCache, + GitRepositoryManager repoManager) { this.dbProvider = dbProvider; this.rewriter = rewriter; this.userFactory = userFactory; @@ -124,6 +128,7 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData> { this.approvalTypes = approvalTypes; this.wildProjectName = wildProjectName; this.patchListCache = patchListCache; + this.repoManager = repoManager; } } @@ -238,21 +243,29 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData> { @Operator public Predicate<ChangeData> project(String name) { + if (name.startsWith("^")) + return new RegexProjectPredicate(args.dbProvider, name); return new ProjectPredicate(args.dbProvider, name); } @Operator public Predicate<ChangeData> branch(String name) { + if (name.startsWith("^")) + return new RegexBranchPredicate(args.dbProvider, name); return new BranchPredicate(args.dbProvider, name); } @Operator public Predicate<ChangeData> topic(String name) { + if (name.startsWith("^")) + return new RegexTopicPredicate(args.dbProvider, name); return new TopicPredicate(args.dbProvider, name); } @Operator public Predicate<ChangeData> ref(String ref) { + if (ref.startsWith("^")) + return new RegexRefPredicate(args.dbProvider, ref); return new RefPredicate(args.dbProvider, ref); } @@ -276,6 +289,11 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData> { } @Operator + public Predicate<ChangeData> message(String text) { + return new MessagePredicate(args.dbProvider, args.repoManager, text); + } + + @Operator public Predicate<ChangeData> starredby(String who) throws QueryParseException, OrmException { Account account = args.accountResolver.find(who); diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java index 904bc3cb70..6fbd060125 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java @@ -38,7 +38,7 @@ public class ChangeQueryRewriter extends QueryRewriter<ChangeData> { new ChangeQueryBuilder.Arguments( // new InvalidProvider<ReviewDb>(), // new InvalidProvider<ChangeQueryRewriter>(), // - null, null, null, null, null, null, null, null, null), null)); + null, null, null, null, null, null, null, null, null, null), null)); private final Provider<ReviewDb> dbProvider; diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/MessagePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/MessagePredicate.java new file mode 100644 index 0000000000..0490127a37 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/MessagePredicate.java @@ -0,0 +1,124 @@ +// Copyright (C) 2010 The Android Open Source Project +// +// 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.google.gerrit.server.query.change; + +import com.google.gerrit.reviewdb.Change; +import com.google.gerrit.reviewdb.PatchSet; +import com.google.gerrit.reviewdb.Project; +import com.google.gerrit.reviewdb.RevId; +import com.google.gerrit.reviewdb.ReviewDb; +import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.gerrit.server.query.OperatorPredicate; +import com.google.gwtorm.client.OrmException; +import com.google.inject.Provider; + +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.errors.RepositoryNotFoundException; +import org.eclipse.jgit.lib.AnyObjectId; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.revwalk.filter.MessageRevFilter; +import org.eclipse.jgit.revwalk.filter.RevFilter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + +/** + * Predicate to match changes that contains specified text in commit messages + * body. + */ +public class MessagePredicate extends OperatorPredicate<ChangeData> { + + private static final Logger log = + LoggerFactory.getLogger(MessagePredicate.class); + + private final Provider<ReviewDb> db; + private final GitRepositoryManager repoManager; + private final RevFilter rFilter; + + public MessagePredicate(Provider<ReviewDb> db, + GitRepositoryManager repoManager, String text) { + super(ChangeQueryBuilder.FIELD_MESSAGE, text); + this.db = db; + this.repoManager = repoManager; + this.rFilter = MessageRevFilter.create(text); + } + + @Override + public boolean match(ChangeData object) throws OrmException { + final PatchSet patchSet = object.currentPatchSet(db); + + if (patchSet == null) { + return false; + } + + final RevId revision = patchSet.getRevision(); + + if (revision == null) { + return false; + } + + final AnyObjectId objectId = ObjectId.fromString(revision.get()); + + if (objectId == null) { + return false; + } + + final Change change = object.change(db); + + if (change == null) { + return false; + } + + final Project.NameKey projectName = change.getProject(); + + if (projectName == null) { + return false; + } + + try { + final Repository repo = repoManager.openRepository(projectName.get()); + try { + final RevWalk rw = new RevWalk(repo); + try { + return rFilter.include(rw, rw.parseCommit(objectId)); + } finally { + rw.release(); + } + } finally { + repo.close(); + } + } catch (RepositoryNotFoundException e) { + log.error("Repository \"" + projectName.get() + "\" unknown.", e); + } catch (MissingObjectException e) { + log.error(projectName.get() + "\" commit does not exist.", e); + } catch (IncorrectObjectTypeException e) { + log.error(projectName.get() + "\" revision is not a commit.", e); + } catch (IOException e) { + log.error("Could not search for commit message in \"" + projectName.get() + + "\" repository.", e); + } + + return false; + } + + @Override + public int getCost() { + return 1; + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java index 3433fa9a8c..6c9fbc0379 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java @@ -20,6 +20,7 @@ import com.google.gerrit.reviewdb.ReviewDb; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.events.ChangeAttribute; import com.google.gerrit.server.events.EventFactory; +import com.google.gerrit.server.events.PatchSetAttribute; import com.google.gerrit.server.events.QueryStats; import com.google.gerrit.server.query.Predicate; import com.google.gerrit.server.query.QueryParseException; @@ -69,6 +70,7 @@ public class QueryProcessor { private OutputFormat outputFormat = OutputFormat.TEXT; private boolean includePatchSets; private boolean includeCurrentPatchSet; + private boolean includeApprovals; private OutputStream outputStream = DisabledOutputStream.INSTANCE; private PrintWriter out; @@ -91,6 +93,10 @@ public class QueryProcessor { includeCurrentPatchSet = on; } + public void setIncludeApprovals(boolean on) { + includeApprovals = on; + } + public void setOutput(OutputStream out, OutputFormat fmt) { this.outputStream = out; this.outputFormat = fmt; @@ -151,7 +157,8 @@ public class QueryProcessor { eventFactory.addTrackingIds(c, d.trackingIds(db)); if (includePatchSets) { - eventFactory.addPatchSets(c, d.patches(db)); + eventFactory.addPatchSets(c, d.patches(db), + includeApprovals ? d.approvalsMap(db) : null); } if (includeCurrentPatchSet) { diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexBranchPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexBranchPredicate.java new file mode 100644 index 0000000000..a18d43ae9b --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexBranchPredicate.java @@ -0,0 +1,60 @@ +// Copyright (C) 2010 The Android Open Source Project +// +// 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.google.gerrit.server.query.change; + +import com.google.gerrit.reviewdb.Branch; +import com.google.gerrit.reviewdb.Change; +import com.google.gerrit.reviewdb.ReviewDb; +import com.google.gerrit.server.query.OperatorPredicate; +import com.google.gwtorm.client.OrmException; +import com.google.inject.Provider; + +import dk.brics.automaton.RegExp; +import dk.brics.automaton.RunAutomaton; + +class RegexBranchPredicate extends OperatorPredicate<ChangeData> { + private final Provider<ReviewDb> dbProvider; + private final RunAutomaton pattern; + + RegexBranchPredicate(Provider<ReviewDb> dbProvider, String re) { + super(ChangeQueryBuilder.FIELD_BRANCH, re); + + if (re.startsWith("^")) { + re = re.substring(1); + } + + if (re.endsWith("$") && !re.endsWith("\\$")) { + re = re.substring(0, re.length() - 1); + } + + this.dbProvider = dbProvider; + this.pattern = new RunAutomaton(new RegExp(re).toAutomaton()); + } + + @Override + public boolean match(final ChangeData object) throws OrmException { + Change change = object.change(dbProvider); + if (change == null) { + return false; + } + return change.getDest().get().startsWith(Branch.R_HEADS) + && pattern.run(change.getDest().getShortName()); + } + + @Override + public int getCost() { + return 1; + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexProjectPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexProjectPredicate.java new file mode 100644 index 0000000000..c35b66e2cc --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexProjectPredicate.java @@ -0,0 +1,61 @@ +// Copyright (C) 2010 The Android Open Source Project +// +// 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.google.gerrit.server.query.change; + +import com.google.gerrit.reviewdb.Change; +import com.google.gerrit.reviewdb.Project; +import com.google.gerrit.reviewdb.ReviewDb; +import com.google.gerrit.server.query.OperatorPredicate; +import com.google.gwtorm.client.OrmException; +import com.google.inject.Provider; + +import dk.brics.automaton.RegExp; +import dk.brics.automaton.RunAutomaton; + +class RegexProjectPredicate extends OperatorPredicate<ChangeData> { + private final Provider<ReviewDb> dbProvider; + private final RunAutomaton pattern; + + RegexProjectPredicate(Provider<ReviewDb> dbProvider, String re) { + super(ChangeQueryBuilder.FIELD_PROJECT, re); + + if (re.startsWith("^")) { + re = re.substring(1); + } + + if (re.endsWith("$") && !re.endsWith("\\$")) { + re = re.substring(0, re.length() - 1); + } + + this.dbProvider = dbProvider; + this.pattern = new RunAutomaton(new RegExp(re).toAutomaton()); + } + + @Override + public boolean match(final ChangeData object) throws OrmException { + Change change = object.change(dbProvider); + if (change == null) { + return false; + } + + Project.NameKey p = change.getDest().getParentKey(); + return pattern.run(p.get()); + } + + @Override + public int getCost() { + return 1; + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexRefPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexRefPredicate.java new file mode 100644 index 0000000000..e9e9958f39 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexRefPredicate.java @@ -0,0 +1,58 @@ +// Copyright (C) 2010 The Android Open Source Project +// +// 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.google.gerrit.server.query.change; + +import com.google.gerrit.reviewdb.Change; +import com.google.gerrit.reviewdb.ReviewDb; +import com.google.gerrit.server.query.OperatorPredicate; +import com.google.gwtorm.client.OrmException; +import com.google.inject.Provider; + +import dk.brics.automaton.RegExp; +import dk.brics.automaton.RunAutomaton; + +class RegexRefPredicate extends OperatorPredicate<ChangeData> { + private final Provider<ReviewDb> dbProvider; + private final RunAutomaton pattern; + + RegexRefPredicate(Provider<ReviewDb> dbProvider, String re) { + super(ChangeQueryBuilder.FIELD_REF, re); + + if (re.startsWith("^")) { + re = re.substring(1); + } + + if (re.endsWith("$") && !re.endsWith("\\$")) { + re = re.substring(0, re.length() - 1); + } + + this.dbProvider = dbProvider; + this.pattern = new RunAutomaton(new RegExp(re).toAutomaton()); + } + + @Override + public boolean match(final ChangeData object) throws OrmException { + Change change = object.change(dbProvider); + if (change == null) { + return false; + } + return pattern.run(change.getDest().get()); + } + + @Override + public int getCost() { + return 1; + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexTopicPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexTopicPredicate.java new file mode 100644 index 0000000000..e16088cf3a --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexTopicPredicate.java @@ -0,0 +1,58 @@ +// Copyright (C) 2010 The Android Open Source Project +// +// 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.google.gerrit.server.query.change; + +import com.google.gerrit.reviewdb.Change; +import com.google.gerrit.reviewdb.ReviewDb; +import com.google.gerrit.server.query.OperatorPredicate; +import com.google.gwtorm.client.OrmException; +import com.google.inject.Provider; + +import dk.brics.automaton.RegExp; +import dk.brics.automaton.RunAutomaton; + +class RegexTopicPredicate extends OperatorPredicate<ChangeData> { + private final Provider<ReviewDb> dbProvider; + private final RunAutomaton pattern; + + RegexTopicPredicate(Provider<ReviewDb> dbProvider, String re) { + super(ChangeQueryBuilder.FIELD_TOPIC, re); + + if (re.startsWith("^")) { + re = re.substring(1); + } + + if (re.endsWith("$") && !re.endsWith("\\$")) { + re = re.substring(0, re.length() - 1); + } + + this.dbProvider = dbProvider; + this.pattern = new RunAutomaton(new RegExp(re).toAutomaton()); + } + + @Override + public boolean match(final ChangeData object) throws OrmException { + Change change = object.change(dbProvider); + if (change == null || change.getTopic() == null) { + return false; + } + return pattern.run(change.getTopic()); + } + + @Override + public int getCost() { + return 1; + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java index 302e22b813..9a266abb6b 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java @@ -150,12 +150,23 @@ public class SchemaCreator { c.accountGroupNames().insert( Collections.singleton(new AccountGroupName(batchUsers))); + final AccountGroup owners = + new AccountGroup(new AccountGroup.NameKey("Project Owners"), + new AccountGroup.Id(c.nextAccountGroupId())); + owners.setDescription("Any owner of the project"); + owners.setOwnerGroupId(admin.getId()); + owners.setType(AccountGroup.Type.SYSTEM); + c.accountGroups().insert(Collections.singleton(owners)); + c.accountGroupNames().insert( + Collections.singleton(new AccountGroupName(owners))); + final SystemConfig s = SystemConfig.create(); s.registerEmailPrivateKey = SignedToken.generateRandomKey(); s.adminGroupId = admin.getId(); s.anonymousGroupId = anonymous.getId(); s.registeredGroupId = registered.getId(); s.batchUsersGroupId = batchUsers.getId(); + s.ownerGroupId = owners.getId(); s.wildProjectName = DEFAULT_WILD_NAME; try { s.sitePath = site_path.getCanonicalPath(); diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java index 2f8d4ca86b..b7328b5394 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java @@ -32,7 +32,7 @@ import java.util.List; /** A version of the database schema. */ public abstract class SchemaVersion { /** The current schema version. */ - private static final Class<? extends SchemaVersion> C = Schema_40.class; + private static final Class<? extends SchemaVersion> C = Schema_47.class; public static class Module extends AbstractModule { @Override diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_41.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_41.java new file mode 100644 index 0000000000..508db43665 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_41.java @@ -0,0 +1,25 @@ +// Copyright (C) 2010 The Android Open Source Project +// +// 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.google.gerrit.server.schema; + +import com.google.inject.Inject; +import com.google.inject.Provider; + +public class Schema_41 extends SchemaVersion { + @Inject + Schema_41(Provider<Schema_40> prior) { + super(prior); + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_42.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_42.java new file mode 100644 index 0000000000..83bca7b837 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_42.java @@ -0,0 +1,25 @@ +// Copyright (C) 2010 The Android Open Source Project +// +// 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.google.gerrit.server.schema; + +import com.google.inject.Inject; +import com.google.inject.Provider; + +public class Schema_42 extends SchemaVersion { + @Inject + Schema_42(Provider<Schema_41> prior) { + super(prior); + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_43.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_43.java new file mode 100644 index 0000000000..0edb7e5ca3 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_43.java @@ -0,0 +1,25 @@ +// Copyright (C) 2010 The Android Open Source Project +// +// 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.google.gerrit.server.schema; + +import com.google.inject.Inject; +import com.google.inject.Provider; + +public class Schema_43 extends SchemaVersion { + @Inject + Schema_43(Provider<Schema_42> prior) { + super(prior); + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_44.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_44.java new file mode 100644 index 0000000000..4ab1986a72 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_44.java @@ -0,0 +1,25 @@ +// Copyright (C) 2010 The Android Open Source Project +// +// 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.google.gerrit.server.schema; + +import com.google.inject.Inject; +import com.google.inject.Provider; + +public class Schema_44 extends SchemaVersion { + @Inject + Schema_44(Provider<Schema_43> prior) { + super(prior); + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_45.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_45.java new file mode 100644 index 0000000000..e37e87de01 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_45.java @@ -0,0 +1,25 @@ +// Copyright (C) 2010 The Android Open Source Project +// +// 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.google.gerrit.server.schema; + +import com.google.inject.Inject; +import com.google.inject.Provider; + +public class Schema_45 extends SchemaVersion { + @Inject + Schema_45(Provider<Schema_44> prior) { + super(prior); + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_46.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_46.java new file mode 100644 index 0000000000..8730b4e943 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_46.java @@ -0,0 +1,62 @@ +// Copyright (C) 2010 The Android Open Source Project +// +// 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.google.gerrit.server.schema; + +import com.google.gerrit.reviewdb.AccountGroup; +import com.google.gerrit.reviewdb.AccountGroupName; +import com.google.gerrit.reviewdb.ReviewDb; +import com.google.gwtorm.client.OrmException; +import com.google.gwtorm.jdbc.JdbcSchema; +import com.google.inject.Inject; +import com.google.inject.Provider; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Collections; + +public class Schema_46 extends SchemaVersion { + + @Inject + Schema_46(final Provider<Schema_45> prior) { + super(prior); + } + + @Override + protected void migrateData(ReviewDb db, UpdateUI ui) throws SQLException, + OrmException { + AccountGroup.Id groupId = new AccountGroup.Id(db.nextAccountGroupId()); + + // update system_config + final Connection connection = ((JdbcSchema) db).getConnection(); + final Statement stmt = connection.createStatement(); + stmt.execute("UPDATE system_config SET OWNER_GROUP_ID = " + groupId.get()); + final ResultSet resultSet = + stmt.executeQuery("SELECT ADMIN_GROUP_ID FROM system_config"); + resultSet.next(); + final int adminGroupId = resultSet.getInt(1); + + // create 'Project Owners' group + AccountGroup.NameKey nameKey = new AccountGroup.NameKey("Project Owners"); + AccountGroup group = new AccountGroup(nameKey, groupId); + group.setType(AccountGroup.Type.SYSTEM); + group.setOwnerGroupId(new AccountGroup.Id(adminGroupId)); + group.setDescription("Any owner of the project"); + AccountGroupName gn = new AccountGroupName(group); + db.accountGroupNames().insert(Collections.singleton(gn)); + db.accountGroups().insert(Collections.singleton(group)); + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_47.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_47.java new file mode 100644 index 0000000000..124cc0232c --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_47.java @@ -0,0 +1,25 @@ +// Copyright (C) 2010 The Android Open Source Project +// +// 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.google.gerrit.server.schema; + +import com.google.inject.Inject; +import com.google.inject.Provider; + +public class Schema_47 extends SchemaVersion { + @Inject + Schema_47(Provider<Schema_46> prior) { + super(prior); + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/workflow/CategoryFunction.java b/gerrit-server/src/main/java/com/google/gerrit/server/workflow/CategoryFunction.java index dd29f0478e..6700393251 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/workflow/CategoryFunction.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/workflow/CategoryFunction.java @@ -33,6 +33,7 @@ public abstract class CategoryFunction { all.put(MaxWithBlock.NAME, new MaxWithBlock()); all.put(MaxNoBlock.NAME, new MaxNoBlock()); all.put(NoOpFunction.NAME, new NoOpFunction()); + all.put(NoBlock.NAME, new NoBlock()); } /** diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/workflow/NoBlock.java b/gerrit-server/src/main/java/com/google/gerrit/server/workflow/NoBlock.java new file mode 100644 index 0000000000..a08981703a --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/workflow/NoBlock.java @@ -0,0 +1,34 @@ +// Copyright (C) 2010 The Android Open Source Project +// +// 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.google.gerrit.server.workflow; + +import com.google.gerrit.common.data.ApprovalType; +import com.google.gerrit.server.CurrentUser; + +/** A function that does nothing. */ +public class NoBlock extends CategoryFunction { + public static String NAME = "NoBlock"; + + @Override + public void run(final ApprovalType at, final FunctionState state) { + state.valid(at, true); + } + + @Override + public boolean isValid(final CurrentUser user, final ApprovalType at, + final FunctionState state) { + return true; + } +} diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Abandoned.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Abandoned.vm new file mode 100644 index 0000000000..20529b222a --- /dev/null +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Abandoned.vm @@ -0,0 +1,44 @@ +## Copyright (C) 2010 The Android Open Source Project +## +## 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. +## +## +## Template Type: +## ------------- +## This is a velocity mail template, see: http://velocity.apache.org and the +## gerrit-docs:config-mail.txt for more info on modifying gerrit mail templates. +## +## Template File Names and extensions: +## ---------------------------------- +## Gerrit will use templates ending in ".vm" but will ignore templates ending +## in ".vm.example". If a .vm template does not exist, the default internal +## gerrit template which is the same as the .vm.example will be used. If you +## want to override the default template, copy the .vm.exmaple file to a .vm +## file and edit it appropriately. +## +## This Template: +## -------------- +## The Abandoned.vm template will determine the contents of the email related +## to a change being abandoned. It is a ChangeEmail: see ChangeSubject.vm and +## ChangeFooter.vm. +## +$fromName has abandoned this change. + +Change subject: $change.subject +...................................................................... + + +#if ($coverLetter) +$coverLetter + +#end diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooter.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooter.vm new file mode 100644 index 0000000000..5b74453450 --- /dev/null +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooter.vm @@ -0,0 +1,51 @@ +## Copyright (C) 2010 The Android Open Source Project +## +## 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. +## +## +## Template Type: +## ------------- +## This is a velocity mail template, see: http://velocity.apache.org and the +## gerrit-docs:config-mail.txt for more info on modifying gerrit mail templates. +## +## Template File Names and extensions: +## ---------------------------------- +## Gerrit will use templates ending in ".vm" but will ignore templates ending +## in ".vm.example". If a .vm template does not exist, the default internal +## gerrit template which is the same as the .vm.example will be used. If you +## want to override the default template, copy the .vm.exmaple file to a .vm +## file and edit it appropriately. +## +## This Template: +## -------------- +## The ChangeFooter.vm template will determine the contents of the footer +## text that will be appended to ALL emails related to changes. +## +-- +#if ($email.changeUrl) +To view, visit $email.changeUrl +#set ($notblank = 1) +#end +#if ($email.settingsUrl) +To unsubscribe, visit $email.settingsUrl +#set ($notblank = 1) +#end +#if ($notblank == 1) + +#end +Gerrit-MessageType: $messageClass +Gerrit-Change-Id: $changeId +Gerrit-PatchSet: $patchSet.patchSetId +Gerrit-Project: $projectName +Gerrit-Branch: $branch.shortName +Gerrit-Owner: $email.getNameEmailFor($change.owner) diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeSubject.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeSubject.vm new file mode 100644 index 0000000000..24cc23cf0c --- /dev/null +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeSubject.vm @@ -0,0 +1,37 @@ +## Copyright (C) 2010 The Android Open Source Project +## +## 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. +## +## +## Template Type: +## ------------- +## This is a velocity mail template, see: http://velocity.apache.org and the +## gerrit-docs:config-mail.txt for more info on modifying gerrit mail templates. +## +## Template File Names and extensions: +## ---------------------------------- +## Gerrit will use templates ending in ".vm" but will ignore templates ending +## in ".vm.example". If a .vm template does not exist, the default internal +## gerrit template which is the same as the .vm.example will be used. If you +## want to override the default template, copy the .vm.exmaple file to a .vm +## file and edit it appropriately. +## +## This Template: +## -------------- +## The ChangeSubject.vm template will determine the contents of the email +## subject line for ALL emails related to changes. +## +#macro(elipses $length $str) +#if($str.length() > $length)${str.substring(0,$length)}...#else$str#end +#end +Change in $projectName.replaceAll('/.*/', '...')[$branch.shortName]: #elipses(60, $change.subject) diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Comment.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Comment.vm new file mode 100644 index 0000000000..4ad355b86a --- /dev/null +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Comment.vm @@ -0,0 +1,47 @@ +## Copyright (C) 2010 The Android Open Source Project +## +## 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. +## +## +## Template Type: +## ------------- +## This is a velocity mail template, see: http://velocity.apache.org and the +## gerrit-docs:config-mail.txt for more info on modifying gerrit mail templates. +## +## Template File Names and extensions: +## ---------------------------------- +## Gerrit will use templates ending in ".vm" but will ignore templates ending +## in ".vm.example". If a .vm template does not exist, the default internal +## gerrit template which is the same as the .vm.example will be used. If you +## want to override the default template, copy the .vm.exmaple file to a .vm +## file and edit it appropriately. +## +## This Template: +## -------------- +## The Comment.vm template will determine the contents of the email related to +## a user submitting comments on changes. It is a ChangeEmail: see +## ChangeSubject.vm and ChangeFooter.vm. +## +#if ($email.coverLetter || $email.inlineComments) +$fromName has posted comments on this change. + +Change subject: $change.subject +...................................................................... + + +#if ($email.coverLetter) +$email.coverLetter + +#end +#if($email.inlineComments)$email.inlineComments#end +#end diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/MergeFail.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/MergeFail.vm new file mode 100644 index 0000000000..dfe3d92f3a --- /dev/null +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/MergeFail.vm @@ -0,0 +1,44 @@ +## Copyright (C) 2010 The Android Open Source Project +## +## 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. +## +## +## Template Type: +## ------------- +## This is a velocity mail template, see: http://velocity.apache.org and the +## gerrit-docs:config-mail.txt for more info on modifying gerrit mail templates. +## +## Template File Names and extensions: +## ---------------------------------- +## Gerrit will use templates ending in ".vm" but will ignore templates ending +## in ".vm.example". If a .vm template does not exist, the default internal +## gerrit template which is the same as the .vm.example will be used. If you +## want to override the default template, copy the .vm.exmaple file to a .vm +## file and edit it appropriately. +## +## This Template: +## -------------- +## The MergeFail.vm template will determine the contents of the email related +## to a failure upon attempting to merge a change to the head. It is a +## ChangeEmail: see ChangeSubject.vm and ChangeFooter.vm. +## +$fromName has submitted this change and it FAILED to merge. + +Change subject: $change.subject +...................................................................... + + +#if ($email.coverLetter) +$email.coverLetter + +#end diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Merged.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Merged.vm new file mode 100644 index 0000000000..296a37a215 --- /dev/null +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Merged.vm @@ -0,0 +1,44 @@ +## Copyright (C) 2010 The Android Open Source Project +## +## 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. +## +## +## Template Type: +## ------------- +## This is a velocity mail template, see: http://velocity.apache.org and the +## gerrit-docs:config-mail.txt for more info on modifying gerrit mail templates. +## +## Template File Names and extensions: +## ---------------------------------- +## Gerrit will use templates ending in ".vm" but will ignore templates ending +## in ".vm.example". If a .vm template does not exist, the default internal +## gerrit template which is the same as the .vm.example will be used. If you +## want to override the default template, copy the .vm.exmaple file to a .vm +## file and edit it appropriately. +## +## This Template: +## -------------- +## The Merged.vm template will determine the contents of the email related to +## a change successfully merged to the head. It is a ChangeEmail: see +## ChangeSubject.vm and ChangeFooter.vm. +## +#macro(elipses $length $str) +#if($str.length() > $length)${str.substring(0,$length)}...#else$str#end +#end +$fromName has submitted this change and it was merged. + +Change subject: $change.subject +...................................................................... + + +$email.changeDetail$email.approvals diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/NewChange.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/NewChange.vm new file mode 100644 index 0000000000..a9a5fed091 --- /dev/null +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/NewChange.vm @@ -0,0 +1,52 @@ +## Copyright (C) 2010 The Android Open Source Project +## +## 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. +## +## +## Template Type: +## ------------- +## This is a velocity mail template, see: http://velocity.apache.org and the +## gerrit-docs:config-mail.txt for more info on modifying gerrit mail templates. +## +## Template File Names and extensions: +## ---------------------------------- +## Gerrit will use templates ending in ".vm" but will ignore templates ending +## in ".vm.example". If a .vm template does not exist, the default internal +## gerrit template which is the same as the .vm.example will be used. If you +## want to override the default template, copy the .vm.exmaple file to a .vm +## file and edit it appropriately. +## +## This Template: +## -------------- +## The NewChange.vm template will determine the contents of the email related +## to a user submitting a new change for review. It is a ChangeEmail: see +## ChangeSubject.vm and ChangeFooter.vm. +## +#if($email.reviewerNames) +Hello $StringUtils.join($email.reviewerNames, ' ,'), + +I'd like you to do a code review.#if($email.changeUrl) Please visit + + $email.changeUrl + +to review the following change. +#end +#else +$fromName has uploaded a new change for review. +#end + +Change subject: $change.subject +...................................................................... + +$email.changeDetail + git pull ssh://$email.sshHost/$projectName $patchSet.refName diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/RegisterNewEmail.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/RegisterNewEmail.vm new file mode 100644 index 0000000000..c1de87e657 --- /dev/null +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/RegisterNewEmail.vm @@ -0,0 +1,49 @@ +## Copyright (C) 2010 The Android Open Source Project +## +## 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. +## +## +## Template Type: +## ------------- +## This is a velocity mail template, see: http://velocity.apache.org and the +## gerrit-docs:config-mail.txt for more info on modifying gerrit mail templates. +## +## Template File Names and extensions: +## ---------------------------------- +## Gerrit will use templates ending in ".vm" but will ignore templates ending +## in ".vm.example". If a .vm template does not exist, the default internal +## gerrit template which is the same as the .vm.example will be used. If you +## want to override the default template, copy the .vm.exmaple file to a .vm +## file and edit it appropriately. +## +## This Template: +## -------------- +## The RegisterNewEmail.vm template will determine the contents of the email +## related to registering new email accounts. +## +Welcome to Gerrit Code Review at ${email.gerritHost}. + +To add a verified email address to your user account, please +click on the following link: + +$email.gerritUrl#VE,$email.emailRegistrationToken + +If you have received this mail in error, you do not need to take any +action to cancel the account. The account will not be activated, and +you will not receive any further emails. + +If clicking the link above does not work, copy and paste the URL in a +new browser window instead. + +This is a send-only email address. Replies to this message will not +be read or answered. diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ReplacePatchSet.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ReplacePatchSet.vm new file mode 100644 index 0000000000..356f02ee4d --- /dev/null +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ReplacePatchSet.vm @@ -0,0 +1,52 @@ +## Copyright (C) 2010 The Android Open Source Project +## +## 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. +## +## +## Template Type: +## ------------- +## This is a velocity mail template, see: http://velocity.apache.org and the +## gerrit-docs:config-mail.txt for more info on modifying gerrit mail templates. +## +## Template File Names and extensions: +## ---------------------------------- +## Gerrit will use templates ending in ".vm" but will ignore templates ending +## in ".vm.example". If a .vm template does not exist, the default internal +## gerrit template which is the same as the .vm.example will be used. If you +## want to override the default template, copy the .vm.exmaple file to a .vm +## file and edit it appropriately. +## +## This Template: +## -------------- +## The ReplacePatchSet.vm template will determine the contents of the email +## related to a user submitting a new patchset for a change. It is a +## ChangeEmail: see ChangeSubject.vm and ChangeFooter.vm. +## +#if($email.reviewerNames) +Hello $StringUtils.join($email.reviewerNames, ' ,'), + +I'd like you to reexamine a change.#if($email.changeUrl) Please visit + + $email.changeUrl + +to look at the new patch set (#$patchSet.patchSetId). +#end +#else +$fromName has uploaded a new patch set (#$patchSet.patchSetId). +#end + +Change subject: $change.subject +...................................................................... + +$email.changeDetail + git pull ssh://$email.sshHost/$projectName $patchSet.refName diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/config/ConfigUtilTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/config/ConfigUtilTest.java new file mode 100644 index 0000000000..386a6d1a6b --- /dev/null +++ b/gerrit-server/src/test/java/com/google/gerrit/server/config/ConfigUtilTest.java @@ -0,0 +1,76 @@ +// Copyright (C) 2010 The Android Open Source Project +// +// 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.google.gerrit.server.config; + +import static java.util.concurrent.TimeUnit.DAYS; +import static java.util.concurrent.TimeUnit.*; + +import junit.framework.TestCase; + +import java.util.concurrent.TimeUnit; + +public class ConfigUtilTest extends TestCase { + public void testTimeUnit() { + assertEquals(ms(2, MILLISECONDS), parse("2ms")); + assertEquals(ms(200, MILLISECONDS), parse("200 milliseconds")); + + assertEquals(ms(2, SECONDS), parse("2s")); + assertEquals(ms(231, SECONDS), parse("231sec")); + assertEquals(ms(1, SECONDS), parse("1second")); + assertEquals(ms(300, SECONDS), parse("300 seconds")); + + assertEquals(ms(2, MINUTES), parse("2m")); + assertEquals(ms(2, MINUTES), parse("2min")); + assertEquals(ms(1, MINUTES), parse("1 minute")); + assertEquals(ms(10, MINUTES), parse("10 minutes")); + + assertEquals(ms(5, HOURS), parse("5h")); + assertEquals(ms(5, HOURS), parse("5hr")); + assertEquals(ms(1, HOURS), parse("1hour")); + assertEquals(ms(48, HOURS), parse("48hours")); + + assertEquals(ms(5, HOURS), parse("5 h")); + assertEquals(ms(5, HOURS), parse("5 hr")); + assertEquals(ms(1, HOURS), parse("1 hour")); + assertEquals(ms(48, HOURS), parse("48 hours")); + assertEquals(ms(48, HOURS), parse("48 \t \r hours")); + + assertEquals(ms(4, DAYS), parse("4d")); + assertEquals(ms(1, DAYS), parse("1day")); + assertEquals(ms(14, DAYS), parse("14days")); + + assertEquals(ms(7, DAYS), parse("1w")); + assertEquals(ms(7, DAYS), parse("1week")); + assertEquals(ms(14, DAYS), parse("2w")); + assertEquals(ms(14, DAYS), parse("2weeks")); + + assertEquals(ms(30, DAYS), parse("1mon")); + assertEquals(ms(30, DAYS), parse("1month")); + assertEquals(ms(60, DAYS), parse("2mon")); + assertEquals(ms(60, DAYS), parse("2months")); + + assertEquals(ms(365, DAYS), parse("1y")); + assertEquals(ms(365, DAYS), parse("1year")); + assertEquals(ms(365 * 2, DAYS), parse("2years")); + } + + private static long ms(int cnt, TimeUnit unit) { + return MILLISECONDS.convert(cnt, unit); + } + + private static long parse(String string) { + return ConfigUtil.getTimeUnit(string, 1, MILLISECONDS); + } +} diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java index 8c26705226..81b1762efd 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java @@ -16,6 +16,7 @@ package com.google.gerrit.server.project; import static com.google.gerrit.reviewdb.ApprovalCategory.OWN; import static com.google.gerrit.reviewdb.ApprovalCategory.READ; +import static com.google.gerrit.reviewdb.ApprovalCategory.SUBMIT; import com.google.gerrit.reviewdb.AccountGroup; import com.google.gerrit.reviewdb.AccountProjectWatch; @@ -50,7 +51,7 @@ import java.util.Set; public class RefControlTest extends TestCase { public void testOwnerProject() { - local.add(grant(OWN, admin, "refs/*", 1)); + grant(local, OWN, admin, "refs/*", 1); ProjectControl uBlah = user(devs); ProjectControl uAdmin = user(devs, admin); @@ -60,8 +61,8 @@ public class RefControlTest extends TestCase { } public void testBranchDelegation1() { - local.add(grant(OWN, admin, "refs/*", 1)); - local.add(grant(OWN, devs, "refs/heads/x/*", 1)); + grant(local, OWN, admin, "refs/*", 1); + grant(local, OWN, devs, "refs/heads/x/*", 1); ProjectControl uDev = user(devs); assertFalse("not owner", uDev.isOwner()); @@ -76,9 +77,9 @@ public class RefControlTest extends TestCase { } public void testBranchDelegation2() { - local.add(grant(OWN, admin, "refs/*", 1)); - local.add(grant(OWN, devs, "refs/heads/x/*", 1)); - local.add(grant(OWN, fixers, "-refs/heads/x/y/*", 1)); + grant(local, OWN, admin, "refs/*", 1); + grant(local, OWN, devs, "refs/heads/x/*", 1); + grant(local, OWN, fixers, "-refs/heads/x/y/*", 1); ProjectControl uDev = user(devs); assertFalse("not owner", uDev.isOwner()); @@ -86,9 +87,9 @@ public class RefControlTest extends TestCase { assertOwner("refs/heads/x/*", uDev); assertOwner("refs/heads/x/y", uDev); + assertOwner("refs/heads/x/y/*", uDev); assertNotOwner("refs/*", uDev); assertNotOwner("refs/heads/master", uDev); - assertNotOwner("refs/heads/x/y/*", uDev); ProjectControl uFix = user(fixers); assertFalse("not owner", uFix.isOwner()); @@ -103,11 +104,11 @@ public class RefControlTest extends TestCase { } public void testInheritRead_SingleBranchDeniesUpload() { - inherited.add(grant(READ, registered, "refs/*", 1, 2)); - local.add(grant(READ, registered, "-refs/heads/foobar", 1, 1)); + grant(parent, READ, registered, "refs/*", 1, 2); + grant(local, READ, registered, "-refs/heads/foobar", 1); ProjectControl u = user(); - assertTrue("can upload", u.canUploadToAtLeastOneRef()); + assertTrue("can upload", u.canPushToAtLeastOneRef()); assertTrue("can upload refs/heads/master", // u.controlForRef("refs/heads/master").canUpload()); @@ -117,11 +118,11 @@ public class RefControlTest extends TestCase { } public void testInheritRead_SingleBranchDoesNotOverrideInherited() { - inherited.add(grant(READ, registered, "refs/*", 1, 2)); - local.add(grant(READ, registered, "refs/heads/foobar", 1, 1)); + grant(parent, READ, registered, "refs/*", 1, 2); + grant(local, READ, registered, "refs/heads/foobar", 1); ProjectControl u = user(); - assertTrue("can upload", u.canUploadToAtLeastOneRef()); + assertTrue("can upload", u.canPushToAtLeastOneRef()); assertTrue("can upload refs/heads/master", // u.controlForRef("refs/heads/master").canUpload()); @@ -130,12 +131,54 @@ public class RefControlTest extends TestCase { u.controlForRef("refs/heads/foobar").canUpload()); } + public void testInheritRead_OverrideWithDeny() { + grant(parent, READ, registered, "refs/*", 1); + grant(local, READ, registered, "refs/*", 0); + + ProjectControl u = user(); + assertFalse("can't read", u.isVisible()); + } + + public void testInheritRead_AppendWithDenyOfRef() { + grant(parent, READ, registered, "refs/*", 1); + grant(local, READ, registered, "refs/heads/*", 0); + + ProjectControl u = user(); + assertTrue("can read", u.isVisible()); + assertTrue("can read", u.controlForRef("refs/master").isVisible()); + assertTrue("can read", u.controlForRef("refs/tags/foobar").isVisible()); + assertTrue("no master", u.controlForRef("refs/heads/master").isVisible()); + } + + public void testInheritRead_OverridesAndDeniesOfRef() { + grant(parent, READ, registered, "refs/*", 1); + grant(local, READ, registered, "refs/*", 0); + grant(local, READ, registered, "refs/heads/*", -1, 1); + + ProjectControl u = user(); + assertTrue("can read", u.isVisible()); + assertFalse("can't read", u.controlForRef("refs/foobar").isVisible()); + assertFalse("can't read", u.controlForRef("refs/tags/foobar").isVisible()); + assertTrue("can read", u.controlForRef("refs/heads/foobar").isVisible()); + } + + public void testInheritSubmit_OverridesAndDeniesOfRef() { + grant(parent, SUBMIT, registered, "refs/*", 1); + grant(local, SUBMIT, registered, "refs/*", 0); + grant(local, SUBMIT, registered, "refs/heads/*", -1, 1); + + ProjectControl u = user(); + assertFalse("can't submit", u.controlForRef("refs/foobar").canSubmit()); + assertFalse("can't submit", u.controlForRef("refs/tags/foobar").canSubmit()); + assertTrue("can submit", u.controlForRef("refs/heads/foobar").canSubmit()); + } + public void testCannotUploadToAnyRef() { - inherited.add(grant(READ, registered, "refs/*", 1, 1)); - local.add(grant(READ, devs, "refs/heads/*",1,2)); + grant(parent, READ, registered, "refs/*", 1); + grant(local, READ, devs, "refs/heads/*", 1, 2); ProjectControl u = user(); - assertFalse("cannot upload", u.canUploadToAtLeastOneRef()); + assertFalse("cannot upload", u.canPushToAtLeastOneRef()); assertFalse("cannot upload refs/heads/master", // u.controlForRef("refs/heads/master").canUpload()); } @@ -143,22 +186,26 @@ public class RefControlTest extends TestCase { // ----------------------------------------------------------------------- - private final Project.NameKey projectNameKey = new Project.NameKey("test"); + private final Project.NameKey local = new Project.NameKey("test"); + private final Project.NameKey parent = new Project.NameKey("parent"); private final AccountGroup.Id admin = new AccountGroup.Id(1); private final AccountGroup.Id anonymous = new AccountGroup.Id(2); private final AccountGroup.Id registered = new AccountGroup.Id(3); + private final AccountGroup.Id owners = new AccountGroup.Id(4); - private final AccountGroup.Id devs = new AccountGroup.Id(4); - private final AccountGroup.Id fixers = new AccountGroup.Id(5); + private final AccountGroup.Id devs = new AccountGroup.Id(5); + private final AccountGroup.Id fixers = new AccountGroup.Id(6); + private final SystemConfig systemConfig; private final AuthConfig authConfig; private final AnonymousUser anonymousUser; public RefControlTest() { - final SystemConfig systemConfig = SystemConfig.create(); + systemConfig = SystemConfig.create(); systemConfig.adminGroupId = admin; systemConfig.anonymousGroupId = anonymous; systemConfig.registeredGroupId = registered; + systemConfig.ownerGroupId = owners; systemConfig.batchUsersGroupId = anonymous; try { byte[] bin = "abcdefghijklmnopqrstuvwxyz".getBytes("UTF-8"); @@ -183,14 +230,14 @@ public class RefControlTest extends TestCase { anonymousUser = injector.getInstance(AnonymousUser.class); } - private List<RefRight> local; - private List<RefRight> inherited; + private List<RefRight> localRights; + private List<RefRight> inheritedRights; @Override protected void setUp() throws Exception { super.setUp(); - local = new ArrayList<RefRight>(); - inherited = new ArrayList<RefRight>(); + localRights = new ArrayList<RefRight>(); + inheritedRights = new ArrayList<RefRight>(); } private static void assertOwner(String ref, ProjectControl u) { @@ -201,32 +248,48 @@ public class RefControlTest extends TestCase { assertFalse("NOT OWN " + ref, u.controlForRef(ref).isOwner()); } - private RefRight grant(ApprovalCategory.Id categoryId, AccountGroup.Id group, - String ref, int maxValue) { - return grant(categoryId, group, ref, maxValue, maxValue); + private void grant(Project.NameKey project, ApprovalCategory.Id categoryId, + AccountGroup.Id group, String ref, int maxValue) { + grant(project, categoryId, group, ref, maxValue, maxValue); } - private RefRight grant(ApprovalCategory.Id categoryId, AccountGroup.Id group, + private void grant(Project.NameKey project, ApprovalCategory.Id categoryId, AccountGroup.Id group, String ref, int minValue, int maxValue) { RefRight right = - new RefRight(new RefRight.Key(projectNameKey, new RefPattern(ref), + new RefRight(new RefRight.Key(project, new RefPattern(ref), categoryId, group)); right.setMinValue((short) minValue); right.setMaxValue((short) maxValue); - return right; + + if (project == parent) { + inheritedRights.add(right); + } else if (project == local) { + localRights.add(right); + } else { + fail("Unknown project key: " + project); + } } private ProjectControl user(AccountGroup.Id... memberOf) { - return new ProjectControl(new MockUser(memberOf), newProjectState()); + RefControl.Factory refControlFactory = new RefControl.Factory() { + @Override + public RefControl create(final ProjectControl projectControl, final String ref) { + return new RefControl(systemConfig, projectControl, ref); + } + }; + return new ProjectControl(Collections.<AccountGroup.Id> emptySet(), + Collections.<AccountGroup.Id> emptySet(), refControlFactory, + new MockUser(memberOf), newProjectState()); } private ProjectState newProjectState() { ProjectCache projectCache = null; Project.NameKey wildProject = null; + ProjectControl.AssistedFactory projectControlFactory = null; ProjectState ps = - new ProjectState(anonymousUser, projectCache, wildProject, new Project( - projectNameKey), local); - ps.setInheritedRights(inherited); + new ProjectState(anonymousUser, projectCache, wildProject, + projectControlFactory, new Project(parent), localRights); + ps.setInheritedRights(inheritedRights); return ps; } diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaCreatorTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaCreatorTest.java index 1644d22ed2..e05ce2e65f 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaCreatorTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaCreatorTest.java @@ -163,6 +163,7 @@ public class SchemaCreatorTest extends TestCase { assertEquals("-- All Projects --", all.getName()); assertFalse(all.isUseContributorAgreements()); assertFalse(all.isUseSignedOffBy()); + assertFalse(all.isRequireChangeID()); } finally { c.close(); } diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/CommitMsgHookTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/CommitMsgHookTest.java index 4738802e73..0d1b6f9b86 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/CommitMsgHookTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/CommitMsgHookTest.java @@ -19,11 +19,11 @@ import com.google.gerrit.server.util.HostPlatform; import org.eclipse.jgit.dircache.DirCache; import org.eclipse.jgit.dircache.DirCacheBuilder; import org.eclipse.jgit.dircache.DirCacheEntry; -import org.eclipse.jgit.lib.Commit; +import org.eclipse.jgit.lib.CommitBuilder; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.lib.ObjectId; -import org.eclipse.jgit.lib.ObjectWriter; +import org.eclipse.jgit.lib.ObjectInserter; import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.RefUpdate; @@ -144,7 +144,7 @@ public class CommitMsgHookTest extends HookTestCase { "Change-Id: I7fc3876fee63c766a2063df97fbe04a2dddd8d7c\n",// call("a\n")); - final DirCacheBuilder builder = DirCache.lock(repository).builder(); + final DirCacheBuilder builder = repository.lockDirCache().builder(); builder.add(file("A")); assertTrue(builder.commit()); @@ -386,35 +386,41 @@ public class CommitMsgHookTest extends HookTestCase { } private DirCacheEntry file(final String name) throws IOException { - final DirCacheEntry e = new DirCacheEntry(name); - e.setFileMode(FileMode.REGULAR_FILE); - e.setObjectId(writer().writeBlob(Constants.encode(name))); - return e; + final ObjectInserter oi = repository.newObjectInserter(); + try { + final DirCacheEntry e = new DirCacheEntry(name); + e.setFileMode(FileMode.REGULAR_FILE); + e.setObjectId(oi.insert(Constants.OBJ_BLOB, Constants.encode(name))); + oi.flush(); + return e; + } finally { + oi.release(); + } } private void setHEAD() throws Exception { - final ObjectWriter ow = writer(); - final Commit commit = new Commit(repository); - commit.setTreeId(DirCache.newInCore().writeTree(ow)); - commit.setAuthor(author); - commit.setCommitter(committer); - commit.setMessage("test\n"); - final ObjectId commitId = ow.writeCommit(commit); - - final RefUpdate ref = repository.updateRef(Constants.HEAD); - ref.setNewObjectId(commitId); - switch (ref.forceUpdate()) { - case NEW: - case FAST_FORWARD: - case FORCED: - case NO_CHANGE: - break; - default: - fail(Constants.HEAD + " did not change: " + ref.getResult()); + final ObjectInserter oi = repository.newObjectInserter(); + try { + final CommitBuilder commit = new CommitBuilder(); + commit.setTreeId(oi.insert(Constants.OBJ_TREE, new byte[] {})); + commit.setAuthor(author); + commit.setCommitter(committer); + commit.setMessage("test\n"); + ObjectId commitId = oi.insert(commit); + + final RefUpdate ref = repository.updateRef(Constants.HEAD); + ref.setNewObjectId(commitId); + switch (ref.forceUpdate()) { + case NEW: + case FAST_FORWARD: + case FORCED: + case NO_CHANGE: + break; + default: + fail(Constants.HEAD + " did not change: " + ref.getResult()); + } + } finally { + oi.release(); } } - - private ObjectWriter writer() { - return new ObjectWriter(repository); - } } diff --git a/gerrit-sshd/pom.xml b/gerrit-sshd/pom.xml index 2bb4713c7e..32bcf57c54 100644 --- a/gerrit-sshd/pom.xml +++ b/gerrit-sshd/pom.xml @@ -22,7 +22,7 @@ limitations under the License. <parent> <groupId>com.google.gerrit</groupId> <artifactId>gerrit-parent</artifactId> - <version>2.1.4-SNAPSHOT</version> + <version>2.1-SNAPSHOT</version> </parent> <artifactId>gerrit-sshd</artifactId> diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java index 6c96b702f1..977d20914f 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java @@ -135,6 +135,11 @@ class DatabasePubKeyAuth implements PublickeyAuthenticator { } } + if (!createUser(sd, key).getAccount().isActive()) { + sd.authenticationError(username, "inactive-account"); + return false; + } + return success(username, session, sd, createUser(sd, key)); } diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/NoShell.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/NoShell.java index 95c3416df1..431b4209bd 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/NoShell.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/NoShell.java @@ -14,15 +14,28 @@ package com.google.gerrit.sshd; +import com.google.gerrit.reviewdb.Account; +import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.config.CanonicalWebUrl; +import com.google.gerrit.server.ssh.SshInfo; +import com.google.gerrit.sshd.SshScope.Context; +import com.google.inject.Inject; +import com.google.inject.Provider; + import org.apache.sshd.common.Factory; import org.apache.sshd.server.Command; import org.apache.sshd.server.Environment; import org.apache.sshd.server.ExitCallback; +import org.apache.sshd.server.SessionAware; +import org.apache.sshd.server.session.ServerSession; import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.util.SystemReader; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.net.MalformedURLException; +import java.net.URL; /** * Dummy shell which prints a message and terminates. @@ -32,41 +45,135 @@ import java.io.OutputStream; * cannot continue further. */ class NoShell implements Factory<Command> { + private final Provider<SendMessage> shell; + + @Inject + NoShell(Provider<SendMessage> shell) { + this.shell = shell; + } + public Command create() { - return new Command() { - private InputStream in; - private OutputStream out; - private OutputStream err; - private ExitCallback exit; - - public void setInputStream(final InputStream in) { - this.in = in; - } + return shell.get(); + } - public void setOutputStream(final OutputStream out) { - this.out = out; - } + static class SendMessage implements Command, SessionAware { + private final Provider<MessageFactory> messageFactory; + + private InputStream in; + private OutputStream out; + private OutputStream err; + private ExitCallback exit; + private Context context; + + @Inject + SendMessage(Provider<MessageFactory> messageFactory) { + this.messageFactory = messageFactory; + } + + public void setInputStream(final InputStream in) { + this.in = in; + } + + public void setOutputStream(final OutputStream out) { + this.out = out; + } + + public void setErrorStream(final OutputStream err) { + this.err = err; + } - public void setErrorStream(final OutputStream err) { - this.err = err; + public void setExitCallback(final ExitCallback callback) { + this.exit = callback; + } + + public void setSession(final ServerSession session) { + this.context = new Context(session.getAttribute(SshSession.KEY), ""); + } + + public void start(final Environment env) throws IOException { + Context old = SshScope.set(context); + String message; + try { + message = messageFactory.get().getMessage(); + } finally { + SshScope.set(old); } + err.write(Constants.encodeASCII(message.toString())); + err.flush(); + + in.close(); + out.close(); + err.close(); + exit.onExit(127); + } + + public void destroy() { + } + } + + static class MessageFactory { + private final IdentifiedUser user; + private final SshInfo sshInfo; + private final Provider<String> urlProvider; - public void setExitCallback(final ExitCallback callback) { - this.exit = callback; + @Inject + MessageFactory(IdentifiedUser user, SshInfo sshInfo, + @CanonicalWebUrl Provider<String> urlProvider) { + this.user = user; + this.sshInfo = sshInfo; + this.urlProvider = urlProvider; + } + + String getMessage() { + StringBuilder msg = new StringBuilder(); + + msg.append("\r\n"); + msg.append(" **** Welcome to Gerrit Code Review ****\r\n"); + msg.append("\r\n"); + + Account account = user.getAccount(); + String name = account.getFullName(); + if (name == null || name.isEmpty()) { + name = user.getUserName(); } + msg.append(" Hi "); + msg.append(name); + msg.append(", you have successfully connected over SSH."); + msg.append("\r\n"); + msg.append("\r\n"); - public void start(final Environment env) throws IOException { - err.write(Constants.encodeASCII("gerrit: no shell available\r\n")); - err.flush(); + msg.append(" Unfortunately, interactive shells are disabled.\r\n"); + msg.append(" To clone a hosted Git repository, use:\r\n"); + msg.append("\r\n"); - in.close(); - out.close(); - err.close(); - exit.onExit(127); + if (!sshInfo.getHostKeys().isEmpty()) { + String host = sshInfo.getHostKeys().get(0).getHost(); + if (host.startsWith("*:")) { + host = getGerritHost() + host.substring(1); + } + + msg.append(" git clone ssh://"); + msg.append(user.getUserName()); + msg.append("@"); + msg.append(host); + msg.append("/"); + msg.append("REPOSITORY_NAME.git"); + msg.append("\r\n"); } - public void destroy() { + msg.append("\r\n"); + return msg.toString(); + } + + private String getGerritHost() { + String url = urlProvider.get(); + if (url != null) { + try { + return new URL(url).getHost(); + } catch (MalformedURLException e) { + } } - }; + return SystemReader.getInstance().getHostname(); + } } } diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java index 2636ff2597..dee55a5922 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java @@ -14,7 +14,12 @@ package com.google.gerrit.sshd; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.concurrent.TimeUnit.SECONDS; + +import com.google.gerrit.common.Version; import com.google.gerrit.lifecycle.LifecycleListener; +import com.google.gerrit.server.config.ConfigUtil; import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.ssh.SshInfo; import com.google.gerrit.server.util.IdGenerator; @@ -58,8 +63,11 @@ import org.apache.sshd.common.util.Buffer; import org.apache.sshd.common.util.SecurityUtils; import org.apache.sshd.server.Command; import org.apache.sshd.server.CommandFactory; +import org.apache.sshd.server.FileSystemFactory; +import org.apache.sshd.server.FileSystemView; import org.apache.sshd.server.ForwardingFilter; import org.apache.sshd.server.PublickeyAuthenticator; +import org.apache.sshd.server.SshFile; import org.apache.sshd.server.UserAuth; import org.apache.sshd.server.auth.UserAuthPublicKey; import org.apache.sshd.server.channel.ChannelDirectTcpip; @@ -116,7 +124,7 @@ public class SshDaemon extends SshServer implements SshInfo, LifecycleListener { private volatile IoAcceptor acceptor; @Inject - SshDaemon(final CommandFactory commandFactory, + SshDaemon(final CommandFactory commandFactory, final NoShell noShell, final PublickeyAuthenticator userAuth, final KeyPairProvider hostKeyProvider, final IdGenerator idGenerator, @GerritServerConfig final Config cfg, final SshLog sshLog) { @@ -126,6 +134,25 @@ public class SshDaemon extends SshServer implements SshInfo, LifecycleListener { reuseAddress = cfg.getBoolean("sshd", "reuseaddress", true); keepAlive = cfg.getBoolean("sshd", "tcpkeepalive", true); + getProperties().put(SERVER_IDENTIFICATION, + "GerritCodeReview_" + Version.getVersion() // + + " (" + super.getVersion() + ")"); + + getProperties().put(MAX_AUTH_REQUESTS, + String.valueOf(cfg.getInt("sshd", "maxAuthTries", 6))); + + getProperties().put( + AUTH_TIMEOUT, + String.valueOf(MILLISECONDS.convert(ConfigUtil.getTimeUnit(cfg, "sshd", + null, "loginGraceTime", 120, SECONDS), SECONDS))); + + final int maxConnectionsPerUser = + cfg.getInt("sshd", "maxConnectionsPerUser", 64); + if (0 < maxConnectionsPerUser) { + getProperties().put(MAX_CONCURRENT_SESSIONS, + String.valueOf(maxConnectionsPerUser)); + } + if (SecurityUtils.isBouncyCastleRegistered()) { initProviderBouncyCastle(); } else { @@ -136,12 +163,13 @@ public class SshDaemon extends SshServer implements SshInfo, LifecycleListener { initSignatures(); initChannels(); initForwardingFilter(); + initFileSystemFactory(); initSubsystems(); initCompression(); initUserAuth(userAuth); setKeyPairProvider(hostKeyProvider); setCommandFactory(commandFactory); - setShellFactory(new NoShell()); + setShellFactory(noShell); setSessionFactory(new SessionFactory() { @Override protected ServerSession createSession(final IoSession io) @@ -485,4 +513,17 @@ public class SshDaemon extends SshServer implements SshInfo, LifecycleListener { } }); } + + private void initFileSystemFactory() { + setFileSystemFactory(new FileSystemFactory() { + @Override + public FileSystemView createFileSystemView(String userName) { + return new FileSystemView() { + @Override + public SshFile getFile(String file) { + return null; + }}; + } + }); + } } diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java index 40d271b975..c9e9b72ee7 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java @@ -26,6 +26,7 @@ import com.google.gerrit.server.PeerDaemonUser; import com.google.gerrit.server.RemotePeer; import com.google.gerrit.server.config.FactoryModule; import com.google.gerrit.server.config.GerritRequestModule; +import com.google.gerrit.server.git.TransferConfig; import com.google.gerrit.server.git.WorkQueue; import com.google.gerrit.server.project.ProjectControl; import com.google.gerrit.server.ssh.SshInfo; @@ -77,7 +78,6 @@ public class SshModule extends FactoryModule { bind(PublickeyAuthenticator.class).to(DatabasePubKeyAuth.class); bind(KeyPairProvider.class).toProvider(HostKeyProvider.class).in(SINGLETON); - bind(TransferConfig.class); install(new DefaultCommandModule()); diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminShowCaches.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminShowCaches.java index a78ced5a1f..c27ad0db32 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminShowCaches.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminShowCaches.java @@ -21,7 +21,7 @@ import net.sf.ehcache.Statistics; import net.sf.ehcache.config.CacheConfiguration; import org.apache.sshd.server.Environment; -import org.eclipse.jgit.lib.WindowCacheStatAccessor; +import org.eclipse.jgit.storage.file.WindowCacheStatAccessor; import java.io.PrintWriter; diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProject.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProject.java index 6d027d71f2..5269e4342f 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProject.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProject.java @@ -14,12 +14,14 @@ package com.google.gerrit.sshd.commands; +import com.google.gerrit.common.CollectionsUtil; import com.google.gerrit.reviewdb.AccountGroup; import com.google.gerrit.reviewdb.ApprovalCategory; import com.google.gerrit.reviewdb.Project; import com.google.gerrit.reviewdb.RefRight; import com.google.gerrit.reviewdb.ReviewDb; import com.google.gerrit.reviewdb.Project.SubmitType; +import com.google.gerrit.server.GerritPersonIdent; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.config.ProjectCreatorGroups; import com.google.gerrit.server.config.ProjectOwnerGroups; @@ -31,14 +33,21 @@ import com.google.gwtorm.client.OrmException; import com.google.inject.Inject; import org.apache.sshd.server.Environment; +import org.eclipse.jgit.lib.CommitBuilder; import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectInserter; +import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.RefUpdate; import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.RefUpdate.Result; import org.kohsuke.args4j.Option; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; @@ -46,6 +55,8 @@ import java.util.Set; /** Create a new project. **/ final class CreateProject extends BaseCommand { + private static final Logger log = LoggerFactory.getLogger(CreateProject.class); + @Option(name = "--name", required = true, aliases = {"-n"}, metaVar = "NAME", usage = "name of project to be created") private String projectName; @@ -55,6 +66,9 @@ final class CreateProject extends BaseCommand { @Option(name = "--parent", aliases = {"-p"}, metaVar = "NAME", usage = "parent project") private ProjectControl newParent; + @Option(name = "--permissions-only", usage = "create project for use only as parent") + private boolean permissionsOnly; + @Option(name = "--description", aliases = {"-d"}, metaVar = "DESC", usage = "description of project") private String projectDescription = ""; @@ -68,10 +82,19 @@ final class CreateProject extends BaseCommand { @Option(name = "--use-signed-off-by", aliases = {"--so"}, usage = "if signed-off-by is required") private boolean signedOffBy; + @Option(name = "--use-content-merge", usage = "allow automatic conflict resolving within files") + private boolean contentMerge; + + @Option(name = "--require-change-id", aliases = {"--id"}, usage = "if change-id is required") + private boolean requireChangeID; + @Option(name = "--branch", aliases = {"-b"}, metaVar = "BRANCH", usage = "initial branch name\n" + "(default: master)") private String branch = Constants.MASTER; + @Option(name = "--empty-commit", usage = "to create initial empty commit") + private boolean createEmptyCommit; + @Inject private ReviewDb db; @@ -92,6 +115,10 @@ final class CreateProject extends BaseCommand { @Inject private ReplicationQueue rq; + @Inject + @GerritPersonIdent + private PersonIdent serverIdent; + @Override public void start(final Environment env) { startThread(new CommandRunnable() { @@ -104,18 +131,30 @@ final class CreateProject extends BaseCommand { try { validateParameters(); - Repository repo = repoManager.createRepository(projectName); - repo.create(true); + if (!permissionsOnly) { + final Repository repo = repoManager.createRepository(projectName); + try { + repo.create(true); - RefUpdate u = repo.updateRef(Constants.HEAD); - u.disableRefLog(); - u.link(branch); + RefUpdate u = repo.updateRef(Constants.HEAD); + u.disableRefLog(); + u.link(branch); - repoManager.setProjectDescription(projectName, projectDescription); + repoManager + .setProjectDescription(projectName, projectDescription); - createProject(); + final Project.NameKey project = new Project.NameKey(projectName); + rq.replicateNewProject(project, branch); + + if (createEmptyCommit) { + createEmptyCommit(repo, project, branch); + } + } finally { + repo.close(); + } + } - rq.replicateNewProject(new Project.NameKey(projectName), branch); + createProject(); } catch (Exception e) { p.print("Error when trying to create project: " + e.getMessage() + "\n"); @@ -126,24 +165,36 @@ final class CreateProject extends BaseCommand { }); } - /** - * Checks if any of the elements in the first collection can be found in the - * second collection. - * - * @param findAnyOfThese which elements to look for. - * @param inThisCollection where to look for them. - * @param <E> type of the elements in question. - * @return {@code true} if any of the elements in {@code findAnyOfThese} can - * be found in {@code inThisCollection}, {@code false} otherwise. - */ - private static <E> boolean isAnyIncludedIn(Collection<E> findAnyOfThese, - Collection<E> inThisCollection) { - for (E findThisItem : findAnyOfThese) { - if (inThisCollection.contains(findThisItem)) { - return true; + private void createEmptyCommit(final Repository repo, + final Project.NameKey project, final String ref) throws IOException { + ObjectInserter oi = repo.newObjectInserter(); + try { + CommitBuilder cb = new CommitBuilder(); + cb.setTreeId(oi.insert(Constants.OBJ_TREE, new byte[] {})); + cb.setCommitter(serverIdent); + cb.setAuthor(cb.getCommitter()); + cb.setMessage("Initial empty repository"); + + ObjectId id = oi.insert(cb); + oi.flush(); + + RefUpdate ru = repo.updateRef(Constants.HEAD); + ru.setNewObjectId(id); + final Result result = ru.update(); + switch (result) { + case NEW: + rq.scheduleUpdate(project, ref); + break; + default: { + throw new IOException(result.name()); + } } + } catch (IOException e) { + log.error("Cannot create empty commit for " + projectName, e); + throw e; + } finally { + oi.release(); } - return false; } private void createProject() throws OrmException { @@ -166,6 +217,8 @@ final class CreateProject extends BaseCommand { newProject.setSubmitType(submitType); newProject.setUseContributorAgreements(contributorAgreements); newProject.setUseSignedOffBy(signedOffBy); + newProject.setUseContentMerge(contentMerge); + newProject.setRequireChangeID(requireChangeID); if (newParent != null) { newProject.setParent(newParent.getProject().getNameKey()); } @@ -179,7 +232,7 @@ final class CreateProject extends BaseCommand { projectName.substring(0, projectName.length() - ".git".length()); } - if (!isAnyIncludedIn(currentUser.getEffectiveGroups(), projectCreatorGroups)) { + if (!CollectionsUtil.isAnyIncludedIn(currentUser.getEffectiveGroups(), projectCreatorGroups)) { throw new Failure(1, "fatal: Not permitted to create " + projectName); } diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Query.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Query.java index d2bf26cb4e..fecdc59e6d 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Query.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Query.java @@ -43,6 +43,14 @@ class Query extends BaseCommand { processor.setIncludePatchSets(on); } + @Option(name = "--all-approvals", usage = "Include information about all patch sets and approvals") + void setApprovals(boolean on) { + if (on) { + processor.setIncludePatchSets(on); + } + processor.setIncludeApprovals(on); + } + @Argument(index = 0, required = true, multiValued = true, metaVar = "QUERY", usage = "Query to execute") private List<String> query; diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Receive.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Receive.java index eb5d1da68b..1996ce466a 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Receive.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Receive.java @@ -17,16 +17,23 @@ package com.google.gerrit.sshd.commands; import com.google.gerrit.reviewdb.Account; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.git.ReceiveCommits; +import com.google.gerrit.server.git.TransferConfig; +import com.google.gerrit.server.git.VisibleRefFilter; import com.google.gerrit.sshd.AbstractGitCommand; -import com.google.gerrit.sshd.TransferConfig; import com.google.inject.Inject; +import org.eclipse.jgit.errors.UnpackException; +import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.transport.ReceivePack; +import org.eclipse.jgit.transport.RefFilter; import org.kohsuke.args4j.Option; import java.io.IOException; import java.io.InterruptedIOException; +import java.util.ArrayList; import java.util.HashSet; +import java.util.List; +import java.util.Map; import java.util.Set; /** Receives change upload over SSH using the Git receive-pack protocol. */ @@ -58,6 +65,10 @@ final class Receive extends AbstractGitCommand { @Override protected void runImpl() throws IOException, Failure { + if (!projectControl.canRunReceivePack()) { + throw new Failure(1, "fatal: receive-pack not permitted on this server"); + } + final ReceiveCommits receive = factory.create(projectControl, repo); ReceiveCommits.Capable r = receive.canUpload(); @@ -78,6 +89,49 @@ final class Receive extends AbstractGitCommand { rp.receive(in, out, err); } catch (InterruptedIOException err) { throw new Failure(128, "fatal: client IO read/write timeout", err); + + } catch (UnpackException badStream) { + // This may have been triggered by branch level access controls. + // Log what the heck is going on, as detailed as we can. + // + StringBuilder msg = new StringBuilder(); + msg.append("Unpack error on project \"" + + projectControl.getProject().getName() + "\":\n"); + + msg.append(" RefFilter: " + rp.getRefFilter()); + if (rp.getRefFilter() == RefFilter.DEFAULT) { + msg.append("DEFAULT"); + } else if (rp.getRefFilter() instanceof VisibleRefFilter) { + msg.append("VisibleRefFilter"); + } else { + msg.append(rp.getRefFilter().getClass()); + } + msg.append("\n"); + + if (rp.getRefFilter() instanceof VisibleRefFilter) { + Map<String, Ref> adv = rp.getAdvertisedRefs(); + msg.append(" Visible references (" + adv.size() + "):\n"); + for (Ref ref : adv.values()) { + msg.append(" - " + ref.getObjectId().abbreviate(8).name() + " " + + ref.getName() + "\n"); + } + + List<Ref> hidden = new ArrayList<Ref>(); + for (Ref ref : rp.getRepository().getAllRefs().values()) { + if (!adv.containsKey(ref.getName())) { + hidden.add(ref); + } + } + + msg.append(" Hidden references (" + hidden.size() + "):\n"); + for (Ref ref : hidden) { + msg.append(" - " + ref.getObjectId().abbreviate(8).name() + " " + + ref.getName() + "\n"); + } + } + + IOException detail = new IOException(msg.toString(), badStream); + throw new Failure(128, "fatal: Unpack error, check server log", detail); } } diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Upload.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Upload.java index eca797d54b..bcc9d19d32 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Upload.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Upload.java @@ -15,9 +15,9 @@ package com.google.gerrit.sshd.commands; import com.google.gerrit.reviewdb.ReviewDb; +import com.google.gerrit.server.git.TransferConfig; import com.google.gerrit.server.git.VisibleRefFilter; import com.google.gerrit.sshd.AbstractGitCommand; -import com.google.gerrit.sshd.TransferConfig; import com.google.inject.Inject; import com.google.inject.Provider; @@ -36,10 +36,15 @@ final class Upload extends AbstractGitCommand { @Override protected void runImpl() throws IOException, Failure { + if (!projectControl.canRunUploadPack()) { + throw new Failure(1, "fatal: upload-pack not permitted on this server"); + } + final UploadPack up = new UploadPack(repo); if (!projectControl.allRefsAreVisible()) { up.setRefFilter(new VisibleRefFilter(repo, projectControl, db.get())); } + up.setPackConfig(config.getPackConfig()); up.setTimeout(config.getTimeout()); try { up.upload(in, out, err); diff --git a/gerrit-util-cli/pom.xml b/gerrit-util-cli/pom.xml index 20f28e10f0..6db5241a94 100644 --- a/gerrit-util-cli/pom.xml +++ b/gerrit-util-cli/pom.xml @@ -22,7 +22,7 @@ limitations under the License. <parent> <groupId>com.google.gerrit</groupId> <artifactId>gerrit-parent</artifactId> - <version>2.1.4-SNAPSHOT</version> + <version>2.1-SNAPSHOT</version> </parent> <artifactId>gerrit-util-cli</artifactId> diff --git a/gerrit-util-ssl/pom.xml b/gerrit-util-ssl/pom.xml index 220ae47978..39d3ce09d9 100644 --- a/gerrit-util-ssl/pom.xml +++ b/gerrit-util-ssl/pom.xml @@ -22,7 +22,7 @@ limitations under the License. <parent> <groupId>com.google.gerrit</groupId> <artifactId>gerrit-parent</artifactId> - <version>2.1.4-SNAPSHOT</version> + <version>2.1-SNAPSHOT</version> </parent> <artifactId>gerrit-util-ssl</artifactId> diff --git a/gerrit-war/pom.xml b/gerrit-war/pom.xml index 4bfa17e266..87cfaa5f54 100644 --- a/gerrit-war/pom.xml +++ b/gerrit-war/pom.xml @@ -22,7 +22,7 @@ limitations under the License. <parent> <groupId>com.google.gerrit</groupId> <artifactId>gerrit-parent</artifactId> - <version>2.1.4-SNAPSHOT</version> + <version>2.1-SNAPSHOT</version> </parent> <artifactId>gerrit-war</artifactId> diff --git a/gerrit-war/src/main/resources/log4j.properties b/gerrit-war/src/main/resources/log4j.properties index f7a5cdcf1b..5993790122 100644 --- a/gerrit-war/src/main/resources/log4j.properties +++ b/gerrit-war/src/main/resources/log4j.properties @@ -57,3 +57,7 @@ log4j.logger.net.sf.ehcache=WARN log4j.logger.com.mchange.v2.c3p0=WARN log4j.logger.com.mchange.v2.resourcepool=WARN log4j.logger.com.mchange.v2.sql=WARN + +# Silence non-critical messages from Velocity +# +log4j.logger.velocity=WARN @@ -22,7 +22,7 @@ limitations under the License. <groupId>com.google.gerrit</groupId> <artifactId>gerrit-parent</artifactId> <packaging>pom</packaging> - <version>2.1.4-SNAPSHOT</version> + <version>2.1-SNAPSHOT</version> <name>Gerrit Code Review - Parent</name> <url>http://code.google.com/p/gerrit/</url> @@ -46,7 +46,7 @@ limitations under the License. </issueManagement> <properties> - <jgitVersion>0.8.4.89-ge2f5716</jgitVersion> + <jgitVersion>0.9.3.133-gaa09599</jgitVersion> <gwtormVersion>1.1.4</gwtormVersion> <gwtjsonrpcVersion>1.2.2</gwtjsonrpcVersion> <gwtexpuiVersion>1.2.2</gwtexpuiVersion> @@ -478,7 +478,13 @@ limitations under the License. <dependency> <groupId>org.apache.sshd</groupId> <artifactId>sshd-core</artifactId> - <version>0.4.0-r897374</version> + <version>0.5.1-r1031886</version> + </dependency> + + <dependency> + <groupId>org.apache.velocity</groupId> + <artifactId>velocity</artifactId> + <version>1.6.4</version> </dependency> <dependency> |