Support triggering GC for one project via REST

The Git garbage collection for one project can now be triggered by
POST on '/projects/*/gc'.

Change-Id: I6d45f33196f3f356648c817d9a2d8465de49bb5c
Signed-off-by: Edwin Kempin <edwin.kempin@sap.com>
This commit is contained in:
Edwin Kempin 2013-03-19 13:31:49 +01:00 committed by Shawn Pearce
parent 19ea9b9733
commit ef3542fb35
9 changed files with 345 additions and 44 deletions

View File

@ -408,6 +408,45 @@ link:#repository-statistics-info[RepositoryStatisticsInfo] entity.
} }
---- ----
[[run-gc]]
Run GC
~~~~~~
[verse]
'POST /projects/link:#project-name[\{project-name\}]/gc'
Run the Git garbage collection for the repository of a project.
.Request
----
POST /projects/plugins%2Freplication/gc HTTP/1.0
----
The response is the streamed output of the garbage collection.
.Response
----
HTTP/1.1 200 OK
Content-Disposition: attachment
Content-Type: text/plain;charset=UTF-8
collecting garbage for "plugins/replication":
Pack refs: 100% (21/21)
Counting objects: 20
Finding sources: 100% (20/20)
Getting sizes: 100% (13/13)
Compressing objects: 83% (5/6)
Writing objects: 100% (20/20)
Selecting commits: 100% (7/7)
Building bitmaps: 100% (7/7)
Finding sources: 100% (41/41)
Getting sizes: 100% (25/25)
Compressing objects: 52% (12/23)
Writing objects: 100% (41/41)
Prune loose objects also found in pack files: 100% (36/36)
Prune loose, unreferenced objects: 100% (36/36)
done.
----
[[dashboard-endpoints]] [[dashboard-endpoints]]
Dashboard Endpoints Dashboard Endpoints
------------------- -------------------

View File

@ -0,0 +1,70 @@
// Copyright (C) 2013 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;
import static org.junit.Assert.assertTrue;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.inject.Inject;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.Repository;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
public class GcAssert {
private final GitRepositoryManager repoManager;
@Inject
public GcAssert(GitRepositoryManager repoManager) {
this.repoManager = repoManager;
}
public void assertHasPackFile(Project.NameKey... projects)
throws RepositoryNotFoundException, IOException {
for (Project.NameKey p : projects) {
assertTrue("Project " + p.get() + " has no pack files.",
getPackFiles(p).length > 0);
}
}
public void assertHasNoPackFile(Project.NameKey... projects)
throws RepositoryNotFoundException, IOException {
for (Project.NameKey p : projects) {
assertTrue("Project " + p.get() + " has pack files.",
getPackFiles(p).length == 0);
}
}
private String[] getPackFiles(Project.NameKey p)
throws RepositoryNotFoundException, IOException {
Repository repo = repoManager.openRepository(p);
try {
File packDir = new File(repo.getDirectory(), "objects/pack");
return packDir.list(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return name.endsWith(".pack");
}
});
} finally {
repo.close();
}
}
}

View File

