Merge branch 'stable-3.0'

* stable-3.0:
  Fix single tab indentation in diff
  Add plugin-manager as core plugin
  Set version to 3.0.0-rc2
  Update highlight.js to master branch
  Stop using deprecated SoyListData and SoyMapData
  PolyGerrit: Fix typos in comments
  gr-create-project-dialog: Fix typo in element id name
  AccountIT: Fix typo in variable name
  Set version to 2.16.9-SNAPSHOT
  AccountIT: Add tests for email notification on adding SSH/GPG keys
  AccountApi: Add methods to generate and set the HTTP password

Change-Id: Ic9f174c5fbe38fa9e4de2bed58e541d94f1df9bd
This commit is contained in:
David Pursehouse
2019-05-03 18:12:47 +09:00
18 changed files with 300 additions and 136 deletions

1
.gitignore vendored
View File

@@ -41,6 +41,7 @@
!/plugins/external_plugin_deps.bzl
!/plugins/gitiles
!/plugins/hooks
!/plugins/plugin-manager
!/plugins/replication
!/plugins/reviewnotes
!/plugins/singleusergroup

5
.gitmodules vendored
View File

@@ -28,6 +28,11 @@
url = ../plugins/hooks
branch = .
[submodule "plugins/plugin-manager"]
path = plugins/plugin-manager
url = ../plugins/plugin-manager
branch = .
[submodule "plugins/replication"]
path = plugins/replication
url = ../plugins/replication

View File

@@ -110,6 +110,20 @@ Documentation] |
link:https://gerrit.googlesource.com/plugins/hooks/+doc/master/src/main/resources/Documentation/config.md[
Configuration]
[[plugin-manager]]
=== plugin-manager
This plugins provides an initial wizard to discover and install Gerrit plugins.
Per default GerritForge CI is used to download the plugin artifacts from, but
this can be changed per plugin configuration.
link:https://gerrit-review.googlesource.com/admin/repos/plugins/plugin-manager[
Project]
link:https://gerrit.googlesource.com/plugins/plugin-manager/+doc/master/src/main/resources/Documentation/about.md[
Documentation]
link:https://gerrit.googlesource.com/plugins/plugin-manager/+doc/master/src/main/resources/Documentation/config.md[
Configuration]
[[replication]]
=== replication

View File

