Merge "Hide commit message edit button in edit mode"
diff --git a/Documentation/config-project-config.txt b/Documentation/config-project-config.txt
index 3b2b65f..cad9489 100644
--- a/Documentation/config-project-config.txt
+++ b/Documentation/config-project-config.txt
@@ -217,6 +217,19 @@
Default is `INHERIT`, which means that this property is inherited from
the parent project.
+[[change.workInProgressByDefault]]change.workInProgressByDefault::
++
+Controls whether all new changes in the project are set as WIP by default.
++
+Note that a new change will be ready if the `workInProgress` field in
+link:rest-api-changes.html#change-input[ChangeInput] is set to `false` explicitly
+when calling the link:rest-api-changes.html#create-change[CreateChange] REST API
+or the `ready` link:user-upload.html#wip[PushOption] is used during
+the Git push.
++
+Default is `INHERIT`, which means that this property is inherited from
+the parent project.
+
[[submit-section]]
=== Submit section
diff --git a/Documentation/dev-bazel.txt b/Documentation/dev-bazel.txt
index 04f749d..8b4bece 100644
--- a/Documentation/dev-bazel.txt
+++ b/Documentation/dev-bazel.txt
@@ -417,6 +417,92 @@
details. Users should watch the cache sizes and clean them manually if
necessary.
+[[npm-binary]]
+== NPM Binaries
+
+Parts of the PolyGerrit build require running NPM-based JavaScript programs as
+"binaries". We don't attempt to resolve and download NPM dependencies at build
+time, but instead use pre-built bundles of the NPM binary along with all its
+dependencies. Some packages on
+link:https://docs.npmjs.com/misc/registry[registry.npmjs.org] come with their
+dependencies bundled, but this is the exception rather than the rule. More
+commonly, to add a new binary to this list, you will need to bundle the binary
+yourself.
+
+[NOTE]
+We can only use binaries that meet certain licensing requirements, and that do
+not include any native code.
+
+Start by checking that the license and file types of the bundle are acceptable:
+[source,bash]
+----
+ gerrit_repo=/path/to/gerrit
+ package=some-npm-package
+ version=1.2.3
+
+ npm install -g license-checker && \
+ rm -rf /tmp/$package-$version && mkdir -p /tmp/$package-$version && \
+ cd /tmp/$package-$version && \
+ npm install $package@$version && \
+ license-checker | grep licenses: | sort -u
+----
+
+This will output a list of the different licenses used by the package and all
+its transitive dependencies. We can only legally distribute a bundle via our
+storage bucket if the licenses allow us to do so. As long as all of the listed
+license are allowed by
+link:https://opensource.google.com/docs/thirdparty/licenses/[Google's
+standards]. Any `by_exception_only`, commercial, prohibited, or unlisted
+licenses are not allowed; otherwise, it is ok to distribute the source. If in
+doubt, contact a maintainer who is a Googler.
+
+Next, check the file types:
+[source,bash]
+----
+ cd /tmp/$package-$version
+ find . -type f | xargs file | grep -v 'ASCII\|UTF-8\|empty$'
+----
+
+If you see anything that looks like a native library or binary, then we can't
+use the bundle.
+
+If everything looks good, create the bundle, and note the SHA-1:
+[source,bash]
+----
+ $gerrit_repo/tools/js/npm_pack.py $package $version && \
+ sha1sum $package-$version.tgz
+----
+
+This creates a file named `$package-$version.tgz` in your working directory.
+
+Any project maintainer can upload this file to the
+link:https://console.cloud.google.com/storage/browser/gerrit-maven/npm-packages[storage
+bucket].
+
+Finally, add the new binary to the build process:
+----
+ # WORKSPACE
+ npm_binary(
+ name = "some-npm-package",
+ repository = GERRIT,
+ )
+
+ # lib/js/npm.bzl
+ NPM_VERSIONS = {
+ ...
+ "some-npm-package": "1.2.3",
+ }
+
+ NPM_SHA1S = {
+ ...
+ "some-npm-package": "<sha1>",
+ }
+----
+
+To use the binary from the Bazel build, you need to use the `run_npm_binary.py`
+wrapper script. For an example, see the use of `crisper` in `tools/bzl/js.bzl`.
+
+
GERRIT
------
Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/dev-contributing.txt b/Documentation/dev-contributing.txt
index e065e57..62d73cc 100644
--- a/Documentation/dev-contributing.txt
+++ b/Documentation/dev-contributing.txt
@@ -164,7 +164,7 @@
To format Java source code, Gerrit uses the
link:https://github.com/google/google-java-format[`google-java-format`]
-tool (version 1.5), and to format Bazel BUILD and WORKSPACE files the
+tool (version 1.5), and to format Bazel BUILD, WORKSPACE and .bzl files the
link:https://github.com/bazelbuild/buildtools/tree/master/buildifier[`buildifier`]
tool (version 0.12.0).
These tools automatically apply format according to the style guides; this
diff --git a/Documentation/install-quick.txt b/Documentation/install-quick.txt
index 7b80229..2503449 100644
--- a/Documentation/install-quick.txt
+++ b/Documentation/install-quick.txt
@@ -172,7 +172,14 @@
user@host:~/demo-project$
----
-Then make a change to it and upload it as a reviewable change in Gerrit.
+Install the link:user-changeid.html[Change-Id commitmsg hook]
+
+----
+ scp -p -P 29418 user@localhost:hooks/commit-msg $(git rev-parse --git-dir)/hooks/
+----
+
+Then make a change to the repository and upload it as a reviewable change
+in Gerrit.
----
user@host:~/demo-project$ date > testfile.txt
diff --git a/Documentation/intro-user.txt b/Documentation/intro-user.txt
index 436408d..662e0b3 100644
--- a/Documentation/intro-user.txt
+++ b/Documentation/intro-user.txt
@@ -827,6 +827,12 @@
and download commands. Note that this option is only shown if the Flash plugin
is available and the JavaScript Clipboard API is unavailable.
+- [[work-in-progress-by-default]]`Set new changes work-in-progress`:
++
+Whether new changes are uploaded as work-in-progress per default. This
+preference just sets the default; the behavior can still be overridden using a
+link:user-upload.html#wip[push option].
+
[[my-menu]]
In addition it is possible to customize the menu entries of the `My`
menu. This can be used to make the navigation to frequently used
diff --git a/Documentation/rest-api-accounts.txt b/Documentation/rest-api-accounts.txt
index 025b29d..edb642e 100644
--- a/Documentation/rest-api-accounts.txt
+++ b/Documentation/rest-api-accounts.txt
@@ -1255,6 +1255,7 @@
"review_category_strategy": "ABBREV",
"mute_common_path_prefixes": true,
"publish_comments_on_push": true,
+ "work_in_progress_by_default": true,
"default_base_for_merges": "FIRST_PARENT",
"my": [
{
@@ -1361,6 +1362,7 @@
"review_category_strategy": "NAME",
"diff_view": "SIDE_BY_SIDE",
"publish_comments_on_push": true,
+ "work_in_progress_by_default": true,
"mute_common_path_prefixes": true,
"my": [
{
@@ -2654,6 +2656,9 @@
|`publish_comments_on_push` |not set if `false`|
Whether to link:user-upload.html#publish-comments[publish draft comments] on
push by default.
+|`work_in_progress_by_default` |not set if `false`|
+Whether to link:user-upload.html#wip[set work-in-progress] on
+push or on create changes online by default.
|============================================
[[preferences-input]]
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index fb0a0df..3b25f47 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -2277,7 +2277,9 @@
--
Marks a change as ignored. The change will not be shown in the incoming
-reviews dashboard, and email notifications will be suppressed.
+reviews dashboard, and email notifications will be suppressed. Ignoring
+a change does not cause the change's "updated" timestamp to be modified,
+and the owner is not notified.
.Request
----
@@ -6282,10 +6284,14 @@
Only set if the file was renamed or copied.
|`lines_inserted`|optional|
Number of inserted lines. +
-Not set for binary files or if no lines were inserted.
+Not set for binary files or if no lines were inserted. +
+An empty last line is not included in the count and hence this number can
+differ by one from details provided in <<#diff-info,DiffInfo>>.
|`lines_deleted` |optional|
Number of deleted lines. +
-Not set for binary files or if no lines were deleted.
+Not set for binary files or if no lines were deleted. +
+An empty last line is not included in the count and hence this number can
+differ by one from details provided in <<#diff-info,DiffInfo>>.
|`size_delta` ||
Number of bytes by which the file size increased/decreased.
|`size` ||
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index 792cca8..4a5cd1d 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -2890,10 +2890,13 @@
|`reject_implicit_merges`|optional|
link:#inherited-boolean-info[InheritedBooleanInfo] that tells whether
implicit merges should be rejected on changes pushed to the project.
-|`private_by_default` ||
+|`private_by_default` ||
link:#inherited-boolean-info[InheritedBooleanInfo] that tells whether
all new changes are set as private by default.
-|`max_object_size_limit` ||
+|`work_in_progress_by_default`||
+link:#inherited-boolean-info[InheritedBooleanInfo] that tells whether
+all new changes are set as work-in-progress by default.
+|`max_object_size_limit` ||
The link:config-gerrit.html#receive.maxObjectSizeLimit[max object size
limit] of this project as a link:#max-object-size-limit-info[
MaxObjectSizeLimitInfo] entity.
diff --git a/Documentation/user-upload.txt b/Documentation/user-upload.txt
index ce62b93..baf388e 100644
--- a/Documentation/user-upload.txt
+++ b/Documentation/user-upload.txt
@@ -309,6 +309,11 @@
Only change owners, project owners and site administrators can specify
`work-in-progress` and `ready` options on push.
+The default for this option can be set as a
+link:intro-user.html#work-in-progress-by-default[user preference]. If the
+preference is set so the default behavior is to create `work-in-progress`
+changes, this can be overridden with the `ready` option.
+
[[message]]
==== Message
diff --git a/WORKSPACE b/WORKSPACE
index dfe2a45..e490c63 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -878,8 +878,8 @@
maven_jar(
name = "commons-io",
- artifact = "commons-io:commons-io:1.4",
- sha1 = "a8762d07e76cfde2395257a5da47ba7c1dbd3dce",
+ artifact = "commons-io:commons-io:2.2",
+ sha1 = "83b5b8a7ba1c08f9e8c8ff2373724e33d3c1e22a",
)
maven_jar(
@@ -946,6 +946,10 @@
load("//tools/bzl:js.bzl", "bower_archive", "npm_binary")
+# NPM binaries bundled along with their dependencies.
+#
+# For full instructions on adding new binaries to the build, see
+# http://gerrit-review.googlesource.com/Documentation/dev-bazel.html#npm-binary
npm_binary(
name = "bower",
)
diff --git a/gerrit-acceptance-tests/tests.bzl b/gerrit-acceptance-tests/tests.bzl
new file mode 100644
index 0000000..c1e34dd
--- /dev/null
+++ b/gerrit-acceptance-tests/tests.bzl
@@ -0,0 +1,21 @@
+load("//tools/bzl:junit.bzl", "junit_tests")
+
+def acceptance_tests(
+ group,
+ deps = [],
+ labels = [],
+ vm_args = ["-Xmx256m"],
+ **kwargs):
+ junit_tests(
+ name = group,
+ deps = deps + [
+ "//gerrit-acceptance-tests:lib",
+ ],
+ tags = labels + [
+ "acceptance",
+ "slow",
+ ],
+ size = "large",
+ jvm_flags = vm_args,
+ **kwargs
+ )
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GeneralPreferences.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GeneralPreferences.java
index 1dcb284..fbdf52c 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GeneralPreferences.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GeneralPreferences.java
@@ -151,6 +151,9 @@
public final native boolean
publishCommentsOnPush() /*-{ return this.publish_comments_on_push || false }-*/;
+ public final native boolean
+ workInProgressByDefault() /*-{ return this.work_in_progress_by_default || false }-*/;
+
public final native JsArray<TopMenuItem> my() /*-{ return this.my; }-*/;
public final native void changesPerPage(int n) /*-{ this.changes_per_page = n }-*/;
@@ -230,6 +233,9 @@
public final native void publishCommentsOnPush(
boolean p) /*-{ this.publish_comments_on_push = p }-*/;
+ public final native void workInProgressByDefault(
+ boolean p) /*-{ this.work_in_progress_by_default = p }-*/;
+
public final void setMyMenus(List<TopMenuItem> myMenus) {
initMy();
for (TopMenuItem n : myMenus) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java
index 0b32cd5..3e21619 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java
@@ -69,6 +69,8 @@
String publishCommentsOnPush();
+ String workInProgressByDefault();
+
String myMenu();
String myMenuInfo();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
index 4b01513..c32efed 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
@@ -39,6 +39,7 @@
muteCommonPathPrefixes = Mute Common Path Prefixes In File List
signedOffBy = Insert Signed-off-by Footer For Inline Edit Changes
publishCommentsOnPush = Publish Comments On Push
+workInProgressByDefault = Set all new changes work-in-progress by default
myMenu = My Menu
myMenuInfo = \
Menu items for the 'My' top level menu. \
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java
index f349065..afb8718 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java
@@ -56,6 +56,7 @@
private CheckBox muteCommonPathPrefixes;
private CheckBox signedOffBy;
private CheckBox publishCommentsOnPush;
+ private CheckBox workInProgressByDefault;
private ListBox maximumPageSize;
private ListBox dateFormat;
private ListBox timeFormat;
@@ -163,9 +164,10 @@
muteCommonPathPrefixes = new CheckBox(Util.C.muteCommonPathPrefixes());
signedOffBy = new CheckBox(Util.C.signedOffBy());
publishCommentsOnPush = new CheckBox(Util.C.publishCommentsOnPush());
+ workInProgressByDefault = new CheckBox(Util.C.workInProgressByDefault());
boolean flashClippy = !UserAgent.hasJavaScriptClipboard() && UserAgent.Flash.isInstalled();
- final Grid formGrid = new Grid(15 + (flashClippy ? 1 : 0), 2);
+ final Grid formGrid = new Grid(16 + (flashClippy ? 1 : 0), 2);
int row = 0;
@@ -229,6 +231,10 @@
formGrid.setWidget(row, fieldIdx, publishCommentsOnPush);
row++;
+ formGrid.setText(row, labelIdx, "");
+ formGrid.setWidget(row, fieldIdx, workInProgressByDefault);
+ row++;
+
if (flashClippy) {
formGrid.setText(row, labelIdx, "");
formGrid.setWidget(row, fieldIdx, useFlashClipboard);
@@ -264,6 +270,7 @@
e.listenTo(muteCommonPathPrefixes);
e.listenTo(signedOffBy);
e.listenTo(publishCommentsOnPush);
+ e.listenTo(workInProgressByDefault);
e.listenTo(diffView);
e.listenTo(reviewCategoryStrategy);
e.listenTo(emailStrategy);
@@ -303,6 +310,7 @@
muteCommonPathPrefixes.setEnabled(on);
signedOffBy.setEnabled(on);
publishCommentsOnPush.setEnabled(on);
+ workInProgressByDefault.setEnabled(on);
reviewCategoryStrategy.setEnabled(on);
diffView.setEnabled(on);
emailStrategy.setEnabled(on);
@@ -329,6 +337,7 @@
muteCommonPathPrefixes.setValue(p.muteCommonPathPrefixes());
signedOffBy.setValue(p.signedOffBy());
publishCommentsOnPush.setValue(p.publishCommentsOnPush());
+ workInProgressByDefault.setValue(p.workInProgressByDefault());
setListBox(
reviewCategoryStrategy,
GeneralPreferencesInfo.ReviewCategoryStrategy.NONE,
@@ -421,6 +430,7 @@
p.muteCommonPathPrefixes(muteCommonPathPrefixes.getValue());
p.signedOffBy(signedOffBy.getValue());
p.publishCommentsOnPush(publishCommentsOnPush.getValue());
+ p.workInProgressByDefault(workInProgressByDefault.getValue());
p.reviewCategoryStrategy(
getListBox(
reviewCategoryStrategy, ReviewCategoryStrategy.NONE, ReviewCategoryStrategy.values()));
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
index c0947a8..9def3b3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
@@ -79,6 +79,8 @@
String privateByDefault();
+ String workInProgressByDefault();
+
String enableReviewerByEmail();
String matchAuthorToCommitterDate();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
index 8d6878f..1d1bb6d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
@@ -31,6 +31,7 @@
requireChangeID = Require <code>Change-Id</code> in commit message
rejectImplicitMerges = Reject implicit merges when changes are pushed for review
privateByDefault = Set all new changes private by default
+workInProgressByDefault = Set all new changes work-in-progress by default
headingMaxObjectSizeLimit = Maximum Git object size limit
headingGroupOptions = Group Options
isVisibleToAll = Make group visible to all registered users.
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
index 64e147d..751e951 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
@@ -88,6 +88,7 @@
private ListBox requireSignedPush;
private ListBox rejectImplicitMerges;
private ListBox privateByDefault;
+ private ListBox workInProgressByDefault;
private ListBox enableReviewerByEmail;
private ListBox matchAuthorToCommitterDate;
private NpTextBox maxObjectSizeLimit;
@@ -198,6 +199,7 @@
requireChangeID.setEnabled(isOwner);
rejectImplicitMerges.setEnabled(isOwner);
privateByDefault.setEnabled(isOwner);
+ workInProgressByDefault.setEnabled(isOwner);
maxObjectSizeLimit.setEnabled(isOwner);
enableReviewerByEmail.setEnabled(isOwner);
matchAuthorToCommitterDate.setEnabled(isOwner);
@@ -278,6 +280,10 @@
saveEnabler.listenTo(privateByDefault);
grid.addHtml(AdminConstants.I.privateByDefault(), privateByDefault);
+ workInProgressByDefault = newInheritedBooleanBox();
+ saveEnabler.listenTo(workInProgressByDefault);
+ grid.addHtml(AdminConstants.I.workInProgressByDefault(), workInProgressByDefault);
+
enableReviewerByEmail = newInheritedBooleanBox();
saveEnabler.listenTo(enableReviewerByEmail);
grid.addHtml(AdminConstants.I.enableReviewerByEmail(), enableReviewerByEmail);
@@ -427,6 +433,7 @@
}
setBool(rejectImplicitMerges, result.rejectImplicitMerges());
setBool(privateByDefault, result.privateByDefault());
+ setBool(workInProgressByDefault, result.workInProgressByDefault());
setBool(enableReviewerByEmail, result.enableReviewerByEmail());
setBool(matchAuthorToCommitterDate, result.matchAuthorToCommitterDate());
setSubmitType(result.defaultSubmitType());
@@ -700,6 +707,7 @@
rsp,
getBool(rejectImplicitMerges),
getBool(privateByDefault),
+ getBool(workInProgressByDefault),
getBool(enableReviewerByEmail),
getBool(matchAuthorToCommitterDate),
maxObjectSizeLimit.getText().trim(),
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffInfo.java
index cf40762..d942c2e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffInfo.java
@@ -137,7 +137,10 @@
private static void append(StringBuilder s, JsArrayString lines) {
for (int i = 0; i < lines.length(); i++) {
- s.append(lines.get(i)).append('\n');
+ if (s.length() > 0) {
+ s.append('\n');
+ }
+ s.append(lines.get(i));
}
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java
index f670ac7..4185ef3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java
@@ -60,6 +60,9 @@
public final native InheritedBooleanInfo privateByDefault()
/*-{ return this.private_by_default; }-*/ ;
+ public final native InheritedBooleanInfo workInProgressByDefault()
+ /*-{ return this.work_in_progress_by_default; }-*/ ;
+
public final native InheritedBooleanInfo enableReviewerByEmail()
/*-{ return this.enable_reviewer_by_email; }-*/ ;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
index 66afdb2..3be52e6 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
@@ -153,6 +153,7 @@
InheritableBoolean requireSignedPush,
InheritableBoolean rejectImplicitMerges,
InheritableBoolean privateByDefault,
+ InheritableBoolean workInProgressByDefault,
InheritableBoolean enableReviewerByEmail,
InheritableBoolean matchAuthorToCommitterDate,
String maxObjectSizeLimit,
@@ -175,6 +176,7 @@
}
in.setRejectImplicitMerges(rejectImplicitMerges);
in.setPrivateByDefault(privateByDefault);
+ in.setWorkInProgressByDefault(workInProgressByDefault);
in.setMaxObjectSizeLimit(maxObjectSizeLimit);
if (submitType != null) {
in.setSubmitType(submitType);
@@ -313,6 +315,13 @@
private native void setPrivateByDefault(String v) /*-{ if(v)this.private_by_default=v; }-*/;
+ final void setWorkInProgressByDefault(InheritableBoolean v) {
+ setWorkInProgressByDefault(v.name());
+ }
+
+ private native void setWorkInProgressByDefault(
+ String v) /*-{ if(v)this.work_in_progress_by_default=v; }-*/;
+
final void setEnableReviewerByEmail(InheritableBoolean v) {
setEnableReviewerByEmailRaw(v.name());
}
diff --git a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index 53686a7..960b558 100644
--- a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -1345,8 +1345,7 @@
protected void assertDiffForNewFile(
DiffInfo diff, RevCommit commit, String path, String expectedContentSideB) throws Exception {
- List<String> expectedLines = new ArrayList<>();
- Collections.addAll(expectedLines, expectedContentSideB.split("\n"));
+ List<String> expectedLines = ImmutableList.copyOf(expectedContentSideB.split("\n", -1));
assertThat(diff.binary).isNull();
assertThat(diff.changeType).isEqualTo(ChangeType.ADDED);
diff --git a/java/com/google/gerrit/common/data/GroupReference.java b/java/com/google/gerrit/common/data/GroupReference.java
index cfaad17..e5b0965 100644
--- a/java/com/google/gerrit/common/data/GroupReference.java
+++ b/java/com/google/gerrit/common/data/GroupReference.java
@@ -43,16 +43,23 @@
protected GroupReference() {}
- public GroupReference(AccountGroup.UUID uuid, String name) {
+ /**
+ * Create a group reference.
+ *
+ * @param uuid UUID of the group, may be {@code null} if the group name couldn't be resolved
+ * @param name the group name, must not be {@code null}
+ */
+ public GroupReference(@Nullable AccountGroup.UUID uuid, String name) {
setUUID(uuid);
setName(name);
}
+ @Nullable
public AccountGroup.UUID getUUID() {
return uuid != null ? new AccountGroup.UUID(uuid) : null;
}
- public void setUUID(AccountGroup.UUID newUUID) {
+ public void setUUID(@Nullable AccountGroup.UUID newUUID) {
uuid = newUUID != null ? newUUID.get() : null;
}
@@ -73,7 +80,11 @@
}
private static String uuid(GroupReference a) {
- return a.getUUID() != null ? a.getUUID().get() : "?";
+ if (a.getUUID() != null && a.getUUID().get() != null) {
+ return a.getUUID().get();
+ }
+
+ return "?";
}
@Override
diff --git a/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java b/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
index 3755faa..f322c3d 100644
--- a/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
+++ b/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
@@ -21,6 +21,7 @@
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.flogger.FluentLogger;
@@ -79,6 +80,7 @@
protected static final String MAPPINGS = "mappings";
protected static final String ORDER = "order";
protected static final String SEARCH = "_search";
+ protected static final String SETTINGS = "settings";
protected static <T> List<T> decodeProtos(
JsonObject doc, String fieldName, ProtobufCodec<T> codec) {
@@ -181,7 +183,8 @@
}
// Recreate the index.
- response = performRequest("PUT", getMappings(), indexName, Collections.emptyMap());
+ String indexCreationFields = concatJsonString(getSettings(), getMappings());
+ response = performRequest("PUT", indexCreationFields, indexName, Collections.emptyMap());
statusCode = response.getStatusLine().getStatusCode();
if (statusCode != HttpStatus.SC_OK) {
String error = String.format("Failed to create index %s: %s", indexName, statusCode);
@@ -193,6 +196,10 @@
protected abstract String getMappings();
+ private String getSettings() {
+ return gson.toJson(ImmutableMap.of(SETTINGS, ElasticSetting.createSetting()));
+ }
+
protected abstract String getId(V v);
protected String getMappingsForSingleType(String candidateType, MappingProperties properties) {
@@ -294,6 +301,10 @@
return performRequest("POST", payload, uri, params);
}
+ private String concatJsonString(String target, String addition) {
+ return target.substring(0, target.length() - 1) + "," + addition.substring(1);
+ }
+
private Response performRequest(
String method, Object payload, String uri, Map<String, String> params) throws IOException {
String payloadStr = payload instanceof String ? (String) payload : payload.toString();
diff --git a/java/com/google/gerrit/elasticsearch/ElasticMapping.java b/java/com/google/gerrit/elasticsearch/ElasticMapping.java
index a30e546..f8c4168 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticMapping.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticMapping.java
@@ -34,9 +34,9 @@
|| fieldType == FieldType.INTEGER_RANGE
|| fieldType == FieldType.LONG) {
mapping.addNumber(name);
- } else if (fieldType == FieldType.PREFIX
- || fieldType == FieldType.FULL_TEXT
- || fieldType == FieldType.STORED_ONLY) {
+ } else if (fieldType == FieldType.FULL_TEXT) {
+ mapping.addStringWithAnalyzer(name);
+ } else if (fieldType == FieldType.PREFIX || fieldType == FieldType.STORED_ONLY) {
mapping.addString(name);
} else {
throw new IllegalStateException("Unsupported field type: " + fieldType.getName());
@@ -88,6 +88,13 @@
return this;
}
+ Builder addStringWithAnalyzer(String name) {
+ FieldProperties key = new FieldProperties(adapter.stringFieldType());
+ key.analyzer = "custom_with_char_filter";
+ fields.put(name, key);
+ return this;
+ }
+
Builder add(String name, String type) {
fields.put(name, new FieldProperties(type));
return this;
@@ -102,6 +109,7 @@
String type;
String index;
String format;
+ String analyzer;
Map<String, FieldProperties> fields;
FieldProperties(String type) {
diff --git a/java/com/google/gerrit/elasticsearch/ElasticSetting.java b/java/com/google/gerrit/elasticsearch/ElasticSetting.java
new file mode 100644
index 0000000..6fd234d
--- /dev/null
+++ b/java/com/google/gerrit/elasticsearch/ElasticSetting.java
@@ -0,0 +1,92 @@
+// Copyright (C) 2018 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.elasticsearch;
+
+import com.google.common.collect.ImmutableMap;
+import java.util.Map;
+
+class ElasticSetting {
+ /** The custom char mappings of "." to " " and "_" to " " in the form of UTF-8 */
+ private static final ImmutableMap<String, String> CUSTOM_CHAR_MAPPING =
+ ImmutableMap.of("\\u002E", "\\u0020", "\\u005F", "\\u0020");
+
+ static SettingProperties createSetting() {
+ ElasticSetting.Builder settings = new ElasticSetting.Builder();
+ settings.addCharFilter();
+ settings.addAnalyzer();
+ return settings.build();
+ }
+
+ static class Builder {
+ private final ImmutableMap.Builder<String, FieldProperties> fields =
+ new ImmutableMap.Builder<>();
+
+ SettingProperties build() {
+ SettingProperties properties = new SettingProperties();
+ properties.analysis = fields.build();
+ return properties;
+ }
+
+ void addCharFilter() {
+ FieldProperties charMapping = new FieldProperties("mapping");
+ charMapping.mappings = getCustomCharMappings(CUSTOM_CHAR_MAPPING);
+
+ FieldProperties charFilter = new FieldProperties();
+ charFilter.customMapping = charMapping;
+ fields.put("char_filter", charFilter);
+ }
+
+ void addAnalyzer() {
+ FieldProperties customAnalyzer = new FieldProperties("custom");
+ customAnalyzer.tokenizer = "standard";
+ customAnalyzer.charFilter = new String[] {"custom_mapping"};
+ customAnalyzer.filter = new String[] {"lowercase"};
+
+ FieldProperties analyzer = new FieldProperties();
+ analyzer.customWithCharFilter = customAnalyzer;
+ fields.put("analyzer", analyzer);
+ }
+
+ private static String[] getCustomCharMappings(ImmutableMap<String, String> map) {
+ int mappingIndex = 0;
+ int numOfMappings = map.size();
+ String[] mapping = new String[numOfMappings];
+ for (Map.Entry<String, String> e : map.entrySet()) {
+ mapping[mappingIndex++] = e.getKey() + "=>" + e.getValue();
+ }
+ return mapping;
+ }
+ }
+
+ static class SettingProperties {
+ Map<String, FieldProperties> analysis;
+ }
+
+ static class FieldProperties {
+ String tokenizer;
+ String type;
+ String[] charFilter;
+ String[] filter;
+ String[] mappings;
+ FieldProperties customMapping;
+ FieldProperties customWithCharFilter;
+
+ FieldProperties() {}
+
+ FieldProperties(String type) {
+ this.type = type;
+ }
+ }
+}
diff --git a/java/com/google/gerrit/extensions/api/projects/ConfigInfo.java b/java/com/google/gerrit/extensions/api/projects/ConfigInfo.java
index 80115aa..b3dd1f1 100644
--- a/java/com/google/gerrit/extensions/api/projects/ConfigInfo.java
+++ b/java/com/google/gerrit/extensions/api/projects/ConfigInfo.java
@@ -33,6 +33,7 @@
public InheritedBooleanInfo requireSignedPush;
public InheritedBooleanInfo rejectImplicitMerges;
public InheritedBooleanInfo privateByDefault;
+ public InheritedBooleanInfo workInProgressByDefault;
public InheritedBooleanInfo enableReviewerByEmail;
public InheritedBooleanInfo matchAuthorToCommitterDate;
public InheritedBooleanInfo rejectEmptyCommit;
diff --git a/java/com/google/gerrit/extensions/api/projects/ConfigInput.java b/java/com/google/gerrit/extensions/api/projects/ConfigInput.java
index 37a2e8b..1a6d77b 100644
--- a/java/com/google/gerrit/extensions/api/projects/ConfigInput.java
+++ b/java/com/google/gerrit/extensions/api/projects/ConfigInput.java
@@ -30,6 +30,7 @@
public InheritableBoolean requireSignedPush;
public InheritableBoolean rejectImplicitMerges;
public InheritableBoolean privateByDefault;
+ public InheritableBoolean workInProgressByDefault;
public InheritableBoolean enableReviewerByEmail;
public InheritableBoolean matchAuthorToCommitterDate;
public InheritableBoolean rejectEmptyCommit;
diff --git a/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java b/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java
index 9dcba5e..1f16d8d 100644
--- a/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java
+++ b/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java
@@ -158,6 +158,7 @@
public EmailFormat emailFormat;
public DefaultBase defaultBaseForMerges;
public Boolean publishCommentsOnPush;
+ public Boolean workInProgressByDefault;
public boolean isShowInfoInReviewCategory() {
return getReviewCategoryStrategy() != ReviewCategoryStrategy.NONE;
@@ -227,6 +228,7 @@
p.signedOffBy = false;
p.defaultBaseForMerges = DefaultBase.FIRST_PARENT;
p.publishCommentsOnPush = false;
+ p.workInProgressByDefault = false;
return p;
}
}
diff --git a/java/com/google/gerrit/extensions/common/testing/DiffInfoSubject.java b/java/com/google/gerrit/extensions/common/testing/DiffInfoSubject.java
index 6918325..057a1a2 100644
--- a/java/com/google/gerrit/extensions/common/testing/DiffInfoSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/DiffInfoSubject.java
@@ -47,4 +47,16 @@
DiffInfo diffInfo = actual();
return Truth.assertThat(diffInfo.changeType).named("changeType");
}
+
+ public FileMetaSubject metaA() {
+ isNotNull();
+ DiffInfo diffInfo = actual();
+ return FileMetaSubject.assertThat(diffInfo.metaA).named("metaA");
+ }
+
+ public FileMetaSubject metaB() {
+ isNotNull();
+ DiffInfo diffInfo = actual();
+ return FileMetaSubject.assertThat(diffInfo.metaB).named("metaB");
+ }
}
diff --git a/java/com/google/gerrit/extensions/common/testing/FileMetaSubject.java b/java/com/google/gerrit/extensions/common/testing/FileMetaSubject.java
new file mode 100644
index 0000000..e77eef1
--- /dev/null
+++ b/java/com/google/gerrit/extensions/common/testing/FileMetaSubject.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2018 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.testing;
+
+import static com.google.common.truth.Truth.assertAbout;
+
+import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.IntegerSubject;
+import com.google.common.truth.Subject;
+import com.google.common.truth.Truth;
+import com.google.gerrit.extensions.common.DiffInfo.FileMeta;
+
+public class FileMetaSubject extends Subject<FileMetaSubject, FileMeta> {
+
+ public static FileMetaSubject assertThat(FileMeta fileMeta) {
+ return assertAbout(FileMetaSubject::new).that(fileMeta);
+ }
+
+ private FileMetaSubject(FailureMetadata failureMetadata, FileMeta fileMeta) {
+ super(failureMetadata, fileMeta);
+ }
+
+ public IntegerSubject totalLineCount() {
+ isNotNull();
+ FileMeta fileMeta = actual();
+ return Truth.assertThat(fileMeta.lines).named("total line count");
+ }
+}
diff --git a/java/com/google/gerrit/extensions/restapi/AcceptsDelete.java b/java/com/google/gerrit/extensions/restapi/AcceptsDelete.java
index e1c4b3b..774e0a3 100644
--- a/java/com/google/gerrit/extensions/restapi/AcceptsDelete.java
+++ b/java/com/google/gerrit/extensions/restapi/AcceptsDelete.java
@@ -17,31 +17,17 @@
/**
* Optional interface for {@link RestCollection}.
*
- * <p>This interface is used for 2 purposes:
- *
- * <ul>
- * <li>to support {@code DELETE} directly on the collection itself
- * <li>to support {@code DELETE} on a non-existing member of the collection (in order to create
- * that member)
- * </ul>
+ * <p>This interface is used to support {@code DELETE} directly on the collection itself.
*
* <p>This interface is not supported for root collections.
*/
public interface AcceptsDelete<P extends RestResource> {
/**
- * Handle
- *
- * <ul>
- * <li>{@code DELETE} directly on the collection itself (in this case id is {@code null})
- * <li>{@code DELETE} on a non-existing member of the collection (in this case id is not {@code
- * null})
- * </ul>
+ * Handle {@code DELETE} directly on the collection itself.
*
* @param parent the collection
- * @param id id of the non-existing collection member for which the {@code DELETE} request is
- * done, {@code null} if the {@code DELETE} request is done on the collection itself
* @return a view to handle the {@code DELETE} request
* @throws RestApiException the view cannot be constructed
*/
- RestModifyView<P, ?> delete(P parent, IdString id) throws RestApiException;
+ RestModifyView<P, ?> delete(P parent) throws RestApiException;
}
diff --git a/java/com/google/gerrit/extensions/restapi/RestApiModule.java b/java/com/google/gerrit/extensions/restapi/RestApiModule.java
index d47b094..c1bb95e 100644
--- a/java/com/google/gerrit/extensions/restapi/RestApiModule.java
+++ b/java/com/google/gerrit/extensions/restapi/RestApiModule.java
@@ -29,6 +29,7 @@
protected static final String DELETE = "DELETE";
protected static final String POST = "POST";
protected static final String CREATE = "CREATE";
+ protected static final String DELETE_MISSING = "DELETE_MISSING";
protected static final String POST_ON_COLLECTION = "POST_ON_COLLECTION";
protected <R extends RestResource> ReadViewBinder<R> get(TypeLiteral<RestView<R>> viewType) {
@@ -57,6 +58,11 @@
return new CreateViewBinder<>(bind(viewType).annotatedWith(export(CREATE, "/")));
}
+ protected <R extends RestResource> DeleteViewBinder<R> deleteMissing(
+ TypeLiteral<RestView<R>> viewType) {
+ return new DeleteViewBinder<>(bind(viewType).annotatedWith(export(DELETE_MISSING, "/")));
+ }
+
protected <R extends RestResource> ReadViewBinder<R> get(
TypeLiteral<RestView<R>> viewType, String name) {
return new ReadViewBinder<>(view(viewType, GET, name));
@@ -203,6 +209,34 @@
}
}
+ public static class DeleteViewBinder<C extends RestResource> {
+ private final LinkedBindingBuilder<RestView<C>> binder;
+
+ private DeleteViewBinder(LinkedBindingBuilder<RestView<C>> binder) {
+ this.binder = binder;
+ }
+
+ public <P extends RestResource, T extends RestDeleteMissingView<P, C, ?>>
+ ScopedBindingBuilder to(Class<T> impl) {
+ return binder.to(impl);
+ }
+
+ public <P extends RestResource, T extends RestDeleteMissingView<P, C, ?>> void toInstance(
+ T impl) {
+ binder.toInstance(impl);
+ }
+
+ public <P extends RestResource, T extends RestDeleteMissingView<P, C, ?>>
+ ScopedBindingBuilder toProvider(Class<? extends Provider<? extends T>> providerType) {
+ return binder.toProvider(providerType);
+ }
+
+ public <P extends RestResource, T extends RestDeleteMissingView<P, C, ?>>
+ ScopedBindingBuilder toProvider(Provider<? extends T> provider) {
+ return binder.toProvider(provider);
+ }
+ }
+
public static class ChildCollectionBinder<P extends RestResource> {
private final LinkedBindingBuilder<RestView<P>> binder;
diff --git a/java/com/google/gerrit/extensions/restapi/RestDeleteMissingView.java b/java/com/google/gerrit/extensions/restapi/RestDeleteMissingView.java
new file mode 100644
index 0000000..1d58c80
--- /dev/null
+++ b/java/com/google/gerrit/extensions/restapi/RestDeleteMissingView.java
@@ -0,0 +1,49 @@
+// Copyright (C) 2018 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.restapi;
+
+/**
+ * RestView that supports accepting input and deleting a resource that is missing.
+ *
+ * <p>The RestDeleteMissingView solely exists to support a special case for creating a change edit
+ * by deleting a path in the non-existing change edit. This interface should not be used for new
+ * REST API's.
+ *
+ * <p>The input must be supplied as JSON as the body of the HTTP request. Delete views can be
+ * invoked by the HTTP method {@code DELETE}.
+ *
+ * <p>The RestDeleteMissingView is only invoked when the parse method of the {@code RestCollection}
+ * throws {@link ResourceNotFoundException}, and hence the resource doesn't exist yet.
+ *
+ * @param <P> type of the parent resource
+ * @param <C> type of the child resource that id deleted
+ * @param <I> type of input the JSON parser will parse the input into.
+ */
+public interface RestDeleteMissingView<P extends RestResource, C extends RestResource, I>
+ extends RestView<C> {
+
+ /**
+ * Process the view operation by deleting the resource.
+ *
+ * @param parentResource parent resource of the resource that should be deleted
+ * @param input input after parsing from request.
+ * @return result to return to the client. Use {@link BinaryResult} to avoid automatic conversion
+ * to JSON.
+ * @throws RestApiException if the resource creation is rejected
+ * @throws Exception the implementation of the view failed. The exception will be logged and HTTP
+ * 500 Internal Server Error will be returned to the client.
+ */
+ Object apply(P parentResource, IdString id, I input) throws Exception;
+}
diff --git a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index 599e98f..70296a3 100644
--- a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -88,6 +88,7 @@
import com.google.gerrit.extensions.restapi.RestCollection;
import com.google.gerrit.extensions.restapi.RestCollectionView;
import com.google.gerrit.extensions.restapi.RestCreateView;
+import com.google.gerrit.extensions.restapi.RestDeleteMissingView;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.extensions.restapi.RestResource;
@@ -325,15 +326,28 @@
checkPreconditions(req);
}
} catch (ResourceNotFoundException e) {
- if (!path.isEmpty() || (!isPost(req) && !isPut(req))) {
+ if (!path.isEmpty()) {
throw e;
}
- RestView<RestResource> createView = rc.views().get("gerrit", "CREATE./");
- if (createView != null) {
- viewData = new ViewData(null, createView);
- status = SC_CREATED;
- path.add(id);
+ if (isPost(req) || isPut(req)) {
+ RestView<RestResource> createView = rc.views().get("gerrit", "CREATE./");
+ if (createView != null) {
+ viewData = new ViewData(null, createView);
+ status = SC_CREATED;
+ path.add(id);
+ } else {
+ throw e;
+ }
+ } else if (isDelete(req)) {
+ RestView<RestResource> deleteView = rc.views().get("gerrit", "DELETE_MISSING./");
+ if (deleteView != null) {
+ viewData = new ViewData(null, deleteView);
+ status = SC_NO_CONTENT;
+ path.add(id);
+ } else {
+ throw e;
+ }
} else {
throw e;
}
@@ -363,7 +377,7 @@
} else if (c instanceof AcceptsDelete && isDelete(req)) {
@SuppressWarnings("unchecked")
AcceptsDelete<RestResource> ac = (AcceptsDelete<RestResource>) c;
- viewData = new ViewData(null, ac.delete(rsrc, null));
+ viewData = new ViewData(null, ac.delete(rsrc));
} else {
throw new MethodNotAllowedException();
}
@@ -388,11 +402,15 @@
} else {
throw e;
}
- } else if (c instanceof AcceptsDelete && isDelete(req)) {
- @SuppressWarnings("unchecked")
- AcceptsDelete<RestResource> ac = (AcceptsDelete<RestResource>) c;
- viewData = new ViewData(viewData.pluginName, ac.delete(rsrc, id));
- status = SC_NO_CONTENT;
+ } else if (isDelete(req)) {
+ RestView<RestResource> deleteView = c.views().get("gerrit", "DELETE_MISSING./");
+ if (deleteView != null) {
+ viewData = new ViewData(null, deleteView);
+ status = SC_NO_CONTENT;
+ path.add(id);
+ } else {
+ throw e;
+ }
} else {
throw e;
}
@@ -440,6 +458,19 @@
ServletUtils.consumeRequestBody(is);
}
}
+ } else if (viewData.view instanceof RestDeleteMissingView<?, ?, ?>) {
+ @SuppressWarnings("unchecked")
+ RestDeleteMissingView<RestResource, RestResource, Object> m =
+ (RestDeleteMissingView<RestResource, RestResource, Object>) viewData.view;
+
+ Type type = inputType(m);
+ inputRequestBody = parseRequest(req, type);
+ result = m.apply(rsrc, path.get(0), inputRequestBody);
+ if (inputRequestBody instanceof RawInput) {
+ try (InputStream is = req.getInputStream()) {
+ ServletUtils.consumeRequestBody(is);
+ }
+ }
} else if (viewData.view instanceof RestCollectionView<?, ?, ?>) {
@SuppressWarnings("unchecked")
RestCollectionView<RestResource, RestResource, Object> m =
@@ -797,6 +828,24 @@
return ((ParameterizedType) supertype).getActualTypeArguments()[2];
}
+ private static Type inputType(RestDeleteMissingView<RestResource, RestResource, Object> m) {
+ // MyCreateView implements RestDeleteMissingView<SomeResource, SomeResource, MyInput>
+ TypeLiteral<?> typeLiteral = TypeLiteral.get(m.getClass());
+
+ // RestDeleteMissingView<SomeResource, SomeResource, MyInput>
+ // This is smart enough to resolve even when there are intervening subclasses, even if they have
+ // reordered type arguments.
+ TypeLiteral<?> supertypeLiteral = typeLiteral.getSupertype(RestDeleteMissingView.class);
+
+ Type supertype = supertypeLiteral.getType();
+ checkState(
+ supertype instanceof ParameterizedType,
+ "supertype of %s is not parameterized: %s",
+ typeLiteral,
+ supertypeLiteral);
+ return ((ParameterizedType) supertype).getActualTypeArguments()[2];
+ }
+
private static Type inputType(RestCollectionView<RestResource, RestResource, Object> m) {
// MyCollectionView implements RestCollectionView<SomeResource, SomeResource, MyInput>
TypeLiteral<?> typeLiteral = TypeLiteral.get(m.getClass());
diff --git a/java/com/google/gerrit/reviewdb/client/BooleanProjectConfig.java b/java/com/google/gerrit/reviewdb/client/BooleanProjectConfig.java
index 765e38c..a70d254 100644
--- a/java/com/google/gerrit/reviewdb/client/BooleanProjectConfig.java
+++ b/java/com/google/gerrit/reviewdb/client/BooleanProjectConfig.java
@@ -40,7 +40,8 @@
PRIVATE_BY_DEFAULT("change", "privateByDefault"),
ENABLE_REVIEWER_BY_EMAIL("reviewer", "enableByEmail"),
MATCH_AUTHOR_TO_COMMITTER_DATE("submit", "matchAuthorToCommitterDate"),
- REJECT_EMPTY_COMMIT("submit", "rejectEmptyCommit");
+ REJECT_EMPTY_COMMIT("submit", "rejectEmptyCommit"),
+ WORK_IN_PROGRESS_BY_DEFAULT("change", "workInProgressByDefault");
// Git config
private final String section;
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index e93b2c57..516dcd4 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -2204,6 +2204,7 @@
}
private void setChangeId(int id) {
+ possiblyOverrideWorkInProgress();
changeId = new Change.Id(id);
ins =
@@ -2224,6 +2225,16 @@
}
}
+ private void possiblyOverrideWorkInProgress() {
+ // When wip or ready explicitly provided, leave it as is.
+ if (magicBranch.workInProgress || magicBranch.ready) {
+ return;
+ }
+ magicBranch.workInProgress =
+ projectState.is(BooleanProjectConfig.WORK_IN_PROGRESS_BY_DEFAULT)
+ || firstNonNull(user.state().getGeneralPreferences().workInProgressByDefault, false);
+ }
+
private void addOps(BatchUpdate bu) throws RestApiException {
checkState(changeId != null, "must call setChangeId before addOps");
try {
diff --git a/java/com/google/gerrit/server/ioutil/HexFormat.java b/java/com/google/gerrit/server/ioutil/HexFormat.java
new file mode 100644
index 0000000..fd9c17a
--- /dev/null
+++ b/java/com/google/gerrit/server/ioutil/HexFormat.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2018 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.ioutil;
+
+public class HexFormat {
+ public static String fromInt(int id) {
+ final char[] r = new char[8];
+ for (int p = 7; 0 <= p; p--) {
+ final int h = id & 0xf;
+ r[p] = h < 10 ? (char) ('0' + h) : (char) ('a' + (h - 10));
+ id >>= 4;
+ }
+ return new String(r);
+ }
+}
diff --git a/java/com/google/gerrit/server/notedb/CommentJsonMigrator.java b/java/com/google/gerrit/server/notedb/CommentJsonMigrator.java
index 5007ec9..b250a34 100644
--- a/java/com/google/gerrit/server/notedb/CommentJsonMigrator.java
+++ b/java/com/google/gerrit/server/notedb/CommentJsonMigrator.java
@@ -14,9 +14,11 @@
package com.google.gerrit.server.notedb;
+import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.gerrit.server.notedb.RevisionNote.MAX_NOTE_SZ;
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
+import com.google.common.collect.ImmutableList;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchLineComment.Status;
@@ -29,6 +31,7 @@
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
+import java.util.List;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.internal.storage.file.FileRepository;
import org.eclipse.jgit.internal.storage.file.PackInserter;
@@ -54,7 +57,7 @@
public static class ProjectMigrationResult {
public int skipped;
public boolean ok;
- public int refsUpdated;
+ public List<String> refsUpdated;
}
private final LegacyChangeNoteRead legacyChangeNoteRead;
@@ -77,9 +80,12 @@
this.allUsers = allUsers;
}
- public ProjectMigrationResult migrateProject(Project.NameKey project, Repository repo) {
+ public ProjectMigrationResult migrateProject(
+ Project.NameKey project, Repository repo, boolean dryRun) {
ProjectMigrationResult progress = new ProjectMigrationResult();
progress.ok = true;
+ progress.skipped = 0;
+ progress.refsUpdated = ImmutableList.of();
try (RevWalk rw = new RevWalk(repo);
ObjectInserter ins = newPackInserter(repo)) {
BatchRefUpdate bru = repo.getRefDatabase().newBatchUpdate();
@@ -89,17 +95,20 @@
progress.ok &= migrateDrafts(allUsers, repo, rw, ins, bru);
}
- progress.refsUpdated += bru.getCommands().size();
+ progress.refsUpdated =
+ bru.getCommands().stream().map(c -> c.getRefName()).collect(toImmutableList());
if (!bru.getCommands().isEmpty()) {
- ins.flush();
- RefUpdateUtil.executeChecked(bru, rw);
-
+ if (!dryRun) {
+ ins.flush();
+ RefUpdateUtil.executeChecked(bru, rw);
+ }
} else {
progress.skipped++;
}
} catch (IOException e) {
progress.ok = false;
}
+
return progress;
}
diff --git a/java/com/google/gerrit/server/patch/PatchScriptBuilder.java b/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
index 6f3e055..b4f7251 100644
--- a/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
+++ b/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
@@ -37,6 +37,7 @@
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
+import java.util.Optional;
import org.eclipse.jgit.diff.Edit;
import org.eclipse.jgit.errors.CorruptObjectException;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
@@ -172,6 +173,8 @@
}
}
+ correctForDifferencesInNewlineAtEnd();
+
if (comments != null) {
ensureCommentsVisible(comments);
}
@@ -277,6 +280,43 @@
}
}
+ private void correctForDifferencesInNewlineAtEnd() {
+ // a.src.size() is the size ignoring a newline at the end whereas a.size() considers it.
+ int aSize = a.src.size();
+ int bSize = b.src.size();
+
+ Optional<Edit> lastEdit = getLast(edits);
+ if (isNewlineAtEndDeleted()) {
+ Optional<Edit> lastLineEdit = lastEdit.filter(edit -> edit.getEndA() == aSize);
+ if (lastLineEdit.isPresent()) {
+ lastLineEdit.get().extendA();
+ } else {
+ Edit newlineEdit = new Edit(aSize, aSize + 1, bSize, bSize);
+ edits.add(newlineEdit);
+ }
+ } else if (isNewlineAtEndAdded()) {
+ Optional<Edit> lastLineEdit = lastEdit.filter(edit -> edit.getEndB() == bSize);
+ if (lastLineEdit.isPresent()) {
+ lastLineEdit.get().extendB();
+ } else {
+ Edit newlineEdit = new Edit(aSize, aSize, bSize, bSize + 1);
+ edits.add(newlineEdit);
+ }
+ }
+ }
+
+ private static <T> Optional<T> getLast(List<T> list) {
+ return list.isEmpty() ? Optional.empty() : Optional.ofNullable(list.get(list.size() - 1));
+ }
+
+ private boolean isNewlineAtEndDeleted() {
+ return !a.src.isMissingNewlineAtEnd() && b.src.isMissingNewlineAtEnd();
+ }
+
+ private boolean isNewlineAtEndAdded() {
+ return a.src.isMissingNewlineAtEnd() && !b.src.isMissingNewlineAtEnd();
+ }
+
private void ensureCommentsVisible(CommentDetail comments) {
if (comments.getCommentsA().isEmpty() && comments.getCommentsB().isEmpty()) {
// No comments, no additional dummy edits are required.
@@ -396,14 +436,14 @@
for (EditList.Hunk hunk : list.getHunks()) {
while (hunk.next()) {
if (hunk.isContextLine()) {
- final String lineA = a.src.getString(hunk.getCurA());
+ String lineA = a.getSourceLine(hunk.getCurA());
a.dst.addLine(hunk.getCurA(), lineA);
if (ignoredWhitespace) {
// If we ignored whitespace in some form, also get the line
// from b when it does not exactly match the line from a.
//
- final String lineB = b.src.getString(hunk.getCurB());
+ String lineB = b.getSourceLine(hunk.getCurB());
if (!lineA.equals(lineB)) {
b.dst.addLine(hunk.getCurB(), lineB);
}
@@ -437,11 +477,22 @@
final SparseFileContent dst = new SparseFileContent();
int size() {
- return src != null ? src.size() : 0;
+ if (src == null) {
+ return 0;
+ }
+ if (src.isMissingNewlineAtEnd()) {
+ return src.size();
+ }
+ return src.size() + 1;
}
- void addLine(int line) {
- dst.addLine(line, src.getString(line));
+ void addLine(int lineNumber) {
+ String lineContent = getSourceLine(lineNumber);
+ dst.addLine(lineNumber, lineContent);
+ }
+
+ String getSourceLine(int lineNumber) {
+ return lineNumber >= src.size() ? "" : src.getString(lineNumber);
}
void resolve(Side other, ObjectId within) throws IOException {
diff --git a/java/com/google/gerrit/server/permissions/ChangeControl.java b/java/com/google/gerrit/server/permissions/ChangeControl.java
index 4aaf61d..f4e659e 100644
--- a/java/com/google/gerrit/server/permissions/ChangeControl.java
+++ b/java/com/google/gerrit/server/permissions/ChangeControl.java
@@ -30,7 +30,6 @@
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.permissions.PermissionBackend.ForChange;
import com.google.gerrit.server.query.change.ChangeData;
@@ -49,16 +48,11 @@
static class Factory {
private final ChangeData.Factory changeDataFactory;
private final ChangeNotes.Factory notesFactory;
- private final IdentifiedUser.GenericFactory identifiedUserFactory;
@Inject
- Factory(
- ChangeData.Factory changeDataFactory,
- ChangeNotes.Factory notesFactory,
- IdentifiedUser.GenericFactory identifiedUserFactory) {
+ Factory(ChangeData.Factory changeDataFactory, ChangeNotes.Factory notesFactory) {
this.changeDataFactory = changeDataFactory;
this.notesFactory = notesFactory;
- this.identifiedUserFactory = identifiedUserFactory;
}
ChangeControl create(
@@ -68,22 +62,17 @@
}
ChangeControl create(RefControl refControl, ChangeNotes notes) {
- return new ChangeControl(changeDataFactory, identifiedUserFactory, refControl, notes);
+ return new ChangeControl(changeDataFactory, refControl, notes);
}
}
private final ChangeData.Factory changeDataFactory;
- private final IdentifiedUser.GenericFactory identifiedUserFactory;
private final RefControl refControl;
private final ChangeNotes notes;
private ChangeControl(
- ChangeData.Factory changeDataFactory,
- IdentifiedUser.GenericFactory identifiedUserFactory,
- RefControl refControl,
- ChangeNotes notes) {
+ ChangeData.Factory changeDataFactory, RefControl refControl, ChangeNotes notes) {
this.changeDataFactory = changeDataFactory;
- this.identifiedUserFactory = identifiedUserFactory;
this.refControl = refControl;
this.notes = notes;
}
@@ -92,11 +81,6 @@
return new ForChangeImpl(cd, db);
}
- private ChangeControl forUser(CurrentUser who) {
- return new ChangeControl(
- changeDataFactory, identifiedUserFactory, refControl.forUser(who), notes);
- }
-
private CurrentUser getUser() {
return refControl.getUser();
}
@@ -261,16 +245,6 @@
}
@Override
- public ForChange user(CurrentUser user) {
- return forUser(user).asForChange(cd, db);
- }
-
- @Override
- public ForChange absentUser(Account.Id id) {
- return user(identifiedUserFactory.create(id));
- }
-
- @Override
public String resourcePath() {
if (resourcePath == null) {
resourcePath =
diff --git a/java/com/google/gerrit/server/permissions/FailedPermissionBackend.java b/java/com/google/gerrit/server/permissions/FailedPermissionBackend.java
index 6c6f136..bd7c549 100644
--- a/java/com/google/gerrit/server/permissions/FailedPermissionBackend.java
+++ b/java/com/google/gerrit/server/permissions/FailedPermissionBackend.java
@@ -16,10 +16,8 @@
import com.google.gerrit.extensions.api.access.GlobalOrPluginPermission;
import com.google.gerrit.extensions.conditions.BooleanCondition;
-import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.permissions.PermissionBackend.ForChange;
import com.google.gerrit.server.permissions.PermissionBackend.ForProject;
@@ -122,16 +120,6 @@
}
@Override
- public ForProject user(CurrentUser user) {
- return this;
- }
-
- @Override
- public ForProject absentUser(Account.Id id) {
- return this;
- }
-
- @Override
public String resourcePath() {
throw new UnsupportedOperationException(
"FailedPermissionBackend is not scoped to a resource");
@@ -181,16 +169,6 @@
}
@Override
- public ForRef user(CurrentUser user) {
- return this;
- }
-
- @Override
- public ForRef absentUser(Account.Id id) {
- return this;
- }
-
- @Override
public String resourcePath() {
throw new UnsupportedOperationException(
"FailedPermissionBackend is not scoped to a resource");
@@ -244,16 +222,6 @@
}
@Override
- public ForChange user(CurrentUser user) {
- return this;
- }
-
- @Override
- public ForChange absentUser(Account.Id id) {
- return this;
- }
-
- @Override
public String resourcePath() {
throw new UnsupportedOperationException(
"FailedPermissionBackend is not scoped to a resource");
diff --git a/java/com/google/gerrit/server/permissions/PermissionBackend.java b/java/com/google/gerrit/server/permissions/PermissionBackend.java
index 0690d6c..4719aa9 100644
--- a/java/com/google/gerrit/server/permissions/PermissionBackend.java
+++ b/java/com/google/gerrit/server/permissions/PermissionBackend.java
@@ -294,12 +294,6 @@
/** Returns the fully qualified resource path that this instance is scoped to. */
public abstract String resourcePath();
- /** Returns a new instance rescoped to same project, but different {@code user}. */
- public abstract ForProject user(CurrentUser user);
-
- /** @see PermissionBackend#absentUser(Account.Id) */
- public abstract ForProject absentUser(Account.Id id);
-
/** Returns an instance scoped for {@code ref} in this project. */
public abstract ForRef ref(String ref);
@@ -400,12 +394,6 @@
/** Returns a fully qualified resource path that this instance is scoped to. */
public abstract String resourcePath();
- /** Returns a new instance rescoped to same reference, but different {@code user}. */
- public abstract ForRef user(CurrentUser user);
-
- /** @see PermissionBackend#absentUser(Account.Id) */
- public abstract ForRef absentUser(Account.Id id);
-
/** Returns an instance scoped to change. */
public abstract ForChange change(ChangeData cd);
@@ -456,12 +444,6 @@
/** Returns the fully qualified resource path that this instance is scoped to. */
public abstract String resourcePath();
- /** Returns a new instance rescoped to same change, but different {@code user}. */
- public abstract ForChange user(CurrentUser user);
-
- /** @see PermissionBackend#absentUser(Account.Id) */
- public abstract ForChange absentUser(Account.Id id);
-
/** Verify scoped user can {@code perm}, throwing if denied. */
public abstract void check(ChangePermissionOrLabel perm)
throws AuthException, PermissionBackendException;
diff --git a/java/com/google/gerrit/server/permissions/ProjectControl.java b/java/com/google/gerrit/server/permissions/ProjectControl.java
index ed12a2b..67662c7 100644
--- a/java/com/google/gerrit/server/permissions/ProjectControl.java
+++ b/java/com/google/gerrit/server/permissions/ProjectControl.java
@@ -21,7 +21,6 @@
import com.google.gerrit.common.data.PermissionRule;
import com.google.gerrit.extensions.conditions.BooleanCondition;
import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Change;
@@ -324,16 +323,6 @@
private String resourcePath;
@Override
- public ForProject user(CurrentUser user) {
- return forUser(user).asForProject().database(db);
- }
-
- @Override
- public ForProject absentUser(Account.Id id) {
- return user(identifiedUserFactory.create(id));
- }
-
- @Override
public String resourcePath() {
if (resourcePath == null) {
resourcePath = "/projects/" + getProjectState().getName();
diff --git a/java/com/google/gerrit/server/permissions/RefControl.java b/java/com/google/gerrit/server/permissions/RefControl.java
index 012d68a..3bd2817 100644
--- a/java/com/google/gerrit/server/permissions/RefControl.java
+++ b/java/com/google/gerrit/server/permissions/RefControl.java
@@ -22,7 +22,6 @@
import com.google.gerrit.common.data.PermissionRule.Action;
import com.google.gerrit.extensions.conditions.BooleanCondition;
import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RefNames;
@@ -409,16 +408,6 @@
private String resourcePath;
@Override
- public ForRef user(CurrentUser user) {
- return forUser(user).asForRef().database(db);
- }
-
- @Override
- public ForRef absentUser(Account.Id id) {
- return user(identifiedUserFactory.create(id));
- }
-
- @Override
public String resourcePath() {
if (resourcePath == null) {
resourcePath =
diff --git a/java/com/google/gerrit/server/project/BooleanProjectConfigTransformations.java b/java/com/google/gerrit/server/project/BooleanProjectConfigTransformations.java
index 61a7ef2..79eccbb 100644
--- a/java/com/google/gerrit/server/project/BooleanProjectConfigTransformations.java
+++ b/java/com/google/gerrit/server/project/BooleanProjectConfigTransformations.java
@@ -68,6 +68,9 @@
.put(
BooleanProjectConfig.REJECT_EMPTY_COMMIT,
new Mapper(i -> i.rejectEmptyCommit, (i, v) -> i.rejectEmptyCommit = v))
+ .put(
+ BooleanProjectConfig.WORK_IN_PROGRESS_BY_DEFAULT,
+ new Mapper(i -> i.workInProgressByDefault, (i, v) -> i.workInProgressByDefault = v))
.build();
static {
diff --git a/java/com/google/gerrit/server/restapi/change/ChangeEdits.java b/java/com/google/gerrit/server/restapi/change/ChangeEdits.java
index efc2712..70eb3e5 100644
--- a/java/com/google/gerrit/server/restapi/change/ChangeEdits.java
+++ b/java/com/google/gerrit/server/restapi/change/ChangeEdits.java
@@ -19,7 +19,6 @@
import com.google.gerrit.extensions.common.EditInfo;
import com.google.gerrit.extensions.common.Input;
import com.google.gerrit.extensions.registration.DynamicMap;
-import com.google.gerrit.extensions.restapi.AcceptsDelete;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.BinaryResult;
@@ -30,9 +29,9 @@
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.Response;
-import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestCollectionView;
import com.google.gerrit.extensions.restapi.RestCreateView;
+import com.google.gerrit.extensions.restapi.RestDeleteMissingView;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.extensions.restapi.RestView;
@@ -59,7 +58,6 @@
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
-import com.google.inject.assistedinject.Assisted;
import java.io.IOException;
import java.util.List;
import java.util.Optional;
@@ -70,10 +68,8 @@
import org.kohsuke.args4j.Option;
@Singleton
-public class ChangeEdits
- implements ChildCollection<ChangeResource, ChangeEditResource>, AcceptsDelete<ChangeResource> {
+public class ChangeEdits implements ChildCollection<ChangeResource, ChangeEditResource> {
private final DynamicMap<RestView<ChangeEditResource>> views;
- private final DeleteFile.Factory deleteFileFactory;
private final Provider<Detail> detail;
private final ChangeEditUtil editUtil;
@@ -81,12 +77,10 @@
ChangeEdits(
DynamicMap<RestView<ChangeEditResource>> views,
Provider<Detail> detail,
- ChangeEditUtil editUtil,
- DeleteFile.Factory deleteFileFactory) {
+ ChangeEditUtil editUtil) {
this.views = views;
this.detail = detail;
this.editUtil = editUtil;
- this.deleteFileFactory = deleteFileFactory;
}
@Override
@@ -110,20 +104,6 @@
}
/**
- * This method is invoked if a DELETE request on a non-existing member is done. For change edits
- * this is the case if a DELETE request for a file in a change edit is done and the change edit
- * doesn't exist yet (and hence the parse method returned ResourceNotFoundException). In this case
- * we want to create the change edit on the fly and delete the file with the given id in it.
- */
- @Override
- public DeleteFile delete(ChangeResource parent, IdString id) throws RestApiException {
- // It's safe to assume that id can never be null, because
- // otherwise we would end up in dedicated endpoint for
- // deleting of change edits and not a file in change edit
- return deleteFileFactory.create(id.get());
- }
-
- /**
* Create handler that is activated when collection element is accessed but doesn't exist, e. g.
* PUT request with a path was called but change edit wasn't created yet. Change edit is created
* and PUT handler is called.
@@ -146,26 +126,20 @@
}
}
- public static class DeleteFile implements RestModifyView<ChangeResource, Input> {
-
- public interface Factory {
- DeleteFile create(String path);
- }
-
+ public static class DeleteFile
+ implements RestDeleteMissingView<ChangeResource, ChangeEditResource, Input> {
private final DeleteContent deleteContent;
- private final String path;
@Inject
- DeleteFile(DeleteContent deleteContent, @Assisted String path) {
+ DeleteFile(DeleteContent deleteContent) {
this.deleteContent = deleteContent;
- this.path = path;
}
@Override
- public Response<?> apply(ChangeResource rsrc, Input in)
+ public Response<?> apply(ChangeResource rsrc, IdString id, Input in)
throws IOException, AuthException, ResourceConflictException, OrmException,
PermissionBackendException {
- return deleteContent.apply(rsrc, path);
+ return deleteContent.apply(rsrc, id.get());
}
}
diff --git a/java/com/google/gerrit/server/restapi/change/CreateChange.java b/java/com/google/gerrit/server/restapi/change/CreateChange.java
index 0dcbeb0..a04716d 100644
--- a/java/com/google/gerrit/server/restapi/change/CreateChange.java
+++ b/java/com/google/gerrit/server/restapi/change/CreateChange.java
@@ -266,6 +266,12 @@
AccountState accountState = me.state();
GeneralPreferencesInfo info = accountState.getGeneralPreferences();
+ boolean isWorkInProgress =
+ input.workInProgress == null
+ ? rsrc.getProjectState().is(BooleanProjectConfig.WORK_IN_PROGRESS_BY_DEFAULT)
+ || MoreObjects.firstNonNull(info.workInProgressByDefault, false)
+ : input.workInProgress;
+
// Add a Change-Id line if there isn't already one
String commitMessage = subject;
if (ChangeIdUtil.indexOfChangeId(commitMessage, "\n") == -1) {
@@ -309,7 +315,7 @@
}
ins.setTopic(topic);
ins.setPrivate(isPrivate);
- ins.setWorkInProgress(input.workInProgress != null && input.workInProgress);
+ ins.setWorkInProgress(isWorkInProgress);
ins.setGroups(groups);
ins.setNotify(input.notify);
ins.setAccountsToNotify(notifyUtil.resolveAccounts(input.notifyDetails));
diff --git a/java/com/google/gerrit/server/restapi/change/Module.java b/java/com/google/gerrit/server/restapi/change/Module.java
index dca64fd..5c13303 100644
--- a/java/com/google/gerrit/server/restapi/change/Module.java
+++ b/java/com/google/gerrit/server/restapi/change/Module.java
@@ -171,6 +171,7 @@
child(CHANGE_KIND, "edit").to(ChangeEdits.class);
create(CHANGE_EDIT_KIND).to(ChangeEdits.Create.class);
+ deleteMissing(CHANGE_EDIT_KIND).to(ChangeEdits.DeleteFile.class);
postOnCollection(CHANGE_EDIT_KIND).to(ChangeEdits.Post.class);
delete(CHANGE_KIND, "edit").to(DeleteChangeEdit.class);
child(CHANGE_KIND, "edit:publish").to(PublishChangeEdit.class);
@@ -189,7 +190,6 @@
get(CHANGE_MESSAGE_KIND).to(GetChangeMessage.class);
factory(AccountLoader.Factory.class);
- factory(ChangeEdits.DeleteFile.Factory.class);
factory(ChangeInserter.Factory.class);
factory(ChangeResource.Factory.class);
factory(DeleteReviewerByEmailOp.Factory.class);
diff --git a/java/com/google/gerrit/server/restapi/config/ListTasks.java b/java/com/google/gerrit/server/restapi/config/ListTasks.java
index d700028..7b69831 100644
--- a/java/com/google/gerrit/server/restapi/config/ListTasks.java
+++ b/java/com/google/gerrit/server/restapi/config/ListTasks.java
@@ -23,13 +23,13 @@
import com.google.gerrit.server.git.WorkQueue;
import com.google.gerrit.server.git.WorkQueue.ProjectTask;
import com.google.gerrit.server.git.WorkQueue.Task;
+import com.google.gerrit.server.ioutil.HexFormat;
import com.google.gerrit.server.permissions.GlobalPermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.permissions.ProjectPermission;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectState;
-import com.google.gerrit.server.util.IdGenerator;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
@@ -133,7 +133,7 @@
public String queueName;
public TaskInfo(Task<?> task) {
- this.id = IdGenerator.format(task.getTaskId());
+ this.id = HexFormat.fromInt(task.getTaskId());
this.state = task.getState();
this.startTime = new Timestamp(task.getStartTime().getTime());
this.delay = task.getDelay(TimeUnit.MILLISECONDS);
diff --git a/java/com/google/gerrit/server/restapi/project/DeleteBranch.java b/java/com/google/gerrit/server/restapi/project/DeleteBranch.java
index aed372c..0134ce3 100644
--- a/java/com/google/gerrit/server/restapi/project/DeleteBranch.java
+++ b/java/com/google/gerrit/server/restapi/project/DeleteBranch.java
@@ -23,9 +23,7 @@
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.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.permissions.RefPermission;
import com.google.gerrit.server.project.BranchResource;
import com.google.gerrit.server.query.change.InternalChangeQuery;
import com.google.gwtorm.server.OrmException;
@@ -38,17 +36,12 @@
public class DeleteBranch implements RestModifyView<BranchResource, Input> {
private final Provider<InternalChangeQuery> queryProvider;
- private final DeleteRef.Factory deleteRefFactory;
- private final PermissionBackend permissionBackend;
+ private final DeleteRef deleteRef;
@Inject
- DeleteBranch(
- Provider<InternalChangeQuery> queryProvider,
- DeleteRef.Factory deleteRefFactory,
- PermissionBackend permissionBackend) {
+ DeleteBranch(Provider<InternalChangeQuery> queryProvider, DeleteRef deleteRef) {
this.queryProvider = queryProvider;
- this.deleteRefFactory = deleteRefFactory;
- this.permissionBackend = permissionBackend;
+ this.deleteRef = deleteRef;
}
@Override
@@ -60,14 +53,11 @@
"not allowed to delete branch " + rsrc.getBranchKey().get());
}
- permissionBackend.currentUser().ref(rsrc.getBranchKey()).check(RefPermission.DELETE);
- rsrc.getProjectState().checkStatePermitsWrite();
-
if (!queryProvider.get().setLimit(1).byBranchOpen(rsrc.getBranchKey()).isEmpty()) {
throw new ResourceConflictException("branch " + rsrc.getBranchKey() + " has open changes");
}
- deleteRefFactory.create(rsrc).ref(rsrc.getRef()).prefix(R_HEADS).delete();
+ deleteRef.deleteSingleRef(rsrc.getProjectState(), rsrc.getRef(), R_HEADS);
return Response.none();
}
}
diff --git a/java/com/google/gerrit/server/restapi/project/DeleteBranches.java b/java/com/google/gerrit/server/restapi/project/DeleteBranches.java
index d8166e1..6e60193 100644
--- a/java/com/google/gerrit/server/restapi/project/DeleteBranches.java
+++ b/java/com/google/gerrit/server/restapi/project/DeleteBranches.java
@@ -16,6 +16,7 @@
import static org.eclipse.jgit.lib.Constants.R_HEADS;
+import com.google.common.collect.ImmutableSet;
import com.google.gerrit.extensions.api.projects.DeleteBranchesInput;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.Response;
@@ -30,11 +31,11 @@
@Singleton
public class DeleteBranches implements RestModifyView<ProjectResource, DeleteBranchesInput> {
- private final DeleteRef.Factory deleteRefFactory;
+ private final DeleteRef deleteRef;
@Inject
- DeleteBranches(DeleteRef.Factory deleteRefFactory) {
- this.deleteRefFactory = deleteRefFactory;
+ DeleteBranches(DeleteRef deleteRef) {
+ this.deleteRef = deleteRef;
}
@Override
@@ -43,7 +44,8 @@
if (input == null || input.branches == null || input.branches.isEmpty()) {
throw new BadRequestException("branches must be specified");
}
- deleteRefFactory.create(project).refs(input.branches).prefix(R_HEADS).delete();
+ deleteRef.deleteMultipleRefs(
+ project.getProjectState(), ImmutableSet.copyOf(input.branches), R_HEADS);
return Response.none();
}
}
diff --git a/java/com/google/gerrit/server/restapi/project/DeleteRef.java b/java/com/google/gerrit/server/restapi/project/DeleteRef.java
index 769eaf8..0016354 100644
--- a/java/com/google/gerrit/server/restapi/project/DeleteRef.java
+++ b/java/com/google/gerrit/server/restapi/project/DeleteRef.java
@@ -14,33 +14,35 @@
package com.google.gerrit.server.restapi.project;
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.gerrit.reviewdb.client.RefNames.isConfigRef;
import static java.lang.String.format;
-import static java.util.stream.Collectors.toList;
import static org.eclipse.jgit.lib.Constants.R_REFS;
import static org.eclipse.jgit.lib.Constants.R_TAGS;
import static org.eclipse.jgit.transport.ReceiveCommand.Type.DELETE;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.GitRepositoryManager;
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.project.ProjectResource;
+import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.project.RefValidationHelper;
import com.google.gerrit.server.query.change.InternalChangeQuery;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
-import com.google.inject.assistedinject.Assisted;
+import com.google.inject.Singleton;
import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
import org.eclipse.jgit.errors.LockFailedException;
import org.eclipse.jgit.lib.BatchRefUpdate;
import org.eclipse.jgit.lib.NullProgressMonitor;
@@ -52,6 +54,7 @@
import org.eclipse.jgit.transport.ReceiveCommand;
import org.eclipse.jgit.transport.ReceiveCommand.Result;
+@Singleton
public class DeleteRef {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@@ -64,13 +67,6 @@
private final GitReferenceUpdated referenceUpdated;
private final RefValidationHelper refDeletionValidator;
private final Provider<InternalChangeQuery> queryProvider;
- private final ProjectResource resource;
- private final List<String> refsToDelete;
- private String prefix;
-
- public interface Factory {
- DeleteRef create(ProjectResource r);
- }
@Inject
DeleteRef(
@@ -79,135 +75,164 @@
GitRepositoryManager repoManager,
GitReferenceUpdated referenceUpdated,
RefValidationHelper.Factory refDeletionValidatorFactory,
- Provider<InternalChangeQuery> queryProvider,
- @Assisted ProjectResource resource) {
+ Provider<InternalChangeQuery> queryProvider) {
this.identifiedUser = identifiedUser;
this.permissionBackend = permissionBackend;
this.repoManager = repoManager;
this.referenceUpdated = referenceUpdated;
this.refDeletionValidator = refDeletionValidatorFactory.create(DELETE);
this.queryProvider = queryProvider;
- this.resource = resource;
- this.refsToDelete = new ArrayList<>();
}
- public DeleteRef ref(String ref) {
- this.refsToDelete.add(ref);
- return this;
+ /**
+ * Deletes a single ref from the repository.
+ *
+ * @param projectState the {@code ProjectState} of the project containing the target ref.
+ * @param ref the ref to be deleted.
+ * @throws IOException
+ * @throws ResourceConflictException
+ */
+ public void deleteSingleRef(ProjectState projectState, String ref)
+ throws IOException, ResourceConflictException, AuthException, PermissionBackendException {
+ deleteSingleRef(projectState, ref, null);
}
- public DeleteRef refs(List<String> refs) {
- this.refsToDelete.addAll(refs);
- return this;
- }
-
- public DeleteRef prefix(String prefix) {
- this.prefix = prefix;
- return this;
- }
-
- public void delete()
- throws OrmException, IOException, ResourceConflictException, PermissionBackendException {
- if (!refsToDelete.isEmpty()) {
- try (Repository r = repoManager.openRepository(resource.getNameKey())) {
- if (refsToDelete.size() == 1) {
- deleteSingleRef(r);
- } else {
- deleteMultipleRefs(r);
- }
- }
- }
- }
-
- private void deleteSingleRef(Repository r) throws IOException, ResourceConflictException {
- String ref = refsToDelete.get(0);
+ /**
+ * Deletes a single ref from the repository.
+ *
+ * @param projectState the {@code ProjectState} of the project containing the target ref.
+ * @param ref the ref to be deleted.
+ * @param prefix the prefix of the ref.
+ * @throws IOException
+ * @throws ResourceConflictException
+ */
+ public void deleteSingleRef(ProjectState projectState, String ref, @Nullable String prefix)
+ throws IOException, ResourceConflictException, AuthException, PermissionBackendException {
if (prefix != null && !ref.startsWith(R_REFS)) {
ref = prefix + ref;
}
- RefUpdate.Result result;
- RefUpdate u = r.updateRef(ref);
- u.setExpectedOldObjectId(r.exactRef(ref).getObjectId());
- u.setNewObjectId(ObjectId.zeroId());
- u.setForceUpdate(true);
- refDeletionValidator.validateRefOperation(resource.getName(), identifiedUser.get(), u);
- int remainingLockFailureCalls = MAX_LOCK_FAILURE_CALLS;
- for (; ; ) {
- try {
- result = u.delete();
- } catch (LockFailedException e) {
- result = RefUpdate.Result.LOCK_FAILURE;
- } catch (IOException e) {
- logger.atSevere().withCause(e).log("Cannot delete %s", ref);
- throw e;
- }
- if (result == RefUpdate.Result.LOCK_FAILURE && --remainingLockFailureCalls > 0) {
+
+ projectState.checkStatePermitsWrite();
+ permissionBackend
+ .currentUser()
+ .project(projectState.getNameKey())
+ .ref(ref)
+ .check(RefPermission.DELETE);
+
+ try (Repository repository = repoManager.openRepository(projectState.getNameKey())) {
+ RefUpdate.Result result;
+ RefUpdate u = repository.updateRef(ref);
+ u.setExpectedOldObjectId(repository.exactRef(ref).getObjectId());
+ u.setNewObjectId(ObjectId.zeroId());
+ u.setForceUpdate(true);
+ refDeletionValidator.validateRefOperation(projectState.getName(), identifiedUser.get(), u);
+ int remainingLockFailureCalls = MAX_LOCK_FAILURE_CALLS;
+ for (; ; ) {
try {
- Thread.sleep(SLEEP_ON_LOCK_FAILURE_MS);
- } catch (InterruptedException ie) {
- // ignore
+ result = u.delete();
+ } catch (LockFailedException e) {
+ result = RefUpdate.Result.LOCK_FAILURE;
+ } catch (IOException e) {
+ logger.atSevere().withCause(e).log("Cannot delete %s", ref);
+ throw e;
}
- } else {
- break;
+ if (result == RefUpdate.Result.LOCK_FAILURE && --remainingLockFailureCalls > 0) {
+ try {
+ Thread.sleep(SLEEP_ON_LOCK_FAILURE_MS);
+ } catch (InterruptedException ie) {
+ // ignore
+ }
+ } else {
+ break;
+ }
}
- }
- switch (result) {
- case NEW:
- case NO_CHANGE:
- case FAST_FORWARD:
- case FORCED:
- referenceUpdated.fire(
- resource.getNameKey(), u, ReceiveCommand.Type.DELETE, identifiedUser.get().state());
- break;
+ switch (result) {
+ case NEW:
+ case NO_CHANGE:
+ case FAST_FORWARD:
+ case FORCED:
+ referenceUpdated.fire(
+ projectState.getNameKey(),
+ u,
+ ReceiveCommand.Type.DELETE,
+ identifiedUser.get().state());
+ break;
- case REJECTED_CURRENT_BRANCH:
- logger.atSevere().log("Cannot delete %s: %s", ref, result.name());
- throw new ResourceConflictException("cannot delete current branch");
+ case REJECTED_CURRENT_BRANCH:
+ logger.atSevere().log("Cannot delete %s: %s", ref, result.name());
+ throw new ResourceConflictException("cannot delete current branch");
- case IO_FAILURE:
- case LOCK_FAILURE:
- case NOT_ATTEMPTED:
- case REJECTED:
- case RENAMED:
- case REJECTED_MISSING_OBJECT:
- case REJECTED_OTHER_REASON:
- default:
- logger.atSevere().log("Cannot delete %s: %s", ref, result.name());
- throw new ResourceConflictException("cannot delete: " + result.name());
+ case IO_FAILURE:
+ case LOCK_FAILURE:
+ case NOT_ATTEMPTED:
+ case REJECTED:
+ case RENAMED:
+ case REJECTED_MISSING_OBJECT:
+ case REJECTED_OTHER_REASON:
+ default:
+ logger.atSevere().log("Cannot delete %s: %s", ref, result.name());
+ throw new ResourceConflictException("cannot delete: " + result.name());
+ }
}
}
- private void deleteMultipleRefs(Repository r)
- throws OrmException, IOException, ResourceConflictException, PermissionBackendException {
- BatchRefUpdate batchUpdate = r.getRefDatabase().newBatchUpdate();
- batchUpdate.setAtomic(false);
- List<String> refs =
- prefix == null
- ? refsToDelete
- : refsToDelete
- .stream()
- .map(ref -> ref.startsWith(R_REFS) ? ref : prefix + ref)
- .collect(toList());
- for (String ref : refs) {
- batchUpdate.addCommand(createDeleteCommand(resource, r, ref));
+ /**
+ * Deletes a set of refs from the repository.
+ *
+ * @param projectState the {@code ProjectState} of the project whose refs are to be deleted.
+ * @param refsToDelete the refs to be deleted.
+ * @param prefix the prefix of the refs.
+ * @throws OrmException
+ * @throws IOException
+ * @throws ResourceConflictException
+ * @throws PermissionBackendException
+ */
+ public void deleteMultipleRefs(
+ ProjectState projectState, ImmutableSet<String> refsToDelete, String prefix)
+ throws OrmException, IOException, ResourceConflictException, PermissionBackendException,
+ AuthException {
+ if (refsToDelete.isEmpty()) {
+ return;
}
- try (RevWalk rw = new RevWalk(r)) {
- batchUpdate.execute(rw, NullProgressMonitor.INSTANCE);
+
+ if (refsToDelete.size() == 1) {
+ deleteSingleRef(projectState, Iterables.getOnlyElement(refsToDelete), prefix);
+ return;
}
- StringBuilder errorMessages = new StringBuilder();
- for (ReceiveCommand command : batchUpdate.getCommands()) {
- if (command.getResult() == Result.OK) {
- postDeletion(resource, command);
- } else {
- appendAndLogErrorMessage(errorMessages, command);
+
+ try (Repository repository = repoManager.openRepository(projectState.getNameKey())) {
+ BatchRefUpdate batchUpdate = repository.getRefDatabase().newBatchUpdate();
+ batchUpdate.setAtomic(false);
+ ImmutableSet<String> refs =
+ prefix == null
+ ? refsToDelete
+ : refsToDelete
+ .stream()
+ .map(ref -> ref.startsWith(R_REFS) ? ref : prefix + ref)
+ .collect(toImmutableSet());
+ for (String ref : refs) {
+ batchUpdate.addCommand(createDeleteCommand(projectState, repository, ref));
}
- }
- if (errorMessages.length() > 0) {
- throw new ResourceConflictException(errorMessages.toString());
+ try (RevWalk rw = new RevWalk(repository)) {
+ batchUpdate.execute(rw, NullProgressMonitor.INSTANCE);
+ }
+ StringBuilder errorMessages = new StringBuilder();
+ for (ReceiveCommand command : batchUpdate.getCommands()) {
+ if (command.getResult() == Result.OK) {
+ postDeletion(projectState.getNameKey(), command);
+ } else {
+ appendAndLogErrorMessage(errorMessages, command);
+ }
+ }
+ if (errorMessages.length() > 0) {
+ throw new ResourceConflictException(errorMessages.toString());
+ }
}
}
- private ReceiveCommand createDeleteCommand(ProjectResource project, Repository r, String refName)
+ private ReceiveCommand createDeleteCommand(
+ ProjectState projectState, Repository r, String refName)
throws OrmException, IOException, ResourceConflictException, PermissionBackendException {
Ref ref = r.getRefDatabase().getRef(refName);
ReceiveCommand command;
@@ -227,7 +252,7 @@
try {
permissionBackend
.currentUser()
- .project(project.getNameKey())
+ .project(projectState.getNameKey())
.ref(refName)
.check(RefPermission.DELETE);
} catch (AuthException denied) {
@@ -237,12 +262,12 @@
}
}
- if (!project.getProjectState().statePermitsWrite()) {
+ if (!projectState.statePermitsWrite()) {
command.setResult(Result.REJECTED_OTHER_REASON, "project state does not permit write");
}
if (!refName.startsWith(R_TAGS)) {
- Branch.NameKey branchKey = new Branch.NameKey(project.getNameKey(), ref.getName());
+ Branch.NameKey branchKey = new Branch.NameKey(projectState.getNameKey(), ref.getName());
if (!queryProvider.get().setLimit(1).byBranchOpen(branchKey).isEmpty()) {
command.setResult(Result.REJECTED_OTHER_REASON, "it has open changes");
}
@@ -252,7 +277,7 @@
u.setForceUpdate(true);
u.setExpectedOldObjectId(r.exactRef(refName).getObjectId());
u.setNewObjectId(ObjectId.zeroId());
- refDeletionValidator.validateRefOperation(project.getName(), identifiedUser.get(), u);
+ refDeletionValidator.validateRefOperation(projectState.getName(), identifiedUser.get(), u);
return command;
}
@@ -281,7 +306,7 @@
errorMessages.append("\n");
}
- private void postDeletion(ProjectResource project, ReceiveCommand cmd) {
- referenceUpdated.fire(project.getNameKey(), cmd, identifiedUser.get().state());
+ private void postDeletion(Project.NameKey project, ReceiveCommand cmd) {
+ referenceUpdated.fire(project, cmd, identifiedUser.get().state());
}
}
diff --git a/java/com/google/gerrit/server/restapi/project/DeleteTag.java b/java/com/google/gerrit/server/restapi/project/DeleteTag.java
index bd5f444..f7cce11 100644
--- a/java/com/google/gerrit/server/restapi/project/DeleteTag.java
+++ b/java/com/google/gerrit/server/restapi/project/DeleteTag.java
@@ -21,9 +21,7 @@
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.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.permissions.RefPermission;
import com.google.gerrit.server.project.RefUtil;
import com.google.gerrit.server.project.TagResource;
import com.google.gwtorm.server.OrmException;
@@ -34,13 +32,11 @@
@Singleton
public class DeleteTag implements RestModifyView<TagResource, Input> {
- private final PermissionBackend permissionBackend;
- private final DeleteRef.Factory deleteRefFactory;
+ private final DeleteRef deleteRef;
@Inject
- DeleteTag(PermissionBackend permissionBackend, DeleteRef.Factory deleteRefFactory) {
- this.permissionBackend = permissionBackend;
- this.deleteRefFactory = deleteRefFactory;
+ DeleteTag(DeleteRef deleteRef) {
+ this.deleteRef = deleteRef;
}
@Override
@@ -53,13 +49,7 @@
throw new MethodNotAllowedException("not allowed to delete " + tag);
}
- permissionBackend
- .currentUser()
- .project(resource.getNameKey())
- .ref(tag)
- .check(RefPermission.DELETE);
- resource.getProjectState().checkStatePermitsWrite();
- deleteRefFactory.create(resource).ref(tag).delete();
+ deleteRef.deleteSingleRef(resource.getProjectState(), tag);
return Response.none();
}
}
diff --git a/java/com/google/gerrit/server/restapi/project/DeleteTags.java b/java/com/google/gerrit/server/restapi/project/DeleteTags.java
index 83fa1ea..bf2c524 100644
--- a/java/com/google/gerrit/server/restapi/project/DeleteTags.java
+++ b/java/com/google/gerrit/server/restapi/project/DeleteTags.java
@@ -16,6 +16,7 @@
import static org.eclipse.jgit.lib.Constants.R_TAGS;
+import com.google.common.collect.ImmutableSet;
import com.google.gerrit.extensions.api.projects.DeleteTagsInput;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.Response;
@@ -30,11 +31,11 @@
@Singleton
public class DeleteTags implements RestModifyView<ProjectResource, DeleteTagsInput> {
- private final DeleteRef.Factory deleteRefFactory;
+ private final DeleteRef deleteRef;
@Inject
- DeleteTags(DeleteRef.Factory deleteRefFactory) {
- this.deleteRefFactory = deleteRefFactory;
+ DeleteTags(DeleteRef deleteRef) {
+ this.deleteRef = deleteRef;
}
@Override
@@ -43,7 +44,8 @@
if (input == null || input.tags == null || input.tags.isEmpty()) {
throw new BadRequestException("tags must be specified");
}
- deleteRefFactory.create(project).refs(input.tags).prefix(R_TAGS).delete();
+ deleteRef.deleteMultipleRefs(
+ project.getProjectState(), ImmutableSet.copyOf(input.tags), R_TAGS);
return Response.none();
}
}
diff --git a/java/com/google/gerrit/server/restapi/project/GarbageCollect.java b/java/com/google/gerrit/server/restapi/project/GarbageCollect.java
index ea1620b..3b7f3e3 100644
--- a/java/com/google/gerrit/server/restapi/project/GarbageCollect.java
+++ b/java/com/google/gerrit/server/restapi/project/GarbageCollect.java
@@ -29,9 +29,9 @@
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.LocalDiskRepositoryManager;
import com.google.gerrit.server.git.WorkQueue;
+import com.google.gerrit.server.ioutil.HexFormat;
import com.google.gerrit.server.project.ProjectResource;
import com.google.gerrit.server.restapi.project.GarbageCollect.Input;
-import com.google.gerrit.server.util.IdGenerator;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
@@ -98,7 +98,7 @@
WorkQueue.Task<Void> task = (WorkQueue.Task<Void>) workQueue.getDefaultQueue().submit(job);
String location =
- canonicalUrl.get() + "a/config/server/tasks/" + IdGenerator.format(task.getTaskId());
+ canonicalUrl.get() + "a/config/server/tasks/" + HexFormat.fromInt(task.getTaskId());
return Response.accepted(location);
}
diff --git a/java/com/google/gerrit/server/restapi/project/ListDashboards.java b/java/com/google/gerrit/server/restapi/project/ListDashboards.java
index 06dbdb0..3808a2f 100644
--- a/java/com/google/gerrit/server/restapi/project/ListDashboards.java
+++ b/java/com/google/gerrit/server/restapi/project/ListDashboards.java
@@ -16,6 +16,7 @@
import static com.google.gerrit.reviewdb.client.RefNames.REFS_DASHBOARDS;
+import com.google.common.collect.ImmutableList;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.extensions.api.projects.DashboardInfo;
import com.google.gerrit.extensions.restapi.AuthException;
@@ -100,15 +101,15 @@
private List<DashboardInfo> scan(ProjectState state, String project, boolean setDefault)
throws ResourceNotFoundException, IOException, PermissionBackendException {
+ if (!state.statePermitsRead()) {
+ return ImmutableList.of();
+ }
+
PermissionBackend.ForProject perm = permissionBackend.currentUser().project(state.getNameKey());
try (Repository git = gitManager.openRepository(state.getNameKey());
RevWalk rw = new RevWalk(git)) {
List<DashboardInfo> all = new ArrayList<>();
for (Ref ref : git.getRefDatabase().getRefsByPrefix(REFS_DASHBOARDS)) {
- if (!state.statePermitsRead()) {
- continue;
- }
-
try {
perm.ref(ref.getName()).check(RefPermission.READ);
all.addAll(scanDashboards(state.getProject(), git, rw, ref, project, setDefault));
diff --git a/java/com/google/gerrit/server/restapi/project/Module.java b/java/com/google/gerrit/server/restapi/project/Module.java
index f2047e0..a57438e 100644
--- a/java/com/google/gerrit/server/restapi/project/Module.java
+++ b/java/com/google/gerrit/server/restapi/project/Module.java
@@ -104,7 +104,6 @@
put(PROJECT_KIND, "config").to(PutConfig.class);
post(COMMIT_KIND, "cherrypick").to(CherryPickCommit.class);
- factory(DeleteRef.Factory.class);
factory(ProjectNode.Factory.class);
}
}
diff --git a/java/com/google/gerrit/server/schema/Schema_169.java b/java/com/google/gerrit/server/schema/Schema_169.java
index 75dd459..2779d47 100644
--- a/java/com/google/gerrit/server/schema/Schema_169.java
+++ b/java/com/google/gerrit/server/schema/Schema_169.java
@@ -72,7 +72,7 @@
int skipped = 0;
for (Project.NameKey project : projects) {
try (Repository repo = repoManager.openRepository(project)) {
- ProjectMigrationResult progress = migrator.migrateProject(project, repo);
+ ProjectMigrationResult progress = migrator.migrateProject(project, repo, false);
skipped += progress.skipped;
} catch (IOException e) {
ok = false;
diff --git a/java/com/google/gerrit/server/util/IdGenerator.java b/java/com/google/gerrit/server/util/IdGenerator.java
index a9a22d9..276df06 100644
--- a/java/com/google/gerrit/server/util/IdGenerator.java
+++ b/java/com/google/gerrit/server/util/IdGenerator.java
@@ -22,16 +22,6 @@
/** Simple class to produce 4 billion keys randomly distributed. */
@Singleton
public class IdGenerator {
- /** Format an id created by this class as a hex string. */
- public static String format(int id) {
- final char[] r = new char[8];
- for (int p = 7; 0 <= p; p--) {
- final int h = id & 0xf;
- r[p] = h < 10 ? (char) ('0' + h) : (char) ('a' + (h - 10));
- id >>= 4;
- }
- return new String(r);
- }
private final AtomicInteger gen;
diff --git a/java/com/google/gerrit/sshd/SshLog.java b/java/com/google/gerrit/sshd/SshLog.java
index b6c2d19..98c649d 100644
--- a/java/com/google/gerrit/sshd/SshLog.java
+++ b/java/com/google/gerrit/sshd/SshLog.java
@@ -27,7 +27,7 @@
import com.google.gerrit.server.config.ConfigUpdatedEvent;
import com.google.gerrit.server.config.GerritConfigListener;
import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.util.IdGenerator;
+import com.google.gerrit.server.ioutil.HexFormat;
import com.google.gerrit.server.util.SystemLog;
import com.google.gerrit.sshd.SshScope.Context;
import com.google.inject.Inject;
@@ -277,7 +277,7 @@
}
private static String id(int id) {
- return IdGenerator.format(id);
+ return HexFormat.fromInt(id);
}
void audit(Context ctx, Object result, String cmd) {
@@ -298,7 +298,7 @@
created = TimeUtil.nowMs();
} else {
SshSession session = ctx.getSession();
- sessionId = IdGenerator.format(session.getSessionId());
+ sessionId = HexFormat.fromInt(session.getSessionId());
currentUser = session.getUser();
created = ctx.created;
}
diff --git a/java/com/google/gerrit/sshd/commands/ShowConnections.java b/java/com/google/gerrit/sshd/commands/ShowConnections.java
index b30799b..9b517c6 100644
--- a/java/com/google/gerrit/sshd/commands/ShowConnections.java
+++ b/java/com/google/gerrit/sshd/commands/ShowConnections.java
@@ -22,7 +22,7 @@
import com.google.gerrit.extensions.annotations.RequiresCapability;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.util.IdGenerator;
+import com.google.gerrit.server.ioutil.HexFormat;
import com.google.gerrit.sshd.CommandMetaData;
import com.google.gerrit.sshd.SshCommand;
import com.google.gerrit.sshd.SshDaemon;
@@ -168,7 +168,7 @@
}
private static String id(SshSession sd) {
- return sd != null ? IdGenerator.format(sd.getSessionId()) : "";
+ return sd != null ? HexFormat.fromInt(sd.getSessionId()) : "";
}
private static String time(long now, long time) {
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index 624e1b0..0a42b1e 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -449,6 +449,16 @@
}
@Test
+ public void createWipChangeWithWorkInProgressByDefaultForProject() throws Exception {
+ ConfigInput input = new ConfigInput();
+ input.workInProgressByDefault = InheritableBoolean.TRUE;
+ gApi.projects().name(project.get()).config(input);
+ String changeId =
+ gApi.changes().create(new ChangeInput(project.get(), "master", "Test Change")).get().id;
+ assertThat(gApi.changes().id(changeId).get().workInProgress).isTrue();
+ }
+
+ @Test
public void setReadyForReviewNotAllowedWithoutPermission() throws Exception {
PushOneCommit.Result rready = createChange();
String changeId = rready.getChangeId();
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
index 51946e9..53cc5ad 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
@@ -32,6 +32,7 @@
import com.google.gerrit.common.RawInputUtil;
import com.google.gerrit.extensions.api.changes.FileApi;
import com.google.gerrit.extensions.api.changes.RebaseInput;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
import com.google.gerrit.extensions.common.ChangeType;
import com.google.gerrit.extensions.common.DiffInfo;
import com.google.gerrit.extensions.common.FileInfo;
@@ -112,16 +113,115 @@
}
@Test
- public void diffDeletedFile() throws Exception {
+ public void deletedFileIsIncludedInDiff() throws Exception {
gApi.changes().id(changeId).edit().deleteFile(FILE_NAME);
gApi.changes().id(changeId).edit().publish();
Map<String, FileInfo> changedFiles = gApi.changes().id(changeId).current().files();
assertThat(changedFiles.keySet()).containsExactly(COMMIT_MSG, FILE_NAME);
+ }
- DiffInfo diff = getDiffRequest(changeId, CURRENT, FILE_NAME).get();
- assertThat(diff.metaA.lines).isEqualTo(100);
- assertThat(diff.metaB).isNull();
+ @Test
+ public void numberOfLinesInDiffOfDeletedFileWithoutNewlineAtEndIsCorrect() throws Exception {
+ String filePath = "a_new_file.txt";
+ String fileContent = "Line 1\nLine 2\nLine 3";
+ gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent));
+ gApi.changes().id(changeId).edit().publish();
+ String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+ gApi.changes().id(changeId).edit().deleteFile(filePath);
+ gApi.changes().id(changeId).edit().publish();
+
+ DiffInfo diffInfo =
+ getDiffRequest(changeId, CURRENT, filePath).withBase(previousPatchSetId).get();
+ assertThat(diffInfo).metaA().totalLineCount().isEqualTo(3);
+ assertThat(diffInfo).metaB().isNull();
+ }
+
+ @Test
+ public void numberOfLinesInFileInfoOfDeletedFileWithoutNewlineAtEndIsCorrect() throws Exception {
+ String filePath = "a_new_file.txt";
+ String fileContent = "Line 1\nLine 2\nLine 3";
+ gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent));
+ gApi.changes().id(changeId).edit().publish();
+ String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+ gApi.changes().id(changeId).edit().deleteFile(filePath);
+ gApi.changes().id(changeId).edit().publish();
+
+ Map<String, FileInfo> changedFiles =
+ gApi.changes().id(changeId).current().files(previousPatchSetId);
+ assertThat(changedFiles.get(filePath)).linesInserted().isNull();
+ assertThat(changedFiles.get(filePath)).linesDeleted().isEqualTo(3);
+ }
+
+ @Test
+ public void numberOfLinesInDiffOfDeletedFileWithNewlineAtEndIsCorrect() throws Exception {
+ String filePath = "a_new_file.txt";
+ String fileContent = "Line 1\nLine 2\nLine 3\n";
+ gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent));
+ gApi.changes().id(changeId).edit().publish();
+ String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+ gApi.changes().id(changeId).edit().deleteFile(filePath);
+ gApi.changes().id(changeId).edit().publish();
+
+ DiffInfo diffInfo =
+ getDiffRequest(changeId, CURRENT, filePath).withBase(previousPatchSetId).get();
+ assertThat(diffInfo).metaA().totalLineCount().isEqualTo(4);
+ assertThat(diffInfo).metaB().isNull();
+ }
+
+ @Test
+ public void numberOfLinesInFileInfoOfDeletedFileWithNewlineAtEndIsCorrect() throws Exception {
+ String filePath = "a_new_file.txt";
+ String fileContent = "Line 1\nLine 2\nLine 3\n";
+ gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent));
+ gApi.changes().id(changeId).edit().publish();
+ String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+ gApi.changes().id(changeId).edit().deleteFile(filePath);
+ gApi.changes().id(changeId).edit().publish();
+
+ Map<String, FileInfo> changedFiles =
+ gApi.changes().id(changeId).current().files(previousPatchSetId);
+ assertThat(changedFiles.get(filePath)).linesInserted().isNull();
+ // Inherited from Git: An empty last line is ignored in the count.
+ assertThat(changedFiles.get(filePath)).linesDeleted().isEqualTo(3);
+ }
+
+ @Test
+ public void deletedFileWithoutNewlineAtEndResultsInOneDiffEntry() throws Exception {
+ String filePath = "a_new_file.txt";
+ String fileContent = "Line 1\nLine 2\nLine 3";
+ gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent));
+ gApi.changes().id(changeId).edit().publish();
+ String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+ gApi.changes().id(changeId).edit().deleteFile(filePath);
+ gApi.changes().id(changeId).edit().publish();
+
+ DiffInfo diffInfo =
+ getDiffRequest(changeId, CURRENT, filePath).withBase(previousPatchSetId).get();
+ assertThat(diffInfo)
+ .content()
+ .onlyElement()
+ .linesOfA()
+ .containsExactly("Line 1", "Line 2", "Line 3");
+ }
+
+ @Test
+ public void deletedFileWithNewlineAtEndResultsInOneDiffEntry() throws Exception {
+ String filePath = "a_new_file.txt";
+ String fileContent = "Line 1\nLine 2\nLine 3\n";
+ gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent));
+ gApi.changes().id(changeId).edit().publish();
+ String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+ gApi.changes().id(changeId).edit().deleteFile(filePath);
+ gApi.changes().id(changeId).edit().publish();
+
+ DiffInfo diffInfo =
+ getDiffRequest(changeId, CURRENT, filePath).withBase(previousPatchSetId).get();
+ assertThat(diffInfo)
+ .content()
+ .onlyElement()
+ .linesOfA()
+ .containsExactly("Line 1", "Line 2", "Line 3", "");
}
@Test
@@ -136,6 +236,91 @@
}
@Test
+ public void numberOfLinesInDiffOfAddedFileWithoutNewlineAtEndIsCorrect() throws Exception {
+ String filePath = "a_new_file.txt";
+ String fileContent = "Line 1\nLine2\nLine 3";
+ gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent));
+ gApi.changes().id(changeId).edit().publish();
+
+ DiffInfo diffInfo =
+ getDiffRequest(changeId, CURRENT, filePath).withBase(initialPatchSetId).get();
+ assertThat(diffInfo).metaA().isNull();
+ assertThat(diffInfo).metaB().totalLineCount().isEqualTo(3);
+ }
+
+ @Test
+ public void numberOfLinesInFileInfoOfAddedFileWithoutNewlineAtEndIsCorrect() throws Exception {
+ String filePath = "a_new_file.txt";
+ String fileContent = "Line 1\nLine2\nLine 3";
+ gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent));
+ gApi.changes().id(changeId).edit().publish();
+
+ Map<String, FileInfo> changedFiles =
+ gApi.changes().id(changeId).current().files(initialPatchSetId);
+ assertThat(changedFiles.get(filePath)).linesInserted().isEqualTo(3);
+ assertThat(changedFiles.get(filePath)).linesDeleted().isNull();
+ }
+
+ @Test
+ public void numberOfLinesInDiffOfAddedFileWithNewlineAtEndIsCorrect() throws Exception {
+ String filePath = "a_new_file.txt";
+ String fileContent = "Line 1\nLine2\nLine 3\n";
+ gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent));
+ gApi.changes().id(changeId).edit().publish();
+
+ DiffInfo diffInfo =
+ getDiffRequest(changeId, CURRENT, filePath).withBase(initialPatchSetId).get();
+ assertThat(diffInfo).metaA().isNull();
+ assertThat(diffInfo).metaB().totalLineCount().isEqualTo(4);
+ }
+
+ @Test
+ public void numberOfLinesInFileInfoOfAddedFileWithNewlineAtEndIsCorrect() throws Exception {
+ String filePath = "a_new_file.txt";
+ String fileContent = "Line 1\nLine2\nLine 3\n";
+ gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent));
+ gApi.changes().id(changeId).edit().publish();
+
+ Map<String, FileInfo> changedFiles =
+ gApi.changes().id(changeId).current().files(initialPatchSetId);
+ // Inherited from Git: An empty last line is ignored in the count.
+ assertThat(changedFiles.get(filePath)).linesInserted().isEqualTo(3);
+ assertThat(changedFiles.get(filePath)).linesDeleted().isNull();
+ }
+
+ @Test
+ public void addedFileWithoutNewlineAtEndResultsInOneDiffEntry() throws Exception {
+ String filePath = "a_new_file.txt";
+ String fileContent = "Line 1\nLine 2\nLine 3";
+ gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent));
+ gApi.changes().id(changeId).edit().publish();
+
+ DiffInfo diffInfo =
+ getDiffRequest(changeId, CURRENT, filePath).withBase(initialPatchSetId).get();
+ assertThat(diffInfo)
+ .content()
+ .onlyElement()
+ .linesOfB()
+ .containsExactly("Line 1", "Line 2", "Line 3");
+ }
+
+ @Test
+ public void addedFileWithNewlineAtEndResultsInOneDiffEntry() throws Exception {
+ String filePath = "a_new_file.txt";
+ String fileContent = "Line 1\nLine 2\nLine 3\n";
+ gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent));
+ gApi.changes().id(changeId).edit().publish();
+
+ DiffInfo diffInfo =
+ getDiffRequest(changeId, CURRENT, filePath).withBase(initialPatchSetId).get();
+ assertThat(diffInfo)
+ .content()
+ .onlyElement()
+ .linesOfB()
+ .containsExactly("Line 1", "Line 2", "Line 3", "");
+ }
+
+ @Test
public void renamedFileIsIncludedInDiff() throws Exception {
String newFilePath = "a_new_file.txt";
gApi.changes().id(changeId).edit().renameFile(FILE_NAME, newFilePath);
@@ -193,11 +378,11 @@
// automerge
diff = getDiffRequest(r.getChangeId(), r.getCommit().name(), "foo").get();
- assertThat(diff.metaA.lines).isEqualTo(5);
+ assertThat(diff.metaA.lines).isEqualTo(6);
assertThat(diff.metaB.lines).isEqualTo(1);
diff = getDiffRequest(r.getChangeId(), r.getCommit().name(), "bar").get();
- assertThat(diff.metaA.lines).isEqualTo(5);
+ assertThat(diff.metaA.lines).isEqualTo(6);
assertThat(diff.metaB.lines).isEqualTo(1);
// parent 1
@@ -212,6 +397,596 @@
}
@Test
+ public void diffOfUnmodifiedFileMarksAllLinesAsCommon() throws Exception {
+ String filePath = "a_new_file.txt";
+ String fileContent = "Line 1\nLine 2\nLine 3\n";
+ gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent));
+ gApi.changes().id(changeId).edit().publish();
+ String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+ gApi.changes().id(changeId).edit().modifyCommitMessage("An unchanged patchset");
+ gApi.changes().id(changeId).edit().publish();
+
+ DiffInfo diffInfo =
+ getDiffRequest(changeId, CURRENT, filePath).withBase(previousPatchSetId).get();
+ assertThat(diffInfo)
+ .content()
+ .onlyElement()
+ .commonLines()
+ .containsAllOf("Line 1", "Line 2", "Line 3")
+ .inOrder();
+ assertThat(diffInfo).content().onlyElement().linesOfA().isNull();
+ assertThat(diffInfo).content().onlyElement().linesOfB().isNull();
+ }
+
+ @Test
+ public void diffOfUnmodifiedFileWithNewlineAtEndHasEmptyLineAtEnd() throws Exception {
+ String filePath = "a_new_file.txt";
+ String fileContent = "Line 1\nLine 2\nLine 3\n";
+ gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent));
+ gApi.changes().id(changeId).edit().publish();
+ String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+ gApi.changes().id(changeId).edit().modifyCommitMessage("An unchanged patchset");
+ gApi.changes().id(changeId).edit().publish();
+
+ DiffInfo diffInfo =
+ getDiffRequest(changeId, CURRENT, filePath).withBase(previousPatchSetId).get();
+ assertThat(diffInfo).content().onlyElement().commonLines().lastElement().isEqualTo("");
+
+ assertThat(diffInfo).metaA().totalLineCount().isEqualTo(4);
+ assertThat(diffInfo).metaB().totalLineCount().isEqualTo(4);
+ }
+
+ @Test
+ public void diffOfUnmodifiedFileWithoutNewlineAtEndEndsWithLastLineContent() throws Exception {
+ String filePath = "a_new_file.txt";
+ String fileContent = "Line 1\nLine 2\nLine 3";
+ gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent));
+ gApi.changes().id(changeId).edit().publish();
+ String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+ gApi.changes().id(changeId).edit().modifyCommitMessage("An unchanged patchset");
+ gApi.changes().id(changeId).edit().publish();
+
+ DiffInfo diffInfo =
+ getDiffRequest(changeId, CURRENT, filePath).withBase(previousPatchSetId).get();
+ assertThat(diffInfo).content().onlyElement().commonLines().lastElement().isEqualTo("Line 3");
+
+ assertThat(diffInfo).metaA().totalLineCount().isEqualTo(3);
+ assertThat(diffInfo).metaB().totalLineCount().isEqualTo(3);
+ }
+
+ @Test
+ public void diffOfModifiedFileWithNewlineAtEndHasEmptyLineAtEnd() throws Exception {
+ String filePath = "a_new_file.txt";
+ String fileContent = "Line 1\nLine 2\nLine 3\n";
+ gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent));
+ gApi.changes().id(changeId).edit().publish();
+ String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+ addModifiedPatchSet(changeId, filePath, content -> content.replace("Line 1\n", "Line one\n"));
+
+ DiffInfo diffInfo =
+ getDiffRequest(changeId, CURRENT, filePath).withBase(previousPatchSetId).get();
+ assertThat(diffInfo).content().lastElement().commonLines().lastElement().isEqualTo("");
+
+ assertThat(diffInfo).metaA().totalLineCount().isEqualTo(4);
+ assertThat(diffInfo).metaB().totalLineCount().isEqualTo(4);
+ }
+
+ @Test
+ public void diffOfModifiedFileWithoutNewlineAtEndEndsWithLastLineContent() throws Exception {
+ String filePath = "a_new_file.txt";
+ String fileContent = "Line 1\nLine 2\nLine 3";
+ gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent));
+ gApi.changes().id(changeId).edit().publish();
+ String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+ addModifiedPatchSet(changeId, filePath, content -> content.replace("Line 1\n", "Line one\n"));
+
+ DiffInfo diffInfo =
+ getDiffRequest(changeId, CURRENT, filePath).withBase(previousPatchSetId).get();
+ assertThat(diffInfo).content().lastElement().commonLines().lastElement().isEqualTo("Line 3");
+
+ assertThat(diffInfo).metaA().totalLineCount().isEqualTo(3);
+ assertThat(diffInfo).metaB().totalLineCount().isEqualTo(3);
+ }
+
+ @Test
+ public void diffOfModifiedLastLineWithNewlineAtEndHasEmptyLineAtEnd() throws Exception {
+ String filePath = "a_new_file.txt";
+ String fileContent = "Line 1\nLine 2\nLine 3\n";
+ gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent));
+ gApi.changes().id(changeId).edit().publish();
+ String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+ addModifiedPatchSet(changeId, filePath, content -> content.replace("Line 3\n", "Line three\n"));
+
+ DiffInfo diffInfo =
+ getDiffRequest(changeId, CURRENT, filePath).withBase(previousPatchSetId).get();
+ assertThat(diffInfo).content().lastElement().commonLines().lastElement().isEqualTo("");
+
+ assertThat(diffInfo).metaA().totalLineCount().isEqualTo(4);
+ assertThat(diffInfo).metaB().totalLineCount().isEqualTo(4);
+ }
+
+ @Test
+ public void diffOfModifiedLastLineWithoutNewlineAtEndEndsWithLastLineContent() throws Exception {
+ String filePath = "a_new_file.txt";
+ String fileContent = "Line 1\nLine 2\nLine 3";
+ gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent));
+ gApi.changes().id(changeId).edit().publish();
+ String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+ addModifiedPatchSet(changeId, filePath, content -> content.replace("Line 3", "Line three"));
+
+ DiffInfo diffInfo =
+ getDiffRequest(changeId, CURRENT, filePath).withBase(previousPatchSetId).get();
+ assertThat(diffInfo).content().lastElement().linesOfA().containsExactly("Line 3");
+ assertThat(diffInfo).content().lastElement().linesOfB().containsExactly("Line three");
+
+ assertThat(diffInfo).metaA().totalLineCount().isEqualTo(3);
+ assertThat(diffInfo).metaB().totalLineCount().isEqualTo(3);
+ }
+
+ @Test
+ public void addedNewlineAtEndOfFileIsMarkedInDiffWhenWhitespaceIsConsidered() throws Exception {
+ addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("Line 101"));
+ String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+ addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("\n"));
+
+ DiffInfo diffInfo =
+ getDiffRequest(changeId, CURRENT, FILE_NAME)
+ .withWhitespace(DiffPreferencesInfo.Whitespace.IGNORE_NONE)
+ .withBase(previousPatchSetId)
+ .get();
+ assertThat(diffInfo).content().element(0).commonLines().isNotEmpty();
+ assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line 101");
+ assertThat(diffInfo).content().element(1).linesOfB().containsExactly("Line 101", "");
+
+ assertThat(diffInfo).metaA().totalLineCount().isEqualTo(101);
+ assertThat(diffInfo).metaB().totalLineCount().isEqualTo(102);
+ }
+
+ @Test
+ public void addedNewlineAtEndOfFileIsMarkedInDiffWhenWhitespaceIsIgnored() throws Exception {
+ addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("Line 101"));
+ String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+ addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("\n"));
+
+ DiffInfo diffInfo =
+ getDiffRequest(changeId, CURRENT, FILE_NAME)
+ .withWhitespace(DiffPreferencesInfo.Whitespace.IGNORE_ALL)
+ .withBase(previousPatchSetId)
+ .get();
+ assertThat(diffInfo).content().element(0).commonLines().isNotEmpty();
+ assertThat(diffInfo).content().element(1).linesOfA().isNull();
+ assertThat(diffInfo).content().element(1).linesOfB().containsExactly("");
+
+ assertThat(diffInfo).metaA().totalLineCount().isEqualTo(101);
+ assertThat(diffInfo).metaB().totalLineCount().isEqualTo(102);
+ }
+
+ @Test
+ public void addedNewlineAtEndOfFileMeansOneModifiedLine() throws Exception {
+ addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("Line 101"));
+ String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+ addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("\n"));
+
+ Map<String, FileInfo> changedFiles =
+ gApi.changes().id(changeId).current().files(previousPatchSetId);
+ assertThat(changedFiles.get(FILE_NAME)).linesInserted().isEqualTo(1);
+ assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(1);
+ }
+
+ @Test
+ public void addedLastLineWithoutNewlineBeforeAndAfterwardsIsMarkedInDiff() throws Exception {
+ addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("Line 101"));
+ String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+ addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("\nLine 102"));
+
+ DiffInfo diffInfo =
+ getDiffRequest(changeId, CURRENT, FILE_NAME)
+ .withWhitespace(DiffPreferencesInfo.Whitespace.IGNORE_NONE)
+ .withBase(previousPatchSetId)
+ .get();
+ assertThat(diffInfo).content().element(0).commonLines().isNotEmpty();
+ assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line 101");
+ assertThat(diffInfo).content().element(1).linesOfB().containsExactly("Line 101", "Line 102");
+
+ assertThat(diffInfo).metaA().totalLineCount().isEqualTo(101);
+ assertThat(diffInfo).metaB().totalLineCount().isEqualTo(102);
+ }
+
+ @Test
+ public void addedLastLineWithoutNewlineBeforeAndAfterwardsMeansTwoModifiedLines()
+ throws Exception {
+ addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("Line 101"));
+ String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+ addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("\nLine 102"));
+
+ Map<String, FileInfo> changedFiles =
+ gApi.changes().id(changeId).current().files(previousPatchSetId);
+ assertThat(changedFiles.get(FILE_NAME)).linesInserted().isEqualTo(2);
+ assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(1);
+ }
+
+ @Test
+ public void addedLastLineWithoutNewlineBeforeButWithOneAfterwardsIsMarkedInDiff()
+ throws Exception {
+ addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("Line 101"));
+ String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+ addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("\nLine 102\n"));
+
+ DiffInfo diffInfo =
+ getDiffRequest(changeId, CURRENT, FILE_NAME)
+ .withWhitespace(DiffPreferencesInfo.Whitespace.IGNORE_NONE)
+ .withBase(previousPatchSetId)
+ .get();
+ assertThat(diffInfo).content().element(0).commonLines().isNotEmpty();
+ assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line 101");
+ assertThat(diffInfo)
+ .content()
+ .element(1)
+ .linesOfB()
+ .containsExactly("Line 101", "Line 102", "");
+
+ assertThat(diffInfo).metaA().totalLineCount().isEqualTo(101);
+ assertThat(diffInfo).metaB().totalLineCount().isEqualTo(103);
+ }
+
+ @Test
+ public void addedLastLineWithoutNewlineBeforeButWithOneAfterwardsMeansTwoModifiedLines()
+ throws Exception {
+ addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("Line 101"));
+ String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+ addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("\nLine 102\n"));
+
+ Map<String, FileInfo> changedFiles =
+ gApi.changes().id(changeId).current().files(previousPatchSetId);
+ // Inherited from Git: An empty last line is ignored in the count.
+ assertThat(changedFiles.get(FILE_NAME)).linesInserted().isEqualTo(2);
+ assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(1);
+ }
+
+ @Test
+ public void addedLastLineWithNewlineBeforeAndAfterwardsIsMarkedInDiff() throws Exception {
+ addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("Line 101\n"));
+
+ DiffInfo diffInfo =
+ getDiffRequest(changeId, CURRENT, FILE_NAME).withBase(initialPatchSetId).get();
+ assertThat(diffInfo).content().element(0).commonLines().isNotEmpty();
+ assertThat(diffInfo).content().element(1).linesOfA().isNull();
+ assertThat(diffInfo).content().element(1).linesOfB().containsExactly("Line 101");
+ assertThat(diffInfo).content().element(2).commonLines().containsExactly("");
+
+ assertThat(diffInfo).metaA().totalLineCount().isEqualTo(101);
+ assertThat(diffInfo).metaB().totalLineCount().isEqualTo(102);
+ }
+
+ @Test
+ public void addedLastLineWithNewlineBeforeAndAfterwardsMeansOneInsertedLine() throws Exception {
+ addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("Line 101\n"));
+
+ Map<String, FileInfo> changedFiles =
+ gApi.changes().id(changeId).current().files(initialPatchSetId);
+ assertThat(changedFiles.get(FILE_NAME)).linesInserted().isEqualTo(1);
+ assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isNull();
+ }
+
+ @Test
+ public void addedLastLineWithNewlineBeforeButWithoutOneAfterwardsIsMarkedInDiff()
+ throws Exception {
+ addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("Line 101"));
+
+ DiffInfo diffInfo =
+ getDiffRequest(changeId, CURRENT, FILE_NAME).withBase(initialPatchSetId).get();
+ assertThat(diffInfo).content().element(0).commonLines().isNotEmpty();
+ assertThat(diffInfo).content().element(1).linesOfA().containsExactly("");
+ assertThat(diffInfo).content().element(1).linesOfB().containsExactly("Line 101");
+
+ assertThat(diffInfo).metaA().totalLineCount().isEqualTo(101);
+ assertThat(diffInfo).metaB().totalLineCount().isEqualTo(101);
+ }
+
+ @Test
+ public void addedLastLineWithNewlineBeforeButWithoutOneAfterwardsMeansOneInsertedLine()
+ throws Exception {
+ addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("Line 101"));
+
+ Map<String, FileInfo> changedFiles =
+ gApi.changes().id(changeId).current().files(initialPatchSetId);
+ assertThat(changedFiles.get(FILE_NAME)).linesInserted().isEqualTo(1);
+ // Inherited from Git: An empty last line is ignored in the count.
+ assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isNull();
+ }
+
+ @Test
+ public void hunkForModifiedLastLineIsCombinedWithHunkForAddedNewlineAtEnd() throws Exception {
+ addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("Line 101"));
+ String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+ addModifiedPatchSet(
+ changeId, FILE_NAME, fileContent -> fileContent.replace("Line 101", "Line one oh one\n"));
+
+ DiffInfo diffInfo =
+ getDiffRequest(changeId, CURRENT, FILE_NAME).withBase(previousPatchSetId).get();
+ assertThat(diffInfo).content().element(0).commonLines().isNotEmpty();
+ assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line 101");
+ assertThat(diffInfo).content().element(1).linesOfB().containsExactly("Line one oh one", "");
+ }
+
+ @Test
+ public void intralineEditsForModifiedLastLineArePreservedWhenNewlineIsAlsoAddedAtEnd()
+ throws Exception {
+ assume().that(intraline).isTrue();
+
+ addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("Line 101"));
+ String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+ addModifiedPatchSet(
+ changeId, FILE_NAME, fileContent -> fileContent.replace("Line 101", "Line one oh one\n"));
+
+ DiffInfo diffInfo =
+ getDiffRequest(changeId, CURRENT, FILE_NAME).withBase(previousPatchSetId).get();
+ assertThat(diffInfo).content().element(0).commonLines().isNotEmpty();
+ assertThat(diffInfo)
+ .content()
+ .element(1)
+ .intralineEditsOfA()
+ .containsExactly(ImmutableList.of(5, 3));
+ assertThat(diffInfo)
+ .content()
+ .element(1)
+ .intralineEditsOfB()
+ .containsExactly(ImmutableList.of(5, 11));
+ }
+
+ @Test
+ public void hunkForModifiedSecondToLastLineIsNotCombinedWithHunkForAddedNewlineAtEnd()
+ throws Exception {
+ addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.concat("Line 101"));
+ String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+ addModifiedPatchSet(
+ changeId,
+ FILE_NAME,
+ fileContent -> fileContent.replace("Line 100\n", "Line one hundred\n").concat("\n"));
+
+ DiffInfo diffInfo =
+ getDiffRequest(changeId, CURRENT, FILE_NAME).withBase(previousPatchSetId).get();
+ assertThat(diffInfo).content().element(0).commonLines().isNotEmpty();
+ assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line 100");
+ assertThat(diffInfo).content().element(1).linesOfB().containsExactly("Line one hundred");
+ assertThat(diffInfo).content().element(2).commonLines().isNotEmpty();
+ assertThat(diffInfo).content().element(3).linesOfA().isNull();
+ assertThat(diffInfo).content().element(3).linesOfB().containsExactly("");
+ }
+
+ @Test
+ public void deletedNewlineAtEndOfFileIsMarkedInDiffWhenWhitespaceIsConsidered() throws Exception {
+ addModifiedPatchSet(
+ changeId, FILE_NAME, fileContent -> fileContent.replace("Line 100\n", "Line 100"));
+
+ DiffInfo diffInfo =
+ getDiffRequest(changeId, CURRENT, FILE_NAME)
+ .withBase(initialPatchSetId)
+ .withWhitespace(DiffPreferencesInfo.Whitespace.IGNORE_NONE)
+ .get();
+ assertThat(diffInfo).content().element(0).commonLines().isNotEmpty();
+ assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line 100", "");
+ assertThat(diffInfo).content().element(1).linesOfB().containsExactly("Line 100");
+
+ assertThat(diffInfo).metaA().totalLineCount().isEqualTo(101);
+ assertThat(diffInfo).metaB().totalLineCount().isEqualTo(100);
+ }
+
+ @Test
+ public void deletedNewlineAtEndOfFileIsMarkedInDiffWhenWhitespaceIsIgnored() throws Exception {
+ addModifiedPatchSet(
+ changeId, FILE_NAME, fileContent -> fileContent.replace("Line 100\n", "Line 100"));
+
+ DiffInfo diffInfo =
+ getDiffRequest(changeId, CURRENT, FILE_NAME)
+ .withBase(initialPatchSetId)
+ .withWhitespace(DiffPreferencesInfo.Whitespace.IGNORE_ALL)
+ .get();
+ assertThat(diffInfo).content().element(0).commonLines().isNotEmpty();
+ assertThat(diffInfo).content().element(1).linesOfA().containsExactly("");
+ assertThat(diffInfo).content().element(1).linesOfB().isNull();
+
+ assertThat(diffInfo).metaA().totalLineCount().isEqualTo(101);
+ assertThat(diffInfo).metaB().totalLineCount().isEqualTo(100);
+ }
+
+ @Test
+ public void deletedNewlineAtEndOfFileMeansOneModifiedLine() throws Exception {
+ addModifiedPatchSet(
+ changeId, FILE_NAME, fileContent -> fileContent.replace("Line 100\n", "Line 100"));
+
+ Map<String, FileInfo> changedFiles =
+ gApi.changes().id(changeId).current().files(initialPatchSetId);
+ assertThat(changedFiles.get(FILE_NAME)).linesInserted().isEqualTo(1);
+ assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(1);
+ }
+
+ @Test
+ public void deletedLastLineWithoutNewlineBeforeAndAfterwardsIsMarkedInDiff() throws Exception {
+ addModifiedPatchSet(
+ changeId, FILE_NAME, fileContent -> fileContent.replace("Line 100\n", "Line 100"));
+ String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+ addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.replace("\nLine 100", ""));
+
+ DiffInfo diffInfo =
+ getDiffRequest(changeId, CURRENT, FILE_NAME)
+ .withBase(previousPatchSetId)
+ .withWhitespace(DiffPreferencesInfo.Whitespace.IGNORE_NONE)
+ .get();
+ assertThat(diffInfo).content().element(0).commonLines().isNotEmpty();
+ assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line 99", "Line 100");
+ assertThat(diffInfo).content().element(1).linesOfB().containsExactly("Line 99");
+
+ assertThat(diffInfo).metaA().totalLineCount().isEqualTo(100);
+ assertThat(diffInfo).metaB().totalLineCount().isEqualTo(99);
+ }
+
+ @Test
+ public void deletedLastLineWithoutNewlineBeforeAndAfterwardsMeansTwoModifiedLines()
+ throws Exception {
+ addModifiedPatchSet(
+ changeId, FILE_NAME, fileContent -> fileContent.replace("Line 100\n", "Line 100"));
+ String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+ addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.replace("\nLine 100", ""));
+
+ Map<String, FileInfo> changedFiles =
+ gApi.changes().id(changeId).current().files(previousPatchSetId);
+ assertThat(changedFiles.get(FILE_NAME)).linesInserted().isEqualTo(1);
+ assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(2);
+ }
+
+ @Test
+ public void deletedLastLineWithoutNewlineBeforeButWithOneAfterwardsIsMarkedInDiff()
+ throws Exception {
+ addModifiedPatchSet(
+ changeId, FILE_NAME, fileContent -> fileContent.replace("Line 100\n", "Line 100"));
+ String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+ addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.replace("Line 100", ""));
+
+ DiffInfo diffInfo =
+ getDiffRequest(changeId, CURRENT, FILE_NAME)
+ .withBase(previousPatchSetId)
+ .withWhitespace(DiffPreferencesInfo.Whitespace.IGNORE_NONE)
+ .get();
+ assertThat(diffInfo).content().element(0).commonLines().isNotEmpty();
+ assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line 100");
+ assertThat(diffInfo).content().element(1).linesOfB().containsExactly("");
+
+ assertThat(diffInfo).metaA().totalLineCount().isEqualTo(100);
+ assertThat(diffInfo).metaB().totalLineCount().isEqualTo(100);
+ }
+
+ @Test
+ public void deletedLastLineWithoutNewlineBeforeButWithOneAfterwardsMeansOneDeletedLine()
+ throws Exception {
+ addModifiedPatchSet(
+ changeId, FILE_NAME, fileContent -> fileContent.replace("Line 100\n", "Line 100"));
+ String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+ addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.replace("Line 100", ""));
+
+ Map<String, FileInfo> changedFiles =
+ gApi.changes().id(changeId).current().files(previousPatchSetId);
+ // Inherited from Git: An empty last line is ignored in the count.
+ assertThat(changedFiles.get(FILE_NAME)).linesInserted().isNull();
+ assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(1);
+ }
+
+ @Test
+ public void deletedLastLineWithNewlineBeforeAndAfterwardsIsMarkedInDiff() throws Exception {
+ addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.replace("Line 100\n", ""));
+
+ DiffInfo diffInfo =
+ getDiffRequest(changeId, CURRENT, FILE_NAME)
+ .withBase(initialPatchSetId)
+ .withWhitespace(DiffPreferencesInfo.Whitespace.IGNORE_NONE)
+ .get();
+ assertThat(diffInfo).content().element(0).commonLines().isNotEmpty();
+ assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line 100");
+ assertThat(diffInfo).content().element(1).linesOfB().isNull();
+ assertThat(diffInfo).content().element(2).commonLines().containsExactly("");
+
+ assertThat(diffInfo).metaA().totalLineCount().isEqualTo(101);
+ assertThat(diffInfo).metaB().totalLineCount().isEqualTo(100);
+ }
+
+ @Test
+ public void deletedLastLineWithNewlineBeforeAndAfterwardsMeansOneDeletedLine() throws Exception {
+ addModifiedPatchSet(changeId, FILE_NAME, fileContent -> fileContent.replace("Line 100\n", ""));
+
+ Map<String, FileInfo> changedFiles =
+ gApi.changes().id(changeId).current().files(initialPatchSetId);
+ assertThat(changedFiles.get(FILE_NAME)).linesInserted().isNull();
+ assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(1);
+ }
+
+ @Test
+ public void deletedLastLineWithNewlineBeforeButWithoutOneAfterwardsIsMarkedInDiff()
+ throws Exception {
+ addModifiedPatchSet(
+ changeId, FILE_NAME, fileContent -> fileContent.replace("\nLine 100\n", ""));
+
+ DiffInfo diffInfo =
+ getDiffRequest(changeId, CURRENT, FILE_NAME)
+ .withBase(initialPatchSetId)
+ .withWhitespace(DiffPreferencesInfo.Whitespace.IGNORE_NONE)
+ .get();
+ assertThat(diffInfo).content().element(0).commonLines().isNotEmpty();
+ assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line 99", "Line 100", "");
+ assertThat(diffInfo).content().element(1).linesOfB().containsExactly("Line 99");
+
+ assertThat(diffInfo).metaA().totalLineCount().isEqualTo(101);
+ assertThat(diffInfo).metaB().totalLineCount().isEqualTo(99);
+ }
+
+ @Test
+ public void deletedLastLineWithNewlineBeforeButWithoutOneAfterwardsMeansTwoModifiedLines()
+ throws Exception {
+ addModifiedPatchSet(
+ changeId, FILE_NAME, fileContent -> fileContent.replace("\nLine 100\n", ""));
+
+ Map<String, FileInfo> changedFiles =
+ gApi.changes().id(changeId).current().files(initialPatchSetId);
+ assertThat(changedFiles.get(FILE_NAME)).linesInserted().isEqualTo(1);
+ assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(2);
+ }
+
+ @Test
+ public void hunkForModifiedLastLineIsCombinedWithHunkForDeletedNewlineAtEnd() throws Exception {
+ addModifiedPatchSet(
+ changeId, FILE_NAME, fileContent -> fileContent.replace("Line 100\n", "Line one hundred"));
+
+ DiffInfo diffInfo =
+ getDiffRequest(changeId, CURRENT, FILE_NAME).withBase(initialPatchSetId).get();
+ assertThat(diffInfo).content().element(0).commonLines().isNotEmpty();
+ assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line 100", "");
+ assertThat(diffInfo).content().element(1).linesOfB().containsExactly("Line one hundred");
+ }
+
+ @Test
+ public void intralineEditsForModifiedLastLineArePreservedWhenNewlineIsAlsoDeletedAtEnd()
+ throws Exception {
+ assume().that(intraline).isTrue();
+
+ addModifiedPatchSet(
+ changeId, FILE_NAME, fileContent -> fileContent.replace("Line 100\n", "Line one hundred"));
+
+ DiffInfo diffInfo =
+ getDiffRequest(changeId, CURRENT, FILE_NAME).withBase(initialPatchSetId).get();
+ assertThat(diffInfo).content().element(0).commonLines().isNotEmpty();
+ assertThat(diffInfo)
+ .content()
+ .element(1)
+ .intralineEditsOfA()
+ .containsExactly(ImmutableList.of(5, 4));
+ assertThat(diffInfo)
+ .content()
+ .element(1)
+ .intralineEditsOfB()
+ .containsExactly(ImmutableList.of(5, 11));
+ }
+
+ @Test
+ public void hunkForModifiedSecondToLastLineIsNotCombinedWithHunkForDeletedNewlineAtEnd()
+ throws Exception {
+ addModifiedPatchSet(
+ changeId,
+ FILE_NAME,
+ fileContent ->
+ fileContent
+ .replace("Line 99\n", "Line ninety-nine\n")
+ .replace("Line 100\n", "Line 100"));
+
+ DiffInfo diffInfo =
+ getDiffRequest(changeId, CURRENT, FILE_NAME).withBase(initialPatchSetId).get();
+ assertThat(diffInfo).content().element(0).commonLines().isNotEmpty();
+ assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line 99");
+ assertThat(diffInfo).content().element(1).linesOfB().containsExactly("Line ninety-nine");
+ assertThat(diffInfo).content().element(2).commonLines().isNotEmpty();
+ assertThat(diffInfo).content().element(3).linesOfA().containsExactly("");
+ assertThat(diffInfo).content().element(3).linesOfB().isNull();
+ }
+
+ @Test
public void addedUnrelatedFileIsIgnored_ForPatchSetDiffWithRebase() throws Exception {
ObjectId commit2 = addCommit(commit1, "file_added_in_another_commit.txt", "Some file content");
@@ -382,7 +1157,7 @@
.content()
.element(1)
.commonLines()
- .containsExactly("Line 2", "Line 3", "Line 4", "Line 5")
+ .containsExactly("Line 2", "Line 3", "Line 4", "Line 5", "")
.inOrder();
}
@@ -430,7 +1205,7 @@
.content()
.element(2)
.commonLines()
- .containsExactly("Line 4", "Line 5", "Line 6")
+ .containsExactly("Line 4", "Line 5", "Line 6", "")
.inOrder();
}
@@ -1027,7 +1802,7 @@
DiffInfo diffInfo =
getDiffRequest(changeId, CURRENT, FILE_NAME).withBase(previousPatchSetId).get();
assertThat(diffInfo).changeType().isEqualTo(ChangeType.DELETED);
- assertThat(diffInfo).content().element(0).linesOfA().hasSize(100);
+ assertThat(diffInfo).content().element(0).linesOfA().hasSize(101);
assertThat(diffInfo).content().element(0).linesOfB().isNull();
assertThat(diffInfo).content().element(0).isNotDueToRebase();
@@ -1050,7 +1825,7 @@
getDiffRequest(changeId, CURRENT, newFilePath).withBase(initialPatchSetId).get();
assertThat(diffInfo).changeType().isEqualTo(ChangeType.ADDED);
assertThat(diffInfo).content().element(0).linesOfA().isNull();
- assertThat(diffInfo).content().element(0).linesOfB().hasSize(3);
+ assertThat(diffInfo).content().element(0).linesOfB().hasSize(4);
assertThat(diffInfo).content().element(0).isNotDueToRebase();
Map<String, FileInfo> changedFiles =
diff --git a/javatests/com/google/gerrit/acceptance/pgm/BUILD b/javatests/com/google/gerrit/acceptance/pgm/BUILD
index 583ecc8..ea4c87d 100644
--- a/javatests/com/google/gerrit/acceptance/pgm/BUILD
+++ b/javatests/com/google/gerrit/acceptance/pgm/BUILD
@@ -22,6 +22,7 @@
group = "elastic",
labels = [
"elastic",
+ "exclusive",
"flaky",
"pgm",
"no_windows",
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/WorkInProgressByDefaultIT.java b/javatests/com/google/gerrit/acceptance/rest/change/WorkInProgressByDefaultIT.java
new file mode 100644
index 0000000..34d87d0
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/change/WorkInProgressByDefaultIT.java
@@ -0,0 +1,158 @@
+// Copyright (C) 2018 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.rest.change;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.extensions.api.projects.ConfigInput;
+import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
+import com.google.gerrit.extensions.client.InheritableBoolean;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.ChangeInput;
+import com.google.gerrit.reviewdb.client.Project;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class WorkInProgressByDefaultIT extends AbstractDaemonTest {
+ private Project.NameKey project1;
+ private Project.NameKey project2;
+
+ @Before
+ public void setUp() throws Exception {
+ project1 = createProject("project-1");
+ project2 = createProject("project-2", project1);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ setApiUser(admin);
+ GeneralPreferencesInfo prefs = gApi.accounts().id(admin.id.get()).getPreferences();
+ prefs.workInProgressByDefault = false;
+ gApi.accounts().id(admin.id.get()).setPreferences(prefs);
+ }
+
+ @Test
+ public void createChangeWithWorkInProgressByDefaultForProjectDisabled() throws Exception {
+ ChangeInfo info =
+ gApi.changes().create(new ChangeInput(project2.get(), "master", "empty change")).get();
+ assertThat(info.workInProgress).isNull();
+ }
+
+ @Test
+ public void createChangeWithWorkInProgressByDefaultForProjectEnabled() throws Exception {
+ setWorkInProgressByDefaultForProject(project2);
+ ChangeInput input = new ChangeInput(project2.get(), "master", "empty change");
+ assertThat(gApi.changes().create(input).get().workInProgress).isTrue();
+ }
+
+ @Test
+ public void createChangeWithWorkInProgressByDefaultForUserEnabled() throws Exception {
+ setWorkInProgressByDefaultForUser();
+ ChangeInput input = new ChangeInput(project2.get(), "master", "empty change");
+ assertThat(gApi.changes().create(input).get().workInProgress).isTrue();
+ }
+
+ @Test
+ public void createChangeBypassWorkInProgressByDefaultForProjectEnabled() throws Exception {
+ setWorkInProgressByDefaultForProject(project2);
+ ChangeInput input = new ChangeInput(project2.get(), "master", "empty change");
+ input.workInProgress = false;
+ assertThat(gApi.changes().create(input).get().workInProgress).isNull();
+ }
+
+ @Test
+ public void createChangeBypassWorkInProgressByDefaultForUserEnabled() throws Exception {
+ setWorkInProgressByDefaultForUser();
+ ChangeInput input = new ChangeInput(project2.get(), "master", "empty change");
+ input.workInProgress = false;
+ assertThat(gApi.changes().create(input).get().workInProgress).isNull();
+ }
+
+ @Test
+ public void createChangeWithWorkInProgressByDefaultForProjectInherited() throws Exception {
+ setWorkInProgressByDefaultForProject(project1);
+ ChangeInfo info =
+ gApi.changes().create(new ChangeInput(project2.get(), "master", "empty change")).get();
+ assertThat(info.workInProgress).isTrue();
+ }
+
+ @Test
+ public void pushWithWorkInProgressByDefaultForProjectEnabled() throws Exception {
+ setWorkInProgressByDefaultForProject(project2);
+ assertThat(createChange(project2).getChange().change().isWorkInProgress()).isTrue();
+ }
+
+ @Test
+ public void pushWithWorkInProgressByDefaultForUserEnabled() throws Exception {
+ setWorkInProgressByDefaultForUser();
+ assertThat(createChange(project2).getChange().change().isWorkInProgress()).isTrue();
+ }
+
+ @Test
+ public void pushBypassWorkInProgressByDefaultForProjectEnabled() throws Exception {
+ setWorkInProgressByDefaultForProject(project2);
+ assertThat(
+ createChange(project2, "refs/for/master%ready").getChange().change().isWorkInProgress())
+ .isFalse();
+ }
+
+ @Test
+ public void pushBypassWorkInProgressByDefaultForUserEnabled() throws Exception {
+ setWorkInProgressByDefaultForUser();
+ assertThat(
+ createChange(project2, "refs/for/master%ready").getChange().change().isWorkInProgress())
+ .isFalse();
+ }
+
+ @Test
+ public void pushWithWorkInProgressByDefaultForProjectDisabled() throws Exception {
+ assertThat(createChange(project2).getChange().change().isWorkInProgress()).isFalse();
+ }
+
+ @Test
+ public void pushWorkInProgressByDefaultForProjectInherited() throws Exception {
+ setWorkInProgressByDefaultForProject(project1);
+ assertThat(createChange(project2).getChange().change().isWorkInProgress()).isTrue();
+ }
+
+ private void setWorkInProgressByDefaultForProject(Project.NameKey p) throws Exception {
+ ConfigInput input = new ConfigInput();
+ input.workInProgressByDefault = InheritableBoolean.TRUE;
+ gApi.projects().name(p.get()).config(input);
+ }
+
+ private void setWorkInProgressByDefaultForUser() throws Exception {
+ GeneralPreferencesInfo prefs = gApi.accounts().id(admin.id.get()).getPreferences();
+ prefs.workInProgressByDefault = true;
+ gApi.accounts().id(admin.id.get()).setPreferences(prefs);
+ }
+
+ private PushOneCommit.Result createChange(Project.NameKey p) throws Exception {
+ return createChange(p, "refs/for/master");
+ }
+
+ private PushOneCommit.Result createChange(Project.NameKey p, String r) throws Exception {
+ TestRepository<InMemoryRepository> testRepo = cloneProject(p);
+ PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
+ PushOneCommit.Result result = push.to(r);
+ result.assertOkStatus();
+ return result;
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java b/javatests/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
index 0b7758e..0017e08 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
@@ -156,12 +156,12 @@
Class<? extends RestApiException> errType,
String errMsg)
throws Exception {
- exception.expect(errType);
+ BranchInput in = new BranchInput();
+ in.revision = revision;
if (errMsg != null) {
exception.expectMessage(errMsg);
}
- BranchInput in = new BranchInput();
- in.revision = revision;
+ exception.expect(errType);
branch(branch).create(in);
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchesIT.java b/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchesIT.java
index 008997b..330f2b8 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchesIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/DeleteBranchesIT.java
@@ -28,6 +28,7 @@
import com.google.gerrit.extensions.api.projects.BranchInput;
import com.google.gerrit.extensions.api.projects.DeleteBranchesInput;
import com.google.gerrit.extensions.api.projects.ProjectApi;
+import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.reviewdb.client.RefNames;
@@ -64,7 +65,24 @@
}
@Test
- public void deleteBranchesForbidden() throws Exception {
+ public void deleteOneBranchWithoutPermissionForbidden() throws Exception {
+ ImmutableList<String> branchToDelete = ImmutableList.of("refs/heads/test-1");
+
+ DeleteBranchesInput input = new DeleteBranchesInput();
+ input.branches = branchToDelete;
+ setApiUser(user);
+ try {
+ project().deleteBranches(input);
+ fail("Expected AuthException");
+ } catch (AuthException e) {
+ assertThat(e).hasMessageThat().isEqualTo("delete not permitted for refs/heads/test-1");
+ }
+ setApiUser(admin);
+ assertBranches(BRANCHES);
+ }
+
+ @Test
+ public void deleteMultiBranchesWithoutPermissionForbidden() throws Exception {
DeleteBranchesInput input = new DeleteBranchesInput();
input.branches = BRANCHES;
setApiUser(user);
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java b/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
index 7096581..9645a94 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
@@ -42,10 +42,14 @@
import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.changes.SubmitInput;
+import com.google.gerrit.extensions.api.projects.ConfigInput;
+import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
import com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailStrategy;
+import com.google.gerrit.extensions.client.InheritableBoolean;
import com.google.gerrit.extensions.client.ReviewerState;
import com.google.gerrit.extensions.common.CommitInfo;
import com.google.gerrit.extensions.common.CommitMessageInput;
+import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.project.ProjectConfig;
import com.google.gerrit.server.project.testing.Util;
import com.google.gerrit.server.restapi.change.PostReview;
@@ -970,6 +974,38 @@
}
@Test
+ public void createWipChangeWithWorkInProgressByDefaultForProject() throws Exception {
+ setWorkInProgressByDefault(project, InheritableBoolean.TRUE);
+ StagedPreChange spc = stagePreChange("refs/for/master");
+ Truth.assertThat(gApi.changes().id(spc.changeId).get().workInProgress).isTrue();
+ assertThat(sender).notSent();
+ }
+
+ @Test
+ public void createWipChangeWithWorkInProgressByDefaultForUser() throws Exception {
+ // Make sure owner user is created
+ StagedChange sc = stageReviewableChange();
+ // All was cleaned already
+ assertThat(sender).notSent();
+
+ // Toggle workInProgress flag for owner
+ GeneralPreferencesInfo prefs = gApi.accounts().id(sc.owner.id.get()).getPreferences();
+ prefs.workInProgressByDefault = true;
+ gApi.accounts().id(sc.owner.id.get()).setPreferences(prefs);
+
+ // Create another change without notification that should be wip
+ StagedPreChange spc = stagePreChange("refs/for/master");
+ Truth.assertThat(gApi.changes().id(spc.changeId).get().workInProgress).isTrue();
+ assertThat(sender).notSent();
+
+ // Clean up workInProgressByDefault by owner
+ prefs = gApi.accounts().id(sc.owner.id.get()).getPreferences();
+ Truth.assertThat(prefs.workInProgressByDefault).isTrue();
+ prefs.workInProgressByDefault = false;
+ gApi.accounts().id(sc.owner.id.get()).setPreferences(prefs);
+ }
+
+ @Test
public void createReviewableChangeWithNotifyOwnerReviewers() throws Exception {
stagePreChange("refs/for/master%notify=OWNER_REVIEWERS");
assertThat(sender).notSent();
@@ -2646,4 +2682,11 @@
// PolyGerrit current immediately follows up with a review.
gApi.changes().id(sc.changeId).revision("current").review(ReviewInput.noScore());
}
+
+ private void setWorkInProgressByDefault(Project.NameKey p, InheritableBoolean v)
+ throws Exception {
+ ConfigInput input = new ConfigInput();
+ input.workInProgressByDefault = v;
+ gApi.projects().name(p.get()).config(input);
+ }
}
diff --git a/javatests/com/google/gerrit/acceptance/ssh/BUILD b/javatests/com/google/gerrit/acceptance/ssh/BUILD
index eefd9d3..b195ecc 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/BUILD
+++ b/javatests/com/google/gerrit/acceptance/ssh/BUILD
@@ -25,8 +25,9 @@
srcs = ["ElasticIndexIT.java"],
group = "elastic",
labels = [
- "elastic",
"docker",
+ "elastic",
+ "exclusive",
"ssh",
],
deps = [
diff --git a/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java b/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java
index 13bb2f2..95da5a6 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java
@@ -24,16 +24,14 @@
import org.eclipse.jgit.lib.Config;
public class ElasticIndexIT extends AbstractIndexTests {
- private static ElasticContainer<?> container;
private static Config getConfig(ElasticVersion version) {
ElasticNodeInfo elasticNodeInfo;
- container = ElasticContainer.createAndStart(version);
+ ElasticContainer<?> container = ElasticContainer.createAndStart(version);
elasticNodeInfo = new ElasticNodeInfo(container.getHttpHost().getPort());
String indicesPrefix = UUID.randomUUID().toString();
Config cfg = new Config();
- String password = version == ElasticVersion.V5_6 ? "changeme" : null;
- ElasticTestUtils.configure(cfg, elasticNodeInfo.port, indicesPrefix, password);
+ ElasticTestUtils.configure(cfg, elasticNodeInfo.port, indicesPrefix, version);
return cfg;
}
diff --git a/javatests/com/google/gerrit/common/data/GroupReferenceTest.java b/javatests/com/google/gerrit/common/data/GroupReferenceTest.java
index 717e122..fdc9dc6 100644
--- a/javatests/com/google/gerrit/common/data/GroupReferenceTest.java
+++ b/javatests/com/google/gerrit/common/data/GroupReferenceTest.java
@@ -154,4 +154,15 @@
assertThat(groupReference1.equals(groupReference3)).isFalse();
assertThat(groupReference2.equals(groupReference3)).isFalse();
}
+
+ @Test
+ public void testHashcode() {
+ AccountGroup.UUID uuid1 = new AccountGroup.UUID("uuid1");
+ assertThat(new GroupReference(uuid1, "foo").hashCode())
+ .isEqualTo(new GroupReference(uuid1, "bar").hashCode());
+
+ // Check that the following calls don't fail with an exception.
+ new GroupReference(null, "bar").hashCode();
+ new GroupReference(new AccountGroup.UUID(null), "bar").hashCode();
+ }
}
diff --git a/javatests/com/google/gerrit/elasticsearch/BUILD b/javatests/com/google/gerrit/elasticsearch/BUILD
index fe9d516..cc849eb 100644
--- a/javatests/com/google/gerrit/elasticsearch/BUILD
+++ b/javatests/com/google/gerrit/elasticsearch/BUILD
@@ -49,6 +49,7 @@
ELASTICSEARCH_TAGS = [
"docker",
"elastic",
+ "exclusive",
]
[junit_tests(
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticTestUtils.java b/javatests/com/google/gerrit/elasticsearch/ElasticTestUtils.java
index 02e0ba2..b46e040 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticTestUtils.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticTestUtils.java
@@ -32,11 +32,12 @@
}
}
- public static void configure(Config config, int port, String prefix, String password) {
+ public static void configure(Config config, int port, String prefix, ElasticVersion version) {
config.setEnum("index", null, "type", IndexType.ELASTICSEARCH);
config.setString("elasticsearch", null, "server", "http://localhost:" + port);
config.setString("elasticsearch", null, "prefix", prefix);
config.setInt("index", null, "maxLimit", 10000);
+ String password = version == ElasticVersion.V5_6 ? "changeme" : null;
if (password != null) {
config.setString("elasticsearch", null, "password", password);
}
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryAccountsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryAccountsTest.java
index 0fcdb20..5d2f944 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryAccountsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryAccountsTest.java
@@ -67,7 +67,8 @@
Config elasticsearchConfig = new Config(config);
InMemoryModule.setDefaults(elasticsearchConfig);
String indicesPrefix = testName();
- ElasticTestUtils.configure(elasticsearchConfig, nodeInfo.port, indicesPrefix, "changeme");
+ ElasticTestUtils.configure(
+ elasticsearchConfig, nodeInfo.port, indicesPrefix, ElasticVersion.V5_6);
return Guice.createInjector(new InMemoryModule(elasticsearchConfig, notesMigration));
}
}
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryChangesTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryChangesTest.java
index 4520020..5d76162 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryChangesTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryChangesTest.java
@@ -67,7 +67,8 @@
Config elasticsearchConfig = new Config(config);
InMemoryModule.setDefaults(elasticsearchConfig);
String indicesPrefix = testName();
- ElasticTestUtils.configure(elasticsearchConfig, nodeInfo.port, indicesPrefix, "changeme");
+ ElasticTestUtils.configure(
+ elasticsearchConfig, nodeInfo.port, indicesPrefix, ElasticVersion.V5_6);
return Guice.createInjector(new InMemoryModule(elasticsearchConfig, notesMigration));
}
}
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryGroupsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryGroupsTest.java
index d953139..9ce2e93 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryGroupsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryGroupsTest.java
@@ -67,7 +67,8 @@
Config elasticsearchConfig = new Config(config);
InMemoryModule.setDefaults(elasticsearchConfig);
String indicesPrefix = testName();
- ElasticTestUtils.configure(elasticsearchConfig, nodeInfo.port, indicesPrefix, "changeme");
+ ElasticTestUtils.configure(
+ elasticsearchConfig, nodeInfo.port, indicesPrefix, ElasticVersion.V5_6);
return Guice.createInjector(new InMemoryModule(elasticsearchConfig, notesMigration));
}
}
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryProjectsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryProjectsTest.java
index 98e466e..4184935 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryProjectsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV5QueryProjectsTest.java
@@ -67,7 +67,8 @@
Config elasticsearchConfig = new Config(config);
InMemoryModule.setDefaults(elasticsearchConfig);
String indicesPrefix = testName();
- ElasticTestUtils.configure(elasticsearchConfig, nodeInfo.port, indicesPrefix, "changeme");
+ ElasticTestUtils.configure(
+ elasticsearchConfig, nodeInfo.port, indicesPrefix, ElasticVersion.V5_6);
return Guice.createInjector(new InMemoryModule(elasticsearchConfig, notesMigration));
}
}
diff --git a/javatests/com/google/gerrit/index/query/NotPredicateTest.java b/javatests/com/google/gerrit/index/query/NotPredicateTest.java
index 88d8349..74eac61 100644
--- a/javatests/com/google/gerrit/index/query/NotPredicateTest.java
+++ b/javatests/com/google/gerrit/index/query/NotPredicateTest.java
@@ -50,17 +50,26 @@
final TestPredicate p = f("author", "bob");
final Predicate<String> n = not(p);
- exception.expect(UnsupportedOperationException.class);
- n.getChildren().clear();
- assertOnlyChild("clear", p, n);
+ try {
+ n.getChildren().clear();
+ fail("Expected UnsupportedOperationException");
+ } catch (UnsupportedOperationException e) {
+ assertOnlyChild("clear", p, n);
+ }
- exception.expect(UnsupportedOperationException.class);
- n.getChildren().remove(0);
- assertOnlyChild("remove(0)", p, n);
+ try {
+ n.getChildren().remove(0);
+ fail("Expected UnsupportedOperationException");
+ } catch (UnsupportedOperationException e) {
+ assertOnlyChild("remove(0)", p, n);
+ }
- exception.expect(UnsupportedOperationException.class);
- n.getChildren().iterator().remove();
- assertOnlyChild("remove(0)", p, n);
+ try {
+ n.getChildren().iterator().remove();
+ fail("Expected UnsupportedOperationException");
+ } catch (UnsupportedOperationException e) {
+ assertOnlyChild("remove()", p, n);
+ }
}
private static void assertOnlyChild(String o, Predicate<String> c, Predicate<String> p) {
diff --git a/javatests/com/google/gerrit/server/extensions/webui/UiActionsTest.java b/javatests/com/google/gerrit/server/extensions/webui/UiActionsTest.java
index f63b181..08d7082 100644
--- a/javatests/com/google/gerrit/server/extensions/webui/UiActionsTest.java
+++ b/javatests/com/google/gerrit/server/extensions/webui/UiActionsTest.java
@@ -49,16 +49,6 @@
}
@Override
- public ForProject user(CurrentUser user) {
- throw new UnsupportedOperationException("not implemented");
- }
-
- @Override
- public ForProject absentUser(Account.Id id) {
- throw new UnsupportedOperationException("not implemented");
- }
-
- @Override
public ForRef ref(String ref) {
throw new UnsupportedOperationException("not implemented");
}
diff --git a/javatests/com/google/gerrit/server/ioutil/HexFormatTest.java b/javatests/com/google/gerrit/server/ioutil/HexFormatTest.java
new file mode 100644
index 0000000..9bb6951
--- /dev/null
+++ b/javatests/com/google/gerrit/server/ioutil/HexFormatTest.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2018 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.ioutil;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+
+public class HexFormatTest {
+
+ @Test
+ public void fromInt() {
+ assertEquals("0000000f", HexFormat.fromInt(0xf));
+ assertEquals("801234ab", HexFormat.fromInt(0x801234ab));
+ assertEquals("deadbeef", HexFormat.fromInt(0xdeadbeef));
+ }
+}
diff --git a/javatests/com/google/gerrit/server/notedb/CommentJsonMigratorTest.java b/javatests/com/google/gerrit/server/notedb/CommentJsonMigratorTest.java
index 718e287..2031a14 100644
--- a/javatests/com/google/gerrit/server/notedb/CommentJsonMigratorTest.java
+++ b/javatests/com/google/gerrit/server/notedb/CommentJsonMigratorTest.java
@@ -38,11 +38,10 @@
import com.google.gerrit.testing.TestChanges;
import com.google.inject.Inject;
import java.io.ByteArrayOutputStream;
+import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import org.eclipse.jgit.junit.TestRepository;
-import org.eclipse.jgit.lib.BatchRefUpdate;
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;
@@ -92,7 +91,7 @@
getRevId(notes, 2), ps2Comment.toString());
ChangeNotes oldNotes = notes;
- migrate(project, 0);
+ checkMigrate(project, ImmutableList.of());
assertNoDifferences(notes, oldNotes);
assertThat(notes.getMetaId()).isEqualTo(oldNotes.getMetaId());
}
@@ -164,8 +163,16 @@
assertThat(getLegacyFormatMapForPublishedComments(notes, oldLog.get(3)))
.containsExactly(ps1Comment1.key, true, ps1Comment2.key, true, ps2Comment1.key, true);
+ // Check that dryRun doesn't touch anything.
+ String refName = RefNames.changeMetaRef(c.getId());
+ ObjectId before = repo.getRefDatabase().getRef(refName).getObjectId();
+ ProjectMigrationResult dryRunResult = migrator.migrateProject(project, repo, true);
+ ObjectId after = repo.getRefDatabase().getRef(refName).getObjectId();
+ assertThat(before).isEqualTo(after);
+ assertThat(dryRunResult.refsUpdated).isEqualTo(ImmutableList.of(refName));
+
ChangeNotes oldNotes = notes;
- migrate(project, 1);
+ checkMigrate(project, ImmutableList.of(refName));
// Comment content is the same.
notes = newNotes(c);
@@ -267,7 +274,11 @@
.containsExactly(otherCommentPs1.key, true);
ChangeNotes oldNotes = notes;
- migrate(allUsers, 2);
+ checkMigrate(
+ allUsers,
+ ImmutableList.of(
+ RefNames.refsDraftComments(c.getId(), changeOwner.getAccountId()),
+ RefNames.refsDraftComments(c.getId(), otherUser.getAccountId())));
assertNoDifferences(notes, oldNotes);
// Migration doesn't touch change ref.
@@ -368,7 +379,7 @@
.containsExactly(ps1Comment.key, true, ps2Comment.key, false, ps3Comment.key, true);
ChangeNotes oldNotes = notes;
- migrate(project, 1);
+ checkMigrate(project, ImmutableList.of(RefNames.changeMetaRef(c.getId())));
assertNoDifferences(notes, oldNotes);
// Comment content is the same.
@@ -393,23 +404,12 @@
.containsExactly(ps1Comment.key, false, ps2Comment.key, false, ps3Comment.key, false);
}
- @FunctionalInterface
- interface MigrateFunction {
- boolean call(
- Project.NameKey project,
- Repository repo,
- RevWalk rw,
- ObjectInserter ins,
- BatchRefUpdate bru)
- throws Exception;
- }
-
- private void migrate(Project.NameKey project, int expectedCommands) throws Exception {
+ private void checkMigrate(Project.NameKey project, List<String> expectedRefs) throws Exception {
try (Repository repo = repoManager.openRepository(project)) {
- ProjectMigrationResult progress = migrator.migrateProject(project, repo);
+ ProjectMigrationResult progress = migrator.migrateProject(project, repo, false);
assertThat(progress.ok).isTrue();
- assertThat(progress.refsUpdated).isEqualTo(expectedCommands);
+ assertThat(progress.refsUpdated).isEqualTo(expectedRefs);
}
}
diff --git a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index cf85aeb..0ff16bb 100644
--- a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -831,7 +831,13 @@
ChangeInserter ins4 = newChangeWithTopic(repo, "feature2-fixup");
Change change4 = insert(repo, ins4);
- Change change5 = insert(repo, newChange(repo));
+ ChangeInserter ins5 = newChangeWithTopic(repo, "https://gerrit.local");
+ Change change5 = insert(repo, ins5);
+
+ ChangeInserter ins6 = newChangeWithTopic(repo, "git_gerrit_training");
+ Change change6 = insert(repo, ins6);
+
+ Change change_no_topic = insert(repo, newChange(repo));
assertQuery("intopic:foo");
assertQuery("intopic:feature1", change1);
@@ -839,8 +845,9 @@
assertQuery("topic:feature2", change2);
assertQuery("intopic:feature2", change4, change3, change2);
assertQuery("intopic:fixup", change4);
- assertQuery("topic:\"\"", change5);
- assertQuery("intopic:\"\"", change5);
+ assertQuery("intopic:gerrit", change6, change5);
+ assertQuery("topic:\"\"", change_no_topic);
+ assertQuery("intopic:\"\"", change_no_topic);
}
@Test
@@ -899,6 +906,14 @@
}
@Test
+ public void byMessageSubstring() throws Exception {
+ TestRepository<Repo> repo = createProject("repo");
+ RevCommit commit1 = repo.parseBody(repo.commit().message("https://gerrit.local").create());
+ Change change1 = insert(repo, newChangeForCommit(repo, commit1));
+ assertQuery("message:gerrit", change1);
+ }
+
+ @Test
public void byLabel() throws Exception {
accountManager.authenticate(AuthRequest.forUser("anotheruser"));
TestRepository<Repo> repo = createProject("repo");
diff --git a/javatests/com/google/gerrit/server/util/IdGeneratorTest.java b/javatests/com/google/gerrit/server/util/IdGeneratorTest.java
index 39afcac..808eca8 100644
--- a/javatests/com/google/gerrit/server/util/IdGeneratorTest.java
+++ b/javatests/com/google/gerrit/server/util/IdGeneratorTest.java
@@ -34,11 +34,4 @@
assertEquals(0xdeadbeef, IdGenerator.unmix(IdGenerator.mix(0xdeadbeef)));
assertEquals(0x0b966b11, IdGenerator.unmix(IdGenerator.mix(0x0b966b11)));
}
-
- @Test
- public void format() {
- assertEquals("0000000f", IdGenerator.format(0xf));
- assertEquals("801234ab", IdGenerator.format(0x801234ab));
- assertEquals("deadbeef", IdGenerator.format(0xdeadbeef));
- }
}
diff --git a/plugins/BUILD b/plugins/BUILD
index 4cc982a..a623ff7 100644
--- a/plugins/BUILD
+++ b/plugins/BUILD
@@ -19,6 +19,7 @@
PLUGIN_API = [
"//java/com/google/gerrit/server",
+ "//java/com/google/gerrit/server/ioutil",
"//java/com/google/gerrit/server/restapi",
"//java/com/google/gerrit/pgm/init/api",
"//java/com/google/gerrit/httpd",
@@ -37,6 +38,7 @@
"//java/com/google/gerrit/metrics/dropwizard",
"//java/com/google/gerrit/reviewdb:server",
"//java/com/google/gerrit/util/http",
+ "//lib/commons:compress",
"//lib/commons:dbcp",
"//lib/commons:lang",
"//lib/dropwizard:dropwizard-core",
diff --git a/plugins/codemirror-editor b/plugins/codemirror-editor
index 318504d..9f614cd 160000
--- a/plugins/codemirror-editor
+++ b/plugins/codemirror-editor
@@ -1 +1 @@
-Subproject commit 318504d0eb74f2f9c2967e9865ace6d5b57638c6
+Subproject commit 9f614cdc7e21afa4eb3d32236b4f7549f9079fd0
diff --git a/plugins/replication b/plugins/replication
index 3a47f8c..1086fac 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 3a47f8c11ebdbcbc65fc6a58c35d18f1f3c3a74b
+Subproject commit 1086faccd0cf2aa53977854767fdc77f048b0253
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.html b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.html
index 704974d..fd6eb80 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.html
@@ -243,6 +243,22 @@
</span>
</section>
<section>
+ <span class="title">
+ Set new changes to "work in progress" by default</span>
+ <span class="value">
+ <gr-select
+ id="setAllNewChangesWorkInProgressByDefaultSelect"
+ bind-value="{{_repoConfig.work_in_progress_by_default.configured_value}}">
+ <select disabled$="[[_readOnly]]">
+ <template is="dom-repeat"
+ items="[[_formatBooleanSelect(_repoConfig.work_in_progress_by_default)]]">
+ <option value="[[item.value]]">[[item.label]]</option>
+ </template>
+ </select>
+ </gr-select>
+ </span>
+ </section>
+ <section>
<span class="title">Maximum Git object size limit</span>
<span class="value">
<input
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html
index 3a7ff10..fc00299 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html
@@ -86,7 +86,8 @@
changes="{{_changes}}"
preferences="[[preferences]]"
selected-index="{{viewState.selectedChangeIndex}}"
- show-star="[[_loggedIn]]"></gr-change-list>
+ show-star="[[_loggedIn]]"
+ on-toggle-star="_handleToggleStar"></gr-change-list>
<nav class$="[[_computeNavClass(_loading)]]">
Page [[_computePage(_offset, _changesPerPage)]]
<a id="prevArrow"
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js
index 2a05a2f..9b099cd 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js
@@ -263,5 +263,10 @@
_computeLoggedIn(account) {
return !!(account && Object.keys(account).length > 0);
},
+
+ _handleToggleStar(e) {
+ this.$.restAPI.saveChangeStarred(e.detail.change._number,
+ e.detail.starred);
+ },
});
})();
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
index 9930bf5..33bc338 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
@@ -18,10 +18,9 @@
'use strict';
const NUMBER_FIXED_COLUMNS = 3;
-
const CLOSED_STATUS = ['MERGED', 'ABANDONED'];
-
const LABEL_PREFIX_INVALID_PROLOG = 'Invalid-Prolog-Rules-Label-Name--';
+ const MAX_SHORTCUT_CHARS = 5;
Polymer({
is: 'gr-change-list',
@@ -185,10 +184,12 @@
if (labelName.startsWith(LABEL_PREFIX_INVALID_PROLOG)) {
labelName = labelName.slice(LABEL_PREFIX_INVALID_PROLOG.length);
}
- return labelName.split('-').reduce((a, i) => {
- if (!i) { return a; }
- return a + i[0].toUpperCase();
- }, '');
+ return labelName.split('-')
+ .reduce((a, i) => {
+ if (!i) { return a; }
+ return a + i[0].toUpperCase();
+ }, '')
+ .slice(0, MAX_SHORTCUT_CHARS);
},
_changesChanged(changes) {
@@ -306,10 +307,7 @@
}
const changeEl = changeEls[index];
- const change = changeEl.change;
- const newVal = !change.starred;
- changeEl.set('change.starred', newVal);
- this.$.restAPI.saveChangeStarred(change._number, newVal);
+ changeEl.$$('gr-change-star').toggleStar();
},
_changeForIndex(index) {
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
index 9ce5764..39d7ab1 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
@@ -132,6 +132,8 @@
'Some-Special-Label-7'), 'SSL7');
assert.equal(element._computeLabelShortcut('--Too----many----dashes---'),
'TMD');
+ assert.equal(element._computeLabelShortcut(
+ 'Really-rather-entirely-too-long-of-a-label-name'), 'RRETL');
});
test('colspans', () => {
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.html b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.html
index 1935962..1b21543 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.html
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.html
@@ -60,7 +60,8 @@
account="[[account]]"
preferences="[[preferences]]"
selected-index="{{viewState.selectedChangeIndex}}"
- sections="[[_results]]"></gr-change-list>
+ sections="[[_results]]"
+ on-toggle-star="_handleToggleStar"></gr-change-list>
</div>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
<gr-reporting id="reporting"></gr-reporting>
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js
index f86c98c..9cf76da 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js
@@ -241,5 +241,10 @@
_computeUserHeaderClass(userParam) {
return userParam === 'self' ? 'hide' : '';
},
+
+ _handleToggleStar(e) {
+ this.$.restAPI.saveChangeStarred(e.detail.change._number,
+ e.detail.starred);
+ },
});
})();
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-syntax-theme.html b/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-syntax-theme.html
index 0e028d8..41d3804 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-syntax-theme.html
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-syntax-theme.html
@@ -32,7 +32,8 @@
.gr-syntax-meta {
color: var(--syntax-meta-color);
}
- .gr-syntax-keyword {
+ .gr-syntax-keyword,
+ .gr-syntax-name {
color: var(--syntax-keyword-color);
line-height: 1;
}
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
index 14e5e6f..18c1734 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
@@ -227,6 +227,16 @@
</span>
</section>
<section>
+ <span class="title">Set new changes to "work in progress" by default</span>
+ <span class="value">
+ <input
+ id="workInProgressByDefault"
+ type="checkbox"
+ checked$="[[_localPrefs.work_in_progress_by_default]]"
+ on-change="_handleWorkInProgressByDefault">
+ </span>
+ </section>
+ <section>
<span class="title">
Insert Signed-off-by Footer For Inline Edit Changes
</span>
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
index 213ab65..8eefc0a 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
@@ -24,6 +24,7 @@
'email_strategy',
'diff_view',
'publish_comments_on_push',
+ 'work_in_progress_by_default',
'signed_off_by',
'email_format',
'size_bar_in_change_table',
@@ -291,6 +292,11 @@
this.$.publishCommentsOnPush.checked);
},
+ _handleWorkInProgressByDefault() {
+ this.set('_localPrefs.work_in_progress_by_default',
+ this.$.workInProgressByDefault.checked);
+ },
+
_handleInsertSignedOff() {
this.set('_localPrefs.signed_off_by', this.$.insertSignedOff.checked);
},
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html
index b208ba2..f47816f 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html
@@ -175,6 +175,9 @@
assert.equal(valueOf('Publish comments on push', 'preferences')
.firstElementChild.checked, false);
assert.equal(valueOf(
+ 'Set new changes to "work in progress" by default', 'preferences')
+ .firstElementChild.checked, false);
+ assert.equal(valueOf(
'Insert Signed-off-by Footer For Inline Edit Changes', 'preferences')
.firstElementChild.checked, false);
@@ -234,6 +237,30 @@
});
});
+ test('set new changes work-in-progress', done => {
+ const newChangesWorkInProgress =
+ valueOf('Set new changes to "work in progress" by default',
+ 'preferences').firstElementChild;
+ MockInteractions.tap(newChangesWorkInProgress);
+
+ assert.isFalse(element._menuChanged);
+ assert.isTrue(element._prefsChanged);
+
+ stub('gr-rest-api-interface', {
+ savePreferences(prefs) {
+ assert.equal(prefs.work_in_progress_by_default, true);
+ return Promise.resolve();
+ },
+ });
+
+ // Save the change.
+ element._handleSavePreferences().then(() => {
+ assert.isFalse(element._prefsChanged);
+ assert.isFalse(element._menuChanged);
+ done();
+ });
+ });
+
test('diff preferences', done => {
// Rendered with the expected preferences selected.
assert.equal(valueOf('Context', 'diffPreferences')
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.html b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.html
index 70b2635..a14c652 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.html
+++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.html
@@ -17,7 +17,6 @@
<link rel="import" href="../../../bower_components/polymer/polymer.html">
<link rel="import" href="../../shared/gr-icons/gr-icons.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
<link rel="import" href="../../../styles/shared-styles.html">
<dom-module id="gr-change-star">
@@ -36,7 +35,6 @@
class$="[[_computeStarClass(change.starred)]]"
icon$="[[_computeStarIcon(change.starred)]]"></iron-icon>
</button>
- <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
</template>
<script src="gr-change-star.js"></script>
</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js
index 9646735..3c46d1b 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js
+++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js
@@ -20,14 +20,18 @@
Polymer({
is: 'gr-change-star',
+ /**
+ * Fired when star state is toggled.
+ *
+ * @event toggle-star
+ */
+
properties: {
/** @type {?} */
change: {
type: Object,
notify: true,
},
-
- _xhrPromise: Object, // Used for testing.
},
_computeStarClass(starred) {
@@ -42,8 +46,10 @@
toggleStar() {
const newVal = !this.change.starred;
this.set('change.starred', newVal);
- this._xhrPromise = this.$.restAPI.saveChangeStarred(this.change._number,
- newVal);
+ this.dispatchEvent(new CustomEvent('toggle-star', {
+ bubbles: true,
+ detail: {change: this.change, starred: newVal},
+ }));
},
});
})();
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.html b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.html
index f24b45c..0ca9368 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.html
@@ -37,9 +37,6 @@
let element;
setup(() => {
- stub('gr-rest-api-interface', {
- saveChangeStarred() { return Promise.resolve({ok: true}); },
- });
element = fixture('basic');
element.change = {
_number: 2,
@@ -60,23 +57,21 @@
});
test('starring', done => {
- element.set('change.starred', false);
- MockInteractions.tap(element.$$('button'));
-
- element._xhrPromise.then(req => {
+ element.addEventListener('toggle-star', () => {
assert.equal(element.change.starred, true);
done();
});
+ element.set('change.starred', false);
+ MockInteractions.tap(element.$$('button'));
});
test('unstarring', done => {
- element.set('change.starred', true);
- MockInteractions.tap(element.$$('button'));
-
- element._xhrPromise.then(req => {
+ element.addEventListener('toggle-star', () => {
assert.equal(element.change.starred, false);
done();
});
+ element.set('change.starred', true);
+ MockInteractions.tap(element.$$('button'));
});
});
</script>
diff --git a/polygerrit-ui/externs/BUILD b/polygerrit-ui/app/externs/BUILD
similarity index 100%
rename from polygerrit-ui/externs/BUILD
rename to polygerrit-ui/app/externs/BUILD
diff --git a/polygerrit-ui/externs/plugin.js b/polygerrit-ui/app/externs/plugin.js
similarity index 100%
rename from polygerrit-ui/externs/plugin.js
rename to polygerrit-ui/app/externs/plugin.js
diff --git a/tools/bzl/js.bzl b/tools/bzl/js.bzl
index 6590cd6..68a929f 100644
--- a/tools/bzl/js.bzl
+++ b/tools/bzl/js.bzl
@@ -471,7 +471,7 @@
no_closure_library = True,
deps = [
"//lib/polymer_externs:polymer_closure",
- "//polygerrit-ui/externs:plugin",
+ "//polygerrit-ui/app/externs:plugin",
],
)
diff --git a/tools/bzl/junit.bzl b/tools/bzl/junit.bzl
index 8076059..d711356 100644
--- a/tools/bzl/junit.bzl
+++ b/tools/bzl/junit.bzl
@@ -43,11 +43,7 @@
if findex != -1:
break
if findex == -1:
- fail(
- "%s does not contain any of %s",
- fname,
- _PREFIXES,
- )
+ fail("%s does not contain any of %s" % (fname, _PREFIXES))
return ".".join(toks[findex:]) + ".class"
def _impl(ctx):
diff --git a/tools/js/npm_pack.py b/tools/js/npm_pack.py
index d817701..f14262a 100755
--- a/tools/js/npm_pack.py
+++ b/tools/js/npm_pack.py
@@ -15,8 +15,8 @@
"""This downloads an NPM binary, and bundles it with its dependencies.
-This is used to assemble a pinned version of crisper, hosted on the
-Google storage bucket ("repository=GERRIT" in WORKSPACE).
+For full instructions on adding new binaries to the build, see
+http://gerrit-review.googlesource.com/Documentation/dev-bazel.html#npm-binary
"""
from __future__ import print_function