diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt index c463d4e62b..9aba0e9133 100644 --- a/Documentation/rest-api-projects.txt +++ b/Documentation/rest-api-projects.txt @@ -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 ------------------- diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GcAssert.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GcAssert.java new file mode 100644 index 0000000000..4c7289b270 --- /dev/null +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GcAssert.java @@ -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(); + } + } +} diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/RestSession.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/RestSession.java index 929be84c71..2bf65234cc 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/RestSession.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/RestSession.java @@ -56,6 +56,10 @@ public class RestSession { 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 { HttpPost post = new HttpPost("http://localhost:8080/a" + endPoint); if (content != null) { diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GarbageCollectionIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GarbageCollectionIT.java new file mode 100644 index 0000000000..e55c52a3d2 --- /dev/null +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GarbageCollectionIT.java @@ -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(); + } +} diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/GarbageCollectionIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/GarbageCollectionIT.java index 8c934fba0b..9c1e7d03e1 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/GarbageCollectionIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/GarbageCollectionIT.java @@ -21,6 +21,7 @@ import static org.junit.Assert.assertTrue; import com.google.gerrit.acceptance.AbstractDaemonTest; import com.google.gerrit.acceptance.AccountCreator; +import com.google.gerrit.acceptance.GcAssert; import com.google.gerrit.acceptance.SshSession; import com.google.gerrit.acceptance.TestAccount; 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.git.GarbageCollection; import com.google.gerrit.server.git.GarbageCollectionQueue; -import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.jcraft.jsch.JSchException; -import org.eclipse.jgit.errors.RepositoryNotFoundException; -import org.eclipse.jgit.lib.Repository; import org.junit.Before; import org.junit.Test; -import java.io.File; -import java.io.FilenameFilter; import java.io.IOException; import java.util.Arrays; import java.util.Locale; @@ -50,9 +46,6 @@ public class GarbageCollectionIT extends AbstractDaemonTest { @Inject private AccountCreator accounts; - @Inject - private GitRepositoryManager repoManager; - @Inject private AllProjectsName allProjects; @@ -62,6 +55,9 @@ public class GarbageCollectionIT extends AbstractDaemonTest { @Inject private GarbageCollectionQueue gcQueue; + @Inject + private GcAssert gcAssert; + private TestAccount admin; private SshSession sshSession; private Project.NameKey project1; @@ -93,8 +89,8 @@ public class GarbageCollectionIT extends AbstractDaemonTest { + project2.get() + "\""); assertFalse(sshSession.hasError()); assertNoError(response); - assertHasPackFile(project1, project2); - assertHasNoPackFile(allProjects, project3); + gcAssert.assertHasPackFile(project1, project2); + gcAssert.assertHasNoPackFile(allProjects, project3); } @Test @@ -102,7 +98,7 @@ public class GarbageCollectionIT extends AbstractDaemonTest { String response = sshSession.exec("gerrit gc --all"); assertFalse(sshSession.hasError()); assertNoError(response); - assertHasPackFile(allProjects, project1, project2, project3); + gcAssert.assertHasPackFile(allProjects, project1, project2, project3); } @Test @@ -132,36 +128,4 @@ public class GarbageCollectionIT extends AbstractDaemonTest { private void assertNoError(String response) { 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(); - } - } } diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/StreamingResponse.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/StreamingResponse.java new file mode 100644 index 0000000000..b2fb901075 --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/StreamingResponse.java @@ -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; +} diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java index 23f678024b..a2aa191369 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java @@ -61,6 +61,7 @@ import com.google.gerrit.extensions.restapi.RestModifyView; import com.google.gerrit.extensions.restapi.RestReadView; import com.google.gerrit.extensions.restapi.RestResource; 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.UnprocessableEntityException; import com.google.gerrit.httpd.WebSession; @@ -301,7 +302,11 @@ public class RestApiServlet extends HttpServlet { } 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); if (result instanceof BinaryResult) { replyBinaryResult(req, res, (BinaryResult) result); diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GarbageCollect.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GarbageCollect.java new file mode 100644 index 0000000000..d4d923e48d --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GarbageCollect.java @@ -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 { + 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(); + } + } + }; + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java index a0ba4f3b86..94e316201c 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java @@ -44,6 +44,7 @@ public class Module extends RestApiModule { put(PROJECT_KIND, "HEAD").to(SetHead.class); get(PROJECT_KIND, "statistics.git").to(GetStatistics.class); + post(PROJECT_KIND, "gc").to(GarbageCollect.class); child(PROJECT_KIND, "dashboards").to(DashboardsCollection.class); get(DASHBOARD_KIND).to(GetDashboard.class);