Merge "user-review-ui: make colon usage a little more consistent"
diff --git a/.gitignore b/.gitignore
index 53bc9f6..0bbcaba 100644
--- a/.gitignore
+++ b/.gitignore
@@ -31,6 +31,8 @@
/infer-out
/local.properties
/node_modules/
+/polygerrit-ui/node_modules/
+/polygerrit-ui/app/node_modules/
/package-lock.json
/plugins/*
/polygerrit-ui/coverage/
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt
index 3da69df..185fa07 100644
--- a/Documentation/access-control.txt
+++ b/Documentation/access-control.txt
@@ -800,6 +800,22 @@
Users without this access right who are able to upload changes can
still do the revert locally and upload the revert commit as a new change.
+[[category_remove_label]]
+=== Remove Label (Remove Vote)
+
+For every configured label `My-Name` in the project, there is a
+corresponding permission `removeLabel-My-Name` with a range corresponding to
+the defined values. For these values, the users are permitted to remove
+other users' votes from a change.
+
+Change owners can always remove zero or positive votes (even without
+having the `Remove Vote` access right assigned).
+
+Project owners and site administrators can always remove any vote (even
+without having the `Remove Vote` access right assigned).
+
+Users without this access right can still remove their own votes.
+
[[category_remove_reviewer]]
=== Remove Reviewer
@@ -890,6 +906,9 @@
the Work In Progress bit of the change (even without having the
`Toggle Work In Progress state` access right assigned).
+Must be assigned on the target branch ref (i.e. on 'refs/heads/*', not on
+'refs/for/*').
+
[[category_delete_own_changes]]
=== Delete Own Changes
@@ -1351,10 +1370,11 @@
[[capability_createProject]]
=== Create Project
-Allow project creation. This capability allows the granted group to
-either link:cmd-create-project.html[create new git projects via ssh]
-or via the web UI.
+Allow project creation.
+This capability allows the granted group to create projects via the web UI, via
+link:rest-api-projects.html#create-project][REST] and via
+link:cmd-create-project.html[SSH].
[[capability_emailReviewers]]
=== Email Reviewers
diff --git a/Documentation/config-submit-requirements.txt b/Documentation/config-submit-requirements.txt
index 601f2bf..fb37287 100644
--- a/Documentation/config-submit-requirements.txt
+++ b/Documentation/config-submit-requirements.txt
@@ -282,6 +282,22 @@
canOverrideInChildProjects = true
----
+Branch configuration supports regular expressions as well, e.g. to exempt 'refs/heads/release/*' pattern,
+when migrating from the label Submit-Rule:
+
+----
+[label "Verified"]
+ branch = refs/heads/release/*
+----
+
+The following SR can be configured:
+
+----
+[submit-requirement "Verified"]
+ submittableIf = label:Verified=MAX AND -label:Verified=MIN
+ applicableIf = branch:^refs/heads/release/.*
+----
+
[[test-submit-requirements]]
== Testing Submit Requirements
diff --git a/Documentation/intro-user.txt b/Documentation/intro-user.txt
index 264ce73..7a042ba 100644
--- a/Documentation/intro-user.txt
+++ b/Documentation/intro-user.txt
@@ -1014,7 +1014,7 @@
inline comment ("Yeah, I see why, let me try again.").
[[security-fixes]]
--- Security Fixes
+== Security Fixes
If a security vulnerability is discovered you normally want to have an
embargo about it until fixed releases have been made available. This
diff --git a/Documentation/pg-plugin-dev.txt b/Documentation/pg-plugin-dev.txt
index 3f7d90d..d7343c2 100644
--- a/Documentation/pg-plugin-dev.txt
+++ b/Documentation/pg-plugin-dev.txt
@@ -98,20 +98,29 @@
See link:https://gerrit.googlesource.com/gerrit/+/master/polygerrit-ui/app/styles/themes/[app-theme.ts]
for the list of available variables.
-Just add code like this to your JavaScript plugin:
+You can just create `<style>` elements yourself and add them to the
+`document.head`, but for your convenience the Plugin API provides a simple
+`styleApi().insertCSSRule()` method for doing just that. Typically you would
+define a CSS rule for `html`, which is always applied, or for a specific theme
+such as `html.lightTheme`.
``` js
Gerrit.install(plugin => {
- const styleEl = document.createElement('style');
- styleEl.innerHTML = `
- html {
- --header-background-color: #c3d9ff;
- }
- html.darkTheme {
- --header-background-color: #c3d9ff90;
- }
- `;
- document.head.appendChild(styleEl);
+ plugin.styleApi().insertCSSRule(`
+ html {
+ --header-text-color: black;
+ }
+ `);
+ plugin.styleApi().insertCSSRule(`
+ html.lightTheme {
+ --header-background-color: red;
+ }
+ `);
+ plugin.styleApi().insertCSSRule(`
+ html.darkTheme {
+ --header-background-color: blue;
+ }
+ `);
});
```
@@ -154,12 +163,6 @@
See link:pg-plugin-endpoints.html[endpoints].
-=== registerStyleModule
-`plugin.registerStyleModule(endpointName, moduleName)`
-
-This API is deprecated and will be removed either in version 3.6 or 3.7,
-see link:#low-level-style[above] for an alternative.
-
=== on
Register a JavaScript callback to be invoked when events occur within
the web interface. Signature
diff --git a/Documentation/pg-plugin-endpoints.txt b/Documentation/pg-plugin-endpoints.txt
index 41f544d..dd82f27 100644
--- a/Documentation/pg-plugin-endpoints.txt
+++ b/Documentation/pg-plugin-endpoints.txt
@@ -132,6 +132,10 @@
=== settings-screen
This endpoint is situated at the end of the body of the settings screen.
+=== profile
+This endpoint is situated at the top of the Profile section of the settings
+screen below the section description text.
+
=== reply-text
This endpoint wraps the textarea in the reply dialog.
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 88018a6..2f144c6 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -103,15 +103,17 @@
"id": "demo~master~Idaf5e098d70898b7119f6f4af5a6c13343d64b57",
"project": "demo",
"branch": "master",
- "attention_set": [
- {
+ "attention_set": {
+ "1000096": {
"account": {
- "name": "John Doe"
+ "_account_id": 1000096,
+ "name": "John Doe",
+ "email": "john.doe@example.com"
},
- "last_update": "2012-07-17 07:19:27.766000000",
- "reason": "reviewer or cc replied"
+ "last_update": "2012-07-17 07:19:27.766000000",
+ "reason": "reviewer or cc replied"
}
- ]
+ },
"change_id": "Idaf5e098d70898b7119f6f4af5a6c13343d64b57",
"subject": "One change",
"status": "NEW",
@@ -545,15 +547,17 @@
"id": "myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940",
"project": "myProject",
"branch": "master",
- "attention_set": [
- {
+ "attention_set": {
+ "1000096": {
"account": {
- "name": "John Doe"
+ "_account_id": 1000096,
+ "name": "John Doe",
+ "email": "john.doe@example.com"
},
- "last_update": "2013-02-21 11:16:36.775000000",
- "reason": "reviewer or cc replied"
+ "last_update": "2013-02-21 11:16:36.775000000",
+ "reason": "reviewer or cc replied"
}
- ]
+ },
"change_id": "I8473b95934b5732ac55d26311a706c9c2bde9940",
"subject": "Implementing Feature X",
"status": "NEW",
@@ -612,15 +616,17 @@
)]}'
{
"added": {
- "attention_set": [
- {
+ "attention_set": {
+ "1000096": {
"account": {
- "name": "John Doe"
+ "_account_id": 1000096,
+ "name": "John Doe",
+ "email": "john.doe@example.com"
},
- "last_update": "2013-02-21 11:16:36.775000000",
- "reason": "reviewer or cc replied"
+ "last_update": "2013-02-21 11:16:36.775000000",
+ "reason": "reviewer or cc replied"
}
- ]
+ },
"updated": "2013-02-21 11:16:36.775000000",
"topic": "new-topic"
},
@@ -651,15 +657,17 @@
"id": "myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940",
"project": "myProject",
"branch": "master",
- "attention_set": [
- {
+ "attention_set": {
+ "1000096": {
"account": {
- "name": "John Doe"
+ "_account_id": 1000096,
+ "name": "John Doe",
+ "email": "john.doe@example.com"
},
- "last_update": "2013-02-21 11:16:36.775000000",
- "reason": "reviewer or cc replied"
+ "last_update": "2013-02-21 11:16:36.775000000",
+ "reason": "reviewer or cc replied"
}
- ],
+ },
"change_id": "I8473b95934b5732ac55d26311a706c9c2bde9940",
"subject": "Implementing Feature X",
"status": "NEW",
@@ -719,18 +727,18 @@
"id": "myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940",
"project": "myProject",
"branch": "master",
- "attention_set": [
- {
+ "attention_set": {
+ "1000096": {
"account": {
"_account_id": 1000096,
"name": "John Doe",
"email": "john.doe@example.com",
"username": "jdoe"
},
- "last_update": "2013-02-21 11:16:36.775000000",
- "reason": "reviewer or cc replied"
+ "last_update": "2013-02-21 11:16:36.775000000",
+ "reason": "reviewer or cc replied"
}
- ]
+ },
"change_id": "I8473b95934b5732ac55d26311a706c9c2bde9940",
"subject": "Implementing Feature X",
"status": "NEW",
@@ -816,6 +824,26 @@
"+2"
]
},
+ "removable_labels": {
+ "Code-Review": {
+ "-1": [
+ {
+ "_account_id": 1000096,
+ "name": "John Doe",
+ "email": "john.doe@example.com",
+ "username": "jdoe"
+ }
+ ],
+ "+1": [
+ {
+ "_account_id": 1000097,
+ "name": "Jane Roe",
+ "email": "jane.roe@example.com",
+ "username": "jroe"
+ }
+ ]
+ }
+ },
"removable_reviewers": [
{
"_account_id": 1000096,
@@ -1505,6 +1533,223 @@
The change could not be rebased due to a path conflict during merge.
----
+[[rebase-chain]]
+=== Rebase Chain
+--
+'POST /changes/link:#change-id[\{change-id\}]/rebase:chain'
+--
+
+Rebases an ancestry chain of changes.
+
+The operated change is treated as the chain tip. All unsubmitted ancestors are rebased.
+
+Requires a linear ancestry relation (single parenting throughout the chain).
+
+Optionally, the parent revision (of the oldest ancestor to be rebased) can be changed to another
+change, revision or branch through the link:#rebase-input[RebaseInput] entity.
+
+If the chain is outdated, i.e., there's a change that depends on an old revision of its parent, the
+result is the same as individually rebasing all outdated changes on top of their parent's latest
+revision before running the rebase chain action.
+
+.Request
+----
+ POST /changes/myProject~master~I08a021fb07b83fe845140a2c11508b3bdd93b48f/rebase:chain HTTP/1.0
+ Content-Type: application/json;charset=UTF-8
+
+ {
+ "base" : "1234",
+ }
+----
+
+As response a link:#rebase-chain-info[RebaseChainInfo] entity is returned that
+describes the rebased changes. Information about the current patch sets
+are included.
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json; charset=UTF-8
+
+ )]}'
+ {
+ "rebased_changes": [
+ {
+ "id": "myProject~master~I0e534de9d7f0d6f35b71f7d726acf835b2110c66",
+ "project": "myProject",
+ "branch": "master",
+ "hashtags": [
+
+ ],
+ "change_id": "I0e534de9d7f0d6f35b71f7d726acf835b2110c66",
+ "subject": "456",
+ "status": "NEW",
+ "created": "2022-11-21 20: 51: 31.000000000",
+ "updated": "2022-11-21 20: 56: 49.000000000",
+ "submit_type": "MERGE_IF_NECESSARY",
+ "insertions": 0,
+ "deletions": 0,
+ "total_comment_count": 0,
+ "unresolved_comment_count": 0,
+ "has_review_started": true,
+ "meta_rev_id": "a2a6692213f546e1045ecf4647439fac8d6d8faa",
+ "_number": 21,
+ "owner": {
+ "_account_id": 1000000
+ },
+ "current_revision": "c3b2ba222d42a56e05c90f88d4509a124620517d",
+ "revisions": {
+ "c3b2ba222d42a56e05c90f88d4509a124620517d": {
+ "kind": "NO_CHANGE",
+ "_number": 2,
+ "created": "2022-11-21 20: 56: 49.000000000",
+ "uploader": {
+ "_account_id": 1000000
+ },
+ "ref": "refs/changes/21/21/2",
+ "fetch": {
+
+ },
+ "commit": {
+ "parents": [
+ {
+ "commit": "7803f427dd7c4a2441466e4d740a1850dcee1af4",
+ "subject": "123"
+ }
+ ],
+ "author": {
+ "name": "Nitzan Gur-Furman",
+ "email": "nitzan@google.com",
+ "date": "2022-11-21 20: 49: 39.000000000",
+ "tz": 60
+ },
+ "committer": {
+ "name": "Administrator",
+ "email": "admin@example.com",
+ "date": "2022-11-21 20: 56: 49.000000000",
+ "tz": 60
+ },
+ "subject": "456",
+ "message": "456\n"
+ },
+ "description": "Rebase"
+ }
+ },
+ "requirements": [
+
+ ],
+ "submit_records": [
+ {
+ "rule_name": "gerrit~DefaultSubmitRule",
+ "status": "NOT_READY",
+ "labels": [
+ {
+ "label": "Code-Review",
+ "status": "NEED"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "id": "myProject~master~I08a021fb07b83fe845140a2c11508b3bdd93b48f",
+ "project": "myProject",
+ "branch": "master",
+ "hashtags": [
+
+ ],
+ "change_id": "I08a021fb07b83fe845140a2c11508b3bdd93b48f",
+ "subject": "789",
+ "status": "NEW",
+ "created": "2022-11-21 20: 51: 31.000000000",
+ "updated": "2022-11-21 20: 56: 49.000000000",
+ "submit_type": "MERGE_IF_NECESSARY",
+ "insertions": 0,
+ "deletions": 0,
+ "total_comment_count": 0,
+ "unresolved_comment_count": 0,
+ "has_review_started": true,
+ "meta_rev_id": "3bfb843fea471f96e16b9199c3a30fff0285bc45",
+ "_number": 22,
+ "owner": {
+ "_account_id": 1000000
+ },
+ "current_revision": "77eb17a9501a5c21963bc6af56085e60f281acbb",
+ "revisions": {
+ "77eb17a9501a5c21963bc6af56085e60f281acbb": {
+ "kind": "NO_CHANGE",
+ "_number": 2,
+ "created": "2022-11-21 20: 56: 49.000000000",
+ "uploader": {
+ "_account_id": 1000000
+ },
+ "ref": "refs/changes/22/22/2",
+ "fetch": {
+
+ },
+ "commit": {
+ "parents": [
+ {
+ "commit": "c3b2ba222d42a56e05c90f88d4509a124620517d",
+ "subject": "456"
+ }
+ ],
+ "author": {
+ "name": "Nitzan Gur-Furman",
+ "email": "nitzan@google.com",
+ "date": "2022-11-21 20: 51: 07.000000000",
+ "tz": 60
+ },
+ "committer": {
+ "name": "Administrator",
+ "email": "admin@example.com",
+ "date": "2022-11-21 20: 56: 49.000000000",
+ "tz": 60
+ },
+ "subject": "789",
+ "message": "789\n"
+ },
+ "description": "Rebase"
+ }
+ },
+ "requirements": [
+
+ ],
+ "submit_records": [
+ {
+ "rule_name": "gerrit~DefaultSubmitRule",
+ "status": "NOT_READY",
+ "labels": [
+ {
+ "label": "Code-Review",
+ "status": "NEED"
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ }
+----
+
+If the change cannot be rebased, e.g. due to conflicts, the response is
+"`409 Conflict`" and the error message is contained in the response
+body.
+
+.Response
+----
+ HTTP/1.1 409 Conflict
+ Content-Disposition: attachment
+ Content-Type: text/plain; charset=UTF-8
+
+ Change I0e534de9d7f0d6f35b71f7d726acf835b2110c66 could not be rebased due to a conflict during
+ merge.
+
+ merge conflict(s):
+ a.txt
+----
+
[[move-change]]
=== Move Change
--
@@ -6520,7 +6765,7 @@
* a commit ID ("674ac754f91e64a0efb8087e59a176484bd534d1")
* an abbreviated commit ID that uniquely identifies one revision of the
change ("674ac754"), at least 4 digits are required
-* a legacy numeric patch number ("1" for first patch set of the change)
+* a numeric patch number ("1" for first patch set of the change)
* "0" or the literal `edit` for a change edit
[[json-entities]]
@@ -6846,6 +7091,13 @@
A map of the permitted labels that maps a label name to the list of
values that are allowed for that label. +
Only set if link:#detailed-labels[detailed labels] are requested.
+|`removable_labels` |optional|
+A map of the removable labels that maps a label name to the map of
+values and reviewers (
+link:rest-api-accounts.html#account-info[AccountInfo] entities)
+that are allowed to be removed from the change. +
+Only set if link:#labels[labels] or
+link:#detailed-labels[detailed labels] are requested.
|`removable_reviewers`|optional|
The reviewers that can be removed by the calling user as a list of
link:rest-api-accounts.html#account-info[AccountInfo] entities. +
@@ -6964,11 +7216,16 @@
Whether the new change should be set to work in progress.
|`base_change` |optional|
A link:#change-id[\{change-id\}] that identifies the base change for a create
-change operation. Mutually exclusive with `base_commit`.
+change operation. +
+Mutually exclusive with `base_commit`. +
+If neither `base_commit` nor `base_change` are set, the target branch tip will
+be used as the parent commit.
|`base_commit` |optional|
A 40-digit hex SHA-1 of the commit which will be the parent commit of the newly
-created change. If set, it must be a merged commit on the destination branch.
-Mutually exclusive with `base_change`.
+created change. If set, it must be a merged commit on the destination branch. +
+Mutually exclusive with `base_change`. +
+If neither `base_commit` nor `base_change` are set, the target branch tip will
+be used as the parent commit.
|`new_branch` |optional, default to `false`|
Allow creating a new branch when set to `true`. Using this option is
only possible for non-merge commits (if the `merge` field is not set).
@@ -7987,6 +8244,22 @@
options. Unknown validation options are silently ignored.
|===========================
+[[rebase-chain-info]]
+=== RebaseChainInfo
+
+The `RebaseChainInfo` entity contains information about a chain of changes
+that were rebased.
+
+[options="header",cols="1,^1,5"]
+|===========================
+|Field Name ||Description
+|`rebased_changes` ||List of the unsubmitted ancestors, as link:#change-info[ChangeInfo]
+entities. Includes both rebased changes, and previously up-to-date ancestors. The list is ordered by
+ancestry, where the oldest ancestor is the first.
+|`contains_git_conflicts` ||Whether any of the rebased changes has conflicts
+due to rebasing.
+|===========================
+
[[related-change-and-commit-info]]
=== RelatedChangeAndCommitInfo
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index 9527a56..e12c27c 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -43,6 +43,17 @@
For more predictable results, use explicit search operators as described
in the following section.
+[IMPORTANT]
+--
+The change search API is backed by a secondary index and might sometimes return
+stale results if the re-indexing operation failed for a change update.
+
+Please also note that changes are not re-indexed if the project configuration
+is updated with newly added or modified
+link:config-submit-requirements.html[submit requirements].
+--
+
+
[[search-operators]]
== Search Operators
@@ -347,6 +358,18 @@
regular expressions is limited to the first 32766 bytes of the
commit message due to limitations in Lucene.
+[[subject]]
+subject:'SUBJECT'::
++
+Changes that have a commit message where the first line (aka the subject)
+matches 'SUBJECT'. The matching is done by full text search over the subject.
+
+[[prefixsubject]]
+prefixsubject:'PREFIX'::
++
+Changes that have a commit message where the first line (aka the subject)
+has the prefix 'PREFIX'.
+
[[comment]]
comment:'TEXT'::
+
diff --git a/contrib/git-gc-preserve b/contrib/git-gc-preserve
old mode 100644
new mode 100755
index 54b8fca..a886721
--- a/contrib/git-gc-preserve
+++ b/contrib/git-gc-preserve
@@ -13,25 +13,11 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-usage() { # error_message
+usage() { # exit code
cat <<-EOF
NAME
git-gc-preserve - Run git gc and preserve old packs to avoid races for JGit
- This command uses custom git config options to configure if preserved packs
- from the last run of git gc should be pruned and if packs should be preserved.
-
- This is similar to the implementation in JGit [1] which is used by
- JGit to avoid errors [2] in such situations.
-
- Don't run multiple instances of this command concurrently on the same
- repository since it does not attempt to implement the file locking
- which git gc --auto does [3].
-
- [1] https://git.eclipse.org/r/c/jgit/jgit/+/87969
- [2] https://git.eclipse.org/r/c/jgit/jgit/+/122288
- [3] https://github.com/git/git/commit/64a99eb4760de2ce2f0c04e146c0a55c34f50f20
-
SYNOPSIS
git gc-preserve
@@ -39,6 +25,29 @@
Runs git gc and can preserve old packs to avoid races with concurrently
executed commands in JGit.
+ This command uses custom git config options to configure if preserved packs
+ from the last run of git gc should be pruned and if packs should be preserved.
+
+ This is similar to the implementation in JGit [1] which is used by
+ JGit to avoid errors [2] in such situations.
+
+ The command prevents concurrent runs of the command on the same repository
+ by acquiring an exclusive file lock on the file
+ "\$repopath/gc-preserve.pid"
+ If it cannot acquire the lock it fails immediately with exit code 3.
+
+ Failure Exit Codes
+ 1: General failure
+ 2: Couldn't determine repository path. If the current working directory
+ is outside of the working tree of the git repository use git option
+ --git-dir to pass the root path of the repository.
+ E.g.
+ $ git --git-dir ~/git/foo gc-preserve
+ 3: Another process already runs $0 on the same repository
+
+ [1] https://git.eclipse.org/r/c/jgit/jgit/+/87969
+ [2] https://git.eclipse.org/r/c/jgit/jgit/+/122288
+
CONFIGURATION
"gc.prunepreserved": if set to "true" preserved packs from the last gc run
are pruned before current packs are preserved.
@@ -49,7 +58,30 @@
across missing objects which might be caused by a concurrent run of
git gc.
EOF
- exit
+ exit "$1"
+}
+
+# acquire file lock, unlock when the script exits
+lock() { # repo
+ readonly LOCKFILE="$1/gc-preserve.pid"
+ test -f "$LOCKFILE" || touch "$LOCKFILE"
+ exec 9> "$LOCKFILE"
+ if flock -nx 9; then
+ echo -n "$$ $USERNAME@$HOSTNAME" >&9
+ trap unlock EXIT
+ else
+ echo "$0 is already running"
+ exit 3
+ fi
+}
+
+unlock() {
+ # only delete if the file descriptor 9 is open
+ if { : >&9 ; } &> /dev/null; then
+ rm -f "$LOCKFILE"
+ fi
+ # close the file handle to release file lock
+ exec 9>&-
}
# prune preserved packs if gc.prunepreserved == true
@@ -74,7 +106,7 @@
return 0
fi
local packdir=$1/objects/pack
- pushd "$packdir" >/dev/null || exit
+ pushd "$packdir" >/dev/null || exit 1
mkdir -p preserved
printf "Preserving packs: "
count=0
@@ -85,7 +117,7 @@
fi
done
echo "$count, done."
- popd >/dev/null || exit
+ popd >/dev/null || exit 1
}
# pack-0...2.pack to pack-0...2.old-pack
@@ -100,7 +132,7 @@
while [ $# -gt 0 ] ; do
case "$1" in
- -u|-h) usage ;;
+ -u|-h) usage 0 ;;
esac
shift
done
@@ -108,10 +140,10 @@
repopath=$(git rev-parse --git-dir)
if [ -z "$repopath" ]; then
- usage
- exit $?
+ usage 2
fi
+lock "$repopath"
prune_preserved "$repopath"
preserve_packs "$repopath"
-git gc ${args:+"$args"}
+git gc ${args:+"$args"} || { echo "git gc failed"; exit "$?"; }
diff --git a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index 5b4a9e5..ccf74d1 100644
--- a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -1393,6 +1393,17 @@
}
}
+ protected void assertOnlyRemovableLabel(
+ ChangeInfo info, String labelId, String labelValue, TestAccount reviewer) {
+ assertThat(info.removableLabels).hasSize(1);
+ assertThat(info.removableLabels).containsKey(labelId);
+ assertThat(info.removableLabels.get(labelId)).hasSize(1);
+ assertThat(info.removableLabels.get(labelId)).containsKey(labelValue);
+ assertThat(info.removableLabels.get(labelId).get(labelValue)).hasSize(1);
+ assertThat(info.removableLabels.get(labelId).get(labelValue).get(0).email)
+ .isEqualTo(reviewer.email());
+ }
+
protected void assertPermissions(
Project.NameKey project,
GroupReference groupReference,
diff --git a/java/com/google/gerrit/acceptance/AccountCreator.java b/java/com/google/gerrit/acceptance/AccountCreator.java
index c67991d..ff5bc00 100644
--- a/java/com/google/gerrit/acceptance/AccountCreator.java
+++ b/java/com/google/gerrit/acceptance/AccountCreator.java
@@ -141,6 +141,10 @@
return create(username, null, username, null, (String[]) null);
}
+ public TestAccount createValid(String username) throws Exception {
+ return create(username, username + "@example.com", username, username);
+ }
+
public TestAccount admin() throws Exception {
return create("admin", "admin@example.com", "Administrator", "Adminny", "Administrators");
}
diff --git a/java/com/google/gerrit/acceptance/DisabledAccountIndex.java b/java/com/google/gerrit/acceptance/DisabledAccountIndex.java
index 7bd0c73..7660948 100644
--- a/java/com/google/gerrit/acceptance/DisabledAccountIndex.java
+++ b/java/com/google/gerrit/acceptance/DisabledAccountIndex.java
@@ -55,6 +55,11 @@
}
@Override
+ public void deleteByValue(AccountState value) {
+ throw new UnsupportedOperationException("AccountIndex is disabled");
+ }
+
+ @Override
public void delete(Account.Id key) {
throw new UnsupportedOperationException("AccountIndex is disabled");
}
diff --git a/java/com/google/gerrit/acceptance/DisabledChangeIndex.java b/java/com/google/gerrit/acceptance/DisabledChangeIndex.java
index 7671ad4..c028a8e 100644
--- a/java/com/google/gerrit/acceptance/DisabledChangeIndex.java
+++ b/java/com/google/gerrit/acceptance/DisabledChangeIndex.java
@@ -62,6 +62,11 @@
}
@Override
+ public void deleteByValue(ChangeData value) {
+ throw new UnsupportedOperationException("ChangeIndex is disabled");
+ }
+
+ @Override
public void delete(Change.Id key) {
throw new UnsupportedOperationException("ChangeIndex is disabled");
}
diff --git a/java/com/google/gerrit/acceptance/DisabledProjectIndex.java b/java/com/google/gerrit/acceptance/DisabledProjectIndex.java
index 2e3dd90..f2aad4a 100644
--- a/java/com/google/gerrit/acceptance/DisabledProjectIndex.java
+++ b/java/com/google/gerrit/acceptance/DisabledProjectIndex.java
@@ -60,6 +60,11 @@
}
@Override
+ public void deleteByValue(ProjectData value) {
+ throw new UnsupportedOperationException("ProjectIndex is disabled");
+ }
+
+ @Override
public void delete(Project.NameKey key) {
throw new UnsupportedOperationException("ProjectIndex is disabled");
}
diff --git a/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java
index b1cd506..69139ce 100644
--- a/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java
+++ b/java/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImpl.java
@@ -197,8 +197,13 @@
PermissionRule.Builder rule = newRule(projectConfig, p.group());
rule.setAction(p.action());
rule.setRange(p.min(), p.max());
- String permissionName =
- p.impersonation() ? Permission.forLabelAs(p.name()) : Permission.forLabel(p.name());
+ String permissionName;
+ if (p.isAddPermission()) {
+ permissionName =
+ p.impersonation() ? Permission.forLabelAs(p.name()) : Permission.forLabel(p.name());
+ } else {
+ permissionName = Permission.forRemoveLabel(p.name());
+ }
projectConfig.upsertAccessSection(
p.ref(), as -> as.upsertPermission(permissionName).add(rule));
}
diff --git a/java/com/google/gerrit/acceptance/testsuite/project/TestProjectUpdate.java b/java/com/google/gerrit/acceptance/testsuite/project/TestProjectUpdate.java
index 9a9a21a..5634c78 100644
--- a/java/com/google/gerrit/acceptance/testsuite/project/TestProjectUpdate.java
+++ b/java/com/google/gerrit/acceptance/testsuite/project/TestProjectUpdate.java
@@ -162,12 +162,34 @@
/** Starts a builder for allowing a label permission. */
public static TestLabelPermission.Builder allowLabel(String name) {
- return TestLabelPermission.builder().name(name).action(PermissionRule.Action.ALLOW);
+ return TestLabelPermission.builder()
+ .name(name)
+ .isAddPermission(true)
+ .action(PermissionRule.Action.ALLOW);
}
/** Starts a builder for denying a label permission. */
public static TestLabelPermission.Builder blockLabel(String name) {
- return TestLabelPermission.builder().name(name).action(PermissionRule.Action.BLOCK);
+ return TestLabelPermission.builder()
+ .name(name)
+ .isAddPermission(true)
+ .action(PermissionRule.Action.BLOCK);
+ }
+
+ /** Starts a builder for allowing a remove-label permission. */
+ public static TestLabelPermission.Builder allowLabelRemoval(String name) {
+ return TestLabelPermission.builder()
+ .name(name)
+ .isAddPermission(false)
+ .action(PermissionRule.Action.ALLOW);
+ }
+
+ /** Starts a builder for denying a remove-label permission. */
+ public static TestLabelPermission.Builder blockLabelRemoval(String name) {
+ return TestLabelPermission.builder()
+ .name(name)
+ .isAddPermission(false)
+ .action(PermissionRule.Action.BLOCK);
}
/** Records a label permission to be updated. */
@@ -191,6 +213,8 @@
abstract boolean impersonation();
+ abstract boolean isAddPermission();
+
/** Builder for {@link TestLabelPermission}. */
@AutoValue.Builder
public abstract static class Builder {
@@ -208,6 +232,8 @@
abstract Builder max(int max);
+ abstract Builder isAddPermission(boolean isAddPermission);
+
/** Sets the minimum and maximum values for the permission. */
public Builder range(int min, int max) {
checkArgument(min != 0 || max != 0, "empty range");
@@ -243,6 +269,12 @@
return TestPermissionKey.builder().name(Permission.forLabel(name));
}
+ /** Starts a builder for describing a label removal permission key for deletion. */
+ public static TestPermissionKey.Builder labelRemovalPermissionKey(String name) {
+ checkLabelName(name);
+ return TestPermissionKey.builder().name(Permission.forRemoveLabel(name));
+ }
+
/** Starts a builder for describing a capability key for deletion. */
public static TestPermissionKey.Builder capabilityKey(String name) {
return TestPermissionKey.builder().name(name).section(GLOBAL_CAPABILITIES);
diff --git a/java/com/google/gerrit/entities/Permission.java b/java/com/google/gerrit/entities/Permission.java
index 6d2fa32..d029fad 100644
--- a/java/com/google/gerrit/entities/Permission.java
+++ b/java/com/google/gerrit/entities/Permission.java
@@ -43,6 +43,7 @@
public static final String FORGE_SERVER = "forgeServerAsCommitter";
public static final String LABEL = "label-";
public static final String LABEL_AS = "labelAs-";
+ public static final String REMOVE_LABEL = "removeLabel-";
public static final String OWNER = "owner";
public static final String PUSH = "push";
public static final String PUSH_MERGE = "pushMerge";
@@ -60,6 +61,7 @@
private static final List<String> NAMES_LC;
private static final int LABEL_INDEX;
private static final int LABEL_AS_INDEX;
+ private static final int REMOVE_LABEL_INDEX;
static {
NAMES_LC = new ArrayList<>();
@@ -79,6 +81,7 @@
NAMES_LC.add(FORGE_SERVER.toLowerCase());
NAMES_LC.add(LABEL.toLowerCase());
NAMES_LC.add(LABEL_AS.toLowerCase());
+ NAMES_LC.add(REMOVE_LABEL.toLowerCase());
NAMES_LC.add(OWNER.toLowerCase());
NAMES_LC.add(PUSH.toLowerCase());
NAMES_LC.add(PUSH_MERGE.toLowerCase());
@@ -93,15 +96,19 @@
LABEL_INDEX = NAMES_LC.indexOf(Permission.LABEL);
LABEL_AS_INDEX = NAMES_LC.indexOf(Permission.LABEL_AS.toLowerCase());
+ REMOVE_LABEL_INDEX = NAMES_LC.indexOf(Permission.REMOVE_LABEL.toLowerCase());
}
/** Returns true if the name is recognized as a permission name. */
public static boolean isPermission(String varName) {
- return isLabel(varName) || isLabelAs(varName) || NAMES_LC.contains(varName.toLowerCase());
+ return isLabel(varName)
+ || isLabelAs(varName)
+ || isRemoveLabel(varName)
+ || NAMES_LC.contains(varName.toLowerCase());
}
public static boolean hasRange(String varName) {
- return isLabel(varName) || isLabelAs(varName);
+ return isLabel(varName) || isLabelAs(varName) || isRemoveLabel(varName);
}
/** Returns true if the permission name is actually for a review label. */
@@ -114,6 +121,11 @@
return var.startsWith(LABEL_AS) && LABEL_AS.length() < var.length();
}
+ /** Returns true if the permission is for impersonated review labels. */
+ public static boolean isRemoveLabel(String var) {
+ return var.startsWith(REMOVE_LABEL) && REMOVE_LABEL.length() < var.length();
+ }
+
/** Returns permission name for the given review label. */
public static String forLabel(String labelName) {
return LABEL + labelName;
@@ -124,12 +136,19 @@
return LABEL_AS + labelName;
}
+ /** Returns permission name to remove a label for another user. */
+ public static String forRemoveLabel(String labelName) {
+ return REMOVE_LABEL + labelName;
+ }
+
@Nullable
public static String extractLabel(String varName) {
if (isLabel(varName)) {
return varName.substring(LABEL.length());
} else if (isLabelAs(varName)) {
return varName.substring(LABEL_AS.length());
+ } else if (isRemoveLabel(varName)) {
+ return varName.substring(REMOVE_LABEL.length());
}
return null;
}
@@ -205,6 +224,8 @@
return LABEL_INDEX;
} else if (isLabelAs(a.getName())) {
return LABEL_AS_INDEX;
+ } else if (isRemoveLabel(a.getName())) {
+ return REMOVE_LABEL_INDEX;
}
int index = NAMES_LC.indexOf(a.getName().toLowerCase());
diff --git a/java/com/google/gerrit/extensions/api/changes/ChangeApi.java b/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
index cce28e9..0ebb859 100644
--- a/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
+++ b/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
@@ -27,12 +27,14 @@
import com.google.gerrit.extensions.common.CommitMessageInput;
import com.google.gerrit.extensions.common.MergePatchSetInput;
import com.google.gerrit.extensions.common.PureRevertInfo;
+import com.google.gerrit.extensions.common.RebaseChainInfo;
import com.google.gerrit.extensions.common.RevertSubmissionInfo;
import com.google.gerrit.extensions.common.RobotCommentInfo;
import com.google.gerrit.extensions.common.SubmitRequirementInput;
import com.google.gerrit.extensions.common.SubmitRequirementResultInfo;
import com.google.gerrit.extensions.common.SuggestedReviewerInfo;
import com.google.gerrit.extensions.restapi.NotImplementedException;
+import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
import java.util.Arrays;
import java.util.Collection;
@@ -178,6 +180,24 @@
/** Rebase the current revision of a change. */
void rebase(RebaseInput in) throws RestApiException;
+ /**
+ * Rebase the current revisions of a change's chain using default options.
+ *
+ * @return a {@code RebaseChainInfo} contains the {@code ChangeInfo} data for the rebased the
+ * chain
+ */
+ default Response<RebaseChainInfo> rebaseChain() throws RestApiException {
+ return rebaseChain(new RebaseInput());
+ }
+
+ /**
+ * Rebase the current revisions of a change's chain.
+ *
+ * @return a {@code RebaseChainInfo} contains the {@code ChangeInfo} data for the rebased the
+ * chain
+ */
+ Response<RebaseChainInfo> rebaseChain(RebaseInput in) throws RestApiException;
+
/** Deletes a change. */
void delete() throws RestApiException;
@@ -634,6 +654,11 @@
}
@Override
+ public Response<RebaseChainInfo> rebaseChain(RebaseInput in) throws RestApiException {
+ throw new NotImplementedException();
+ }
+
+ @Override
public void delete() throws RestApiException {
throw new NotImplementedException();
}
diff --git a/java/com/google/gerrit/extensions/common/ChangeInfo.java b/java/com/google/gerrit/extensions/common/ChangeInfo.java
index 40ae2ec..a865187 100644
--- a/java/com/google/gerrit/extensions/common/ChangeInfo.java
+++ b/java/com/google/gerrit/extensions/common/ChangeInfo.java
@@ -105,6 +105,7 @@
public Map<String, ActionInfo> actions;
public Map<String, LabelInfo> labels;
public Map<String, Collection<String>> permittedLabels;
+ public Map<String, Map<String, List<AccountInfo>>> removableLabels;
public Collection<AccountInfo> removableReviewers;
public Map<ReviewerState, Collection<AccountInfo>> reviewers;
public Map<ReviewerState, Collection<AccountInfo>> pendingReviewers;
diff --git a/java/com/google/gerrit/extensions/common/ChangeInfoDiffer.java b/java/com/google/gerrit/extensions/common/ChangeInfoDiffer.java
index 24182cc..51c35dc 100644
--- a/java/com/google/gerrit/extensions/common/ChangeInfoDiffer.java
+++ b/java/com/google/gerrit/extensions/common/ChangeInfoDiffer.java
@@ -150,16 +150,17 @@
/** Returns {@code null} if nothing has been added to {@code oldCollection} */
@Nullable
private static ImmutableList<?> getAddedForCollection(
- Collection<?> oldCollection, Collection<?> newCollection) {
- ImmutableList<?> notInOldCollection = getAdditions(oldCollection, newCollection);
+ @Nullable Collection<?> oldCollection, Collection<?> newCollection) {
+ ImmutableList<?> notInOldCollection = getAdditionsForCollection(oldCollection, newCollection);
return notInOldCollection.isEmpty() ? null : notInOldCollection;
}
@Nullable
- private static ImmutableList<Object> getAdditions(
- Collection<?> oldCollection, Collection<?> newCollection) {
- if (oldCollection == null)
- return newCollection != null ? ImmutableList.copyOf(newCollection) : null;
+ private static ImmutableList<Object> getAdditionsForCollection(
+ @Nullable Collection<?> oldCollection, Collection<?> newCollection) {
+ if (oldCollection == null) {
+ return ImmutableList.copyOf(newCollection);
+ }
Map<Object, List<Object>> duplicatesMap = newCollection.stream().collect(groupingBy(v -> v));
oldCollection.forEach(
@@ -173,7 +174,18 @@
/** Returns {@code null} if nothing has been added to {@code oldMap} */
@Nullable
- private static ImmutableMap<Object, Object> getAddedForMap(Map<?, ?> oldMap, Map<?, ?> newMap) {
+ private static ImmutableMap<Object, Object> getAddedForMap(
+ @Nullable Map<?, ?> oldMap, Map<?, ?> newMap) {
+ ImmutableMap<Object, Object> notInOldMap = getAdditionsForMap(oldMap, newMap);
+ return notInOldMap.isEmpty() ? null : notInOldMap;
+ }
+
+ @Nullable
+ private static ImmutableMap<Object, Object> getAdditionsForMap(
+ @Nullable Map<?, ?> oldMap, Map<?, ?> newMap) {
+ if (oldMap == null) {
+ return ImmutableMap.copyOf(newMap);
+ }
ImmutableMap.Builder<Object, Object> additionsBuilder = ImmutableMap.builder();
for (Map.Entry<?, ?> entry : newMap.entrySet()) {
Object added = getAdded(oldMap.get(entry.getKey()), entry.getValue());
@@ -181,8 +193,7 @@
additionsBuilder.put(entry.getKey(), added);
}
}
- ImmutableMap<Object, Object> additions = additionsBuilder.build();
- return additions.isEmpty() ? null : additions;
+ return additionsBuilder.build();
}
private static Object get(Field field, Object obj) {
diff --git a/java/com/google/gerrit/extensions/common/RebaseChainInfo.java b/java/com/google/gerrit/extensions/common/RebaseChainInfo.java
new file mode 100644
index 0000000..b327007
--- /dev/null
+++ b/java/com/google/gerrit/extensions/common/RebaseChainInfo.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2022 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.extensions.common;
+
+import java.util.List;
+
+public class RebaseChainInfo {
+ public List<ChangeInfo> rebasedChanges;
+ /**
+ * Whether any of the changes contain conflicts.
+ *
+ * <p>If {@code true}, some of the rebased changes are marked with conflicts.
+ */
+ public Boolean containsGitConflicts;
+}
diff --git a/java/com/google/gerrit/httpd/init/WebAppInitializer.java b/java/com/google/gerrit/httpd/init/WebAppInitializer.java
index 0073ec2..df2c5cb 100644
--- a/java/com/google/gerrit/httpd/init/WebAppInitializer.java
+++ b/java/com/google/gerrit/httpd/init/WebAppInitializer.java
@@ -55,6 +55,7 @@
import com.google.gerrit.server.account.externalids.ExternalIdCaseSensitivityMigrator;
import com.google.gerrit.server.api.GerritApiModule;
import com.google.gerrit.server.api.PluginApiModule;
+import com.google.gerrit.server.api.projects.ProjectQueryBuilderModule;
import com.google.gerrit.server.audit.AuditModule;
import com.google.gerrit.server.cache.h2.H2CacheModule;
import com.google.gerrit.server.cache.mem.DefaultMemoryCacheModule;
@@ -308,6 +309,7 @@
modules.add(new MimeUtil2Module());
modules.add(cfgInjector.getInstance(GerritGlobalModule.class));
modules.add(new GerritApiModule());
+ modules.add(new ProjectQueryBuilderModule());
modules.add(new PluginApiModule());
modules.add(new SearchingChangeCacheImplModule());
modules.add(new InternalAccountDirectoryModule());
diff --git a/java/com/google/gerrit/httpd/raw/StaticModule.java b/java/com/google/gerrit/httpd/raw/StaticModule.java
index 129d961..961bf9b 100644
--- a/java/com/google/gerrit/httpd/raw/StaticModule.java
+++ b/java/com/google/gerrit/httpd/raw/StaticModule.java
@@ -79,7 +79,6 @@
"/dashboard/*",
"/groups/self",
"/settings/*",
- "/topic/*",
"/Documentation/q/*");
/**
diff --git a/java/com/google/gerrit/index/Index.java b/java/com/google/gerrit/index/Index.java
index cc3117d..870d827 100644
--- a/java/com/google/gerrit/index/Index.java
+++ b/java/com/google/gerrit/index/Index.java
@@ -60,6 +60,9 @@
*/
void replace(V obj);
+ /** Delete a document from the index by value */
+ void deleteByValue(V value);
+
/**
* Delete a document from the index by key.
*
@@ -153,4 +156,14 @@
default boolean isEnabled() {
return true;
}
+
+ /**
+ * Rewriter that should be invoked on queries to this index.
+ *
+ * <p>The default implementation does not do anything. Should be overridden by implementation, if
+ * needed.
+ */
+ default IndexRewriter<V> getIndexRewriter() {
+ return (in, opts) -> in;
+ }
}
diff --git a/java/com/google/gerrit/index/project/ProjectField.java b/java/com/google/gerrit/index/project/ProjectField.java
index 3114b4c..e050f53 100644
--- a/java/com/google/gerrit/index/project/ProjectField.java
+++ b/java/com/google/gerrit/index/project/ProjectField.java
@@ -15,14 +15,12 @@
package com.google.gerrit.index.project;
import static com.google.common.collect.ImmutableList.toImmutableList;
-import static com.google.gerrit.index.FieldDef.exact;
-import static com.google.gerrit.index.FieldDef.fullText;
-import static com.google.gerrit.index.FieldDef.prefix;
import static com.google.gerrit.index.FieldDef.storedOnly;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.index.FieldDef;
+import com.google.gerrit.index.IndexedField;
import com.google.gerrit.index.RefState;
import com.google.gerrit.index.SchemaUtil;
@@ -38,23 +36,53 @@
.toByteArray(project.getNameKey());
}
- public static final FieldDef<ProjectData, String> NAME =
- exact("name").stored().build(p -> p.getProject().getName());
+ public static final IndexedField<ProjectData, String> NAME_FIELD =
+ IndexedField.<ProjectData>stringBuilder("RepoName")
+ .required()
+ .size(200)
+ .stored()
+ .build(p -> p.getProject().getName());
- public static final FieldDef<ProjectData, String> DESCRIPTION =
- fullText("description").stored().build(p -> p.getProject().getDescription());
+ public static final IndexedField<ProjectData, String>.SearchSpec NAME_SPEC =
+ NAME_FIELD.exact("name");
- public static final FieldDef<ProjectData, String> PARENT_NAME =
- exact("parent_name").build(p -> p.getProject().getParentName());
+ public static final IndexedField<ProjectData, String> DESCRIPTION_FIELD =
+ IndexedField.<ProjectData>stringBuilder("Description")
+ .stored()
+ .build(p -> p.getProject().getDescription());
- public static final FieldDef<ProjectData, Iterable<String>> NAME_PART =
- prefix("name_part").buildRepeatable(p -> SchemaUtil.getNameParts(p.getProject().getName()));
+ public static final IndexedField<ProjectData, String>.SearchSpec DESCRIPTION_SPEC =
+ DESCRIPTION_FIELD.fullText("description");
- public static final FieldDef<ProjectData, String> STATE =
- exact("state").stored().build(p -> p.getProject().getState().name());
+ public static final IndexedField<ProjectData, String> PARENT_NAME_FIELD =
+ IndexedField.<ProjectData>stringBuilder("ParentName")
+ .build(p -> p.getProject().getParentName());
- public static final FieldDef<ProjectData, Iterable<String>> ANCESTOR_NAME =
- exact("ancestor_name").buildRepeatable(ProjectData::getParentNames);
+ public static final IndexedField<ProjectData, String>.SearchSpec PARENT_NAME_SPEC =
+ PARENT_NAME_FIELD.exact("parent_name");
+
+ public static final IndexedField<ProjectData, Iterable<String>> NAME_PART_FIELD =
+ IndexedField.<ProjectData>iterableStringBuilder("NamePart")
+ .size(200)
+ .build(p -> SchemaUtil.getNameParts(p.getProject().getName()));
+
+ public static final IndexedField<ProjectData, Iterable<String>>.SearchSpec NAME_PART_SPEC =
+ NAME_PART_FIELD.prefix("name_part");
+
+ public static final IndexedField<ProjectData, String> STATE_FIELD =
+ IndexedField.<ProjectData>stringBuilder("State")
+ .stored()
+ .build(p -> p.getProject().getState().name());
+
+ public static final IndexedField<ProjectData, String>.SearchSpec STATE_SPEC =
+ STATE_FIELD.exact("state");
+
+ public static final IndexedField<ProjectData, Iterable<String>> ANCESTOR_NAME_FIELD =
+ IndexedField.<ProjectData>iterableStringBuilder("AncestorName")
+ .build(ProjectData::getParentNames);
+
+ public static final IndexedField<ProjectData, Iterable<String>>.SearchSpec ANCESTOR_NAME_SPEC =
+ ANCESTOR_NAME_FIELD.exact("ancestor_name");
/**
* All values of all refs that were used in the course of indexing this document. This covers
diff --git a/java/com/google/gerrit/index/project/ProjectIndex.java b/java/com/google/gerrit/index/project/ProjectIndex.java
index b2ddaff..0aa7393 100644
--- a/java/com/google/gerrit/index/project/ProjectIndex.java
+++ b/java/com/google/gerrit/index/project/ProjectIndex.java
@@ -18,6 +18,7 @@
import com.google.gerrit.index.Index;
import com.google.gerrit.index.IndexDefinition;
import com.google.gerrit.index.query.Predicate;
+import java.util.function.Function;
/**
* Index for Gerrit projects (repositories). This class is mainly used for typing the generic parent
@@ -30,6 +31,8 @@
@Override
default Predicate<ProjectData> keyPredicate(Project.NameKey nameKey) {
- return new ProjectPredicate(ProjectField.NAME, nameKey.get());
+ return new ProjectPredicate(ProjectField.NAME_SPEC, nameKey.get());
}
+
+ Function<ProjectData, Project.NameKey> ENTITY_TO_KEY = (p) -> p.getProject().getNameKey();
}
diff --git a/java/com/google/gerrit/index/project/ProjectPredicate.java b/java/com/google/gerrit/index/project/ProjectPredicate.java
index 11875ef..0eaf2b6 100644
--- a/java/com/google/gerrit/index/project/ProjectPredicate.java
+++ b/java/com/google/gerrit/index/project/ProjectPredicate.java
@@ -14,12 +14,12 @@
package com.google.gerrit.index.project;
-import com.google.gerrit.index.FieldDef;
+import com.google.gerrit.index.SchemaFieldDefs.SchemaField;
import com.google.gerrit.index.query.IndexPredicate;
/** Predicate that is mapped to a field in the project index. */
public class ProjectPredicate extends IndexPredicate<ProjectData> {
- public ProjectPredicate(FieldDef<ProjectData, ?> def, String value) {
+ public ProjectPredicate(SchemaField<ProjectData, ?> def, String value) {
super(def, value);
}
}
diff --git a/java/com/google/gerrit/index/project/ProjectSchemaDefinitions.java b/java/com/google/gerrit/index/project/ProjectSchemaDefinitions.java
index 0619566..05c23e1 100644
--- a/java/com/google/gerrit/index/project/ProjectSchemaDefinitions.java
+++ b/java/com/google/gerrit/index/project/ProjectSchemaDefinitions.java
@@ -16,6 +16,8 @@
import static com.google.gerrit.index.SchemaUtil.schema;
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.index.IndexedField;
import com.google.gerrit.index.Schema;
import com.google.gerrit.index.SchemaDefinitions;
@@ -31,14 +33,26 @@
static final Schema<ProjectData> V1 =
schema(
/* version= */ 1,
- ProjectField.NAME,
- ProjectField.DESCRIPTION,
- ProjectField.PARENT_NAME,
- ProjectField.NAME_PART,
- ProjectField.ANCESTOR_NAME);
+ ImmutableList.of(
+ ProjectField.NAME_FIELD,
+ ProjectField.DESCRIPTION_FIELD,
+ ProjectField.PARENT_NAME_FIELD,
+ ProjectField.NAME_PART_FIELD,
+ ProjectField.ANCESTOR_NAME_FIELD),
+ ImmutableList.<IndexedField<ProjectData, ?>.SearchSpec>of(
+ ProjectField.NAME_SPEC,
+ ProjectField.DESCRIPTION_SPEC,
+ ProjectField.PARENT_NAME_SPEC,
+ ProjectField.NAME_PART_SPEC,
+ ProjectField.ANCESTOR_NAME_SPEC));
@Deprecated
- static final Schema<ProjectData> V2 = schema(V1, ProjectField.STATE, ProjectField.REF_STATE);
+ static final Schema<ProjectData> V2 =
+ schema(
+ V1,
+ ImmutableList.of(ProjectField.REF_STATE),
+ ImmutableList.<IndexedField<ProjectData, ?>>of(ProjectField.STATE_FIELD),
+ ImmutableList.<IndexedField<ProjectData, ?>.SearchSpec>of(ProjectField.STATE_SPEC));
// Bump Lucene version requires reindexing
@Deprecated static final Schema<ProjectData> V3 = schema(V2);
diff --git a/java/com/google/gerrit/index/query/AndPredicate.java b/java/com/google/gerrit/index/query/AndPredicate.java
index 23ae312..fda961d 100644
--- a/java/com/google/gerrit/index/query/AndPredicate.java
+++ b/java/com/google/gerrit/index/query/AndPredicate.java
@@ -134,7 +134,7 @@
cmp = a.estimateCost() - b.estimateCost();
}
- if (cmp == 0 && a instanceof DataSource && b instanceof DataSource) {
+ if (cmp == 0 && a instanceof DataSource) {
DataSource<?> as = (DataSource<?>) a;
DataSource<?> bs = (DataSource<?>) b;
cmp = as.getCardinality() - bs.getCardinality();
diff --git a/java/com/google/gerrit/index/query/QueryProcessor.java b/java/com/google/gerrit/index/query/QueryProcessor.java
index f237006..1c8bbc3 100644
--- a/java/com/google/gerrit/index/query/QueryProcessor.java
+++ b/java/com/google/gerrit/index/query/QueryProcessor.java
@@ -268,7 +268,9 @@
limit,
getRequestedFields());
logger.atFine().log("Query options: %s", opts);
- Predicate<T> pred = rewriter.rewrite(q, opts);
+ // Apply index-specific rewrite first
+ Predicate<T> pred = indexes.getSearchIndex().getIndexRewriter().rewrite(q, opts);
+ pred = rewriter.rewrite(pred, opts);
if (enforceVisibility) {
pred = enforceVisibility(pred);
}
diff --git a/java/com/google/gerrit/index/testing/AbstractFakeIndex.java b/java/com/google/gerrit/index/testing/AbstractFakeIndex.java
index d60be14..92bc126 100644
--- a/java/com/google/gerrit/index/testing/AbstractFakeIndex.java
+++ b/java/com/google/gerrit/index/testing/AbstractFakeIndex.java
@@ -279,6 +279,11 @@
@Override
public void insert(ChangeData obj) {}
+
+ @Override
+ public void deleteByValue(ChangeData value) {
+ delete(ChangeIndex.ENTITY_TO_KEY.apply(value));
+ }
}
/** Fake implementation of {@link AccountIndex} where all filtering happens in-memory. */
@@ -311,6 +316,11 @@
@Override
public void insert(AccountState obj) {}
+
+ @Override
+ public void deleteByValue(AccountState value) {
+ delete(AccountIndex.ENTITY_TO_KEY.apply(value));
+ }
}
/** Fake implementation of {@link GroupIndex} where all filtering happens in-memory. */
@@ -344,6 +354,11 @@
@Override
public void insert(InternalGroup obj) {}
+
+ @Override
+ public void deleteByValue(InternalGroup value) {
+ delete(GroupIndex.ENTITY_TO_KEY.apply(value));
+ }
}
/** Fake implementation of {@link ProjectIndex} where all filtering happens in-memory. */
@@ -376,5 +391,10 @@
@Override
public void insert(ProjectData obj) {}
+
+ @Override
+ public void deleteByValue(ProjectData value) {
+ delete(ProjectIndex.ENTITY_TO_KEY.apply(value));
+ }
}
}
diff --git a/java/com/google/gerrit/lucene/AbstractLuceneIndex.java b/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
index c158c65..f9dc31a 100644
--- a/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
+++ b/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
@@ -107,6 +107,7 @@
private final Set<NrtFuture> notDoneNrtFutures;
private final AutoFlush autoFlush;
private ScheduledExecutorService autoCommitExecutor;
+ private final Function<V, K> valueToKeyFunction;
@SuppressWarnings("ThreadPriorityCheck")
AbstractLuceneIndex(
@@ -118,7 +119,8 @@
String subIndex,
GerritIndexWriterConfig writerConfig,
SearcherFactory searcherFactory,
- AutoFlush autoFlush)
+ AutoFlush autoFlush,
+ Function<V, K> valueToKeyFunction)
throws IOException {
this.schema = schema;
this.sitePaths = sitePaths;
@@ -126,6 +128,7 @@
this.name = name;
this.skipFields = skipFields;
this.autoFlush = autoFlush;
+ this.valueToKeyFunction = valueToKeyFunction;
String index = Joiner.on('_').skipNulls().join(name, subIndex);
long commitPeriod = writerConfig.getCommitWithinMs();
@@ -299,6 +302,11 @@
}
@Override
+ public void deleteByValue(V value) {
+ delete(valueToKeyFunction.apply(value));
+ }
+
+ @Override
public void deleteAll() {
try {
writer.deleteAll();
diff --git a/java/com/google/gerrit/lucene/ChangeSubIndex.java b/java/com/google/gerrit/lucene/ChangeSubIndex.java
index 84cc83a..024b102 100644
--- a/java/com/google/gerrit/lucene/ChangeSubIndex.java
+++ b/java/com/google/gerrit/lucene/ChangeSubIndex.java
@@ -85,7 +85,8 @@
subIndex,
writerConfig,
searcherFactory,
- autoFlush);
+ autoFlush,
+ ChangeIndex.ENTITY_TO_KEY);
}
@Override
diff --git a/java/com/google/gerrit/lucene/LuceneAccountIndex.java b/java/com/google/gerrit/lucene/LuceneAccountIndex.java
index 2e1771f..9c0baa8 100644
--- a/java/com/google/gerrit/lucene/LuceneAccountIndex.java
+++ b/java/com/google/gerrit/lucene/LuceneAccountIndex.java
@@ -108,7 +108,8 @@
null,
new GerritIndexWriterConfig(cfg, ACCOUNTS),
new SearcherFactory(),
- autoFlush);
+ autoFlush,
+ AccountIndex.ENTITY_TO_KEY);
this.accountCache = accountCache;
indexWriterConfig = new GerritIndexWriterConfig(cfg, ACCOUNTS);
diff --git a/java/com/google/gerrit/lucene/LuceneChangeIndex.java b/java/com/google/gerrit/lucene/LuceneChangeIndex.java
index e16adf5..6365260 100644
--- a/java/com/google/gerrit/lucene/LuceneChangeIndex.java
+++ b/java/com/google/gerrit/lucene/LuceneChangeIndex.java
@@ -243,6 +243,11 @@
}
@Override
+ public void deleteByValue(ChangeData value) {
+ delete(ChangeIndex.ENTITY_TO_KEY.apply(value));
+ }
+
+ @Override
public void delete(Change.Id changeId) {
Term idTerm = LuceneChangeIndex.idTerm(changeId);
try {
diff --git a/java/com/google/gerrit/lucene/LuceneGroupIndex.java b/java/com/google/gerrit/lucene/LuceneGroupIndex.java
index b741042..6301421 100644
--- a/java/com/google/gerrit/lucene/LuceneGroupIndex.java
+++ b/java/com/google/gerrit/lucene/LuceneGroupIndex.java
@@ -98,7 +98,8 @@
null,
new GerritIndexWriterConfig(cfg, GROUPS),
new SearcherFactory(),
- autoFlush);
+ autoFlush,
+ GroupIndex.ENTITY_TO_KEY);
this.groupCache = groupCache;
indexWriterConfig = new GerritIndexWriterConfig(cfg, GROUPS);
diff --git a/java/com/google/gerrit/lucene/LuceneProjectIndex.java b/java/com/google/gerrit/lucene/LuceneProjectIndex.java
index 6b2b693..911d91f 100644
--- a/java/com/google/gerrit/lucene/LuceneProjectIndex.java
+++ b/java/com/google/gerrit/lucene/LuceneProjectIndex.java
@@ -15,7 +15,7 @@
package com.google.gerrit.lucene;
import static com.google.common.collect.Iterables.getOnlyElement;
-import static com.google.gerrit.index.project.ProjectField.NAME;
+import static com.google.gerrit.index.project.ProjectField.NAME_SPEC;
import com.google.common.collect.ImmutableSet;
import com.google.gerrit.common.Nullable;
@@ -58,14 +58,14 @@
implements ProjectIndex {
private static final String PROJECTS = "projects";
- private static final String NAME_SORT_FIELD = sortFieldName(NAME);
+ private static final String NAME_SORT_FIELD = sortFieldName(NAME_SPEC);
private static Term idTerm(ProjectData projectState) {
return idTerm(projectState.getProject().getNameKey());
}
private static Term idTerm(Project.NameKey nameKey) {
- return QueryBuilder.stringTerm(NAME.getName(), nameKey.get());
+ return QueryBuilder.stringTerm(NAME_SPEC.getName(), nameKey.get());
}
private final GerritIndexWriterConfig indexWriterConfig;
@@ -98,7 +98,8 @@
null,
new GerritIndexWriterConfig(cfg, PROJECTS),
new SearcherFactory(),
- autoFlush);
+ autoFlush,
+ ProjectIndex.ENTITY_TO_KEY);
this.projectCache = projectCache;
indexWriterConfig = new GerritIndexWriterConfig(cfg, PROJECTS);
@@ -109,7 +110,7 @@
void add(Document doc, Values<ProjectData> values) {
// Add separate DocValues field for the field that is needed for sorting.
SchemaField<ProjectData, ?> f = values.getField();
- if (f == NAME) {
+ if (f == NAME_SPEC) {
String value = (String) getOnlyElement(values.getValues());
doc.add(new SortedDocValuesField(NAME_SORT_FIELD, new BytesRef(value)));
}
@@ -155,7 +156,7 @@
@Nullable
@Override
protected ProjectData fromDocument(Document doc) {
- Project.NameKey nameKey = Project.nameKey(doc.getField(NAME.getName()).stringValue());
+ Project.NameKey nameKey = Project.nameKey(doc.getField(NAME_SPEC.getName()).stringValue());
return projectCache.get().get(nameKey).map(ProjectState::toProjectData).orElse(null);
}
}
diff --git a/java/com/google/gerrit/pgm/Daemon.java b/java/com/google/gerrit/pgm/Daemon.java
index 75891fe..0342fe5 100644
--- a/java/com/google/gerrit/pgm/Daemon.java
+++ b/java/com/google/gerrit/pgm/Daemon.java
@@ -64,6 +64,7 @@
import com.google.gerrit.server.account.externalids.ExternalIdCaseSensitivityMigrator;
import com.google.gerrit.server.api.GerritApiModule;
import com.google.gerrit.server.api.PluginApiModule;
+import com.google.gerrit.server.api.projects.ProjectQueryBuilderModule;
import com.google.gerrit.server.audit.AuditModule;
import com.google.gerrit.server.cache.h2.H2CacheModule;
import com.google.gerrit.server.cache.mem.DefaultMemoryCacheModule;
@@ -446,6 +447,7 @@
modules.add(new MimeUtil2Module());
modules.add(cfgInjector.getInstance(GerritGlobalModule.class));
modules.add(new GerritApiModule());
+ modules.add(new ProjectQueryBuilderModule());
modules.add(new PluginApiModule());
modules.add(new SearchingChangeCacheImplModule(replica));
diff --git a/java/com/google/gerrit/server/IdentifiedUser.java b/java/com/google/gerrit/server/IdentifiedUser.java
index 65a81f7..eda6e09 100644
--- a/java/com/google/gerrit/server/IdentifiedUser.java
+++ b/java/com/google/gerrit/server/IdentifiedUser.java
@@ -100,30 +100,30 @@
enablePeerIPInReflogRecord,
Providers.of(null),
state,
- null);
+ /* realUser= */ null);
}
public IdentifiedUser create(Account.Id id) {
- return create(null, id);
+ return create(/* remotePeer= */ null, id);
}
@VisibleForTesting
@UsedAt(UsedAt.Project.GOOGLE)
public IdentifiedUser forTest(Account.Id id, PropertyMap properties) {
- return runAs(null, id, null, properties);
+ return runAs(/* remotePeer= */ null, id, /* caller= */ null, properties);
}
- public IdentifiedUser create(SocketAddress remotePeer, Account.Id id) {
- return runAs(remotePeer, id, null);
+ public IdentifiedUser create(@Nullable SocketAddress remotePeer, Account.Id id) {
+ return runAs(remotePeer, id, /* caller= */ null);
}
public IdentifiedUser runAs(
- SocketAddress remotePeer, Account.Id id, @Nullable CurrentUser caller) {
+ @Nullable SocketAddress remotePeer, Account.Id id, @Nullable CurrentUser caller) {
return runAs(remotePeer, id, caller, PropertyMap.EMPTY);
}
private IdentifiedUser runAs(
- SocketAddress remotePeer,
+ @Nullable SocketAddress remotePeer,
Account.Id id,
@Nullable CurrentUser caller,
PropertyMap properties) {
@@ -244,7 +244,7 @@
AccountCache accountCache,
GroupBackend groupBackend,
Boolean enablePeerIPInReflogRecord,
- @Nullable Provider<SocketAddress> remotePeerProvider,
+ Provider<SocketAddress> remotePeerProvider,
AccountState state,
@Nullable CurrentUser realUser) {
this(
@@ -270,7 +270,7 @@
AccountCache accountCache,
GroupBackend groupBackend,
Boolean enablePeerIPInReflogRecord,
- @Nullable Provider<SocketAddress> remotePeerProvider,
+ Provider<SocketAddress> remotePeerProvider,
Account.Id id,
@Nullable CurrentUser realUser,
PropertyMap properties) {
diff --git a/java/com/google/gerrit/server/PatchSetUtil.java b/java/com/google/gerrit/server/PatchSetUtil.java
index 3d449b7..2962108 100644
--- a/java/com/google/gerrit/server/PatchSetUtil.java
+++ b/java/com/google/gerrit/server/PatchSetUtil.java
@@ -35,11 +35,13 @@
import com.google.gerrit.server.notedb.ChangeUpdate;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.update.RepoContext;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
import java.util.List;
+import java.util.Map;
import java.util.Optional;
import java.util.Set;
import org.eclipse.jgit.lib.ObjectId;
@@ -169,4 +171,54 @@
return src;
}
}
+
+ /**
+ * Gets the commit ID for the latest patch-set of a given change.
+ *
+ * <p>This also takes into account the patch sets that are added in the provided {@link
+ * RepoContext}.
+ *
+ * @param ctx to look for pending updates in.
+ * @param notesFactory to fetch existing patch sets with.
+ * @param changeId to get the latest commit for.
+ * @return the latest commit ID.
+ * @throws IOException if no committed nor pending commits found for the change.
+ */
+ public static RevCommit getCurrentRevCommitIncludingPending(
+ RepoContext ctx, ChangeNotes.Factory notesFactory, Change.Id changeId) throws IOException {
+ Map<String, ObjectId> refUpdates = ctx.getRepoView().getRefs(changeId.toRefPrefix());
+ refUpdates.remove("meta");
+ if (!refUpdates.isEmpty()) {
+ Optional<PatchSet.Id> latestPendingPatchSet =
+ refUpdates.keySet().stream()
+ .map(r -> PatchSet.Id.fromRef(changeId.toRefPrefix() + r))
+ .max(PatchSet.Id::compareTo);
+ if (latestPendingPatchSet.isPresent()) {
+ return ctx.getRevWalk().parseCommit(refUpdates.get(latestPendingPatchSet.get().getId()));
+ }
+ }
+ return getCurrentCommittedRevCommit(ctx.getProject(), ctx.getRevWalk(), notesFactory, changeId);
+ }
+
+ /**
+ * Gets the commit ID for the latest committed patch-set of a given change.
+ *
+ * <p>This DOES NOT take into account the patch sets that are added in the provided {@link
+ * RepoContext}.
+ *
+ * @param project name.
+ * @param notesFactory to fetch existing patch sets with.
+ * @param changeId to get the latest commit for.
+ * @return the latest commit ID.
+ * @throws IOException if no committed commits found for the change.
+ */
+ public static RevCommit getCurrentCommittedRevCommit(
+ Project.NameKey project,
+ RevWalk revWalk,
+ ChangeNotes.Factory notesFactory,
+ Change.Id changeId)
+ throws IOException {
+ ChangeNotes notes = notesFactory.createChecked(project, changeId);
+ return revWalk.parseCommit(notes.getCurrentPatchSet().commitId());
+ }
}
diff --git a/java/com/google/gerrit/server/PublishCommentsOp.java b/java/com/google/gerrit/server/PublishCommentsOp.java
index 827c078..830928a 100644
--- a/java/com/google/gerrit/server/PublishCommentsOp.java
+++ b/java/com/google/gerrit/server/PublishCommentsOp.java
@@ -23,7 +23,6 @@
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.server.change.EmailReviewComments;
-import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.extensions.events.CommentAdded;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ChangeUpdate;
@@ -112,19 +111,16 @@
}
ChangeNotes changeNotes = changeNotesFactory.createChecked(projectNameKey, psId.changeId());
PatchSet ps = psUtil.get(changeNotes, psId);
- NotifyResolver.Result notify = ctx.getNotify(changeNotes.getChangeId());
- if (notify.shouldNotify()) {
- email
- .create(
- ctx,
- ps,
- preUpdateMetaId,
- mailMessage,
- comments,
- /* patchSetComment= */ null,
- /* labels= */ ImmutableList.of())
- .sendAsync();
- }
+ email
+ .create(
+ ctx,
+ ps,
+ preUpdateMetaId,
+ mailMessage,
+ comments,
+ /* patchSetComment= */ null,
+ /* labels= */ ImmutableList.of())
+ .sendAsync();
commentAdded.fire(
ctx.getChangeData(changeNotes),
ps,
diff --git a/java/com/google/gerrit/server/StarredChangesUtil.java b/java/com/google/gerrit/server/StarredChangesUtil.java
index fd08fa8..8f413f9 100644
--- a/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/java/com/google/gerrit/server/StarredChangesUtil.java
@@ -23,7 +23,6 @@
import com.google.common.base.CharMatcher;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
-import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
@@ -39,21 +38,17 @@
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.index.change.ChangeField;
import com.google.gerrit.server.logging.Metadata;
import com.google.gerrit.server.logging.TraceContext;
import com.google.gerrit.server.logging.TraceContext.TraceTimer;
-import com.google.gerrit.server.project.NoSuchChangeException;
-import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gerrit.server.query.change.InternalChangeQuery;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
-import java.util.List;
import java.util.NavigableSet;
+import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import org.eclipse.jgit.lib.BatchRefUpdate;
@@ -166,20 +161,17 @@
private final GitReferenceUpdated gitRefUpdated;
private final AllUsersName allUsers;
private final Provider<PersonIdent> serverIdent;
- private final Provider<InternalChangeQuery> queryProvider;
@Inject
StarredChangesUtil(
GitRepositoryManager repoManager,
GitReferenceUpdated gitRefUpdated,
AllUsersName allUsers,
- @GerritPersonIdent Provider<PersonIdent> serverIdent,
- Provider<InternalChangeQuery> queryProvider) {
+ @GerritPersonIdent Provider<PersonIdent> serverIdent) {
this.repoManager = repoManager;
this.gitRefUpdated = gitRefUpdated;
this.allUsers = allUsers;
this.serverIdent = serverIdent;
- this.queryProvider = queryProvider;
}
public NavigableSet<String> getLabels(Account.Id accountId, Change.Id changeId) {
@@ -238,7 +230,7 @@
batchUpdate.setAllowNonFastForwards(true);
batchUpdate.setRefLogIdent(serverIdent.get());
batchUpdate.setRefLogMessage("Unstar change " + changeId.get(), true);
- for (Account.Id accountId : byChangeFromIndex(changeId).keySet()) {
+ for (Account.Id accountId : getStars(repo, changeId)) {
String refName = RefNames.refsStarredChanges(changeId, accountId);
Ref ref = repo.getRefDatabase().exactRef(refName);
if (ref != null) {
@@ -264,12 +256,7 @@
public ImmutableMap<Account.Id, StarRef> byChange(Change.Id changeId) {
try (Repository repo = repoManager.openRepository(allUsers)) {
ImmutableMap.Builder<Account.Id, StarRef> builder = ImmutableMap.builder();
- for (String refPart : getRefNames(repo, RefNames.refsStarredChangesPrefix(changeId))) {
- Integer id = Ints.tryParse(refPart);
- if (id == null) {
- continue;
- }
- Account.Id accountId = Account.id(id);
+ for (Account.Id accountId : getStars(repo, changeId)) {
builder.put(accountId, readLabels(repo, RefNames.refsStarredChanges(changeId, accountId)));
}
return builder.build();
@@ -308,22 +295,15 @@
}
}
- public ImmutableListMultimap<Account.Id, String> byChangeFromIndex(Change.Id changeId) {
- List<ChangeData> changeData =
- queryProvider
- .get()
- .setRequestedFields(ChangeField.CHANGE_ID_SPEC, ChangeField.STAR_SPEC)
- .byLegacyChangeId(changeId);
- if (changeData.size() != 1) {
- throw new NoSuchChangeException(changeId);
- }
- return changeData.get(0).stars();
- }
-
- private static Set<String> getRefNames(Repository repo, String prefix) throws IOException {
- RefDatabase refDb = repo.getRefDatabase();
+ private static Set<Account.Id> getStars(Repository allUsers, Change.Id changeId)
+ throws IOException {
+ String prefix = RefNames.refsStarredChangesPrefix(changeId);
+ RefDatabase refDb = allUsers.getRefDatabase();
return refDb.getRefsByPrefix(prefix).stream()
.map(r -> r.getName().substring(prefix.length()))
+ .map(refPart -> Ints.tryParse(refPart))
+ .filter(Objects::nonNull)
+ .map(id -> Account.id(id))
.collect(toSet());
}
diff --git a/java/com/google/gerrit/server/account/AccountControl.java b/java/com/google/gerrit/server/account/AccountControl.java
index c80059b..ca63565 100644
--- a/java/com/google/gerrit/server/account/AccountControl.java
+++ b/java/com/google/gerrit/server/account/AccountControl.java
@@ -82,12 +82,12 @@
* accounts.
*/
@UsedAt(UsedAt.Project.PLUGIN_CODE_OWNERS)
- public AccountControl get(IdentifiedUser identifiedUser) {
+ public AccountControl get(CurrentUser user) {
return new AccountControl(
permissionBackend,
projectCache,
groupControlFactory,
- identifiedUser,
+ user,
userFactory,
accountVisibility);
}
diff --git a/java/com/google/gerrit/server/account/AccountResolver.java b/java/com/google/gerrit/server/account/AccountResolver.java
index 65eb332..fcfc805 100644
--- a/java/com/google/gerrit/server/account/AccountResolver.java
+++ b/java/com/google/gerrit/server/account/AccountResolver.java
@@ -132,12 +132,18 @@
private final String input;
private final ImmutableList<AccountState> list;
private final ImmutableList<AccountState> filteredInactive;
+ private final CurrentUser searchedAsUser;
@VisibleForTesting
- Result(String input, List<AccountState> list, List<AccountState> filteredInactive) {
+ Result(
+ String input,
+ List<AccountState> list,
+ List<AccountState> filteredInactive,
+ CurrentUser searchedAsUser) {
this.input = requireNonNull(input);
this.list = canonicalize(list);
this.filteredInactive = canonicalize(filteredInactive);
+ this.searchedAsUser = requireNonNull(searchedAsUser);
}
private ImmutableList<AccountState> canonicalize(List<AccountState> list) {
@@ -180,13 +186,21 @@
}
}
- public IdentifiedUser asUniqueUser() throws UnresolvableAccountException {
+ private void ensureSelfIsUniqueIdentifiedUser() throws UnresolvableAccountException {
ensureUnique();
+ if (!searchedAsUser.isIdentifiedUser()) {
+ throw new UnresolvableAccountException(this);
+ }
+ }
+
+ public IdentifiedUser asUniqueUser() throws UnresolvableAccountException {
if (isSelf()) {
+ ensureSelfIsUniqueIdentifiedUser();
// In the special case of "self", use the exact IdentifiedUser from the request context, to
// preserve the peer address and any other per-request state.
- return self.get().asIdentifiedUser();
+ return searchedAsUser.asIdentifiedUser();
}
+ ensureUnique();
return userFactory.create(asUnique());
}
@@ -194,11 +208,10 @@
throws UnresolvableAccountException {
ensureUnique();
if (isSelf()) {
- // TODO(dborowitz): This preserves old behavior, but it seems wrong to discard the caller.
- return self.get().asIdentifiedUser();
+ return searchedAsUser.asIdentifiedUser();
}
return userFactory.runAs(
- null, list.get(0).account().id(), requireNonNull(caller).getRealUser());
+ /* remotePeer= */ null, list.get(0).account().id(), requireNonNull(caller).getRealUser());
}
@VisibleForTesting
@@ -221,16 +234,57 @@
return false;
}
+ /**
+ * Searches can be done on behalf of either the current user or another provided user. The
+ * results of some searchers, such as BySelf, are affected by the context user.
+ */
+ default boolean requiresContextUser() {
+ return false;
+ }
+
Optional<I> tryParse(String input) throws IOException;
- Stream<AccountState> search(I input) throws IOException, ConfigInvalidException;
+ /**
+ * This method should be implemented for every searcher which doesn't require a context user.
+ *
+ * @param input to search for
+ * @return stream of the matching accounts
+ * @throws IOException by some subclasses
+ * @throws ConfigInvalidException by some subclasses
+ */
+ default Stream<AccountState> search(I input) throws IOException, ConfigInvalidException {
+ throw new IllegalStateException("search(I) default implementation should never be called.");
+ }
+
+ /**
+ * This method should be implemented for every searcher which requires a context user.
+ *
+ * @param input to search for
+ * @param asUser the context user for the search
+ * @return stream of the matching accounts
+ * @throws IOException by some subclasses
+ * @throws ConfigInvalidException by some subclasses
+ */
+ default Stream<AccountState> search(I input, CurrentUser asUser)
+ throws IOException, ConfigInvalidException {
+ if (!requiresContextUser()) {
+ return search(input);
+ }
+ throw new IllegalStateException(
+ "The searcher requires a context user, but doesn't implement search(input, asUser).");
+ }
boolean shortCircuitIfNoResults();
- default Optional<Stream<AccountState>> trySearch(String input)
+ default Optional<Stream<AccountState>> trySearch(String input, CurrentUser asUser)
throws IOException, ConfigInvalidException {
Optional<I> parsed = tryParse(input);
- return parsed.isPresent() ? Optional.of(search(parsed.get())) : Optional.empty();
+ if (parsed.isEmpty()) {
+ return Optional.empty();
+ }
+ return requiresContextUser()
+ ? Optional.of(search(parsed.get(), asUser))
+ : Optional.of(search(parsed.get()));
}
}
@@ -251,7 +305,7 @@
}
}
- private class BySelf extends StringSearcher {
+ private static class BySelf extends StringSearcher {
@Override
public boolean callerShouldFilterOutInactiveCandidates() {
return false;
@@ -263,17 +317,21 @@
}
@Override
+ public boolean requiresContextUser() {
+ return true;
+ }
+
+ @Override
protected boolean matches(String input) {
return "self".equals(input) || "me".equals(input);
}
@Override
- public Stream<AccountState> search(String input) {
- CurrentUser user = self.get();
- if (!user.isIdentifiedUser()) {
+ public Stream<AccountState> search(String input, CurrentUser asUser) {
+ if (!asUser.isIdentifiedUser()) {
return Stream.empty();
}
- return Stream.of(user.asIdentifiedUser().state());
+ return Stream.of(asUser.asIdentifiedUser().state());
}
@Override
@@ -400,9 +458,20 @@
}
private class ByFullName implements Searcher<AccountState> {
+ boolean allowSkippingVisibilityCheck = true;
+
+ ByFullName() {
+ super();
+ }
+
+ ByFullName(boolean allowSkippingVisibilityCheck) {
+ this();
+ this.allowSkippingVisibilityCheck = allowSkippingVisibilityCheck;
+ }
+
@Override
public boolean callerMayAssumeCandidatesAreVisible() {
- return true; // Rely on enforceVisibility from the index.
+ return allowSkippingVisibilityCheck;
}
@Override
@@ -424,9 +493,25 @@
}
private class ByDefaultSearch extends StringSearcher {
+ boolean allowSkippingVisibilityCheck = true;
+
+ ByDefaultSearch() {
+ super();
+ }
+
+ ByDefaultSearch(boolean allowSkippingVisibilityCheck) {
+ this();
+ this.allowSkippingVisibilityCheck = allowSkippingVisibilityCheck;
+ }
+
@Override
public boolean callerMayAssumeCandidatesAreVisible() {
- return true; // Rely on enforceVisibility from the index.
+ return allowSkippingVisibilityCheck;
+ }
+
+ @Override
+ public boolean requiresContextUser() {
+ return true;
}
@Override
@@ -435,14 +520,14 @@
}
@Override
- public Stream<AccountState> search(String input) {
+ public Stream<AccountState> search(String input, CurrentUser asUser) {
// At this point we have no clue. Just perform a whole bunch of suggestions and pray we come
// up with a reasonable result list.
// TODO(dborowitz): This doesn't match the documentation; consider whether it's possible to be
// more strict here.
boolean canSeeSecondaryEmails = false;
try {
- if (permissionBackend.user(self.get()).test(GlobalPermission.MODIFY_ACCOUNT)) {
+ if (permissionBackend.user(asUser).test(GlobalPermission.MODIFY_ACCOUNT)) {
canSeeSecondaryEmails = true;
}
} catch (PermissionBackendException e) {
@@ -477,6 +562,18 @@
.addAll(nameOrEmailSearchers)
.build();
+ private final ImmutableList<Searcher<?>> forcedVisibilitySearchers =
+ ImmutableList.of(
+ new ByNameAndEmail(),
+ new ByEmail(),
+ new FromRealm(),
+ new ByFullName(false),
+ new ByDefaultSearch(false),
+ new BySelf(),
+ new ByExactAccountId(),
+ new ByParenthesizedAccountId(),
+ new ByUsername());
+
private final AccountCache accountCache;
private final AccountControl.Factory accountControlFactory;
private final Emails emails;
@@ -538,12 +635,63 @@
* @throws IOException if an error occurs.
*/
public Result resolve(String input) throws ConfigInvalidException, IOException {
- return searchImpl(input, searchers, this::canSeePredicate, AccountResolver::isActive);
+ return searchImpl(
+ input, searchers, self.get(), this::currentUserCanSeePredicate, AccountResolver::isActive);
}
public Result resolve(String input, Predicate<AccountState> accountActivityPredicate)
throws ConfigInvalidException, IOException {
- return searchImpl(input, searchers, this::canSeePredicate, accountActivityPredicate);
+ return searchImpl(
+ input, searchers, self.get(), this::currentUserCanSeePredicate, accountActivityPredicate);
+ }
+
+ /**
+ * Resolves all accounts matching the input string, visible to the provided user.
+ *
+ * <p>The following input formats are recognized:
+ *
+ * <ul>
+ * <li>The strings {@code "self"} and {@code "me"}, if the provided user is an {@link
+ * IdentifiedUser}. In this case, may return exactly one inactive account.
+ * <li>A bare account ID ({@code "18419"}). In this case, may return exactly one inactive
+ * account. This case short-circuits if the input matches.
+ * <li>An account ID in parentheses following a full name ({@code "Full Name (18419)"}). This
+ * case short-circuits if the input matches.
+ * <li>A username ({@code "username"}).
+ * <li>A full name and email address ({@code "Full Name <email@example>"}). This case
+ * short-circuits if the input matches.
+ * <li>An email address ({@code "email@example"}. This case short-circuits if the input matches.
+ * <li>An account name recognized by the configured {@link Realm#lookup(String)} Realm}.
+ * <li>A full name ({@code "Full Name"}).
+ * <li>As a fallback, a {@link
+ * com.google.gerrit.server.query.account.AccountPredicates#defaultPredicate(Schema,
+ * boolean, String) default search} against the account index.
+ * </ul>
+ *
+ * @param asUser user to resolve the users by.
+ * @param input input string.
+ * @param forceVisibilityCheck whether to force all searchers to check for visibility.
+ * @return a result describing matching accounts. Never null even if the result set is empty.
+ * @throws ConfigInvalidException if an error occurs.
+ * @throws IOException if an error occurs.
+ */
+ public Result resolveAsUser(CurrentUser asUser, String input, boolean forceVisibilityCheck)
+ throws ConfigInvalidException, IOException {
+ return resolveAsUser(asUser, input, AccountResolver::isActive, forceVisibilityCheck);
+ }
+
+ public Result resolveAsUser(
+ CurrentUser asUser,
+ String input,
+ Predicate<AccountState> accountActivityPredicate,
+ boolean forceVisibilityCheck)
+ throws ConfigInvalidException, IOException {
+ return searchImpl(
+ input,
+ forceVisibilityCheck ? forcedVisibilitySearchers : searchers,
+ asUser,
+ new ProvidedUserCanSeePredicate(asUser),
+ accountActivityPredicate);
}
/**
@@ -556,22 +704,23 @@
* instead will be stored as a link to the corresponding Gerrit Account.
*/
public Result resolveIncludeInactive(String input) throws ConfigInvalidException, IOException {
- return searchImpl(input, searchers, this::canSeePredicate, AccountResolver::allVisible);
+ return searchImpl(
+ input,
+ searchers,
+ self.get(),
+ this::currentUserCanSeePredicate,
+ AccountResolver::allVisible);
}
public Result resolveIncludeInactiveIgnoreVisibility(String input)
throws ConfigInvalidException, IOException {
- return searchImpl(input, searchers, this::allVisiblePredicate, AccountResolver::allVisible);
+ return searchImpl(
+ input, searchers, self.get(), this::allVisiblePredicate, AccountResolver::allVisible);
}
public Result resolveIgnoreVisibility(String input) throws ConfigInvalidException, IOException {
- return searchImpl(input, searchers, this::allVisiblePredicate, AccountResolver::isActive);
- }
-
- public Result resolveIgnoreVisibility(
- String input, Predicate<AccountState> accountActivityPredicate)
- throws ConfigInvalidException, IOException {
- return searchImpl(input, searchers, this::allVisiblePredicate, accountActivityPredicate);
+ return searchImpl(
+ input, searchers, self.get(), this::allVisiblePredicate, AccountResolver::isActive);
}
/**
@@ -600,7 +749,11 @@
@Deprecated
public Result resolveByNameOrEmail(String input) throws ConfigInvalidException, IOException {
return searchImpl(
- input, nameOrEmailSearchers, this::canSeePredicate, AccountResolver::isActive);
+ input,
+ nameOrEmailSearchers,
+ self.get(),
+ this::currentUserCanSeePredicate,
+ AccountResolver::isActive);
}
/**
@@ -619,16 +772,26 @@
return searchImpl(
input,
ImmutableList.of(new ByNameAndEmail(), new ByEmail(), new ByFullName(), new ByUsername()),
- this::canSeePredicate,
+ self.get(),
+ this::currentUserCanSeePredicate,
AccountResolver::isActive);
}
- private Predicate<AccountState> canSeePredicate() {
- return this::canSee;
+ private Predicate<AccountState> currentUserCanSeePredicate() {
+ return accountControlFactory.get()::canSee;
}
- private boolean canSee(AccountState accountState) {
- return accountControlFactory.get().canSee(accountState);
+ private class ProvidedUserCanSeePredicate implements Supplier<Predicate<AccountState>> {
+ CurrentUser asUser;
+
+ ProvidedUserCanSeePredicate(CurrentUser asUser) {
+ this.asUser = asUser;
+ }
+
+ @Override
+ public Predicate<AccountState> get() {
+ return accountControlFactory.get(asUser.asIdentifiedUser())::canSee;
+ }
}
private Predicate<AccountState> allVisiblePredicate() {
@@ -648,14 +811,16 @@
Result searchImpl(
String input,
ImmutableList<Searcher<?>> searchers,
+ CurrentUser asUser,
Supplier<Predicate<AccountState>> visibilitySupplier,
Predicate<AccountState> accountActivityPredicate)
throws ConfigInvalidException, IOException {
+ requireNonNull(asUser);
visibilitySupplier = Suppliers.memoize(visibilitySupplier::get);
List<AccountState> inactive = new ArrayList<>();
for (Searcher<?> searcher : searchers) {
- Optional<Stream<AccountState>> maybeResults = searcher.trySearch(input);
+ Optional<Stream<AccountState>> maybeResults = searcher.trySearch(input, asUser);
if (!maybeResults.isPresent()) {
continue;
}
@@ -677,22 +842,25 @@
}
if (!list.isEmpty()) {
- return createResult(input, list);
+ return createResult(input, list, asUser);
}
if (searcher.shortCircuitIfNoResults()) {
// For a short-circuiting searcher, return results even if empty.
- return !inactive.isEmpty() ? emptyResult(input, inactive) : createResult(input, list);
+ return !inactive.isEmpty()
+ ? emptyResult(input, inactive, asUser)
+ : createResult(input, list, asUser);
}
}
- return emptyResult(input, inactive);
+ return emptyResult(input, inactive, asUser);
}
- private Result createResult(String input, List<AccountState> list) {
- return new Result(input, list, ImmutableList.of());
+ private Result createResult(String input, List<AccountState> list, CurrentUser searchedAsUser) {
+ return new Result(input, list, ImmutableList.of(), searchedAsUser);
}
- private Result emptyResult(String input, List<AccountState> inactive) {
- return new Result(input, ImmutableList.of(), inactive);
+ private Result emptyResult(
+ String input, List<AccountState> inactive, CurrentUser searchedAsUser) {
+ return new Result(input, ImmutableList.of(), inactive, searchedAsUser);
}
private Stream<AccountState> toAccountStates(Set<Account.Id> ids) {
diff --git a/java/com/google/gerrit/server/account/AccountsUpdate.java b/java/com/google/gerrit/server/account/AccountsUpdate.java
index 8d5fea4..d6ea294 100644
--- a/java/com/google/gerrit/server/account/AccountsUpdate.java
+++ b/java/com/google/gerrit/server/account/AccountsUpdate.java
@@ -202,7 +202,6 @@
private ExternalIdNotes externalIdNotes;
@AssistedInject
- @SuppressWarnings("BindingAnnotationWithoutInject")
AccountsUpdate(
GitRepositoryManager repoManager,
GitReferenceUpdated gitRefUpdated,
@@ -228,7 +227,6 @@
}
@AssistedInject
- @SuppressWarnings("BindingAnnotationWithoutInject")
AccountsUpdate(
GitRepositoryManager repoManager,
GitReferenceUpdated gitRefUpdated,
diff --git a/java/com/google/gerrit/server/account/GroupCache.java b/java/com/google/gerrit/server/account/GroupCache.java
index 1e28d7d..46c730c 100644
--- a/java/com/google/gerrit/server/account/GroupCache.java
+++ b/java/com/google/gerrit/server/account/GroupCache.java
@@ -14,11 +14,15 @@
package com.google.gerrit.server.account;
+import com.google.gerrit.common.UsedAt;
+import com.google.gerrit.common.UsedAt.Project;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.InternalGroup;
+import com.google.gerrit.exceptions.StorageException;
import java.util.Collection;
import java.util.Map;
import java.util.Optional;
+import org.eclipse.jgit.lib.ObjectId;
/** Tracks group objects in memory for efficient access. */
public interface GroupCache {
@@ -62,6 +66,22 @@
Map<AccountGroup.UUID, InternalGroup> get(Collection<AccountGroup.UUID> groupUuids);
/**
+ * Returns an {@code InternalGroup} instance for the given {@code AccountGroup.UUID} at the given
+ * {@code metaId} of {@link com.google.gerrit.entities.RefNames#refsGroups} ref.
+ *
+ * <p>The caller is responsible to ensure the presence of {@code metaId} and the corresponding
+ * meta ref.
+ *
+ * @param groupUuid the UUID of the internal group
+ * @param metaId the sha1 of commit in {@link com.google.gerrit.entities.RefNames#refsGroups} ref.
+ * @return the internal group at specific sha1 {@code metaId}
+ * @throws StorageException if no internal group with this UUID exists on this server at the
+ * specific sha1, or if an error occurred during lookup.
+ */
+ @UsedAt(Project.GOOGLE)
+ InternalGroup getFromMetaId(AccountGroup.UUID groupUuid, ObjectId metaId) throws StorageException;
+
+ /**
* Removes the association of the given ID with a group.
*
* <p>The next call to {@link #get(AccountGroup.Id)} won't provide a cached value.
diff --git a/java/com/google/gerrit/server/account/GroupCacheImpl.java b/java/com/google/gerrit/server/account/GroupCacheImpl.java
index 2d947ba..6f4fce9 100644
--- a/java/com/google/gerrit/server/account/GroupCacheImpl.java
+++ b/java/com/google/gerrit/server/account/GroupCacheImpl.java
@@ -27,6 +27,7 @@
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.InternalGroup;
import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.proto.Protos;
import com.google.gerrit.server.cache.CacheModule;
import com.google.gerrit.server.cache.proto.Cache;
@@ -122,15 +123,19 @@
private final LoadingCache<AccountGroup.Id, Optional<InternalGroup>> byId;
private final LoadingCache<String, Optional<InternalGroup>> byName;
private final LoadingCache<String, Optional<InternalGroup>> byUUID;
+ private final LoadingCache<Cache.GroupKeyProto, InternalGroup> persistedByUuidCache;
@Inject
GroupCacheImpl(
@Named(BYID_NAME) LoadingCache<AccountGroup.Id, Optional<InternalGroup>> byId,
@Named(BYNAME_NAME) LoadingCache<String, Optional<InternalGroup>> byName,
- @Named(BYUUID_NAME) LoadingCache<String, Optional<InternalGroup>> byUUID) {
+ @Named(BYUUID_NAME) LoadingCache<String, Optional<InternalGroup>> byUUID,
+ @Named(BYUUID_NAME_PERSISTED)
+ LoadingCache<Cache.GroupKeyProto, InternalGroup> persistedByUuidCache) {
this.byId = byId;
this.byName = byName;
this.byUUID = byUUID;
+ this.persistedByUuidCache = persistedByUuidCache;
}
@Override
@@ -185,6 +190,21 @@
}
@Override
+ public InternalGroup getFromMetaId(AccountGroup.UUID groupUuid, ObjectId metaId)
+ throws StorageException {
+ Cache.GroupKeyProto key =
+ Cache.GroupKeyProto.newBuilder()
+ .setUuid(groupUuid.get())
+ .setRevision(ObjectIdConverter.create().toByteString(metaId))
+ .build();
+ try {
+ return persistedByUuidCache.get(key);
+ } catch (ExecutionException e) {
+ throw new StorageException(e);
+ }
+ }
+
+ @Override
public void evict(AccountGroup.Id groupId) {
if (groupId != null) {
logger.atFine().log("Evict group %s by ID", groupId.get());
diff --git a/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java b/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
index e0569f4..66a845a 100644
--- a/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
+++ b/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
@@ -54,6 +54,7 @@
import com.google.gerrit.extensions.common.InputWithMessage;
import com.google.gerrit.extensions.common.MergePatchSetInput;
import com.google.gerrit.extensions.common.PureRevertInfo;
+import com.google.gerrit.extensions.common.RebaseChainInfo;
import com.google.gerrit.extensions.common.RevertSubmissionInfo;
import com.google.gerrit.extensions.common.RobotCommentInfo;
import com.google.gerrit.extensions.common.SubmitRequirementInput;
@@ -100,6 +101,7 @@
import com.google.gerrit.server.restapi.change.PutMessage;
import com.google.gerrit.server.restapi.change.PutTopic;
import com.google.gerrit.server.restapi.change.Rebase;
+import com.google.gerrit.server.restapi.change.RebaseChain;
import com.google.gerrit.server.restapi.change.Restore;
import com.google.gerrit.server.restapi.change.Revert;
import com.google.gerrit.server.restapi.change.RevertSubmission;
@@ -144,6 +146,7 @@
private final ApplyPatch applyPatch;
private final Provider<SubmittedTogether> submittedTogether;
private final Rebase.CurrentRevision rebase;
+ private final RebaseChain rebaseChain;
private final DeleteChange deleteChange;
private final GetTopic getTopic;
private final PutTopic putTopic;
@@ -197,6 +200,7 @@
ApplyPatch applyPatch,
Provider<SubmittedTogether> submittedTogether,
Rebase.CurrentRevision rebase,
+ RebaseChain rebaseChain,
DeleteChange deleteChange,
GetTopic getTopic,
PutTopic putTopic,
@@ -248,6 +252,7 @@
this.applyPatch = applyPatch;
this.submittedTogether = submittedTogether;
this.rebase = rebase;
+ this.rebaseChain = rebaseChain;
this.deleteChange = deleteChange;
this.getTopic = getTopic;
this.putTopic = putTopic;
@@ -427,6 +432,15 @@
}
@Override
+ public Response<RebaseChainInfo> rebaseChain(RebaseInput in) throws RestApiException {
+ try {
+ return rebaseChain.apply(change, in);
+ } catch (Exception e) {
+ throw asRestApiException("Cannot rebase chain", e);
+ }
+ }
+
+ @Override
public void delete() throws RestApiException {
try {
deleteChange.apply(change, null);
diff --git a/java/com/google/gerrit/server/api/projects/ProjectQueryBuilderModule.java b/java/com/google/gerrit/server/api/projects/ProjectQueryBuilderModule.java
new file mode 100644
index 0000000..8ed1175
--- /dev/null
+++ b/java/com/google/gerrit/server/api/projects/ProjectQueryBuilderModule.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2023 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.api.projects;
+
+import com.google.gerrit.server.query.project.ProjectQueryBuilder;
+import com.google.gerrit.server.query.project.ProjectQueryBuilderImpl;
+import com.google.inject.AbstractModule;
+
+public class ProjectQueryBuilderModule extends AbstractModule {
+ @Override
+ protected void configure() {
+ bind(ProjectQueryBuilder.class).to(ProjectQueryBuilderImpl.class);
+ }
+}
diff --git a/java/com/google/gerrit/server/change/ChangeInserter.java b/java/com/google/gerrit/server/change/ChangeInserter.java
index 2883ef8..8773bb7 100644
--- a/java/com/google/gerrit/server/change/ChangeInserter.java
+++ b/java/com/google/gerrit/server/change/ChangeInserter.java
@@ -525,7 +525,7 @@
public void postUpdate(PostUpdateContext ctx) throws Exception {
reviewerAdditions.postUpdate(ctx);
NotifyResolver.Result notify = ctx.getNotify(change.getId());
- if (sendMail && notify.shouldNotify()) {
+ if (sendMail) {
Runnable sender =
new Runnable() {
@Override
diff --git a/java/com/google/gerrit/server/change/ChangeJson.java b/java/com/google/gerrit/server/change/ChangeJson.java
index 500bb77..912d202 100644
--- a/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/java/com/google/gerrit/server/change/ChangeJson.java
@@ -146,7 +146,7 @@
public static final SubmitRuleOptions SUBMIT_RULE_OPTIONS_STRICT =
ChangeField.SUBMIT_RULE_OPTIONS_STRICT.toBuilder().build();
- static final ImmutableSet<ListChangesOption> REQUIRE_LAZY_LOAD =
+ public static final ImmutableSet<ListChangesOption> REQUIRE_LAZY_LOAD =
ImmutableSet.of(
ALL_COMMITS,
ALL_REVISIONS,
@@ -688,6 +688,7 @@
!cd.change().isAbandoned()
? labelsJson.permittedLabels(user.getAccountId(), cd)
: ImmutableMap.of();
+ out.removableLabels = labelsJson.removableLabels(accountLoader, user, cd);
}
}
diff --git a/java/com/google/gerrit/server/change/DeleteReviewerByEmailOp.java b/java/com/google/gerrit/server/change/DeleteReviewerByEmailOp.java
index b512a2d..f3fd68e 100644
--- a/java/com/google/gerrit/server/change/DeleteReviewerByEmailOp.java
+++ b/java/com/google/gerrit/server/change/DeleteReviewerByEmailOp.java
@@ -76,9 +76,6 @@
if (sendEmail) {
try {
NotifyResolver.Result notify = ctx.getNotify(change.getId());
- if (!notify.shouldNotify()) {
- return;
- }
DeleteReviewerSender emailSender =
deleteReviewerSenderFactory.create(ctx.getProject(), change.getId());
emailSender.setFrom(ctx.getAccountId());
diff --git a/java/com/google/gerrit/server/change/DeleteReviewerOp.java b/java/com/google/gerrit/server/change/DeleteReviewerOp.java
index 1199be5..afb9d76 100644
--- a/java/com/google/gerrit/server/change/DeleteReviewerOp.java
+++ b/java/com/google/gerrit/server/change/DeleteReviewerOp.java
@@ -193,15 +193,13 @@
notify = notify.withHandling(NotifyHandling.OWNER);
}
try {
- if (notify.shouldNotify()) {
- emailReviewers(
- ctx.getProject(),
- currChange,
- mailMessage,
- Timestamp.from(ctx.getWhen()),
- notify,
- ctx.getRepoView());
- }
+ emailReviewers(
+ ctx.getProject(),
+ currChange,
+ mailMessage,
+ Timestamp.from(ctx.getWhen()),
+ notify,
+ ctx.getRepoView());
} catch (Exception err) {
logger.atSevere().withCause(err).log(
"Cannot email update for change %s", currChange.getId());
diff --git a/java/com/google/gerrit/server/change/EmailNewPatchSet.java b/java/com/google/gerrit/server/change/EmailNewPatchSet.java
index f6ae6a3..f67ce4a 100644
--- a/java/com/google/gerrit/server/change/EmailNewPatchSet.java
+++ b/java/com/google/gerrit/server/change/EmailNewPatchSet.java
@@ -16,6 +16,7 @@
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.PatchSet;
@@ -52,7 +53,7 @@
EmailNewPatchSet create(
PostUpdateContext postUpdateContext,
PatchSet patchSet,
- String message,
+ @Nullable String message,
ImmutableSet<PatchSetApproval> outdatedApprovals,
@Assisted("reviewers") ImmutableSet<Account.Id> reviewers,
@Assisted("extraCcs") ImmutableSet<Account.Id> extraCcs,
@@ -75,7 +76,7 @@
MessageIdGenerator messageIdGenerator,
@Assisted PostUpdateContext postUpdateContext,
@Assisted PatchSet patchSet,
- @Assisted String message,
+ @Nullable @Assisted String message,
@Assisted ImmutableSet<PatchSetApproval> outdatedApprovals,
@Assisted("reviewers") ImmutableSet<Account.Id> reviewers,
@Assisted("extraCcs") ImmutableSet<Account.Id> extraCcs,
diff --git a/java/com/google/gerrit/server/change/GetRelatedChangesUtil.java b/java/com/google/gerrit/server/change/GetRelatedChangesUtil.java
index 194a4f0..9a75469 100644
--- a/java/com/google/gerrit/server/change/GetRelatedChangesUtil.java
+++ b/java/com/google/gerrit/server/change/GetRelatedChangesUtil.java
@@ -65,6 +65,33 @@
*/
public List<RelatedChangesSorter.PatchSetData> getRelated(ChangeData changeData, PatchSet basePs)
throws IOException, PermissionBackendException {
+ List<ChangeData> cds = getUnsortedRelated(changeData, basePs, false);
+ if (cds.isEmpty()) {
+ return Collections.emptyList();
+ }
+ return sorter.sort(cds, basePs);
+ }
+
+ /**
+ * Gets ancestor changes of a specific change revision.
+ *
+ * @param changeData the change of the inputted revision.
+ * @param basePs the revision that the method checks for related changes.
+ * @param alwaysIncludeOriginalChange whether to return the given change when no ancestors found.
+ * @return list of ancestor changes, sorted via {@link RelatedChangesSorter}
+ */
+ public List<RelatedChangesSorter.PatchSetData> getAncestors(
+ ChangeData changeData, PatchSet basePs, boolean alwaysIncludeOriginalChange)
+ throws IOException, PermissionBackendException {
+ List<ChangeData> cds = getUnsortedRelated(changeData, basePs, alwaysIncludeOriginalChange);
+ if (cds.isEmpty()) {
+ return Collections.emptyList();
+ }
+ return sorter.sortAncestors(cds, basePs);
+ }
+
+ private List<ChangeData> getUnsortedRelated(
+ ChangeData changeData, PatchSet basePs, boolean alwaysIncludeOriginalChange) {
Set<String> groups = getAllGroups(changeData.patchSets());
logger.atFine().log("groups = %s", groups);
if (groups.isEmpty()) {
@@ -78,12 +105,10 @@
return Collections.emptyList();
}
if (cds.size() == 1 && cds.get(0).getId().equals(changeData.getId())) {
- return Collections.emptyList();
+ return alwaysIncludeOriginalChange ? cds : Collections.emptyList();
}
- cds = reloadChangeIfStale(cds, changeData, basePs);
-
- return sorter.sort(cds, basePs);
+ return reloadChangeIfStale(cds, changeData, basePs);
}
private List<ChangeData> reloadChangeIfStale(
diff --git a/java/com/google/gerrit/server/change/LabelsJson.java b/java/com/google/gerrit/server/change/LabelsJson.java
index cfa15ae..5555ba6 100644
--- a/java/com/google/gerrit/server/change/LabelsJson.java
+++ b/java/com/google/gerrit/server/change/LabelsJson.java
@@ -36,15 +36,19 @@
import com.google.gerrit.entities.LabelValue;
import com.google.gerrit.entities.PatchSetApproval;
import com.google.gerrit.entities.SubmitRecord;
+import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.common.ApprovalInfo;
import com.google.gerrit.extensions.common.LabelInfo;
import com.google.gerrit.extensions.common.VotingRangeInfo;
import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.account.AccountLoader;
import com.google.gerrit.server.notedb.ReviewerStateInternal;
import com.google.gerrit.server.permissions.LabelPermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.project.DeleteVoteControl;
+import com.google.gerrit.server.project.RemoveReviewerControl;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.inject.Inject;
import com.google.inject.Singleton;
@@ -69,10 +73,17 @@
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private final PermissionBackend permissionBackend;
+ private final DeleteVoteControl deleteVoteControl;
+ private final RemoveReviewerControl removeReviewerControl;
@Inject
- LabelsJson(PermissionBackend permissionBackend) {
+ LabelsJson(
+ PermissionBackend permissionBackend,
+ DeleteVoteControl deleteVoteControl,
+ RemoveReviewerControl removeReviewerControl) {
this.permissionBackend = permissionBackend;
+ this.deleteVoteControl = deleteVoteControl;
+ this.removeReviewerControl = removeReviewerControl;
}
/**
@@ -133,6 +144,46 @@
return permitted.asMap();
}
+ /**
+ * Returns A map of all labels that the provided user has permission to remove.
+ *
+ * @param accountLoader to load the reviewers' data with.
+ * @param user a Gerrit user.
+ * @param cd {@link ChangeData} corresponding to a specific gerrit change.
+ * @return A Map of {@code labelName} -> {Map of {@code value} -> List of {@link AccountInfo}}
+ * that the user can remove votes from.
+ */
+ Map<String, Map<String, List<AccountInfo>>> removableLabels(
+ AccountLoader accountLoader, CurrentUser user, ChangeData cd)
+ throws PermissionBackendException {
+ if (cd.change().isMerged()) {
+ return new HashMap<>();
+ }
+
+ Map<String, Map<String, List<AccountInfo>>> res = new HashMap<>();
+ LabelTypes labelTypes = cd.getLabelTypes();
+ for (PatchSetApproval approval : cd.currentApprovals()) {
+ Optional<LabelType> labelType = labelTypes.byLabel(approval.labelId());
+ if (!labelType.isPresent()) {
+ continue;
+ }
+ if (!(deleteVoteControl.testDeleteVotePermissions(user, cd, approval, labelType.get())
+ || removeReviewerControl.testRemoveReviewer(
+ cd, user, approval.accountId(), approval.value()))) {
+ continue;
+ }
+ if (!res.containsKey(approval.label())) {
+ res.put(approval.label(), new HashMap<>());
+ }
+ String labelValue = LabelValue.formatValue(approval.value());
+ if (!res.get(approval.label()).containsKey(labelValue)) {
+ res.get(approval.label()).put(labelValue, new ArrayList<>());
+ }
+ res.get(approval.label()).get(labelValue).add(accountLoader.get(approval.accountId()));
+ }
+ return res;
+ }
+
private static void clearOnlyZerosEntries(SetMultimap<String, String> permitted) {
List<String> toClear = Lists.newArrayListWithCapacity(permitted.keySet().size());
for (Map.Entry<String, Collection<String>> e : permitted.asMap().entrySet()) {
@@ -217,10 +268,10 @@
}
}
- private Map<String, Short> currentLabels(Account.Id accountId, ChangeData cd) {
+ private Map<String, Short> currentLabels(@Nullable Account.Id accountId, ChangeData cd) {
Map<String, Short> result = new HashMap<>();
for (PatchSetApproval psa : cd.currentApprovals()) {
- if (psa.accountId().equals(accountId)) {
+ if (accountId == null || psa.accountId().equals(accountId)) {
result.put(psa.label(), psa.value());
}
}
diff --git a/java/com/google/gerrit/server/change/PatchSetInserter.java b/java/com/google/gerrit/server/change/PatchSetInserter.java
index 1fe67af..cff3de2 100644
--- a/java/com/google/gerrit/server/change/PatchSetInserter.java
+++ b/java/com/google/gerrit/server/change/PatchSetInserter.java
@@ -22,6 +22,7 @@
import static java.util.Objects.requireNonNull;
import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.ImmutableSet;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.PatchSet;
@@ -361,9 +362,7 @@
@Override
public void postUpdate(PostUpdateContext ctx) {
NotifyResolver.Result notify = ctx.getNotify(change.getId());
- if (notify.shouldNotify() && sendEmail) {
- requireNonNull(mailMessage);
-
+ if (sendEmail) {
emailNewPatchSetFactory
.create(
ctx,
@@ -372,8 +371,8 @@
approvalCopierResult.outdatedApprovals().stream()
.map(ApprovalCopier.Result.PatchSetApprovalData::patchSetApproval)
.collect(toImmutableSet()),
- oldReviewers.byState(REVIEWER),
- oldReviewers.byState(CC),
+ oldReviewers == null ? ImmutableSet.of() : oldReviewers.byState(REVIEWER),
+ oldReviewers == null ? ImmutableSet.of() : oldReviewers.byState(CC),
changeKind,
preUpdateMetaId)
.sendAsync();
diff --git a/java/com/google/gerrit/server/change/RebaseChangeOp.java b/java/com/google/gerrit/server/change/RebaseChangeOp.java
index 4de21d6..49ec812 100644
--- a/java/com/google/gerrit/server/change/RebaseChangeOp.java
+++ b/java/com/google/gerrit/server/change/RebaseChangeOp.java
@@ -22,7 +22,9 @@
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.MergeConflictException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -30,6 +32,8 @@
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.IdentifiedUser.GenericFactory;
+import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.change.RebaseUtil.Base;
import com.google.gerrit.server.git.CodeReviewCommit;
import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
@@ -46,8 +50,8 @@
import com.google.gerrit.server.update.ChangeContext;
import com.google.gerrit.server.update.PostUpdateContext;
import com.google.gerrit.server.update.RepoContext;
-import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
import java.io.IOException;
import java.util.List;
import java.util.Map;
@@ -73,19 +77,24 @@
public class RebaseChangeOp implements BatchUpdateOp {
public interface Factory {
RebaseChangeOp create(ChangeNotes notes, PatchSet originalPatchSet, ObjectId baseCommitId);
+
+ RebaseChangeOp create(ChangeNotes notes, PatchSet originalPatchSet, Change.Id baseChangeId);
}
private final PatchSetInserter.Factory patchSetInserterFactory;
private final MergeUtilFactory mergeUtilFactory;
private final RebaseUtil rebaseUtil;
private final ChangeResource.Factory changeResourceFactory;
+ private final ChangeNotes.Factory notesFactory;
private final ChangeNotes notes;
private final PatchSet originalPatchSet;
private final IdentifiedUser.GenericFactory identifiedUserFactory;
private final ProjectCache projectCache;
+ private final Project.NameKey projectName;
private ObjectId baseCommitId;
+ private Change.Id baseChangeId;
private PersonIdent committerIdent;
private boolean fireRevisionCreated = true;
private boolean validate = true;
@@ -104,26 +113,78 @@
private PatchSetInserter patchSetInserter;
private PatchSet rebasedPatchSet;
- @Inject
+ @AssistedInject
RebaseChangeOp(
PatchSetInserter.Factory patchSetInserterFactory,
MergeUtilFactory mergeUtilFactory,
RebaseUtil rebaseUtil,
ChangeResource.Factory changeResourceFactory,
- IdentifiedUser.GenericFactory identifiedUserFactory,
+ ChangeNotes.Factory notesFactory,
+ GenericFactory identifiedUserFactory,
ProjectCache projectCache,
@Assisted ChangeNotes notes,
@Assisted PatchSet originalPatchSet,
@Assisted ObjectId baseCommitId) {
+ this(
+ patchSetInserterFactory,
+ mergeUtilFactory,
+ rebaseUtil,
+ changeResourceFactory,
+ notesFactory,
+ identifiedUserFactory,
+ projectCache,
+ notes,
+ originalPatchSet);
+ this.baseCommitId = baseCommitId;
+ this.baseChangeId = null;
+ }
+
+ @AssistedInject
+ RebaseChangeOp(
+ PatchSetInserter.Factory patchSetInserterFactory,
+ MergeUtilFactory mergeUtilFactory,
+ RebaseUtil rebaseUtil,
+ ChangeResource.Factory changeResourceFactory,
+ ChangeNotes.Factory notesFactory,
+ GenericFactory identifiedUserFactory,
+ ProjectCache projectCache,
+ @Assisted ChangeNotes notes,
+ @Assisted PatchSet originalPatchSet,
+ @Assisted Change.Id baseChangeId) {
+ this(
+ patchSetInserterFactory,
+ mergeUtilFactory,
+ rebaseUtil,
+ changeResourceFactory,
+ notesFactory,
+ identifiedUserFactory,
+ projectCache,
+ notes,
+ originalPatchSet);
+ this.baseChangeId = baseChangeId;
+ this.baseCommitId = null;
+ }
+
+ private RebaseChangeOp(
+ PatchSetInserter.Factory patchSetInserterFactory,
+ MergeUtilFactory mergeUtilFactory,
+ RebaseUtil rebaseUtil,
+ ChangeResource.Factory changeResourceFactory,
+ ChangeNotes.Factory notesFactory,
+ GenericFactory identifiedUserFactory,
+ ProjectCache projectCache,
+ ChangeNotes notes,
+ PatchSet originalPatchSet) {
this.patchSetInserterFactory = patchSetInserterFactory;
this.mergeUtilFactory = mergeUtilFactory;
this.rebaseUtil = rebaseUtil;
this.changeResourceFactory = changeResourceFactory;
+ this.notesFactory = notesFactory;
this.identifiedUserFactory = identifiedUserFactory;
this.projectCache = projectCache;
this.notes = notes;
+ this.projectName = notes.getProjectName();
this.originalPatchSet = originalPatchSet;
- this.baseCommitId = baseCommitId;
}
public RebaseChangeOp setCommitterIdent(PersonIdent committerIdent) {
@@ -204,14 +265,23 @@
@Override
public void updateRepo(RepoContext ctx)
- throws MergeConflictException, InvalidChangeOperationException, RestApiException, IOException,
- NoSuchChangeException, PermissionBackendException {
+ throws InvalidChangeOperationException, RestApiException, IOException, NoSuchChangeException,
+ PermissionBackendException {
// Ok that originalPatchSet was not read in a transaction, since we just
// need its revision.
RevWalk rw = ctx.getRevWalk();
RevCommit original = rw.parseCommit(originalPatchSet.commitId());
rw.parseBody(original);
- RevCommit baseCommit = rw.parseCommit(baseCommitId);
+ RevCommit baseCommit;
+ if (baseCommitId != null && baseChangeId == null) {
+ baseCommit = rw.parseCommit(baseCommitId);
+ } else if (baseChangeId != null) {
+ baseCommit =
+ PatchSetUtil.getCurrentRevCommitIncludingPending(ctx, notesFactory, baseChangeId);
+ } else {
+ throw new IllegalStateException(
+ "Exactly one of base commit and base change must be provided.");
+ }
CurrentUser changeOwner = identifiedUserFactory.create(notes.getChange().getOwner());
String newCommitMessage;
@@ -224,12 +294,12 @@
newCommitMessage = original.getFullMessage();
}
- rebasedCommit = rebaseCommit(ctx, original, baseCommit, newCommitMessage);
+ rebasedCommit = rebaseCommit(ctx, original, baseCommit, newCommitMessage, notes.getChangeId());
Base base =
rebaseUtil.parseBase(
new RevisionResource(
changeResourceFactory.create(notes, changeOwner), originalPatchSet),
- baseCommitId.name());
+ baseCommit.getName());
rebasedPatchSetId =
ChangeUtil.nextPatchSetIdFromChangeRefs(
@@ -320,8 +390,7 @@
}
private MergeUtil newMergeUtil() {
- ProjectState project =
- projectCache.get(notes.getProjectName()).orElseThrow(illegalState(notes.getProjectName()));
+ ProjectState project = projectCache.get(projectName).orElseThrow(illegalState(projectName));
return forceContentMerge
? mergeUtilFactory.create(project, true)
: mergeUtilFactory.create(project);
@@ -338,7 +407,11 @@
* @throws IOException the merge failed for another reason.
*/
private CodeReviewCommit rebaseCommit(
- RepoContext ctx, RevCommit original, ObjectId base, String commitMessage)
+ RepoContext ctx,
+ RevCommit original,
+ ObjectId base,
+ String commitMessage,
+ Change.Id originalChangeId)
throws ResourceConflictException, IOException {
RevCommit parentCommit = original.getParent(0);
@@ -372,8 +445,9 @@
if (!allowConflicts || !(merger instanceof ResolveMerger)) {
throw new MergeConflictException(
- "The change could not be rebased due to a conflict during merge.\n\n"
- + MergeUtil.createConflictMessage(conflicts));
+ String.format(
+ "Change %s could not be rebased due to a conflict during merge.\n\n%s",
+ originalChangeId.toString(), MergeUtil.createConflictMessage(conflicts)));
}
Map<String, MergeResult<? extends Sequence>> mergeResults =
@@ -413,7 +487,6 @@
cb.getAuthor(), cb.getCommitter().getWhen(), cb.getCommitter().getTimeZone()));
}
ObjectId objectId = ctx.getInserter().insert(cb);
- ctx.getInserter().flush();
CodeReviewCommit commit = ((CodeReviewRevWalk) ctx.getRevWalk()).parseCommit(objectId);
commit.setFilesWithGitConflicts(filesWithGitConflicts);
return commit;
diff --git a/java/com/google/gerrit/server/change/RebaseUtil.java b/java/com/google/gerrit/server/change/RebaseUtil.java
index ba938ee..8acc925 100644
--- a/java/com/google/gerrit/server/change/RebaseUtil.java
+++ b/java/com/google/gerrit/server/change/RebaseUtil.java
@@ -22,12 +22,19 @@
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.extensions.api.changes.RebaseInput;
+import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.git.ObjectIds;
+import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.permissions.ChangePermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.InternalChangeQuery;
import com.google.inject.Inject;
@@ -46,20 +53,65 @@
private final Provider<InternalChangeQuery> queryProvider;
private final ChangeNotes.Factory notesFactory;
private final PatchSetUtil psUtil;
+ private final RebaseChangeOp.Factory rebaseFactory;
@Inject
RebaseUtil(
Provider<InternalChangeQuery> queryProvider,
ChangeNotes.Factory notesFactory,
- PatchSetUtil psUtil) {
+ PatchSetUtil psUtil,
+ RebaseChangeOp.Factory rebaseFactory) {
this.queryProvider = queryProvider;
this.notesFactory = notesFactory;
this.psUtil = psUtil;
+ this.rebaseFactory = rebaseFactory;
+ }
+
+ /**
+ * Checks whether the given change fulfills all preconditions to be rebased.
+ *
+ * <p>This method does not check whether the calling user is allowed to rebase the change.
+ */
+ public void verifyRebasePreconditions(RevWalk rw, ChangeNotes changeNotes, PatchSet patchSet)
+ throws ResourceConflictException, IOException {
+ // Not allowed to rebase if the current patch set is locked.
+ psUtil.checkPatchSetNotLocked(changeNotes);
+
+ Change change = changeNotes.getChange();
+ if (!change.isNew()) {
+ throw new ResourceConflictException(
+ String.format("Change %s is %s", change.getId(), ChangeUtil.status(change)));
+ }
+
+ if (!hasOneParent(rw, patchSet)) {
+ throw new ResourceConflictException(
+ String.format(
+ "Error rebasing %s. Cannot rebase %s",
+ change.getId(),
+ countParents(rw, patchSet) > 1 ? "merge commits" : "commit with no ancestor"));
+ }
+ }
+
+ public static boolean hasOneParent(RevWalk rw, PatchSet ps) throws IOException {
+ // Prevent rebase of exotic changes (merge commit, no ancestor).
+ return countParents(rw, ps) == 1;
+ }
+
+ private static int countParents(RevWalk rw, PatchSet ps) throws IOException {
+ RevCommit c = rw.parseCommit(ps.commitId());
+ return c.getParentCount();
+ }
+
+ private static boolean isMergedInto(RevWalk rw, PatchSet base, PatchSet tip) throws IOException {
+ ObjectId baseId = base.commitId();
+ ObjectId tipId = tip.commitId();
+ return rw.isMergedInto(rw.parseCommit(baseId), rw.parseCommit(tipId));
}
public boolean canRebase(PatchSet patchSet, BranchNameKey dest, Repository git, RevWalk rw) {
try {
- findBaseRevision(patchSet, dest, git, rw);
+ @SuppressWarnings("unused")
+ ObjectId base = findBaseRevision(patchSet, dest, git, rw, true);
return true;
} catch (RestApiException e) {
return false;
@@ -129,6 +181,100 @@
}
/**
+ * Parse or find the commit onto which a patch set should be rebased.
+ *
+ * <p>If a {@code rebaseInput.base} is provided, parse it. Otherwise, finds the latest patch set
+ * of the change corresponding to this commit's parent, or the destination branch tip in the case
+ * where the parent's change is merged.
+ *
+ * @param git the repository.
+ * @param rw the RevWalk.
+ * @param permissionBackend to check base reading permissions with.
+ * @param rsrc to find the base for
+ * @param rebaseInput to optionally parse the base from.
+ * @param verifyNeedsRebase whether to verify if the change base is not already up to date
+ * @return the commit onto which the patch set should be rebased.
+ * @throws RestApiException if rebase is not possible.
+ * @throws IOException if accessing the repository fails.
+ * @throws PermissionBackendException if the user don't have permissions to read the base change.
+ */
+ public ObjectId parseOrFindBaseRevision(
+ Repository git,
+ RevWalk rw,
+ PermissionBackend permissionBackend,
+ RevisionResource rsrc,
+ RebaseInput rebaseInput,
+ boolean verifyNeedsRebase)
+ throws RestApiException, IOException, PermissionBackendException {
+ Change change = rsrc.getChange();
+
+ if (rebaseInput == null || rebaseInput.base == null) {
+ return findBaseRevision(rsrc.getPatchSet(), change.getDest(), git, rw, verifyNeedsRebase);
+ }
+
+ String inputBase = rebaseInput.base.trim();
+
+ if (inputBase.isEmpty()) {
+ return getDestRefTip(git, change.getDest());
+ }
+
+ Base base;
+ try {
+ base = parseBase(rsrc, inputBase);
+ } catch (NoSuchChangeException e) {
+ throw new UnprocessableEntityException(
+ String.format("Base change not found: %s", inputBase), e);
+ }
+ if (base == null) {
+ throw new ResourceConflictException(
+ "base revision is missing from the destination branch: " + inputBase);
+ }
+ return getLatestRevisionForBaseChange(rw, permissionBackend, rsrc, base);
+ }
+
+ private ObjectId getDestRefTip(Repository git, BranchNameKey destRefKey)
+ throws ResourceConflictException, IOException {
+ // Remove existing dependency to other patch set.
+ Ref destRef = git.exactRef(destRefKey.branch());
+ if (destRef == null) {
+ throw new ResourceConflictException(
+ "can't rebase onto tip of branch " + destRefKey.branch() + "; branch doesn't exist");
+ }
+ return destRef.getObjectId();
+ }
+
+ private ObjectId getLatestRevisionForBaseChange(
+ RevWalk rw, PermissionBackend permissionBackend, RevisionResource childRsrc, Base base)
+ throws ResourceConflictException, AuthException, PermissionBackendException, IOException {
+
+ Change child = childRsrc.getChange();
+ PatchSet.Id baseId = base.patchSet().id();
+ if (child.getId().equals(baseId.changeId())) {
+ throw new ResourceConflictException(
+ String.format("cannot rebase change %s onto itself", childRsrc.getChange().getId()));
+ }
+
+ permissionBackend.user(childRsrc.getUser()).change(base.notes()).check(ChangePermission.READ);
+
+ Change baseChange = base.notes().getChange();
+ if (!baseChange.getProject().equals(child.getProject())) {
+ throw new ResourceConflictException(
+ "base change is in wrong project: " + baseChange.getProject());
+ } else if (!baseChange.getDest().equals(child.getDest())) {
+ throw new ResourceConflictException(
+ "base change is targeting wrong branch: " + baseChange.getDest());
+ } else if (baseChange.isAbandoned()) {
+ throw new ResourceConflictException("base change is abandoned: " + baseChange.getKey());
+ } else if (isMergedInto(rw, childRsrc.getPatchSet(), base.patchSet())) {
+ throw new ResourceConflictException(
+ "base change "
+ + baseChange.getKey()
+ + " is a descendant of the current change - recursion not allowed");
+ }
+ return base.patchSet().commitId();
+ }
+
+ /**
* Find the commit onto which a patch set should be rebased.
*
* <p>This is defined as the latest patch set of the change corresponding to this commit's parent,
@@ -138,12 +284,17 @@
* @param destBranch the destination branch.
* @param git the repository.
* @param rw the RevWalk.
+ * @param verifyNeedsRebase whether to verify if the change base is not already up to date
* @return the commit onto which the patch set should be rebased.
* @throws RestApiException if rebase is not possible.
* @throws IOException if accessing the repository fails.
*/
public ObjectId findBaseRevision(
- PatchSet patchSet, BranchNameKey destBranch, Repository git, RevWalk rw)
+ PatchSet patchSet,
+ BranchNameKey destBranch,
+ Repository git,
+ RevWalk rw,
+ boolean verifyNeedsRebase)
throws RestApiException, IOException {
ObjectId baseId = null;
RevCommit commit = rw.parseCommit(patchSet.commitId());
@@ -170,7 +321,7 @@
}
if (depChange.isNew()) {
- if (depPatchSet.id().equals(depChange.currentPatchSetId())) {
+ if (verifyNeedsRebase && depPatchSet.id().equals(depChange.currentPatchSetId())) {
throw new ResourceConflictException(
"Change is already based on the latest patch set of the dependent change.");
}
@@ -189,10 +340,29 @@
"The destination branch does not exist: " + destBranch.branch());
}
baseId = destRef.getObjectId();
- if (baseId.equals(parentId)) {
+ if (verifyNeedsRebase && baseId.equals(parentId)) {
throw new ResourceConflictException("Change is already up to date.");
}
}
return baseId;
}
+
+ public RebaseChangeOp getRebaseOp(RevisionResource revRsrc, RebaseInput input, ObjectId baseRev) {
+ return applyRebaseInputToOp(
+ rebaseFactory.create(revRsrc.getNotes(), revRsrc.getPatchSet(), baseRev), input);
+ }
+
+ public RebaseChangeOp getRebaseOp(
+ RevisionResource revRsrc, RebaseInput input, Change.Id baseChange) {
+ return applyRebaseInputToOp(
+ rebaseFactory.create(revRsrc.getNotes(), revRsrc.getPatchSet(), baseChange), input);
+ }
+
+ private RebaseChangeOp applyRebaseInputToOp(RebaseChangeOp op, RebaseInput input) {
+ return op.setForceContentMerge(true)
+ .setAllowConflicts(input.allowConflicts)
+ .setValidationOptions(
+ ValidationOptionsUtil.getValidateOptionsAsMultimap(input.validationOptions))
+ .setFireRevisionCreated(true);
+ }
}
diff --git a/java/com/google/gerrit/server/change/RelatedChangesSorter.java b/java/com/google/gerrit/server/change/RelatedChangesSorter.java
index b6e3121..f4b1a83c 100644
--- a/java/com/google/gerrit/server/change/RelatedChangesSorter.java
+++ b/java/com/google/gerrit/server/change/RelatedChangesSorter.java
@@ -75,16 +75,7 @@
checkArgument(!in.isEmpty(), "Input may not be empty");
// Map of all patch sets, keyed by commit SHA-1.
Map<ObjectId, PatchSetData> byId = collectById(in);
- PatchSetData start = byId.get(startPs.commitId());
- requireNonNull(
- start,
- () ->
- String.format(
- "commit %s of patch set %s not found in %s",
- startPs.commitId().name(),
- startPs.id(),
- byId.entrySet().stream()
- .collect(toMap(e -> e.getKey().name(), e -> e.getValue().patchSet().id()))));
+ PatchSetData start = getCheckedPatchSetData(byId, startPs);
// Map of patch set -> immediate parent.
ListMultimap<PatchSetData, PatchSetData> parents =
@@ -120,6 +111,34 @@
return result;
}
+ public List<PatchSetData> sortAncestors(List<ChangeData> in, PatchSet startPs)
+ throws IOException, PermissionBackendException {
+ checkArgument(!in.isEmpty(), "Input may not be empty");
+ // Map of all patch sets, keyed by commit SHA-1.
+ Map<ObjectId, PatchSetData> byId = collectById(in);
+ PatchSetData start = getCheckedPatchSetData(byId, startPs);
+
+ // Map of patch set -> immediate parent.
+ ListMultimap<PatchSetData, PatchSetData> parents =
+ MultimapBuilder.hashKeys(in.size()).arrayListValues(3).build();
+
+ for (ChangeData cd : in) {
+ for (PatchSet ps : cd.patchSets()) {
+ PatchSetData thisPsd = requireNonNull(byId.get(ps.commitId()));
+
+ for (RevCommit p : thisPsd.commit().getParents()) {
+ PatchSetData parentPsd = byId.get(p);
+ if (parentPsd != null) {
+ parents.put(thisPsd, parentPsd);
+ }
+ }
+ }
+ }
+
+ Collection<PatchSetData> ancestors = walkAncestors(parents, start);
+ return List.copyOf(ancestors);
+ }
+
private Map<ObjectId, PatchSetData> collectById(List<ChangeData> in) throws IOException {
Project.NameKey project = in.get(0).change().getProject();
Map<ObjectId, PatchSetData> result = Maps.newHashMapWithExpectedSize(in.size() * 3);
@@ -143,6 +162,19 @@
return result;
}
+ private PatchSetData getCheckedPatchSetData(Map<ObjectId, PatchSetData> byId, PatchSet ps) {
+ PatchSetData psData = byId.get(ps.commitId());
+ return requireNonNull(
+ psData,
+ () ->
+ String.format(
+ "commit %s of patch set %s not found in %s",
+ ps.commitId().name(),
+ ps.id(),
+ byId.entrySet().stream()
+ .collect(toMap(e -> e.getKey().name(), e -> e.getValue().patchSet().id()))));
+ }
+
private Collection<PatchSetData> walkAncestors(
ListMultimap<PatchSetData, PatchSetData> parents, PatchSetData start)
throws PermissionBackendException {
diff --git a/java/com/google/gerrit/server/change/ReviewerModifier.java b/java/com/google/gerrit/server/change/ReviewerModifier.java
index f3ad4f7..b5e0181 100644
--- a/java/com/google/gerrit/server/change/ReviewerModifier.java
+++ b/java/com/google/gerrit/server/change/ReviewerModifier.java
@@ -277,8 +277,9 @@
IdentifiedUser reviewerUser;
boolean exactMatchFound = false;
try {
- if (input instanceof InternalReviewerInput
- && ((InternalReviewerInput) input).skipVisibilityCheck) {
+ if (ReviewerState.REMOVED.equals(input.state)
+ || (input instanceof InternalReviewerInput
+ && ((InternalReviewerInput) input).skipVisibilityCheck)) {
reviewerUser =
accountResolver.resolveIncludeInactiveIgnoreVisibility(input.reviewer).asUniqueUser();
} else {
diff --git a/java/com/google/gerrit/server/change/ValidationOptionsUtil.java b/java/com/google/gerrit/server/change/ValidationOptionsUtil.java
new file mode 100644
index 0000000..137239c
--- /dev/null
+++ b/java/com/google/gerrit/server/change/ValidationOptionsUtil.java
@@ -0,0 +1,38 @@
+// Copyright (C) 2022 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.change;
+
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.gerrit.common.Nullable;
+import java.util.Map;
+
+/** Utilities for validation options parsing. */
+public final class ValidationOptionsUtil {
+ public static ImmutableListMultimap<String, String> getValidateOptionsAsMultimap(
+ @Nullable Map<String, String> validationOptions) {
+ if (validationOptions == null) {
+ return ImmutableListMultimap.of();
+ }
+
+ ImmutableListMultimap.Builder<String, String> validationOptionsBuilder =
+ ImmutableListMultimap.builder();
+ validationOptions
+ .entrySet()
+ .forEach(e -> validationOptionsBuilder.put(e.getKey(), e.getValue()));
+ return validationOptionsBuilder.build();
+ }
+
+ private ValidationOptionsUtil() {}
+}
diff --git a/java/com/google/gerrit/server/experiments/ExperimentFeaturesConstants.java b/java/com/google/gerrit/server/experiments/ExperimentFeaturesConstants.java
index 5a4580c..32ec401 100644
--- a/java/com/google/gerrit/server/experiments/ExperimentFeaturesConstants.java
+++ b/java/com/google/gerrit/server/experiments/ExperimentFeaturesConstants.java
@@ -20,14 +20,9 @@
public class ExperimentFeaturesConstants {
/** Features that are known experiments and can be referenced in the code. */
- public static String UI_FEATURE_PATCHSET_COMMENTS = "UiFeature__patchset_comments";
-
- public static String UI_FEATURE_SUBMIT_REQUIREMENTS_UI = "UiFeature__submit_requirements_ui";
-
public static String GERRIT_BACKEND_FEATURE_ATTACH_NONCE_TO_DOCUMENTATION =
"GerritBackendFeature__attach_nonce_to_documentation";
/** Features, enabled by default in the current release. */
- public static final ImmutableSet<String> DEFAULT_ENABLED_FEATURES =
- ImmutableSet.of(UI_FEATURE_PATCHSET_COMMENTS, UI_FEATURE_SUBMIT_REQUIREMENTS_UI);
+ public static final ImmutableSet<String> DEFAULT_ENABLED_FEATURES = ImmutableSet.of();
}
diff --git a/java/com/google/gerrit/server/git/CommitUtil.java b/java/com/google/gerrit/server/git/CommitUtil.java
index 2841f92..f0b2a78 100644
--- a/java/com/google/gerrit/server/git/CommitUtil.java
+++ b/java/com/google/gerrit/server/git/CommitUtil.java
@@ -17,7 +17,6 @@
import static com.google.common.base.MoreObjects.firstNonNull;
import com.google.common.base.Strings;
-import com.google.common.collect.ImmutableListMultimap;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
@@ -41,6 +40,7 @@
import com.google.gerrit.server.change.ChangeInserter;
import com.google.gerrit.server.change.ChangeMessages;
import com.google.gerrit.server.change.NotifyResolver;
+import com.google.gerrit.server.change.ValidationOptionsUtil;
import com.google.gerrit.server.extensions.events.ChangeReverted;
import com.google.gerrit.server.mail.send.MessageIdGenerator;
import com.google.gerrit.server.mail.send.RevertedSender;
@@ -64,7 +64,6 @@
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
-import java.util.Map;
import java.util.Set;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.errors.InvalidObjectIdException;
@@ -317,7 +316,8 @@
.create(changeId, revertCommit, changeToRevert.getDest().branch())
.setTopic(input.topic == null ? changeToRevert.getTopic() : input.topic.trim());
ins.setMessage("Uploaded patch set 1.");
- ins.setValidationOptions(getValidateOptionsAsMultimap(input.validationOptions));
+ ins.setValidationOptions(
+ ValidationOptionsUtil.getValidateOptionsAsMultimap(input.validationOptions));
ReviewerSet reviewerSet = approvalsUtil.getReviewers(notes);
@@ -344,20 +344,6 @@
return changeId;
}
- private static ImmutableListMultimap<String, String> getValidateOptionsAsMultimap(
- @Nullable Map<String, String> validationOptions) {
- if (validationOptions == null) {
- return ImmutableListMultimap.of();
- }
-
- ImmutableListMultimap.Builder<String, String> validationOptionsBuilder =
- ImmutableListMultimap.builder();
- validationOptions
- .entrySet()
- .forEach(e -> validationOptionsBuilder.put(e.getKey(), e.getValue()));
- return validationOptionsBuilder.build();
- }
-
/**
* Notify the owners of a change that their change is being reverted.
*
diff --git a/java/com/google/gerrit/server/group/db/GroupsUpdate.java b/java/com/google/gerrit/server/group/db/GroupsUpdate.java
index c0c934b..87d8db1 100644
--- a/java/com/google/gerrit/server/group/db/GroupsUpdate.java
+++ b/java/com/google/gerrit/server/group/db/GroupsUpdate.java
@@ -115,7 +115,6 @@
private final RetryHelper retryHelper;
@AssistedInject
- @SuppressWarnings("BindingAnnotationWithoutInject")
GroupsUpdate(
GitRepositoryManager repoManager,
AllUsersName allUsersName,
@@ -150,7 +149,6 @@
}
@AssistedInject
- @SuppressWarnings("BindingAnnotationWithoutInject")
GroupsUpdate(
GitRepositoryManager repoManager,
AllUsersName allUsersName,
@@ -185,7 +183,6 @@
Optional.of(currentUser));
}
- @SuppressWarnings("BindingAnnotationWithoutInject")
private GroupsUpdate(
GitRepositoryManager repoManager,
AllUsersName allUsersName,
diff --git a/java/com/google/gerrit/server/index/IndexUtils.java b/java/com/google/gerrit/server/index/IndexUtils.java
index 213094e..352d376 100644
--- a/java/com/google/gerrit/server/index/IndexUtils.java
+++ b/java/com/google/gerrit/server/index/IndexUtils.java
@@ -116,9 +116,9 @@
*/
public static Set<String> projectFields(QueryOptions opts) {
Set<String> fs = opts.fields();
- return fs.contains(ProjectField.NAME.getName())
+ return fs.contains(ProjectField.NAME_SPEC.getName())
? fs
- : Sets.union(fs, ImmutableSet.of(ProjectField.NAME.getName()));
+ : Sets.union(fs, ImmutableSet.of(ProjectField.NAME_SPEC.getName()));
}
private IndexUtils() {
diff --git a/java/com/google/gerrit/server/index/account/AccountIndex.java b/java/com/google/gerrit/server/index/account/AccountIndex.java
index ca7264c..66b85af 100644
--- a/java/com/google/gerrit/server/index/account/AccountIndex.java
+++ b/java/com/google/gerrit/server/index/account/AccountIndex.java
@@ -20,6 +20,7 @@
import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.query.account.AccountPredicates;
+import java.util.function.Function;
/**
* Index for Gerrit accounts. This class is mainly used for typing the generic parent class that
@@ -32,4 +33,6 @@
default Predicate<AccountState> keyPredicate(Account.Id id) {
return AccountPredicates.id(getSchema(), id);
}
+
+ Function<AccountState, Account.Id> ENTITY_TO_KEY = (a) -> a.account().id();
}
diff --git a/java/com/google/gerrit/server/index/change/ChangeField.java b/java/com/google/gerrit/server/index/change/ChangeField.java
index 254a7e5..1c89566f 100644
--- a/java/com/google/gerrit/server/index/change/ChangeField.java
+++ b/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -988,7 +988,7 @@
/** Serialized change object, used for pre-populating results. */
private static final TypeToken<Entities.Change> CHANGE_TYPE_TOKEN =
- new TypeToken<Entities.Change>() {
+ new TypeToken<>() {
private static final long serialVersionUID = 1L;
};
@@ -1007,7 +1007,7 @@
/** Serialized approvals for the current patch set, used for pre-populating results. */
private static final TypeToken<Iterable<Entities.PatchSetApproval>> APPROVAL_TYPE_TOKEN =
- new TypeToken<Iterable<Entities.PatchSetApproval>>() {
+ new TypeToken<>() {
private static final long serialVersionUID = 1L;
};
@@ -1086,7 +1086,7 @@
public static final IndexedField<ChangeData, String>.SearchSpec COMMIT_MESSAGE_EXACT =
COMMIT_MESSAGE_EXACT_FIELD.exact(ChangeQueryBuilder.FIELD_MESSAGE_EXACT);
- /** Commit message of the current patch set. */
+ /** Subject of the current patch set (aka first line of the commit message). */
public static final IndexedField<ChangeData, String> SUBJECT_FIELD =
IndexedField.<ChangeData>stringBuilder("Subject")
.required()
@@ -1095,6 +1095,9 @@
public static final IndexedField<ChangeData, String>.SearchSpec SUBJECT_SPEC =
SUBJECT_FIELD.fullText(ChangeQueryBuilder.FIELD_SUBJECT);
+ public static final IndexedField<ChangeData, String>.SearchSpec PREFIX_SUBJECT_SPEC =
+ SUBJECT_FIELD.prefix(ChangeQueryBuilder.FIELD_PREFIX_SUBJECT);
+
/** Summary or inline comment. */
public static final IndexedField<ChangeData, Iterable<String>> COMMENT_FIELD =
IndexedField.<ChangeData>iterableStringBuilder("Comment")
@@ -1297,7 +1300,7 @@
/** Serialized patch set object, used for pre-populating results. */
private static final TypeToken<Iterable<Entities.PatchSet>> PATCH_SET_TYPE_TOKEN =
- new TypeToken<Iterable<Entities.PatchSet>>() {
+ new TypeToken<>() {
private static final long serialVersionUID = 1L;
};
@@ -1618,7 +1621,7 @@
/** Serialized submit requirements, used for pre-populating results. */
private static final TypeToken<Iterable<Cache.SubmitRequirementResultProto>>
STORED_SUBMIT_REQUIREMENTS_TYPE_TOKEN =
- new TypeToken<Iterable<Cache.SubmitRequirementResultProto>>() {
+ new TypeToken<>() {
private static final long serialVersionUID = 1L;
};
diff --git a/java/com/google/gerrit/server/index/change/ChangeIndex.java b/java/com/google/gerrit/server/index/change/ChangeIndex.java
index 6fc2665..74e9af1 100644
--- a/java/com/google/gerrit/server/index/change/ChangeIndex.java
+++ b/java/com/google/gerrit/server/index/change/ChangeIndex.java
@@ -20,6 +20,7 @@
import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.ChangePredicates;
+import java.util.function.Function;
/**
* Index for Gerrit changes. This class is mainly used for typing the generic parent class that
@@ -32,4 +33,6 @@
default Predicate<ChangeData> keyPredicate(Change.Id id) {
return ChangePredicates.idStr(id);
}
+
+ Function<ChangeData, Change.Id> ENTITY_TO_KEY = ChangeData::getId;
}
diff --git a/java/com/google/gerrit/server/index/change/ChangeIndexer.java b/java/com/google/gerrit/server/index/change/ChangeIndexer.java
index 4b88919..dc3907d 100644
--- a/java/com/google/gerrit/server/index/change/ChangeIndexer.java
+++ b/java/com/google/gerrit/server/index/change/ChangeIndexer.java
@@ -46,6 +46,7 @@
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
+import java.util.Optional;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
@@ -283,9 +284,9 @@
* @param id change to delete.
* @return future for the deleting task, the result of the future is always {@code null}
*/
- public ListenableFuture<ChangeData> deleteAsync(Change.Id id) {
+ public ListenableFuture<ChangeData> deleteAsync(Project.NameKey project, Change.Id id) {
fireChangeScheduledForDeletionFromIndexEvent(id.get());
- return submit(new DeleteTask(id));
+ return submit(new DeleteTask(id, Optional.of(project)));
}
/**
@@ -298,8 +299,12 @@
doDelete(id);
}
+ private void doDelete(Project.NameKey project, Change.Id id) {
+ new DeleteTask(id, Optional.of(project)).call();
+ }
+
private void doDelete(Change.Id id) {
- new DeleteTask(id).call();
+ new DeleteTask(id, Optional.empty()).call();
}
/**
@@ -423,7 +428,7 @@
doIndex(changeData);
return changeData;
} catch (NoSuchChangeException e) {
- doDelete(id);
+ doDelete(project, id);
}
return null;
}
@@ -456,9 +461,11 @@
// Not AbstractIndexTask as it doesn't need a request context.
private class DeleteTask implements Callable<ChangeData> {
private final Change.Id id;
+ private final Optional<Project.NameKey> project;
- private DeleteTask(Change.Id id) {
+ private DeleteTask(Change.Id id, Optional<Project.NameKey> project) {
this.id = id;
+ this.project = project;
}
@Nullable
@@ -476,7 +483,12 @@
.changeId(id.get())
.indexVersion(i.getSchema().getVersion())
.build())) {
- i.delete(id);
+ // Some index implementation require ProjectKey to build a database key
+ // If delete(K) method is used, this will require changeId -> projectKey lookup (index
+ // query), which is expensive.
+ // Use changeData with ProjectKey and deleteByValue(V) method, if possible
+ project.ifPresentOrElse(
+ p -> i.deleteByValue(changeDataFactory.create(p, id)), () -> i.delete(id));
} catch (RuntimeException e) {
throw new StorageException(
String.format(
diff --git a/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java b/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
index 5dc1500..895c4d8 100644
--- a/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
+++ b/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
@@ -214,6 +214,7 @@
@Deprecated static final Schema<ChangeData> V78 = schema(V77);
/** Remove draft and star fields. */
+ @Deprecated
static final Schema<ChangeData> V79 =
new Schema.Builder<ChangeData>()
.add(V78)
@@ -222,6 +223,7 @@
.build();
/** Add subject field. */
+ @Deprecated
static final Schema<ChangeData> V80 =
new Schema.Builder<ChangeData>()
.add(V79)
@@ -229,6 +231,13 @@
.addSearchSpecs(ChangeField.SUBJECT_SPEC)
.build();
+ /** Add prefixsubject field. */
+ static final Schema<ChangeData> V81 =
+ new Schema.Builder<ChangeData>()
+ .add(V80)
+ .addSearchSpecs(ChangeField.PREFIX_SUBJECT_SPEC)
+ .build();
+
/**
* Name of the change index to be used when contacting index backends or loading configurations.
*/
diff --git a/java/com/google/gerrit/server/index/change/IndexedChangeQuery.java b/java/com/google/gerrit/server/index/change/IndexedChangeQuery.java
index 26477a4..8f5e36e 100644
--- a/java/com/google/gerrit/server/index/change/IndexedChangeQuery.java
+++ b/java/com/google/gerrit/server/index/change/IndexedChangeQuery.java
@@ -16,6 +16,7 @@
import static com.google.common.base.Preconditions.checkState;
import static com.google.gerrit.server.index.change.ChangeField.CHANGE_SPEC;
+import static com.google.gerrit.server.index.change.ChangeField.NUMERIC_ID_STR_SPEC;
import static com.google.gerrit.server.index.change.ChangeField.PROJECT_SPEC;
import com.google.common.annotations.VisibleForTesting;
@@ -68,10 +69,13 @@
int pageSizeMultiplier,
int limit,
Set<String> fields) {
- // Always include project since it is needed to load the change from NoteDb.
- if (!fields.contains(CHANGE_SPEC.getName()) && !fields.contains(PROJECT_SPEC.getName())) {
+ // Always include project and change id since both are needed to load the change from NoteDb.
+ if (!fields.contains(CHANGE_SPEC.getName())
+ && !(fields.contains(PROJECT_SPEC.getName())
+ && fields.contains(NUMERIC_ID_STR_SPEC.getName()))) {
fields = new HashSet<>(fields);
fields.add(PROJECT_SPEC.getName());
+ fields.add(NUMERIC_ID_STR_SPEC.getName());
}
return QueryOptions.create(config, start, pageSize, pageSizeMultiplier, limit, fields);
}
diff --git a/java/com/google/gerrit/server/index/group/GroupIndex.java b/java/com/google/gerrit/server/index/group/GroupIndex.java
index 28c0384..f6a9224 100644
--- a/java/com/google/gerrit/server/index/group/GroupIndex.java
+++ b/java/com/google/gerrit/server/index/group/GroupIndex.java
@@ -20,6 +20,7 @@
import com.google.gerrit.index.IndexDefinition;
import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.server.query.group.GroupPredicates;
+import java.util.function.Function;
/**
* Index for internal Gerrit groups. This class is mainly used for typing the generic parent class
@@ -33,4 +34,6 @@
default Predicate<InternalGroup> keyPredicate(AccountGroup.UUID uuid) {
return GroupPredicates.uuid(uuid);
}
+
+ Function<InternalGroup, AccountGroup.UUID> ENTITY_TO_KEY = (g) -> g.getGroupUUID();
}
diff --git a/java/com/google/gerrit/server/index/project/StalenessChecker.java b/java/com/google/gerrit/server/index/project/StalenessChecker.java
index 9c44c00..9f6bb31 100644
--- a/java/com/google/gerrit/server/index/project/StalenessChecker.java
+++ b/java/com/google/gerrit/server/index/project/StalenessChecker.java
@@ -40,7 +40,7 @@
*/
public class StalenessChecker {
private static final ImmutableSet<String> FIELDS =
- ImmutableSet.of(ProjectField.NAME.getName(), ProjectField.REF_STATE.getName());
+ ImmutableSet.of(ProjectField.NAME_SPEC.getName(), ProjectField.REF_STATE.getName());
private final ProjectCache projectCache;
private final ProjectIndexCollection indexes;
diff --git a/java/com/google/gerrit/server/mail/receive/MailProcessor.java b/java/com/google/gerrit/server/mail/receive/MailProcessor.java
index e362c4b..2b8a501 100644
--- a/java/com/google/gerrit/server/mail/receive/MailProcessor.java
+++ b/java/com/google/gerrit/server/mail/receive/MailProcessor.java
@@ -85,7 +85,13 @@
import java.util.Optional;
import java.util.Set;
-/** A service that can attach the comments from a {@link MailMessage} to a change. */
+/**
+ * Users can post comments on gerrit changes by replying directly to gerrit emails. This service
+ * parses the {@link MailMessage} sent by users and attaches the comments to a change.
+ *
+ * <p>This functionality can be configured or disabled by host. See {@link
+ * com.google.gerrit.server.mail.receive.MailReceiver.MailReceiverModule}
+ */
@Singleton
public class MailProcessor {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
diff --git a/java/com/google/gerrit/server/mail/receive/MailReceiver.java b/java/com/google/gerrit/server/mail/receive/MailReceiver.java
index 23e1cc3..a308168 100644
--- a/java/com/google/gerrit/server/mail/receive/MailReceiver.java
+++ b/java/com/google/gerrit/server/mail/receive/MailReceiver.java
@@ -131,27 +131,20 @@
if (async) {
@SuppressWarnings("unused")
Future<?> possiblyIgnoredError =
- workQueue
- .getDefaultQueue()
- .submit(
- () -> {
- try {
- mailProcessor.process(m);
- requestDeletion(m.id());
- } catch (RestApiException | UpdateException e) {
- logger.atSevere().withCause(e).log(
- "Mail: Can't process message %s . Won't delete.", m.id());
- }
- });
+ workQueue.getDefaultQueue().submit(() -> processMessage(m));
} else {
// Synchronous processing is used only in tests.
- try {
- mailProcessor.process(m);
- requestDeletion(m.id());
- } catch (RestApiException | UpdateException e) {
- logger.atSevere().withCause(e).log("Mail: Can't process messages. Won't delete.");
- }
+ processMessage(m);
}
}
}
+
+ private void processMessage(MailMessage m) {
+ try {
+ mailProcessor.process(m);
+ requestDeletion(m.id());
+ } catch (RestApiException | UpdateException e) {
+ logger.atSevere().withCause(e).log("Mail: Can't process message %s . Won't delete.", m.id());
+ }
+ }
}
diff --git a/java/com/google/gerrit/server/mail/send/NewChangeSender.java b/java/com/google/gerrit/server/mail/send/NewChangeSender.java
index 800066e..968bb1a 100644
--- a/java/com/google/gerrit/server/mail/send/NewChangeSender.java
+++ b/java/com/google/gerrit/server/mail/send/NewChangeSender.java
@@ -98,7 +98,7 @@
}
@Nullable
- public List<String> getReviewerNames() {
+ private List<String> getReviewerNames() {
if (reviewers.isEmpty()) {
return null;
}
@@ -110,7 +110,7 @@
}
@Nullable
- public List<String> getRemovedReviewerNames() {
+ private List<String> getRemovedReviewerNames() {
if (removedReviewers.isEmpty() && removedByEmailReviewers.isEmpty()) {
return null;
}
diff --git a/java/com/google/gerrit/server/mail/send/ProjectWatch.java b/java/com/google/gerrit/server/mail/send/ProjectWatch.java
index 9299d74..cbf47c5 100644
--- a/java/com/google/gerrit/server/mail/send/ProjectWatch.java
+++ b/java/com/google/gerrit/server/mail/send/ProjectWatch.java
@@ -16,6 +16,7 @@
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AccountGroup;
@@ -24,6 +25,7 @@
import com.google.gerrit.entities.GroupReference;
import com.google.gerrit.entities.NotifyConfig;
import com.google.gerrit.entities.Project;
+import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.index.query.QueryParseException;
import com.google.gerrit.server.CurrentUser;
@@ -35,11 +37,13 @@
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.ChangeQueryBuilder;
import com.google.gerrit.server.query.change.GroupBackedUser;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
+import org.eclipse.jgit.errors.ConfigInvalidException;
public class ProjectWatch {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -240,15 +244,16 @@
}
private boolean filterMatch(CurrentUser user, String filter) throws QueryParseException {
- ChangeQueryBuilder qb;
+ WatcherChangeQueryBuilder qb;
Predicate<ChangeData> p = null;
if (user == null) {
- qb = args.queryBuilder.get().asUser(args.anonymousUser.get());
+ qb = WatcherChangeQueryBuilder.asUser(args.queryBuilder.get(), args.anonymousUser.get());
} else {
- qb = args.queryBuilder.get().asUser(user);
+ qb = WatcherChangeQueryBuilder.asUser(args.queryBuilder.get(), user);
p = qb.isVisible();
}
+ qb.forceAccountVisibilityCheck();
if (filter != null) {
Predicate<ChangeData> filterPredicate = qb.parse(filter);
@@ -260,4 +265,40 @@
}
return p == null || p.asMatchable().match(changeData);
}
+
+ private static class WatcherChangeQueryBuilder extends ChangeQueryBuilder {
+ private WatcherChangeQueryBuilder(Arguments args) {
+ super(args);
+ }
+
+ public static WatcherChangeQueryBuilder asUser(ChangeQueryBuilder other, CurrentUser user) {
+ return new WatcherChangeQueryBuilder(other.getArgs().asUser(user));
+ }
+
+ @Override
+ protected Predicate<ChangeData> defaultField(String query) throws QueryParseException {
+ if (query.startsWith("refs/")) {
+ return ref(query);
+ }
+
+ // Adapt the capacity of this list when adding more default predicates.
+ List<Predicate<ChangeData>> predicates = Lists.newArrayListWithCapacity(11);
+ predicates.add(file(query));
+ try {
+ predicates.add(label(query));
+ } catch (StorageException | IOException | ConfigInvalidException | QueryParseException e) {
+ // Skip.
+ }
+ predicates.add(commit(query));
+ predicates.add(message(query));
+ predicates.add(comment(query));
+ predicates.add(projects(query));
+ predicates.add(ref(query));
+ predicates.add(branch(query));
+ predicates.add(topic(query));
+ // Adapt the capacity of the "predicates" list when adding more default
+ // predicates.
+ return Predicate.or(predicates);
+ }
+ }
}
diff --git a/java/com/google/gerrit/server/patch/gitfilediff/GitFileDiffCacheImpl.java b/java/com/google/gerrit/server/patch/gitfilediff/GitFileDiffCacheImpl.java
index 2b856fb..43a212c 100644
--- a/java/com/google/gerrit/server/patch/gitfilediff/GitFileDiffCacheImpl.java
+++ b/java/com/google/gerrit/server/patch/gitfilediff/GitFileDiffCacheImpl.java
@@ -28,6 +28,7 @@
import com.google.common.collect.MultimapBuilder;
import com.google.common.collect.Multimaps;
import com.google.common.collect.Streams;
+import com.google.common.flogger.FluentLogger;
import com.google.gerrit.entities.Patch;
import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
@@ -66,6 +67,7 @@
import org.eclipse.jgit.diff.DiffFormatter;
import org.eclipse.jgit.diff.HistogramDiff;
import org.eclipse.jgit.diff.RawTextComparator;
+import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.lib.AbbreviatedObjectId;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
@@ -76,6 +78,7 @@
/** Implementation of the {@link GitFileDiffCache} */
@Singleton
public class GitFileDiffCacheImpl implements GitFileDiffCache {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private static final String GIT_DIFF = "git_file_diff";
public static Module module() {
@@ -340,8 +343,7 @@
throws IOException {
if (!key.useTimeout()) {
try (CloseablePool<DiffFormatter>.Handle formatter = diffPool.get()) {
- FileHeader fileHeader = formatter.get().toFileHeader(diffEntry);
- return GitFileDiff.create(diffEntry, fileHeader);
+ return GitFileDiff.create(diffEntry, getFileHeader(formatter, diffEntry));
}
}
// This submits the DiffFormatter to a different thread. The CloseablePool and our usage of it
@@ -353,7 +355,7 @@
diffExecutor.submit(
() -> {
try (CloseablePool<DiffFormatter>.Handle formatter = diffPool.get()) {
- return GitFileDiff.create(diffEntry, formatter.get().toFileHeader(diffEntry));
+ return GitFileDiff.create(diffEntry, getFileHeader(formatter, diffEntry));
}
});
try {
@@ -385,6 +387,46 @@
? diffEntry.getOldPath()
: diffEntry.getNewPath();
}
+
+ private FileHeader getFileHeader(
+ CloseablePool<DiffFormatter>.Handle formatter, DiffEntry diffEntry) throws IOException {
+ logger.atFine().log("getting file header for %s", formatDiffEntryForLogging(diffEntry));
+ try {
+ return formatter.get().toFileHeader(diffEntry);
+ } catch (MissingObjectException e) {
+ throw new IOException(
+ String.format("Failed to get file header for %s", formatDiffEntryForLogging(diffEntry)),
+ e);
+ }
+ }
+
+ private String formatDiffEntryForLogging(DiffEntry diffEntry) {
+ StringBuilder buf = new StringBuilder();
+ buf.append("DiffEntry[");
+ buf.append(diffEntry.getChangeType());
+ buf.append(" ");
+ switch (diffEntry.getChangeType()) {
+ case ADD:
+ buf.append(String.format("%s (%s)", diffEntry.getNewPath(), diffEntry.getNewId().name()));
+ break;
+ case COPY:
+ case RENAME:
+ buf.append(
+ String.format(
+ "%s (%s) -> %s (%s)",
+ diffEntry.getOldPath(),
+ diffEntry.getOldId().name(),
+ diffEntry.getNewPath(),
+ diffEntry.getNewId().name()));
+ break;
+ case DELETE:
+ case MODIFY:
+ buf.append(String.format("%s (%s)", diffEntry.getOldPath(), diffEntry.getOldId().name()));
+ break;
+ }
+ buf.append("]");
+ return buf.toString();
+ }
}
/**
diff --git a/java/com/google/gerrit/server/permissions/AbstractLabelPermission.java b/java/com/google/gerrit/server/permissions/AbstractLabelPermission.java
new file mode 100644
index 0000000..622f0cf
--- /dev/null
+++ b/java/com/google/gerrit/server/permissions/AbstractLabelPermission.java
@@ -0,0 +1,155 @@
+// Copyright (C) 2022 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.permissions;
+
+import static com.google.gerrit.server.permissions.AbstractLabelPermission.ForUser.ON_BEHALF_OF;
+import static java.util.Objects.requireNonNull;
+
+import com.google.gerrit.entities.LabelType;
+import com.google.gerrit.server.util.LabelVote;
+
+/** Abstract permission representing a label. */
+public abstract class AbstractLabelPermission implements ChangePermissionOrLabel {
+ public enum ForUser {
+ SELF,
+ ON_BEHALF_OF
+ }
+
+ protected final ForUser forUser;
+ protected final String name;
+
+ /**
+ * Construct a reference to an abstract label permission.
+ *
+ * @param forUser {@code SELF} (default) or {@code ON_BEHALF_OF} for labelAs behavior.
+ * @param name name of the label, e.g. {@code "Code-Review"} or {@code "Verified"}.
+ */
+ public AbstractLabelPermission(ForUser forUser, String name) {
+ this.forUser = requireNonNull(forUser, "ForUser");
+ this.name = LabelType.checkName(name);
+ }
+
+ /** Returns {@code SELF} or {@code ON_BEHALF_OF} (or labelAs). */
+ public ForUser forUser() {
+ return forUser;
+ }
+
+ /** Returns name of the label, e.g. {@code "Code-Review"}. */
+ public String label() {
+ return name;
+ }
+
+ protected abstract String permissionPrefix();
+
+ protected String permissionName() {
+ if (forUser == ON_BEHALF_OF) {
+ return permissionPrefix() + "As";
+ }
+ return permissionPrefix();
+ }
+
+ @Override
+ public final String describeForException() {
+ if (forUser == ON_BEHALF_OF) {
+ return permissionPrefix() + " on behalf of " + name;
+ }
+ return permissionPrefix() + " " + name;
+ }
+
+ @Override
+ public final int hashCode() {
+ return (permissionPrefix() + name).hashCode();
+ }
+
+ @Override
+ @SuppressWarnings("EqualsGetClass")
+ public final boolean equals(Object other) {
+ if (this.getClass().isAssignableFrom(other.getClass())) {
+ AbstractLabelPermission b = (AbstractLabelPermission) other;
+ return forUser == b.forUser && name.equals(b.name);
+ }
+ return false;
+ }
+
+ @Override
+ public final String toString() {
+ return permissionName() + "[" + name + ']';
+ }
+
+ /** A {@link AbstractLabelPermission} at a specific value. */
+ public abstract static class WithValue implements ChangePermissionOrLabel {
+ private final ForUser forUser;
+ private final LabelVote label;
+
+ /**
+ * Construct a reference to an abstract label permission at a specific value.
+ *
+ * @param forUser {@code SELF} (default) or {@code ON_BEHALF_OF} for labelAs behavior.
+ * @param label label name and vote.
+ */
+ public WithValue(ForUser forUser, LabelVote label) {
+ this.forUser = requireNonNull(forUser, "ForUser");
+ this.label = requireNonNull(label, "LabelVote");
+ }
+
+ /** Returns {@code SELF} or {@code ON_BEHALF_OF} (or labelAs). */
+ public ForUser forUser() {
+ return forUser;
+ }
+
+ /** Returns name of the label, e.g. {@code "Code-Review"}. */
+ public String label() {
+ return label.label();
+ }
+
+ /** Returns specific value of the label, e.g. 1 or 2. */
+ public short value() {
+ return label.value();
+ }
+
+ public abstract String permissionName();
+
+ @Override
+ public final String describeForException() {
+ if (forUser == ON_BEHALF_OF) {
+ return permissionName() + " on behalf of " + label.formatWithEquals();
+ }
+ return permissionName() + " " + label.formatWithEquals();
+ }
+
+ @Override
+ public final int hashCode() {
+ return (permissionName() + label).hashCode();
+ }
+
+ @Override
+ @SuppressWarnings("EqualsGetClass")
+ public final boolean equals(Object other) {
+ if (this.getClass().isAssignableFrom(other.getClass())) {
+ AbstractLabelPermission.WithValue b = (AbstractLabelPermission.WithValue) other;
+ return forUser == b.forUser && label.equals(b.label);
+ }
+ return false;
+ }
+
+ @Override
+ public final String toString() {
+ if (forUser == ON_BEHALF_OF) {
+ return permissionName() + "As[" + label.format() + ']';
+ }
+ return permissionName() + "[" + label.format() + ']';
+ }
+ }
+}
diff --git a/java/com/google/gerrit/server/permissions/ChangeControl.java b/java/com/google/gerrit/server/permissions/ChangeControl.java
index 8d432c8..e36ce7b 100644
--- a/java/com/google/gerrit/server/permissions/ChangeControl.java
+++ b/java/com/google/gerrit/server/permissions/ChangeControl.java
@@ -14,8 +14,8 @@
package com.google.gerrit.server.permissions;
+import static com.google.gerrit.server.permissions.AbstractLabelPermission.ForUser.ON_BEHALF_OF;
import static com.google.gerrit.server.permissions.DefaultPermissionMappings.labelPermissionName;
-import static com.google.gerrit.server.permissions.LabelPermission.ForUser.ON_BEHALF_OF;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
@@ -216,7 +216,10 @@
public void check(ChangePermissionOrLabel perm)
throws AuthException, PermissionBackendException {
if (!can(perm)) {
- throw new AuthException(perm.describeForException() + " not permitted");
+ throw new AuthException(
+ perm.describeForException()
+ + " not permitted"
+ + perm.hintForException().map(hint -> " (" + hint + ")").orElse(""));
}
}
@@ -240,10 +243,10 @@
private boolean can(ChangePermissionOrLabel perm) throws PermissionBackendException {
if (perm instanceof ChangePermission) {
return can((ChangePermission) perm);
- } else if (perm instanceof LabelPermission) {
- return can((LabelPermission) perm);
- } else if (perm instanceof LabelPermission.WithValue) {
- return can((LabelPermission.WithValue) perm);
+ } else if (perm instanceof AbstractLabelPermission) {
+ return can((AbstractLabelPermission) perm);
+ } else if (perm instanceof AbstractLabelPermission.WithValue) {
+ return can((AbstractLabelPermission.WithValue) perm);
}
throw new PermissionBackendException(perm + " unsupported");
}
@@ -288,11 +291,11 @@
throw new PermissionBackendException(perm + " unsupported");
}
- private boolean can(LabelPermission perm) {
+ private boolean can(AbstractLabelPermission perm) {
return !label(labelPermissionName(perm)).isEmpty();
}
- private boolean can(LabelPermission.WithValue perm) {
+ private boolean can(AbstractLabelPermission.WithValue perm) {
PermissionRange r = label(labelPermissionName(perm));
if (perm.forUser() == ON_BEHALF_OF && r.isEmpty()) {
return false;
diff --git a/java/com/google/gerrit/server/permissions/ChangePermission.java b/java/com/google/gerrit/server/permissions/ChangePermission.java
index 63b0378..6ceed3e 100644
--- a/java/com/google/gerrit/server/permissions/ChangePermission.java
+++ b/java/com/google/gerrit/server/permissions/ChangePermission.java
@@ -16,7 +16,9 @@
import static java.util.Objects.requireNonNull;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.api.access.GerritPermission;
+import java.util.Optional;
public enum ChangePermission implements ChangePermissionOrLabel {
READ,
@@ -53,24 +55,40 @@
* <p>Before checking this permission, the caller should first verify the current patch set of the
* change is not locked by calling {@code PatchSetUtil.isPatchSetLocked}.
*/
- REBASE,
+ REBASE(
+ /* description= */ null,
+ /* hint= */ "change owners and users with the 'Submit' or 'Rebase' permission can rebase"
+ + " if they have the 'Push' permission"),
REVERT,
SUBMIT,
SUBMIT_AS("submit on behalf of other users"),
TOGGLE_WORK_IN_PROGRESS_STATE;
private final String description;
+ private final String hint;
ChangePermission() {
this.description = null;
+ this.hint = null;
}
ChangePermission(String description) {
this.description = requireNonNull(description);
+ this.hint = null;
+ }
+
+ ChangePermission(@Nullable String description, String hint) {
+ this.description = description;
+ this.hint = requireNonNull(hint);
}
@Override
public String describeForException() {
return description != null ? description : GerritPermission.describeEnumValue(this);
}
+
+ @Override
+ public Optional<String> hintForException() {
+ return Optional.ofNullable(hint);
+ }
}
diff --git a/java/com/google/gerrit/server/permissions/ChangePermissionOrLabel.java b/java/com/google/gerrit/server/permissions/ChangePermissionOrLabel.java
index 2824efd..9254158 100644
--- a/java/com/google/gerrit/server/permissions/ChangePermissionOrLabel.java
+++ b/java/com/google/gerrit/server/permissions/ChangePermissionOrLabel.java
@@ -15,6 +15,17 @@
package com.google.gerrit.server.permissions;
import com.google.gerrit.extensions.api.access.GerritPermission;
+import java.util.Optional;
-/** A {@link ChangePermission} or a {@link LabelPermission}. */
-public interface ChangePermissionOrLabel extends GerritPermission {}
+/** A {@link ChangePermission} or a {@link AbstractLabelPermission}. */
+public interface ChangePermissionOrLabel extends GerritPermission {
+ /**
+ * A hint that explains under which conditions this permission is permitted.
+ *
+ * <p>This is useful for permissions that are not directly assigned but are indirectly permitted
+ * by the user having other permissions or being the change owner.
+ */
+ default Optional<String> hintForException() {
+ return Optional.empty();
+ }
+}
diff --git a/java/com/google/gerrit/server/permissions/DefaultPermissionMappings.java b/java/com/google/gerrit/server/permissions/DefaultPermissionMappings.java
index 9d69d9b..89f0493 100644
--- a/java/com/google/gerrit/server/permissions/DefaultPermissionMappings.java
+++ b/java/com/google/gerrit/server/permissions/DefaultPermissionMappings.java
@@ -24,7 +24,7 @@
import com.google.gerrit.extensions.api.access.GlobalOrPluginPermission;
import com.google.gerrit.extensions.api.access.PluginPermission;
import com.google.gerrit.extensions.api.access.PluginProjectPermission;
-import com.google.gerrit.server.permissions.LabelPermission.ForUser;
+import com.google.gerrit.server.permissions.AbstractLabelPermission.ForUser;
import java.util.EnumSet;
import java.util.Optional;
import java.util.Set;
@@ -160,19 +160,29 @@
return Optional.ofNullable(CHANGE_PERMISSIONS.inverse().get(permissionName));
}
- public static String labelPermissionName(LabelPermission labelPermission) {
- if (labelPermission.forUser() == ForUser.ON_BEHALF_OF) {
- return Permission.forLabelAs(labelPermission.label());
+ public static String labelPermissionName(AbstractLabelPermission labelPermission) {
+ if (labelPermission instanceof LabelPermission) {
+ if (labelPermission.forUser() == ForUser.ON_BEHALF_OF) {
+ return Permission.forLabelAs(labelPermission.label());
+ }
+ return Permission.forLabel(labelPermission.label());
+ } else if (labelPermission instanceof LabelRemovalPermission) {
+ return Permission.forRemoveLabel(labelPermission.label());
}
- return Permission.forLabel(labelPermission.label());
+ throw new IllegalStateException("invalid AbstractLabelPermission subtype");
}
// TODO(dborowitz): Can these share a common superinterface?
- public static String labelPermissionName(LabelPermission.WithValue labelPermission) {
- if (labelPermission.forUser() == ForUser.ON_BEHALF_OF) {
- return Permission.forLabelAs(labelPermission.label());
+ public static String labelPermissionName(AbstractLabelPermission.WithValue labelPermission) {
+ if (labelPermission instanceof LabelPermission.WithValue) {
+ if (labelPermission.forUser() == ForUser.ON_BEHALF_OF) {
+ return Permission.forLabelAs(labelPermission.label());
+ }
+ return Permission.forLabel(labelPermission.label());
+ } else if (labelPermission instanceof LabelRemovalPermission.WithValue) {
+ return Permission.forRemoveLabel(labelPermission.label());
}
- return Permission.forLabel(labelPermission.label());
+ throw new IllegalStateException("invalid AbstractLabelPermission.WithValue subtype");
}
private DefaultPermissionMappings() {}
diff --git a/java/com/google/gerrit/server/permissions/LabelPermission.java b/java/com/google/gerrit/server/permissions/LabelPermission.java
index c266caa..4652364 100644
--- a/java/com/google/gerrit/server/permissions/LabelPermission.java
+++ b/java/com/google/gerrit/server/permissions/LabelPermission.java
@@ -14,24 +14,14 @@
package com.google.gerrit.server.permissions;
-import static com.google.gerrit.server.permissions.LabelPermission.ForUser.ON_BEHALF_OF;
-import static com.google.gerrit.server.permissions.LabelPermission.ForUser.SELF;
-import static java.util.Objects.requireNonNull;
+import static com.google.gerrit.server.permissions.AbstractLabelPermission.ForUser.SELF;
import com.google.gerrit.entities.LabelType;
import com.google.gerrit.entities.LabelValue;
import com.google.gerrit.server.util.LabelVote;
/** Permission representing a label. */
-public class LabelPermission implements ChangePermissionOrLabel {
- public enum ForUser {
- SELF,
- ON_BEHALF_OF;
- }
-
- private final ForUser forUser;
- private final String name;
-
+public class LabelPermission extends AbstractLabelPermission {
/**
* Construct a reference to a label permission.
*
@@ -67,55 +57,16 @@
* @param name name of the label, e.g. {@code "Code-Review"} or {@code "Verified"}.
*/
public LabelPermission(ForUser forUser, String name) {
- this.forUser = requireNonNull(forUser, "ForUser");
- this.name = LabelType.checkName(name);
- }
-
- /** Returns {@code SELF} or {@code ON_BEHALF_OF} (or labelAs). */
- public ForUser forUser() {
- return forUser;
- }
-
- /** Returns name of the label, e.g. {@code "Code-Review"}. */
- public String label() {
- return name;
+ super(forUser, name);
}
@Override
- public String describeForException() {
- if (forUser == ON_BEHALF_OF) {
- return "label on behalf of " + name;
- }
- return "label " + name;
- }
-
- @Override
- public int hashCode() {
- return name.hashCode();
- }
-
- @Override
- public boolean equals(Object other) {
- if (other instanceof LabelPermission) {
- LabelPermission b = (LabelPermission) other;
- return forUser == b.forUser && name.equals(b.name);
- }
- return false;
- }
-
- @Override
- public String toString() {
- if (forUser == ON_BEHALF_OF) {
- return "LabelAs[" + name + ']';
- }
- return "Label[" + name + ']';
+ public String permissionPrefix() {
+ return "label";
}
/** A {@link LabelPermission} at a specific value. */
- public static class WithValue implements ChangePermissionOrLabel {
- private final ForUser forUser;
- private final LabelVote label;
-
+ public static class WithValue extends AbstractLabelPermission.WithValue {
/**
* Construct a reference to a label at a specific value.
*
@@ -195,53 +146,12 @@
* @param label label name and vote.
*/
public WithValue(ForUser forUser, LabelVote label) {
- this.forUser = requireNonNull(forUser, "ForUser");
- this.label = requireNonNull(label, "LabelVote");
- }
-
- /** Returns {@code SELF} or {@code ON_BEHALF_OF} (or labelAs). */
- public ForUser forUser() {
- return forUser;
- }
-
- /** Returns name of the label, e.g. {@code "Code-Review"}. */
- public String label() {
- return label.label();
- }
-
- /** Returns specific value of the label, e.g. 1 or 2. */
- public short value() {
- return label.value();
+ super(forUser, label);
}
@Override
- public String describeForException() {
- if (forUser == ON_BEHALF_OF) {
- return "label on behalf of " + label.formatWithEquals();
- }
- return "label " + label.formatWithEquals();
- }
-
- @Override
- public int hashCode() {
- return label.hashCode();
- }
-
- @Override
- public boolean equals(Object other) {
- if (other instanceof WithValue) {
- WithValue b = (WithValue) other;
- return forUser == b.forUser && label.equals(b.label);
- }
- return false;
- }
-
- @Override
- public String toString() {
- if (forUser == ON_BEHALF_OF) {
- return "LabelAs[" + label.format() + ']';
- }
- return "Label[" + label.format() + ']';
+ public String permissionName() {
+ return "label";
}
}
}
diff --git a/java/com/google/gerrit/server/permissions/LabelRemovalPermission.java b/java/com/google/gerrit/server/permissions/LabelRemovalPermission.java
new file mode 100644
index 0000000..2553601
--- /dev/null
+++ b/java/com/google/gerrit/server/permissions/LabelRemovalPermission.java
@@ -0,0 +1,94 @@
+// Copyright (C) 2022 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.permissions;
+
+import static com.google.gerrit.server.permissions.AbstractLabelPermission.ForUser.SELF;
+
+import com.google.gerrit.entities.LabelType;
+import com.google.gerrit.entities.LabelValue;
+import com.google.gerrit.server.util.LabelVote;
+
+/** Permission representing a label removal. */
+public class LabelRemovalPermission extends AbstractLabelPermission {
+ /**
+ * Construct a reference to a label removal permission.
+ *
+ * @param type type description of the label.
+ */
+ public LabelRemovalPermission(LabelType type) {
+ this(type.getName());
+ }
+
+ /**
+ * Construct a reference to a label removal permission.
+ *
+ * @param name name of the label, e.g. {@code "Code-Review"} or {@code "Verified"}.
+ */
+ public LabelRemovalPermission(String name) {
+ super(SELF, name);
+ }
+
+ @Override
+ public String permissionPrefix() {
+ return "removeLabel";
+ }
+
+ /** A {@link LabelRemovalPermission} at a specific value. */
+ public static class WithValue extends AbstractLabelPermission.WithValue {
+ /**
+ * Construct a reference to a label removal at a specific value.
+ *
+ * @param type description of the label.
+ * @param value numeric score assigned to the label.
+ */
+ public WithValue(LabelType type, LabelValue value) {
+ this(type.getName(), value.getValue());
+ }
+
+ /**
+ * Construct a reference to a label removal at a specific value.
+ *
+ * @param type description of the label.
+ * @param value numeric score assigned to the label.
+ */
+ public WithValue(LabelType type, short value) {
+ this(type.getName(), value);
+ }
+
+ /**
+ * Construct a reference to a label removal at a specific value.
+ *
+ * @param name name of the label, e.g. {@code "Code-Review"} or {@code "Verified"}.
+ * @param value numeric score assigned to the label.
+ */
+ public WithValue(String name, short value) {
+ this(LabelVote.create(name, value));
+ }
+
+ /**
+ * Construct a reference to a label removal at a specific value.
+ *
+ * @param label label name and vote.
+ */
+ public WithValue(LabelVote label) {
+ super(SELF, label);
+ }
+
+ @Override
+ public String permissionName() {
+ return "removeLabel";
+ }
+ }
+}
diff --git a/java/com/google/gerrit/server/permissions/PermissionBackend.java b/java/com/google/gerrit/server/permissions/PermissionBackend.java
index fea2827..eb5e053 100644
--- a/java/com/google/gerrit/server/permissions/PermissionBackend.java
+++ b/java/com/google/gerrit/server/permissions/PermissionBackend.java
@@ -474,6 +474,18 @@
}
/**
+ * Test which values of a label the user may be able to remove.
+ *
+ * @param label definition of the label to test values of.
+ * @return set containing values the user may be able to use; may be empty if none.
+ * @throws PermissionBackendException if failure consulting backend configuration.
+ */
+ public Set<LabelRemovalPermission.WithValue> testRemoval(LabelType label)
+ throws PermissionBackendException {
+ return test(removalValuesOf(requireNonNull(label, "LabelType")));
+ }
+
+ /**
* Test which values of a group of labels the user may be able to set.
*
* @param types definition of the labels to test values of.
@@ -486,10 +498,29 @@
return test(types.stream().flatMap(t -> valuesOf(t).stream()).collect(toSet()));
}
+ /**
+ * Test which values of a group of labels the user may be able to remove.
+ *
+ * @param types definition of the labels to test values of.
+ * @return set containing values the user may be able to use; may be empty if none.
+ * @throws PermissionBackendException if failure consulting backend configuration.
+ */
+ public Set<LabelRemovalPermission.WithValue> testLabelRemovals(Collection<LabelType> types)
+ throws PermissionBackendException {
+ requireNonNull(types, "LabelType");
+ return test(types.stream().flatMap(t -> removalValuesOf(t).stream()).collect(toSet()));
+ }
+
private static Set<LabelPermission.WithValue> valuesOf(LabelType label) {
return label.getValues().stream()
.map(v -> new LabelPermission.WithValue(label, v))
.collect(toSet());
}
+
+ private static Set<LabelRemovalPermission.WithValue> removalValuesOf(LabelType label) {
+ return label.getValues().stream()
+ .map(v -> new LabelRemovalPermission.WithValue(label, v))
+ .collect(toSet());
+ }
}
}
diff --git a/java/com/google/gerrit/server/project/DeleteVoteControl.java b/java/com/google/gerrit/server/project/DeleteVoteControl.java
new file mode 100644
index 0000000..3f3f88a
--- /dev/null
+++ b/java/com/google/gerrit/server/project/DeleteVoteControl.java
@@ -0,0 +1,81 @@
+// Copyright (C) 2022 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 com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.LabelType;
+import com.google.gerrit.entities.PatchSetApproval;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.LabelRemovalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.permissions.RefPermission;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.inject.Inject;
+import java.util.Set;
+
+public class DeleteVoteControl {
+ private final PermissionBackend permissionBackend;
+ private final ChangeData.Factory changeDataFactory;
+
+ @Inject
+ public DeleteVoteControl(
+ PermissionBackend permissionBackend, ChangeData.Factory changeDataFactory) {
+ this.permissionBackend = permissionBackend;
+ this.changeDataFactory = changeDataFactory;
+ }
+
+ public boolean testDeleteVotePermissions(
+ CurrentUser user, ChangeNotes notes, PatchSetApproval approval, LabelType labelType)
+ throws PermissionBackendException {
+ return testDeleteVotePermissions(user, changeDataFactory.create(notes), approval, labelType);
+ }
+
+ public boolean testDeleteVotePermissions(
+ CurrentUser user, ChangeData cd, PatchSetApproval approval, LabelType labelType)
+ throws PermissionBackendException {
+ if (canRemoveReviewerWithoutRemoveLabelPermission(
+ cd.change(), user, approval.accountId(), approval.value())) {
+ return true;
+ }
+ // Test if the user is allowed to remove vote of the given label type and value.
+ Set<LabelRemovalPermission.WithValue> allowed =
+ permissionBackend.user(user).change(cd).testRemoval(labelType);
+ return allowed.contains(new LabelRemovalPermission.WithValue(labelType, approval.value()));
+ }
+
+ private boolean canRemoveReviewerWithoutRemoveLabelPermission(
+ Change change, CurrentUser user, Account.Id reviewer, int value)
+ throws PermissionBackendException {
+ if (user.isIdentifiedUser()) {
+ Account.Id aId = user.getAccountId();
+ if (aId.equals(reviewer)) {
+ return true; // A user can always remove their own votes.
+ } else if (aId.equals(change.getOwner()) && 0 <= value) {
+ return true; // The change owner may remove any zero or positive score.
+ }
+ }
+
+ // Users with the remove reviewer permission, the branch owner, project
+ // owner and site admin can remove anyone
+ PermissionBackend.WithUser withUser = permissionBackend.user(user);
+ PermissionBackend.ForProject forProject = withUser.project(change.getProject());
+ return forProject.ref(change.getDest().branch()).test(RefPermission.WRITE_CONFIG)
+ || withUser.test(GlobalPermission.ADMINISTRATE_SERVER);
+ }
+}
diff --git a/java/com/google/gerrit/server/project/PeriodicProjectListCacheWarmer.java b/java/com/google/gerrit/server/project/PeriodicProjectListCacheWarmer.java
index 85a3ab9..df2e1cf 100644
--- a/java/com/google/gerrit/server/project/PeriodicProjectListCacheWarmer.java
+++ b/java/com/google/gerrit/server/project/PeriodicProjectListCacheWarmer.java
@@ -92,7 +92,6 @@
}
@Override
- @SuppressWarnings("CheckReturnValue")
public void run() {
logger.atFine().log("Loading project_list cache");
cache.refreshProjectList();
diff --git a/java/com/google/gerrit/server/project/ProjectCacheImpl.java b/java/com/google/gerrit/server/project/ProjectCacheImpl.java
index 7fdd113..6498d1b 100644
--- a/java/com/google/gerrit/server/project/ProjectCacheImpl.java
+++ b/java/com/google/gerrit/server/project/ProjectCacheImpl.java
@@ -300,17 +300,21 @@
@Override
public Set<AccountGroup.UUID> guessRelevantGroupUUIDs() {
try (Timer0.Context ignored = guessRelevantGroupsLatency.start()) {
- return Streams.concat(
- Arrays.stream(config.getStringList("groups", /* subsection= */ null, "relevantGroup"))
- .map(AccountGroup::uuid),
- all().stream()
- .map(n -> inMemoryProjectCache.getIfPresent(n))
- .filter(Objects::nonNull)
- .flatMap(p -> p.getAllGroupUUIDs().stream())
- // getAllGroupUUIDs shouldn't really return null UUIDs, but harden
- // against them just in case there is a bug or corner case.
- .filter(id -> id != null && id.get() != null))
- .collect(toSet());
+ Set<AccountGroup.UUID> relevantGroupUuids =
+ Streams.concat(
+ Arrays.stream(
+ config.getStringList("groups", /* subsection= */ null, "relevantGroup"))
+ .map(AccountGroup::uuid),
+ all().stream()
+ .map(n -> inMemoryProjectCache.getIfPresent(n))
+ .filter(Objects::nonNull)
+ .flatMap(p -> p.getAllGroupUUIDs().stream())
+ // getAllGroupUUIDs shouldn't really return null UUIDs, but harden
+ // against them just in case there is a bug or corner case.
+ .filter(id -> id != null && id.get() != null))
+ .collect(toSet());
+ logger.atFine().log("relevant group UUIDs: %s", relevantGroupUuids);
+ return relevantGroupUuids;
}
}
diff --git a/java/com/google/gerrit/server/project/RemoveReviewerControl.java b/java/com/google/gerrit/server/project/RemoveReviewerControl.java
index 1bc309c..3fda87a 100644
--- a/java/com/google/gerrit/server/project/RemoveReviewerControl.java
+++ b/java/com/google/gerrit/server/project/RemoveReviewerControl.java
@@ -32,10 +32,12 @@
@Singleton
public class RemoveReviewerControl {
private final PermissionBackend permissionBackend;
+ private final ChangeData.Factory changeDataFactory;
@Inject
- RemoveReviewerControl(PermissionBackend permissionBackend) {
+ RemoveReviewerControl(PermissionBackend permissionBackend, ChangeData.Factory changeDataFactory) {
this.permissionBackend = permissionBackend;
+ this.changeDataFactory = changeDataFactory;
}
/**
@@ -64,6 +66,20 @@
/** Returns true if the user is allowed to remove this reviewer. */
public boolean testRemoveReviewer(
+ ChangeNotes notes, CurrentUser currentUser, PatchSetApproval approval)
+ throws PermissionBackendException {
+ return testRemoveReviewer(notes, currentUser, approval.accountId(), approval.value());
+ }
+
+ /** Returns true if the user is allowed to remove this reviewer. */
+ public boolean testRemoveReviewer(
+ ChangeNotes notes, CurrentUser currentUser, Account.Id reviewer, int value)
+ throws PermissionBackendException {
+ return testRemoveReviewer(changeDataFactory.create(notes), currentUser, reviewer, value);
+ }
+
+ /** Returns true if the user is allowed to remove this reviewer. */
+ public boolean testRemoveReviewer(
ChangeData cd, CurrentUser currentUser, Account.Id reviewer, int value)
throws PermissionBackendException {
if (canRemoveReviewerWithoutPermissionCheck(
diff --git a/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java b/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
index e31411c..d5c4a97 100644
--- a/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
@@ -159,7 +159,7 @@
return AccountPredicates.preferredEmail(email);
}
- throw new QueryParseException("'email' operator is not supported by account index version");
+ throw new QueryParseException("'email' operator is not supported on this gerrit host");
}
@Operator
diff --git a/java/com/google/gerrit/server/query/change/ChangeData.java b/java/com/google/gerrit/server/query/change/ChangeData.java
index ec1fcad..fc4df49 100644
--- a/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -334,7 +334,7 @@
* Map from {@link com.google.gerrit.entities.Account.Id} to the tip of the edit ref for this
* change and a given user.
*/
- private Table<Account.Id, PatchSet.Id, ObjectId> editsByUser;
+ private Table<Account.Id, PatchSet.Id, Ref> editRefsByUser;
private Set<Account.Id> reviewedBy;
/**
@@ -966,7 +966,7 @@
* submit requirements are evaluated online.
*
* <p>For changes loaded from the index, the value will be set from index field {@link
- * com.google.gerrit.server.index.change.ChangeField#STORED_SUBMIT_REQUIREMENTS}.
+ * com.google.gerrit.server.index.change.ChangeField#STORED_SUBMIT_REQUIREMENTS_FIELD}.
*/
public Map<SubmitRequirement, SubmitRequirementResult> submitRequirements() {
if (submitRequirements == null) {
@@ -1097,8 +1097,8 @@
return editRefs().rowKeySet();
}
- public Table<Account.Id, PatchSet.Id, ObjectId> editRefs() {
- if (editsByUser == null) {
+ public Table<Account.Id, PatchSet.Id, Ref> editRefs() {
+ if (editRefsByUser == null) {
if (!lazyload()) {
return HashBasedTable.create();
}
@@ -1106,7 +1106,7 @@
if (c == null) {
return HashBasedTable.create();
}
- editsByUser = HashBasedTable.create();
+ editRefsByUser = HashBasedTable.create();
Change.Id id = requireNonNull(change.getId());
try (Repository repo = repoManager.openRepository(project())) {
for (Ref ref : repo.getRefDatabase().getRefsByPrefix(RefNames.REFS_USERS)) {
@@ -1117,7 +1117,7 @@
if (id.equals(ps.changeId())) {
Account.Id accountId = Account.Id.fromRef(ref.getName());
if (accountId != null) {
- editsByUser.put(accountId, ps, ref.getObjectId());
+ editRefsByUser.put(accountId, ps, ref);
}
}
}
@@ -1125,7 +1125,7 @@
throw new StorageException(e);
}
}
- return editsByUser;
+ return editRefsByUser;
}
public Set<Account.Id> draftsByUser() {
@@ -1273,13 +1273,13 @@
}
ImmutableSetMultimap.Builder<NameKey, RefState> result = ImmutableSetMultimap.builder();
- for (Table.Cell<Account.Id, PatchSet.Id, ObjectId> edit : editRefs().cellSet()) {
+ for (Table.Cell<Account.Id, PatchSet.Id, Ref> edit : editRefs().cellSet()) {
result.put(
project,
RefState.create(
RefNames.refsEdit(
edit.getRowKey(), edit.getColumnKey().changeId(), edit.getColumnKey()),
- edit.getValue()));
+ edit.getValue().getObjectId()));
}
// TODO: instantiating the notes is too much. We don't want to parse NoteDb, we just want the
@@ -1309,21 +1309,6 @@
.forEach(r -> draftsByUser.put(Account.Id.fromRef(r.ref()), r.id()));
}
}
- if (editsByUser == null) {
- // Recover edit refs as well. Edits are represented as refs in the repository.
- // ChangeData exposes #editsByUser which just provides a Set of Account.Ids of users who
- // have edits on this change. Recovering this list from RefStates makes it available even
- // on ChangeData instances retrieved from the index.
- editsByUser = HashBasedTable.create();
- if (refStates.containsKey(project())) {
- refStates.get(project()).stream()
- .filter(r -> RefNames.isRefsEdit(r.ref()))
- .forEach(
- r ->
- editsByUser.put(
- Account.Id.fromRef(r.ref()), PatchSet.Id.fromEditRef(r.ref()), r.id()));
- }
- }
}
public ImmutableList<byte[]> getRefStatePatterns() {
diff --git a/java/com/google/gerrit/server/query/change/ChangePredicates.java b/java/com/google/gerrit/server/query/change/ChangePredicates.java
index 84ceb3d..c344edd 100644
--- a/java/com/google/gerrit/server/query/change/ChangePredicates.java
+++ b/java/com/google/gerrit/server/query/change/ChangePredicates.java
@@ -336,6 +336,10 @@
return new ChangeIndexPredicate(ChangeField.SUBJECT_SPEC, subject);
}
+ public static Predicate<ChangeData> prefixSubject(String subject) {
+ return new ChangeIndexPredicate(ChangeField.PREFIX_SUBJECT_SPEC, subject);
+ }
+
/**
* Returns a predicate that matches changes where the provided {@code comment} appears in any
* comment on any patch set of the change. Uses full-text search semantics.
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index 83c348c..ca18ab2 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -187,6 +187,7 @@
public static final String FIELD_MERGED_ON = "mergedon";
public static final String FIELD_MESSAGE = "message";
public static final String FIELD_SUBJECT = "subject";
+ public static final String FIELD_PREFIX_SUBJECT = "prefixsubject";
public static final String FIELD_MESSAGE_EXACT = "messageexact";
public static final String FIELD_OWNER = "owner";
public static final String FIELD_OWNERIN = "ownerin";
@@ -410,7 +411,7 @@
this.submitRules = submitRules;
}
- Arguments asUser(CurrentUser otherUser) {
+ public Arguments asUser(CurrentUser otherUser) {
return new Arguments(
queryProvider,
rewriter,
@@ -487,13 +488,14 @@
private final Arguments args;
protected Map<String, String> hasOperandAliases = Collections.emptyMap();
private Map<Account.Id, DestinationList> destinationListByAccount = new HashMap<>();
+ private boolean forceAccountVisibilityCheck = false;
private static final Splitter RULE_SPLITTER = Splitter.on("=");
private static final Splitter PLUGIN_SPLITTER = Splitter.on("_");
private static final Splitter LABEL_SPLITTER = Splitter.on(",");
@Inject
- ChangeQueryBuilder(Arguments args) {
+ protected ChangeQueryBuilder(Arguments args) {
this(mydef, args);
setupAliases();
}
@@ -513,6 +515,15 @@
return new ChangeQueryBuilder(builderDef, args.asUser(user));
}
+ public Arguments getArgs() {
+ return args;
+ }
+
+ /** Whether to force account visibility check when searching for changes by account(s). */
+ public void forceAccountVisibilityCheck() {
+ forceAccountVisibilityCheck = true;
+ }
+
@Operator
public Predicate<ChangeData> age(String value) {
return new AgePredicate(value);
@@ -540,14 +551,14 @@
@Operator
public Predicate<ChangeData> mergedBefore(String value) throws QueryParseException {
- checkFieldAvailable(ChangeField.MERGED_ON_SPEC, OPERATOR_MERGED_BEFORE);
+ checkOperatorAvailable(ChangeField.MERGED_ON_SPEC, OPERATOR_MERGED_BEFORE);
return new BeforePredicate(
ChangeField.MERGED_ON_SPEC, ChangeQueryBuilder.OPERATOR_MERGED_BEFORE, value);
}
@Operator
public Predicate<ChangeData> mergedAfter(String value) throws QueryParseException {
- checkFieldAvailable(ChangeField.MERGED_ON_SPEC, OPERATOR_MERGED_AFTER);
+ checkOperatorAvailable(ChangeField.MERGED_ON_SPEC, OPERATOR_MERGED_AFTER);
return new AfterPredicate(
ChangeField.MERGED_ON_SPEC, ChangeQueryBuilder.OPERATOR_MERGED_AFTER, value);
}
@@ -630,7 +641,7 @@
}
if ("attention".equalsIgnoreCase(value)) {
- checkFieldAvailable(ChangeField.ATTENTION_SET_USERS, "has:attention");
+ checkOperatorAvailable(ChangeField.ATTENTION_SET_USERS, "has:attention");
return new IsAttentionPredicate();
}
@@ -673,7 +684,7 @@
}
if ("uploader".equalsIgnoreCase(value)) {
- checkFieldAvailable(ChangeField.UPLOADER_SPEC, "is:uploader");
+ checkOperatorAvailable(ChangeField.UPLOADER_SPEC, "is:uploader");
return ChangePredicates.uploader(self());
}
@@ -689,13 +700,14 @@
if ("mergeable".equalsIgnoreCase(value)) {
if (!args.indexMergeable) {
- throw new QueryParseException("'is:mergeable' operator is not supported by server");
+ throw new QueryParseException(
+ "'is:mergeable' operator is not supported on this gerrit host");
}
return new BooleanPredicate(ChangeField.MERGEABLE_SPEC);
}
if ("merge".equalsIgnoreCase(value)) {
- checkFieldAvailable(ChangeField.MERGE_SPEC, "is:merge");
+ checkOperatorAvailable(ChangeField.MERGE_SPEC, "is:merge");
return new BooleanPredicate(ChangeField.MERGE_SPEC);
}
@@ -704,7 +716,7 @@
}
if ("attention".equalsIgnoreCase(value)) {
- checkFieldAvailable(ChangeField.ATTENTION_SET_USERS, "is:attention");
+ checkOperatorAvailable(ChangeField.ATTENTION_SET_USERS, "is:attention");
return new IsAttentionPredicate();
}
@@ -717,7 +729,7 @@
}
if ("pure-revert".equalsIgnoreCase(value)) {
- checkFieldAvailable(ChangeField.IS_PURE_REVERT_SPEC, "is:pure-revert");
+ checkOperatorAvailable(ChangeField.IS_PURE_REVERT_SPEC, "is:pure-revert");
return ChangePredicates.pureRevert("1");
}
@@ -733,12 +745,12 @@
Predicate.not(new SubmittablePredicate(SubmitRecord.Status.NOT_READY)),
Predicate.not(new SubmittablePredicate(SubmitRecord.Status.RULE_ERROR)));
}
- checkFieldAvailable(ChangeField.IS_SUBMITTABLE_SPEC, "is:submittable");
+ checkOperatorAvailable(ChangeField.IS_SUBMITTABLE_SPEC, "is:submittable");
return new IsSubmittablePredicate();
}
if ("started".equalsIgnoreCase(value)) {
- checkFieldAvailable(ChangeField.STARTED_SPEC, "is:started");
+ checkOperatorAvailable(ChangeField.STARTED_SPEC, "is:started");
return new BooleanPredicate(ChangeField.STARTED_SPEC);
}
@@ -747,7 +759,7 @@
}
if ("cherrypick".equalsIgnoreCase(value)) {
- checkFieldAvailable(ChangeField.CHERRY_PICK_SPEC, "is:cherrypick");
+ checkOperatorAvailable(ChangeField.CHERRY_PICK_SPEC, "is:cherrypick");
return new BooleanPredicate(ChangeField.CHERRY_PICK_SPEC);
}
@@ -770,7 +782,7 @@
@Operator
public Predicate<ChangeData> conflicts(String value) throws QueryParseException {
if (!args.conflictsPredicateEnabled) {
- throw new QueryParseException("'conflicts:' operator is not supported by server");
+ throw new QueryParseException("'conflicts:' operator is not supported on this gerrit host");
}
List<Change> changes = parseChange(value);
List<Predicate<ChangeData>> or = new ArrayList<>(changes.size());
@@ -882,7 +894,7 @@
return ChangePredicates.hashtag(hashtag);
}
- checkFieldAvailable(ChangeField.FUZZY_HASHTAG, "inhashtag");
+ checkOperatorAvailable(ChangeField.FUZZY_HASHTAG, "inhashtag");
return ChangePredicates.fuzzyHashtag(hashtag);
}
@@ -892,7 +904,7 @@
return ChangePredicates.hashtag(hashtag);
}
- checkFieldAvailable(ChangeField.PREFIX_HASHTAG, "prefixhashtag");
+ checkOperatorAvailable(ChangeField.PREFIX_HASHTAG, "prefixhashtag");
return ChangePredicates.prefixHashtag(hashtag);
}
@@ -918,7 +930,7 @@
return ChangePredicates.exactTopic(name);
}
- checkFieldAvailable(ChangeField.PREFIX_TOPIC, "prefixtopic");
+ checkOperatorAvailable(ChangeField.PREFIX_TOPIC, "prefixtopic");
return ChangePredicates.prefixTopic(name);
}
@@ -984,7 +996,7 @@
@Operator
public Predicate<ChangeData> hasfooter(String footerName) throws QueryParseException {
- checkFieldAvailable(ChangeField.FOOTER_NAME, "hasfooter");
+ checkOperatorAvailable(ChangeField.FOOTER_NAME, "hasfooter");
return ChangePredicates.hasFooter(footerName);
}
@@ -1135,7 +1147,9 @@
@Operator
public Predicate<ChangeData> message(String text) throws QueryParseException {
if (text.startsWith("^")) {
- checkFieldAvailable(ChangeField.COMMIT_MESSAGE_EXACT, "messageexact");
+ checkFieldAvailable(
+ ChangeField.COMMIT_MESSAGE_EXACT,
+ "'message' operator with regular expression is not supported on this gerrit host");
return new RegexMessagePredicate(text);
}
return ChangePredicates.message(text);
@@ -1143,10 +1157,17 @@
@Operator
public Predicate<ChangeData> subject(String value) throws QueryParseException {
- checkFieldAvailable(ChangeField.SUBJECT_SPEC, ChangeQueryBuilder.FIELD_SUBJECT);
+ checkOperatorAvailable(ChangeField.SUBJECT_SPEC, ChangeQueryBuilder.FIELD_SUBJECT);
return ChangePredicates.subject(value);
}
+ @Operator
+ public Predicate<ChangeData> prefixsubject(String value) throws QueryParseException {
+ checkOperatorAvailable(
+ ChangeField.PREFIX_SUBJECT_SPEC, ChangeQueryBuilder.FIELD_PREFIX_SUBJECT);
+ return ChangePredicates.prefixSubject(value);
+ }
+
private Predicate<ChangeData> starredBySelf() throws QueryParseException {
return ChangePredicates.starBy(
args.starredChangesUtil, self(), StarredChangesUtil.DEFAULT_LABEL);
@@ -1231,7 +1252,7 @@
@Operator
public Predicate<ChangeData> uploader(String who)
throws QueryParseException, IOException, ConfigInvalidException {
- checkFieldAvailable(ChangeField.UPLOADER_SPEC, "uploader");
+ checkOperatorAvailable(ChangeField.UPLOADER_SPEC, "uploader");
return uploader(parseAccount(who, (AccountState s) -> true));
}
@@ -1246,7 +1267,7 @@
@Operator
public Predicate<ChangeData> attention(String who)
throws QueryParseException, IOException, ConfigInvalidException {
- checkFieldAvailable(ChangeField.ATTENTION_SET_USERS, "attention");
+ checkOperatorAvailable(ChangeField.ATTENTION_SET_USERS, "attention");
return attention(parseAccount(who, (AccountState s) -> true));
}
@@ -1291,7 +1312,7 @@
@Operator
public Predicate<ChangeData> uploaderin(String group) throws QueryParseException, IOException {
- checkFieldAvailable(ChangeField.UPLOADER_SPEC, "uploaderin");
+ checkOperatorAvailable(ChangeField.UPLOADER_SPEC, "uploaderin");
GroupReference g = GroupBackends.findBestSuggestion(args.groupBackend, group);
if (g == null) {
@@ -1557,8 +1578,8 @@
@Operator
public Predicate<ChangeData> cherryPickOf(String value) throws QueryParseException {
- checkFieldAvailable(ChangeField.CHERRY_PICK_OF_CHANGE, "cherryPickOf");
- checkFieldAvailable(ChangeField.CHERRY_PICK_OF_PATCHSET, "cherryPickOf");
+ checkOperatorAvailable(ChangeField.CHERRY_PICK_OF_CHANGE, "cherryPickOf");
+ checkOperatorAvailable(ChangeField.CHERRY_PICK_OF_PATCHSET, "cherryPickOf");
if (Ints.tryParse(value) != null) {
return ChangePredicates.cherryPickOf(Change.id(Ints.tryParse(value)));
}
@@ -1630,11 +1651,16 @@
return Predicate.or(predicates);
}
- protected void checkFieldAvailable(SchemaField<ChangeData, ?> field, String operator)
+ private void checkOperatorAvailable(SchemaField<ChangeData, ?> field, String operator)
+ throws QueryParseException {
+ checkFieldAvailable(
+ field, String.format("'%s' operator is not supported on this gerrit host", operator));
+ }
+
+ protected void checkFieldAvailable(SchemaField<ChangeData, ?> field, String errorMessage)
throws QueryParseException {
if (!args.index.getSchema().hasField(field)) {
- throw new QueryParseException(
- String.format("'%s' operator is not supported by change index version", operator));
+ throw new QueryParseException(errorMessage);
}
}
@@ -1685,7 +1711,9 @@
private Set<Account.Id> parseAccount(String who)
throws QueryParseException, IOException, ConfigInvalidException {
try {
- return args.accountResolver.resolve(who).asNonEmptyIdSet();
+ return args.accountResolver
+ .resolveAsUser(args.getUser(), who, forceAccountVisibilityCheck)
+ .asNonEmptyIdSet();
} catch (UnresolvableAccountException e) {
if (e.isSelf()) {
throw new QueryRequiresAuthException(e.getMessage(), e);
@@ -1698,7 +1726,9 @@
String who, java.util.function.Predicate<AccountState> activityFilter)
throws QueryParseException, IOException, ConfigInvalidException {
try {
- return args.accountResolver.resolve(who, activityFilter).asNonEmptyIdSet();
+ return args.accountResolver
+ .resolveAsUser(args.getUser(), who, activityFilter, forceAccountVisibilityCheck)
+ .asNonEmptyIdSet();
} catch (UnresolvableAccountException e) {
if (e.isSelf()) {
throw new QueryRequiresAuthException(e.getMessage(), e);
diff --git a/java/com/google/gerrit/server/query/change/InternalChangeQuery.java b/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
index ccd645b..3edad69 100644
--- a/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
+++ b/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
@@ -26,6 +26,7 @@
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
+import com.google.gerrit.common.UsedAt;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.Project;
@@ -103,6 +104,7 @@
return query(ChangePredicates.idStr(id));
}
+ @UsedAt(UsedAt.Project.GOOGLE)
public List<ChangeData> byLegacyChangeIds(Collection<Change.Id> ids) {
List<Predicate<ChangeData>> preds = new ArrayList<>(ids.size());
for (Change.Id id : ids) {
@@ -115,15 +117,6 @@
return query(byBranchKeyPred(branch, key));
}
- public List<ChangeData> byBranchKeyOpen(Project.NameKey project, String branch, Change.Key key) {
- return query(and(byBranchKeyPred(BranchNameKey.create(project, branch), key), open()));
- }
-
- public static Predicate<ChangeData> byBranchKeyOpenPred(
- Project.NameKey project, String branch, Change.Key key) {
- return and(byBranchKeyPred(BranchNameKey.create(project, branch), key), open());
- }
-
private static Predicate<ChangeData> byBranchKeyPred(BranchNameKey branch, Change.Key key) {
return and(ref(branch), project(branch.project()), change(key));
}
diff --git a/java/com/google/gerrit/server/query/project/ProjectPredicates.java b/java/com/google/gerrit/server/query/project/ProjectPredicates.java
index 8b4048f..a7b0743 100644
--- a/java/com/google/gerrit/server/query/project/ProjectPredicates.java
+++ b/java/com/google/gerrit/server/query/project/ProjectPredicates.java
@@ -25,23 +25,23 @@
/** Utility class to create predicates for project index queries. */
public class ProjectPredicates {
public static Predicate<ProjectData> name(Project.NameKey nameKey) {
- return new ProjectPredicate(ProjectField.NAME, nameKey.get());
+ return new ProjectPredicate(ProjectField.NAME_SPEC, nameKey.get());
}
public static Predicate<ProjectData> parent(Project.NameKey parentNameKey) {
- return new ProjectPredicate(ProjectField.PARENT_NAME, parentNameKey.get());
+ return new ProjectPredicate(ProjectField.PARENT_NAME_SPEC, parentNameKey.get());
}
public static Predicate<ProjectData> inname(String name) {
- return new ProjectPredicate(ProjectField.NAME_PART, name.toLowerCase(Locale.US));
+ return new ProjectPredicate(ProjectField.NAME_PART_SPEC, name.toLowerCase(Locale.US));
}
public static Predicate<ProjectData> description(String description) {
- return new ProjectPredicate(ProjectField.DESCRIPTION, description);
+ return new ProjectPredicate(ProjectField.DESCRIPTION_SPEC, description);
}
public static Predicate<ProjectData> state(ProjectState state) {
- return new ProjectPredicate(ProjectField.STATE, state.name());
+ return new ProjectPredicate(ProjectField.STATE_SPEC, state.name());
}
private ProjectPredicates() {}
diff --git a/java/com/google/gerrit/server/query/project/ProjectQueryBuilder.java b/java/com/google/gerrit/server/query/project/ProjectQueryBuilder.java
index d234546..edb12ec 100644
--- a/java/com/google/gerrit/server/query/project/ProjectQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/project/ProjectQueryBuilder.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2017 The Android Open Source Project
+// Copyright (C) 2023 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.
@@ -14,93 +14,21 @@
package com.google.gerrit.server.query.project;
-import com.google.common.base.Strings;
-import com.google.common.collect.Lists;
-import com.google.common.primitives.Ints;
-import com.google.gerrit.entities.Project;
-import com.google.gerrit.extensions.client.ProjectState;
import com.google.gerrit.index.project.ProjectData;
-import com.google.gerrit.index.query.LimitPredicate;
import com.google.gerrit.index.query.Predicate;
-import com.google.gerrit.index.query.QueryBuilder;
import com.google.gerrit.index.query.QueryParseException;
-import com.google.inject.Inject;
import java.util.List;
-/** Parses a query string meant to be applied to project objects. */
-public class ProjectQueryBuilder extends QueryBuilder<ProjectData, ProjectQueryBuilder> {
- public static final String FIELD_LIMIT = "limit";
+/**
+ * Provides methods required for parsing projects queries.
+ *
+ * <p>Internally (at google), this interface has a different implementation, comparing to upstream.
+ */
+public interface ProjectQueryBuilder {
+ String FIELD_LIMIT = "limit";
- private static final QueryBuilder.Definition<ProjectData, ProjectQueryBuilder> mydef =
- new QueryBuilder.Definition<>(ProjectQueryBuilder.class);
-
- @Inject
- ProjectQueryBuilder() {
- super(mydef, null);
- }
-
- @Operator
- public Predicate<ProjectData> name(String name) {
- return ProjectPredicates.name(Project.nameKey(name));
- }
-
- @Operator
- public Predicate<ProjectData> parent(String parentName) {
- return ProjectPredicates.parent(Project.nameKey(parentName));
- }
-
- @Operator
- public Predicate<ProjectData> inname(String namePart) {
- if (namePart.isEmpty()) {
- return name(namePart);
- }
- return ProjectPredicates.inname(namePart);
- }
-
- @Operator
- public Predicate<ProjectData> description(String description) throws QueryParseException {
- if (Strings.isNullOrEmpty(description)) {
- throw error("description operator requires a value");
- }
-
- return ProjectPredicates.description(description);
- }
-
- @Operator
- public Predicate<ProjectData> state(String state) throws QueryParseException {
- if (Strings.isNullOrEmpty(state)) {
- throw error("state operator requires a value");
- }
- ProjectState parsedState;
- try {
- parsedState = ProjectState.valueOf(state.replace('-', '_').toUpperCase());
- } catch (IllegalArgumentException e) {
- throw error("state operator must be either 'active' or 'read-only'", e);
- }
- if (parsedState == ProjectState.HIDDEN) {
- throw error("state operator must be either 'active' or 'read-only'");
- }
- return ProjectPredicates.state(parsedState);
- }
-
- @Override
- protected Predicate<ProjectData> defaultField(String query) throws QueryParseException {
- // Adapt the capacity of this list when adding more default predicates.
- List<Predicate<ProjectData>> preds = Lists.newArrayListWithCapacity(3);
- preds.add(name(query));
- preds.add(inname(query));
- if (!Strings.isNullOrEmpty(query)) {
- preds.add(description(query));
- }
- return Predicate.or(preds);
- }
-
- @Operator
- public Predicate<ProjectData> limit(String query) throws QueryParseException {
- Integer limit = Ints.tryParse(query);
- if (limit == null) {
- throw error("Invalid limit: " + query);
- }
- return new LimitPredicate<>(FIELD_LIMIT, limit);
- }
+ /** See {@link com.google.gerrit.index.query.QueryBuilder#parse(String)}. */
+ Predicate<ProjectData> parse(String query) throws QueryParseException;
+ /** See {@link com.google.gerrit.index.query.QueryBuilder#parse(List<String>)}. */
+ List<Predicate<ProjectData>> parse(List<String> queries) throws QueryParseException;
}
diff --git a/java/com/google/gerrit/server/query/project/ProjectQueryBuilderImpl.java b/java/com/google/gerrit/server/query/project/ProjectQueryBuilderImpl.java
new file mode 100644
index 0000000..f7135982
--- /dev/null
+++ b/java/com/google/gerrit/server/query/project/ProjectQueryBuilderImpl.java
@@ -0,0 +1,105 @@
+// Copyright (C) 2017 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.project;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import com.google.common.primitives.Ints;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.client.ProjectState;
+import com.google.gerrit.index.project.ProjectData;
+import com.google.gerrit.index.query.LimitPredicate;
+import com.google.gerrit.index.query.Predicate;
+import com.google.gerrit.index.query.QueryBuilder;
+import com.google.gerrit.index.query.QueryParseException;
+import com.google.inject.Inject;
+import java.util.List;
+
+/** Parses a query string meant to be applied to project objects. */
+public class ProjectQueryBuilderImpl extends QueryBuilder<ProjectData, ProjectQueryBuilderImpl>
+ implements ProjectQueryBuilder {
+ private static final QueryBuilder.Definition<ProjectData, ProjectQueryBuilderImpl> mydef =
+ new QueryBuilder.Definition<>(ProjectQueryBuilderImpl.class);
+
+ @Inject
+ ProjectQueryBuilderImpl() {
+ super(mydef, null);
+ }
+
+ @Operator
+ public Predicate<ProjectData> name(String name) {
+ return ProjectPredicates.name(Project.nameKey(name));
+ }
+
+ @Operator
+ public Predicate<ProjectData> parent(String parentName) {
+ return ProjectPredicates.parent(Project.nameKey(parentName));
+ }
+
+ @Operator
+ public Predicate<ProjectData> inname(String namePart) {
+ if (namePart.isEmpty()) {
+ return name(namePart);
+ }
+ return ProjectPredicates.inname(namePart);
+ }
+
+ @Operator
+ public Predicate<ProjectData> description(String description) throws QueryParseException {
+ if (Strings.isNullOrEmpty(description)) {
+ throw error("description operator requires a value");
+ }
+
+ return ProjectPredicates.description(description);
+ }
+
+ @Operator
+ public Predicate<ProjectData> state(String state) throws QueryParseException {
+ if (Strings.isNullOrEmpty(state)) {
+ throw error("state operator requires a value");
+ }
+ ProjectState parsedState;
+ try {
+ parsedState = ProjectState.valueOf(state.replace('-', '_').toUpperCase());
+ } catch (IllegalArgumentException e) {
+ throw error("state operator must be either 'active' or 'read-only'", e);
+ }
+ if (parsedState == ProjectState.HIDDEN) {
+ throw error("state operator must be either 'active' or 'read-only'");
+ }
+ return ProjectPredicates.state(parsedState);
+ }
+
+ @Override
+ protected Predicate<ProjectData> defaultField(String query) throws QueryParseException {
+ // Adapt the capacity of this list when adding more default predicates.
+ List<Predicate<ProjectData>> preds = Lists.newArrayListWithCapacity(3);
+ preds.add(name(query));
+ preds.add(inname(query));
+ if (!Strings.isNullOrEmpty(query)) {
+ preds.add(description(query));
+ }
+ return Predicate.or(preds);
+ }
+
+ @Operator
+ public Predicate<ProjectData> limit(String query) throws QueryParseException {
+ Integer limit = Ints.tryParse(query);
+ if (limit == null) {
+ throw error("Invalid limit: " + query);
+ }
+ return new LimitPredicate<>(FIELD_LIMIT, limit);
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/change/ChangeRestApiModule.java b/java/com/google/gerrit/server/restapi/change/ChangeRestApiModule.java
index 87d8cd0..f49ee7f 100644
--- a/java/com/google/gerrit/server/restapi/change/ChangeRestApiModule.java
+++ b/java/com/google/gerrit/server/restapi/change/ChangeRestApiModule.java
@@ -113,6 +113,7 @@
post(CHANGE_KIND, "submit").to(Submit.CurrentRevision.class);
get(CHANGE_KIND, "submitted_together").to(SubmittedTogether.class);
post(CHANGE_KIND, "rebase").to(Rebase.CurrentRevision.class);
+ post(CHANGE_KIND, "rebase:chain").to(RebaseChain.class);
post(CHANGE_KIND, "index").to(Index.class);
post(CHANGE_KIND, "move").to(Move.class);
post(CHANGE_KIND, "private").to(PostPrivate.class);
diff --git a/java/com/google/gerrit/server/restapi/change/CherryPickChange.java b/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
index 7e7892c..c192500 100644
--- a/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
+++ b/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
@@ -19,7 +19,6 @@
import com.google.auto.value.AutoValue;
import com.google.common.base.Strings;
-import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
@@ -42,6 +41,7 @@
import com.google.gerrit.server.change.PatchSetInserter;
import com.google.gerrit.server.change.ResetCherryPickOp;
import com.google.gerrit.server.change.SetCherryPickOp;
+import com.google.gerrit.server.change.ValidationOptionsUtil;
import com.google.gerrit.server.git.CodeReviewCommit;
import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
import com.google.gerrit.server.git.CommitUtil;
@@ -72,7 +72,6 @@
import java.time.ZoneId;
import java.util.HashSet;
import java.util.List;
-import java.util.Map;
import java.util.Set;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.ObjectId;
@@ -403,7 +402,8 @@
if (shouldSetToReady(cherryPickCommit, destNotes, workInProgress)) {
inserter.setWorkInProgress(false);
}
- inserter.setValidationOptions(getValidateOptionsAsMultimap(input.validationOptions));
+ inserter.setValidationOptions(
+ ValidationOptionsUtil.getValidateOptionsAsMultimap(input.validationOptions));
bu.addOp(destChange.getId(), inserter);
PatchSet.Id sourcePatchSetId = sourceChange == null ? null : sourceChange.currentPatchSetId();
// If sourceChange is not provided, reset cherryPickOf to avoid stale value.
@@ -454,7 +454,8 @@
(sourceChange != null && sourceChange.isWorkInProgress())
|| !cherryPickCommit.getFilesWithGitConflicts().isEmpty());
}
- ins.setValidationOptions(getValidateOptionsAsMultimap(input.validationOptions));
+ ins.setValidationOptions(
+ ValidationOptionsUtil.getValidateOptionsAsMultimap(input.validationOptions));
BranchNameKey sourceBranch = sourceChange == null ? null : sourceChange.getDest();
PatchSet.Id sourcePatchSetId = sourceChange == null ? null : sourceChange.currentPatchSetId();
ins.setMessage(
@@ -500,20 +501,6 @@
return changeId;
}
- private static ImmutableListMultimap<String, String> getValidateOptionsAsMultimap(
- @Nullable Map<String, String> validationOptions) {
- if (validationOptions == null) {
- return ImmutableListMultimap.of();
- }
-
- ImmutableListMultimap.Builder<String, String> validationOptionsBuilder =
- ImmutableListMultimap.builder();
- validationOptions
- .entrySet()
- .forEach(e -> validationOptionsBuilder.put(e.getKey(), e.getValue()));
- return validationOptionsBuilder.build();
- }
-
private NotifyResolver.Result resolveNotify(CherryPickInput input)
throws BadRequestException, ConfigInvalidException, IOException {
return notifyResolver.resolve(
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteVoteOp.java b/java/com/google/gerrit/server/restapi/change/DeleteVoteOp.java
index 432f0da..0e1a218 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteVoteOp.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteVoteOp.java
@@ -20,6 +20,7 @@
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.LabelType;
import com.google.gerrit.entities.LabelTypes;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.PatchSetApproval;
@@ -37,7 +38,9 @@
import com.google.gerrit.server.mail.send.DeleteVoteSender;
import com.google.gerrit.server.mail.send.MessageIdGenerator;
import com.google.gerrit.server.mail.send.ReplyToChangeSender;
+import com.google.gerrit.server.permissions.LabelRemovalPermission;
import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.project.DeleteVoteControl;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.RemoveReviewerControl;
import com.google.gerrit.server.update.BatchUpdateOp;
@@ -75,6 +78,7 @@
private final VoteDeleted voteDeleted;
private final DeleteVoteSender.Factory deleteVoteSenderFactory;
+ private final DeleteVoteControl deleteVoteControl;
private final RemoveReviewerControl removeReviewerControl;
private final MessageIdGenerator messageIdGenerator;
@@ -96,8 +100,9 @@
ChangeMessagesUtil cmUtil,
VoteDeleted voteDeleted,
DeleteVoteSender.Factory deleteVoteSenderFactory,
- RemoveReviewerControl removeReviewerControl,
+ DeleteVoteControl deleteVoteControl,
MessageIdGenerator messageIdGenerator,
+ RemoveReviewerControl removeReviewerControl,
@Assisted Project.NameKey projectName,
@Assisted AccountState reviewerToDeleteVoteFor,
@Assisted String label,
@@ -109,6 +114,7 @@
this.cmUtil = cmUtil;
this.voteDeleted = voteDeleted;
this.deleteVoteSenderFactory = deleteVoteSenderFactory;
+ this.deleteVoteControl = deleteVoteControl;
this.removeReviewerControl = removeReviewerControl;
this.messageIdGenerator = messageIdGenerator;
@@ -143,12 +149,7 @@
newApprovals.put(a.label(), a.value());
continue;
} else if (enforcePermissions) {
- // For regular users, check if they are allowed to remove the vote.
- try {
- removeReviewerControl.checkRemoveReviewer(ctx.getNotes(), ctx.getUser(), a);
- } catch (AuthException e) {
- throw new AuthException("delete vote not permitted", e);
- }
+ checkPermissions(ctx, labelTypes.byLabel(a.labelId()).get(), a);
}
// Set the approval to 0 if vote is being removed.
newApprovals.put(a.label(), (short) 0);
@@ -185,18 +186,16 @@
CurrentUser user = ctx.getUser();
try {
NotifyResolver.Result notify = ctx.getNotify(change.getId());
- if (notify.shouldNotify()) {
- ReplyToChangeSender emailSender =
- deleteVoteSenderFactory.create(ctx.getProject(), change.getId());
- if (user.isIdentifiedUser()) {
- emailSender.setFrom(user.getAccountId());
- }
- emailSender.setChangeMessage(mailMessage, ctx.getWhen());
- emailSender.setNotify(notify);
- emailSender.setMessageId(
- messageIdGenerator.fromChangeUpdate(ctx.getRepoView(), change.currentPatchSetId()));
- emailSender.send();
+ ReplyToChangeSender emailSender =
+ deleteVoteSenderFactory.create(ctx.getProject(), change.getId());
+ if (user.isIdentifiedUser()) {
+ emailSender.setFrom(user.getAccountId());
}
+ emailSender.setChangeMessage(mailMessage, ctx.getWhen());
+ emailSender.setNotify(notify);
+ emailSender.setMessageId(
+ messageIdGenerator.fromChangeUpdate(ctx.getRepoView(), change.currentPatchSetId()));
+ emailSender.send();
} catch (Exception e) {
logger.atSevere().withCause(e).log("Cannot email update for change %s", change.getId());
}
@@ -211,4 +210,21 @@
user.isIdentifiedUser() ? user.asIdentifiedUser().state() : null,
ctx.getWhen());
}
+
+ private void checkPermissions(ChangeContext ctx, LabelType labelType, PatchSetApproval approval)
+ throws PermissionBackendException, AuthException {
+ boolean permitted =
+ removeReviewerControl.testRemoveReviewer(ctx.getNotes(), ctx.getUser(), approval)
+ || deleteVoteControl.testDeleteVotePermissions(
+ ctx.getUser(), ctx.getNotes(), approval, labelType);
+ if (!permitted) {
+ throw new AuthException(
+ "Delete vote not permitted.",
+ new AuthException(
+ "Both "
+ + new LabelRemovalPermission.WithValue(labelType, approval.value())
+ .describeForException()
+ + " and remove-reviewer are not permitted"));
+ }
+ }
}
diff --git a/java/com/google/gerrit/server/restapi/change/PostReview.java b/java/com/google/gerrit/server/restapi/change/PostReview.java
index 9a6b03e..22eb32c 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReview.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReview.java
@@ -17,7 +17,7 @@
import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.gerrit.entities.Patch.PATCHSET_LEVEL;
-import static com.google.gerrit.server.permissions.LabelPermission.ForUser.ON_BEHALF_OF;
+import static com.google.gerrit.server.permissions.AbstractLabelPermission.ForUser.ON_BEHALF_OF;
import static com.google.gerrit.server.project.ProjectCache.illegalState;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.stream.Collectors.groupingBy;
@@ -379,7 +379,8 @@
// Add the review ops.
logger.atFine().log("posting review");
PostReviewOp postReviewOp =
- postReviewOpFactory.create(projectState, revision.getPatchSet().id(), input);
+ postReviewOpFactory.create(
+ projectState, revision.getPatchSet().id(), input, revision.getAccountId());
bu.addOp(revision.getChange().getId(), postReviewOp);
// Adjust the attention set based on the input
diff --git a/java/com/google/gerrit/server/restapi/change/PostReviewOp.java b/java/com/google/gerrit/server/restapi/change/PostReviewOp.java
index 8a92046..29e453b 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReviewOp.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReviewOp.java
@@ -65,7 +65,6 @@
import com.google.gerrit.server.approval.ApprovalCopier;
import com.google.gerrit.server.approval.ApprovalsUtil;
import com.google.gerrit.server.change.EmailReviewComments;
-import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.extensions.events.CommentAdded;
import com.google.gerrit.server.logging.Metadata;
@@ -98,7 +97,8 @@
public class PostReviewOp implements BatchUpdateOp {
interface Factory {
- PostReviewOp create(ProjectState projectState, PatchSet.Id psId, ReviewInput in);
+ PostReviewOp create(
+ ProjectState projectState, PatchSet.Id psId, ReviewInput in, Account.Id reviewerId);
}
/**
@@ -193,6 +193,7 @@
private final ProjectState projectState;
private final PatchSet.Id psId;
private final ReviewInput in;
+ private final Account.Id reviewerId;
private final boolean publishPatchSetLevelComment;
private IdentifiedUser user;
@@ -221,7 +222,8 @@
PluginSetContext<OnPostReview> onPostReviews,
@Assisted ProjectState projectState,
@Assisted PatchSet.Id psId,
- @Assisted ReviewInput in) {
+ @Assisted ReviewInput in,
+ @Assisted Account.Id reviewerId) {
this.approvalCopier = approvalCopier;
this.approvalsUtil = approvalsUtil;
this.publishCommentUtil = publishCommentUtil;
@@ -238,6 +240,7 @@
this.projectState = projectState;
this.psId = psId;
this.in = in;
+ this.reviewerId = reviewerId;
}
@Override
@@ -273,12 +276,9 @@
if (mailMessage == null) {
return;
}
- NotifyResolver.Result notify = ctx.getNotify(notes.getChangeId());
- if (notify.shouldNotify()) {
- email
- .create(ctx, ps, notes.getMetaId(), mailMessage, comments, in.message, labelDelta)
- .sendAsync();
- }
+ email
+ .create(ctx, ps, notes.getMetaId(), mailMessage, comments, in.message, labelDelta)
+ .sendAsync();
String comment = mailMessage;
if (publishPatchSetLevelComment) {
// TODO(davido): Remove this workaround when patch set level comments are exposed in comment
@@ -649,10 +649,11 @@
del.add(c);
update.putApproval(normName, (short) 0);
}
- // Only allow voting again if the vote is copied over from a past patch-set, or the
- // values are different.
+ // Only allow voting again the values are different, if the real account differs or if the
+ // vote is copied over from a past patch-set.
} else if (c != null
&& (c.value() != ent.getValue()
+ || !c.realAccountId().equals(reviewerId)
|| (inLabels.containsKey(c.label()) && isApprovalCopiedOver(c, ctx.getNotes())))) {
PatchSetApproval.Builder b =
c.toBuilder()
diff --git a/java/com/google/gerrit/server/restapi/change/Rebase.java b/java/com/google/gerrit/server/restapi/change/Rebase.java
index 5e30dae..8a8d2ca 100644
--- a/java/com/google/gerrit/server/restapi/change/Rebase.java
+++ b/java/com/google/gerrit/server/restapi/change/Rebase.java
@@ -16,38 +16,30 @@
import static com.google.gerrit.server.project.ProjectCache.illegalState;
-import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.extensions.api.changes.RebaseInput;
import com.google.gerrit.extensions.client.ListChangesOption;
import com.google.gerrit.extensions.common.ChangeInfo;
-import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.extensions.webui.UiAction;
-import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.change.ChangeJson;
import com.google.gerrit.server.change.ChangeResource;
import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.change.RebaseChangeOp;
import com.google.gerrit.server.change.RebaseUtil;
-import com.google.gerrit.server.change.RebaseUtil.Base;
import com.google.gerrit.server.change.RevisionResource;
import com.google.gerrit.server.git.CodeReviewCommit;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.permissions.ChangePermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.update.BatchUpdate;
import com.google.gerrit.server.update.UpdateException;
@@ -55,13 +47,9 @@
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
-import java.util.Map;
-import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.ObjectReader;
-import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
@Singleton
@@ -72,7 +60,6 @@
private final BatchUpdate.Factory updateFactory;
private final GitRepositoryManager repoManager;
- private final RebaseChangeOp.Factory rebaseFactory;
private final RebaseUtil rebaseUtil;
private final ChangeJson.Factory json;
private final PermissionBackend permissionBackend;
@@ -83,7 +70,6 @@
public Rebase(
BatchUpdate.Factory updateFactory,
GitRepositoryManager repoManager,
- RebaseChangeOp.Factory rebaseFactory,
RebaseUtil rebaseUtil,
ChangeJson.Factory json,
PermissionBackend permissionBackend,
@@ -91,7 +77,6 @@
PatchSetUtil patchSetUtil) {
this.updateFactory = updateFactory;
this.repoManager = repoManager;
- this.rebaseFactory = rebaseFactory;
this.rebaseUtil = rebaseUtil;
this.json = json;
this.permissionBackend = permissionBackend;
@@ -102,10 +87,8 @@
@Override
public Response<ChangeInfo> apply(RevisionResource rsrc, RebaseInput input)
throws UpdateException, RestApiException, IOException, PermissionBackendException {
- // Not allowed to rebase if the current patch set is locked.
- patchSetUtil.checkPatchSetNotLocked(rsrc.getNotes());
-
rsrc.permissions().check(ChangePermission.REBASE);
+
projectCache
.get(rsrc.getProject())
.orElseThrow(illegalState(rsrc.getProject()))
@@ -118,19 +101,14 @@
RevWalk rw = CodeReviewCommit.newRevWalk(reader);
BatchUpdate bu =
updateFactory.create(change.getProject(), rsrc.getUser(), TimeUtil.now())) {
- if (!change.isNew()) {
- throw new ResourceConflictException("change is " + ChangeUtil.status(change));
- } else if (!hasOneParent(rw, rsrc.getPatchSet())) {
- throw new ResourceConflictException(
- "cannot rebase merge commits or commit with no ancestor");
- }
+ rebaseUtil.verifyRebasePreconditions(rw, rsrc.getNotes(), rsrc.getPatchSet());
+
RebaseChangeOp rebaseOp =
- rebaseFactory
- .create(rsrc.getNotes(), rsrc.getPatchSet(), findBaseRev(repo, rw, rsrc, input))
- .setForceContentMerge(true)
- .setAllowConflicts(input.allowConflicts)
- .setValidationOptions(getValidateOptionsAsMultimap(input.validationOptions))
- .setFireRevisionCreated(true);
+ rebaseUtil.getRebaseOp(
+ rsrc,
+ input,
+ rebaseUtil.parseOrFindBaseRevision(repo, rw, permissionBackend, rsrc, input, true));
+
// TODO(dborowitz): Why no notification? This seems wrong; dig up blame.
bu.setNotify(NotifyResolver.Result.none());
bu.setRepository(repo, rw, oi);
@@ -144,76 +122,6 @@
}
}
- private ObjectId findBaseRev(
- Repository repo, RevWalk rw, RevisionResource rsrc, RebaseInput input)
- throws RestApiException, IOException, NoSuchChangeException, AuthException,
- PermissionBackendException {
- BranchNameKey destRefKey = rsrc.getChange().getDest();
- if (input == null || input.base == null) {
- return rebaseUtil.findBaseRevision(rsrc.getPatchSet(), destRefKey, repo, rw);
- }
-
- Change change = rsrc.getChange();
- String str = input.base.trim();
- if (str.equals("")) {
- // Remove existing dependency to other patch set.
- Ref destRef = repo.exactRef(destRefKey.branch());
- if (destRef == null) {
- throw new ResourceConflictException(
- "can't rebase onto tip of branch " + destRefKey.branch() + "; branch doesn't exist");
- }
- return destRef.getObjectId();
- }
-
- Base base;
- try {
- base = rebaseUtil.parseBase(rsrc, str);
- if (base == null) {
- throw new ResourceConflictException(
- "base revision is missing from the destination branch: " + str);
- }
- } catch (NoSuchChangeException e) {
- throw new UnprocessableEntityException(
- String.format("Base change not found: %s", input.base), e);
- }
-
- PatchSet.Id baseId = base.patchSet().id();
- if (change.getId().equals(baseId.changeId())) {
- throw new ResourceConflictException("cannot rebase change onto itself");
- }
-
- permissionBackend.user(rsrc.getUser()).change(base.notes()).check(ChangePermission.READ);
-
- Change baseChange = base.notes().getChange();
- if (!baseChange.getProject().equals(change.getProject())) {
- throw new ResourceConflictException(
- "base change is in wrong project: " + baseChange.getProject());
- } else if (!baseChange.getDest().equals(change.getDest())) {
- throw new ResourceConflictException(
- "base change is targeting wrong branch: " + baseChange.getDest());
- } else if (baseChange.isAbandoned()) {
- throw new ResourceConflictException("base change is abandoned: " + baseChange.getKey());
- } else if (isMergedInto(rw, rsrc.getPatchSet(), base.patchSet())) {
- throw new ResourceConflictException(
- "base change "
- + baseChange.getKey()
- + " is a descendant of the current change - recursion not allowed");
- }
- return base.patchSet().commitId();
- }
-
- private boolean isMergedInto(RevWalk rw, PatchSet base, PatchSet tip) throws IOException {
- ObjectId baseId = base.commitId();
- ObjectId tipId = tip.commitId();
- return rw.isMergedInto(rw.parseCommit(baseId), rw.parseCommit(tipId));
- }
-
- private boolean hasOneParent(RevWalk rw, PatchSet ps) throws IOException {
- // Prevent rebase of exotic changes (merge commit, no ancestor).
- RevCommit c = rw.parseCommit(ps.commitId());
- return c.getParentCount() == 1;
- }
-
@Override
public UiAction.Description getDescription(RevisionResource rsrc) throws IOException {
UiAction.Description description =
@@ -241,7 +149,7 @@
boolean enabled = false;
try (Repository repo = repoManager.openRepository(change.getDest().project());
RevWalk rw = new RevWalk(repo)) {
- if (hasOneParent(rw, rsrc.getPatchSet())) {
+ if (RebaseUtil.hasOneParent(rw, rsrc.getPatchSet())) {
enabled = rebaseUtil.canRebase(rsrc.getPatchSet(), change.getDest(), repo, rw);
}
}
@@ -252,20 +160,6 @@
return description;
}
- private static ImmutableListMultimap<String, String> getValidateOptionsAsMultimap(
- @Nullable Map<String, String> validationOptions) {
- if (validationOptions == null) {
- return ImmutableListMultimap.of();
- }
-
- ImmutableListMultimap.Builder<String, String> validationOptionsBuilder =
- ImmutableListMultimap.builder();
- validationOptions
- .entrySet()
- .forEach(e -> validationOptionsBuilder.put(e.getKey(), e.getValue()));
- return validationOptionsBuilder.build();
- }
-
public static class CurrentRevision implements RestModifyView<ChangeResource, RebaseInput> {
private final PatchSetUtil psUtil;
private final Rebase rebase;
diff --git a/java/com/google/gerrit/server/restapi/change/RebaseChain.java b/java/com/google/gerrit/server/restapi/change/RebaseChain.java
new file mode 100644
index 0000000..786bba7
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/change/RebaseChain.java
@@ -0,0 +1,277 @@
+// Copyright (C) 2022 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.restapi.change;
+
+import static com.google.gerrit.server.project.ProjectCache.illegalState;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.api.changes.RebaseInput;
+import com.google.gerrit.extensions.client.ListChangesOption;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.RebaseChainInfo;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.webui.UiAction;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.PatchSetUtil;
+import com.google.gerrit.server.change.ChangeJson;
+import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.change.GetRelatedChangesUtil;
+import com.google.gerrit.server.change.NotifyResolver;
+import com.google.gerrit.server.change.RebaseChangeOp;
+import com.google.gerrit.server.change.RebaseUtil;
+import com.google.gerrit.server.change.RelatedChangesSorter.PatchSetData;
+import com.google.gerrit.server.change.RevisionResource;
+import com.google.gerrit.server.git.CodeReviewCommit;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.permissions.ChangePermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.update.BatchUpdate;
+import com.google.gerrit.server.update.UpdateException;
+import com.google.gerrit.server.util.time.TimeUtil;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+/** Rest API for rebasing an ancestry chain of changes. */
+@Singleton
+public class RebaseChain
+ implements RestModifyView<ChangeResource, RebaseInput>, UiAction<ChangeResource> {
+ private static final ImmutableSet<ListChangesOption> OPTIONS =
+ Sets.immutableEnumSet(ListChangesOption.CURRENT_REVISION, ListChangesOption.CURRENT_COMMIT);
+
+ private final GitRepositoryManager repoManager;
+ private final RebaseUtil rebaseUtil;
+ private final GetRelatedChangesUtil getRelatedChangesUtil;
+ private final ChangeResource.Factory changeResourceFactory;
+ private final ChangeData.Factory changeDataFactory;
+ private final PermissionBackend permissionBackend;
+ private final BatchUpdate.Factory updateFactory;
+ private final ChangeNotes.Factory notesFactory;
+ private final ProjectCache projectCache;
+ private final PatchSetUtil patchSetUtil;
+ private final ChangeJson.Factory json;
+
+ @Inject
+ RebaseChain(
+ GitRepositoryManager repoManager,
+ RebaseUtil rebaseUtil,
+ GetRelatedChangesUtil getRelatedChangesUtil,
+ ChangeResource.Factory changeResourceFactory,
+ ChangeData.Factory changeDataFactory,
+ PermissionBackend permissionBackend,
+ BatchUpdate.Factory updateFactory,
+ ChangeNotes.Factory notesFactory,
+ ProjectCache projectCache,
+ PatchSetUtil patchSetUtil,
+ ChangeJson.Factory json) {
+ this.repoManager = repoManager;
+ this.getRelatedChangesUtil = getRelatedChangesUtil;
+ this.changeDataFactory = changeDataFactory;
+ this.rebaseUtil = rebaseUtil;
+ this.changeResourceFactory = changeResourceFactory;
+ this.permissionBackend = permissionBackend;
+ this.updateFactory = updateFactory;
+ this.notesFactory = notesFactory;
+ this.projectCache = projectCache;
+ this.patchSetUtil = patchSetUtil;
+ this.json = json;
+ }
+
+ @Override
+ public Response<RebaseChainInfo> apply(ChangeResource tipRsrc, RebaseInput input)
+ throws IOException, PermissionBackendException, RestApiException, UpdateException {
+ tipRsrc.permissions().check(ChangePermission.REBASE);
+
+ Project.NameKey project = tipRsrc.getProject();
+ projectCache.get(project).orElseThrow(illegalState(project)).checkStatePermitsWrite();
+
+ CurrentUser user = tipRsrc.getUser();
+
+ List<Change.Id> upToDateAncestors = new ArrayList<>();
+ Map<Change.Id, RebaseChangeOp> rebaseOps = new LinkedHashMap<>();
+ try (Repository repo = repoManager.openRepository(project);
+ ObjectInserter oi = repo.newObjectInserter();
+ ObjectReader reader = oi.newReader();
+ RevWalk rw = CodeReviewCommit.newRevWalk(reader);
+ BatchUpdate bu = updateFactory.create(project, user, TimeUtil.now())) {
+ List<PatchSetData> chain = getChainForCurrentPatchSet(tipRsrc);
+
+ boolean ancestorsAreUpToDate = true;
+ for (int i = 0; i < chain.size(); i++) {
+ ChangeData changeData = chain.get(i).data();
+ PatchSet ps = patchSetUtil.current(changeData.notes());
+ if (ps == null) {
+ throw new IllegalStateException(
+ "current revision is missing for change " + changeData.getId());
+ }
+
+ RevisionResource revRsrc =
+ new RevisionResource(changeResourceFactory.create(changeData, user), ps);
+ revRsrc.permissions().check(ChangePermission.REBASE);
+ rebaseUtil.verifyRebasePreconditions(rw, changeData.notes(), ps);
+
+ boolean isUpToDate = false;
+ RebaseChangeOp rebaseOp = null;
+ if (i == 0) {
+ ObjectId desiredBase =
+ rebaseUtil.parseOrFindBaseRevision(
+ repo, rw, permissionBackend, revRsrc, input, false);
+ if (currentBase(rw, ps).equals(desiredBase)) {
+ isUpToDate = true;
+ } else {
+ rebaseOp = rebaseUtil.getRebaseOp(revRsrc, input, desiredBase);
+ }
+ } else {
+ if (ancestorsAreUpToDate) {
+ ObjectId latestCommittedBase =
+ PatchSetUtil.getCurrentCommittedRevCommit(
+ project, rw, notesFactory, chain.get(i - 1).id());
+ isUpToDate = currentBase(rw, ps).equals(latestCommittedBase);
+ }
+ if (!isUpToDate) {
+ rebaseOp = rebaseUtil.getRebaseOp(revRsrc, input, chain.get(i - 1).id());
+ }
+ }
+
+ if (isUpToDate) {
+ upToDateAncestors.add(changeData.getId());
+ continue;
+ }
+ ancestorsAreUpToDate = false;
+ bu.addOp(revRsrc.getChange().getId(), rebaseOp);
+ rebaseOps.put(revRsrc.getChange().getId(), rebaseOp);
+ }
+
+ if (ancestorsAreUpToDate) {
+ throw new ResourceConflictException("The whole chain is already up to date.");
+ }
+
+ bu.setNotify(NotifyResolver.Result.none());
+ bu.setRepository(repo, rw, oi);
+ bu.execute();
+ }
+
+ RebaseChainInfo res = new RebaseChainInfo();
+ res.rebasedChanges = new ArrayList<>();
+ ChangeJson changeJson = json.create(OPTIONS);
+ for (Change.Id c : upToDateAncestors) {
+ res.rebasedChanges.add(changeJson.format(project, c));
+ }
+ for (Map.Entry<Change.Id, RebaseChangeOp> e : rebaseOps.entrySet()) {
+ Change.Id id = e.getKey();
+ RebaseChangeOp op = e.getValue();
+ ChangeInfo changeInfo = changeJson.format(project, id);
+ changeInfo.containsGitConflicts =
+ !op.getRebasedCommit().getFilesWithGitConflicts().isEmpty() ? true : null;
+ res.rebasedChanges.add(changeInfo);
+ }
+ if (res.rebasedChanges.stream()
+ .anyMatch(i -> i.containsGitConflicts != null && i.containsGitConflicts)) {
+ res.containsGitConflicts = true;
+ }
+ return Response.ok(res);
+ }
+
+ @Override
+ public Description getDescription(ChangeResource tipRsrc) throws Exception {
+ UiAction.Description description =
+ new UiAction.Description()
+ .setLabel("Rebase Chain")
+ .setTitle(
+ "Rebase the ancestry chain onto the tip of the target branch. Makes you the "
+ + "uploader of the changes which can affect validity of approvals.")
+ .setVisible(false);
+
+ Change tip = tipRsrc.getChange();
+ if (!tip.isNew()) {
+ return description;
+ }
+ if (!projectCache
+ .get(tipRsrc.getProject())
+ .orElseThrow(illegalState(tipRsrc.getProject()))
+ .statePermitsWrite()) {
+ return description;
+ }
+
+ if (patchSetUtil.isPatchSetLocked(tipRsrc.getNotes())) {
+ return description;
+ }
+
+ boolean visible = true;
+ boolean enabled = true;
+ try (Repository repo = repoManager.openRepository(tipRsrc.getProject());
+ RevWalk rw = new RevWalk(repo)) {
+ List<PatchSetData> chain = getChainForCurrentPatchSet(tipRsrc);
+ PatchSetData oldestAncestor = chain.get(0);
+ if (rebaseUtil.canRebase(
+ oldestAncestor.patchSet(), oldestAncestor.data().change().getDest(), repo, rw)) {
+ enabled = false;
+ }
+
+ for (PatchSetData ps : chain) {
+ RevisionResource psRsrc =
+ new RevisionResource(
+ changeResourceFactory.create(ps.data(), tipRsrc.getUser()), ps.patchSet());
+
+ if (!psRsrc.permissions().testOrFalse(ChangePermission.REBASE)) {
+ visible = false;
+ break;
+ }
+
+ if (patchSetUtil.isPatchSetLocked(psRsrc.getNotes())) {
+ enabled = false;
+ }
+ if (!RebaseUtil.hasOneParent(rw, psRsrc.getPatchSet())) {
+ enabled = false;
+ }
+ }
+ }
+ return description.setVisible(visible).setEnabled(enabled);
+ }
+
+ private ObjectId currentBase(RevWalk rw, PatchSet ps) throws IOException {
+ return rw.parseCommit(ps.commitId()).getParent(0);
+ }
+
+ private List<PatchSetData> getChainForCurrentPatchSet(ChangeResource rsrc)
+ throws PermissionBackendException, IOException {
+ return Lists.reverse(
+ getRelatedChangesUtil.getAncestors(
+ changeDataFactory.create(rsrc.getNotes()),
+ patchSetUtil.current(rsrc.getNotes()),
+ true));
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/project/CreateBranch.java b/java/com/google/gerrit/server/restapi/project/CreateBranch.java
index c39b1f4..17fc6db 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateBranch.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateBranch.java
@@ -17,8 +17,6 @@
import static com.google.gerrit.entities.RefNames.isConfigRef;
import com.google.common.base.Strings;
-import com.google.common.collect.ImmutableListMultimap;
-import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.extensions.api.projects.BranchInfo;
@@ -32,6 +30,7 @@
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.git.LockFailureException;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.change.ValidationOptionsUtil;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.permissions.PermissionBackend;
@@ -48,7 +47,6 @@
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
-import java.util.Map;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
@@ -160,7 +158,7 @@
rsrc.getName(),
identifiedUser.get(),
u,
- getValidateOptionsAsMultimap(input.validationOptions));
+ ValidationOptionsUtil.getValidateOptionsAsMultimap(input.validationOptions));
RefUpdate.Result result = u.update(rw);
switch (result) {
case FAST_FORWARD:
@@ -220,18 +218,4 @@
private boolean isBranchAllowed(String branch) {
return !RefNames.isGerritRef(branch) && !branch.startsWith(RefNames.REFS_TAGS);
}
-
- private static ImmutableListMultimap<String, String> getValidateOptionsAsMultimap(
- @Nullable Map<String, String> validationOptions) {
- if (validationOptions == null) {
- return ImmutableListMultimap.of();
- }
-
- ImmutableListMultimap.Builder<String, String> validationOptionsBuilder =
- ImmutableListMultimap.builder();
- validationOptions
- .entrySet()
- .forEach(e -> validationOptionsBuilder.put(e.getKey(), e.getValue()));
- return validationOptionsBuilder.build();
- }
}
diff --git a/java/com/google/gerrit/server/restapi/project/CreateChange.java b/java/com/google/gerrit/server/restapi/project/CreateChange.java
index 59efd06..2f1153e 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateChange.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateChange.java
@@ -24,6 +24,7 @@
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.ProjectUtil;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.InvalidChangeOperationException;
import com.google.gerrit.server.project.ProjectResource;
@@ -59,7 +60,8 @@
throw new AuthException("Authentication required");
}
- if (!Strings.isNullOrEmpty(input.project) && !rsrc.getName().equals(input.project)) {
+ if (!Strings.isNullOrEmpty(input.project)
+ && !rsrc.getName().equals(ProjectUtil.sanitizeProjectName(input.project))) {
throw new BadRequestException("project must match URL");
}
diff --git a/java/com/google/gerrit/server/schema/MigrateLabelFunctionsToSubmitRequirement.java b/java/com/google/gerrit/server/schema/MigrateLabelFunctionsToSubmitRequirement.java
index 491b5cd..f3c741f 100644
--- a/java/com/google/gerrit/server/schema/MigrateLabelFunctionsToSubmitRequirement.java
+++ b/java/com/google/gerrit/server/schema/MigrateLabelFunctionsToSubmitRequirement.java
@@ -37,6 +37,7 @@
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
+import java.util.stream.Collectors;
import javax.inject.Inject;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.Config;
@@ -216,12 +217,19 @@
Arrays.asList(
cfg.getStringList(ProjectConfig.LABEL, labelName, ProjectConfig.KEY_VALUE)))
.build();
+ ImmutableList<String> refPatterns =
+ ImmutableList.<String>builder()
+ .addAll(
+ Arrays.asList(
+ cfg.getStringList(ProjectConfig.LABEL, labelName, ProjectConfig.KEY_BRANCH)))
+ .build();
LabelAttributes attributes =
LabelAttributes.create(
function == null ? "MaxWithBlock" : function,
canOverride,
ignoreSelfApproval,
- values);
+ values,
+ refPatterns);
labelTypes.put(labelName, attributes);
}
return labelTypes;
@@ -320,6 +328,15 @@
default:
break;
}
+ if (!attributes.refPatterns().isEmpty()) {
+ builder.setApplicabilityExpression(
+ SubmitRequirementExpression.of(
+ String.join(
+ " OR ",
+ attributes.refPatterns().stream()
+ .map(b -> "branch:\\\"" + b + "\\\"")
+ .collect(Collectors.toList()))));
+ }
return builder.build();
}
@@ -435,13 +452,16 @@
abstract ImmutableList<String> values();
+ abstract ImmutableList<String> refPatterns();
+
static LabelAttributes create(
String function,
boolean canOverride,
boolean ignoreSelfApproval,
- ImmutableList<String> values) {
+ ImmutableList<String> values,
+ ImmutableList<String> refPatterns) {
return new AutoValue_MigrateLabelFunctionsToSubmitRequirement_LabelAttributes(
- function, canOverride, ignoreSelfApproval, values);
+ function, canOverride, ignoreSelfApproval, values, refPatterns);
}
}
}
diff --git a/java/com/google/gerrit/server/submit/MergeOp.java b/java/com/google/gerrit/server/submit/MergeOp.java
index a34aeac..14a636f 100644
--- a/java/com/google/gerrit/server/submit/MergeOp.java
+++ b/java/com/google/gerrit/server/submit/MergeOp.java
@@ -535,9 +535,6 @@
// Multiply the timeout by the number of projects we're actually attempting to
// submit. Times 2 to retry more persistently, to increase success rate.
.defaultTimeoutMultiplier(filteredNoteDbChangeSet.projects().size() * 2)
- // By default, we only retry lock failures. Here it's better to also retry unexpected
- // runtime exceptions.
- .retryOn(t -> t instanceof RuntimeException)
.call();
submissionExecutor.afterExecutions(orm);
diff --git a/java/com/google/gerrit/server/submit/SubmitStrategy.java b/java/com/google/gerrit/server/submit/SubmitStrategy.java
index f638078..bdda3fc5 100644
--- a/java/com/google/gerrit/server/submit/SubmitStrategy.java
+++ b/java/com/google/gerrit/server/submit/SubmitStrategy.java
@@ -21,6 +21,7 @@
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Sets;
+import com.google.common.flogger.FluentLogger;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.SubmissionId;
@@ -76,6 +77,8 @@
* merged.
*/
public abstract class SubmitStrategy {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
public static Module module() {
return new FactoryModule() {
@Override
@@ -275,6 +278,7 @@
Change.Id id = c.change().getId();
bu.addOp(id, args.setPrivateOpFactory.create(false, null));
ImplicitIntegrateOp implicitIntegrateOp = new ImplicitIntegrateOp(args, c);
+ logger.atFine().log("Add implicit integrate op: %s", implicitIntegrateOp);
bu.addOp(id, implicitIntegrateOp);
maybeAddTestHelperOp(bu, id);
this.submitStrategyOps.add(implicitIntegrateOp);
@@ -282,6 +286,7 @@
// Then ops for explicitly merged changes
for (SubmitStrategyOp op : ops) {
+ logger.atFine().log("Add explicit integrate op: %s", op);
bu.addOp(op.getId(), args.setPrivateOpFactory.create(false, null));
bu.addOp(op.getId(), op);
maybeAddTestHelperOp(bu, op.getId());
diff --git a/java/com/google/gerrit/server/submit/SubmitStrategyOp.java b/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
index bb2b1a4..96dc326 100644
--- a/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
+++ b/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
@@ -21,6 +21,7 @@
import static java.util.Comparator.comparing;
import static java.util.Objects.requireNonNull;
+import com.google.common.base.MoreObjects;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.BranchNameKey;
@@ -127,7 +128,8 @@
logger.atFine().log("No merge tip, no update to perform");
return;
}
- logger.atFine().log("Moved tip from %s to %s", tipBefore, tipAfter);
+ logger.atFine().log(
+ "Moved tip from %s to %s (branch = %s)", tipBefore, tipAfter, getDest().branch());
checkProjectConfig(ctx, tipAfter);
@@ -567,4 +569,14 @@
e);
}
}
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("commit", getCommit().name())
+ .add("changeId", getId())
+ .add("dest", getDest().branch())
+ .add("project", getProject())
+ .toString();
+ }
}
diff --git a/java/com/google/gerrit/server/update/BatchUpdate.java b/java/com/google/gerrit/server/update/BatchUpdate.java
index 32529f7..412a8ee 100644
--- a/java/com/google/gerrit/server/update/BatchUpdate.java
+++ b/java/com/google/gerrit/server/update/BatchUpdate.java
@@ -374,8 +374,13 @@
/** Per-change result status from {@link #executeChangeOps}. */
private enum ChangeResult {
+ /** Change was not modified by any of the batch update ops. */
SKIPPED,
+
+ /** Change was inserted or updated. */
UPSERTED,
+
+ /** Change was deleted. */
DELETED
}
@@ -669,7 +674,7 @@
indexFutures.add(indexer.indexAsync(project, id));
break;
case DELETED:
- indexFutures.add(indexer.deleteAsync(id));
+ indexFutures.add(indexer.deleteAsync(project, id));
break;
case SKIPPED:
break;
diff --git a/java/com/google/gerrit/testing/InMemoryModule.java b/java/com/google/gerrit/testing/InMemoryModule.java
index aadf6d4..b828037 100644
--- a/java/com/google/gerrit/testing/InMemoryModule.java
+++ b/java/com/google/gerrit/testing/InMemoryModule.java
@@ -44,6 +44,7 @@
import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.api.GerritApiModule;
import com.google.gerrit.server.api.PluginApiModule;
+import com.google.gerrit.server.api.projects.ProjectQueryBuilderModule;
import com.google.gerrit.server.audit.AuditModule;
import com.google.gerrit.server.cache.h2.H2CacheModule;
import com.google.gerrit.server.cache.mem.DefaultMemoryCacheModule;
@@ -191,6 +192,7 @@
AuthConfig authConfig = cfgInjector.getInstance(AuthConfig.class);
install(new AuthModule(authConfig));
install(new GerritApiModule());
+ install(new ProjectQueryBuilderModule());
factory(PluginUser.Factory.class);
install(new PluginApiModule());
install(new DefaultPermissionBackendModule());
diff --git a/java/com/google/gerrit/testing/TestChanges.java b/java/com/google/gerrit/testing/TestChanges.java
index 4a97bc5..c8f89cf 100644
--- a/java/com/google/gerrit/testing/TestChanges.java
+++ b/java/com/google/gerrit/testing/TestChanges.java
@@ -32,6 +32,7 @@
import com.google.gerrit.server.util.time.TimeUtil;
import com.google.inject.Injector;
import java.time.ZoneId;
+import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.ObjectId;
@@ -77,15 +78,20 @@
}
public static ChangeUpdate newUpdate(
- Injector injector, Change c, CurrentUser user, boolean shouldExist) throws Exception {
+ Injector injector, Change c, Optional<CurrentUser> user, boolean shouldExist)
+ throws Exception {
injector =
injector.createChildInjector(
new FactoryModule() {
@Override
public void configure() {
- bind(CurrentUser.class).toInstance(user);
+ if (user.isPresent()) {
+ // user may be already bound in injector
+ bind(CurrentUser.class).toInstance(user.get());
+ }
}
});
+ CurrentUser currentUser = injector.getProvider(CurrentUser.class).get();
ChangeUpdate update =
injector
.getInstance(ChangeUpdate.Factory.class)
@@ -93,7 +99,7 @@
new ChangeNotes(
injector.getInstance(AbstractChangeNotes.Args.class), c, shouldExist, null)
.load(),
- user,
+ currentUser,
TimeUtil.now(),
Ordering.natural());
@@ -109,7 +115,9 @@
try (Repository repo = repoManager.openRepository(c.getProject());
TestRepository<Repository> tr = new TestRepository<>(repo)) {
PersonIdent ident =
- user.asIdentifiedUser().newCommitterIdent(update.getWhen(), ZoneId.systemDefault());
+ currentUser
+ .asIdentifiedUser()
+ .newCommitterIdent(update.getWhen(), ZoneId.systemDefault());
TestRepository<Repository>.CommitBuilder cb =
tr.commit()
.author(ident)
diff --git a/java/com/google/gerrit/util/cli/ApiProtocolBufferGenerator.java b/java/com/google/gerrit/util/cli/ApiProtocolBufferGenerator.java
new file mode 100644
index 0000000..27e4b17
--- /dev/null
+++ b/java/com/google/gerrit/util/cli/ApiProtocolBufferGenerator.java
@@ -0,0 +1,176 @@
+// Copyright (C) 2023 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.util.cli;
+
+import com.google.common.base.CaseFormat;
+import com.google.common.reflect.ClassPath;
+import java.lang.reflect.Field;
+import java.lang.reflect.ParameterizedType;
+import java.sql.Timestamp;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * Utility to generate Protocol Buffers (*.proto) files from existing POJO API types.
+ *
+ * <p>Usage:
+ *
+ * <ul>
+ * <li>Print proto representation of all API objects: {@code bazelisk run
+ * java/com/google/gerrit/util/cli:protogen}
+ * </ul>
+ */
+public class ApiProtocolBufferGenerator {
+ private static String NOTICE =
+ "// Copyright (C) 2023 The Android Open Source Project\n"
+ + "//\n"
+ + "// Licensed under the Apache License, Version 2.0 (the \"License\");\n"
+ + "// you may not use this file except in compliance with the License.\n"
+ + "// You may obtain a copy of the License at\n"
+ + "//\n"
+ + "// http://www.apache.org/licenses/LICENSE-2.0\n"
+ + "//\n"
+ + "// Unless required by applicable law or agreed to in writing, software\n"
+ + "// distributed under the License is distributed on an \"AS IS\" BASIS,\n"
+ + "// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n"
+ + "// See the License for the specific language governing permissions and\n"
+ + "// limitations under the License.";
+
+ private static String PACKAGE = "com.google.gerrit.extensions.common";
+
+ public static void main(String[] args) {
+ try {
+ ClassPath.from(ClassLoader.getSystemClassLoader()).getAllClasses().stream()
+ .filter(c -> c.getPackageName().equalsIgnoreCase(PACKAGE))
+ .filter(c -> c.getName().endsWith("Input") || c.getName().endsWith("Info"))
+ .map(clazz -> clazz.load())
+ .forEach(ApiProtocolBufferGenerator::exportSingleClass);
+ } catch (Exception e) {
+ System.err.println(e);
+ }
+ }
+
+ private static void exportSingleClass(Class<?> clazz) {
+ StringBuilder proto = new StringBuilder(NOTICE);
+ proto.append("\n\nsyntax = \"proto3\";");
+ proto.append("\n\npackage gerrit.api;");
+ proto.append("\n\noption java_package = \"" + PACKAGE + "\";");
+
+ int fieldNumber = 1;
+
+ proto.append("\n\n\nmessage " + clazz.getSimpleName() + " {\n");
+
+ for (Field f : clazz.getFields()) {
+ Class<?> type = f.getType();
+
+ if (type.isAssignableFrom(List.class)) {
+ ParameterizedType list = (ParameterizedType) f.getGenericType();
+ Class<?> genericType = (Class<?>) list.getActualTypeArguments()[0];
+ String protoType =
+ protoType(genericType)
+ .orElseThrow(() -> new IllegalStateException("unknown type: " + genericType));
+ proto.append(
+ String.format(
+ "repeated %s %s = %d;\n", protoType, protoName(f.getName()), fieldNumber));
+ } else if (type.isAssignableFrom(Map.class)) {
+ ParameterizedType map = (ParameterizedType) f.getGenericType();
+ Class<?> key = (Class<?>) map.getActualTypeArguments()[0];
+ if (map.getActualTypeArguments()[1] instanceof ParameterizedType) {
+ // TODO: This is list multimap which proto doesn't support. Move to
+ // it's own types.
+ proto.append(
+ "reserved "
+ + fieldNumber
+ + "; // TODO(hiesel): Add support for map<?,repeated <?>>\n");
+ } else {
+ Class<?> value = (Class<?>) map.getActualTypeArguments()[1];
+ String keyProtoType =
+ protoType(key).orElseThrow(() -> new IllegalStateException("unknown type: " + key));
+ String valueProtoType =
+ protoType(value)
+ .orElseThrow(() -> new IllegalStateException("unknown type: " + value));
+ proto.append(
+ String.format(
+ "map<%s,%s> %s = %d;\n",
+ keyProtoType, valueProtoType, protoName(f.getName()), fieldNumber));
+ }
+ } else if (protoType(type).isPresent()) {
+ proto.append(
+ String.format(
+ "%s %s = %d;\n", protoType(type).get(), protoName(f.getName()), fieldNumber));
+ } else {
+ proto.append(
+ "reserved "
+ + fieldNumber
+ + "; // TODO(hiesel): Add support for "
+ + type.getName()
+ + "\n");
+ }
+ fieldNumber++;
+ }
+ proto.append("}");
+
+ System.out.println(proto);
+ }
+
+ private static Optional<String> protoType(Class<?> type) {
+ if (isInt(type)) {
+ return Optional.of("int32");
+ } else if (isLong(type)) {
+ return Optional.of("int64");
+ } else if (isChar(type)) {
+ return Optional.of("string");
+ } else if (isShort(type)) {
+ return Optional.of("int32");
+ } else if (isShort(type)) {
+ return Optional.of("int32");
+ } else if (isBoolean(type)) {
+ return Optional.of("bool");
+ } else if (type.isAssignableFrom(String.class)) {
+ return Optional.of("string");
+ } else if (type.isAssignableFrom(Timestamp.class)) {
+ // See https://gerrit-review.googlesource.com/Documentation/rest-api.html#timestamp
+ return Optional.of("string");
+ } else if (type.getPackageName().startsWith("com.google.gerrit.extensions")) {
+ return Optional.of("gerrit.api." + type.getSimpleName());
+ }
+ return Optional.empty();
+ }
+
+ private static boolean isInt(Class<?> type) {
+ return type.isAssignableFrom(Integer.class) || type.isAssignableFrom(int.class);
+ }
+
+ private static boolean isLong(Class<?> type) {
+ return type.isAssignableFrom(Long.class) || type.isAssignableFrom(long.class);
+ }
+
+ private static boolean isChar(Class<?> type) {
+ return type.isAssignableFrom(Character.class) || type.isAssignableFrom(char.class);
+ }
+
+ private static boolean isShort(Class<?> type) {
+ return type.isAssignableFrom(Short.class) || type.isAssignableFrom(short.class);
+ }
+
+ private static boolean isBoolean(Class<?> type) {
+ return type.isAssignableFrom(Boolean.class) || type.isAssignableFrom(boolean.class);
+ }
+
+ private static String protoName(String name) {
+ return CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, name);
+ }
+}
diff --git a/java/com/google/gerrit/util/cli/BUILD b/java/com/google/gerrit/util/cli/BUILD
index ebcc67e..b464f32 100644
--- a/java/com/google/gerrit/util/cli/BUILD
+++ b/java/com/google/gerrit/util/cli/BUILD
@@ -2,7 +2,10 @@
java_library(
name = "cli",
- srcs = glob(["**/*.java"]),
+ srcs = glob(
+ ["**/*.java"],
+ exclude = ["ApiProtocolBufferGenerator.java"],
+ ),
visibility = ["//visibility:public"],
deps = [
"//java/com/google/gerrit/common:annotations",
@@ -14,3 +17,15 @@
"//lib/guice:guice-assistedinject",
],
)
+
+# Util to generate *.proto files from *Info and *Input objects
+java_binary(
+ name = "protogen",
+ srcs = ["ApiProtocolBufferGenerator.java"],
+ main_class = "com.google.gerrit.util.cli.ApiProtocolBufferGenerator",
+ deps = [
+ "//java/com/google/gerrit/extensions:api",
+ "//lib:guava",
+ "//lib:protobuf",
+ ],
+)
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index 76009bf..215d1e8 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -28,6 +28,7 @@
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.blockLabel;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.blockLabelRemoval;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.labelPermissionKey;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.permissionKey;
import static com.google.gerrit.entities.RefNames.changeMetaRef;
@@ -47,7 +48,6 @@
import static com.google.gerrit.extensions.client.ReviewerState.CC;
import static com.google.gerrit.extensions.client.ReviewerState.REMOVED;
import static com.google.gerrit.extensions.client.ReviewerState.REVIEWER;
-import static com.google.gerrit.git.ObjectIds.abbreviateName;
import static com.google.gerrit.server.StarredChangesUtil.DEFAULT_LABEL;
import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
import static com.google.gerrit.server.group.SystemGroupBackend.CHANGE_OWNER;
@@ -62,7 +62,6 @@
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;
-import static org.eclipse.jgit.lib.Constants.HEAD;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheStats;
@@ -118,9 +117,7 @@
import com.google.gerrit.extensions.api.changes.DraftInput;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.api.changes.NotifyInfo;
-import com.google.gerrit.extensions.api.changes.RebaseInput;
import com.google.gerrit.extensions.api.changes.RecipientType;
-import com.google.gerrit.extensions.api.changes.RelatedChangeAndCommitInfo;
import com.google.gerrit.extensions.api.changes.RevertInput;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.changes.ReviewInput.DraftHandling;
@@ -148,33 +145,27 @@
import com.google.gerrit.extensions.common.ChangeMessageInfo;
import com.google.gerrit.extensions.common.CommentInfo;
import com.google.gerrit.extensions.common.CommitInfo;
-import com.google.gerrit.extensions.common.GitPerson;
import com.google.gerrit.extensions.common.LabelInfo;
import com.google.gerrit.extensions.common.RevisionInfo;
import com.google.gerrit.extensions.common.TrackingIdInfo;
import com.google.gerrit.extensions.events.AttentionSetListener;
-import com.google.gerrit.extensions.events.WorkInProgressStateChangedListener;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
-import com.google.gerrit.extensions.restapi.BinaryResult;
import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.httpd.raw.IndexPreloadingUtil;
import com.google.gerrit.index.IndexConfig;
import com.google.gerrit.index.query.PostFilterPredicate;
import com.google.gerrit.server.ChangeMessagesUtil;
+import com.google.gerrit.server.account.AccountControl;
+import com.google.gerrit.server.change.ChangeJson;
import com.google.gerrit.server.change.ChangeMessages;
import com.google.gerrit.server.change.ChangeResource;
import com.google.gerrit.server.change.testing.TestChangeETagComputation;
-import com.google.gerrit.server.events.CommitReceivedEvent;
import com.google.gerrit.server.git.ChangeMessageModifier;
-import com.google.gerrit.server.git.validators.CommitValidationException;
-import com.google.gerrit.server.git.validators.CommitValidationListener;
-import com.google.gerrit.server.git.validators.CommitValidationMessage;
import com.google.gerrit.server.group.SystemGroupBackend;
import com.google.gerrit.server.index.change.ChangeIndex;
import com.google.gerrit.server.index.change.ChangeIndexCollection;
@@ -196,7 +187,6 @@
import com.google.inject.AbstractModule;
import com.google.inject.Inject;
import com.google.inject.name.Named;
-import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.sql.Timestamp;
import java.text.MessageFormat;
@@ -218,7 +208,6 @@
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.RefUpdate.Result;
@@ -241,6 +230,7 @@
@Inject private RequestScopeOperations requestScopeOperations;
@Inject private ExtensionRegistry extensionRegistry;
@Inject private IndexOperations.Change changeIndexOperations;
+ @Inject private AccountControl.Factory accountControlFactory;
@Inject
@Named("diff_intraline")
@@ -779,366 +769,6 @@
assertThat(thrown).hasMessageThat().contains("Multiple changes found for " + changeId);
}
- @FunctionalInterface
- private interface Rebase {
- void call(String id) throws RestApiException;
- }
-
- @Test
- public void rebaseViaRevisionApi() throws Exception {
- testRebase(id -> gApi.changes().id(id).current().rebase());
- }
-
- @Test
- public void rebaseViaChangeApi() throws Exception {
- testRebase(id -> gApi.changes().id(id).rebase());
- }
-
- private void testRebase(Rebase rebase) throws Exception {
- // Create two changes both with the same parent
- PushOneCommit.Result r = createChange();
- testRepo.reset("HEAD~1");
- PushOneCommit.Result r2 = createChange();
-
- // Approve and submit the first change
- RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
- revision.review(ReviewInput.approve());
- revision.submit();
-
- // Add an approval whose score should be copied on trivial rebase
- gApi.changes().id(r2.getChangeId()).current().review(ReviewInput.recommend());
-
- String changeId = r2.getChangeId();
- // Rebase the second change
- rebase.call(changeId);
-
- // Second change should have 2 patch sets and an approval
- ChangeInfo c2 = gApi.changes().id(changeId).get(CURRENT_REVISION, DETAILED_LABELS);
- assertThat(c2.revisions.get(c2.currentRevision)._number).isEqualTo(2);
-
- // ...and the committer and description should be correct
- ChangeInfo info = gApi.changes().id(changeId).get(CURRENT_REVISION, CURRENT_COMMIT);
- GitPerson committer = info.revisions.get(info.currentRevision).commit.committer;
- assertThat(committer.name).isEqualTo(admin.fullName());
- assertThat(committer.email).isEqualTo(admin.email());
- String description = info.revisions.get(info.currentRevision).description;
- assertThat(description).isEqualTo("Rebase");
-
- // ...and the approval was copied
- LabelInfo cr = c2.labels.get(LabelId.CODE_REVIEW);
- assertThat(cr).isNotNull();
- assertThat(cr.all).hasSize(1);
- assertThat(cr.all.get(0).value).isEqualTo(1);
-
- // Rebasing the second change again should fail
- ResourceConflictException thrown =
- assertThrows(
- ResourceConflictException.class, () -> gApi.changes().id(changeId).current().rebase());
- assertThat(thrown).hasMessageThat().contains("Change is already up to date");
- }
-
- @Test
- public void rebaseAsUploaderInAttentionSet() throws Exception {
- // Create two changes both with the same parent
- PushOneCommit.Result r = createChange();
- testRepo.reset("HEAD~1");
- PushOneCommit.Result r2 = createChange();
-
- // Approve and submit the first change
- RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
- revision.review(ReviewInput.approve());
- revision.submit();
-
- TestAccount admin2 = accountCreator.admin2();
- requestScopeOperations.setApiUser(admin2.id());
- amendChangeWithUploader(r2, project, admin2);
- gApi.changes()
- .id(r2.getChangeId())
- .addToAttentionSet(new AttentionSetInput(admin2.id().toString(), "manual update"));
-
- gApi.changes().id(r2.getChangeId()).rebase();
- }
-
- @Test
- public void rebaseOnChangeNumber() throws Exception {
- String branchTip = testRepo.getRepository().exactRef("HEAD").getObjectId().name();
- PushOneCommit.Result r1 = createChange();
- testRepo.reset("HEAD~1");
- PushOneCommit.Result r2 = createChange();
-
- ChangeInfo ci2 = get(r2.getChangeId(), CURRENT_REVISION, CURRENT_COMMIT);
- RevisionInfo ri2 = ci2.revisions.get(ci2.currentRevision);
- assertThat(ri2.commit.parents.get(0).commit).isEqualTo(branchTip);
-
- Change.Id id1 = r1.getChange().getId();
- RebaseInput in = new RebaseInput();
- in.base = id1.toString();
- gApi.changes().id(r2.getChangeId()).rebase(in);
-
- Change.Id id2 = r2.getChange().getId();
- ci2 = get(r2.getChangeId(), CURRENT_REVISION, CURRENT_COMMIT);
- ri2 = ci2.revisions.get(ci2.currentRevision);
- assertThat(ri2.commit.parents.get(0).commit).isEqualTo(r1.getCommit().name());
-
- List<RelatedChangeAndCommitInfo> related =
- gApi.changes().id(id2.get()).revision(ri2._number).related().changes;
- assertThat(related).hasSize(2);
- assertThat(related.get(0)._changeNumber).isEqualTo(id2.get());
- assertThat(related.get(0)._revisionNumber).isEqualTo(2);
- assertThat(related.get(1)._changeNumber).isEqualTo(id1.get());
- assertThat(related.get(1)._revisionNumber).isEqualTo(1);
- }
-
- @Test
- public void rebaseOnClosedChange() throws Exception {
- String branchTip = testRepo.getRepository().exactRef("HEAD").getObjectId().name();
- PushOneCommit.Result r1 = createChange();
- testRepo.reset("HEAD~1");
- PushOneCommit.Result r2 = createChange();
-
- ChangeInfo ci2 = get(r2.getChangeId(), CURRENT_REVISION, CURRENT_COMMIT);
- RevisionInfo ri2 = ci2.revisions.get(ci2.currentRevision);
- assertThat(ri2.commit.parents.get(0).commit).isEqualTo(branchTip);
-
- // Submit first change.
- Change.Id id1 = r1.getChange().getId();
- gApi.changes().id(id1.get()).current().review(ReviewInput.approve());
- gApi.changes().id(id1.get()).current().submit();
-
- // Rebase second change on first change.
- RebaseInput in = new RebaseInput();
- in.base = id1.toString();
- gApi.changes().id(r2.getChangeId()).rebase(in);
-
- Change.Id id2 = r2.getChange().getId();
- ci2 = get(r2.getChangeId(), CURRENT_REVISION, CURRENT_COMMIT);
- ri2 = ci2.revisions.get(ci2.currentRevision);
- assertThat(ri2.commit.parents.get(0).commit).isEqualTo(r1.getCommit().name());
-
- assertThat(gApi.changes().id(id2.get()).revision(ri2._number).related().changes).isEmpty();
- }
-
- @Test
- public void rebaseOnNonExistingChange() throws Exception {
- String changeId = createChange().getChangeId();
- RebaseInput in = new RebaseInput();
- in.base = "999999";
- UnprocessableEntityException exception =
- assertThrows(
- UnprocessableEntityException.class, () -> gApi.changes().id(changeId).rebase(in));
- assertThat(exception).hasMessageThat().isEqualTo("Base change not found: " + in.base);
- }
-
- @Test
- public void rebaseFromRelationChainToClosedChange() throws Exception {
- PushOneCommit.Result r1 = createChange();
- testRepo.reset("HEAD~1");
-
- createChange();
- PushOneCommit.Result r3 = createChange();
-
- // Submit first change.
- Change.Id id1 = r1.getChange().getId();
- gApi.changes().id(id1.get()).current().review(ReviewInput.approve());
- gApi.changes().id(id1.get()).current().submit();
-
- // Rebase third change on first change.
- RebaseInput in = new RebaseInput();
- in.base = id1.toString();
- gApi.changes().id(r3.getChangeId()).rebase(in);
-
- Change.Id id3 = r3.getChange().getId();
- ChangeInfo ci3 = get(r3.getChangeId(), CURRENT_REVISION, CURRENT_COMMIT);
- RevisionInfo ri3 = ci3.revisions.get(ci3.currentRevision);
- assertThat(ri3.commit.parents.get(0).commit).isEqualTo(r1.getCommit().name());
-
- assertThat(gApi.changes().id(id3.get()).revision(ri3._number).related().changes).isEmpty();
- }
-
- @Test
- public void rebaseNotAllowedWithoutPermission() throws Exception {
- // Create two changes both with the same parent
- PushOneCommit.Result r = createChange();
- testRepo.reset("HEAD~1");
- PushOneCommit.Result r2 = createChange();
-
- // Approve and submit the first change
- RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
- revision.review(ReviewInput.approve());
- revision.submit();
-
- // Rebase the second
- String changeId = r2.getChangeId();
- requestScopeOperations.setApiUser(user.id());
- AuthException thrown =
- assertThrows(AuthException.class, () -> gApi.changes().id(changeId).rebase());
- assertThat(thrown).hasMessageThat().contains("rebase not permitted");
- }
-
- @Test
- public void rebaseAllowedWithPermission() throws Exception {
- // Create two changes both with the same parent
- PushOneCommit.Result r = createChange();
- testRepo.reset("HEAD~1");
- PushOneCommit.Result r2 = createChange();
-
- // Approve and submit the first change
- RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
- revision.review(ReviewInput.approve());
- revision.submit();
-
- projectOperations
- .project(project)
- .forUpdate()
- .add(allow(Permission.REBASE).ref("refs/heads/master").group(REGISTERED_USERS))
- .update();
-
- // Rebase the second
- String changeId = r2.getChangeId();
- requestScopeOperations.setApiUser(user.id());
- gApi.changes().id(changeId).rebase();
- }
-
- @Test
- public void rebaseNotAllowedWithoutPushPermission() throws Exception {
- // Create two changes both with the same parent
- PushOneCommit.Result r = createChange();
- testRepo.reset("HEAD~1");
- PushOneCommit.Result r2 = createChange();
-
- // Approve and submit the first change
- RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
- revision.review(ReviewInput.approve());
- revision.submit();
-
- projectOperations
- .project(project)
- .forUpdate()
- .add(allow(Permission.REBASE).ref("refs/heads/master").group(REGISTERED_USERS))
- .add(block(Permission.PUSH).ref("refs/for/*").group(REGISTERED_USERS))
- .update();
-
- // Rebase the second
- String changeId = r2.getChangeId();
- requestScopeOperations.setApiUser(user.id());
- AuthException thrown =
- assertThrows(AuthException.class, () -> gApi.changes().id(changeId).rebase());
- assertThat(thrown).hasMessageThat().contains("rebase not permitted");
- }
-
- @Test
- public void rebaseNotAllowedForOwnerWithoutPushPermission() throws Exception {
- // Create two changes both with the same parent
- PushOneCommit.Result r = createChange();
- testRepo.reset("HEAD~1");
- PushOneCommit.Result r2 = createChange();
-
- // Approve and submit the first change
- RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
- revision.review(ReviewInput.approve());
- revision.submit();
-
- projectOperations
- .project(project)
- .forUpdate()
- .add(block(Permission.PUSH).ref("refs/for/*").group(REGISTERED_USERS))
- .update();
-
- // Rebase the second
- String changeId = r2.getChangeId();
- AuthException thrown =
- assertThrows(AuthException.class, () -> gApi.changes().id(changeId).rebase());
- assertThat(thrown).hasMessageThat().contains("rebase not permitted");
- }
-
- @Test
- public void rebaseWithValidationOptions() throws Exception {
- // Create two changes both with the same parent
- PushOneCommit.Result r = createChange();
- testRepo.reset("HEAD~1");
- PushOneCommit.Result r2 = createChange();
-
- // Approve and submit the first change
- RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
- revision.review(ReviewInput.approve());
- revision.submit();
-
- RebaseInput rebaseInput = new RebaseInput();
- rebaseInput.validationOptions = ImmutableMap.of("key", "value");
-
- TestCommitValidationListener testCommitValidationListener = new TestCommitValidationListener();
- try (Registration registration =
- extensionRegistry.newRegistration().add(testCommitValidationListener)) {
- // Rebase the second change
- gApi.changes().id(r2.getChangeId()).current().rebase(rebaseInput);
- assertThat(testCommitValidationListener.receiveEvent.pushOptions)
- .containsExactly("key", "value");
- }
- }
-
- @Test
- public void rebaseOutdatedPatchSet() throws Exception {
- String fileName1 = "a.txt";
- String fileContent1 = "some content";
- String fileName2 = "b.txt";
- String fileContent2Ps1 = "foo";
- String fileContent2Ps2 = "foo/bar";
-
- // Create two changes both with the same parent touching disjunct files
- PushOneCommit.Result r =
- pushFactory
- .create(admin.newIdent(), testRepo, PushOneCommit.SUBJECT, fileName1, fileContent1)
- .to("refs/for/master");
- r.assertOkStatus();
- String changeId1 = r.getChangeId();
- testRepo.reset("HEAD~1");
- PushOneCommit push =
- pushFactory.create(
- admin.newIdent(), testRepo, PushOneCommit.SUBJECT, fileName2, fileContent2Ps1);
- PushOneCommit.Result r2 = push.to("refs/for/master");
- r2.assertOkStatus();
- String changeId2 = r2.getChangeId();
-
- // Approve and submit the first change
- RevisionApi revision = gApi.changes().id(changeId1).current();
- revision.review(ReviewInput.approve());
- revision.submit();
-
- // Amend the second change so that it has 2 patch sets
- amendChange(
- changeId2,
- "refs/for/master",
- admin,
- testRepo,
- PushOneCommit.SUBJECT,
- fileName2,
- fileContent2Ps2)
- .assertOkStatus();
- ChangeInfo changeInfo2 = gApi.changes().id(changeId2).get();
- assertThat(changeInfo2.revisions.get(changeInfo2.currentRevision)._number).isEqualTo(2);
-
- // Rebase the first patch set of the second change
- gApi.changes().id(changeId2).revision(1).rebase();
-
- // Second change should have 3 patch sets
- changeInfo2 = gApi.changes().id(changeId2).get();
- assertThat(changeInfo2.revisions.get(changeInfo2.currentRevision)._number).isEqualTo(3);
-
- // ... and the committer and description should be correct
- ChangeInfo info = gApi.changes().id(changeId2).get(CURRENT_REVISION, CURRENT_COMMIT);
- GitPerson committer = info.revisions.get(info.currentRevision).commit.committer;
- assertThat(committer.name).isEqualTo(admin.fullName());
- assertThat(committer.email).isEqualTo(admin.email());
- String description = info.revisions.get(info.currentRevision).description;
- assertThat(description).isEqualTo("Rebase");
-
- // ... and the file contents should match with patch set 1 based on change1
- assertThat(gApi.changes().id(changeId2).current().file(fileName1).content().asString())
- .isEqualTo(fileContent1);
- assertThat(gApi.changes().id(changeId2).current().file(fileName2).content().asString())
- .isEqualTo(fileContent2Ps1);
- }
-
@Test
public void deleteNewChangeAsAdmin() throws Exception {
deleteChangeAsUser(admin, admin);
@@ -1471,166 +1101,6 @@
}
@Test
- public void rebaseUpToDateChange() throws Exception {
- PushOneCommit.Result r = createChange();
- ResourceConflictException thrown =
- assertThrows(
- ResourceConflictException.class,
- () -> gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).rebase());
- assertThat(thrown).hasMessageThat().contains("Change is already up to date");
- }
-
- @Test
- public void rebaseConflict() throws Exception {
- PushOneCommit.Result r1 = createChange();
- gApi.changes()
- .id(r1.getChangeId())
- .revision(r1.getCommit().name())
- .review(ReviewInput.approve());
- gApi.changes().id(r1.getChangeId()).revision(r1.getCommit().name()).submit();
-
- PushOneCommit push =
- pushFactory.create(
- admin.newIdent(),
- testRepo,
- PushOneCommit.SUBJECT,
- PushOneCommit.FILE_NAME,
- "other content",
- "If09d8782c1e59dd0b33de2b1ec3595d69cc10ad5");
- PushOneCommit.Result r2 = push.to("refs/for/master");
- r2.assertOkStatus();
- ResourceConflictException exception =
- assertThrows(
- ResourceConflictException.class,
- () -> gApi.changes().id(r2.getChangeId()).revision(r2.getCommit().name()).rebase());
- assertThat(exception)
- .hasMessageThat()
- .isEqualTo(
- String.format(
- "The change could not be rebased due to a conflict during merge.\n\n"
- + "merge conflict(s):\n%s",
- PushOneCommit.FILE_NAME));
- }
-
- @Test
- public void rebaseDoesNotAddWorkInProgress() throws Exception {
- PushOneCommit.Result r = createChange();
-
- // create an unrelated change so that we can rebase
- testRepo.reset("HEAD~1");
- PushOneCommit.Result unrelated = createChange();
- gApi.changes().id(unrelated.getChangeId()).current().review(ReviewInput.approve());
- gApi.changes().id(unrelated.getChangeId()).current().submit();
-
- gApi.changes().id(r.getChangeId()).rebase();
-
- // change is still ready for review after rebase
- assertThat(gApi.changes().id(r.getChangeId()).get().workInProgress).isNull();
- }
-
- @Test
- public void rebaseDoesNotRemoveWorkInProgress() throws Exception {
- PushOneCommit.Result r = createChange();
- change(r).setWorkInProgress();
-
- // create an unrelated change so that we can rebase
- testRepo.reset("HEAD~1");
- PushOneCommit.Result unrelated = createChange();
- gApi.changes().id(unrelated.getChangeId()).current().review(ReviewInput.approve());
- gApi.changes().id(unrelated.getChangeId()).current().submit();
-
- gApi.changes().id(r.getChangeId()).rebase();
-
- // change is still work in progress after rebase
- assertThat(gApi.changes().id(r.getChangeId()).get().workInProgress).isTrue();
- }
-
- @Test
- public void rebaseConflict_conflictsAllowed() throws Exception {
- String patchSetSubject = "patch set change";
- String patchSetContent = "patch set content";
- String baseSubject = "base change";
- String baseContent = "base content";
-
- PushOneCommit.Result r1 = createChange(baseSubject, PushOneCommit.FILE_NAME, baseContent);
- gApi.changes()
- .id(r1.getChangeId())
- .revision(r1.getCommit().name())
- .review(ReviewInput.approve());
- gApi.changes().id(r1.getChangeId()).revision(r1.getCommit().name()).submit();
-
- testRepo.reset("HEAD~1");
- PushOneCommit push =
- pushFactory.create(
- admin.newIdent(), testRepo, patchSetSubject, PushOneCommit.FILE_NAME, patchSetContent);
- PushOneCommit.Result r2 = push.to("refs/for/master");
- r2.assertOkStatus();
-
- String changeId = r2.getChangeId();
- RevCommit patchSet = r2.getCommit();
- RevCommit base = r1.getCommit();
-
- TestWorkInProgressStateChangedListener wipStateChangedListener =
- new TestWorkInProgressStateChangedListener();
- try (Registration registration =
- extensionRegistry.newRegistration().add(wipStateChangedListener)) {
- RebaseInput rebaseInput = new RebaseInput();
- rebaseInput.allowConflicts = true;
- ChangeInfo changeInfo =
- gApi.changes().id(changeId).revision(patchSet.name()).rebaseAsInfo(rebaseInput);
- assertThat(changeInfo.containsGitConflicts).isTrue();
- assertThat(changeInfo.workInProgress).isTrue();
- }
- assertThat(wipStateChangedListener.invoked).isTrue();
- assertThat(wipStateChangedListener.wip).isTrue();
-
- // To get the revisions, we must retrieve the change with more change options.
- ChangeInfo changeInfo =
- gApi.changes().id(changeId).get(ALL_REVISIONS, CURRENT_COMMIT, CURRENT_REVISION);
- assertThat(changeInfo.revisions).hasSize(2);
- assertThat(changeInfo.revisions.get(changeInfo.currentRevision).commit.parents.get(0).commit)
- .isEqualTo(base.name());
-
- // Verify that the file content in the created patch set is correct.
- // We expect that it has conflict markers to indicate the conflict.
- BinaryResult bin =
- gApi.changes().id(changeId).current().file(PushOneCommit.FILE_NAME).content();
- ByteArrayOutputStream os = new ByteArrayOutputStream();
- bin.writeTo(os);
- String fileContent = new String(os.toByteArray(), UTF_8);
- String patchSetSha1 = abbreviateName(patchSet, 6);
- String baseSha1 = abbreviateName(base, 6);
- assertThat(fileContent)
- .isEqualTo(
- "<<<<<<< PATCH SET ("
- + patchSetSha1
- + " "
- + patchSetSubject
- + ")\n"
- + patchSetContent
- + "\n"
- + "=======\n"
- + baseContent
- + "\n"
- + ">>>>>>> BASE ("
- + baseSha1
- + " "
- + baseSubject
- + ")\n");
-
- // Verify the message that has been posted on the change.
- List<ChangeMessageInfo> messages = gApi.changes().id(changeId).messages();
- assertThat(messages).hasSize(2);
- assertThat(Iterables.getLast(messages).message)
- .isEqualTo(
- "Patch Set 2: Patch Set 1 was rebased\n\n"
- + "The following files contain Git conflicts:\n"
- + "* "
- + PushOneCommit.FILE_NAME
- + "\n");
- }
-
- @Test
public void attentionSetListener_firesOnChange() throws Exception {
PushOneCommit.Result r1 = createChange();
AttentionSetInput addUser = new AttentionSetInput(user.email(), "some reason");
@@ -1663,156 +1133,6 @@
}
@Test
- public void rebaseChangeBase() throws Exception {
- PushOneCommit.Result r1 = createChange();
- PushOneCommit.Result r2 = createChange();
- PushOneCommit.Result r3 = createChange();
- RebaseInput ri = new RebaseInput();
-
- // rebase r3 directly onto master (break dep. towards r2)
- ri.base = "";
- gApi.changes().id(r3.getChangeId()).revision(r3.getCommit().name()).rebase(ri);
- PatchSet ps3 = r3.getPatchSet();
- assertThat(ps3.id().get()).isEqualTo(2);
-
- // rebase r2 onto r3 (referenced by ref)
- ri.base = ps3.id().toRefName();
- gApi.changes().id(r2.getChangeId()).revision(r2.getCommit().name()).rebase(ri);
- PatchSet ps2 = r2.getPatchSet();
- assertThat(ps2.id().get()).isEqualTo(2);
-
- // rebase r1 onto r2 (referenced by commit)
- ri.base = ps2.commitId().name();
- gApi.changes().id(r1.getChangeId()).revision(r1.getCommit().name()).rebase(ri);
- PatchSet ps1 = r1.getPatchSet();
- assertThat(ps1.id().get()).isEqualTo(2);
-
- // rebase r1 onto r3 (referenced by change number)
- ri.base = String.valueOf(r3.getChange().getId().get());
- gApi.changes().id(r1.getChangeId()).revision(ps1.commitId().name()).rebase(ri);
- assertThat(r1.getPatchSetId().get()).isEqualTo(3);
- }
-
- @Test
- public void rebaseChangeBaseRecursion() throws Exception {
- PushOneCommit.Result r1 = createChange();
- PushOneCommit.Result r2 = createChange();
-
- RebaseInput ri = new RebaseInput();
- ri.base = r2.getCommit().name();
- String expectedMessage =
- "base change "
- + r2.getChangeId()
- + " is a descendant of the current change - recursion not allowed";
- ResourceConflictException thrown =
- assertThrows(
- ResourceConflictException.class,
- () -> gApi.changes().id(r1.getChangeId()).revision(r1.getCommit().name()).rebase(ri));
- assertThat(thrown).hasMessageThat().contains(expectedMessage);
- }
-
- @Test
- public void rebaseAbandonedChange() throws Exception {
- PushOneCommit.Result r = createChange();
- String changeId = r.getChangeId();
- assertThat(info(changeId).status).isEqualTo(ChangeStatus.NEW);
- gApi.changes().id(changeId).abandon();
- ChangeInfo info = info(changeId);
- assertThat(info.status).isEqualTo(ChangeStatus.ABANDONED);
-
- ResourceConflictException thrown =
- assertThrows(
- ResourceConflictException.class,
- () -> gApi.changes().id(changeId).revision(r.getCommit().name()).rebase());
- assertThat(thrown).hasMessageThat().contains("change is abandoned");
- }
-
- @Test
- public void rebaseOntoAbandonedChange() throws Exception {
- // Create two changes both with the same parent
- PushOneCommit.Result r = createChange();
- testRepo.reset("HEAD~1");
- PushOneCommit.Result r2 = createChange();
-
- // Abandon the first change
- String changeId = r.getChangeId();
- assertThat(info(changeId).status).isEqualTo(ChangeStatus.NEW);
- gApi.changes().id(changeId).abandon();
- ChangeInfo info = info(changeId);
- assertThat(info.status).isEqualTo(ChangeStatus.ABANDONED);
-
- RebaseInput ri = new RebaseInput();
- ri.base = r.getCommit().name();
-
- ResourceConflictException thrown =
- assertThrows(
- ResourceConflictException.class,
- () -> gApi.changes().id(r2.getChangeId()).revision(r2.getCommit().name()).rebase(ri));
- assertThat(thrown).hasMessageThat().contains("base change is abandoned: " + changeId);
- }
-
- @Test
- public void rebaseOntoSelf() throws Exception {
- PushOneCommit.Result r = createChange();
- String changeId = r.getChangeId();
- String commit = r.getCommit().name();
- RebaseInput ri = new RebaseInput();
- ri.base = commit;
- ResourceConflictException thrown =
- assertThrows(
- ResourceConflictException.class,
- () -> gApi.changes().id(changeId).revision(commit).rebase(ri));
- assertThat(thrown).hasMessageThat().contains("cannot rebase change onto itself");
- }
-
- @Test
- public void cannotRebaseOntoBaseThatIsNotPresentInTargetBranch() throws Exception {
- ObjectId initial = repo().exactRef(HEAD).getLeaf().getObjectId();
-
- BranchInput branchInput = new BranchInput();
- branchInput.revision = initial.getName();
- gApi.projects().name(project.get()).branch("foo").create(branchInput);
-
- PushOneCommit.Result r1 =
- pushFactory
- .create(admin.newIdent(), testRepo, "Change on foo branch", "a.txt", "a-content")
- .to("refs/for/foo");
- approve(r1.getChangeId());
- gApi.changes().id(r1.getChangeId()).current().submit();
-
- // reset HEAD in order to create a sibling of the first change
- testRepo.reset(initial);
-
- PushOneCommit.Result r2 =
- pushFactory
- .create(admin.newIdent(), testRepo, "Change on master branch", "b.txt", "b-content")
- .to("refs/for/master");
-
- RebaseInput rebaseInput = new RebaseInput();
- rebaseInput.base = r1.getCommit().getName();
- ResourceConflictException thrown =
- assertThrows(
- ResourceConflictException.class,
- () -> gApi.changes().id(r2.getChangeId()).current().rebase(rebaseInput));
- assertThat(thrown)
- .hasMessageThat()
- .contains(
- String.format(
- "base change is targeting wrong branch: %s,refs/heads/foo", project.get()));
-
- rebaseInput.base = "refs/heads/foo";
- thrown =
- assertThrows(
- ResourceConflictException.class,
- () -> gApi.changes().id(r2.getChangeId()).current().rebase(rebaseInput));
- assertThat(thrown)
- .hasMessageThat()
- .contains(
- String.format(
- "base revision is missing from the destination branch: %s", rebaseInput.base));
- }
-
- @Test
@TestProjectInput(createEmptyCommit = false)
public void changeNoParentToOneParent() throws Exception {
// create initial commit with no parent and push it as change, so that patch
@@ -2805,6 +2125,78 @@
}
@Test
+ @GerritConfig(name = "accounts.visibility", value = "SAME_GROUP")
+ public void removeNonVisibleReviewer() throws Exception {
+ // allow all users to remove reviewers
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(allow(Permission.REMOVE_REVIEWER).ref("refs/*").group(REGISTERED_USERS))
+ .update();
+
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+ gApi.changes().id(changeId).addReviewer(user.email());
+ AccountInfo reviewerInfo =
+ Iterables.getOnlyElement(
+ gApi.changes().id(changeId).get().reviewers.get(ReviewerState.REVIEWER));
+ assertThat(reviewerInfo._accountId).isEqualTo(user.id().get());
+
+ TestAccount user2 = accountCreator.user2();
+ requestScopeOperations.setApiUser(user2.id());
+
+ // user2 cannot see user
+ assertThat(
+ accountControlFactory.get(identifiedUserFactory.create(user.id())).canSee(user2.id()))
+ .isFalse();
+
+ gApi.changes().id(changeId).reviewer(user.id().toString()).remove(new DeleteReviewerInput());
+ assertThat(gApi.changes().id(changeId).get().reviewers).isEmpty();
+ }
+
+ @Test
+ @GerritConfig(name = "accounts.visibility", value = "SAME_GROUP")
+ public void removeNonVisibleReviewerThroughPostReview() throws Exception {
+ // allow all users to remove reviewers
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(allow(Permission.REMOVE_REVIEWER).ref("refs/*").group(REGISTERED_USERS))
+ .update();
+
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+ gApi.changes().id(changeId).addReviewer(user.email());
+ AccountInfo reviewerInfo =
+ Iterables.getOnlyElement(
+ gApi.changes().id(changeId).get().reviewers.get(ReviewerState.REVIEWER));
+ assertThat(reviewerInfo._accountId).isEqualTo(user.id().get());
+
+ TestAccount user2 = accountCreator.user2();
+ requestScopeOperations.setApiUser(user2.id());
+
+ // user2 cannot see user
+ assertThat(
+ accountControlFactory.get(identifiedUserFactory.create(user.id())).canSee(user2.id()))
+ .isFalse();
+
+ ReviewerInput reviewerInput = new ReviewerInput();
+ reviewerInput.reviewer = user.email();
+ reviewerInput.state = ReviewerState.REMOVED;
+ ReviewInput reviewInput = new ReviewInput();
+ reviewInput.reviewers = ImmutableList.of(reviewerInput);
+ ReviewResult reviewResult = gApi.changes().id(changeId).current().review(reviewInput);
+ assertThat(reviewResult.error).isNull();
+
+ // user is removed as a reviewer, user2 is added as a CC by doing the post review request that
+ // removed user as a reviewer
+ assertThat(gApi.changes().id(changeId).get().reviewers.get(ReviewerState.REVIEWER)).isNull();
+ reviewerInfo =
+ Iterables.getOnlyElement(gApi.changes().id(changeId).get().reviewers.get(ReviewerState.CC));
+ assertThat(reviewerInfo._accountId).isEqualTo(user2.id().get());
+ }
+
+ @Test
public void removeReviewerNotPermitted() throws Exception {
PushOneCommit.Result r = createChange();
String changeId = r.getChangeId();
@@ -3016,7 +2408,68 @@
.id(r.getChangeId())
.reviewer(admin.id().toString())
.deleteVote(LabelId.CODE_REVIEW));
- assertThat(thrown).hasMessageThat().contains("delete vote not permitted");
+ assertThat(thrown).hasMessageThat().contains("Delete vote not permitted");
+ }
+
+ @Test
+ public void deleteVoteAlwaysPermittedForSelfVotes() throws Exception {
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(
+ allowLabel(LabelId.CODE_REVIEW)
+ .ref("refs/heads/*")
+ .group(REGISTERED_USERS)
+ .range(-2, 2))
+ .add(
+ blockLabelRemoval(LabelId.CODE_REVIEW)
+ .ref("refs/heads/*")
+ .group(REGISTERED_USERS)
+ .range(-2, 2))
+ .add(block(Permission.REMOVE_REVIEWER).ref("refs/heads/*").group(REGISTERED_USERS))
+ .update();
+
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+
+ requestScopeOperations.setApiUser(user.id());
+ gApi.changes().id(changeId).revision(r.getCommit().name()).review(ReviewInput.approve());
+
+ gApi.changes()
+ .id(r.getChangeId())
+ .reviewer(user.id().toString())
+ .deleteVote(LabelId.CODE_REVIEW);
+ }
+
+ @Test
+ public void deleteVoteAlwaysPermittedForAdmin() throws Exception {
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(
+ allowLabel(LabelId.CODE_REVIEW)
+ .ref("refs/heads/*")
+ .group(REGISTERED_USERS)
+ .range(-2, 2))
+ .add(
+ blockLabelRemoval(LabelId.CODE_REVIEW)
+ .ref("refs/heads/*")
+ .group(REGISTERED_USERS)
+ .range(-2, 2))
+ .add(block(Permission.REMOVE_REVIEWER).ref("refs/heads/*").group(REGISTERED_USERS))
+ .update();
+
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+
+ requestScopeOperations.setApiUser(user.id());
+ gApi.changes().id(changeId).revision(r.getCommit().name()).review(ReviewInput.approve());
+
+ requestScopeOperations.setApiUser(admin.id());
+ gApi.changes()
+ .id(r.getChangeId())
+ .reviewer(user.id().toString())
+ .deleteVote(LabelId.CODE_REVIEW);
}
@Test
@@ -3312,6 +2765,40 @@
}
@Test
+ public void queryChangesDefaultFieldMatchesOwner() throws Exception {
+ // We have to create a new user since changes are not deleted between tests, which means
+ // querying the standard users will lead to dirty results.
+ TestAccount changeOwner = accountCreator.createValid("changeOwner");
+ requestScopeOperations.setApiUser(changeOwner.id());
+ // Creating a change through the API since PushOneCommit changes are always owned by admin().
+ ChangeInput in = new ChangeInput();
+ in.branch = Constants.MASTER;
+ in.subject = "subject";
+ in.project = project.get();
+ ChangeInfo info = gApi.changes().createAsInfo(in);
+ assertThat(info.owner._accountId).isEqualTo(changeOwner.id().get());
+ requestScopeOperations.setApiUser(user.id());
+ List<ChangeInfo> results = query(changeOwner.email());
+ assertThat(Iterables.getOnlyElement(results).changeId).isEqualTo(info.changeId);
+ }
+
+ @Test
+ public void queryChangesDefaultFieldMatchesReviewer() throws Exception {
+ requestScopeOperations.setApiUser(admin.id());
+ PushOneCommit.Result r =
+ pushFactory
+ .create(admin.newIdent(), testRepo, "subject", "a.txt", "a1")
+ .to("refs/for/master");
+ // We have to create a new user since changes are not deleted between tests, which means
+ // querying the standard users will lead to dirty results.
+ TestAccount changeReviewer = accountCreator.createValid("changeReviewer");
+ gApi.changes().id(r.getChangeId()).addReviewer(changeReviewer.email());
+ requestScopeOperations.setApiUser(user.id());
+ List<ChangeInfo> results = query(changeReviewer.email());
+ assertThat(Iterables.getOnlyElement(results).changeId).isEqualTo(r.getChangeId());
+ }
+
+ @Test
public void checkReviewedFlagBeforeAndAfterReview() throws Exception {
PushOneCommit.Result r = createChange();
ReviewerInput in = new ReviewerInput();
@@ -3572,7 +3059,11 @@
.review(ReviewInput.approve());
gApi.changes().id(r1.getChangeId()).revision(r1.getCommit().name()).submit();
- createChange();
+ PushOneCommit.Result change = createChange();
+ // Populate change with a reasonable set of fields. We can't exhaustively
+ // test all possible variations, but can try to cover a reasonable set.
+ approve(change.getChangeId());
+ gApi.changes().id(change.getChangeId()).addReviewer(user.email());
requestScopeOperations.setApiUser(user.id());
try (AutoCloseable ignored = disableNoteDb()) {
@@ -3587,6 +3078,34 @@
}
@Test
+ public void nonLazyloadQueryOptionsDoNotTouchDatabase() throws Exception {
+ requestScopeOperations.setApiUser(admin.id());
+ PushOneCommit.Result r1 = createChange();
+ gApi.changes()
+ .id(r1.getChangeId())
+ .revision(r1.getCommit().name())
+ .review(ReviewInput.approve());
+ gApi.changes().id(r1.getChangeId()).revision(r1.getCommit().name()).submit();
+
+ PushOneCommit.Result change = createChange();
+ // Populate change with a reasonable set of fields. We can't exhaustively
+ // test all possible variations, but can try to cover a reasonable set.
+ approve(change.getChangeId());
+ gApi.changes().id(change.getChangeId()).addReviewer(user.email());
+
+ requestScopeOperations.setApiUser(user.id());
+ try (AutoCloseable ignored = disableNoteDb()) {
+ assertThat(
+ gApi.changes()
+ .query()
+ .withQuery("project:{" + project.get() + "} (status:open OR status:closed)")
+ .withOptions(EnumSet.complementOf(EnumSet.copyOf(ChangeJson.REQUIRE_LAZY_LOAD)))
+ .get())
+ .hasSize(2);
+ }
+ }
+
+ @Test
public void votable() throws Exception {
PushOneCommit.Result r = createChange();
String triplet = project.get() + "~master~" + r.getChangeId();
@@ -3851,6 +3370,7 @@
assertThat(change.status).isEqualTo(ChangeStatus.NEW);
assertThat(change.labels.keySet()).containsExactly(LabelId.CODE_REVIEW);
assertThat(change.permittedLabels.keySet()).containsExactly(LabelId.CODE_REVIEW);
+ assertThat(change.removableLabels).isEmpty();
// add new label and assert that it's returned for existing changes
AccountGroup.UUID registeredUsers = systemGroupBackend.getGroup(REGISTERED_USERS).getUUID();
@@ -3880,6 +3400,9 @@
.id(r.getChangeId())
.revision(r.getCommit().name())
.review(new ReviewInput().label(verified.getName(), verified.getMax().getValue()));
+ change = gApi.changes().id(r.getChangeId()).get();
+ assertPermitted(change, LabelId.VERIFIED, -1, 0, 1);
+ assertOnlyRemovableLabel(change, LabelId.VERIFIED, "+1", admin);
try (ProjectConfigUpdate u = updateProject(project)) {
// remove label and assert that it's no longer returned for existing
@@ -3899,6 +3422,7 @@
change = gApi.changes().id(r.getChangeId()).get();
assertThat(change.labels.keySet()).containsExactly(LabelId.CODE_REVIEW);
assertThat(change.permittedLabels.keySet()).containsExactly(LabelId.CODE_REVIEW);
+ assertThat(change.removableLabels).isEmpty();
// abandon the change and see that the returned labels stay the same
// while all permitted labels disappear.
@@ -3907,6 +3431,7 @@
assertThat(change.status).isEqualTo(ChangeStatus.ABANDONED);
assertThat(change.labels.keySet()).containsExactly(LabelId.CODE_REVIEW);
assertThat(change.permittedLabels).isEmpty();
+ assertThat(change.removableLabels).isEmpty();
}
@Test
@@ -4023,6 +3548,7 @@
assertThat(change.labels.keySet()).containsExactly(LabelId.CODE_REVIEW, LabelId.VERIFIED);
assertPermitted(change, LabelId.CODE_REVIEW, 2);
assertPermitted(change, LabelId.VERIFIED, 1);
+ assertThat(change.removableLabels).isEmpty();
// remove label and assert that it's no longer returned for existing
// changes, even if there is an approval for it
@@ -4040,6 +3566,7 @@
assertThat(change.labels.keySet()).containsExactly(LabelId.CODE_REVIEW);
assertThat(change.permittedLabels.keySet()).containsExactly(LabelId.CODE_REVIEW);
assertPermitted(change, LabelId.CODE_REVIEW, 2);
+ assertThat(change.removableLabels).isEmpty();
}
@Test
@@ -4136,6 +3663,7 @@
.containsExactly(LabelId.CODE_REVIEW, "Non-Author-Code-Review");
assertThat(change.permittedLabels.keySet()).containsExactly(LabelId.CODE_REVIEW);
assertPermitted(change, LabelId.CODE_REVIEW, 0, 1, 2);
+ assertThat(change.removableLabels).isEmpty();
}
@Test
@@ -4151,6 +3679,7 @@
assertThat(change.submissionId).isNotNull();
assertThat(change.labels.keySet()).containsExactly(LabelId.CODE_REVIEW);
assertPermitted(change, LabelId.CODE_REVIEW, 0, 1, 2);
+ assertThat(change.removableLabels).isEmpty();
}
@Test
@@ -4857,8 +4386,12 @@
ListChangesOption.SKIP_DIFFSTAT);
PushOneCommit.Result change = createChange();
- int number = gApi.changes().id(change.getChangeId()).get()._number;
+ // Populate change with a reasonable set of fields. We can't exhaustively
+ // test all possible variations, but can try to cover a reasonable set.
+ approve(change.getChangeId());
+ gApi.changes().id(change.getChangeId()).addReviewer(user.email());
+ int number = gApi.changes().id(change.getChangeId()).get()._number;
try (AutoCloseable ignored = changeIndexOperations.disableReadsAndWrites()) {
assertThat(gApi.changes().id(project.get(), number).get(options).changeId)
.isEqualTo(change.getChangeId());
@@ -5030,19 +4563,6 @@
void call(String changeId, String reviewer) throws RestApiException;
}
- private static class TestWorkInProgressStateChangedListener
- implements WorkInProgressStateChangedListener {
- boolean invoked;
- Boolean wip;
-
- @Override
- public void onWorkInProgressStateChanged(WorkInProgressStateChangedListener.Event event) {
- this.invoked = true;
- this.wip =
- event.getChange().workInProgress != null ? event.getChange().workInProgress : false;
- }
- }
-
public static class TestAttentionSetListenerModule extends AbstractModule {
@Override
public void configure() {
@@ -5067,15 +4587,4 @@
private void voteLabel(String changeId, String labelName, int score) throws RestApiException {
gApi.changes().id(changeId).current().review(new ReviewInput().label(labelName, score));
}
-
- private static class TestCommitValidationListener implements CommitValidationListener {
- public CommitReceivedEvent receiveEvent;
-
- @Override
- public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
- throws CommitValidationException {
- this.receiveEvent = receiveEvent;
- return ImmutableList.of();
- }
- }
}
diff --git a/javatests/com/google/gerrit/acceptance/api/change/RebaseIT.java b/javatests/com/google/gerrit/acceptance/api/change/RebaseIT.java
new file mode 100644
index 0000000..522013e
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/api/change/RebaseIT.java
@@ -0,0 +1,1142 @@
+// Copyright (C) 2022 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.acceptance.api.change;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
+import static com.google.gerrit.extensions.client.ListChangesOption.ALL_REVISIONS;
+import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_COMMIT;
+import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_REVISION;
+import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS;
+import static com.google.gerrit.git.ObjectIds.abbreviateName;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.eclipse.jgit.lib.Constants.HEAD;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.ExtensionRegistry;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.common.RawInputUtil;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.LabelId;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Permission;
+import com.google.gerrit.extensions.api.changes.AttentionSetInput;
+import com.google.gerrit.extensions.api.changes.RebaseInput;
+import com.google.gerrit.extensions.api.changes.RelatedChangeAndCommitInfo;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.api.changes.RevisionApi;
+import com.google.gerrit.extensions.api.projects.BranchInput;
+import com.google.gerrit.extensions.client.ChangeStatus;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.ChangeMessageInfo;
+import com.google.gerrit.extensions.common.GitPerson;
+import com.google.gerrit.extensions.common.LabelInfo;
+import com.google.gerrit.extensions.common.RebaseChainInfo;
+import com.google.gerrit.extensions.common.RevisionInfo;
+import com.google.gerrit.extensions.events.WorkInProgressStateChangedListener;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BinaryResult;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.server.events.CommitReceivedEvent;
+import com.google.gerrit.server.git.validators.CommitValidationException;
+import com.google.gerrit.server.git.validators.CommitValidationListener;
+import com.google.gerrit.server.git.validators.CommitValidationMessage;
+import com.google.inject.Inject;
+import java.io.ByteArrayOutputStream;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+
+@RunWith(Suite.class)
+@Suite.SuiteClasses({
+ RebaseIT.RebaseViaRevisionApi.class, //
+ RebaseIT.RebaseViaChangeApi.class, //
+ RebaseIT.RebaseChain.class, //
+})
+public class RebaseIT {
+ public abstract static class Base extends AbstractDaemonTest {
+ @Inject protected RequestScopeOperations requestScopeOperations;
+ @Inject protected ProjectOperations projectOperations;
+ @Inject protected ExtensionRegistry extensionRegistry;
+
+ @FunctionalInterface
+ protected interface RebaseCall {
+ void call(String id) throws RestApiException;
+ }
+
+ @FunctionalInterface
+ protected interface RebaseCallWithInput {
+ void call(String id, RebaseInput in) throws RestApiException;
+ }
+
+ protected RebaseCall rebaseCall;
+ protected RebaseCallWithInput rebaseCallWithInput;
+
+ protected void init(RebaseCall call, RebaseCallWithInput callWithInput) {
+ this.rebaseCall = call;
+ this.rebaseCallWithInput = callWithInput;
+ }
+
+ @Test
+ public void rebaseChange() throws Exception {
+ // Create two changes both with the same parent
+ PushOneCommit.Result r = createChange();
+ testRepo.reset("HEAD~1");
+ PushOneCommit.Result r2 = createChange();
+
+ // Approve and submit the first change
+ RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
+ revision.review(ReviewInput.approve());
+ revision.submit();
+
+ // Add an approval whose score should be copied on trivial rebase
+ gApi.changes().id(r2.getChangeId()).current().review(ReviewInput.recommend());
+
+ // Rebase the second change
+ rebaseCall.call(r2.getChangeId());
+
+ verifyRebaseForChange(r2.getChange().getId(), r.getCommit().name(), true, 2);
+
+ // Rebasing the second change again should fail
+ verifyChangeIsUpToDate(r2);
+ }
+
+ @Test
+ public void rebaseAbandonedChange() throws Exception {
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+ assertThat(info(changeId).status).isEqualTo(ChangeStatus.NEW);
+ gApi.changes().id(changeId).abandon();
+ ChangeInfo info = info(changeId);
+ assertThat(info.status).isEqualTo(ChangeStatus.ABANDONED);
+
+ ResourceConflictException thrown =
+ assertThrows(ResourceConflictException.class, () -> rebaseCall.call(changeId));
+ assertThat(thrown)
+ .hasMessageThat()
+ .contains("Change " + r.getChange().getId() + " is abandoned");
+ }
+
+ @Test
+ public void rebaseOntoAbandonedChange() throws Exception {
+ // Create two changes both with the same parent
+ PushOneCommit.Result r = createChange();
+ testRepo.reset("HEAD~1");
+ PushOneCommit.Result r2 = createChange();
+
+ // Abandon the first change
+ String changeId = r.getChangeId();
+ assertThat(info(changeId).status).isEqualTo(ChangeStatus.NEW);
+ gApi.changes().id(changeId).abandon();
+ ChangeInfo info = info(changeId);
+ assertThat(info.status).isEqualTo(ChangeStatus.ABANDONED);
+
+ RebaseInput ri = new RebaseInput();
+ ri.base = r.getCommit().name();
+
+ ResourceConflictException thrown =
+ assertThrows(
+ ResourceConflictException.class,
+ () -> rebaseCallWithInput.call(r2.getChangeId(), ri));
+ assertThat(thrown).hasMessageThat().contains("base change is abandoned: " + changeId);
+ }
+
+ @Test
+ public void rebaseOntoSelf() throws Exception {
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+ String commit = r.getCommit().name();
+ RebaseInput ri = new RebaseInput();
+ ri.base = commit;
+ ResourceConflictException thrown =
+ assertThrows(
+ ResourceConflictException.class, () -> rebaseCallWithInput.call(changeId, ri));
+ assertThat(thrown)
+ .hasMessageThat()
+ .contains("cannot rebase change " + r.getChange().getId() + " onto itself");
+ }
+
+ @Test
+ public void rebaseChangeBaseRecursion() throws Exception {
+ PushOneCommit.Result r1 = createChange();
+ PushOneCommit.Result r2 = createChange();
+
+ RebaseInput ri = new RebaseInput();
+ ri.base = r2.getCommit().name();
+ String expectedMessage =
+ "base change "
+ + r2.getChangeId()
+ + " is a descendant of the current change - recursion not allowed";
+ ResourceConflictException thrown =
+ assertThrows(
+ ResourceConflictException.class,
+ () -> rebaseCallWithInput.call(r1.getChangeId(), ri));
+ assertThat(thrown).hasMessageThat().contains(expectedMessage);
+ }
+
+ @Test
+ public void cannotRebaseOntoBaseThatIsNotPresentInTargetBranch() throws Exception {
+ ObjectId initial = repo().exactRef(HEAD).getLeaf().getObjectId();
+
+ BranchInput branchInput = new BranchInput();
+ branchInput.revision = initial.getName();
+ gApi.projects().name(project.get()).branch("foo").create(branchInput);
+
+ PushOneCommit.Result r1 =
+ pushFactory
+ .create(admin.newIdent(), testRepo, "Change on foo branch", "a.txt", "a-content")
+ .to("refs/for/foo");
+ approve(r1.getChangeId());
+ gApi.changes().id(r1.getChangeId()).current().submit();
+
+ // reset HEAD in order to create a sibling of the first change
+ testRepo.reset(initial);
+
+ PushOneCommit.Result r2 =
+ pushFactory
+ .create(admin.newIdent(), testRepo, "Change on master branch", "b.txt", "b-content")
+ .to("refs/for/master");
+
+ RebaseInput rebaseInput = new RebaseInput();
+ rebaseInput.base = r1.getCommit().getName();
+ ResourceConflictException thrown =
+ assertThrows(
+ ResourceConflictException.class,
+ () -> rebaseCallWithInput.call(r2.getChangeId(), rebaseInput));
+ assertThat(thrown)
+ .hasMessageThat()
+ .contains(
+ String.format(
+ "base change is targeting wrong branch: %s,refs/heads/foo", project.get()));
+
+ rebaseInput.base = "refs/heads/foo";
+ thrown =
+ assertThrows(
+ ResourceConflictException.class,
+ () -> rebaseCallWithInput.call(r2.getChangeId(), rebaseInput));
+ assertThat(thrown)
+ .hasMessageThat()
+ .contains(
+ String.format(
+ "base revision is missing from the destination branch: %s", rebaseInput.base));
+ }
+
+ @Test
+ public void rebaseUpToDateChange() throws Exception {
+ PushOneCommit.Result r = createChange();
+ verifyChangeIsUpToDate(r);
+ }
+
+ @Test
+ public void rebaseDoesNotAddWorkInProgress() throws Exception {
+ PushOneCommit.Result r = createChange();
+
+ // create an unrelated change so that we can rebase
+ testRepo.reset("HEAD~1");
+ PushOneCommit.Result unrelated = createChange();
+ gApi.changes().id(unrelated.getChangeId()).current().review(ReviewInput.approve());
+ gApi.changes().id(unrelated.getChangeId()).current().submit();
+
+ rebaseCall.call(r.getChangeId());
+
+ // change is still ready for review after rebase
+ assertThat(gApi.changes().id(r.getChangeId()).get().workInProgress).isNull();
+ }
+
+ @Test
+ public void rebaseDoesNotRemoveWorkInProgress() throws Exception {
+ PushOneCommit.Result r = createChange();
+ change(r).setWorkInProgress();
+
+ // create an unrelated change so that we can rebase
+ testRepo.reset("HEAD~1");
+ PushOneCommit.Result unrelated = createChange();
+ gApi.changes().id(unrelated.getChangeId()).current().review(ReviewInput.approve());
+ gApi.changes().id(unrelated.getChangeId()).current().submit();
+
+ rebaseCall.call(r.getChangeId());
+
+ // change is still work in progress after rebase
+ assertThat(gApi.changes().id(r.getChangeId()).get().workInProgress).isTrue();
+ }
+
+ @Test
+ public void rebaseAsUploaderInAttentionSet() throws Exception {
+ // Create two changes both with the same parent
+ PushOneCommit.Result r = createChange();
+ testRepo.reset("HEAD~1");
+ PushOneCommit.Result r2 = createChange();
+
+ // Approve and submit the first change
+ RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
+ revision.review(ReviewInput.approve());
+ revision.submit();
+
+ TestAccount admin2 = accountCreator.admin2();
+ requestScopeOperations.setApiUser(admin2.id());
+ amendChangeWithUploader(r2, project, admin2);
+ gApi.changes()
+ .id(r2.getChangeId())
+ .addToAttentionSet(new AttentionSetInput(admin2.id().toString(), "manual update"));
+
+ rebaseCall.call(r2.getChangeId());
+ }
+
+ @Test
+ public void rebaseOnChangeNumber() throws Exception {
+ String branchTip = testRepo.getRepository().exactRef("HEAD").getObjectId().name();
+ PushOneCommit.Result r1 = createChange();
+ testRepo.reset("HEAD~1");
+ PushOneCommit.Result r2 = createChange();
+
+ ChangeInfo ci2 = get(r2.getChangeId(), CURRENT_REVISION, CURRENT_COMMIT);
+ RevisionInfo ri2 = ci2.revisions.get(ci2.currentRevision);
+ assertThat(ri2.commit.parents.get(0).commit).isEqualTo(branchTip);
+
+ Change.Id id1 = r1.getChange().getId();
+ RebaseInput in = new RebaseInput();
+ in.base = id1.toString();
+ rebaseCallWithInput.call(r2.getChangeId(), in);
+
+ Change.Id id2 = r2.getChange().getId();
+ ci2 = get(r2.getChangeId(), CURRENT_REVISION, CURRENT_COMMIT);
+ ri2 = ci2.revisions.get(ci2.currentRevision);
+ assertThat(ri2.commit.parents.get(0).commit).isEqualTo(r1.getCommit().name());
+
+ List<RelatedChangeAndCommitInfo> related =
+ gApi.changes().id(id2.get()).revision(ri2._number).related().changes;
+ assertThat(related).hasSize(2);
+ assertThat(related.get(0)._changeNumber).isEqualTo(id2.get());
+ assertThat(related.get(0)._revisionNumber).isEqualTo(2);
+ assertThat(related.get(1)._changeNumber).isEqualTo(id1.get());
+ assertThat(related.get(1)._revisionNumber).isEqualTo(1);
+ }
+
+ @Test
+ public void rebaseOnClosedChange() throws Exception {
+ String branchTip = testRepo.getRepository().exactRef("HEAD").getObjectId().name();
+ PushOneCommit.Result r1 = createChange();
+ testRepo.reset("HEAD~1");
+ PushOneCommit.Result r2 = createChange();
+
+ ChangeInfo ci2 = get(r2.getChangeId(), CURRENT_REVISION, CURRENT_COMMIT);
+ RevisionInfo ri2 = ci2.revisions.get(ci2.currentRevision);
+ assertThat(ri2.commit.parents.get(0).commit).isEqualTo(branchTip);
+
+ // Submit first change.
+ Change.Id id1 = r1.getChange().getId();
+ gApi.changes().id(id1.get()).current().review(ReviewInput.approve());
+ gApi.changes().id(id1.get()).current().submit();
+
+ // Rebase second change on first change.
+ RebaseInput in = new RebaseInput();
+ in.base = id1.toString();
+ rebaseCallWithInput.call(r2.getChangeId(), in);
+
+ Change.Id id2 = r2.getChange().getId();
+ ci2 = get(r2.getChangeId(), CURRENT_REVISION, CURRENT_COMMIT);
+ ri2 = ci2.revisions.get(ci2.currentRevision);
+ assertThat(ri2.commit.parents.get(0).commit).isEqualTo(r1.getCommit().name());
+
+ assertThat(gApi.changes().id(id2.get()).revision(ri2._number).related().changes).isEmpty();
+ }
+
+ @Test
+ public void rebaseOnNonExistingChange() throws Exception {
+ String changeId = createChange().getChangeId();
+ RebaseInput in = new RebaseInput();
+ in.base = "999999";
+ UnprocessableEntityException exception =
+ assertThrows(
+ UnprocessableEntityException.class, () -> rebaseCallWithInput.call(changeId, in));
+ assertThat(exception).hasMessageThat().contains("Base change not found: " + in.base);
+ }
+
+ @Test
+ public void rebaseNotAllowedWithoutPermission() throws Exception {
+ // Create two changes both with the same parent
+ PushOneCommit.Result r = createChange();
+ testRepo.reset("HEAD~1");
+ PushOneCommit.Result r2 = createChange();
+
+ // Approve and submit the first change
+ RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
+ revision.review(ReviewInput.approve());
+ revision.submit();
+
+ // Rebase the second
+ String changeId = r2.getChangeId();
+ requestScopeOperations.setApiUser(user.id());
+ AuthException thrown = assertThrows(AuthException.class, () -> rebaseCall.call(changeId));
+ assertThat(thrown)
+ .hasMessageThat()
+ .isEqualTo(
+ "rebase not permitted (change owners and users with the 'Submit' or 'Rebase'"
+ + " permission can rebase if they have the 'Push' permission)");
+ }
+
+ @Test
+ public void rebaseAllowedWithPermission() throws Exception {
+ // Create two changes both with the same parent
+ PushOneCommit.Result r = createChange();
+ testRepo.reset("HEAD~1");
+ PushOneCommit.Result r2 = createChange();
+
+ // Approve and submit the first change
+ RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
+ revision.review(ReviewInput.approve());
+ revision.submit();
+
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(allow(Permission.REBASE).ref("refs/heads/master").group(REGISTERED_USERS))
+ .update();
+
+ // Rebase the second
+ String changeId = r2.getChangeId();
+ requestScopeOperations.setApiUser(user.id());
+ rebaseCall.call(changeId);
+ }
+
+ @Test
+ public void rebaseNotAllowedWithoutPushPermission() throws Exception {
+ // Create two changes both with the same parent
+ PushOneCommit.Result r = createChange();
+ testRepo.reset("HEAD~1");
+ PushOneCommit.Result r2 = createChange();
+
+ // Approve and submit the first change
+ RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
+ revision.review(ReviewInput.approve());
+ revision.submit();
+
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(allow(Permission.REBASE).ref("refs/heads/master").group(REGISTERED_USERS))
+ .add(block(Permission.PUSH).ref("refs/for/*").group(REGISTERED_USERS))
+ .update();
+
+ // Rebase the second
+ String changeId = r2.getChangeId();
+ requestScopeOperations.setApiUser(user.id());
+ AuthException thrown = assertThrows(AuthException.class, () -> rebaseCall.call(changeId));
+ assertThat(thrown)
+ .hasMessageThat()
+ .isEqualTo(
+ "rebase not permitted (change owners and users with the 'Submit' or 'Rebase'"
+ + " permission can rebase if they have the 'Push' permission)");
+ }
+
+ @Test
+ public void rebaseNotAllowedForOwnerWithoutPushPermission() throws Exception {
+ // Create two changes both with the same parent
+ PushOneCommit.Result r = createChange();
+ testRepo.reset("HEAD~1");
+ PushOneCommit.Result r2 = createChange();
+
+ // Approve and submit the first change
+ RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
+ revision.review(ReviewInput.approve());
+ revision.submit();
+
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(block(Permission.PUSH).ref("refs/for/*").group(REGISTERED_USERS))
+ .update();
+
+ // Rebase the second
+ String changeId = r2.getChangeId();
+ AuthException thrown = assertThrows(AuthException.class, () -> rebaseCall.call(changeId));
+ assertThat(thrown)
+ .hasMessageThat()
+ .isEqualTo(
+ "rebase not permitted (change owners and users with the 'Submit' or 'Rebase'"
+ + " permission can rebase if they have the 'Push' permission)");
+ }
+
+ @Test
+ public void rebaseWithValidationOptions() throws Exception {
+ // Create two changes both with the same parent
+ PushOneCommit.Result r = createChange();
+ testRepo.reset("HEAD~1");
+ PushOneCommit.Result r2 = createChange();
+
+ // Approve and submit the first change
+ RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
+ revision.review(ReviewInput.approve());
+ revision.submit();
+
+ RebaseInput rebaseInput = new RebaseInput();
+ rebaseInput.validationOptions = ImmutableMap.of("key", "value");
+
+ TestCommitValidationListener testCommitValidationListener =
+ new TestCommitValidationListener();
+ try (ExtensionRegistry.Registration unusedRegistration =
+ extensionRegistry.newRegistration().add(testCommitValidationListener)) {
+ // Rebase the second change
+ rebaseCallWithInput.call(r2.getChangeId(), rebaseInput);
+ assertThat(testCommitValidationListener.receiveEvent.pushOptions)
+ .containsExactly("key", "value");
+ }
+ }
+
+ protected void verifyRebaseForChange(
+ Change.Id changeId, Change.Id baseChangeId, boolean shouldHaveApproval)
+ throws RestApiException {
+ verifyRebaseForChange(changeId, baseChangeId, shouldHaveApproval, 2);
+ }
+
+ protected void verifyRebaseForChange(
+ Change.Id changeId,
+ Change.Id baseChangeId,
+ boolean shouldHaveApproval,
+ int expectedNumRevisions)
+ throws RestApiException {
+ ChangeInfo baseInfo = gApi.changes().id(baseChangeId.get()).get(CURRENT_REVISION);
+ verifyRebaseForChange(
+ changeId, baseInfo.currentRevision, shouldHaveApproval, expectedNumRevisions);
+ }
+
+ protected void verifyRebaseForChange(
+ Change.Id changeId, String baseCommit, boolean shouldHaveApproval, int expectedNumRevisions)
+ throws RestApiException {
+ ChangeInfo info =
+ gApi.changes().id(changeId.get()).get(CURRENT_REVISION, CURRENT_COMMIT, DETAILED_LABELS);
+
+ RevisionInfo r = info.revisions.get(info.currentRevision);
+ assertThat(r._number).isEqualTo(expectedNumRevisions);
+
+ // ...and the base should be correct
+ assertThat(r.commit.parents).hasSize(1);
+ assertWithMessage("base commit for change " + changeId)
+ .that(r.commit.parents.get(0).commit)
+ .isEqualTo(baseCommit);
+
+ // ...and the committer and description should be correct
+ GitPerson committer = info.revisions.get(info.currentRevision).commit.committer;
+ assertThat(committer.name).isEqualTo(admin.fullName());
+ assertThat(committer.email).isEqualTo(admin.email());
+ String description = info.revisions.get(info.currentRevision).description;
+ assertThat(description).isEqualTo("Rebase");
+
+ if (shouldHaveApproval) {
+ // ...and the approval was copied
+ LabelInfo cr = info.labels.get(LabelId.CODE_REVIEW);
+ assertThat(cr).isNotNull();
+ assertThat(cr.all).isNotNull();
+ assertThat(cr.all).hasSize(1);
+ assertThat(cr.all.get(0).value).isEqualTo(1);
+ }
+ }
+
+ protected void verifyChangeIsUpToDate(PushOneCommit.Result r) {
+ ResourceConflictException thrown =
+ assertThrows(ResourceConflictException.class, () -> rebaseCall.call(r.getChangeId()));
+ assertThat(thrown).hasMessageThat().contains("Change is already up to date");
+ }
+
+ protected static class TestCommitValidationListener implements CommitValidationListener {
+ public CommitReceivedEvent receiveEvent;
+
+ @Override
+ public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
+ throws CommitValidationException {
+ this.receiveEvent = receiveEvent;
+ return ImmutableList.of();
+ }
+ }
+
+ protected static class TestWorkInProgressStateChangedListener
+ implements WorkInProgressStateChangedListener {
+ boolean invoked;
+ Boolean wip;
+
+ @Override
+ public void onWorkInProgressStateChanged(WorkInProgressStateChangedListener.Event event) {
+ this.invoked = true;
+ this.wip =
+ event.getChange().workInProgress != null ? event.getChange().workInProgress : false;
+ }
+ }
+ }
+
+ public abstract static class Rebase extends Base {
+ @Test
+ public void rebaseChangeBase() throws Exception {
+ PushOneCommit.Result r1 = createChange();
+ PushOneCommit.Result r2 = createChange();
+ PushOneCommit.Result r3 = createChange();
+ RebaseInput ri = new RebaseInput();
+
+ // rebase r3 directly onto master (break dep. towards r2)
+ ri.base = "";
+ rebaseCallWithInput.call(r3.getChangeId(), ri);
+ PatchSet ps3 = r3.getPatchSet();
+ assertThat(ps3.id().get()).isEqualTo(2);
+
+ // rebase r2 onto r3 (referenced by ref)
+ ri.base = ps3.id().toRefName();
+ rebaseCallWithInput.call(r2.getChangeId(), ri);
+ PatchSet ps2 = r2.getPatchSet();
+ assertThat(ps2.id().get()).isEqualTo(2);
+
+ // rebase r1 onto r2 (referenced by commit)
+ ri.base = ps2.commitId().name();
+ rebaseCallWithInput.call(r1.getChangeId(), ri);
+ PatchSet ps1 = r1.getPatchSet();
+ assertThat(ps1.id().get()).isEqualTo(2);
+
+ // rebase r1 onto r3 (referenced by change number)
+ ri.base = String.valueOf(r3.getChange().getId().get());
+ rebaseCallWithInput.call(r1.getChangeId(), ri);
+ assertThat(r1.getPatchSetId().get()).isEqualTo(3);
+ }
+
+ @Test
+ public void rebaseWithConflict_conflictsAllowed() throws Exception {
+ String patchSetSubject = "patch set change";
+ String patchSetContent = "patch set content";
+ String baseSubject = "base change";
+ String baseContent = "base content";
+
+ PushOneCommit.Result r1 = createChange(baseSubject, PushOneCommit.FILE_NAME, baseContent);
+ gApi.changes()
+ .id(r1.getChangeId())
+ .revision(r1.getCommit().name())
+ .review(ReviewInput.approve());
+ gApi.changes().id(r1.getChangeId()).revision(r1.getCommit().name()).submit();
+
+ testRepo.reset("HEAD~1");
+ PushOneCommit push =
+ pushFactory.create(
+ admin.newIdent(),
+ testRepo,
+ patchSetSubject,
+ PushOneCommit.FILE_NAME,
+ patchSetContent);
+ PushOneCommit.Result r2 = push.to("refs/for/master");
+ r2.assertOkStatus();
+
+ String changeId = r2.getChangeId();
+ RevCommit patchSet = r2.getCommit();
+ RevCommit base = r1.getCommit();
+
+ TestWorkInProgressStateChangedListener wipStateChangedListener =
+ new TestWorkInProgressStateChangedListener();
+ try (ExtensionRegistry.Registration registration =
+ extensionRegistry.newRegistration().add(wipStateChangedListener)) {
+ RebaseInput rebaseInput = new RebaseInput();
+ rebaseInput.allowConflicts = true;
+ ChangeInfo changeInfo =
+ gApi.changes().id(changeId).revision(patchSet.name()).rebaseAsInfo(rebaseInput);
+ assertThat(changeInfo.containsGitConflicts).isTrue();
+ assertThat(changeInfo.workInProgress).isTrue();
+ }
+ assertThat(wipStateChangedListener.invoked).isTrue();
+ assertThat(wipStateChangedListener.wip).isTrue();
+
+ // To get the revisions, we must retrieve the change with more change options.
+ ChangeInfo changeInfo =
+ gApi.changes().id(changeId).get(ALL_REVISIONS, CURRENT_COMMIT, CURRENT_REVISION);
+ assertThat(changeInfo.revisions).hasSize(2);
+ assertThat(changeInfo.revisions.get(changeInfo.currentRevision).commit.parents.get(0).commit)
+ .isEqualTo(base.name());
+
+ // Verify that the file content in the created patch set is correct.
+ // We expect that it has conflict markers to indicate the conflict.
+ BinaryResult bin =
+ gApi.changes().id(changeId).current().file(PushOneCommit.FILE_NAME).content();
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ bin.writeTo(os);
+ String fileContent = new String(os.toByteArray(), UTF_8);
+ String patchSetSha1 = abbreviateName(patchSet, 6);
+ String baseSha1 = abbreviateName(base, 6);
+ assertThat(fileContent)
+ .isEqualTo(
+ "<<<<<<< PATCH SET ("
+ + patchSetSha1
+ + " "
+ + patchSetSubject
+ + ")\n"
+ + patchSetContent
+ + "\n"
+ + "=======\n"
+ + baseContent
+ + "\n"
+ + ">>>>>>> BASE ("
+ + baseSha1
+ + " "
+ + baseSubject
+ + ")\n");
+
+ // Verify the message that has been posted on the change.
+ List<ChangeMessageInfo> messages = gApi.changes().id(changeId).messages();
+ assertThat(messages).hasSize(2);
+ assertThat(Iterables.getLast(messages).message)
+ .isEqualTo(
+ "Patch Set 2: Patch Set 1 was rebased\n\n"
+ + "The following files contain Git conflicts:\n"
+ + "* "
+ + PushOneCommit.FILE_NAME
+ + "\n");
+ }
+
+ @Test
+ public void rebaseWithConflict_conflictsForbidden() throws Exception {
+ PushOneCommit.Result r1 = createChange();
+ gApi.changes()
+ .id(r1.getChangeId())
+ .revision(r1.getCommit().name())
+ .review(ReviewInput.approve());
+ gApi.changes().id(r1.getChangeId()).revision(r1.getCommit().name()).submit();
+
+ PushOneCommit push =
+ pushFactory.create(
+ admin.newIdent(),
+ testRepo,
+ PushOneCommit.SUBJECT,
+ PushOneCommit.FILE_NAME,
+ "other content",
+ "If09d8782c1e59dd0b33de2b1ec3595d69cc10ad5");
+ PushOneCommit.Result r2 = push.to("refs/for/master");
+ r2.assertOkStatus();
+ ResourceConflictException exception =
+ assertThrows(ResourceConflictException.class, () -> rebaseCall.call(r2.getChangeId()));
+ assertThat(exception)
+ .hasMessageThat()
+ .isEqualTo(
+ String.format(
+ "Change %s could not be rebased due to a conflict during merge.\n\n"
+ + "merge conflict(s):\n%s",
+ r2.getChange().getId(), PushOneCommit.FILE_NAME));
+ }
+
+ @Test
+ public void rebaseFromRelationChainToClosedChange() throws Exception {
+ PushOneCommit.Result r1 = createChange();
+ testRepo.reset("HEAD~1");
+
+ createChange();
+ PushOneCommit.Result r3 = createChange();
+
+ // Submit first change.
+ Change.Id id1 = r1.getChange().getId();
+ gApi.changes().id(id1.get()).current().review(ReviewInput.approve());
+ gApi.changes().id(id1.get()).current().submit();
+
+ // Rebase third change on first change.
+ RebaseInput in = new RebaseInput();
+ in.base = id1.toString();
+ rebaseCallWithInput.call(r3.getChangeId(), in);
+
+ Change.Id id3 = r3.getChange().getId();
+ ChangeInfo ci3 = get(r3.getChangeId(), CURRENT_REVISION, CURRENT_COMMIT);
+ RevisionInfo ri3 = ci3.revisions.get(ci3.currentRevision);
+ assertThat(ri3.commit.parents.get(0).commit).isEqualTo(r1.getCommit().name());
+
+ assertThat(gApi.changes().id(id3.get()).revision(ri3._number).related().changes).isEmpty();
+ }
+ }
+
+ public static class RebaseViaRevisionApi extends Rebase {
+ @Before
+ public void setUp() throws Exception {
+ init(
+ id -> gApi.changes().id(id).current().rebase(),
+ (id, in) -> gApi.changes().id(id).current().rebase(in));
+ }
+
+ @Test
+ public void rebaseOutdatedPatchSet() throws Exception {
+ String fileName1 = "a.txt";
+ String fileContent1 = "some content";
+ String fileName2 = "b.txt";
+ String fileContent2Ps1 = "foo";
+ String fileContent2Ps2 = "foo/bar";
+
+ // Create two changes both with the same parent touching disjunct files
+ PushOneCommit.Result r =
+ pushFactory
+ .create(admin.newIdent(), testRepo, PushOneCommit.SUBJECT, fileName1, fileContent1)
+ .to("refs/for/master");
+ r.assertOkStatus();
+ String changeId1 = r.getChangeId();
+ testRepo.reset("HEAD~1");
+ PushOneCommit push =
+ pushFactory.create(
+ admin.newIdent(), testRepo, PushOneCommit.SUBJECT, fileName2, fileContent2Ps1);
+ PushOneCommit.Result r2 = push.to("refs/for/master");
+ r2.assertOkStatus();
+ String changeId2 = r2.getChangeId();
+
+ // Approve and submit the first change
+ RevisionApi revision = gApi.changes().id(changeId1).current();
+ revision.review(ReviewInput.approve());
+ revision.submit();
+
+ // Amend the second change so that it has 2 patch sets
+ amendChange(
+ changeId2,
+ "refs/for/master",
+ admin,
+ testRepo,
+ PushOneCommit.SUBJECT,
+ fileName2,
+ fileContent2Ps2)
+ .assertOkStatus();
+ ChangeInfo changeInfo2 = gApi.changes().id(changeId2).get();
+ assertThat(changeInfo2.revisions.get(changeInfo2.currentRevision)._number).isEqualTo(2);
+
+ // Rebase the first patch set of the second change
+ gApi.changes().id(changeId2).revision(1).rebase();
+
+ // Second change should have 3 patch sets
+ changeInfo2 = gApi.changes().id(changeId2).get();
+ assertThat(changeInfo2.revisions.get(changeInfo2.currentRevision)._number).isEqualTo(3);
+
+ // ... and the committer and description should be correct
+ ChangeInfo info = gApi.changes().id(changeId2).get(CURRENT_REVISION, CURRENT_COMMIT);
+ GitPerson committer = info.revisions.get(info.currentRevision).commit.committer;
+ assertThat(committer.name).isEqualTo(admin.fullName());
+ assertThat(committer.email).isEqualTo(admin.email());
+ String description = info.revisions.get(info.currentRevision).description;
+ assertThat(description).isEqualTo("Rebase");
+
+ // ... and the file contents should match with patch set 1 based on change1
+ assertThat(gApi.changes().id(changeId2).current().file(fileName1).content().asString())
+ .isEqualTo(fileContent1);
+ assertThat(gApi.changes().id(changeId2).current().file(fileName2).content().asString())
+ .isEqualTo(fileContent2Ps1);
+ }
+ }
+
+ public static class RebaseViaChangeApi extends Rebase {
+ @Before
+ public void setUp() throws Exception {
+ init(id -> gApi.changes().id(id).rebase(), (id, in) -> gApi.changes().id(id).rebase(in));
+ }
+ }
+
+ public static class RebaseChain extends Base {
+ @Before
+ public void setUp() throws Exception {
+ init(
+ id -> {
+ @SuppressWarnings("unused")
+ Object unused = gApi.changes().id(id).rebaseChain();
+ },
+ (id, in) -> {
+ @SuppressWarnings("unused")
+ Object unused = gApi.changes().id(id).rebaseChain(in);
+ });
+ }
+
+ @Override
+ protected void verifyChangeIsUpToDate(PushOneCommit.Result r) {
+ ResourceConflictException thrown =
+ assertThrows(ResourceConflictException.class, () -> rebaseCall.call(r.getChangeId()));
+ assertThat(thrown).hasMessageThat().contains("The whole chain is already up to date.");
+ }
+
+ @Test
+ public void rebaseChain() throws Exception {
+ // Create changes with the following hierarchy:
+ // * HEAD
+ // * r1
+ // * r2
+ // * r3
+ // * r4
+ // *r5
+ PushOneCommit.Result r = createChange();
+ testRepo.reset("HEAD~1");
+ PushOneCommit.Result r2 = createChange();
+ PushOneCommit.Result r3 = createChange();
+ PushOneCommit.Result r4 = createChange();
+ PushOneCommit.Result r5 = createChange();
+
+ // Approve and submit the first change
+ RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
+ revision.review(ReviewInput.approve());
+ revision.submit();
+
+ // Add an approval whose score should be copied on trivial rebase
+ gApi.changes().id(r2.getChangeId()).current().review(ReviewInput.recommend());
+ gApi.changes().id(r3.getChangeId()).current().review(ReviewInput.recommend());
+
+ // Rebase the chain through r4.
+ verifyRebaseChainResponse(
+ gApi.changes().id(r4.getChangeId()).rebaseChain(), false, r2, r3, r4);
+
+ // Only r2, r3 and r4 are rebased.
+ verifyRebaseForChange(r2.getChange().getId(), r.getCommit().name(), true, 2);
+ verifyRebaseForChange(r3.getChange().getId(), r2.getChange().getId(), true);
+ verifyRebaseForChange(r4.getChange().getId(), r3.getChange().getId(), false);
+
+ verifyChangeIsUpToDate(r2);
+ verifyChangeIsUpToDate(r3);
+ verifyChangeIsUpToDate(r4);
+
+ // r5 wasn't rebased.
+ ChangeInfo r5info = gApi.changes().id(r5.getChangeId()).get(CURRENT_REVISION);
+ assertThat(r5info.revisions.get(r5info.currentRevision)._number).isEqualTo(1);
+
+ // Rebasing r5
+ verifyRebaseChainResponse(
+ gApi.changes().id(r5.getChangeId()).rebaseChain(), false, r2, r3, r4, r5);
+
+ verifyRebaseForChange(r5.getChange().getId(), r4.getChange().getId(), false);
+ }
+
+ @Test
+ public void rebasePartlyOutdatedChain() throws Exception {
+ final String file = "modified_file.txt";
+ final String oldContent = "old content";
+ final String newContent = "new content";
+ // Create changes with the following revision hierarchy:
+ // * HEAD
+ // * r1
+ // * r2
+ // * r3/1 r3/2
+ // * r4
+ PushOneCommit.Result r = createChange();
+ testRepo.reset("HEAD~1");
+ PushOneCommit.Result r2 = createChange();
+ PushOneCommit.Result r3 = createChange("original patch-set", file, oldContent);
+ PushOneCommit.Result r4 = createChange();
+ gApi.changes()
+ .id(r3.getChangeId())
+ .edit()
+ .modifyFile(file, RawInputUtil.create(newContent.getBytes(UTF_8)));
+ gApi.changes().id(r3.getChangeId()).edit().publish();
+
+ // Approve and submit the first change
+ RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
+ revision.review(ReviewInput.approve());
+ revision.submit();
+
+ // Rebase the chain through r4.
+ rebaseCall.call(r4.getChangeId());
+
+ verifyRebaseForChange(r2.getChange().getId(), r.getCommit().name(), false, 2);
+ verifyRebaseForChange(r3.getChange().getId(), r2.getChange().getId(), false, 3);
+ verifyRebaseForChange(r4.getChange().getId(), r3.getChange().getId(), false);
+
+ assertThat(gApi.changes().id(r3.getChangeId()).current().file(file).content().asString())
+ .isEqualTo(newContent);
+
+ verifyChangeIsUpToDate(r2);
+ verifyChangeIsUpToDate(r3);
+ verifyChangeIsUpToDate(r4);
+ }
+
+ @Test
+ public void rebaseChainWithConflicts_conflictsForbidden() throws Exception {
+ PushOneCommit.Result r1 = createChange();
+ gApi.changes()
+ .id(r1.getChangeId())
+ .revision(r1.getCommit().name())
+ .review(ReviewInput.approve());
+ gApi.changes().id(r1.getChangeId()).revision(r1.getCommit().name()).submit();
+
+ PushOneCommit push =
+ pushFactory.create(
+ admin.newIdent(),
+ testRepo,
+ PushOneCommit.SUBJECT,
+ PushOneCommit.FILE_NAME,
+ "other content",
+ "I0020020020020020020020020020020020020002");
+ PushOneCommit.Result r2 = push.to("refs/for/master");
+ r2.assertOkStatus();
+ PushOneCommit.Result r3 = createChange("refs/for/master");
+ r3.assertOkStatus();
+ ResourceConflictException exception =
+ assertThrows(
+ ResourceConflictException.class,
+ () -> gApi.changes().id(r3.getChangeId()).rebaseChain());
+ assertThat(exception)
+ .hasMessageThat()
+ .isEqualTo(
+ String.format(
+ "Change %s could not be rebased due to a conflict during merge.\n\n"
+ + "merge conflict(s):\n%s",
+ r2.getChange().getId(), PushOneCommit.FILE_NAME));
+ }
+
+ @Test
+ public void rebaseChainWithConflicts_conflictsAllowed() throws Exception {
+ String patchSetSubject = "patch set change";
+ String patchSetContent = "patch set content";
+ String baseSubject = "base change";
+ String baseContent = "base content";
+
+ PushOneCommit.Result r1 = createChange(baseSubject, PushOneCommit.FILE_NAME, baseContent);
+ gApi.changes()
+ .id(r1.getChangeId())
+ .revision(r1.getCommit().name())
+ .review(ReviewInput.approve());
+ gApi.changes().id(r1.getChangeId()).revision(r1.getCommit().name()).submit();
+
+ testRepo.reset("HEAD~1");
+ PushOneCommit push =
+ pushFactory.create(
+ admin.newIdent(),
+ testRepo,
+ patchSetSubject,
+ PushOneCommit.FILE_NAME,
+ patchSetContent);
+ PushOneCommit.Result r2 = push.to("refs/for/master");
+ r2.assertOkStatus();
+
+ String changeWithConflictId = r2.getChangeId();
+ RevCommit patchSet = r2.getCommit();
+ RevCommit base = r1.getCommit();
+ PushOneCommit.Result r3 = createChange("refs/for/master");
+ r3.assertOkStatus();
+
+ TestWorkInProgressStateChangedListener wipStateChangedListener =
+ new TestWorkInProgressStateChangedListener();
+ try (ExtensionRegistry.Registration registration =
+ extensionRegistry.newRegistration().add(wipStateChangedListener)) {
+ RebaseInput rebaseInput = new RebaseInput();
+ rebaseInput.allowConflicts = true;
+ Response<RebaseChainInfo> res =
+ gApi.changes().id(r3.getChangeId()).rebaseChain(rebaseInput);
+ verifyRebaseChainResponse(res, true, r2, r3);
+ RebaseChainInfo rebaseChainInfo = res.value();
+ ChangeInfo changeWithConflictInfo = rebaseChainInfo.rebasedChanges.get(0);
+ assertThat(changeWithConflictInfo.changeId).isEqualTo(r2.getChangeId());
+ assertThat(changeWithConflictInfo.containsGitConflicts).isTrue();
+ assertThat(changeWithConflictInfo.workInProgress).isTrue();
+ ChangeInfo childChangeInfo = rebaseChainInfo.rebasedChanges.get(1);
+ assertThat(childChangeInfo.changeId).isEqualTo(r3.getChangeId());
+ assertThat(childChangeInfo.containsGitConflicts).isTrue();
+ assertThat(childChangeInfo.workInProgress).isTrue();
+ }
+ assertThat(wipStateChangedListener.invoked).isTrue();
+ assertThat(wipStateChangedListener.wip).isTrue();
+
+ // To get the revisions, we must retrieve the change with more change options.
+ ChangeInfo changeInfo =
+ gApi.changes()
+ .id(changeWithConflictId)
+ .get(ALL_REVISIONS, CURRENT_COMMIT, CURRENT_REVISION);
+ assertThat(changeInfo.revisions).hasSize(2);
+ assertThat(changeInfo.revisions.get(changeInfo.currentRevision).commit.parents.get(0).commit)
+ .isEqualTo(base.name());
+
+ // Verify that the file content in the created patch set is correct.
+ // We expect that it has conflict markers to indicate the conflict.
+ BinaryResult bin =
+ gApi.changes().id(changeWithConflictId).current().file(PushOneCommit.FILE_NAME).content();
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ bin.writeTo(os);
+ String fileContent = new String(os.toByteArray(), UTF_8);
+ String patchSetSha1 = abbreviateName(patchSet, 6);
+ String baseSha1 = abbreviateName(base, 6);
+ assertThat(fileContent)
+ .isEqualTo(
+ "<<<<<<< PATCH SET ("
+ + patchSetSha1
+ + " "
+ + patchSetSubject
+ + ")\n"
+ + patchSetContent
+ + "\n"
+ + "=======\n"
+ + baseContent
+ + "\n"
+ + ">>>>>>> BASE ("
+ + baseSha1
+ + " "
+ + baseSubject
+ + ")\n");
+
+ // Verify the message that has been posted on the change.
+ List<ChangeMessageInfo> messages = gApi.changes().id(changeWithConflictId).messages();
+ assertThat(messages).hasSize(2);
+ assertThat(Iterables.getLast(messages).message)
+ .isEqualTo(
+ "Patch Set 2: Patch Set 1 was rebased\n\n"
+ + "The following files contain Git conflicts:\n"
+ + "* "
+ + PushOneCommit.FILE_NAME
+ + "\n");
+ }
+
+ @Test
+ public void rebaseOntoMidChain() throws Exception {
+ // Create changes with the following hierarchy:
+ // * HEAD
+ // * r1
+ // * r2
+ // * r3
+ // * r4
+ PushOneCommit.Result r = createChange();
+ r.assertOkStatus();
+ testRepo.reset("HEAD~1");
+ PushOneCommit.Result r2 = createChange();
+ r2.assertOkStatus();
+ PushOneCommit.Result r3 = createChange();
+ r3.assertOkStatus();
+ PushOneCommit.Result r4 = createChange();
+
+ RebaseInput ri = new RebaseInput();
+ ri.base = r3.getCommit().name();
+ ResourceConflictException thrown =
+ assertThrows(
+ ResourceConflictException.class,
+ () -> rebaseCallWithInput.call(r4.getChangeId(), ri));
+ assertThat(thrown).hasMessageThat().contains("recursion not allowed");
+ }
+
+ private void verifyRebaseChainResponse(
+ Response<RebaseChainInfo> res,
+ boolean shouldHaveConflicts,
+ PushOneCommit.Result... changes) {
+ assertThat(res.statusCode()).isEqualTo(200);
+ RebaseChainInfo info = res.value();
+ assertThat(info.rebasedChanges.stream().map(c -> c._number).collect(Collectors.toList()))
+ .containsExactlyElementsIn(
+ Arrays.stream(changes)
+ .map(c -> c.getChange().getId().get())
+ .collect(Collectors.toList()))
+ .inOrder();
+ assertThat(info.containsGitConflicts).isEqualTo(shouldHaveConflicts ? true : null);
+ }
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
index d630296..04bdf15 100644
--- a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
@@ -601,6 +601,20 @@
}
@Test
+ public void getGroupFromMetaId() throws Exception {
+ AccountGroup.UUID uuid = groupOperations.newGroup().create();
+ InternalGroup preUpdateState = groupCache.get(uuid).get();
+ gApi.groups().id(uuid.toString()).description("New description");
+
+ InternalGroup postUpdateState = groupCache.get(uuid).get();
+ assertThat(postUpdateState).isNotEqualTo(preUpdateState);
+ assertThat(groupCache.getFromMetaId(uuid, preUpdateState.getRefState()))
+ .isEqualTo(preUpdateState);
+ assertThat(groupCache.getFromMetaId(uuid, postUpdateState.getRefState()))
+ .isEqualTo(postUpdateState);
+ }
+
+ @Test
@GerritConfig(name = "groups.global:Anonymous-Users.name", value = "All Users")
public void getSystemGroupByConfiguredName() throws Exception {
GroupReference anonymousUsersGroup = systemGroupBackend.getGroup(ANONYMOUS_USERS);
diff --git a/javatests/com/google/gerrit/acceptance/api/project/ProjectIndexerIT.java b/javatests/com/google/gerrit/acceptance/api/project/ProjectIndexerIT.java
index a625a70..93f91dd 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/ProjectIndexerIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/ProjectIndexerIT.java
@@ -54,7 +54,7 @@
@Inject private IndexOperations.Project projectIndexOperations;
private static final ImmutableSet<String> FIELDS =
- ImmutableSet.of(ProjectField.NAME.getName(), ProjectField.REF_STATE.getName());
+ ImmutableSet.of(ProjectField.NAME_SPEC.getName(), ProjectField.REF_STATE.getName());
@Test
public void indexProject_indexesRefStateOfProjectAndParents() throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/pgm/MigrateLabelFunctionsToSubmitRequirementIT.java b/javatests/com/google/gerrit/acceptance/pgm/MigrateLabelFunctionsToSubmitRequirementIT.java
index 74bfe0f..9d37497 100644
--- a/javatests/com/google/gerrit/acceptance/pgm/MigrateLabelFunctionsToSubmitRequirementIT.java
+++ b/javatests/com/google/gerrit/acceptance/pgm/MigrateLabelFunctionsToSubmitRequirementIT.java
@@ -17,6 +17,7 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -260,6 +261,96 @@
}
@Test
+ public void migrateBlockingLabel_withBranchAttribute() throws Exception {
+ createLabelWithBranch(
+ "Foo",
+ "MaxWithBlock",
+ /* ignoreSelfApproval= */ false,
+ ImmutableList.of("refs/heads/master"));
+
+ assertNonExistentSr(/* srName = */ "Foo");
+
+ TestUpdateUI updateUI = runMigration(/* expectedResult= */ Status.MIGRATED);
+ assertThat(updateUI.newlyCreatedSrs).isEqualTo(1);
+ assertThat(updateUI.existingSrsMismatchingWithMigration).isEqualTo(0);
+
+ assertExistentSr(
+ /* srName */ "Foo",
+ /* applicabilityExpression= */ "branch:\\\"refs/heads/master\\\"",
+ /* submittabilityExpression= */ "label:Foo=MAX AND -label:Foo=MIN",
+ /* canOverride= */ true);
+ assertLabelFunction("Foo", "NoBlock");
+ }
+
+ @Test
+ public void migrateBlockingLabel_withMultipleBranchAttributes() throws Exception {
+ createLabelWithBranch(
+ "Foo",
+ "MaxWithBlock",
+ /* ignoreSelfApproval= */ false,
+ ImmutableList.of("refs/heads/master", "refs/heads/develop"));
+
+ assertNonExistentSr(/* srName = */ "Foo");
+
+ TestUpdateUI updateUI = runMigration(/* expectedResult= */ Status.MIGRATED);
+ assertThat(updateUI.newlyCreatedSrs).isEqualTo(1);
+ assertThat(updateUI.existingSrsMismatchingWithMigration).isEqualTo(0);
+
+ assertExistentSr(
+ /* srName */ "Foo",
+ /* applicabilityExpression= */ "branch:\\\"refs/heads/master\\\" "
+ + "OR branch:\\\"refs/heads/develop\\\"",
+ /* submittabilityExpression= */ "label:Foo=MAX AND -label:Foo=MIN",
+ /* canOverride= */ true);
+ assertLabelFunction("Foo", "NoBlock");
+ }
+
+ @Test
+ public void migrateBlockingLabel_withRegexBranchAttribute() throws Exception {
+ createLabelWithBranch(
+ "Foo",
+ "MaxWithBlock",
+ /* ignoreSelfApproval= */ false,
+ ImmutableList.of("^refs/heads/main-.*"));
+
+ assertNonExistentSr(/* srName = */ "Foo");
+
+ TestUpdateUI updateUI = runMigration(/* expectedResult= */ Status.MIGRATED);
+ assertThat(updateUI.newlyCreatedSrs).isEqualTo(1);
+ assertThat(updateUI.existingSrsMismatchingWithMigration).isEqualTo(0);
+
+ assertExistentSr(
+ /* srName */ "Foo",
+ /* applicabilityExpression= */ "branch:\\\"^refs/heads/main-.*\\\"",
+ /* submittabilityExpression= */ "label:Foo=MAX AND -label:Foo=MIN",
+ /* canOverride= */ true);
+ assertLabelFunction("Foo", "NoBlock");
+ }
+
+ @Test
+ public void migrateBlockingLabel_withRegexAndNonRegexBranchAttributes() throws Exception {
+ createLabelWithBranch(
+ "Foo",
+ "MaxWithBlock",
+ /* ignoreSelfApproval= */ false,
+ ImmutableList.of("refs/heads/master", "^refs/heads/main-.*"));
+
+ assertNonExistentSr(/* srName = */ "Foo");
+
+ TestUpdateUI updateUI = runMigration(/* expectedResult= */ Status.MIGRATED);
+ assertThat(updateUI.newlyCreatedSrs).isEqualTo(1);
+ assertThat(updateUI.existingSrsMismatchingWithMigration).isEqualTo(0);
+
+ assertExistentSr(
+ /* srName */ "Foo",
+ /* applicabilityExpression= */ "branch:\\\"refs/heads/master\\\" "
+ + "OR branch:\\\"^refs/heads/main-.*\\\"",
+ /* submittabilityExpression= */ "label:Foo=MAX AND -label:Foo=MIN",
+ /* canOverride= */ true);
+ assertLabelFunction("Foo", "NoBlock");
+ }
+
+ @Test
public void migrationIsIdempotent() throws Exception {
String oldRefsConfigId;
try (Repository repo = repoManager.openRepository(project)) {
@@ -381,6 +472,21 @@
gApi.projects().name(project.get()).label(labelName).create(input);
}
+ private void createLabelWithBranch(
+ String labelName,
+ String function,
+ boolean ignoreSelfApproval,
+ ImmutableList<String> refPatterns)
+ throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.name = labelName;
+ input.function = function;
+ input.ignoreSelfApproval = ignoreSelfApproval;
+ input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+ input.branches = refPatterns;
+ gApi.projects().name(project.get()).label(labelName).create(input);
+ }
+
@CanIgnoreReturnValue
private SubmitRequirementApi createSubmitRequirement(
String name, String submitExpression, boolean canOverride) throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/rest/TraceIT.java b/javatests/com/google/gerrit/acceptance/rest/TraceIT.java
index 64e3762..9710bf4 100644
--- a/javatests/com/google/gerrit/acceptance/rest/TraceIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/TraceIT.java
@@ -43,6 +43,7 @@
import com.google.gerrit.extensions.events.ChangeIndexedListener;
import com.google.gerrit.httpd.restapi.ParameterParser;
import com.google.gerrit.httpd.restapi.RestApiServlet;
+import com.google.gerrit.server.ExceptionHook;
import com.google.gerrit.server.events.CommitReceivedEvent;
import com.google.gerrit.server.git.WorkQueue;
import com.google.gerrit.server.git.validators.CommitValidationException;
@@ -801,7 +802,7 @@
@Test
@GerritConfig(name = "retry.retryWithTraceOnFailure", value = "true")
- public void noAutoRetryIfExceptionCausesNormalRetrying() throws Exception {
+ public void autoRetryWithTrace() throws Exception {
String changeId = createChange().getChangeId();
approve(changeId);
@@ -811,6 +812,49 @@
RestResponse response = adminRestSession.post("/changes/" + changeId + "/submit");
assertThat(response.getStatusCode()).isEqualTo(SC_INTERNAL_SERVER_ERROR);
assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).startsWith("retry-on-failure-");
+ assertThat(traceSubmitRule.traceId).startsWith("retry-on-failure-");
+ assertThat(traceSubmitRule.isLoggingForced).isTrue();
+ }
+ }
+
+ @Test
+ @GerritConfig(name = "retry.retryWithTraceOnFailure", value = "true")
+ public void noAutoRetryIfExceptionCausesNormalRetrying() throws Exception {
+ String changeId = createChange().getChangeId();
+ approve(changeId);
+
+ TraceSubmitRule traceSubmitRule = new TraceSubmitRule();
+ traceSubmitRule.failAlways = true;
+ try (Registration registration =
+ extensionRegistry
+ .newRegistration()
+ .add(traceSubmitRule)
+ .add(
+ new ExceptionHook() {
+ @Override
+ public boolean shouldRetry(String actionType, String actionName, Throwable t) {
+ return true;
+ }
+ })) {
+ RestResponse response = adminRestSession.post("/changes/" + changeId + "/submit");
+ assertThat(response.getStatusCode()).isEqualTo(SC_INTERNAL_SERVER_ERROR);
+ assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
+ assertThat(traceSubmitRule.traceId).isNull();
+ assertThat(traceSubmitRule.isLoggingForced).isFalse();
+ }
+ }
+
+ @Test
+ public void noAutoRetryWithTraceIfDisabled() throws Exception {
+ String changeId = createChange().getChangeId();
+ approve(changeId);
+
+ TraceSubmitRule traceSubmitRule = new TraceSubmitRule();
+ traceSubmitRule.failOnce = true;
+ try (Registration registration = extensionRegistry.newRegistration().add(traceSubmitRule)) {
+ RestResponse response = adminRestSession.post("/changes/" + changeId + "/submit");
+ assertThat(response.getStatusCode()).isEqualTo(SC_INTERNAL_SERVER_ERROR);
+ assertThat(response.getHeader(RestApiServlet.X_GERRIT_TRACE)).isNull();
assertThat(traceSubmitRule.traceId).isNull();
assertThat(traceSubmitRule.isLoggingForced).isFalse();
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java b/javatests/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java
index eb827c0..3531d1c 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java
@@ -20,6 +20,8 @@
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.capabilityKey;
+import static com.google.gerrit.entities.RefNames.changeMetaRef;
+import static com.google.gerrit.entities.RefNames.patchSetRef;
import static com.google.gerrit.extensions.client.ListChangesOption.MESSAGES;
import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
@@ -28,11 +30,13 @@
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.RestResponse;
import com.google.gerrit.acceptance.RestSession;
import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.UseLocalDisk;
import com.google.gerrit.acceptance.config.GerritConfig;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
@@ -43,8 +47,10 @@
import com.google.gerrit.entities.LabelId;
import com.google.gerrit.entities.LabelType;
import com.google.gerrit.entities.Patch;
+import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.PatchSetApproval;
import com.google.gerrit.entities.Permission;
+import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RobotComment;
import com.google.gerrit.extensions.api.changes.DraftInput;
import com.google.gerrit.extensions.api.changes.ReviewInput;
@@ -55,6 +61,7 @@
import com.google.gerrit.extensions.api.changes.SubmitInput;
import com.google.gerrit.extensions.api.groups.GroupInput;
import com.google.gerrit.extensions.client.Side;
+import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.ChangeMessageInfo;
@@ -71,8 +78,15 @@
import com.google.gerrit.server.project.testing.TestLabels;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.inject.Inject;
+import java.io.File;
+import java.io.IOException;
import org.apache.http.Header;
import org.apache.http.message.BasicHeader;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.ReflogEntry;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -105,28 +119,246 @@
}
@Test
+ @UseLocalDisk
public void voteOnBehalfOf() throws Exception {
allowCodeReviewOnBehalfOf();
+ TestAccount realUser = admin;
+ TestAccount impersonatedUser = user;
PushOneCommit.Result r = createChange();
RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
+ try (Repository repo = repoManager.openRepository(project)) {
+ String changeMetaRef = changeMetaRef(r.getChange().getId());
+ createRefLogFileIfMissing(repo, changeMetaRef);
+
+ ReviewInput in = ReviewInput.recommend();
+ in.onBehalfOf = impersonatedUser.id().toString();
+ in.message = "Message on behalf of";
+ revision.review(in);
+
+ PatchSetApproval psa = Iterables.getOnlyElement(r.getChange().approvals().values());
+ assertThat(psa.patchSetId().get()).isEqualTo(1);
+ assertThat(psa.label()).isEqualTo("Code-Review");
+ assertThat(psa.accountId()).isEqualTo(impersonatedUser.id());
+ assertThat(psa.value()).isEqualTo(1);
+ assertThat(psa.realAccountId()).isEqualTo(realUser.id());
+
+ ChangeData cd = r.getChange();
+ ChangeMessage m = Iterables.getLast(cmUtil.byChange(cd.notes()));
+ assertThat(m.getMessage()).endsWith(in.message);
+ assertThat(m.getAuthor()).isEqualTo(impersonatedUser.id());
+ assertThat(m.getRealAuthor()).isEqualTo(realUser.id());
+
+ // The change meta commit is created by the server and has the impersonated user as the
+ // author.
+ // Person idents of users in NoteDb commits are obfuscated due to privacy reasons.
+ RevCommit changeMetaCommit = projectOperations.project(project).getHead(changeMetaRef);
+ assertThat(changeMetaCommit.getCommitterIdent().getEmailAddress())
+ .isEqualTo(serverIdent.get().getEmailAddress());
+ assertThat(changeMetaCommit.getAuthorIdent().getEmailAddress())
+ .isEqualTo(changeNoteUtil.getAccountIdAsEmailAddress(impersonatedUser.id()));
+
+ // The ref log for the change meta ref records the impersonated user.
+ ReflogEntry changeMetaRefLogEntry = repo.getReflogReader(changeMetaRef).getLastEntry();
+ assertThat(changeMetaRefLogEntry.getWho().getEmailAddress())
+ .isEqualTo(impersonatedUser.email());
+ }
+ }
+
+ @Test
+ public void overrideImpersonatedVoteWithOtherImpersonatedVote_sameValue() throws Exception {
+ allowCodeReviewOnBehalfOf();
+ TestAccount realUser = admin;
+ TestAccount realUser2 = admin2;
+ TestAccount impersonatedUser = user;
+ PushOneCommit.Result r = createChange();
+ RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
+
+ // realUser votes Code-Review+1 on behalf of impersonatedUser
ReviewInput in = ReviewInput.recommend();
- in.onBehalfOf = user.id().toString();
+ in.onBehalfOf = impersonatedUser.id().toString();
in.message = "Message on behalf of";
revision.review(in);
PatchSetApproval psa = Iterables.getOnlyElement(r.getChange().approvals().values());
assertThat(psa.patchSetId().get()).isEqualTo(1);
assertThat(psa.label()).isEqualTo("Code-Review");
- assertThat(psa.accountId()).isEqualTo(user.id());
+ assertThat(psa.accountId()).isEqualTo(impersonatedUser.id());
assertThat(psa.value()).isEqualTo(1);
- assertThat(psa.realAccountId()).isEqualTo(admin.id());
+ assertThat(psa.realAccountId()).isEqualTo(realUser.id());
ChangeData cd = r.getChange();
ChangeMessage m = Iterables.getLast(cmUtil.byChange(cd.notes()));
assertThat(m.getMessage()).endsWith(in.message);
- assertThat(m.getAuthor()).isEqualTo(user.id());
- assertThat(m.getRealAuthor()).isEqualTo(admin.id());
+ assertThat(m.getAuthor()).isEqualTo(impersonatedUser.id());
+ assertThat(m.getRealAuthor()).isEqualTo(realUser.id());
+
+ // realUser2 votes Code-Review+1 on behalf of impersonatedUser, this should override the
+ // impersonated Code-Review+1 of realUser with an impersonated Code-Review+1 of realUser2
+ requestScopeOperations.setApiUser(realUser2.id());
+ in = ReviewInput.recommend();
+ in.onBehalfOf = impersonatedUser.id().toString();
+ in.message = "Another message on behalf of";
+ gApi.changes().id(r.getChangeId()).current().review(in);
+
+ psa = Iterables.getOnlyElement(r.getChange().approvals().values());
+ assertThat(psa.patchSetId().get()).isEqualTo(1);
+ assertThat(psa.label()).isEqualTo("Code-Review");
+ assertThat(psa.accountId()).isEqualTo(impersonatedUser.id());
+ assertThat(psa.value()).isEqualTo(1);
+ assertThat(psa.realAccountId()).isEqualTo(realUser2.id());
+
+ cd = r.getChange();
+ m = Iterables.getLast(cmUtil.byChange(cd.notes()));
+ assertThat(m.getMessage()).endsWith(in.message);
+ assertThat(m.getAuthor()).isEqualTo(impersonatedUser.id());
+ assertThat(m.getRealAuthor()).isEqualTo(realUser2.id());
+ }
+
+ @Test
+ public void overrideImpersonatedVoteWithOtherImpersonatedVote_differentValue() throws Exception {
+ allowCodeReviewOnBehalfOf();
+ TestAccount realUser = admin;
+ TestAccount realUser2 = admin2;
+ TestAccount impersonatedUser = user;
+ PushOneCommit.Result r = createChange();
+ RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
+
+ // realUser votes Code-Review+1 on behalf of impersonatedUser
+ ReviewInput in = ReviewInput.recommend();
+ in.onBehalfOf = impersonatedUser.id().toString();
+ in.message = "Message on behalf of";
+ revision.review(in);
+
+ PatchSetApproval psa = Iterables.getOnlyElement(r.getChange().approvals().values());
+ assertThat(psa.patchSetId().get()).isEqualTo(1);
+ assertThat(psa.label()).isEqualTo("Code-Review");
+ assertThat(psa.accountId()).isEqualTo(impersonatedUser.id());
+ assertThat(psa.value()).isEqualTo(1);
+ assertThat(psa.realAccountId()).isEqualTo(realUser.id());
+
+ ChangeData cd = r.getChange();
+ ChangeMessage m = Iterables.getLast(cmUtil.byChange(cd.notes()));
+ assertThat(m.getMessage()).endsWith(in.message);
+ assertThat(m.getAuthor()).isEqualTo(impersonatedUser.id());
+ assertThat(m.getRealAuthor()).isEqualTo(realUser.id());
+
+ // realUser2 votes Code-Review-1 on behalf of impersonatedUser, this should override the
+ // impersonated Code-Review+1 of realUser with an impersonated Code-Review-1 of realUser2
+ requestScopeOperations.setApiUser(realUser2.id());
+ in = ReviewInput.dislike();
+ in.onBehalfOf = impersonatedUser.id().toString();
+ in.message = "Another message on behalf of";
+ gApi.changes().id(r.getChangeId()).current().review(in);
+
+ psa = Iterables.getOnlyElement(r.getChange().approvals().values());
+ assertThat(psa.patchSetId().get()).isEqualTo(1);
+ assertThat(psa.label()).isEqualTo("Code-Review");
+ assertThat(psa.accountId()).isEqualTo(impersonatedUser.id());
+ assertThat(psa.value()).isEqualTo(-1);
+ assertThat(psa.realAccountId()).isEqualTo(realUser2.id());
+
+ cd = r.getChange();
+ m = Iterables.getLast(cmUtil.byChange(cd.notes()));
+ assertThat(m.getMessage()).endsWith(in.message);
+ assertThat(m.getAuthor()).isEqualTo(impersonatedUser.id());
+ assertThat(m.getRealAuthor()).isEqualTo(realUser2.id());
+ }
+
+ @Test
+ public void overrideImpersonatedVoteWithNonImpersonatedVote_sameValue() throws Exception {
+ allowCodeReviewOnBehalfOf();
+ TestAccount realUser = admin;
+ TestAccount impersonatedUser = user;
+ PushOneCommit.Result r = createChange();
+ RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
+
+ // realUser votes Code-Review+1 on behalf of impersonatedUser
+ ReviewInput in = ReviewInput.recommend();
+ in.onBehalfOf = impersonatedUser.id().toString();
+ in.message = "Message on behalf of";
+ revision.review(in);
+
+ PatchSetApproval psa = Iterables.getOnlyElement(r.getChange().approvals().values());
+ assertThat(psa.patchSetId().get()).isEqualTo(1);
+ assertThat(psa.label()).isEqualTo("Code-Review");
+ assertThat(psa.accountId()).isEqualTo(impersonatedUser.id());
+ assertThat(psa.value()).isEqualTo(1);
+ assertThat(psa.realAccountId()).isEqualTo(realUser.id());
+
+ ChangeData cd = r.getChange();
+ ChangeMessage m = Iterables.getLast(cmUtil.byChange(cd.notes()));
+ assertThat(m.getMessage()).endsWith(in.message);
+ assertThat(m.getAuthor()).isEqualTo(impersonatedUser.id());
+ assertThat(m.getRealAuthor()).isEqualTo(realUser.id());
+
+ // impersonatedUser votes Code-Review+1 themselves, this should override the impersonated
+ // Code-Review+1 with a non-impersonated Code-Review+1
+ requestScopeOperations.setApiUser(impersonatedUser.id());
+ in = ReviewInput.recommend();
+ in.message = "Message";
+ gApi.changes().id(r.getChangeId()).current().review(in);
+
+ psa = Iterables.getOnlyElement(r.getChange().approvals().values());
+ assertThat(psa.patchSetId().get()).isEqualTo(1);
+ assertThat(psa.label()).isEqualTo("Code-Review");
+ assertThat(psa.accountId()).isEqualTo(impersonatedUser.id());
+ assertThat(psa.value()).isEqualTo(1);
+ assertThat(psa.realAccountId()).isEqualTo(impersonatedUser.id());
+
+ cd = r.getChange();
+ m = Iterables.getLast(cmUtil.byChange(cd.notes()));
+ assertThat(m.getMessage()).endsWith(in.message);
+ assertThat(m.getAuthor()).isEqualTo(impersonatedUser.id());
+ assertThat(m.getRealAuthor()).isEqualTo(impersonatedUser.id());
+ }
+
+ @Test
+ public void overrideImpersonatedVoteWithNonImpersonatedVote_differentValue() throws Exception {
+ allowCodeReviewOnBehalfOf();
+ TestAccount realUser = admin;
+ TestAccount impersonatedUser = user;
+ PushOneCommit.Result r = createChange();
+ RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
+
+ // realUser votes Code-Review+1 on behalf of impersonatedUser
+ ReviewInput in = ReviewInput.recommend();
+ in.onBehalfOf = impersonatedUser.id().toString();
+ in.message = "Message on behalf of";
+ revision.review(in);
+
+ PatchSetApproval psa = Iterables.getOnlyElement(r.getChange().approvals().values());
+ assertThat(psa.patchSetId().get()).isEqualTo(1);
+ assertThat(psa.label()).isEqualTo("Code-Review");
+ assertThat(psa.accountId()).isEqualTo(impersonatedUser.id());
+ assertThat(psa.value()).isEqualTo(1);
+ assertThat(psa.realAccountId()).isEqualTo(realUser.id());
+
+ ChangeData cd = r.getChange();
+ ChangeMessage m = Iterables.getLast(cmUtil.byChange(cd.notes()));
+ assertThat(m.getMessage()).endsWith(in.message);
+ assertThat(m.getAuthor()).isEqualTo(impersonatedUser.id());
+ assertThat(m.getRealAuthor()).isEqualTo(realUser.id());
+
+ // impersonatedUser votes Code-Review-1 themselves, this should override the impersonated
+ // Code-Review+1 with a non-impersonated Code-Review-1
+ requestScopeOperations.setApiUser(impersonatedUser.id());
+ in = ReviewInput.dislike();
+ in.message = "Message";
+ gApi.changes().id(r.getChangeId()).current().review(in);
+
+ psa = Iterables.getOnlyElement(r.getChange().approvals().values());
+ assertThat(psa.patchSetId().get()).isEqualTo(1);
+ assertThat(psa.label()).isEqualTo("Code-Review");
+ assertThat(psa.accountId()).isEqualTo(impersonatedUser.id());
+ assertThat(psa.value()).isEqualTo(-1);
+ assertThat(psa.realAccountId()).isEqualTo(impersonatedUser.id());
+
+ cd = r.getChange();
+ m = Iterables.getLast(cmUtil.byChange(cd.notes()));
+ assertThat(m.getMessage()).endsWith(in.message);
+ assertThat(m.getAuthor()).isEqualTo(impersonatedUser.id());
+ assertThat(m.getRealAuthor()).isEqualTo(impersonatedUser.id());
}
@Test
@@ -342,21 +574,120 @@
}
@Test
- public void submitOnBehalfOf() throws Exception {
- allowSubmitOnBehalfOf();
- PushOneCommit.Result r = createChange();
+ @UseLocalDisk
+ public void submitOnBehalfOf_mergeAlways() throws Exception {
+ TestAccount realUser = admin;
+ TestAccount impersonatedUser = admin2;
+
+ // Create a project with MERGE_ALWAYS submit strategy so that a merge commit is created on
+ // submit and we can verify its committer and author and the ref log for the update of the
+ // target branch.
+ Project.NameKey project =
+ projectOperations.newProject().submitType(SubmitType.MERGE_ALWAYS).create();
+
+ testSubmitOnBehalfOf(project, realUser, impersonatedUser);
+
+ // The merge commit is created by the server and has the impersonated user as the author.
+ RevCommit mergeCommit = projectOperations.project(project).getHead("refs/heads/master");
+ assertThat(mergeCommit.getCommitterIdent().getEmailAddress())
+ .isEqualTo(serverIdent.get().getEmailAddress());
+ assertThat(mergeCommit.getAuthorIdent().getEmailAddress()).isEqualTo(impersonatedUser.email());
+
+ // The ref log for the target branch records the impersonated user.
+ try (Repository repo = repoManager.openRepository(project)) {
+ ReflogEntry targetBranchRefLogEntry =
+ repo.getReflogReader("refs/heads/master").getLastEntry();
+ assertThat(targetBranchRefLogEntry.getWho().getEmailAddress())
+ .isEqualTo(impersonatedUser.email());
+ }
+ }
+
+ @Test
+ @UseLocalDisk
+ public void submitOnBehalfOf_rebaseAlways() throws Exception {
+ TestAccount realUser = admin;
+ TestAccount impersonatedUser = admin2;
+
+ // Create a project with REBASE_ALWAYS submit strategy so that a new patch set is created on
+ // submit and we can verify its committer and author and the ref log for the update of the
+ // patch set ref and the target branch.
+ Project.NameKey project =
+ projectOperations.newProject().submitType(SubmitType.REBASE_ALWAYS).create();
+
+ ChangeData cd = testSubmitOnBehalfOf(project, realUser, impersonatedUser);
+
+ // Rebase on submit is expected to create a new patch set.
+ assertThat(cd.currentPatchSet().id().get()).isEqualTo(2);
+
+ // The patch set commit is created by the impersonated user and has the real user as the author.
+ // Recording the real user as the author seems to a bug, we would expect the author to be the
+ // impersonated user.
+ RevCommit newPatchSetCommit =
+ projectOperations.project(project).getHead(cd.currentPatchSet().refName());
+ assertThat(newPatchSetCommit.getCommitterIdent().getEmailAddress())
+ .isEqualTo(impersonatedUser.email());
+ assertThat(newPatchSetCommit.getAuthorIdent().getEmailAddress()).isEqualTo(realUser.email());
+
+ try (Repository repo = repoManager.openRepository(project)) {
+ // The ref log for the patch set ref records the impersonated user.
+ ReflogEntry patchSetRefLogEntry =
+ repo.getReflogReader(cd.currentPatchSet().refName()).getLastEntry();
+ assertThat(patchSetRefLogEntry.getWho().getEmailAddress())
+ .isEqualTo(impersonatedUser.email());
+
+ // The ref log for the target branch records the impersonated user.
+ ReflogEntry targetBranchRefLogEntry =
+ repo.getReflogReader("refs/heads/master").getLastEntry();
+ assertThat(targetBranchRefLogEntry.getWho().getEmailAddress())
+ .isEqualTo(impersonatedUser.email());
+ }
+ }
+
+ @CanIgnoreReturnValue
+ private ChangeData testSubmitOnBehalfOf(
+ Project.NameKey project, TestAccount realUser, TestAccount impersonatedUser)
+ throws Exception {
+ allowSubmitOnBehalfOf(project);
+
+ TestRepository<InMemoryRepository> testRepo = cloneProject(project, realUser);
+
+ PushOneCommit.Result r = createChange(testRepo);
String changeId = project.get() + "~master~" + r.getChangeId();
gApi.changes().id(changeId).current().review(ReviewInput.approve());
SubmitInput in = new SubmitInput();
- in.onBehalfOf = admin2.email();
- gApi.changes().id(changeId).current().submit(in);
+ in.onBehalfOf = impersonatedUser.email();
- ChangeData cd = r.getChange();
- assertThat(cd.change().isMerged()).isTrue();
- PatchSetApproval submitter =
- approvalsUtil.getSubmitter(cd.notes(), cd.change().currentPatchSetId());
- assertThat(submitter.accountId()).isEqualTo(admin2.id());
- assertThat(submitter.realAccountId()).isEqualTo(admin.id());
+ try (Repository repo = repoManager.openRepository(project)) {
+ String changeMetaRef = changeMetaRef(r.getChange().getId());
+ createRefLogFileIfMissing(repo, changeMetaRef);
+ createRefLogFileIfMissing(repo, "refs/heads/master");
+ createRefLogFileIfMissing(repo, patchSetRef(PatchSet.id(r.getChange().getId(), 2)));
+
+ gApi.changes().id(changeId).current().submit(in);
+
+ ChangeData cd = r.getChange();
+ assertThat(cd.change().isMerged()).isTrue();
+ PatchSetApproval submitter =
+ approvalsUtil.getSubmitter(cd.notes(), cd.change().currentPatchSetId());
+ assertThat(submitter.accountId()).isEqualTo(impersonatedUser.id());
+ assertThat(submitter.realAccountId()).isEqualTo(realUser.id());
+
+ // The change meta commit is created by the server and has the impersonated user as the
+ // author.
+ // Person idents of users in NoteDb commits are obfuscated due to privacy reasons.
+ RevCommit changeMetaCommit = projectOperations.project(project).getHead(changeMetaRef);
+ assertThat(changeMetaCommit.getCommitterIdent().getEmailAddress())
+ .isEqualTo(serverIdent.get().getEmailAddress());
+ assertThat(changeMetaCommit.getAuthorIdent().getEmailAddress())
+ .isEqualTo(changeNoteUtil.getAccountIdAsEmailAddress(impersonatedUser.id()));
+
+ // The ref log for the change meta ref records the impersonated user.
+ ReflogEntry changeMetaRefLogEntry = repo.getReflogReader(changeMetaRef).getLastEntry();
+ assertThat(changeMetaRefLogEntry.getWho().getEmailAddress())
+ .isEqualTo(impersonatedUser.email());
+
+ return cd;
+ }
}
@Test
@@ -591,6 +922,10 @@
}
private void allowSubmitOnBehalfOf() throws Exception {
+ allowSubmitOnBehalfOf(project);
+ }
+
+ private void allowSubmitOnBehalfOf(Project.NameKey project) throws Exception {
String heads = "refs/heads/*";
projectOperations
.project(project)
@@ -630,4 +965,12 @@
private static Header runAsHeader(Object user) {
return new BasicHeader("X-Gerrit-RunAs", user.toString());
}
+
+ private void createRefLogFileIfMissing(Repository repo, String ref) throws IOException {
+ File log = new File(repo.getDirectory(), "logs/" + ref);
+ if (!log.exists()) {
+ log.getParentFile().mkdirs();
+ assertThat(log.createNewFile()).isTrue();
+ }
+ }
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByRebase.java b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByRebase.java
index 81c098f..aeebc10 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByRebase.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByRebase.java
@@ -234,11 +234,11 @@
PushOneCommit.Result change2 = createChange("Change 2", "a.txt", "other content");
submitWithConflict(
change2.getChangeId(),
- "Cannot rebase "
- + change2.getCommit().name()
- + ": The change could not be rebased due to a conflict during merge.\n\n"
- + "merge conflict(s):\n"
- + "a.txt");
+ String.format(
+ "Cannot rebase %s: Change %s could not be rebased due to a conflict during merge.\n\n"
+ + "merge conflict(s):\n"
+ + "a.txt",
+ change2.getCommit().name(), change2.getChange().getId()));
RevCommit head = projectOperations.project(project).getHead("master");
assertThat(head).isEqualTo(headAfterFirstSubmit);
assertCurrentRevision(change2.getChangeId(), 1, change2.getCommit());
@@ -362,12 +362,11 @@
submitWithConflict(
change2.getChangeId(),
- "Cannot rebase "
- + change2.getCommit().getName()
- + ": "
- + "The change could not be rebased due to a conflict during merge.\n\n"
- + "merge conflict(s):\n"
- + "fileName 2");
+ String.format(
+ "Cannot rebase %s: Change %s could not be rebased due to a conflict during merge.\n\n"
+ + "merge conflict(s):\n"
+ + "fileName 2",
+ change2.getCommit().name(), change2.getChange().getId()));
assertThat(projectOperations.project(project).getHead("master")).isEqualTo(headAfterChange1);
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/DeleteVoteIT.java b/javatests/com/google/gerrit/acceptance/rest/change/DeleteVoteIT.java
index c57d285..016b1e6 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/DeleteVoteIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/DeleteVoteIT.java
@@ -15,16 +15,24 @@
package com.google.gerrit.acceptance.rest.change;
import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabelRemoval;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.blockLabelRemoval;
import static com.google.gerrit.extensions.client.ReviewerState.REVIEWER;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.LabelId;
+import com.google.gerrit.entities.Permission;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.common.ChangeInfo;
@@ -39,56 +47,141 @@
import org.junit.Test;
public class DeleteVoteIT extends AbstractDaemonTest {
+ @Inject private ProjectOperations projectOperations;
@Inject private RequestScopeOperations requestScopeOperations;
@Test
- public void deleteVoteOnChange() throws Exception {
- deleteVote(false);
+ public void deleteVoteOnChange_withRemoveLabelPermission() throws Exception {
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(
+ allowLabelRemoval(LabelId.CODE_REVIEW)
+ .ref("refs/heads/*")
+ .group(REGISTERED_USERS)
+ .range(-2, 2))
+ .add(block(Permission.REMOVE_REVIEWER).ref("refs/*").group(REGISTERED_USERS))
+ .update();
+ verifyDeleteVote(false);
}
@Test
- public void deleteVoteOnRevision() throws Exception {
- deleteVote(true);
+ public void deleteVoteOnChange_withRemoveReviewerPermission() throws Exception {
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(
+ blockLabelRemoval(LabelId.CODE_REVIEW)
+ .ref("refs/heads/*")
+ .group(REGISTERED_USERS)
+ .range(-2, 2))
+ .add(allow(Permission.REMOVE_REVIEWER).ref("refs/*").group(REGISTERED_USERS))
+ .update();
+ verifyDeleteVote(false);
}
- private void deleteVote(boolean onRevisionLevel) throws Exception {
+ @Test
+ public void deleteVoteOnChange_noPermission() throws Exception {
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(
+ blockLabelRemoval(LabelId.CODE_REVIEW)
+ .ref("refs/heads/*")
+ .group(REGISTERED_USERS)
+ .range(-2, 2))
+ .add(block(Permission.REMOVE_REVIEWER).ref("refs/*").group(REGISTERED_USERS))
+ .update();
+ verifyCannotDeleteVote(false);
+ }
+
+ @Test
+ public void deleteVoteOnRevision_withRemoveLabelPermission() throws Exception {
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(
+ allowLabelRemoval(LabelId.CODE_REVIEW)
+ .ref("refs/heads/*")
+ .group(REGISTERED_USERS)
+ .range(-2, 2))
+ .add(block(Permission.REMOVE_REVIEWER).ref("refs/*").group(REGISTERED_USERS))
+ .update();
+ verifyDeleteVote(true);
+ }
+
+ @Test
+ public void deleteVoteOnRevision_withRemoveReviewerPermission() throws Exception {
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(
+ blockLabelRemoval(LabelId.CODE_REVIEW)
+ .ref("refs/heads/*")
+ .group(REGISTERED_USERS)
+ .range(-2, 2))
+ .add(allow(Permission.REMOVE_REVIEWER).ref("refs/*").group(REGISTERED_USERS))
+ .update();
+ verifyDeleteVote(true);
+ }
+
+ @Test
+ public void deleteVoteOnRevision_noPermission() throws Exception {
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(
+ blockLabelRemoval(LabelId.CODE_REVIEW)
+ .ref("refs/heads/*")
+ .group(REGISTERED_USERS)
+ .range(-2, 2))
+ .add(block(Permission.REMOVE_REVIEWER).ref("refs/*").group(REGISTERED_USERS))
+ .update();
+ verifyCannotDeleteVote(true);
+ }
+
+ private void verifyDeleteVote(boolean onRevisionLevel) throws Exception {
PushOneCommit.Result r = createChange();
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
PushOneCommit.Result r2 = amendChange(r.getChangeId());
- requestScopeOperations.setApiUser(user.id());
+ requestScopeOperations.setApiUser(admin.id());
+ recommend(r.getChangeId());
+
+ TestAccount user2 = accountCreator.user2();
+ requestScopeOperations.setApiUser(user2.id());
recommend(r.getChangeId());
sender.clear();
- String endPoint =
+ String deleteAdminVoteEndPoint =
"/changes/"
+ r.getChangeId()
+ (onRevisionLevel ? ("/revisions/" + r2.getCommit().getName()) : "")
+ "/reviewers/"
- + user.id().toString()
+ + admin.id().toString()
+ "/votes/Code-Review";
- RestResponse response = adminRestSession.delete(endPoint);
+ RestResponse response = userRestSession.delete(deleteAdminVoteEndPoint);
response.assertNoContent();
List<FakeEmailSender.Message> messages = sender.getMessages();
assertThat(messages).hasSize(1);
FakeEmailSender.Message msg = messages.get(0);
- assertThat(msg.rcpt()).containsExactly(user.getNameEmail());
- assertThat(msg.body()).contains(admin.fullName() + " has removed a vote from this change.");
+ assertThat(msg.rcpt()).containsExactly(admin.getNameEmail(), user2.getNameEmail());
+ assertThat(msg.body()).contains(user.fullName() + " has removed a vote from this change.");
assertThat(msg.body())
- .contains("Removed Code-Review+1 by " + user.fullName() + " <" + user.email() + ">\n");
+ .contains("Removed Code-Review+1 by " + admin.fullName() + " <" + admin.email() + ">\n");
- endPoint =
+ String viewVotesEndPoint =
"/changes/"
+ r.getChangeId()
+ (onRevisionLevel ? ("/revisions/" + r2.getCommit().getName()) : "")
+ "/reviewers/"
- + user.id().toString()
+ + admin.id().toString()
+ "/votes";
- response = adminRestSession.get(endPoint);
+ response = userRestSession.get(viewVotesEndPoint);
response.assertOK();
Map<String, Short> m =
@@ -99,14 +192,38 @@
ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
ChangeMessageInfo message = Iterables.getLast(c.messages);
- assertThat(message.author._accountId).isEqualTo(admin.id().get());
+ assertThat(message.author._accountId).isEqualTo(user.id().get());
assertThat(message.message)
.isEqualTo(
String.format(
"Removed Code-Review+1 by %s\n",
- AccountTemplateUtil.getAccountTemplate(user.id())));
+ AccountTemplateUtil.getAccountTemplate(admin.id())));
assertThat(getReviewers(c.reviewers.get(REVIEWER)))
- .containsExactlyElementsIn(ImmutableSet.of(admin.id(), user.id()));
+ .containsExactlyElementsIn(ImmutableSet.of(user2.id(), admin.id()));
+ }
+
+ private void verifyCannotDeleteVote(boolean onRevisionLevel) throws Exception {
+ PushOneCommit.Result r = createChange();
+ gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
+
+ PushOneCommit.Result r2 = amendChange(r.getChangeId());
+
+ requestScopeOperations.setApiUser(admin.id());
+ recommend(r.getChangeId());
+
+ sender.clear();
+ String deleteAdminVoteEndPoint =
+ "/changes/"
+ + r.getChangeId()
+ + (onRevisionLevel ? ("/revisions/" + r2.getCommit().getName()) : "")
+ + "/reviewers/"
+ + admin.id().toString()
+ + "/votes/Code-Review";
+
+ RestResponse response = userRestSession.delete(deleteAdminVoteEndPoint);
+ response.assertForbidden();
+
+ assertThat(sender.getMessages()).isEmpty();
}
private Iterable<Account.Id> getReviewers(Collection<AccountInfo> r) {
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/CreateChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/project/CreateChangeIT.java
index 0c221aa..7b42d93 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/CreateChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/CreateChangeIT.java
@@ -14,6 +14,7 @@
package com.google.gerrit.acceptance.rest.project;
+import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth8.assertThat;
import static com.google.gerrit.entities.RefNames.REFS_HEADS;
@@ -21,6 +22,7 @@
import com.google.gerrit.acceptance.RestResponse;
import com.google.gerrit.extensions.api.projects.BranchInput;
import com.google.gerrit.extensions.common.ChangeInput;
+import com.google.gerrit.extensions.restapi.IdString;
import org.junit.Test;
public class CreateChangeIT extends AbstractDaemonTest {
@@ -43,7 +45,44 @@
ChangeInput input = new ChangeInput();
input.branch = "foo";
input.subject = "subject";
- RestResponse cr = adminRestSession.post("/projects/" + project.get() + "/create.change", input);
- cr.assertCreated();
+ RestResponse response =
+ adminRestSession.post("/projects/" + project.get() + "/create.change", input);
+ response.assertCreated();
+ }
+
+ @Test
+ public void nonMatchingProjectIsRejected() throws Exception {
+ ChangeInput input = new ChangeInput();
+ input.project = "non-matching-project";
+ input.branch = "master";
+ input.subject = "subject";
+ RestResponse response =
+ adminRestSession.post("/projects/" + project.get() + "/create.change", input);
+ response.assertBadRequest();
+ assertThat(response.getEntityContent()).isEqualTo("project must match URL");
+ }
+
+ @Test
+ public void matchingProjectIsAccepted() throws Exception {
+ ChangeInput input = new ChangeInput();
+ input.project = project.get();
+ input.branch = "master";
+ input.subject = "subject";
+ RestResponse response =
+ adminRestSession.post("/projects/" + project.get() + "/create.change", input);
+ response.assertCreated();
+ }
+
+ @Test
+ public void matchingProjectWithTrailingSlashIsAccepted() throws Exception {
+ ChangeInput input = new ChangeInput();
+ input.project = project.get() + "/";
+ input.branch = "master";
+ input.subject = "subject";
+ RestResponse response =
+ adminRestSession.post(
+ "/projects/" + IdString.fromDecoded(project.get() + "/").encoded() + "/create.change",
+ input);
+ response.assertCreated();
}
}
diff --git a/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java b/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
index 2123ac2..15baa78 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
@@ -1262,7 +1262,7 @@
+ c
+ "/comment/"
+ ps1List.get(0).id
- + " \n"
+ + " :\n"
+ "PS1, Line 1: initial\n"
+ "what happened to this?\n"
+ "\n"
@@ -1274,7 +1274,7 @@
+ c
+ "/comment/"
+ ps1List.get(1).id
- + " \n"
+ + " :\n"
+ "PS1, Line 1: boring\n"
+ "Is it that bad?\n"
+ "\n"
@@ -1288,7 +1288,7 @@
+ c
+ "/comment/"
+ ps2List.get(0).id
- + " \n"
+ + " :\n"
+ "PS2, Line 1: initial content\n"
+ "comment 1 on base\n"
+ "\n"
@@ -1300,7 +1300,7 @@
+ c
+ "/comment/"
+ ps2List.get(1).id
- + " \n"
+ + " :\n"
+ "PS2, Line 2: \n"
+ "comment 2 on base\n"
+ "\n"
@@ -1312,7 +1312,7 @@
+ c
+ "/comment/"
+ ps2List.get(2).id
- + " \n"
+ + " :\n"
+ "PS2, Line 1: interesting\n"
+ "better now\n"
+ "\n"
@@ -1324,7 +1324,7 @@
+ c
+ "/comment/"
+ ps2List.get(3).id
- + " \n"
+ + " :\n"
+ "PS2, Line 2: cntent\n"
+ "typo: content\n"
+ "\n"
diff --git a/javatests/com/google/gerrit/acceptance/server/experiments/ExperimentFeaturesIT.java b/javatests/com/google/gerrit/acceptance/server/experiments/ExperimentFeaturesIT.java
index b2a0ded..e011ffc 100644
--- a/javatests/com/google/gerrit/acceptance/server/experiments/ExperimentFeaturesIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/experiments/ExperimentFeaturesIT.java
@@ -68,10 +68,7 @@
values = {"UiFeature__patchset_comments", "UiFeature__submit_requirements_ui"})
public void configOverride_defaultFeatureDisabled() {
assertThat(experimentFeatures.isFeatureEnabled("enabledFeature")).isTrue();
- assertThat(
- experimentFeatures.isFeatureEnabled(
- ExperimentFeaturesConstants.UI_FEATURE_PATCHSET_COMMENTS))
- .isFalse();
+ assertThat(experimentFeatures.isFeatureEnabled("UiFeature__patchset_comments")).isFalse();
assertThat(experimentFeatures.getEnabledExperimentFeatures()).containsExactly("enabledFeature");
}
}
diff --git a/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java b/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
index 1900158..cf1eee0 100644
--- a/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
@@ -22,6 +22,7 @@
import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.config.GerritConfig;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
import com.google.gerrit.entities.AccountGroup;
@@ -348,6 +349,163 @@
}
@Test
+ public void noNotificationForWatchKeywordWhenKeywordMatchesChangeOwner() throws Exception {
+ String watchedProject = projectOperations.newProject().create().get();
+ requestScopeOperations.setApiUser(user.id());
+
+ // watch keyword in project as user
+ watch(watchedProject, admin.email());
+
+ // push a change with owner=keyword -> should not trigger email notification
+ requestScopeOperations.setApiUser(admin.id());
+ TestRepository<InMemoryRepository> watchedRepo =
+ cloneProject(Project.nameKey(watchedProject), admin);
+ PushOneCommit.Result r =
+ pushFactory
+ .create(admin.newIdent(), watchedRepo, "subject", "a.txt", "a1")
+ .to("refs/for/master");
+ r.assertOkStatus();
+
+ // assert email notification for user
+ assertThat(sender.getMessages()).isEmpty();
+ }
+
+ @Test
+ public void noNotificationForWatchKeywordWhenKeywordMatchesChangeReviewer() throws Exception {
+ TestAccount user2 = accountCreator.user2();
+ String watchedProject = projectOperations.newProject().create().get();
+ requestScopeOperations.setApiUser(user.id());
+
+ // watch keyword in project as user
+ watch(watchedProject, user2.email());
+
+ requestScopeOperations.setApiUser(admin.id());
+ TestRepository<InMemoryRepository> watchedRepo =
+ cloneProject(Project.nameKey(watchedProject), admin);
+ PushOneCommit.Result r =
+ pushFactory
+ .create(admin.newIdent(), watchedRepo, "subject", "a.txt", "a1")
+ .to("refs/for/master");
+ r.assertOkStatus();
+ sender.clear();
+
+ // Add reviewer=keyword -> should trigger email notification only to new reviewer
+ gApi.changes().id(r.getChangeId()).addReviewer(user2.email());
+
+ // assert email notification
+ List<Message> messages = sender.getMessages();
+ assertThat(messages).hasSize(1);
+ Message m = messages.get(0);
+ assertNotifyTo(user2);
+ assertThat(m.body()).contains("Change subject: subject\n");
+ }
+
+ @Test
+ public void watchOwner() throws Exception {
+ String watchedProject = projectOperations.newProject().create().get();
+ requestScopeOperations.setApiUser(user.id());
+
+ // watch keyword in project as user
+ watch(watchedProject, "owner:admin");
+
+ // push a change with keyword -> should trigger email notification
+ requestScopeOperations.setApiUser(admin.id());
+ TestRepository<InMemoryRepository> watchedRepo =
+ cloneProject(Project.nameKey(watchedProject), admin);
+ PushOneCommit.Result r =
+ pushFactory
+ .create(admin.newIdent(), watchedRepo, "subject", "a.txt", "a1")
+ .to("refs/for/master");
+ r.assertOkStatus();
+
+ // assert email notification for user
+ List<Message> messages = sender.getMessages();
+ assertThat(messages).hasSize(1);
+ Message m = messages.get(0);
+ assertThat(m.rcpt()).containsExactly(user.getNameEmail());
+ assertThat(m.body()).contains("Change subject: subject\n");
+ assertThat(m.body()).contains("Gerrit-PatchSet: 1\n");
+ sender.clear();
+ }
+
+ @Test
+ @GerritConfig(name = "accounts.visibility", value = "SAME_GROUP")
+ public void watchNonVisibleOwner() throws Exception {
+ String watchedProject = projectOperations.newProject().create().get();
+ requestScopeOperations.setApiUser(user.id());
+
+ // watch keyword in project as user
+ watch(watchedProject, "owner:admin");
+
+ // Verify that 'user' can't see 'admin'
+ assertThatAccountIsNotVisible(admin);
+
+ // push a change with keyword -> should trigger email notification
+ requestScopeOperations.setApiUser(admin.id());
+ TestRepository<InMemoryRepository> watchedRepo =
+ cloneProject(Project.nameKey(watchedProject), admin);
+ PushOneCommit.Result r =
+ pushFactory
+ .create(admin.newIdent(), watchedRepo, "subject", "a.txt", "a1")
+ .to("refs/for/master");
+ r.assertOkStatus();
+
+ // assert no email notifications for user
+ assertThat(sender.getMessages()).isEmpty();
+ }
+
+ @Test
+ public void watchChangesCommentedBySelf() throws Exception {
+ String watchedProject = projectOperations.newProject().create().get();
+ requestScopeOperations.setApiUser(user.id());
+
+ // user watches all changes that have a comment by themselves
+ watch(watchedProject, "commentby:self");
+
+ // pushing a change as admin should not trigger an email to user
+ requestScopeOperations.setApiUser(admin.id());
+ TestRepository<InMemoryRepository> watchedRepo =
+ cloneProject(Project.nameKey(watchedProject), admin);
+ PushOneCommit.Result r =
+ pushFactory
+ .create(admin.newIdent(), watchedRepo, "subject", "a.txt", "a1")
+ .to("refs/for/master");
+ r.assertOkStatus();
+ assertThat(sender.getMessages()).isEmpty();
+
+ // commenting by admin should not trigger an email to user
+ ReviewInput reviewInput = new ReviewInput();
+ reviewInput.message = "A Comment";
+ gApi.changes().id(r.getChangeId()).current().review(reviewInput);
+ assertThat(sender.getMessages()).isEmpty();
+
+ // commenting by user matches the project watch, but doesn't send an email to user because
+ // CC_ON_OWN_COMMENTS is false by default, so the user is removed from the TO list, but an email
+ // is sent to the admin user
+ requestScopeOperations.setApiUser(user.id());
+ gApi.changes().id(r.getChangeId()).current().review(reviewInput);
+ List<Message> messages = sender.getMessages();
+ assertThat(messages).hasSize(1);
+ Message m = messages.get(0);
+ assertThat(m.rcpt()).containsExactly(admin.getNameEmail());
+ assertThat(m.body()).contains("Change subject: subject\n");
+ assertThat(m.body()).contains("Gerrit-PatchSet: 1\n");
+ sender.clear();
+
+ // commenting by admin now triggers an email to user because the change has a comment by user
+ // and hence matches the project watch
+ requestScopeOperations.setApiUser(admin.id());
+ gApi.changes().id(r.getChangeId()).current().review(reviewInput);
+ messages = sender.getMessages();
+ assertThat(messages).hasSize(1);
+ m = messages.get(0);
+ assertThat(m.rcpt()).containsExactly(user.getNameEmail());
+ assertThat(m.body()).contains("Change subject: subject\n");
+ assertThat(m.body()).contains("Gerrit-PatchSet: 1\n");
+ sender.clear();
+ }
+
+ @Test
public void watchAllProjects() throws Exception {
String anyProject = projectOperations.newProject().create().get();
requestScopeOperations.setApiUser(user.id());
diff --git a/javatests/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImplTest.java b/javatests/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImplTest.java
index c1a7627..661802e 100644
--- a/javatests/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImplTest.java
+++ b/javatests/com/google/gerrit/acceptance/testsuite/project/ProjectOperationsImplTest.java
@@ -18,11 +18,14 @@
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowCapability;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabelRemoval;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.blockLabel;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.blockLabelRemoval;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.capabilityKey;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.deny;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.labelPermissionKey;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.labelRemovalPermissionKey;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.permissionKey;
import static com.google.gerrit.common.data.GlobalCapability.ADMINISTRATE_SERVER;
import static com.google.gerrit.common.data.GlobalCapability.DEFAULT_MAX_QUERY_LIMIT;
@@ -443,6 +446,73 @@
}
@Test
+ public void addAllowLabelRemovalPermission() throws Exception {
+ Project.NameKey key = projectOperations.newProject().create();
+ projectOperations
+ .project(key)
+ .forUpdate()
+ .add(allowLabelRemoval("Code-Review").ref("refs/foo").group(REGISTERED_USERS).range(-1, 2))
+ .update();
+
+ Config config = projectOperations.project(key).getConfig();
+ assertThat(config).sections().containsExactly("access", "submit");
+ assertThat(config).subsections("access").containsExactly("refs/foo");
+ assertThat(config)
+ .subsectionValues("access", "refs/foo")
+ .containsExactly("removeLabel-Code-Review", "-1..+2 group global:Registered-Users");
+ }
+
+ @Test
+ public void addBlockLabelRemovalPermission() throws Exception {
+ Project.NameKey key = projectOperations.newProject().create();
+ projectOperations
+ .project(key)
+ .forUpdate()
+ .add(blockLabelRemoval("Code-Review").ref("refs/foo").group(REGISTERED_USERS).range(-1, 2))
+ .update();
+
+ Config config = projectOperations.project(key).getConfig();
+ assertThat(config).sections().containsExactly("access", "submit");
+ assertThat(config).subsections("access").containsExactly("refs/foo");
+ assertThat(config)
+ .subsectionValues("access", "refs/foo")
+ .containsExactly("removeLabel-Code-Review", "block -1..+2 group global:Registered-Users");
+ }
+
+ @Test
+ public void addAllowExclusiveLabelRemovalPermission() throws Exception {
+ Project.NameKey key = projectOperations.newProject().create();
+ projectOperations
+ .project(key)
+ .forUpdate()
+ .add(allowLabelRemoval("Code-Review").ref("refs/foo").group(REGISTERED_USERS).range(-1, 2))
+ .setExclusiveGroup(labelRemovalPermissionKey("Code-Review").ref("refs/foo"), true)
+ .update();
+
+ Config config = projectOperations.project(key).getConfig();
+ assertThat(config).sections().containsExactly("access", "submit");
+ assertThat(config).subsections("access").containsExactly("refs/foo");
+ assertThat(config)
+ .subsectionValues("access", "refs/foo")
+ .containsExactly(
+ "removeLabel-Code-Review", "-1..+2 group global:Registered-Users",
+ "exclusiveGroupPermissions", "removeLabel-Code-Review");
+
+ projectOperations
+ .project(key)
+ .forUpdate()
+ .setExclusiveGroup(labelRemovalPermissionKey("Code-Review").ref("refs/foo"), false)
+ .update();
+
+ config = projectOperations.project(key).getConfig();
+ assertThat(config).sections().containsExactly("access", "submit");
+ assertThat(config).subsections("access").containsExactly("refs/foo");
+ assertThat(config)
+ .subsectionValues("access", "refs/foo")
+ .containsExactly("removeLabel-Code-Review", "-1..+2 group global:Registered-Users");
+ }
+
+ @Test
public void addAllowCapability() throws Exception {
Config config = projectOperations.project(allProjects).getConfig();
assertThat(config)
@@ -540,6 +610,31 @@
}
@Test
+ public void removeLabelRemovalPermission() throws Exception {
+ Project.NameKey key = projectOperations.newProject().create();
+ projectOperations
+ .project(key)
+ .forUpdate()
+ .add(allowLabelRemoval("Code-Review").ref("refs/foo").group(REGISTERED_USERS).range(-1, 2))
+ .add(allowLabelRemoval("Code-Review").ref("refs/foo").group(PROJECT_OWNERS).range(-2, 1))
+ .update();
+ assertThat(projectOperations.project(key).getConfig())
+ .subsectionValues("access", "refs/foo")
+ .containsExactly(
+ "removeLabel-Code-Review", "-1..+2 group global:Registered-Users",
+ "removeLabel-Code-Review", "-2..+1 group global:Project-Owners");
+
+ projectOperations
+ .project(key)
+ .forUpdate()
+ .remove(labelRemovalPermissionKey("Code-Review").ref("refs/foo").group(REGISTERED_USERS))
+ .update();
+ assertThat(projectOperations.project(key).getConfig())
+ .subsectionValues("access", "refs/foo")
+ .containsExactly("removeLabel-Code-Review", "-2..+1 group global:Project-Owners");
+ }
+
+ @Test
public void removeCapability() throws Exception {
projectOperations
.allProjectsForUpdate()
diff --git a/javatests/com/google/gerrit/entities/PermissionTest.java b/javatests/com/google/gerrit/entities/PermissionTest.java
index 3175671..d25d833 100644
--- a/javatests/com/google/gerrit/entities/PermissionTest.java
+++ b/javatests/com/google/gerrit/entities/PermissionTest.java
@@ -36,6 +36,7 @@
assertThat(Permission.isPermission(Permission.LABEL + LabelId.CODE_REVIEW)).isTrue();
assertThat(Permission.isPermission(Permission.LABEL_AS + LabelId.CODE_REVIEW)).isTrue();
+ assertThat(Permission.isPermission(Permission.REMOVE_LABEL + LabelId.CODE_REVIEW)).isTrue();
assertThat(Permission.isPermission(LabelId.CODE_REVIEW)).isFalse();
}
@@ -56,6 +57,7 @@
assertThat(Permission.isLabel(Permission.LABEL + LabelId.CODE_REVIEW)).isTrue();
assertThat(Permission.isLabel(Permission.LABEL_AS + LabelId.CODE_REVIEW)).isFalse();
+ assertThat(Permission.isLabel(Permission.REMOVE_LABEL + LabelId.CODE_REVIEW)).isFalse();
assertThat(Permission.isLabel(LabelId.CODE_REVIEW)).isFalse();
}
@@ -66,10 +68,22 @@
assertThat(Permission.isLabelAs(Permission.LABEL + LabelId.CODE_REVIEW)).isFalse();
assertThat(Permission.isLabelAs(Permission.LABEL_AS + LabelId.CODE_REVIEW)).isTrue();
+ assertThat(Permission.isLabelAs(Permission.REMOVE_LABEL + LabelId.CODE_REVIEW)).isFalse();
assertThat(Permission.isLabelAs(LabelId.CODE_REVIEW)).isFalse();
}
@Test
+ public void isRemoveLabel() {
+ assertThat(Permission.isRemoveLabel(Permission.ABANDON)).isFalse();
+ assertThat(Permission.isRemoveLabel("no-permission")).isFalse();
+
+ assertThat(Permission.isRemoveLabel(Permission.LABEL + LabelId.CODE_REVIEW)).isFalse();
+ assertThat(Permission.isRemoveLabel(Permission.LABEL_AS + LabelId.CODE_REVIEW)).isFalse();
+ assertThat(Permission.isRemoveLabel(Permission.REMOVE_LABEL + LabelId.CODE_REVIEW)).isTrue();
+ assertThat(Permission.isRemoveLabel(LabelId.CODE_REVIEW)).isFalse();
+ }
+
+ @Test
public void forLabel() {
assertThat(Permission.forLabel(LabelId.CODE_REVIEW))
.isEqualTo(Permission.LABEL + LabelId.CODE_REVIEW);
@@ -82,11 +96,19 @@
}
@Test
+ public void forRemoveLabel() {
+ assertThat(Permission.forRemoveLabel(LabelId.CODE_REVIEW))
+ .isEqualTo(Permission.REMOVE_LABEL + LabelId.CODE_REVIEW);
+ }
+
+ @Test
public void extractLabel() {
assertThat(Permission.extractLabel(Permission.LABEL + LabelId.CODE_REVIEW))
.isEqualTo(LabelId.CODE_REVIEW);
assertThat(Permission.extractLabel(Permission.LABEL_AS + LabelId.CODE_REVIEW))
.isEqualTo(LabelId.CODE_REVIEW);
+ assertThat(Permission.extractLabel(Permission.REMOVE_LABEL + LabelId.CODE_REVIEW))
+ .isEqualTo(LabelId.CODE_REVIEW);
assertThat(Permission.extractLabel(LabelId.CODE_REVIEW)).isNull();
assertThat(Permission.extractLabel(Permission.ABANDON)).isNull();
}
@@ -103,6 +125,10 @@
Permission.canBeOnAllProjects(
AccessSection.ALL, Permission.LABEL_AS + LabelId.CODE_REVIEW))
.isTrue();
+ assertThat(
+ Permission.canBeOnAllProjects(
+ AccessSection.ALL, Permission.REMOVE_LABEL + LabelId.CODE_REVIEW))
+ .isTrue();
assertThat(Permission.canBeOnAllProjects("refs/heads/*", Permission.ABANDON)).isTrue();
assertThat(Permission.canBeOnAllProjects("refs/heads/*", Permission.OWNER)).isTrue();
@@ -113,6 +139,10 @@
Permission.canBeOnAllProjects(
"refs/heads/*", Permission.LABEL_AS + LabelId.CODE_REVIEW))
.isTrue();
+ assertThat(
+ Permission.canBeOnAllProjects(
+ "refs/heads/*", Permission.REMOVE_LABEL + LabelId.CODE_REVIEW))
+ .isTrue();
}
@Test
@@ -126,6 +156,8 @@
.isEqualTo(LabelId.CODE_REVIEW);
assertThat(Permission.create(Permission.LABEL_AS + LabelId.CODE_REVIEW).getLabel())
.isEqualTo(LabelId.CODE_REVIEW);
+ assertThat(Permission.create(Permission.REMOVE_LABEL + LabelId.CODE_REVIEW).getLabel())
+ .isEqualTo(LabelId.CODE_REVIEW);
assertThat(Permission.create(LabelId.CODE_REVIEW).getLabel()).isNull();
assertThat(Permission.create(Permission.ABANDON).getLabel()).isNull();
}
diff --git a/javatests/com/google/gerrit/extensions/common/ChangeInfoDifferTest.java b/javatests/com/google/gerrit/extensions/common/ChangeInfoDifferTest.java
index 7ed236a..f45d33b 100644
--- a/javatests/com/google/gerrit/extensions/common/ChangeInfoDifferTest.java
+++ b/javatests/com/google/gerrit/extensions/common/ChangeInfoDifferTest.java
@@ -48,6 +48,7 @@
assertThat(diff.added().messages).isNull();
assertThat(diff.added().reviewers).isNull();
assertThat(diff.added().hashtags).isNull();
+ assertThat(diff.added().removableLabels).isNull();
assertThat(diff.removed()._number).isNull();
assertThat(diff.removed().branch).isNull();
assertThat(diff.removed().project).isNull();
@@ -56,6 +57,7 @@
assertThat(diff.removed().messages).isNull();
assertThat(diff.removed().reviewers).isNull();
assertThat(diff.removed().hashtags).isNull();
+ assertThat(diff.removed().removableLabels).isNull();
}
@Test
@@ -315,6 +317,295 @@
}
@Test
+ public void getDiff_removableLabelsEmpty_returnsNullRemovableLabels() {
+ ChangeInfo oldChangeInfo = new ChangeInfo();
+ ChangeInfo newChangeInfo = new ChangeInfo();
+ oldChangeInfo.removableLabels = ImmutableMap.of();
+ newChangeInfo.removableLabels = ImmutableMap.of();
+
+ ChangeInfoDifference diff = ChangeInfoDiffer.getDifference(oldChangeInfo, newChangeInfo);
+
+ assertThat(diff.added().removableLabels).isNull();
+ assertThat(diff.removed().removableLabels).isNull();
+ }
+
+ @Test
+ public void getDiff_removableLabelsNullAndEmpty_returnsEmptyRemovableLabels() {
+ ChangeInfo oldChangeInfo = new ChangeInfo();
+ ChangeInfo newChangeInfo = new ChangeInfo();
+ newChangeInfo.removableLabels = ImmutableMap.of();
+
+ ChangeInfoDifference diff = ChangeInfoDiffer.getDifference(oldChangeInfo, newChangeInfo);
+
+ assertThat(diff.added().removableLabels).isEmpty();
+ assertThat(diff.removed().removableLabels).isNull();
+ }
+
+ @Test
+ public void getDiff_removableLabelsEmptyAndNull_returnsEmptyRemovableLabels() {
+ ChangeInfo oldChangeInfo = new ChangeInfo();
+ ChangeInfo newChangeInfo = new ChangeInfo();
+ oldChangeInfo.removableLabels = ImmutableMap.of();
+
+ ChangeInfoDifference diff = ChangeInfoDiffer.getDifference(oldChangeInfo, newChangeInfo);
+
+ assertThat(diff.added().removableLabels).isNull();
+ assertThat(diff.removed().removableLabels).isEmpty();
+ }
+
+ @Test
+ public void getDiff_removableLabelsLabelAdded() {
+ ChangeInfo oldChangeInfo = new ChangeInfo();
+ ChangeInfo newChangeInfo = new ChangeInfo();
+ AccountInfo acc1 = new AccountInfo();
+ acc1.name = "Cow";
+ AccountInfo acc2 = new AccountInfo();
+ acc2.name = "Pig";
+ AccountInfo acc3 = new AccountInfo();
+ acc3.name = "Cat";
+ AccountInfo acc4 = new AccountInfo();
+ acc4.name = "Dog";
+
+ oldChangeInfo.removableLabels =
+ ImmutableMap.of(
+ "Code-Review",
+ ImmutableMap.of("+1", ImmutableList.of(acc1), "-1", ImmutableList.of(acc2, acc3)));
+ newChangeInfo.removableLabels =
+ ImmutableMap.of(
+ "Code-Review",
+ ImmutableMap.of("+1", ImmutableList.of(acc1), "-1", ImmutableList.of(acc2, acc3)),
+ "Verified",
+ ImmutableMap.of("-1", ImmutableList.of(acc4)));
+
+ ChangeInfoDifference diff = ChangeInfoDiffer.getDifference(oldChangeInfo, newChangeInfo);
+
+ assertThat(diff.added().removableLabels)
+ .containsExactlyEntriesIn(
+ ImmutableMap.of("Verified", ImmutableMap.of("-1", ImmutableList.of(acc4))));
+ assertThat(diff.removed().removableLabels).isNull();
+ }
+
+ @Test
+ public void getDiff_removableLabelsLabelRemoved() {
+ ChangeInfo oldChangeInfo = new ChangeInfo();
+ ChangeInfo newChangeInfo = new ChangeInfo();
+ AccountInfo acc1 = new AccountInfo();
+ acc1.name = "Cow";
+ AccountInfo acc2 = new AccountInfo();
+ acc2.name = "Pig";
+ AccountInfo acc3 = new AccountInfo();
+ acc3.name = "Cat";
+ AccountInfo acc4 = new AccountInfo();
+ acc4.name = "Dog";
+
+ oldChangeInfo.removableLabels =
+ ImmutableMap.of(
+ "Code-Review",
+ ImmutableMap.of("+1", ImmutableList.of(acc1), "-1", ImmutableList.of(acc2, acc3)),
+ "Verified",
+ ImmutableMap.of("-1", ImmutableList.of(acc4)));
+ newChangeInfo.removableLabels =
+ ImmutableMap.of(
+ "Code-Review",
+ ImmutableMap.of("+1", ImmutableList.of(acc1), "-1", ImmutableList.of(acc2, acc3)));
+
+ ChangeInfoDifference diff = ChangeInfoDiffer.getDifference(oldChangeInfo, newChangeInfo);
+
+ assertThat(diff.added().removableLabels).isNull();
+ assertThat(diff.removed().removableLabels)
+ .containsExactlyEntriesIn(
+ ImmutableMap.of("Verified", ImmutableMap.of("-1", ImmutableList.of(acc4))));
+ }
+
+ @Test
+ public void getDiff_removableLabelsVoteAdded() {
+ ChangeInfo oldChangeInfo = new ChangeInfo();
+ ChangeInfo newChangeInfo = new ChangeInfo();
+ AccountInfo acc1 = new AccountInfo();
+ acc1.name = "acc1";
+ AccountInfo acc2 = new AccountInfo();
+ acc2.name = "acc2";
+ AccountInfo acc3 = new AccountInfo();
+ acc3.name = "acc3";
+
+ oldChangeInfo.removableLabels =
+ ImmutableMap.of("Code-Review", ImmutableMap.of("+1", ImmutableList.of(acc1)));
+ newChangeInfo.removableLabels =
+ ImmutableMap.of(
+ "Code-Review",
+ ImmutableMap.of("+1", ImmutableList.of(acc1), "-1", ImmutableList.of(acc2, acc3)));
+
+ ChangeInfoDifference diff = ChangeInfoDiffer.getDifference(oldChangeInfo, newChangeInfo);
+
+ assertThat(diff.added().removableLabels)
+ .containsExactlyEntriesIn(
+ ImmutableMap.of("Code-Review", ImmutableMap.of("-1", ImmutableList.of(acc2, acc3))));
+ assertThat(diff.removed().removableLabels).isNull();
+ }
+
+ @Test
+ public void getDiff_removableLabelsVoteRemoved() {
+ ChangeInfo oldChangeInfo = new ChangeInfo();
+ ChangeInfo newChangeInfo = new ChangeInfo();
+ AccountInfo acc1 = new AccountInfo();
+ acc1.name = "acc1";
+ AccountInfo acc2 = new AccountInfo();
+ acc2.name = "acc2";
+ AccountInfo acc3 = new AccountInfo();
+ acc3.name = "acc3";
+
+ oldChangeInfo.removableLabels =
+ ImmutableMap.of(
+ "Code-Review",
+ ImmutableMap.of("+1", ImmutableList.of(acc1), "-1", ImmutableList.of(acc2, acc3)));
+ newChangeInfo.removableLabels =
+ ImmutableMap.of("Code-Review", ImmutableMap.of("+1", ImmutableList.of(acc1)));
+
+ ChangeInfoDifference diff = ChangeInfoDiffer.getDifference(oldChangeInfo, newChangeInfo);
+
+ assertThat(diff.added().removableLabels).isNull();
+ assertThat(diff.removed().removableLabels)
+ .containsExactlyEntriesIn(
+ ImmutableMap.of("Code-Review", ImmutableMap.of("-1", ImmutableList.of(acc2, acc3))));
+ }
+
+ @Test
+ public void getDiff_removableLabelsAccountAdded() {
+ ChangeInfo oldChangeInfo = new ChangeInfo();
+ ChangeInfo newChangeInfo = new ChangeInfo();
+ AccountInfo acc1 = new AccountInfo();
+ acc1.name = "acc1";
+ AccountInfo acc2 = new AccountInfo();
+ acc2.name = "acc2";
+
+ oldChangeInfo.removableLabels =
+ ImmutableMap.of("Code-Review", ImmutableMap.of("+1", ImmutableList.of(acc1)));
+ newChangeInfo.removableLabels =
+ ImmutableMap.of("Code-Review", ImmutableMap.of("+1", ImmutableList.of(acc1, acc2)));
+
+ ChangeInfoDifference diff = ChangeInfoDiffer.getDifference(oldChangeInfo, newChangeInfo);
+
+ assertThat(diff.added().removableLabels)
+ .containsExactlyEntriesIn(
+ ImmutableMap.of("Code-Review", ImmutableMap.of("+1", ImmutableList.of(acc2))));
+ assertThat(diff.removed().removableLabels).isNull();
+ }
+
+ @Test
+ public void getDiff_removableLabelsAccountRemoved() {
+ ChangeInfo oldChangeInfo = new ChangeInfo();
+ ChangeInfo newChangeInfo = new ChangeInfo();
+ AccountInfo acc1 = new AccountInfo();
+ acc1.name = "acc1";
+ AccountInfo acc2 = new AccountInfo();
+ acc2.name = "acc2";
+
+ oldChangeInfo.removableLabels =
+ ImmutableMap.of("Code-Review", ImmutableMap.of("+1", ImmutableList.of(acc1)));
+ newChangeInfo.removableLabels =
+ ImmutableMap.of("Code-Review", ImmutableMap.of("+1", ImmutableList.of(acc1, acc2)));
+
+ ChangeInfoDifference diff = ChangeInfoDiffer.getDifference(oldChangeInfo, newChangeInfo);
+
+ assertThat(diff.added().removableLabels)
+ .containsExactlyEntriesIn(
+ ImmutableMap.of("Code-Review", ImmutableMap.of("+1", ImmutableList.of(acc2))));
+ assertThat(diff.removed().removableLabels).isNull();
+ }
+
+ @Test
+ public void getDiff_removableLabelsAccountChanged() {
+ ChangeInfo oldChangeInfo = new ChangeInfo();
+ ChangeInfo newChangeInfo = new ChangeInfo();
+ AccountInfo acc1 = new AccountInfo();
+ acc1.name = "acc1";
+ AccountInfo acc2 = new AccountInfo();
+ acc2.name = "acc2";
+
+ oldChangeInfo.removableLabels =
+ ImmutableMap.of("Code-Review", ImmutableMap.of("+1", ImmutableList.of(acc1)));
+ newChangeInfo.removableLabels =
+ ImmutableMap.of("Code-Review", ImmutableMap.of("+1", ImmutableList.of(acc2)));
+
+ ChangeInfoDifference diff = ChangeInfoDiffer.getDifference(oldChangeInfo, newChangeInfo);
+
+ assertThat(diff.added().removableLabels)
+ .containsExactlyEntriesIn(
+ ImmutableMap.of("Code-Review", ImmutableMap.of("+1", ImmutableList.of(acc2))));
+ assertThat(diff.removed().removableLabels)
+ .containsExactlyEntriesIn(
+ ImmutableMap.of("Code-Review", ImmutableMap.of("+1", ImmutableList.of(acc1))));
+ }
+
+ @Test
+ public void getDiff_removableLabelsScoreChanged() {
+ ChangeInfo oldChangeInfo = new ChangeInfo();
+ ChangeInfo newChangeInfo = new ChangeInfo();
+ AccountInfo acc1 = new AccountInfo();
+ acc1.name = "acc1";
+
+ oldChangeInfo.removableLabels =
+ ImmutableMap.of("Code-Review", ImmutableMap.of("+1", ImmutableList.of(acc1)));
+ newChangeInfo.removableLabels =
+ ImmutableMap.of("Code-Review", ImmutableMap.of("-1", ImmutableList.of(acc1)));
+
+ ChangeInfoDifference diff = ChangeInfoDiffer.getDifference(oldChangeInfo, newChangeInfo);
+
+ assertThat(diff.added().removableLabels)
+ .containsExactlyEntriesIn(
+ ImmutableMap.of("Code-Review", ImmutableMap.of("-1", ImmutableList.of(acc1))));
+ assertThat(diff.removed().removableLabels)
+ .containsExactlyEntriesIn(
+ ImmutableMap.of("Code-Review", ImmutableMap.of("+1", ImmutableList.of(acc1))));
+ }
+
+ @Test
+ public void getDiff_removableLabelsLabelChanged() {
+ ChangeInfo oldChangeInfo = new ChangeInfo();
+ ChangeInfo newChangeInfo = new ChangeInfo();
+ AccountInfo acc1 = new AccountInfo();
+ acc1.name = "acc1";
+
+ oldChangeInfo.removableLabels =
+ ImmutableMap.of("Code-Review", ImmutableMap.of("+1", ImmutableList.of(acc1)));
+ newChangeInfo.removableLabels =
+ ImmutableMap.of("Verified", ImmutableMap.of("+1", ImmutableList.of(acc1)));
+
+ ChangeInfoDifference diff = ChangeInfoDiffer.getDifference(oldChangeInfo, newChangeInfo);
+
+ assertThat(diff.added().removableLabels)
+ .containsExactlyEntriesIn(
+ ImmutableMap.of("Verified", ImmutableMap.of("+1", ImmutableList.of(acc1))));
+ assertThat(diff.removed().removableLabels)
+ .containsExactlyEntriesIn(
+ ImmutableMap.of("Code-Review", ImmutableMap.of("+1", ImmutableList.of(acc1))));
+ }
+
+ @Test
+ public void getDiff_removableLabelsLabelScoreAndAccountChanged() {
+ ChangeInfo oldChangeInfo = new ChangeInfo();
+ ChangeInfo newChangeInfo = new ChangeInfo();
+ AccountInfo acc1 = new AccountInfo();
+ acc1.name = "acc1";
+ AccountInfo acc2 = new AccountInfo();
+ acc2.name = "acc2";
+
+ oldChangeInfo.removableLabels =
+ ImmutableMap.of("Code-Review", ImmutableMap.of("+1", ImmutableList.of(acc1)));
+ newChangeInfo.removableLabels =
+ ImmutableMap.of("Verified", ImmutableMap.of("-1", ImmutableList.of(acc2)));
+
+ ChangeInfoDifference diff = ChangeInfoDiffer.getDifference(oldChangeInfo, newChangeInfo);
+
+ assertThat(diff.added().removableLabels)
+ .containsExactlyEntriesIn(
+ ImmutableMap.of("Verified", ImmutableMap.of("-1", ImmutableList.of(acc2))));
+ assertThat(diff.removed().removableLabels)
+ .containsExactlyEntriesIn(
+ ImmutableMap.of("Code-Review", ImmutableMap.of("+1", ImmutableList.of(acc1))));
+ }
+
+ @Test
public void getDiff_assertCanConstructAllChangeInfoReferences() throws Exception {
buildObjectWithFullFields(ChangeInfo.class);
}
diff --git a/javatests/com/google/gerrit/httpd/raw/IndexServletTest.java b/javatests/com/google/gerrit/httpd/raw/IndexServletTest.java
index f65e823..06ea8b6 100644
--- a/javatests/com/google/gerrit/httpd/raw/IndexServletTest.java
+++ b/javatests/com/google/gerrit/httpd/raw/IndexServletTest.java
@@ -60,15 +60,13 @@
String testCdnPath = "bar-cdn";
String testFaviconURL = "zaz-url";
- // Pick any known experiment enabled by default;
- String disabledDefault = ExperimentFeaturesConstants.UI_FEATURE_PATCHSET_COMMENTS;
- assertThat(ExperimentFeaturesConstants.DEFAULT_ENABLED_FEATURES).contains(disabledDefault);
+ assertThat(ExperimentFeaturesConstants.DEFAULT_ENABLED_FEATURES).isEmpty();
org.eclipse.jgit.lib.Config serverConfig = new org.eclipse.jgit.lib.Config();
serverConfig.setStringList(
"experiments", null, "enabled", ImmutableList.of("NewFeature", "DisabledFeature"));
serverConfig.setStringList(
- "experiments", null, "disabled", ImmutableList.of("DisabledFeature", disabledDefault));
+ "experiments", null, "disabled", ImmutableList.of("DisabledFeature"));
ExperimentFeatures experimentFeatures = new ConfigExperimentFeatures(serverConfig);
IndexServlet servlet =
new IndexServlet(
@@ -97,7 +95,6 @@
+ "\\x5b\\x5d\\x7d');");
ImmutableSet<String> enabledDefaults =
ExperimentFeaturesConstants.DEFAULT_ENABLED_FEATURES.stream()
- .filter(e -> !e.equals(disabledDefault))
.collect(ImmutableSet.toImmutableSet());
List<String> expectedEnabled = new ArrayList<>();
expectedEnabled.add("NewFeature");
diff --git a/javatests/com/google/gerrit/index/IndexUpgradeValidatorTest.java b/javatests/com/google/gerrit/index/IndexUpgradeValidatorTest.java
index 7f98a9d..affaadf 100644
--- a/javatests/com/google/gerrit/index/IndexUpgradeValidatorTest.java
+++ b/javatests/com/google/gerrit/index/IndexUpgradeValidatorTest.java
@@ -37,17 +37,17 @@
IndexUpgradeValidator.assertValid(
schema(
1,
- ImmutableList.of(ChangeField.CHANGE_ID_FIELD),
- ImmutableList.of(ChangeField.CHANGE_ID_SPEC)),
+ ImmutableList.<IndexedField<ChangeData, ?>>of(ChangeField.CHANGE_ID_FIELD),
+ ImmutableList.<IndexedField<ChangeData, ?>.SearchSpec>of(ChangeField.CHANGE_ID_SPEC)),
schema(
2,
- ImmutableList.of(ChangeField.CHANGE_ID_FIELD),
- ImmutableList.of(ChangeField.CHANGE_ID_SPEC)));
+ ImmutableList.<IndexedField<ChangeData, ?>>of(ChangeField.CHANGE_ID_FIELD),
+ ImmutableList.<IndexedField<ChangeData, ?>.SearchSpec>of(ChangeField.CHANGE_ID_SPEC)));
IndexUpgradeValidator.assertValid(
schema(
1,
- ImmutableList.of(ChangeField.CHANGE_ID_FIELD),
- ImmutableList.of(ChangeField.CHANGE_ID_SPEC)),
+ ImmutableList.<IndexedField<ChangeData, ?>>of(ChangeField.CHANGE_ID_FIELD),
+ ImmutableList.<IndexedField<ChangeData, ?>.SearchSpec>of(ChangeField.CHANGE_ID_SPEC)),
schema(
2,
ImmutableList.<FieldDef<ChangeData, ?>>of(),
@@ -58,8 +58,8 @@
IndexUpgradeValidator.assertValid(
schema(
1,
- ImmutableList.of(ChangeField.CHANGE_ID_FIELD),
- ImmutableList.of(ChangeField.CHANGE_ID_SPEC)),
+ ImmutableList.<IndexedField<ChangeData, ?>>of(ChangeField.CHANGE_ID_FIELD),
+ ImmutableList.<IndexedField<ChangeData, ?>.SearchSpec>of(ChangeField.CHANGE_ID_SPEC)),
schema(
2,
ImmutableList.<IndexedField<ChangeData, ?>>of(
@@ -81,8 +81,9 @@
IndexUpgradeValidator.assertValid(
schema(
1,
- ImmutableList.of(ChangeField.CHANGE_ID_FIELD),
- ImmutableList.of(ChangeField.CHANGE_ID_SPEC)),
+ ImmutableList.<IndexedField<ChangeData, ?>>of(ChangeField.CHANGE_ID_FIELD),
+ ImmutableList.<IndexedField<ChangeData, ?>.SearchSpec>of(
+ ChangeField.CHANGE_ID_SPEC)),
schema(
2,
ImmutableList.<IndexedField<ChangeData, ?>>of(ChangeField.OWNER_FIELD),
@@ -105,9 +106,13 @@
IndexUpgradeValidator.assertValid(
schema(
1,
- ImmutableList.of(ChangeField.CHANGE_ID_FIELD),
- ImmutableList.of(ChangeField.CHANGE_ID_SPEC)),
- schema(2, ImmutableList.of(ID_MODIFIED), ImmutableList.of())));
+ ImmutableList.<IndexedField<ChangeData, ?>>of(ChangeField.CHANGE_ID_FIELD),
+ ImmutableList.<IndexedField<ChangeData, ?>.SearchSpec>of(
+ ChangeField.CHANGE_ID_SPEC)),
+ schema(
+ 2,
+ ImmutableList.<IndexedField<ChangeData, ?>>of(ID_MODIFIED),
+ ImmutableList.<IndexedField<ChangeData, ?>.SearchSpec>of())));
assertThat(e).hasMessageThat().contains("Fields may not be modified");
assertThat(e).hasMessageThat().contains(ChangeField.CHANGE_ID_FIELD.name());
}
diff --git a/javatests/com/google/gerrit/server/account/AccountResolverTest.java b/javatests/com/google/gerrit/server/account/AccountResolverTest.java
index 3658834..37728f7 100644
--- a/javatests/com/google/gerrit/server/account/AccountResolverTest.java
+++ b/javatests/com/google/gerrit/server/account/AccountResolverTest.java
@@ -23,6 +23,8 @@
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.gerrit.entities.Account;
+import com.google.gerrit.server.AnonymousUser;
+import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.account.AccountResolver.Result;
import com.google.gerrit.server.account.AccountResolver.Searcher;
import com.google.gerrit.server.account.AccountResolver.StringSearcher;
@@ -35,6 +37,8 @@
import org.junit.Test;
public class AccountResolverTest {
+ private final CurrentUser user = new AnonymousUser();
+
private static class TestSearcher extends StringSearcher {
private final String pattern;
private final boolean shortCircuit;
@@ -282,7 +286,7 @@
AccountResolver resolver = newAccountResolver();
assertThat(
new UnresolvableAccountException(
- resolver.new Result("foo", ImmutableList.of(), ImmutableList.of())))
+ resolver.new Result("foo", ImmutableList.of(), ImmutableList.of(), user)))
.hasMessageThat()
.isEqualTo("Account 'foo' not found");
}
@@ -292,7 +296,7 @@
AccountResolver resolver = newAccountResolver();
UnresolvableAccountException e =
new UnresolvableAccountException(
- resolver.new Result("self", ImmutableList.of(), ImmutableList.of()));
+ resolver.new Result("self", ImmutableList.of(), ImmutableList.of(), user));
assertThat(e.isSelf()).isTrue();
assertThat(e).hasMessageThat().isEqualTo("Resolving account 'self' requires login");
}
@@ -302,7 +306,7 @@
AccountResolver resolver = newAccountResolver();
UnresolvableAccountException e =
new UnresolvableAccountException(
- resolver.new Result("me", ImmutableList.of(), ImmutableList.of()));
+ resolver.new Result("me", ImmutableList.of(), ImmutableList.of(), user));
assertThat(e.isSelf()).isTrue();
assertThat(e).hasMessageThat().isEqualTo("Resolving account 'me' requires login");
}
@@ -314,7 +318,10 @@
new UnresolvableAccountException(
resolver
.new Result(
- "foo", ImmutableList.of(newAccount(3), newAccount(1)), ImmutableList.of())))
+ "foo",
+ ImmutableList.of(newAccount(3), newAccount(1)),
+ ImmutableList.of(),
+ user)))
.hasMessageThat()
.isEqualTo(
"Account 'foo' is ambiguous (at most 3 shown):\n1: Anonymous Name (1)\n3: Anonymous Name (3)");
@@ -329,7 +336,8 @@
.new Result(
"foo",
ImmutableList.of(),
- ImmutableList.of(newInactiveAccount(3), newInactiveAccount(1)))))
+ ImmutableList.of(newInactiveAccount(3), newInactiveAccount(1)),
+ user)))
.hasMessageThat()
.isEqualTo(
"Account 'foo' only matches inactive accounts. To use an inactive account, retry"
@@ -352,10 +360,11 @@
Supplier<Predicate<AccountState>> visibilitySupplier,
Predicate<AccountState> activityPredicate)
throws Exception {
- return newAccountResolver().searchImpl(input, searchers, visibilitySupplier, activityPredicate);
+ return newAccountResolver()
+ .searchImpl(input, searchers, user, visibilitySupplier, activityPredicate);
}
- private static AccountResolver newAccountResolver() {
+ private AccountResolver newAccountResolver() {
return new AccountResolver(null, null, null, null, null, null, null, null, "Anonymous Name");
}
diff --git a/javatests/com/google/gerrit/server/index/account/AccountFieldTest.java b/javatests/com/google/gerrit/server/index/account/AccountFieldTest.java
index 65eb3b8..a40afe8 100644
--- a/javatests/com/google/gerrit/server/index/account/AccountFieldTest.java
+++ b/javatests/com/google/gerrit/server/index/account/AccountFieldTest.java
@@ -40,8 +40,7 @@
String metaId = "0e39795bb25dc914118224995c53c5c36923a461";
account.setMetaId(metaId);
Iterable<byte[]> refStates =
- (Iterable<byte[]>)
- AccountField.REF_STATE_SPEC.get(AccountState.forAccount(account.build()));
+ AccountField.REF_STATE_SPEC.get(AccountState.forAccount(account.build()));
List<String> values = toStrings(refStates);
String expectedValue =
allUsersName.get() + ":" + RefNames.refsUsers(account.id()) + ":" + metaId;
diff --git a/javatests/com/google/gerrit/server/index/change/FakeChangeIndex.java b/javatests/com/google/gerrit/server/index/change/FakeChangeIndex.java
index 95e8aa5..15adcf8 100644
--- a/javatests/com/google/gerrit/server/index/change/FakeChangeIndex.java
+++ b/javatests/com/google/gerrit/server/index/change/FakeChangeIndex.java
@@ -98,6 +98,11 @@
}
@Override
+ public void deleteByValue(ChangeData value) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
public void delete(Change.Id id) {
throw new UnsupportedOperationException();
}
diff --git a/javatests/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java b/javatests/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
index 37d8468..be8f1f9 100644
--- a/javatests/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
+++ b/javatests/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
@@ -76,6 +76,7 @@
import com.google.inject.TypeLiteral;
import java.time.Instant;
import java.time.ZoneId;
+import java.util.Optional;
import java.util.concurrent.ExecutorService;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
@@ -276,7 +277,7 @@
protected ChangeUpdate newUpdate(
Injector injector, Change c, CurrentUser user, boolean shouldExist) throws Exception {
- ChangeUpdate update = TestChanges.newUpdate(injector, c, user, shouldExist);
+ ChangeUpdate update = TestChanges.newUpdate(injector, c, Optional.of(user), shouldExist);
update.setPatchSetId(c.currentPatchSetId());
update.setAllowWriteToNewRef(true);
return update;
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
index 61b5e55..9cd002e 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
@@ -825,7 +825,8 @@
ImmutableList.of(", ", ":\"", ",", "!@#$%^\0&*):\" \n: \r\"#$@,. :");
for (String strangeTag : strangeTags) {
Change c = newChange();
- CurrentUser otherUserAsOwner = userFactory.runAs(null, changeOwner.getAccountId(), otherUser);
+ CurrentUser otherUserAsOwner =
+ userFactory.runAs(/* remotePeer= */ null, changeOwner.getAccountId(), otherUser);
ChangeUpdate update = newUpdate(c, otherUserAsOwner);
update.putApproval(LabelId.CODE_REVIEW, (short) 2);
update.setTag(strangeTag);
diff --git a/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java b/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
index b53de89..25f2f98 100644
--- a/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
+++ b/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
@@ -354,7 +354,8 @@
@Test
public void realUser() throws Exception {
Change c = newChange();
- CurrentUser ownerAsOtherUser = userFactory.runAs(null, otherUserId, changeOwner);
+ CurrentUser ownerAsOtherUser =
+ userFactory.runAs(/* remotePeer= */ null, otherUserId, changeOwner);
ChangeUpdate update = newUpdate(c, ownerAsOtherUser);
update.setChangeMessage("Message on behalf of other user");
update.commit();
diff --git a/javatests/com/google/gerrit/server/notedb/CommitRewriterTest.java b/javatests/com/google/gerrit/server/notedb/CommitRewriterTest.java
index 5e6803e..527e78e 100644
--- a/javatests/com/google/gerrit/server/notedb/CommitRewriterTest.java
+++ b/javatests/com/google/gerrit/server/notedb/CommitRewriterTest.java
@@ -399,7 +399,9 @@
IdentifiedUser impersonatedChangeOwner =
this.userFactory.runAs(
- null, changeOwner.getAccountId(), requireNonNull(otherUser).getRealUser());
+ /* remotePeer= */ null,
+ changeOwner.getAccountId(),
+ requireNonNull(otherUser).getRealUser());
ChangeUpdate impersonatedChangeMessageUpdate = newUpdate(c, impersonatedChangeOwner);
impersonatedChangeMessageUpdate.setChangeMessage("Other comment on behalf of");
impersonatedChangeMessageUpdate.commit();
diff --git a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index 23f33fc..fbf9c87 100644
--- a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -46,6 +46,7 @@
import com.google.common.collect.Multimaps;
import com.google.common.collect.Streams;
import com.google.common.truth.ThrowableSubject;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.acceptance.ExtensionRegistry;
import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
import com.google.gerrit.acceptance.FakeSubmitRule;
@@ -109,12 +110,14 @@
import com.google.gerrit.server.account.AuthRequest;
import com.google.gerrit.server.account.ListGroupMembership;
import com.google.gerrit.server.account.VersionedAccountQueries;
+import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.account.externalids.ExternalIdFactory;
import com.google.gerrit.server.change.ChangeInserter;
import com.google.gerrit.server.change.ChangeTriplet;
import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.change.PatchSetInserter;
import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.meta.MetaDataUpdate;
import com.google.gerrit.server.group.testing.TestGroupBackend;
import com.google.gerrit.server.index.change.ChangeField;
@@ -122,6 +125,7 @@
import com.google.gerrit.server.index.change.ChangeIndexer;
import com.google.gerrit.server.index.change.IndexedChangeQuery;
import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.notedb.ChangeUpdate;
import com.google.gerrit.server.notedb.Sequences;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectConfig;
@@ -133,8 +137,7 @@
import com.google.gerrit.server.util.ThreadLocalRequestContext;
import com.google.gerrit.server.util.time.TimeUtil;
import com.google.gerrit.testing.GerritServerTests;
-import com.google.gerrit.testing.InMemoryRepositoryManager;
-import com.google.gerrit.testing.InMemoryRepositoryManager.Repo;
+import com.google.gerrit.testing.TestChanges;
import com.google.gerrit.testing.TestTimeUtil;
import com.google.inject.Inject;
import com.google.inject.Injector;
@@ -158,7 +161,7 @@
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.util.SystemReader;
@@ -183,7 +186,7 @@
@Inject protected ChangeIndexer indexer;
@Inject protected ExtensionRegistry extensionRegistry;
@Inject protected IndexConfig indexConfig;
- @Inject protected InMemoryRepositoryManager repoManager;
+ @Inject protected GitRepositoryManager repoManager;
@Inject protected Provider<AnonymousUser> anonymousUserProvider;
@Inject protected Provider<InternalChangeQuery> queryProvider;
@Inject protected ChangeNotes.Factory notesFactory;
@@ -206,11 +209,20 @@
protected Injector injector;
protected LifecycleManager lifecycle;
+
+ /**
+ * Index tests should not use username in query assert, since some backends do not use {@link
+ * ExternalId#SCHEME_USERNAME}
+ */
protected Account.Id userId;
+
protected CurrentUser user;
+ protected Account userAccount;
private String systemTimeZone;
+ protected TestRepository<Repository> repo;
+
protected abstract Injector createInjector();
@Before
@@ -226,6 +238,10 @@
@After
public void cleanUp() {
+ if (repo != null) {
+ repo.close();
+ repo = null;
+ }
lifecycle.stop();
}
@@ -252,8 +268,9 @@
return () -> requestUser;
}
- protected void resetUser() {
+ protected void resetUser() throws ConfigInvalidException, IOException {
user = userFactory.create(userId);
+ userAccount = accounts.get(userId).get().account();
requestContext.setContext(newRequestContext(userId));
}
@@ -289,9 +306,9 @@
@Test
public void byId() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
- Change change1 = insert(repo, newChange(repo));
- Change change2 = insert(repo, newChange(repo));
+ repo = createAndOpenProject("repo");
+ Change change1 = insert("repo", newChange(repo));
+ Change change2 = insert("repo", newChange(repo));
assertQuery("12345");
assertQuery(change1.getId().get(), change1);
@@ -300,8 +317,8 @@
@Test
public void byKey() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
- Change change = insert(repo, newChange(repo));
+ repo = createAndOpenProject("repo");
+ Change change = insert("repo", newChange(repo));
String key = change.getKey().get();
assertQuery("I0000000000000000000000000000000000000000");
@@ -313,8 +330,8 @@
@Test
public void byTriplet() throws Exception {
- TestRepository<Repo> repo = createProject("iabcde");
- Change change = insert(repo, newChangeForBranch(repo, "branch"));
+ repo = createAndOpenProject("iabcde");
+ Change change = insert("iabcde", newChangeForBranch(repo, "branch"));
String k = change.getKey().get();
assertQuery("iabcde~branch~" + k, change);
@@ -336,11 +353,11 @@
@Test
public void byStatus() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
ChangeInserter ins1 = newChangeWithStatus(repo, Change.Status.NEW);
- Change change1 = insert(repo, ins1);
+ Change change1 = insert("repo", ins1);
ChangeInserter ins2 = newChangeWithStatus(repo, Change.Status.MERGED);
- Change change2 = insert(repo, ins2);
+ Change change2 = insert("repo", ins2);
assertQuery("status:new", change1);
assertQuery("status:NEW", change1);
@@ -355,11 +372,11 @@
@Test
public void byStatusOr() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
ChangeInserter ins1 = newChangeWithStatus(repo, Change.Status.NEW);
- Change change1 = insert(repo, ins1);
+ Change change1 = insert("repo", ins1);
ChangeInserter ins2 = newChangeWithStatus(repo, Change.Status.MERGED);
- Change change2 = insert(repo, ins2);
+ Change change2 = insert("repo", ins2);
assertQuery("status:new OR status:merged", change2, change1);
assertQuery("status:new or status:merged", change2, change1);
@@ -367,10 +384,10 @@
@Test
public void byStatusOpen() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
ChangeInserter ins1 = newChangeWithStatus(repo, Change.Status.NEW);
- Change change1 = insert(repo, ins1);
- insert(repo, newChangeWithStatus(repo, Change.Status.MERGED));
+ Change change1 = insert("repo", ins1);
+ insert("repo", newChangeWithStatus(repo, Change.Status.MERGED));
Change[] expected = new Change[] {change1};
assertQuery("status:open", expected);
@@ -389,12 +406,12 @@
@Test
public void byStatusClosed() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
ChangeInserter ins1 = newChangeWithStatus(repo, Change.Status.MERGED);
- Change change1 = insert(repo, ins1);
+ Change change1 = insert("repo", ins1);
ChangeInserter ins2 = newChangeWithStatus(repo, Change.Status.ABANDONED);
- Change change2 = insert(repo, ins2);
- insert(repo, newChangeWithStatus(repo, Change.Status.NEW));
+ Change change2 = insert("repo", ins2);
+ insert("repo", newChangeWithStatus(repo, Change.Status.NEW));
Change[] expected = new Change[] {change2, change1};
assertQuery("status:closed", expected);
@@ -410,12 +427,12 @@
@Test
public void byStatusAbandoned() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
ChangeInserter ins1 = newChangeWithStatus(repo, Change.Status.MERGED);
- insert(repo, ins1);
+ insert("repo", ins1);
ChangeInserter ins2 = newChangeWithStatus(repo, Change.Status.ABANDONED);
- Change change1 = insert(repo, ins2);
- insert(repo, newChangeWithStatus(repo, Change.Status.NEW));
+ Change change1 = insert("repo", ins2);
+ insert("repo", newChangeWithStatus(repo, Change.Status.NEW));
assertQuery("status:abandoned", change1);
assertQuery("status:ABANDONED", change1);
@@ -424,10 +441,10 @@
@Test
public void byStatusPrefix() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
ChangeInserter ins1 = newChangeWithStatus(repo, Change.Status.NEW);
- Change change1 = insert(repo, ins1);
- insert(repo, newChangeWithStatus(repo, Change.Status.MERGED));
+ Change change1 = insert("repo", ins1);
+ Change change2 = insert("repo", newChangeWithStatus(repo, Change.Status.MERGED));
assertQuery("status:n", change1);
assertQuery("status:ne", change1);
@@ -435,6 +452,7 @@
assertQuery("status:N", change1);
assertQuery("status:nE", change1);
assertQuery("status:neW", change1);
+ assertQuery("status:m", change2);
Exception thrown = assertThrows(BadRequestException.class, () -> assertQuery("status:newx"));
assertThat(thrown).hasMessageThat().isEqualTo("Unrecognized value: newx");
thrown = assertThrows(BadRequestException.class, () -> assertQuery("status:nx"));
@@ -443,11 +461,11 @@
@Test
public void byPrivate() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
- Change change1 = insert(repo, newChange(repo), userId);
+ repo = createAndOpenProject("repo");
+ Change change1 = insert("repo", newChange(repo), userId);
Account.Id user2 =
accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId();
- Change change2 = insert(repo, newChange(repo), user2);
+ Change change2 = insert("repo", newChange(repo), user2);
// No private changes.
assertQuery("is:open", change2, change1);
@@ -467,8 +485,8 @@
@Test
public void byWip() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
- Change change1 = insert(repo, newChange(repo), userId);
+ repo = createAndOpenProject("repo");
+ Change change1 = insert("repo", newChange(repo), userId);
assertQuery("is:open", change1);
assertQuery("is:wip");
@@ -485,8 +503,8 @@
@Test
public void excludeWipChangeFromReviewersDashboards() throws Exception {
Account.Id user1 = createAccount("user1");
- TestRepository<Repo> repo = createProject("repo");
- Change change1 = insert(repo, newChangeWorkInProgress(repo), userId);
+ repo = createAndOpenProject("repo");
+ Change change1 = insert("repo", newChangeWorkInProgress(repo), userId);
assertQuery("is:wip", change1);
assertQuery("reviewer:" + user1);
@@ -502,8 +520,8 @@
@Test
public void byStarted() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
- Change change1 = insert(repo, newChangeWorkInProgress(repo));
+ repo = createAndOpenProject("repo");
+ Change change1 = insert("repo", newChangeWorkInProgress(repo));
assertQuery("is:started");
@@ -538,11 +556,11 @@
@Test
public void restorePendingReviewers() throws Exception {
Project.NameKey project = Project.nameKey("repo");
- TestRepository<Repo> repo = createProject(project.get());
+ repo = createAndOpenProject(project.get());
ConfigInput conf = new ConfigInput();
conf.enableReviewerByEmail = InheritableBoolean.TRUE;
gApi.projects().name(project.get()).config(conf);
- Change change1 = insert(repo, newChangeWorkInProgress(repo));
+ Change change1 = insert("repo", newChangeWorkInProgress(repo));
Account.Id user1 = createAccount("user1");
Account.Id user2 = createAccount("user2");
String email1 = "email1@example.com";
@@ -595,9 +613,9 @@
@Test
public void byCommit() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
ChangeInserter ins = newChange(repo);
- Change change = insert(repo, ins);
+ Change change = insert("repo", ins);
String sha = ins.getCommitId().name();
assertQuery("0000000000000000000000000000000000000000");
@@ -611,11 +629,11 @@
@Test
public void byOwner() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
- Change change1 = insert(repo, newChange(repo), userId);
+ repo = createAndOpenProject("repo");
+ Change change1 = insert("repo", newChange(repo), userId);
Account.Id user2 =
accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId();
- Change change2 = insert(repo, newChange(repo), user2);
+ Change change2 = insert("repo", newChange(repo), user2);
assertQuery("is:owner", change1);
assertQuery("owner:" + userId.get(), change1);
@@ -628,15 +646,16 @@
@Test
public void byUploader() throws Exception {
assume().that(getSchema().hasField(ChangeField.UPLOADER_SPEC)).isTrue();
- Account.Id user2 =
- accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId();
- CurrentUser user2CurrentUser = userFactory.create(user2);
- TestRepository<Repo> repo = createProject("repo");
- Change change1 = insert(repo, newChange(repo), userId);
+ repo = createAndOpenProject("repo");
+ Change change1 = insert("repo", newChange(repo), userId);
assertQuery("is:uploader", change1);
assertQuery("uploader:" + userId.get(), change1);
- change1 = newPatchSet(repo, change1, user2CurrentUser, /* message= */ Optional.empty());
+
+ Account.Id user2 = createAccount("anotheruser");
+ CurrentUser user2CurrentUser = userFactory.create(user2);
+
+ change1 = newPatchSet("repo", change1, user2CurrentUser, /* message= */ Optional.empty());
// Uploader has changed
assertQuery("uploader:" + userId.get());
assertQuery("uploader:" + user2.get(), change1);
@@ -669,7 +688,7 @@
}
private void byAuthorOrCommitterExact(String searchOperator) throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ createProject("repo");
PersonIdent johnDoe = new PersonIdent("John Doe", "john.doe@example.com");
PersonIdent john = new PersonIdent("John", "john@example.com");
PersonIdent doeSmith = new PersonIdent("Doe Smith", "doe_smith@example.com");
@@ -677,10 +696,10 @@
PersonIdent myself = new PersonIdent("I Am", ua.preferredEmail());
PersonIdent selfName = new PersonIdent("My Self", "my.self@example.com");
- Change change1 = createChange(repo, johnDoe);
- Change change2 = createChange(repo, john);
- Change change3 = createChange(repo, doeSmith);
- createChange(repo, selfName);
+ Change change1 = createChange("repo", johnDoe);
+ Change change2 = createChange("repo", john);
+ Change change3 = createChange("repo", doeSmith);
+ createChange("repo", selfName);
// Only email address.
assertQuery(searchOperator + "john.doe@example.com", change1);
@@ -705,19 +724,19 @@
assertQuery(searchOperator + "self");
// ':self' matches a change created with the current user's email address
- Change change5 = createChange(repo, myself);
+ Change change5 = createChange("repo", myself);
assertQuery(searchOperator + "me", change5);
assertQuery(searchOperator + "self", change5);
}
private void byAuthorOrCommitterFullText(String searchOperator) throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ createProject("repo");
PersonIdent johnDoe = new PersonIdent("John Doe", "john.doe@example.com");
PersonIdent john = new PersonIdent("John", "john@example.com");
PersonIdent doeSmith = new PersonIdent("Doe Smith", "doe_smith@example.com");
- Change change1 = createChange(repo, johnDoe);
- Change change2 = createChange(repo, john);
- Change change3 = createChange(repo, doeSmith);
+ Change change1 = createChange("repo", johnDoe);
+ Change change2 = createChange("repo", john);
+ Change change3 = createChange("repo", doeSmith);
// By exact name.
assertQuery(searchOperator + "\"John Doe\"", change1);
@@ -738,20 +757,25 @@
assertThat(thrown).hasMessageThat().contains("invalid value");
}
- protected Change createChange(TestRepository<Repo> repo, PersonIdent person) throws Exception {
- RevCommit commit =
- repo.parseBody(repo.commit().message("message").author(person).committer(person).create());
- return insert(repo, newChangeForCommit(repo, commit), null);
+ @CanIgnoreReturnValue
+ protected Change createChange(String repoName, PersonIdent person) throws Exception {
+ try (TestRepository<Repository> repo =
+ new TestRepository<>(repoManager.openRepository(Project.nameKey(repoName)))) {
+ RevCommit commit =
+ repo.parseBody(
+ repo.commit().message("message").author(person).committer(person).create());
+ return insert("repo", newChangeForCommit(repo, commit), null);
+ }
}
@Test
public void byOwnerIn() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
- Change change1 = insert(repo, newChange(repo), userId);
+ repo = createAndOpenProject("repo");
+ Change change1 = insert("repo", newChange(repo), userId);
Account.Id user2 =
accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId();
- Change change2 = insert(repo, newChange(repo), user2);
- Change change3 = insert(repo, newChange(repo), user2);
+ Change change2 = insert("repo", newChange(repo), user2);
+ Change change3 = insert("repo", newChange(repo), user2);
gApi.changes().id(change3.getId().get()).current().review(ReviewInput.approve());
gApi.changes().id(change3.getId().get()).current().submit();
@@ -764,24 +788,25 @@
@Test
public void byUploaderIn() throws Exception {
assume().that(getSchema().hasField(ChangeField.UPLOADER_SPEC)).isTrue();
- TestRepository<Repo> repo = createProject("repo");
- Change change1 = insert(repo, newChange(repo), userId);
+ repo = createAndOpenProject("repo");
+ Change change1 = insert("repo", newChange(repo), userId);
+
assertQuery("uploaderin:Administrators", change1);
- Account.Id user2 =
- accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId();
+ Account.Id user2 = createAccount("anotheruser");
CurrentUser user2CurrentUser = userFactory.create(user2);
- change1 = newPatchSet(repo, change1, user2CurrentUser, /* message= */ Optional.empty());
+ change1 = newPatchSet("repo", change1, user2CurrentUser, /* message= */ Optional.empty());
+
assertQuery("uploaderin:Administrators");
assertQuery("uploaderin:\"Registered Users\"", change1);
}
@Test
public void byProject() throws Exception {
- TestRepository<Repo> repo1 = createProject("repo1");
- TestRepository<Repo> repo2 = createProject("repo2");
- Change change1 = insert(repo1, newChange(repo1));
- Change change2 = insert(repo2, newChange(repo2));
+ createProject("repo1");
+ createProject("repo2");
+ Change change1 = insert("repo1", newChange("repo1"));
+ Change change2 = insert("repo2", newChange("repo2"));
assertQuery("project:foo");
assertQuery("project:repo");
@@ -791,16 +816,16 @@
@Test
public void byProjectWithHidden() throws Exception {
- TestRepository<Repo> hiddenProject = createProject("hiddenProject");
- insert(hiddenProject, newChange(hiddenProject));
+ createProject("hiddenProject");
+ insert("hiddenProject", newChange("hiddenProject"));
projectOperations
.project(Project.nameKey("hiddenProject"))
.forUpdate()
.add(block(Permission.READ).ref("refs/*").group(REGISTERED_USERS))
.update();
- TestRepository<Repo> visibleProject = createProject("visibleProject");
- Change visibleChange = insert(visibleProject, newChange(visibleProject));
+ createProject("visibleProject");
+ Change visibleChange = insert("visibleProject", newChange("visibleProject"));
assertQuery("project:visibleProject", visibleChange);
assertQuery("project:hiddenProject");
assertQuery("project:visibleProject OR project:hiddenProject", visibleChange);
@@ -808,13 +833,13 @@
@Test
public void byParentOf() throws Exception {
- TestRepository<Repo> repo1 = createProject("repo1");
- RevCommit commit1 = repo1.parseBody(repo1.commit().message("message").create());
- Change change1 = insert(repo1, newChangeForCommit(repo1, commit1));
- RevCommit commit2 = repo1.parseBody(repo1.commit(commit1));
- Change change2 = insert(repo1, newChangeForCommit(repo1, commit2));
- RevCommit commit3 = repo1.parseBody(repo1.commit(commit1, commit2));
- Change change3 = insert(repo1, newChangeForCommit(repo1, commit3));
+ repo = createAndOpenProject("repo1");
+ RevCommit commit1 = repo.parseBody(repo.commit().message("message").create());
+ Change change1 = insert("repo1", newChangeForCommit(repo, commit1));
+ RevCommit commit2 = repo.parseBody(repo.commit(commit1));
+ Change change2 = insert("repo1", newChangeForCommit(repo, commit2));
+ RevCommit commit3 = repo.parseBody(repo.commit(commit1, commit2));
+ Change change3 = insert("repo1", newChangeForCommit(repo, commit3));
assertQuery("parentof:" + change1.getId().get());
assertQuery("parentof:" + change1.getKey().get());
@@ -826,10 +851,10 @@
@Test
public void byParentProject() throws Exception {
- TestRepository<Repo> repo1 = createProject("repo1");
- TestRepository<Repo> repo2 = createProject("repo2", "repo1");
- Change change1 = insert(repo1, newChange(repo1));
- Change change2 = insert(repo2, newChange(repo2));
+ createProject("repo1");
+ createProject("repo2", "repo1");
+ Change change1 = insert("repo1", newChange("repo1"));
+ Change change2 = insert("repo2", newChange("repo2"));
assertQuery("parentproject:repo1", change2, change1);
assertQuery("parentproject:repo2", change2);
@@ -837,10 +862,10 @@
@Test
public void byProjectPrefix() throws Exception {
- TestRepository<Repo> repo1 = createProject("repo1");
- TestRepository<Repo> repo2 = createProject("repo2");
- Change change1 = insert(repo1, newChange(repo1));
- Change change2 = insert(repo2, newChange(repo2));
+ createProject("repo1");
+ createProject("repo2");
+ Change change1 = insert("repo1", newChange("repo1"));
+ Change change2 = insert("repo2", newChange("repo2"));
assertQuery("projects:foo");
assertQuery("projects:repo1", change1);
@@ -850,10 +875,10 @@
@Test
public void byRepository() throws Exception {
- TestRepository<Repo> repo1 = createProject("repo1");
- TestRepository<Repo> repo2 = createProject("repo2");
- Change change1 = insert(repo1, newChange(repo1));
- Change change2 = insert(repo2, newChange(repo2));
+ createProject("repo1");
+ createProject("repo2");
+ Change change1 = insert("repo1", newChange("repo1"));
+ Change change2 = insert("repo2", newChange("repo2"));
assertQuery("repository:foo");
assertQuery("repository:repo");
@@ -863,10 +888,10 @@
@Test
public void byParentRepository() throws Exception {
- TestRepository<Repo> repo1 = createProject("repo1");
- TestRepository<Repo> repo2 = createProject("repo2", "repo1");
- Change change1 = insert(repo1, newChange(repo1));
- Change change2 = insert(repo2, newChange(repo2));
+ createProject("repo1");
+ createProject("repo2", "repo1");
+ Change change1 = insert("repo1", newChange("repo1"));
+ Change change2 = insert("repo2", newChange("repo2"));
assertQuery("parentrepository:repo1", change2, change1);
assertQuery("parentrepository:repo2", change2);
@@ -874,10 +899,10 @@
@Test
public void byRepositoryPrefix() throws Exception {
- TestRepository<Repo> repo1 = createProject("repo1");
- TestRepository<Repo> repo2 = createProject("repo2");
- Change change1 = insert(repo1, newChange(repo1));
- Change change2 = insert(repo2, newChange(repo2));
+ createProject("repo1");
+ createProject("repo2");
+ Change change1 = insert("repo1", newChange("repo1"));
+ Change change2 = insert("repo2", newChange("repo2"));
assertQuery("repositories:foo");
assertQuery("repositories:repo1", change1);
@@ -887,10 +912,10 @@
@Test
public void byRepo() throws Exception {
- TestRepository<Repo> repo1 = createProject("repo1");
- TestRepository<Repo> repo2 = createProject("repo2");
- Change change1 = insert(repo1, newChange(repo1));
- Change change2 = insert(repo2, newChange(repo2));
+ createProject("repo1");
+ createProject("repo2");
+ Change change1 = insert("repo1", newChange("repo1"));
+ Change change2 = insert("repo2", newChange("repo2"));
assertQuery("repo:foo");
assertQuery("repo:repo");
@@ -900,10 +925,10 @@
@Test
public void byParentRepo() throws Exception {
- TestRepository<Repo> repo1 = createProject("repo1");
- TestRepository<Repo> repo2 = createProject("repo2", "repo1");
- Change change1 = insert(repo1, newChange(repo1));
- Change change2 = insert(repo2, newChange(repo2));
+ createProject("repo1");
+ createProject("repo2", "repo1");
+ Change change1 = insert("repo1", newChange("repo1"));
+ Change change2 = insert("repo2", newChange("repo2"));
assertQuery("parentrepo:repo1", change2, change1);
assertQuery("parentrepo:repo2", change2);
@@ -911,10 +936,10 @@
@Test
public void byRepoPrefix() throws Exception {
- TestRepository<Repo> repo1 = createProject("repo1");
- TestRepository<Repo> repo2 = createProject("repo2");
- Change change1 = insert(repo1, newChange(repo1));
- Change change2 = insert(repo2, newChange(repo2));
+ createProject("repo1");
+ createProject("repo2");
+ Change change1 = insert("repo1", newChange("repo1"));
+ Change change2 = insert("repo2", newChange("repo2"));
assertQuery("repos:foo");
assertQuery("repos:repo1", change1);
@@ -924,9 +949,9 @@
@Test
public void byBranchAndRef() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
- Change change1 = insert(repo, newChangeForBranch(repo, "master"));
- Change change2 = insert(repo, newChangeForBranch(repo, "branch"));
+ repo = createAndOpenProject("repo");
+ Change change1 = insert("repo", newChangeForBranch(repo, "master"));
+ Change change2 = insert("repo", newChangeForBranch(repo, "branch"));
assertQuery("branch:foo");
assertQuery("branch:master", change1);
@@ -943,26 +968,26 @@
@Test
public void byTopic() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
ChangeInserter ins1 = newChangeWithTopic(repo, "feature1");
- Change change1 = insert(repo, ins1);
+ Change change1 = insert("repo", ins1);
ChangeInserter ins2 = newChangeWithTopic(repo, "feature2");
- Change change2 = insert(repo, ins2);
+ Change change2 = insert("repo", ins2);
ChangeInserter ins3 = newChangeWithTopic(repo, "Cherrypick-feature2");
- Change change3 = insert(repo, ins3);
+ Change change3 = insert("repo", ins3);
ChangeInserter ins4 = newChangeWithTopic(repo, "feature2-fixup");
- Change change4 = insert(repo, ins4);
+ Change change4 = insert("repo", ins4);
ChangeInserter ins5 = newChangeWithTopic(repo, "https://gerrit.local");
- Change change5 = insert(repo, ins5);
+ Change change5 = insert("repo", ins5);
ChangeInserter ins6 = newChangeWithTopic(repo, "git_gerrit_training");
- Change change6 = insert(repo, ins6);
+ Change change6 = insert("repo", ins6);
- Change change_no_topic = insert(repo, newChange(repo));
+ Change changeNoTopic = insert("repo", newChange(repo));
assertQuery("intopic:foo");
assertQuery("intopic:feature1", change1);
@@ -971,8 +996,8 @@
assertQuery("intopic:feature2", change4, change3, change2);
assertQuery("intopic:fixup", change4);
assertQuery("intopic:gerrit", change6, change5);
- assertQuery("topic:\"\"", change_no_topic);
- assertQuery("intopic:\"\"", change_no_topic);
+ assertQuery("topic:\"\"", changeNoTopic);
+ assertQuery("intopic:\"\"", changeNoTopic);
assume().that(getSchema().hasField(ChangeField.PREFIX_TOPIC)).isTrue();
assertQuery("prefixtopic:feature", change4, change2, change1);
@@ -982,16 +1007,16 @@
@Test
public void byTopicRegex() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
ChangeInserter ins1 = newChangeWithTopic(repo, "feature1");
- Change change1 = insert(repo, ins1);
+ Change change1 = insert("repo", ins1);
ChangeInserter ins2 = newChangeWithTopic(repo, "Cherrypick-feature1");
- Change change2 = insert(repo, ins2);
+ Change change2 = insert("repo", ins2);
ChangeInserter ins3 = newChangeWithTopic(repo, "feature1-fixup");
- Change change3 = insert(repo, ins3);
+ Change change3 = insert("repo", ins3);
assertQuery("intopic:^feature1.*", change3, change1);
assertQuery("intopic:{^.*feature1$}", change2, change1);
@@ -999,13 +1024,13 @@
@Test
public void byMessageExact() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
RevCommit commit1 = repo.parseBody(repo.commit().message("one").create());
- Change change1 = insert(repo, newChangeForCommit(repo, commit1));
+ Change change1 = insert("repo", newChangeForCommit(repo, commit1));
RevCommit commit2 = repo.parseBody(repo.commit().message("two").create());
- Change change2 = insert(repo, newChangeForCommit(repo, commit2));
+ Change change2 = insert("repo", newChangeForCommit(repo, commit2));
RevCommit commit3 = repo.parseBody(repo.commit().message("A great \"fix\" to my bug").create());
- Change change3 = insert(repo, newChangeForCommit(repo, commit3));
+ Change change3 = insert("repo", newChangeForCommit(repo, commit3));
assertQuery("message:foo");
assertQuery("message:one", change1);
@@ -1016,16 +1041,16 @@
@Test
public void byMessageRegEx() throws Exception {
assume().that(getSchema().hasField(ChangeField.COMMIT_MESSAGE_EXACT)).isTrue();
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
RevCommit commit1 = repo.parseBody(repo.commit().message("aaaabcc").create());
- Change change1 = insert(repo, newChangeForCommit(repo, commit1));
+ Change change1 = insert("repo", newChangeForCommit(repo, commit1));
RevCommit commit2 = repo.parseBody(repo.commit().message("aaaacc").create());
- Change change2 = insert(repo, newChangeForCommit(repo, commit2));
+ Change change2 = insert("repo", newChangeForCommit(repo, commit2));
RevCommit commit3 = repo.parseBody(repo.commit().message("Title\n\nHELLO WORLD").create());
- Change change3 = insert(repo, newChangeForCommit(repo, commit3));
+ Change change3 = insert("repo", newChangeForCommit(repo, commit3));
RevCommit commit4 =
repo.parseBody(repo.commit().message("Title\n\nfoobar hello WORLD").create());
- Change change4 = insert(repo, newChangeForCommit(repo, commit4));
+ Change change4 = insert("repo", newChangeForCommit(repo, commit4));
assertQuery("message:\"^aaaa(b|c)*\"", change2, change1);
assertQuery("message:\"^aaaa(c)*c.*\"", change2);
@@ -1037,7 +1062,7 @@
@Test
public void bySubject() throws Exception {
assume().that(getSchema().hasField(ChangeField.SUBJECT_SPEC)).isTrue();
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
RevCommit commit1 =
repo.parseBody(
repo.commit()
@@ -1046,7 +1071,7 @@
+ "Message body\n\n"
+ "Change-Id: I986c6a013dd5b3a2e8a0271c04deac2c9752b920")
.create());
- Change change1 = insert(repo, newChangeForCommit(repo, commit1));
+ Change change1 = insert("repo", newChangeForCommit(repo, commit1));
RevCommit commit2 =
repo.parseBody(
repo.commit()
@@ -1055,7 +1080,7 @@
+ "Message body for another commit\n\n"
+ "Change-Id: I986c6a013dd5b3a2e8a0271c04deac2c9752b921")
.create());
- Change change2 = insert(repo, newChangeForCommit(repo, commit2));
+ Change change2 = insert("repo", newChangeForCommit(repo, commit2));
RevCommit commit3 =
repo.parseBody(
repo.commit()
@@ -1064,7 +1089,7 @@
+ "Last message body\n\n"
+ "Change-Id: I986c6a013dd5b3a2e8a0271c04deac2c9752b921")
.create());
- Change change3 = insert(repo, newChangeForCommit(repo, commit3));
+ Change change3 = insert("repo", newChangeForCommit(repo, commit3));
assertQuery("subject:First", change1);
assertQuery("subject:Second", change2);
@@ -1074,7 +1099,7 @@
assertQuery("subject:body");
change1 =
newPatchSet(
- repo,
+ "repo",
change1,
user,
Optional.of("Rework of commit with test subject\n\n" + "Message body\n\n"));
@@ -1084,12 +1109,60 @@
}
@Test
+ public void bySubjectPrefix() throws Exception {
+ assume().that(getSchema().hasField(ChangeField.PREFIX_SUBJECT_SPEC)).isTrue();
+ repo = createAndOpenProject("repo");
+ RevCommit commit1 =
+ repo.parseBody(
+ repo.commit()
+ .message(
+ "[FOO123] First commit with test subject\n\n"
+ + "Message body\n\n"
+ + "Change-Id: I986c6a013dd5b3a2e8a0271c04deac2c9752b920")
+ .create());
+ Change change1 = insert("repo", newChangeForCommit(repo, commit1));
+ RevCommit commit2 =
+ repo.parseBody(
+ repo.commit()
+ .message(
+ "[BAR45] Second commit with test subject\n\n"
+ + "Message body for another commit\n\n"
+ + "Change-Id: I986c6a013dd5b3a2e8a0271c04deac2c9752b921")
+ .create());
+ Change change2 = insert("repo", newChangeForCommit(repo, commit2));
+ RevCommit commit3 =
+ repo.parseBody(
+ repo.commit()
+ .message(
+ "[FOO99] Third commit with test subject\n\n"
+ + "Last message body\n\n"
+ + "Change-Id: I986c6a013dd5b3a2e8a0271c04deac2c9752b921")
+ .create());
+ Change change3 = insert("repo", newChangeForCommit(repo, commit3));
+
+ assertQuery("prefixsubject:\"[FOO\"", change3, change1);
+ assertQuery("prefixsubject:\"[BAR\"", change2);
+ assertQuery("prefixsubject:\"[FOO1\"", change1);
+ assertQuery("prefixsubject:\"[FOO123]\"", change1);
+ assertQuery("prefixsubject:\"[\"", change3, change2, change1);
+ assertQuery("prefixsubject:FOO");
+ change1 =
+ newPatchSet(
+ "repo",
+ change1,
+ user,
+ Optional.of("[BAR123] Rework of commit with test subject\n\n" + "Message body\n\n"));
+ assertQuery("prefixsubject:\"[FOO\"", change3);
+ assertQuery("prefixsubject:\"[BAR\"", change1, change2);
+ }
+
+ @Test
public void fullTextWithNumbers() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
RevCommit commit1 = repo.parseBody(repo.commit().message("12345 67890").create());
- Change change1 = insert(repo, newChangeForCommit(repo, commit1));
+ Change change1 = insert("repo", newChangeForCommit(repo, commit1));
RevCommit commit2 = repo.parseBody(repo.commit().message("12346 67891").create());
- Change change2 = insert(repo, newChangeForCommit(repo, commit2));
+ Change change2 = insert("repo", newChangeForCommit(repo, commit2));
assertQuery("message:1234");
assertQuery("message:12345", change1);
@@ -1098,13 +1171,13 @@
@Test
public void fullTextMultipleTerms() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
RevCommit commit1 = repo.parseBody(repo.commit().message("Signed-off: owner").create());
- Change change1 = insert(repo, newChangeForCommit(repo, commit1));
+ Change change1 = insert("repo", newChangeForCommit(repo, commit1));
RevCommit commit2 = repo.parseBody(repo.commit().message("Signed by owner").create());
- Change change2 = insert(repo, newChangeForCommit(repo, commit2));
+ Change change2 = insert("repo", newChangeForCommit(repo, commit2));
RevCommit commit3 = repo.parseBody(repo.commit().message("This change is off").create());
- Change change3 = insert(repo, newChangeForCommit(repo, commit3));
+ Change change3 = insert("repo", newChangeForCommit(repo, commit3));
assertQuery("message:\"Signed-off: owner\"", change1);
assertQuery("message:\"Signed\"", change2, change1);
@@ -1113,11 +1186,11 @@
@Test
public void byMessageMixedCase() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
RevCommit commit1 = repo.parseBody(repo.commit().message("Hello gerrit").create());
- Change change1 = insert(repo, newChangeForCommit(repo, commit1));
+ Change change1 = insert("repo", newChangeForCommit(repo, commit1));
RevCommit commit2 = repo.parseBody(repo.commit().message("Hello Gerrit").create());
- Change change2 = insert(repo, newChangeForCommit(repo, commit2));
+ Change change2 = insert("repo", newChangeForCommit(repo, commit2));
assertQuery("message:gerrit", change2, change1);
assertQuery("message:Gerrit", change2, change1);
@@ -1125,16 +1198,16 @@
@Test
public void byMessageSubstring() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
RevCommit commit1 = repo.parseBody(repo.commit().message("https://gerrit.local").create());
- Change change1 = insert(repo, newChangeForCommit(repo, commit1));
+ Change change1 = insert("repo", newChangeForCommit(repo, commit1));
assertQuery("message:gerrit", change1);
}
@Test
public void byLabel() throws Exception {
- accountManager.authenticate(authRequestFactory.createForUser("anotheruser"));
- TestRepository<Repo> repo = createProject("repo");
+ Account.Id anotherUser = createAccount("anotheruser");
+ repo = createAndOpenProject("repo");
ChangeInserter ins = newChange(repo);
ChangeInserter ins2 = newChange(repo);
ChangeInserter ins3 = newChange(repo);
@@ -1142,24 +1215,24 @@
ChangeInserter ins5 = newChange(repo);
ChangeInserter ins6 = newChange(repo);
- Change reviewMinus2Change = insert(repo, ins);
+ Change reviewMinus2Change = insert("repo", ins);
gApi.changes().id(reviewMinus2Change.getId().get()).current().review(ReviewInput.reject());
- Change reviewMinus1Change = insert(repo, ins2);
+ Change reviewMinus1Change = insert("repo", ins2);
gApi.changes().id(reviewMinus1Change.getId().get()).current().review(ReviewInput.dislike());
- Change noLabelChange = insert(repo, ins3);
+ Change noLabelChange = insert("repo", ins3);
- Change reviewPlus1Change = insert(repo, ins4);
+ Change reviewPlus1Change = insert("repo", ins4);
gApi.changes().id(reviewPlus1Change.getId().get()).current().review(ReviewInput.recommend());
- Change reviewTwoPlus1Change = insert(repo, ins5);
+ Change reviewTwoPlus1Change = insert("repo", ins5);
gApi.changes().id(reviewTwoPlus1Change.getId().get()).current().review(ReviewInput.recommend());
requestContext.setContext(newRequestContext(createAccount("user1")));
gApi.changes().id(reviewTwoPlus1Change.getId().get()).current().review(ReviewInput.recommend());
requestContext.setContext(newRequestContext(userId));
- Change reviewPlus2Change = insert(repo, ins6);
+ Change reviewPlus2Change = insert("repo", ins6);
gApi.changes().id(reviewPlus2Change.getId().get()).current().review(ReviewInput.approve());
Map<String, Short> m =
@@ -1223,9 +1296,15 @@
assertQuery("label:Code-Review<=-2", reviewMinus2Change);
assertQuery("label:Code-Review<-2");
- assertQuery("label:Code-Review=+1,anotheruser");
- assertQuery("label:Code-Review=+1,user", reviewTwoPlus1Change, reviewPlus1Change);
- assertQuery("label:Code-Review=+1,user=user", reviewTwoPlus1Change, reviewPlus1Change);
+ assertQuery("label:Code-Review=+1," + anotherUser);
+ assertQuery(
+ String.format("label:Code-Review=+1,%s", userAccount.preferredEmail()),
+ reviewTwoPlus1Change,
+ reviewPlus1Change);
+ assertQuery(
+ String.format("label:Code-Review=+1,user=%s", userAccount.preferredEmail()),
+ reviewTwoPlus1Change,
+ reviewPlus1Change);
assertQuery("label:Code-Review=+1,Administrators", reviewTwoPlus1Change, reviewPlus1Change);
assertQuery(
"label:Code-Review=+1,group=Administrators", reviewTwoPlus1Change, reviewPlus1Change);
@@ -1292,9 +1371,8 @@
@Test
public void byLabelMulti() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
- Project.NameKey project =
- Project.nameKey(repo.getRepository().getDescription().getRepositoryName());
+ Project.NameKey project = Project.nameKey("repo");
+ repo = createAndOpenProject(project.get());
LabelType verified =
label(LabelId.VERIFIED, value(1, "Passes"), value(0, "No score"), value(-1, "Failed"));
@@ -1321,25 +1399,25 @@
ChangeInserter ins5 = newChange(repo);
// CR+1
- Change reviewCRplus1 = insert(repo, ins);
+ Change reviewCRplus1 = insert(project.get(), ins);
gApi.changes().id(reviewCRplus1.getId().get()).current().review(ReviewInput.recommend());
// CR+2
- Change reviewCRplus2 = insert(repo, ins2);
+ Change reviewCRplus2 = insert(project.get(), ins2);
gApi.changes().id(reviewCRplus2.getId().get()).current().review(ReviewInput.approve());
// CR+1 VR+1
- Change reviewCRplus1VRplus1 = insert(repo, ins3);
+ Change reviewCRplus1VRplus1 = insert(project.get(), ins3);
gApi.changes().id(reviewCRplus1VRplus1.getId().get()).current().review(ReviewInput.recommend());
gApi.changes().id(reviewCRplus1VRplus1.getId().get()).current().review(reviewVerified);
// CR+2 VR+1
- Change reviewCRplus2VRplus1 = insert(repo, ins4);
+ Change reviewCRplus2VRplus1 = insert(project.get(), ins4);
gApi.changes().id(reviewCRplus2VRplus1.getId().get()).current().review(ReviewInput.approve());
gApi.changes().id(reviewCRplus2VRplus1.getId().get()).current().review(reviewVerified);
// VR+1
- Change reviewVRplus1 = insert(repo, ins5);
+ Change reviewVRplus1 = insert(project.get(), ins5);
gApi.changes().id(reviewVRplus1.getId().get()).current().review(reviewVerified);
assertQuery("label:Code-Review=+1", reviewCRplus1VRplus1, reviewCRplus1);
@@ -1358,28 +1436,28 @@
@Test
public void byLabelNotOwner() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
ChangeInserter ins = newChange(repo);
Account.Id user1 = createAccount("user1");
- Change reviewPlus1Change = insert(repo, ins);
+ Change reviewPlus1Change = insert("repo", ins);
// post a review with user1
requestContext.setContext(newRequestContext(user1));
gApi.changes().id(reviewPlus1Change.getId().get()).current().review(ReviewInput.recommend());
- assertQuery("label:Code-Review=+1,user=user1", reviewPlus1Change);
+ assertQuery("label:Code-Review=+1,user=" + user1, reviewPlus1Change);
assertQuery("label:Code-Review=+1,owner");
}
@Test
public void byLabelNonUploader() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
ChangeInserter ins = newChange(repo);
Account.Id user1 = createAccount("user1");
// create a change with "user"
- Change reviewPlus1Change = insert(repo, ins);
+ Change reviewPlus1Change = insert("repo", ins);
// add a +1 vote with "user". Query doesn't match since voter is the uploader.
gApi.changes().id(reviewPlus1Change.getId().get()).current().review(ReviewInput.recommend());
@@ -1418,8 +1496,8 @@
@Test
public void byLabelGroup() throws Exception {
Account.Id user1 = createAccount("user1");
- createAccount("user2");
- TestRepository<Repo> repo = createProject("repo");
+ Account.Id user2 = createAccount("user2");
+ repo = createAndOpenProject("repo");
// create group and add users
String g1 = createGroup("group1", "Administrators");
@@ -1428,7 +1506,7 @@
gApi.groups().id(g2).addMembers("user2");
// create a change
- Change change1 = insert(repo, newChange(repo), user1);
+ Change change1 = insert("repo", newChange(repo), user1);
// post a review with user1
requestContext.setContext(newRequestContext(user1));
@@ -1441,8 +1519,8 @@
requestContext.setContext(newRequestContext(userId));
assertQuery("label:Code-Review=+1,group1", change1);
assertQuery("label:Code-Review=+1,group=group1", change1);
- assertQuery("label:Code-Review=+1,user=user1", change1);
- assertQuery("label:Code-Review=+1,user=user2");
+ assertQuery("label:Code-Review=+1,user=" + user1, change1);
+ assertQuery("label:Code-Review=+1,user=" + user2);
assertQuery("label:Code-Review=+1,group=group2");
}
@@ -1450,7 +1528,7 @@
public void byLabelExternalGroup() throws Exception {
Account.Id user1 = createAccount("user1");
Account.Id user2 = createAccount("user2");
- TestRepository<InMemoryRepositoryManager.Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
// create group and add users
AccountGroup.UUID external_group1 = AccountGroup.uuid("testbackend:group1");
@@ -1462,8 +1540,8 @@
testGroupBackend.setMembershipsOf(
user2, new ListGroupMembership(ImmutableList.of(external_group2)));
- Change change1 = insert(repo, newChange(repo), user1);
- Change change2 = insert(repo, newChange(repo), user1);
+ Change change1 = insert("repo", newChange(repo), user1);
+ Change change2 = insert("repo", newChange(repo), user1);
// post a review with user1 and other_user
requestContext.setContext(newRequestContext(user1));
@@ -1479,18 +1557,18 @@
assertQuery("label:Code-Review=+1," + external_group1.get(), change1);
assertQuery("label:Code-Review=+1,group=" + external_group1.get(), change1);
- assertQuery("label:Code-Review=+1,user=user1", change1);
- assertQuery("label:Code-Review=+1,user=user2");
+ assertQuery("label:Code-Review=+1,user=" + user1, change1);
+ assertQuery("label:Code-Review=+1,user=" + user2);
assertQuery("label:Code-Review=+1,group=" + external_group2.get());
}
@Test
public void limit() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
Change last = null;
int n = 5;
for (int i = 0; i < n; i++) {
- last = insert(repo, newChange(repo));
+ last = insert("repo", newChange(repo));
}
for (int i = 1; i <= n + 2; i++) {
@@ -1515,10 +1593,10 @@
@Test
public void start() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
List<Change> changes = new ArrayList<>();
for (int i = 0; i < 2; i++) {
- changes.add(insert(repo, newChange(repo)));
+ changes.add(insert("repo", newChange(repo)));
}
assertQuery("status:new", changes.get(1), changes.get(0));
@@ -1535,10 +1613,10 @@
@Test
public void startWithLimit() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
List<Change> changes = new ArrayList<>();
for (int i = 0; i < 3; i++) {
- changes.add(insert(repo, newChange(repo)));
+ changes.add(insert("repo", newChange(repo)));
}
assertQuery("status:new limit:2", changes.get(2), changes.get(1));
@@ -1549,8 +1627,8 @@
@Test
public void maxPages() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
- Change change = insert(repo, newChange(repo));
+ repo = createAndOpenProject("repo");
+ Change change = insert("repo", newChange(repo));
QueryRequest query = newQuery("status:new").withLimit(10);
assertQuery(query, change);
@@ -1565,12 +1643,12 @@
@Test
public void updateOrder() throws Exception {
resetTimeWithClockStep(2, MINUTES);
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
List<ChangeInserter> inserters = new ArrayList<>();
List<Change> changes = new ArrayList<>();
for (int i = 0; i < 5; i++) {
inserters.add(newChange(repo));
- changes.add(insert(repo, inserters.get(i)));
+ changes.add(insert("repo", inserters.get(i)));
}
for (int i : ImmutableList.of(2, 0, 1, 4, 3)) {
@@ -1592,10 +1670,10 @@
@Test
public void updatedOrder() throws Exception {
resetTimeWithClockStep(1, SECONDS);
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
ChangeInserter ins1 = newChange(repo);
- Change change1 = insert(repo, ins1);
- Change change2 = insert(repo, newChange(repo));
+ Change change1 = insert("repo", ins1);
+ Change change2 = insert("repo", newChange(repo));
assertThat(lastUpdatedMs(change1)).isLessThan(lastUpdatedMs(change2));
assertQuery("status:new", change2, change1);
@@ -1613,12 +1691,12 @@
@Test
public void filterOutMoreThanOnePageOfResults() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
- Change change = insert(repo, newChange(repo), userId);
+ repo = createAndOpenProject("repo");
+ Change change = insert("repo", newChange(repo), userId);
Account.Id user2 =
accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId();
for (int i = 0; i < 5; i++) {
- insert(repo, newChange(repo), user2);
+ insert("repo", newChange(repo), user2);
}
assertQuery("status:new ownerin:Administrators", change);
@@ -1627,11 +1705,11 @@
@Test
public void filterOutAllResults() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
Account.Id user2 =
accountManager.authenticate(authRequestFactory.createForUser("anotheruser")).getAccountId();
for (int i = 0; i < 5; i++) {
- insert(repo, newChange(repo), user2);
+ insert("repo", newChange(repo), user2);
}
assertQuery("status:new ownerin:Administrators");
@@ -1640,8 +1718,8 @@
@Test
public void byFileExact() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
- Change change = insert(repo, newChangeWithFiles(repo, "dir/file1", "dir/file2"));
+ repo = createAndOpenProject("repo");
+ Change change = insert("repo", newChangeWithFiles(repo, "dir/file1", "dir/file2"));
assertQuery("file:file");
assertQuery("file:dir", change);
@@ -1653,8 +1731,8 @@
@Test
public void byFileRegex() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
- Change change = insert(repo, newChangeWithFiles(repo, "dir/file1", "dir/file2"));
+ repo = createAndOpenProject("repo");
+ Change change = insert("repo", newChangeWithFiles(repo, "dir/file1", "dir/file2"));
assertQuery("file:.*file.*");
assertQuery("file:^file.*"); // Whole path only.
@@ -1663,8 +1741,8 @@
@Test
public void byPathExact() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
- Change change = insert(repo, newChangeWithFiles(repo, "dir/file1", "dir/file2"));
+ repo = createAndOpenProject("repo");
+ Change change = insert("repo", newChangeWithFiles(repo, "dir/file1", "dir/file2"));
assertQuery("path:file");
assertQuery("path:dir");
@@ -1676,8 +1754,8 @@
@Test
public void byPathRegex() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
- Change change = insert(repo, newChangeWithFiles(repo, "dir/file1", "dir/file2"));
+ repo = createAndOpenProject("repo");
+ Change change = insert("repo", newChangeWithFiles(repo, "dir/file1", "dir/file2"));
assertQuery("path:.*file.*");
assertQuery("path:^dir.file.*", change);
@@ -1685,12 +1763,12 @@
@Test
public void byExtension() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
- Change change1 = insert(repo, newChangeWithFiles(repo, "foo.h", "foo.cc"));
- Change change2 = insert(repo, newChangeWithFiles(repo, "bar.H", "bar.CC"));
- Change change3 = insert(repo, newChangeWithFiles(repo, "dir/baz.h", "dir/baz.cc"));
- Change change4 = insert(repo, newChangeWithFiles(repo, "Quux.java", "foo"));
- Change change5 = insert(repo, newChangeWithFiles(repo, "foo"));
+ repo = createAndOpenProject("repo");
+ Change change1 = insert("repo", newChangeWithFiles(repo, "foo.h", "foo.cc"));
+ Change change2 = insert("repo", newChangeWithFiles(repo, "bar.H", "bar.CC"));
+ Change change3 = insert("repo", newChangeWithFiles(repo, "dir/baz.h", "dir/baz.cc"));
+ Change change4 = insert("repo", newChangeWithFiles(repo, "Quux.java", "foo"));
+ Change change5 = insert("repo", newChangeWithFiles(repo, "foo"));
assertQuery("extension:java", change4);
assertQuery("ext:java", change4);
@@ -1706,14 +1784,14 @@
@Test
public void byOnlyExtensions() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
- Change change1 = insert(repo, newChangeWithFiles(repo, "foo.h", "foo.cc", "bar.cc"));
- Change change2 = insert(repo, newChangeWithFiles(repo, "bar.H", "bar.CC", "foo.H"));
- Change change3 = insert(repo, newChangeWithFiles(repo, "foo.CC", "bar.cc"));
- Change change4 = insert(repo, newChangeWithFiles(repo, "dir/baz.h", "dir/baz.cc"));
- Change change5 = insert(repo, newChangeWithFiles(repo, "Quux.java"));
- Change change6 = insert(repo, newChangeWithFiles(repo, "foo.txt", "foo"));
- Change change7 = insert(repo, newChangeWithFiles(repo, "foo"));
+ repo = createAndOpenProject("repo");
+ Change change1 = insert("repo", newChangeWithFiles(repo, "foo.h", "foo.cc", "bar.cc"));
+ Change change2 = insert("repo", newChangeWithFiles(repo, "bar.H", "bar.CC", "foo.H"));
+ Change change3 = insert("repo", newChangeWithFiles(repo, "foo.CC", "bar.cc"));
+ Change change4 = insert("repo", newChangeWithFiles(repo, "dir/baz.h", "dir/baz.cc"));
+ Change change5 = insert("repo", newChangeWithFiles(repo, "Quux.java"));
+ Change change6 = insert("repo", newChangeWithFiles(repo, "foo.txt", "foo"));
+ Change change7 = insert("repo", newChangeWithFiles(repo, "foo"));
// case doesn't matter
assertQuery("onlyextensions:cc,h", change4, change2, change1);
@@ -1753,23 +1831,23 @@
@Test
public void byFooter() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
RevCommit commit1 = repo.parseBody(repo.commit().message("Test\n\nfoo: bar").create());
- Change change1 = insert(repo, newChangeForCommit(repo, commit1));
+ Change change1 = insert("repo", newChangeForCommit(repo, commit1));
RevCommit commit2 = repo.parseBody(repo.commit().message("Test\n\nfoo: baz").create());
- Change change2 = insert(repo, newChangeForCommit(repo, commit2));
+ Change change2 = insert("repo", newChangeForCommit(repo, commit2));
RevCommit commit3 = repo.parseBody(repo.commit().message("Test\n\nfoo: bar\nfoo:baz").create());
- Change change3 = insert(repo, newChangeForCommit(repo, commit3));
+ Change change3 = insert("repo", newChangeForCommit(repo, commit3));
RevCommit commit4 = repo.parseBody(repo.commit().message("Test\n\nfoo: bar=baz").create());
- Change change4 = insert(repo, newChangeForCommit(repo, commit4));
+ Change change4 = insert("repo", newChangeForCommit(repo, commit4));
// create a changes with lines that look like footers, but which are not
RevCommit commit5 =
repo.parseBody(
repo.commit().message("Test\n\nfoo: bar\n\nfoo=bar").insertChangeId().create());
- Change change5 = insert(repo, newChangeForCommit(repo, commit5));
+ Change change5 = insert("repo", newChangeForCommit(repo, commit5));
RevCommit commit6 = repo.parseBody(repo.commit().message("Test\n\na=b: c").create());
- insert(repo, newChangeForCommit(repo, commit6));
+ insert("repo", newChangeForCommit(repo, commit6));
// matching by 'key=value' works
assertQuery("footer:foo=bar", change3, change1);
@@ -1803,15 +1881,15 @@
@Test
public void byFooterName() throws Exception {
assume().that(getSchema().hasField(ChangeField.FOOTER_NAME)).isTrue();
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
RevCommit commit1 = repo.parseBody(repo.commit().message("Test\n\nfoo: bar").create());
- Change change1 = insert(repo, newChangeForCommit(repo, commit1));
+ Change change1 = insert("repo", newChangeForCommit(repo, commit1));
RevCommit commit2 = repo.parseBody(repo.commit().message("Test\n\nBaR: baz").create());
- Change change2 = insert(repo, newChangeForCommit(repo, commit2));
+ Change change2 = insert("repo", newChangeForCommit(repo, commit2));
// create a changes with lines that look like footers, but which are not
RevCommit commit6 = repo.parseBody(repo.commit().message("Test\n\na=b: c").create());
- insert(repo, newChangeForCommit(repo, commit6));
+ insert("repo", newChangeForCommit(repo, commit6));
// matching by 'key=value' works
assertQuery("hasfooter:foo", change1);
@@ -1823,14 +1901,14 @@
@Test
public void byDirectory() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
- Change change1 = insert(repo, newChangeWithFiles(repo, "src/foo.h", "src/foo.cc"));
- Change change2 = insert(repo, newChangeWithFiles(repo, "src/java/foo.java", "src/js/bar.js"));
+ repo = createAndOpenProject("repo");
+ Change change1 = insert("repo", newChangeWithFiles(repo, "src/foo.h", "src/foo.cc"));
+ Change change2 = insert("repo", newChangeWithFiles(repo, "src/java/foo.java", "src/js/bar.js"));
Change change3 =
- insert(repo, newChangeWithFiles(repo, "documentation/training/slides/README.txt"));
- Change change4 = insert(repo, newChangeWithFiles(repo, "a.txt"));
- Change change5 = insert(repo, newChangeWithFiles(repo, "a/b/c/d/e/foo.txt"));
- Change change6 = insert(repo, newChangeWithFiles(repo, "all/caps/DIRECTORY/file.txt"));
+ insert("repo", newChangeWithFiles(repo, "documentation/training/slides/README.txt"));
+ Change change4 = insert("repo", newChangeWithFiles(repo, "a.txt"));
+ Change change5 = insert("repo", newChangeWithFiles(repo, "a/b/c/d/e/foo.txt"));
+ Change change6 = insert("repo", newChangeWithFiles(repo, "all/caps/DIRECTORY/file.txt"));
// matching by directory prefix works
assertQuery("directory:src", change2, change1);
@@ -1891,10 +1969,10 @@
@Test
public void byDirectoryRegex() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
- Change change1 = insert(repo, newChangeWithFiles(repo, "src/java/foo.java", "src/js/bar.js"));
+ repo = createAndOpenProject("repo");
+ Change change1 = insert("repo", newChangeWithFiles(repo, "src/java/foo.java", "src/js/bar.js"));
Change change2 =
- insert(repo, newChangeWithFiles(repo, "documentation/training/slides/README.txt"));
+ insert("repo", newChangeWithFiles(repo, "documentation/training/slides/README.txt"));
// match by regexp
assertQuery("directory:^.*va.*", change1);
@@ -1904,9 +1982,9 @@
@Test
public void byComment() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
ChangeInserter ins = newChange(repo);
- Change change = insert(repo, ins);
+ Change change = insert("repo", ins);
ReviewInput input = new ReviewInput();
input.message = "toplevel";
@@ -1934,11 +2012,11 @@
public void byAge() throws Exception {
long thirtyHoursInMs = MILLISECONDS.convert(30, HOURS);
resetTimeWithClockStep(thirtyHoursInMs, MILLISECONDS);
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
long startMs = TestTimeUtil.START.toEpochMilli();
- Change change1 = insert(repo, newChange(repo), null, Instant.ofEpochMilli(startMs));
+ Change change1 = insert("repo", newChange(repo), null, Instant.ofEpochMilli(startMs));
Change change2 =
- insert(repo, newChange(repo), null, Instant.ofEpochMilli(startMs + thirtyHoursInMs));
+ insert("repo", newChange(repo), null, Instant.ofEpochMilli(startMs + thirtyHoursInMs));
// Stop time so age queries use the same endpoint.
TestTimeUtil.setClockStep(0, MILLISECONDS);
@@ -1975,11 +2053,11 @@
public void byBeforeUntil() throws Exception {
long thirtyHoursInMs = MILLISECONDS.convert(30, HOURS);
resetTimeWithClockStep(thirtyHoursInMs, MILLISECONDS);
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
long startMs = TestTimeUtil.START.toEpochMilli();
- Change change1 = insert(repo, newChange(repo), null, Instant.ofEpochMilli(startMs));
+ Change change1 = insert("repo", newChange(repo), null, Instant.ofEpochMilli(startMs));
Change change2 =
- insert(repo, newChange(repo), null, Instant.ofEpochMilli(startMs + thirtyHoursInMs));
+ insert("repo", newChange(repo), null, Instant.ofEpochMilli(startMs + thirtyHoursInMs));
TestTimeUtil.setClockStep(0, MILLISECONDS);
// Change1 was last updated on 2009-09-30 21:00:00 -0000
@@ -2027,11 +2105,11 @@
public void byAfterSince() throws Exception {
long thirtyHoursInMs = MILLISECONDS.convert(30, HOURS);
resetTimeWithClockStep(thirtyHoursInMs, MILLISECONDS);
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
long startMs = TestTimeUtil.START.toEpochMilli();
- Change change1 = insert(repo, newChange(repo), null, Instant.ofEpochMilli(startMs));
+ Change change1 = insert("repo", newChange(repo), null, Instant.ofEpochMilli(startMs));
Change change2 =
- insert(repo, newChange(repo), null, Instant.ofEpochMilli(startMs + thirtyHoursInMs));
+ insert("repo", newChange(repo), null, Instant.ofEpochMilli(startMs + thirtyHoursInMs));
TestTimeUtil.setClockStep(0, MILLISECONDS);
// Change1 was last updated on 2009-09-30 21:00:00 -0000
@@ -2071,12 +2149,12 @@
// Stop the clock, will set time to specific test values.
resetTimeWithClockStep(0, MILLISECONDS);
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
long startMs = TestTimeUtil.START.toEpochMilli();
TestTimeUtil.setClock(new Timestamp(startMs));
- Change change1 = insert(repo, newChange(repo));
- Change change2 = insert(repo, newChange(repo));
- Change change3 = insert(repo, newChange(repo));
+ Change change1 = insert("repo", newChange(repo));
+ Change change2 = insert("repo", newChange(repo));
+ Change change3 = insert("repo", newChange(repo));
TestTimeUtil.setClock(new Timestamp(startMs + thirtyHoursInMs));
submit(change3);
@@ -2131,12 +2209,12 @@
// Stop the clock, will set time to specific test values.
resetTimeWithClockStep(0, MILLISECONDS);
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
long startMs = TestTimeUtil.START.toEpochMilli();
TestTimeUtil.setClock(new Timestamp(startMs));
- Change change1 = insert(repo, newChange(repo));
- Change change2 = insert(repo, newChange(repo));
- Change change3 = insert(repo, newChange(repo));
+ Change change1 = insert("repo", newChange(repo));
+ Change change2 = insert("repo", newChange(repo));
+ Change change3 = insert("repo", newChange(repo));
assertThat(TimeUtil.nowMs()).isEqualTo(startMs);
TestTimeUtil.setClock(new Timestamp(startMs + thirtyHoursInMs));
@@ -2201,13 +2279,13 @@
// Stop the clock, will set time to specific test values.
resetTimeWithClockStep(0, MILLISECONDS);
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
long startMs = TestTimeUtil.START.toEpochMilli();
TestTimeUtil.setClock(new Timestamp(startMs));
- Change change1 = insert(repo, newChange(repo));
- Change change2 = insert(repo, newChange(repo));
- Change change3 = insert(repo, newChange(repo));
+ Change change1 = insert("repo", newChange(repo));
+ Change change2 = insert("repo", newChange(repo));
+ Change change3 = insert("repo", newChange(repo));
TestTimeUtil.setClock(new Timestamp(startMs + thirtyHoursInMs));
submit(change2);
@@ -2234,15 +2312,15 @@
@Test
public void bySize() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
// added = 3, deleted = 0, delta = 3
RevCommit commit1 = repo.parseBody(repo.commit().add("file1", "foo\n\foo\nfoo").create());
// added = 0, deleted = 2, delta = 2
RevCommit commit2 = repo.parseBody(repo.commit().parent(commit1).add("file1", "foo").create());
- Change change1 = insert(repo, newChangeForCommit(repo, commit1));
- Change change2 = insert(repo, newChangeForCommit(repo, commit2));
+ Change change1 = insert("repo", newChangeForCommit(repo, commit1));
+ Change change2 = insert("repo", newChangeForCommit(repo, commit2));
assertQuery("added:>4");
assertQuery("-added:<=4");
@@ -2290,9 +2368,9 @@
}
private List<Change> setUpHashtagChanges() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
- Change change1 = insert(repo, newChange(repo));
- Change change2 = insert(repo, newChange(repo));
+ repo = createAndOpenProject("repo");
+ Change change1 = insert("repo", newChange(repo));
+ Change change2 = insert("repo", newChange(repo));
addHashtags(change1.getId(), "foo", "aaa-bbb-ccc");
addHashtags(change2.getId(), "foo", "bar", "a tag", "ACamelCaseTag");
@@ -2339,10 +2417,10 @@
@Test
public void byHashtagRegex() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
- Change change1 = insert(repo, newChange(repo));
- Change change2 = insert(repo, newChange(repo));
- Change change3 = insert(repo, newChange(repo));
+ repo = createAndOpenProject("repo");
+ Change change1 = insert("repo", newChange(repo));
+ Change change2 = insert("repo", newChange(repo));
+ Change change3 = insert("repo", newChange(repo));
addHashtags(change1.getId(), "feature1");
addHashtags(change1.getId(), "trending");
addHashtags(change2.getId(), "Cherrypick-feature1");
@@ -2355,27 +2433,27 @@
@Test
public void byDefault() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
- Change change1 = insert(repo, newChange(repo));
+ Change change1 = insert("repo", newChange(repo));
RevCommit commit2 = repo.parseBody(repo.commit().message("foosubject").create());
- Change change2 = insert(repo, newChangeForCommit(repo, commit2));
+ Change change2 = insert("repo", newChangeForCommit(repo, commit2));
RevCommit commit3 = repo.parseBody(repo.commit().add("Foo.java", "foo contents").create());
- Change change3 = insert(repo, newChangeForCommit(repo, commit3));
+ Change change3 = insert("repo", newChangeForCommit(repo, commit3));
ChangeInserter ins4 = newChange(repo);
- Change change4 = insert(repo, ins4);
+ Change change4 = insert("repo", ins4);
ReviewInput ri4 = new ReviewInput();
ri4.message = "toplevel";
ri4.labels = ImmutableMap.of("Code-Review", (short) 1);
gApi.changes().id(change4.getId().get()).current().review(ri4);
ChangeInserter ins5 = newChangeWithTopic(repo, "feature5");
- Change change5 = insert(repo, ins5);
+ Change change5 = insert("repo", ins5);
- Change change6 = insert(repo, newChangeForBranch(repo, "branch6"));
+ Change change6 = insert("repo", newChangeForBranch(repo, "branch6"));
assertQuery(change1.getId().get(), change1);
assertQuery(ChangeTriplet.format(change1), change1);
@@ -2396,18 +2474,18 @@
@Test
public void byDefaultWithCommitPrefix() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
+ repo = createAndOpenProject("repo");
RevCommit commit = repo.parseBody(repo.commit().message("message").create());
- Change change = insert(repo, newChangeForCommit(repo, commit));
+ Change change = insert("repo", newChangeForCommit(repo, commit));
assertQuery(commit.getId().getName().substring(0, 6), change);
}
@Test
public void visible() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
- Change change1 = insert(repo, newChange(repo));
- Change change2 = insert(repo, newChangePrivate(repo));
+ repo = createAndOpenProject("repo");
+ Change change1 = insert("repo", newChange(repo));
+ Change change2 = insert("repo", newChangePrivate(repo));
String q = "project:repo";
@@ -2461,8 +2539,8 @@
// Switch to user3
requestContext.setContext(newRequestContext(user3));
- Change change3 = insert(repo, newChange(repo), user3);
- Change change4 = insert(repo, newChangePrivate(repo), user3);
+ Change change3 = insert("repo", newChange(repo), user3);
+ Change change4 = insert("repo", newChangePrivate(repo), user3);
// User3 can see both their changes and the first user's change
assertQuery(q + " visibleto:" + user3.get(), change4, change3, change1);
@@ -2502,9 +2580,9 @@
@Test
public void visibleToSelf() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
- Change change1 = insert(repo, newChange(repo));
- Change change2 = insert(repo, newChange(repo));
+ repo = createAndOpenProject("repo");
+ Change change1 = insert("repo", newChange(repo));
+ Change change2 = insert("repo", newChange(repo));
gApi.changes().id(change2.getChangeId()).setPrivate(true, "private");
@@ -2520,16 +2598,12 @@
@Test
public void byCommentBy() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
- Change change1 = insert(repo, newChange(repo));
- Change change2 = insert(repo, newChange(repo));
- int user2 =
- accountManager
- .authenticate(authRequestFactory.createForUser("anotheruser"))
- .getAccountId()
- .get();
+ repo = createAndOpenProject("repo");
+ Change change1 = insert("repo", newChange(repo));
+ Change change2 = insert("repo", newChange(repo));
+ Account.Id user2 = createAccount("anotheruser");
ReviewInput input = new ReviewInput();
input.message = "toplevel";
ReviewInput.CommentInput comment = new ReviewInput.CommentInput();
@@ -2550,8 +2624,8 @@
public void bySubmitRuleResult() throws Exception {
try (Registration registration =
extensionRegistry.newRegistration().add(new FakeSubmitRule())) {
- TestRepository<Repo> repo = createProject("repo");
- Change change = insert(repo, newChange(repo));
+ repo = createAndOpenProject("repo");
+ Change change = insert("repo", newChange(repo));
// The fake submit rule exports its ruleName as "FakeSubmitRule"
assertQuery("rule:FakeSubmitRule");
@@ -2570,17 +2644,17 @@
public void byNonExistingSubmitRule_returnsEmpty() throws Exception {
try (Registration registration =
extensionRegistry.newRegistration().add(new FakeSubmitRule())) {
- TestRepository<Repo> repo = createProject("repo");
- insert(repo, newChange(repo));
+ repo = createAndOpenProject("repo");
+ insert("repo", newChange(repo));
assertQuery("rule:non-existent-rule");
}
}
@Test
public void byHasDraft() throws Exception {
- TestRepository<Repo> repo = createProject("repo");
- Change change1 = insert(repo, newChange(repo));
- Change change2 = insert(repo, newChange(repo));
+ repo = createAndOpenProject("repo");
+ Change change1 = insert("repo", newChange(repo));
+ Change change2 = insert("repo", newChange(repo));
assertQuery("has:draft");
@@ -2612,8 +2686,8 @@
*/
public void byHasDraftExcludesZombieDrafts() throws Exception {
Project.NameKey project = Project.nameKey("repo");
- TestRepository<Repo> repo = createProject(project.get());
- Change change = insert(repo, newChange(repo));
+ repo = createAndOpenProject(project.get());
+ Change change = insert("repo", newChange(repo));
Change.Id id = change.getId();
DraftInput in = new DraftInput();
@@ -2625,7 +2699,7 @@
assertQuery("has:draft", change);
assertQuery("commentby:" + userId);
- try (TestRepository<Repo> allUsers =
+ try (TestRepository<Repository> allUsers =
new TestRepository<>(repoManager.openRepository(allUsersName))) {
Ref draftsRef = allUsers.getRepository().exactRef(RefNames.refsDraftComments(id, userId));
assertThat(draftsRef).isNotNull();
@@ -2649,15 +2723,15 @@
@Test