@@ -114,6 +114,23 @@ public interface AccountApi {
void setName(String name) throws RestApiException;
/**
* Generate a new HTTP password.
*
* @return the generated password.
*/
String generateHttpPassword() throws RestApiException;
/**
* Set a new HTTP password.
*
* <p>May only be invoked by administrators.
*
* @param httpPassword the new password, {@code null} to remove the password.
* @return the new password, {@code null} if the password was removed.
*/
String setHttpPassword(String httpPassword) throws RestApiException;
/**
* A default implementation which allows source compatibility when adding new methods to the
* interface.
@@ -317,5 +334,15 @@ public interface AccountApi {
public void setName(String name) throws RestApiException {
throw new NotImplementedException();
}
@Override
public String generateHttpPassword() throws RestApiException {
throw new NotImplementedException();
}
@Override
public String setHttpPassword(String httpPassword) throws RestApiException {
throw new NotImplementedException();
}
}
}

View File

@@ -22,13 +22,14 @@ import com.google.common.io.Resources;
import com.google.gerrit.common.Nullable;
import com.google.template.soy.SoyFileSet;
import com.google.template.soy.data.SanitizedContent;
import com.google.template.soy.data.SoyMapData;
import com.google.template.soy.data.UnsafeSanitizedContentOrdainer;
import com.google.template.soy.tofu.SoyTofu;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@@ -74,8 +75,8 @@ public class IndexServlet extends HttpServlet {
return uri.getPath().replaceAll("/$", "");
}
static SoyMapData getTemplateData(String canonicalURL, String cdnPath, String faviconPath)
throws URISyntaxException {
static Map<String, String> getTemplateData(
String canonicalURL, String cdnPath, String faviconPath) throws URISyntaxException {
String canonicalPath = computeCanonicalPath(canonicalURL);
String staticPath = "";
@@ -91,9 +92,10 @@ public class IndexServlet extends HttpServlet {
UnsafeSanitizedContentOrdainer.ordainAsSafe(
staticPath, SanitizedContent.ContentKind.TRUSTED_RESOURCE_URI);
return new SoyMapData(
"canonicalPath", canonicalPath,
"staticResourcePath", sanitizedStaticPath,
"faviconPath", faviconPath);
Map<String, String> data = new HashMap<>();
data.put("canonicalPath", canonicalPath);
data.put("staticResourcePath", sanitizedStaticPath.coerceToString());
data.put("faviconPath", faviconPath);
return data;
}
}

View File

@@ -40,6 +40,7 @@ import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.EmailInfo;
import com.google.gerrit.extensions.common.GpgKeyInfo;
import com.google.gerrit.extensions.common.GroupInfo;
import com.google.gerrit.extensions.common.HttpPasswordInput;
import com.google.gerrit.extensions.common.Input;
import com.google.gerrit.extensions.common.NameInput;
import com.google.gerrit.extensions.common.SshKeyInfo;
@@ -75,6 +76,7 @@ import com.google.gerrit.server.restapi.account.Index;
import com.google.gerrit.server.restapi.account.PostWatchedProjects;
import com.google.gerrit.server.restapi.account.PutActive;
import com.google.gerrit.server.restapi.account.PutAgreement;
import com.google.gerrit.server.restapi.account.PutHttpPassword;
import com.google.gerrit.server.restapi.account.PutName;
import com.google.gerrit.server.restapi.account.PutStatus;
import com.google.gerrit.server.restapi.account.SetDiffPreferences;
@@ -135,6 +137,7 @@ public class AccountApiImpl implements AccountApi {
private final GetGroups getGroups;
private final EmailApiImpl.Factory emailApi;
private final PutName putName;
private final PutHttpPassword putHttpPassword;
@Inject
AccountApiImpl(
@@ -177,6 +180,7 @@ public class AccountApiImpl implements AccountApi {
GetGroups getGroups,
EmailApiImpl.Factory emailApi,
PutName putName,
PutHttpPassword putPassword,
@Assisted AccountResource account) {
this.account = account;
this.accountLoaderFactory = ailf;
@@ -218,6 +222,7 @@ public class AccountApiImpl implements AccountApi {
this.getGroups = getGroups;
this.emailApi = emailApi;
this.putName = putName;
this.putHttpPassword = putPassword;
}
@Override
@@ -593,4 +598,31 @@ public class AccountApiImpl implements AccountApi {
throw asRestApiException("Cannot set account name", e);
}
}
@Override
public String generateHttpPassword() throws RestApiException {
HttpPasswordInput input = new HttpPasswordInput();
input.generate = true;
try {
// Response should never be 'none' for a generated password, but
// let's make sure.
Response<String> result = putHttpPassword.apply(account, input);
return result.isNone() ? null : result.value();
} catch (Exception e) {
throw asRestApiException("Cannot generate HTTP password", e);
}
}
@Override
public String setHttpPassword(String password) throws RestApiException {
HttpPasswordInput input = new HttpPasswordInput();
input.generate = false;
input.httpPassword = password;
try {
Response<String> result = putHttpPassword.apply(account, input);
return result.isNone() ? null : result.value();
} catch (Exception e) {
throw asRestApiException("Cannot generate HTTP password", e);
}
}
}

View File

@@ -44,15 +44,15 @@ import com.google.gerrit.server.permissions.GlobalPermission;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.template.soy.data.SoyListData;
import com.google.template.soy.data.SoyMapData;
import java.io.IOException;
import java.sql.Timestamp;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
@@ -560,11 +560,11 @@ public abstract class ChangeEmail extends NotificationEmail {
* a 'type' key which maps to one of 'common', 'add' or 'remove' and a 'text' key which maps to
* the line's content.
*/
private SoyListData getDiffTemplateData() {
SoyListData result = new SoyListData();
private List<Map<String, String>> getDiffTemplateData() {
List<Map<String, String>> result = new ArrayList<>();
Splitter lineSplitter = Splitter.on(System.getProperty("line.separator"));
for (String diffLine : lineSplitter.split(getUnifiedDiff())) {
SoyMapData lineData = new SoyMapData();
Map<String, String> lineData = new HashMap<>();
lineData.put("text", diffLine);
// Skip empty lines and lines that look like diff headers.

View File

@@ -1890,8 +1890,11 @@ public class AccountIT extends AbstractDaemonTest {
String id = key.getKeyIdString();
addExternalIdEmail(admin, "test1@example.com");
sender.clear();
assertKeyMapContains(key, addGpgKey(key.getPublicKeyArmored()));
assertKeys(key);
assertThat(sender.getMessages()).hasSize(1);
assertThat(sender.getMessages().get(0).body()).contains("new GPG keys have been added");
requestScopeOperations.setApiUser(user.id());
exception.expect(ResourceNotFoundException.class);
@@ -1906,14 +1909,21 @@ public class AccountIT extends AbstractDaemonTest {
String id = key.getKeyIdString();
PGPPublicKey pk = key.getPublicKey();
sender.clear();
GpgKeyInfo info = addGpgKey(armor(pk)).get(id);
assertThat(info.userIds).hasSize(2);
assertIteratorSize(2, getOnlyKeyFromStore(key).getUserIDs());
assertThat(sender.getMessages()).hasSize(1);
assertThat(sender.getMessages().get(0).body()).contains("new GPG keys have been added");
pk = PGPPublicKey.removeCertification(pk, "foo:myId");
sender.clear();
info = addGpgKeyNoReindex(armor(pk)).get(id);
assertThat(info.userIds).hasSize(1);
assertIteratorSize(1, getOnlyKeyFromStore(key).getUserIDs());
// TODO: Issue 10769: Adding an already existing key should not result in a notification email
assertThat(sender.getMessages()).hasSize(1);
assertThat(sender.getMessages().get(0).body()).contains("new GPG keys have been added");
}
@Test
@@ -2023,32 +2033,42 @@ public class AccountIT extends AbstractDaemonTest {
assertSequenceNumbers(info);
SshKeyInfo key = info.get(0);
KeyPair keyPair = sshKeys.getKeyPair(admin);
String inital = TestSshKeys.publicKey(keyPair, admin.email());
assertThat(key.sshPublicKey).isEqualTo(inital);
String initial = TestSshKeys.publicKey(keyPair, admin.email());
assertThat(key.sshPublicKey).isEqualTo(initial);
accountIndexedCounter.assertNoReindex();
// Add a new key
sender.clear();
String newKey = TestSshKeys.publicKey(TestSshKeys.genSshKey(), admin.email());
gApi.accounts().self().addSshKey(newKey);
info = gApi.accounts().self().listSshKeys();
assertThat(info).hasSize(2);
assertSequenceNumbers(info);
accountIndexedCounter.assertReindexOf(admin);
assertThat(sender.getMessages()).hasSize(1);
assertThat(sender.getMessages().get(0).body()).contains("new SSH keys have been added");
// Add an existing key (the request succeeds, but the key isn't added again)
gApi.accounts().self().addSshKey(inital);
sender.clear();
gApi.accounts().self().addSshKey(initial);
info = gApi.accounts().self().listSshKeys();
assertThat(info).hasSize(2);
assertSequenceNumbers(info);
accountIndexedCounter.assertNoReindex();
// TODO: Issue 10769: Adding an already existing key should not result in a notification email
assertThat(sender.getMessages()).hasSize(1);
assertThat(sender.getMessages().get(0).body()).contains("new SSH keys have been added");
// Add another new key
sender.clear();
String newKey2 = TestSshKeys.publicKey(TestSshKeys.genSshKey(), admin.email());
gApi.accounts().self().addSshKey(newKey2);
info = gApi.accounts().self().listSshKeys();
assertThat(info).hasSize(3);
assertSequenceNumbers(info);
accountIndexedCounter.assertReindexOf(admin);
assertThat(sender.getMessages()).hasSize(1);
assertThat(sender.getMessages().get(0).body()).contains("new SSH keys have been added");
// Delete second key
gApi.accounts().self().deleteSshKey(2);
@@ -2718,6 +2738,67 @@ public class AccountIT extends AbstractDaemonTest {
}
}
@Test
public void userCanGenerateNewHttpPassword() throws Exception {
String newPassword = gApi.accounts().self().generateHttpPassword();
assertThat(newPassword).isNotNull();
}
@Test
public void adminCanGenerateNewHttpPasswordForUser() throws Exception {
requestScopeOperations.setApiUser(admin.id());
String newPassword = gApi.accounts().id(user.username()).generateHttpPassword();
assertThat(newPassword).isNotNull();
}
@Test
public void userCannotGenerateNewHttpPasswordForOtherUser() throws Exception {
requestScopeOperations.setApiUser(user.id());
exception.expect(AuthException.class);
gApi.accounts().id(admin.username()).generateHttpPassword();
}
@Test
public void userCannotExplicitlySetHttpPassword() throws Exception {
requestScopeOperations.setApiUser(user.id());
exception.expect(AuthException.class);
gApi.accounts().self().setHttpPassword("my-new-password");
}
@Test
public void userCannotExplicitlySetHttpPasswordForOtherUser() throws Exception {
requestScopeOperations.setApiUser(user.id());
exception.expect(AuthException.class);
gApi.accounts().id(admin.username()).setHttpPassword("my-new-password");
}
@Test
public void userCanRemoveHttpPassword() throws Exception {
requestScopeOperations.setApiUser(user.id());
assertThat(gApi.accounts().self().setHttpPassword(null)).isNull();
}
@Test
public void userCannotRemoveHttpPasswordForOtherUser() throws Exception {
requestScopeOperations.setApiUser(user.id());
exception.expect(AuthException.class);
gApi.accounts().id(admin.username()).setHttpPassword(null);
}
@Test
public void adminCanExplicitlySetHttpPasswordForUser() throws Exception {
requestScopeOperations.setApiUser(admin.id());
String httpPassword = "new-password-for-user";
assertThat(gApi.accounts().id(user.username()).setHttpPassword(httpPassword))
.isEqualTo(httpPassword);
}
@Test
public void adminCanRemoveHttpPasswordForUser() throws Exception {
requestScopeOperations.setApiUser(admin.id());
assertThat(gApi.accounts().id(user.username()).setHttpPassword(null)).isNull();
}
private void createDraft(PushOneCommit.Result r, String path, String message) throws Exception {
DraftInput in = new DraftInput();
in.path = path;

View File

@@ -18,8 +18,8 @@ import static com.google.common.truth.Truth.assertThat;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.gerrit.testing.GerritBaseTests;
import com.google.template.soy.data.SoyMapData;
import java.net.URISyntaxException;
import java.util.Map;
import org.junit.Test;
public class IndexServletTest extends GerritBaseTests {
@@ -38,35 +38,34 @@ public class IndexServletTest extends GerritBaseTests {
@Test
public void noPathAndNoCDN() throws URISyntaxException {
SoyMapData data = IndexServlet.getTemplateData("http://example.com/", null, null);
assertThat(data.getSingle("canonicalPath").stringValue()).isEqualTo("");
assertThat(data.getSingle("staticResourcePath").stringValue()).isEqualTo("");
Map<String, String> data = IndexServlet.getTemplateData("http://example.com/", null, null);
assertThat(data.get("canonicalPath")).isEqualTo("");
assertThat(data.get("staticResourcePath")).isEqualTo("");
}
@Test
public void pathAndNoCDN() throws URISyntaxException {
SoyMapData data = IndexServlet.getTemplateData("http://example.com/gerrit/", null, null);
assertThat(data.getSingle("canonicalPath").stringValue()).isEqualTo("/gerrit");
assertThat(data.getSingle("staticResourcePath").stringValue()).isEqualTo("/gerrit");
Map<String, String> data =
IndexServlet.getTemplateData("http://example.com/gerrit/", null, null);
assertThat(data.get("canonicalPath")).isEqualTo("/gerrit");
assertThat(data.get("staticResourcePath")).isEqualTo("/gerrit");
}
@Test
public void noPathAndCDN() throws URISyntaxException {
SoyMapData data =
Map<String, String> data =
IndexServlet.getTemplateData("http://example.com/", "http://my-cdn.com/foo/bar/", null);
assertThat(data.getSingle("canonicalPath").stringValue()).isEqualTo("");
assertThat(data.getSingle("staticResourcePath").stringValue())
.isEqualTo("http://my-cdn.com/foo/bar/");
assertThat(data.get("canonicalPath")).isEqualTo("");
assertThat(data.get("staticResourcePath")).isEqualTo("http://my-cdn.com/foo/bar/");
}
@Test
public void pathAndCDN() throws URISyntaxException {
SoyMapData data =
Map<String, String> data =
IndexServlet.getTemplateData(
"http://example.com/gerrit", "http://my-cdn.com/foo/bar/", null);
assertThat(data.getSingle("canonicalPath").stringValue()).isEqualTo("/gerrit");
assertThat(data.getSingle("staticResourcePath").stringValue())
.isEqualTo("http://my-cdn.com/foo/bar/");
assertThat(data.get("canonicalPath")).isEqualTo("/gerrit");
assertThat(data.get("staticResourcePath")).isEqualTo("http://my-cdn.com/foo/bar/");
}
@Test

File diff suppressed because one or more lines are too long

View File

@@ -85,7 +85,7 @@ limitations under the License.
<span class="title">Create initial empty commit</span>
<span class="value">
<gr-select
id="initalCommit"
id="initialCommit"
bind-value="{{_repoConfig.create_empty_commit}}">
<select>
<option value="false">False</option>

View File

@@ -50,7 +50,7 @@ limitations under the License.
});
test('default values are populated', () => {
assert.isTrue(element.$.initalCommit.bindValue);
assert.isTrue(element.$.initialCommit.bindValue);
assert.isFalse(element.$.parentRepo.bindValue);
});
@@ -83,7 +83,7 @@ limitations under the License.
element.$.repoNameInput.bindValue = configInputObj.name;
element.$.rightsInheritFromInput.bindValue = configInputObj.parent;
element.$.ownerInput.text = configInputObj.owners[0];
element.$.initalCommit.bindValue =
element.$.initialCommit.bindValue =
configInputObj.create_empty_commit;
element.$.parentRepo.bindValue =
configInputObj.permissions_only;

View File

@@ -98,7 +98,7 @@ limitations under the License.
loadCommentSpy = sandbox.spy(commentApiWrapper.$.commentAPI, 'loadAll');
// Stub methods on the changeComments object after changeComments has
// been initalized.
// been initialized.
commentApiWrapper.loadComments().then(() => {
sandbox.stub(element.changeComments, 'getPaths').returns({});
sandbox.stub(element.changeComments, 'getCommentsBySideForPath')
@@ -1451,7 +1451,7 @@ limitations under the License.
sandbox.stub(element, '_reviewFile');
// Stub methods on the changeComments object after changeComments has
// been initalized.
// been initialized.
commentApiWrapper.loadComments().then(() => {
sandbox.stub(element.changeComments, 'getPaths').returns({});
sandbox.stub(element.changeComments, 'getCommentsBySideForPath')

View File

@@ -149,7 +149,7 @@ limitations under the License.
element.messages = messages;
// Stub methods on the changeComments object after changeComments has
// been initalized.
// been initialized.
return commentApiWrapper.loadComments();
});
@@ -466,7 +466,7 @@ limitations under the License.
element.messages = messages;
// Stub methods on the changeComments object after changeComments has
// been initalized.
// been initialized.
return commentApiWrapper.loadComments();
});

View File

@@ -183,6 +183,7 @@ limitations under the License.
color: var(--diff-tab-indicator-color);
/* >> character */
content: '\00BB';
position: absolute;
}
/* Is defined after other background-colors, such that this
rule wins in case of same specificity. */

View File

@@ -76,7 +76,7 @@ limitations under the License.
element = commentApiWrapper.$.patchRange;
// Stub methods on the changeComments object after changeComments has
// been initalized.
// been initialized.
return commentApiWrapper.loadComments();
});

View File

@@ -5,6 +5,7 @@ CORE_PLUGINS = [
"download-commands",
"gitiles",
"hooks",
"plugin-manager",
"replication",
"reviewnotes",
"singleusergroup",