@ -56,6 +56,10 @@ public class RestSession {
return new RestResponse(getClient().execute(put)); return new RestResponse(getClient().execute(put));
} }
public RestResponse post(String endPoint) throws IOException {
return post(endPoint, null);
}
public RestResponse post(String endPoint, Object content) throws IOException { public RestResponse post(String endPoint, Object content) throws IOException {
HttpPost post = new HttpPost("http://localhost:8080/a" + endPoint); HttpPost post = new HttpPost("http://localhost:8080/a" + endPoint);
if (content != null) { if (content != null) {

View File

@ -0,0 +1,102 @@
// Copyright (C) 2013 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.project;
import static com.google.gerrit.acceptance.git.GitUtil.createProject;
import static org.junit.Assert.assertEquals;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.AccountCreator;
import com.google.gerrit.acceptance.GcAssert;
import com.google.gerrit.acceptance.RestResponse;
import com.google.gerrit.acceptance.RestSession;
import com.google.gerrit.acceptance.SshSession;
import com.google.gerrit.acceptance.TestAccount;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.git.GarbageCollectionQueue;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.jcraft.jsch.JSchException;
import org.apache.http.HttpStatus;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
public class GarbageCollectionIT extends AbstractDaemonTest {
@Inject
private AccountCreator accounts;
@Inject
private AllProjectsName allProjects;
@Inject
private GarbageCollectionQueue gcQueue;
@Inject
private GcAssert gcAssert;
private TestAccount admin;
private RestSession session;
private Project.NameKey project1;
private Project.NameKey project2;
@Before
public void setUp() throws Exception {
admin =
accounts.create("admin", "admin@example.com", "Administrator",
"Administrators");
SshSession sshSession = new SshSession(admin);
project1 = new Project.NameKey("p1");
createProject(sshSession, project1.get());
project2 = new Project.NameKey("p2");
createProject(sshSession, project2.get());
session = new RestSession(admin);
}
@Test
public void testGcNonExistingProject_NotFound() throws IOException {
assertEquals(HttpStatus.SC_NOT_FOUND, POST("/projects/non-existing/gc"));
}
@Test
public void testGcNotAllowed_Forbidden() throws IOException, OrmException, JSchException {
assertEquals(HttpStatus.SC_FORBIDDEN,
new RestSession(accounts.create("user", "user@example.com", "User"))
.post("/projects/" + allProjects.get() + "/gc").getStatusCode());
}
@Test
public void testGcOneProject() throws JSchException, IOException {
assertEquals(HttpStatus.SC_OK, POST("/projects/" + allProjects.get() + "/gc"));
gcAssert.assertHasPackFile(allProjects);
gcAssert.assertHasNoPackFile(project1, project2);
}
private int POST(String endPoint) throws IOException {
RestResponse r = session.post(endPoint);
r.consume();
return r.getStatusCode();
}
}

View File

@ -21,6 +21,7 @@ import static org.junit.Assert.assertTrue;
import com.google.gerrit.acceptance.AbstractDaemonTest; import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.AccountCreator; import com.google.gerrit.acceptance.AccountCreator;
import com.google.gerrit.acceptance.GcAssert;
import com.google.gerrit.acceptance.SshSession; import com.google.gerrit.acceptance.SshSession;
import com.google.gerrit.acceptance.TestAccount; import com.google.gerrit.acceptance.TestAccount;
import com.google.gerrit.common.data.GarbageCollectionResult; import com.google.gerrit.common.data.GarbageCollectionResult;
@ -28,19 +29,14 @@ import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.config.AllProjectsName; import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.git.GarbageCollection; import com.google.gerrit.server.git.GarbageCollection;
import com.google.gerrit.server.git.GarbageCollectionQueue; import com.google.gerrit.server.git.GarbageCollectionQueue;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gwtorm.server.OrmException; import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject; import com.google.inject.Inject;
import com.jcraft.jsch.JSchException; import com.jcraft.jsch.JSchException;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.Repository;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
import java.util.Locale; import java.util.Locale;
@ -50,9 +46,6 @@ public class GarbageCollectionIT extends AbstractDaemonTest {
@Inject @Inject
private AccountCreator accounts; private AccountCreator accounts;
@Inject
private GitRepositoryManager repoManager;
@Inject @Inject
private AllProjectsName allProjects; private AllProjectsName allProjects;
@ -62,6 +55,9 @@ public class GarbageCollectionIT extends AbstractDaemonTest {
@Inject @Inject
private GarbageCollectionQueue gcQueue; private GarbageCollectionQueue gcQueue;
@Inject
private GcAssert gcAssert;
private TestAccount admin; private TestAccount admin;
private SshSession sshSession; private SshSession sshSession;
private Project.NameKey project1; private Project.NameKey project1;
@ -93,8 +89,8 @@ public class GarbageCollectionIT extends AbstractDaemonTest {
+ project2.get() + "\""); + project2.get() + "\"");
assertFalse(sshSession.hasError()); assertFalse(sshSession.hasError());
assertNoError(response); assertNoError(response);
assertHasPackFile(project1, project2); gcAssert.assertHasPackFile(project1, project2);
assertHasNoPackFile(allProjects, project3); gcAssert.assertHasNoPackFile(allProjects, project3);
} }
@Test @Test
@ -102,7 +98,7 @@ public class GarbageCollectionIT extends AbstractDaemonTest {
String response = sshSession.exec("gerrit gc --all"); String response = sshSession.exec("gerrit gc --all");
assertFalse(sshSession.hasError()); assertFalse(sshSession.hasError());
assertNoError(response); assertNoError(response);
assertHasPackFile(allProjects, project1, project2, project3); gcAssert.assertHasPackFile(allProjects, project1, project2, project3);
} }
@Test @Test
@ -132,36 +128,4 @@ public class GarbageCollectionIT extends AbstractDaemonTest {
private void assertNoError(String response) { private void assertNoError(String response) {
assertFalse(response, response.toLowerCase(Locale.US).contains("error")); assertFalse(response, response.toLowerCase(Locale.US).contains("error"));
} }
private void assertHasPackFile(Project.NameKey... projects)
throws RepositoryNotFoundException, IOException {
for (Project.NameKey p : projects) {
assertTrue("Project " + p.get() + "has no pack files.",
getPackFiles(p).length > 0);
}
}
private void assertHasNoPackFile(Project.NameKey... projects)
throws RepositoryNotFoundException, IOException {
for (Project.NameKey p : projects) {
assertTrue("Project " + p.get() + "has pack files.",
getPackFiles(p).length == 0);
}
}
private String[] getPackFiles(Project.NameKey p)
throws RepositoryNotFoundException, IOException {
Repository repo = repoManager.openRepository(p);
try {
File packDir = new File(repo.getDirectory(), "objects/pack");
return packDir.list(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return name.endsWith(".pack");
}
});
} finally {
repo.close();
}
}
} }

