Merge branch 'stable-3.3' into stable-3.4
* stable-3.3:
Fix the default logic in managing refs-filter
Reuse Gerrit code for head update
Add HEAD update REST API endpoint
Add project initialisation during fetch REST Api call
Add project delete REST API endpoint
Change-Id: I00d81c7eadc25c8f1d3b55b6479c94630804e7f2
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/ReplicationQueue.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/ReplicationQueue.java
index 0e4ace1..61f2a64 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/ReplicationQueue.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/ReplicationQueue.java
@@ -46,6 +46,7 @@
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
+import org.apache.http.client.ClientProtocolException;
import org.eclipse.jgit.errors.InvalidObjectIdException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.transport.URIish;
@@ -232,17 +233,8 @@
FetchRestApiClient fetchClient = fetchClientFactory.create(source);
HttpResult result = fetchClient.callSendObject(project, refName, revision, uri);
- if (!result.isSuccessful()
- && source.isCreateMissingRepositories()
- && result.isProjectMissing(project)) {
- HttpResult initProjectResult = fetchClient.initProject(project, uri);
- if (initProjectResult.isSuccessful()) {
- result = fetchClient.callFetch(project, "refs/*", uri);
- } else {
- String errorMessage =
- initProjectResult.getMessage().map(e -> " - Error: " + e).orElse("");
- repLog.error("Cannot create project " + project + errorMessage);
- }
+ if (isProjectMissing(result, project) && source.isCreateMissingRepositories()) {
+ result = initProject(project, uri, fetchClient, result);
}
if (!result.isSuccessful()) {
repLog.warn(
@@ -275,8 +267,10 @@
try {
URIish uri = new URIish(apiUrl);
FetchRestApiClient fetchClient = fetchClientFactory.create(source);
-
HttpResult result = fetchClient.callFetch(project, refName, uri);
+ if (isProjectMissing(result, project) && source.isCreateMissingRepositories()) {
+ result = initProject(project, uri, fetchClient, result);
+ }
if (!result.isSuccessful()) {
stateLog.warn(
String.format(
@@ -302,6 +296,23 @@
return maxRetries == 0 || attempt < maxRetries;
}
+ private Boolean isProjectMissing(HttpResult result, Project.NameKey project) {
+ return !result.isSuccessful() && result.isProjectMissing(project);
+ }
+
+ private HttpResult initProject(
+ Project.NameKey project, URIish uri, FetchRestApiClient fetchClient, HttpResult result)
+ throws IOException, ClientProtocolException {
+ HttpResult initProjectResult = fetchClient.initProject(project, uri);
+ if (initProjectResult.isSuccessful()) {
+ result = fetchClient.callFetch(project, "refs/*", uri);
+ } else {
+ String errorMessage = initProjectResult.getMessage().map(e -> " - Error: " + e).orElse("");
+ repLog.error("Cannot create project " + project + errorMessage);
+ }
+ return result;
+ }
+
private void fireBeforeStartupEvents() {
Set<String> eventsReplayed = new HashSet<>();
for (ReferenceUpdatedEvent event : beforeStartupEventsQueue) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationApiModule.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationApiModule.java
index f94f49b..e7b3a7f 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationApiModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationApiModule.java
@@ -27,8 +27,12 @@
protected void configure() {
bind(FetchAction.class).in(Scopes.SINGLETON);
bind(ApplyObjectAction.class).in(Scopes.SINGLETON);
+ bind(ProjectDeletionAction.class).in(Scopes.SINGLETON);
+ bind(UpdateHeadAction.class).in(Scopes.SINGLETON);
post(PROJECT_KIND, "fetch").to(FetchAction.class);
post(PROJECT_KIND, "apply-object").to(ApplyObjectAction.class);
+ delete(PROJECT_KIND, "delete-project").to(ProjectDeletionAction.class);
+ put(PROJECT_KIND, "HEAD").to(UpdateHeadAction.class);
bind(FetchPreconditions.class).in(Scopes.SINGLETON);
bind(CapabilityDefinition.class)
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilter.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilter.java
index 44dd3bb..4af2bf7 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilter.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilter.java
@@ -27,6 +27,7 @@
import com.google.common.base.Splitter;
import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.extensions.annotations.PluginName;
import com.google.gerrit.extensions.api.projects.HeadInput;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -80,6 +81,7 @@
private ProjectsCollection projectsCollection;
private Gson gson;
private Provider<CurrentUser> userProvider;
+ private String pluginName;
@Inject
public PullReplicationFilter(
@@ -89,7 +91,8 @@
UpdateHeadAction updateHEADAction,
ProjectDeletionAction projectDeletionAction,
ProjectsCollection projectsCollection,
- Provider<CurrentUser> userProvider) {
+ Provider<CurrentUser> userProvider,
+ @PluginName String pluginName) {
this.fetchAction = fetchAction;
this.applyObjectAction = applyObjectAction;
this.projectInitializationAction = projectInitializationAction;
@@ -97,6 +100,7 @@
this.projectDeletionAction = projectDeletionAction;
this.projectsCollection = projectsCollection;
this.userProvider = userProvider;
+ this.pluginName = pluginName;
this.gson = OutputFormat.JSON.newGsonBuilder().create();
}
@@ -303,7 +307,7 @@
}
private boolean isDeleteProjectAction(HttpServletRequest httpRequest) {
- return httpRequest.getRequestURI().matches("(/a)?/projects/[^/]+$")
+ return httpRequest.getRequestURI().endsWith(String.format("%s~delete-project", pluginName))
&& "DELETE".equals(httpRequest.getMethod());
}
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/UpdateHeadAction.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/UpdateHeadAction.java
index 4195435..ae9c072 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/UpdateHeadAction.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/UpdateHeadAction.java
@@ -14,66 +14,28 @@
package com.googlesource.gerrit.plugins.replication.pull.api;
-import com.google.common.base.Strings;
-import com.google.gerrit.entities.RefNames;
import com.google.gerrit.extensions.api.projects.HeadInput;
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.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.server.permissions.PermissionBackend;
-import com.google.gerrit.server.permissions.RefPermission;
import com.google.gerrit.server.project.ProjectResource;
+import com.google.gerrit.server.restapi.project.SetHead;
import com.google.inject.Inject;
-import com.google.inject.Singleton;
-import com.googlesource.gerrit.plugins.replication.LocalFS;
-import com.googlesource.gerrit.plugins.replication.pull.GerritConfigOps;
-import java.util.Optional;
-import org.eclipse.jgit.transport.URIish;
-@Singleton
public class UpdateHeadAction implements RestModifyView<ProjectResource, HeadInput> {
- private final GerritConfigOps gerritConfigOps;
- private final PermissionBackend permissionBackend;
+
+ private final SetHead setHead;
@Inject
- UpdateHeadAction(GerritConfigOps gerritConfigOps, PermissionBackend permissionBackend) {
- this.gerritConfigOps = gerritConfigOps;
- this.permissionBackend = permissionBackend;
+ public UpdateHeadAction(SetHead setHead) {
+ this.setHead = setHead;
}
@Override
- public Response<?> apply(ProjectResource projectResource, HeadInput input)
+ public Response<?> apply(ProjectResource resource, HeadInput input)
throws AuthException, BadRequestException, ResourceConflictException, Exception {
- if (input == null || Strings.isNullOrEmpty(input.ref)) {
- throw new BadRequestException("ref required");
- }
- String ref = RefNames.fullName(input.ref);
-
- permissionBackend
- .user(projectResource.getUser())
- .project(projectResource.getNameKey())
- .ref(ref)
- .check(RefPermission.SET_HEAD);
-
- // TODO: the .git suffix should not be added here, but rather it should be
- // dealt with by the caller, honouring the naming style from the
- // replication.config (Issue 15221)
- Optional<URIish> maybeRepo =
- gerritConfigOps.getGitRepositoryURI(String.format("%s.git", projectResource.getName()));
-
- if (maybeRepo.isPresent()) {
- if (new LocalFS(maybeRepo.get()).updateHead(projectResource.getNameKey(), ref)) {
- return Response.ok(ref);
- }
- throw new UnprocessableEntityException(
- String.format(
- "Could not update HEAD of repo %s to ref %s", projectResource.getName(), ref));
- }
- throw new ResourceNotFoundException(
- String.format("Could not compute URL for repo: %s", projectResource.getName()));
+ return setHead.apply(resource, input);
}
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClient.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClient.java
index df97609..a916a7c 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClient.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClient.java
@@ -190,10 +190,10 @@
}
String getProjectDeletionUrl(String projectName) {
- return String.format("a/projects/%s", Url.encode(projectName));
+ return String.format("a/projects/%s/%s~delete-project", Url.encode(projectName), pluginName);
}
String getProjectUpdateHeadUrl(String projectName) {
- return String.format("a/projects/%s/HEAD", Url.encode(projectName));
+ return String.format("a/projects/%s/%s~HEAD", Url.encode(projectName), pluginName);
}
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/filter/ExcludedRefsFilter.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/filter/ExcludedRefsFilter.java
index f2e4ab3..63755b4 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/filter/ExcludedRefsFilter.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/filter/ExcludedRefsFilter.java
@@ -24,21 +24,8 @@
@Singleton
public class ExcludedRefsFilter extends RefsFilter {
- @Inject
- public ExcludedRefsFilter(ReplicationConfig replicationConfig) {
- super(replicationConfig);
- }
-
- @Override
- protected List<String> getRefNamePatterns(Config cfg) {
- return ImmutableList.<String>builder()
- .addAll(getDefaultExcludeRefPatterns())
- .addAll(ImmutableList.copyOf(cfg.getStringList("replication", null, "excludeRefs")))
- .build();
- }
-
- private List<String> getDefaultExcludeRefPatterns() {
- return ImmutableList.of(
+ public static String[] DEFAULT_REPLICATION_EXCLUDE_REFS =
+ new String[] {
RefNames.REFS_USERS + "*",
RefNames.REFS_CONFIG,
RefNames.REFS_SEQUENCES + "*",
@@ -46,6 +33,20 @@
RefNames.REFS_GROUPS + "*",
RefNames.REFS_GROUPNAMES,
RefNames.REFS_CACHE_AUTOMERGE + "*",
- RefNames.REFS_STARRED_CHANGES + "*");
+ RefNames.REFS_STARRED_CHANGES + "*"
+ };
+
+ @Inject
+ public ExcludedRefsFilter(ReplicationConfig replicationConfig) {
+ super(replicationConfig);
+ }
+
+ @Override
+ protected List<String> getRefNamePatterns(Config cfg) {
+ String[] replicationExcludeRefs = cfg.getStringList("replication", null, "excludeRefs");
+ if (replicationExcludeRefs.length == 0) {
+ replicationExcludeRefs = DEFAULT_REPLICATION_EXCLUDE_REFS;
+ }
+ return ImmutableList.copyOf(replicationExcludeRefs);
}
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/filter/RefsFilter.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/filter/RefsFilter.java
index 7ec19ea..7ab44f9 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/filter/RefsFilter.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/filter/RefsFilter.java
@@ -48,9 +48,6 @@
throw new IllegalArgumentException(
String.format("Ref name cannot be null or empty, but was %s", refName));
}
- if (refsPatterns.isEmpty()) {
- return true;
- }
for (String pattern : refsPatterns) {
if (matchesPattern(refName, pattern)) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/filter/SyncRefsFilter.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/filter/SyncRefsFilter.java
index a069935..7d004f5 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/filter/SyncRefsFilter.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/filter/SyncRefsFilter.java
@@ -30,6 +30,10 @@
@Override
protected List<String> getRefNamePatterns(Config cfg) {
- return ImmutableList.copyOf(cfg.getStringList("replication", null, "syncRefs"));
+ String[] replicationSyncRefs = cfg.getStringList("replication", null, "syncRefs");
+ if (replicationSyncRefs.length == 0) {
+ replicationSyncRefs = new String[] {"*"};
+ }
+ return ImmutableList.copyOf(replicationSyncRefs);
}
}
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index 68c9001..b2470a4 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -169,7 +169,7 @@
treated as single ref matches. So `foo/bar` matches only
the ref `foo/bar`, but no other refs.
- Following refs are always excluded from the git fetch calls:
+ By default, the following refs are excluded from the git fetch calls:
- refs/users/*
- refs/meta/config
- refs/sequences/*
@@ -179,8 +179,6 @@
- refs/cache-automerge/*
- refs/starred-changes/*
- By default, all other refs are included.
-
Note that if you are using @PLUGIN@ together with multi-site, you should
explicitly exclude `refs/multi-site/version` from being replicated.
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationIT.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationIT.java
index c2335e8..c5b1790 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/PullReplicationIT.java
@@ -15,7 +15,6 @@
package com.googlesource.gerrit.plugins.replication.pull;
import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.server.project.ProjectResource.PROJECT_KIND;
import static java.util.stream.Collectors.toList;
import com.google.common.flogger.FluentLogger;
@@ -38,11 +37,9 @@
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.extensions.restapi.RestApiModule;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.server.config.SitePaths;
import com.google.gerrit.server.project.ProjectResource;
-import com.google.inject.AbstractModule;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.googlesource.gerrit.plugins.replication.AutoReloadConfigDecorator;
@@ -83,28 +80,10 @@
private FileBasedConfig config;
private FileBasedConfig secureConfig;
- static FakeDeleteProjectPlugin fakeDeleteProjectPlugin;
-
- static class FakeDeleteModule extends AbstractModule {
-
- @Override
- public void configure() {
- install(
- new RestApiModule() {
- @Override
- public void configure() {
- fakeDeleteProjectPlugin = new FakeDeleteProjectPlugin();
- delete(PROJECT_KIND).toInstance(fakeDeleteProjectPlugin);
- }
- });
- }
- }
-
@Override
public void setUpTestPlugin() throws Exception {
gitPath = sitePaths.site_path.resolve("git");
- installPlugin("fakeDeleteProjectPlugin", FakeDeleteModule.class, null, null);
config =
new FileBasedConfig(sitePaths.etc_dir.resolve("replication.config").toFile(), FS.DETECTED);
setReplicationSource(
@@ -278,12 +257,11 @@
return NotifyHandling.NONE;
}
};
-
for (ProjectDeletedListener l : deletedListeners) {
l.onProjectDeleted(event);
}
- waitUntil(() -> fakeDeleteProjectPlugin.getDeleteEndpointCalls() == 1);
+ waitUntil(() -> !repoManager.list().contains(project));
}
@Test
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/ReplicationQueueTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/ReplicationQueueTest.java
index 78eb3f7..8402782 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/ReplicationQueueTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/ReplicationQueueTest.java
@@ -137,7 +137,7 @@
public void shouldNotCallInitProjectWhenReplicateNewRepositoriesNotSet() throws IOException {
Event event = new TestEvent("refs/changes/01/1/meta");
when(httpResult.isSuccessful()).thenReturn(false);
-
+ when(httpResult.isProjectMissing(any())).thenReturn(true);
when(source.isCreateMissingRepositories()).thenReturn(false);
objectUnderTest.start();
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ActionITBase.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ActionITBase.java
index 343ce66..34aaf8b 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ActionITBase.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ActionITBase.java
@@ -127,7 +127,6 @@
protected HttpDelete createDeleteRequest() {
HttpDelete delete = new HttpDelete(url);
- delete.addHeader(new BasicHeader("Content-Type", "application/json"));
return delete;
}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectDeletionActionIT.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectDeletionActionIT.java
index 4c09266..f5c8d4c 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectDeletionActionIT.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/ProjectDeletionActionIT.java
@@ -31,6 +31,64 @@
@Inject private ProjectOperations projectOperations;
@Test
+ public void shouldReturnUnauthorizedForUserWithoutPermissions() throws Exception {
+ httpClientFactory
+ .create(source)
+ .execute(
+ createDeleteRequest(),
+ assertHttpResponseCode(HttpServletResponse.SC_UNAUTHORIZED),
+ getAnonymousContext());
+ }
+
+ @Test
+ public void shouldDeleteRepositoryWhenUserHasProjectDeletionCapabilities() throws Exception {
+ String testProjectName = project.get();
+ url = getURL(testProjectName);
+ httpClientFactory
+ .create(source)
+ .execute(
+ createDeleteRequest(),
+ assertHttpResponseCode(HttpServletResponse.SC_FORBIDDEN),
+ getUserContext());
+
+ projectOperations
+ .project(allProjects)
+ .forUpdate()
+ .add(allowCapability(DELETE_PROJECT_PERMISSION).group(SystemGroupBackend.REGISTERED_USERS))
+ .update();
+
+ httpClientFactory
+ .create(source)
+ .execute(
+ createDeleteRequest(),
+ assertHttpResponseCode(HttpServletResponse.SC_OK),
+ getUserContext());
+ }
+
+ @Test
+ public void shouldReturnOKWhenProjectIsDeleted() throws Exception {
+ String testProjectName = project.get();
+ url = getURL(testProjectName);
+
+ httpClientFactory
+ .create(source)
+ .execute(
+ createDeleteRequest(), assertHttpResponseCode(HttpServletResponse.SC_OK), getContext());
+ }
+
+ @Test
+ public void shouldReturnInternalServerErrorIfProjectCannotBeDeleted() throws Exception {
+ url = getURL(INVALID_TEST_PROJECT_NAME);
+
+ httpClientFactory
+ .create(source)
+ .execute(
+ createDeleteRequest(),
+ assertHttpResponseCode(HttpServletResponse.SC_INTERNAL_SERVER_ERROR),
+ getContext());
+ }
+
+ @Test
@GerritConfig(name = "container.replica", value = "true")
public void shouldReturnUnauthorizedForUserWithoutPermissionsOnReplica() throws Exception {
httpClientFactory
@@ -96,6 +154,8 @@
@Override
protected String getURL(String projectName) {
- return String.format("%s/a/projects/%s", adminRestSession.url(), Url.encode(projectName));
+ return String.format(
+ "%s/a/projects/%s/pull-replication~delete-project",
+ adminRestSession.url(), Url.encode(projectName));
}
}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClientTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClientTest.java
index ed1eef4..a778049 100644
--- a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClientTest.java
+++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/client/FetchRestApiClientTest.java
@@ -376,7 +376,8 @@
HttpDelete httpDelete = httpDeleteCaptor.getValue();
assertThat(httpDelete.getURI().getHost()).isEqualTo("gerrit-host");
- assertThat(httpDelete.getURI().getPath()).isEqualTo("/a/projects/test_repo");
+ assertThat(httpDelete.getURI().getPath())
+ .isEqualTo("/a/projects/test_repo/pull-replication~delete-project");
}
@Test
@@ -394,7 +395,7 @@
assertThat(httpPut.getURI().getHost()).isEqualTo("gerrit-host");
assertThat(httpPut.getURI().getPath())
- .isEqualTo(String.format("/a/projects/%s/HEAD", projectName));
+ .isEqualTo(String.format("/a/projects/%s/pull-replication~HEAD", projectName));
assertThat(payload).isEqualTo(String.format("{\"ref\": \"%s\"}", newHead));
}