View File

@ -0,0 +1,23 @@
// Copyright (C) 2013 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;
import java.io.IOException;
import java.io.OutputStream;
public interface StreamingResponse {
public String getContentType();
public void stream(OutputStream out) throws IOException;
}

View File

@ -61,6 +61,7 @@ import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.extensions.restapi.RestReadView; import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.extensions.restapi.RestResource; import com.google.gerrit.extensions.restapi.RestResource;
import com.google.gerrit.extensions.restapi.RestView; import com.google.gerrit.extensions.restapi.RestView;
import com.google.gerrit.extensions.restapi.StreamingResponse;
import com.google.gerrit.extensions.restapi.TopLevelResource; import com.google.gerrit.extensions.restapi.TopLevelResource;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException; import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.httpd.WebSession; import com.google.gerrit.httpd.WebSession;
@ -301,7 +302,11 @@ public class RestApiServlet extends HttpServlet {
} }
res.setStatus(status); res.setStatus(status);
if (result != Response.none()) { if (result instanceof StreamingResponse) {
StreamingResponse r = (StreamingResponse) result;
res.setContentType(r.getContentType());
r.stream(res.getOutputStream());
} else if (result != Response.none()) {
result = Response.unwrap(result); result = Response.unwrap(result);
if (result instanceof BinaryResult) { if (result instanceof BinaryResult) {
replyBinaryResult(req, res, (BinaryResult) result); replyBinaryResult(req, res, (BinaryResult) result);

View File

@ -0,0 +1,93 @@
// Copyright (C) 2013 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.server.project;
import com.google.common.base.Charsets;
import com.google.gerrit.common.data.GarbageCollectionResult;
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.extensions.annotations.RequiresCapability;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.extensions.restapi.StreamingResponse;
import com.google.gerrit.server.git.GarbageCollection;
import com.google.gerrit.server.project.GarbageCollect.Input;
import com.google.inject.Inject;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.util.Collections;
@RequiresCapability(GlobalCapability.RUN_GC)
public class GarbageCollect implements RestModifyView<ProjectResource, Input> {
public static class Input {
}
private GarbageCollection.Factory garbageCollectionFactory;
@Inject
GarbageCollect(GarbageCollection.Factory garbageCollectionFactory) {
this.garbageCollectionFactory = garbageCollectionFactory;
}
@Override
public StreamingResponse apply(final ProjectResource rsrc, Input input) {
return new StreamingResponse() {
@Override
public String getContentType() {
return "text/plain;charset=UTF-8";
}
@Override
public void stream(OutputStream out) throws IOException {
PrintWriter writer = new PrintWriter(
new OutputStreamWriter(out, Charsets.UTF_8)) {
@Override
public void println() {
write('\n');
}
};
try {
GarbageCollectionResult result = garbageCollectionFactory.create().run(
Collections.singletonList(rsrc.getNameKey()), writer);
if (result.hasErrors()) {
for (GarbageCollectionResult.Error e : result.getErrors()) {
String msg;
switch (e.getType()) {
case REPOSITORY_NOT_FOUND:
msg = "error: project \"" + e.getProjectName() + "\" not found";
break;
case GC_ALREADY_SCHEDULED:
msg = "error: garbage collection for project \""
+ e.getProjectName() + "\" was already scheduled";
break;
case GC_FAILED:
msg = "error: garbage collection for project \"" + e.getProjectName()
+ "\" failed";
break;
default:
msg = "error: garbage collection for project \"" + e.getProjectName()
+ "\" failed: " + e.getType();
}
writer.println(msg);
}
}
} finally {
writer.flush();
}
}
};
}
}

View File

@ -44,6 +44,7 @@ public class Module extends RestApiModule {
put(PROJECT_KIND, "HEAD").to(SetHead.class); put(PROJECT_KIND, "HEAD").to(SetHead.class);
get(PROJECT_KIND, "statistics.git").to(GetStatistics.class); get(PROJECT_KIND, "statistics.git").to(GetStatistics.class);
post(PROJECT_KIND, "gc").to(GarbageCollect.class);
child(PROJECT_KIND, "dashboards").to(DashboardsCollection.class); child(PROJECT_KIND, "dashboards").to(DashboardsCollection.class);
get(DASHBOARD_KIND).to(GetDashboard.class); get(DASHBOARD_KIND).to(GetDashboard.class);