Merge branch 'stable-2.8'

* stable-2.8:
  Correct Gerrit releases download link in installation documentation
  Make the documentation embedding work.
  Update download-commands plugin
  Check preconditions when setting parent project through REST
  Add acceptance tests for setting parent project via REST
  Update release website index to use new documentation site

Change-Id: I0c387d621920913d947ee45da046c9168c0db499
diff --git a/Documentation/asciidoc.defs b/Documentation/asciidoc.defs
index e2de785..44313d2 100644
--- a/Documentation/asciidoc.defs
+++ b/Documentation/asciidoc.defs
@@ -23,6 +23,7 @@
   EXPN = '.expn'
 
   asciidoc = [
+      'cd $SRCDIR;',
       '$(exe //lib/asciidoctor:asciidoc)',
       '-z', '$OUT',
       '--in-ext', '".txt%s"' % EXPN,
@@ -33,7 +34,7 @@
   for attribute in attributes:
     asciidoc.extend(['-a', attribute])
   asciidoc.append('$SRCS')
-  newsrcs = []
+  newsrcs = ["doc.css"]
   newdeps = deps + ['//lib/asciidoctor:asciidoc']
 
   for src in srcs:
diff --git a/Documentation/install.txt b/Documentation/install.txt
index 8900442..e7bf50e 100644
--- a/Documentation/install.txt
+++ b/Documentation/install.txt
@@ -18,9 +18,8 @@
 ---------------
 
 Current and past binary releases of Gerrit can be obtained from
-the downloads page at the project site:
-
-* http://code.google.com/p/gerrit/downloads/list[Gerrit Downloads]
+the link:https://gerrit-releases.storage.googleapis.com/index.html[
+Gerrit Releases site].
 
 Download any current `*.war` package. The war will be referred to as
 `gerrit.war` from this point forward, so you may find it easier to
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/SetParentIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/SetParentIT.java
new file mode 100644
index 0000000..abc3bb6
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/SetParentIT.java
@@ -0,0 +1,155 @@
+// 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 com.google.gerrit.acceptance.git.GitUtil.initSsh;
+import static org.junit.Assert.assertEquals;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.AccountCreator;
+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.AllProjectsNameProvider;
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import com.google.inject.Inject;
+
+import com.jcraft.jsch.JSchException;
+
+import org.apache.http.HttpStatus;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+
+public class SetParentIT extends AbstractDaemonTest {
+
+  @Inject
+  private AccountCreator accounts;
+
+  @Inject
+  private AllProjectsNameProvider allProjects;
+
+  private RestSession adminSession;
+  private RestSession userSession;
+  private SshSession sshSession;
+
+  private String project;
+
+  @Before
+  public void setUp() throws Exception {
+    TestAccount admin = accounts.admin();
+    adminSession = new RestSession(server, admin);
+
+    TestAccount user = accounts.create("user", "user@example.com", "User");
+    userSession = new RestSession(server, user);
+
+
+    initSsh(admin);
+    sshSession = new SshSession(server, admin);
+    project = "p";
+    createProject(sshSession, project, null, true);
+  }
+
+  @After
+  public void cleanup() {
+    sshSession.close();
+  }
+
+  @Test
+  public void setParent_Forbidden() throws IOException, JSchException {
+    String parent = "parent";
+    createProject(sshSession, parent, null, true);
+    RestResponse r =
+        userSession.put("/projects/" + project + "/parent",
+            new ParentInput(parent));
+    assertEquals(HttpStatus.SC_FORBIDDEN, r.getStatusCode());
+    r.consume();
+  }
+
+  @Test
+  public void setParent() throws IOException, JSchException {
+    String parent = "parent";
+    createProject(sshSession, parent, null, true);
+    RestResponse r =
+        adminSession.put("/projects/" + project + "/parent",
+            new ParentInput(parent));
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    r.consume();
+
+    r = adminSession.get("/projects/" + project + "/parent");
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    String newParent =
+        (new Gson()).fromJson(r.getReader(),
+            new TypeToken<String>() {}.getType());
+    assertEquals(parent, newParent);
+    r.consume();
+  }
+
+  @Test
+  public void setParentForAllProjects_Conflict() throws IOException {
+    RestResponse r =
+        adminSession.put("/projects/" + allProjects.get() + "/parent",
+            new ParentInput(project));
+    assertEquals(HttpStatus.SC_CONFLICT, r.getStatusCode());
+    r.consume();
+  }
+
+  @Test
+  public void setInvalidParent_Conflict() throws IOException, JSchException {
+    RestResponse r =
+        adminSession.put("/projects/" + project + "/parent",
+            new ParentInput(project));
+    assertEquals(HttpStatus.SC_CONFLICT, r.getStatusCode());
+    r.consume();
+
+    String child = "child";
+    createProject(sshSession, child, new Project.NameKey(project), true);
+    r = adminSession.put("/projects/" + project + "/parent",
+           new ParentInput(child));
+    assertEquals(HttpStatus.SC_CONFLICT, r.getStatusCode());
+    r.consume();
+
+    String grandchild = "grandchild";
+    createProject(sshSession, grandchild, new Project.NameKey(child), true);
+    r = adminSession.put("/projects/" + project + "/parent",
+           new ParentInput(grandchild));
+    assertEquals(HttpStatus.SC_CONFLICT, r.getStatusCode());
+    r.consume();
+  }
+
+  @Test
+  public void setNonExistingParent_UnprocessibleEntity() throws IOException {
+    RestResponse r =
+        adminSession.put("/projects/" + project + "/parent",
+            new ParentInput("non-existing"));
+    assertEquals(HttpStatus.SC_UNPROCESSABLE_ENTITY, r.getStatusCode());
+    r.consume();
+  }
+
+  @SuppressWarnings("unused")
+  private static class ParentInput {
+    String parent;
+
+    ParentInput(String parent) {
+      this.parent = parent;
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetParent.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetParent.java
index c1fb5de..999358c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetParent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetParent.java
@@ -15,13 +15,16 @@
 package com.google.gerrit.server.project;
 
 import com.google.common.base.Objects;
+import com.google.common.base.Predicate;
 import com.google.common.base.Strings;
+import com.google.common.collect.Iterables;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.DefaultInput;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.config.AllProjectsName;
@@ -33,6 +36,8 @@
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 
+import java.io.IOException;
+
 class SetParent implements RestModifyView<ProjectResource, Input> {
   static class Input {
     @DefaultInput
@@ -54,21 +59,45 @@
   }
 
   @Override
-  public String apply(ProjectResource resource, Input input)
-      throws AuthException, BadRequestException, ResourceConflictException,
-      Exception {
-    ProjectControl ctl = resource.getControl();
+  public String apply(final ProjectResource rsrc, Input input) throws AuthException,
+      BadRequestException, ResourceConflictException,
+      ResourceNotFoundException, UnprocessableEntityException, IOException {
+    ProjectControl ctl = rsrc.getControl();
     IdentifiedUser user = (IdentifiedUser) ctl.getCurrentUser();
     if (!user.getCapabilities().canAdministrateServer()) {
       throw new AuthException("not administrator");
     }
 
+    if (rsrc.getNameKey().equals(allProjects)) {
+      throw new ResourceConflictException("cannot set parent of "
+          + allProjects.get());
+    }
+
+    input.parent = Strings.emptyToNull(input.parent);
+    if (input.parent != null) {
+      ProjectState parent = cache.get(new Project.NameKey(input.parent));
+      if (parent == null) {
+        throw new UnprocessableEntityException("parent project " + input.parent
+            + " not found");
+      }
+
+      if (Iterables.tryFind(parent.tree(), new Predicate<ProjectState>() {
+        @Override
+        public boolean apply(ProjectState input) {
+          return input.getProject().getNameKey().equals(rsrc.getNameKey());
+        }
+      }).isPresent()) {
+        throw new ResourceConflictException("cycle exists between "
+            + rsrc.getName() + " and " + parent.getProject().getName());
+      }
+    }
+
     try {
-      MetaDataUpdate md = updateFactory.create(resource.getNameKey());
+      MetaDataUpdate md = updateFactory.create(rsrc.getNameKey());
       try {
         ProjectConfig config = ProjectConfig.read(md);
         Project project = config.getProject();
-        project.setParentName(Strings.emptyToNull(input.parent));
+        project.setParentName(input.parent);
 
         String msg = Strings.emptyToNull(input.commitMessage);
         if (msg == null) {
@@ -89,7 +118,7 @@
         md.close();
       }
     } catch (RepositoryNotFoundException notFound) {
-      throw new ResourceNotFoundException(resource.getName());
+      throw new ResourceNotFoundException(rsrc.getName());
     } catch (ConfigInvalidException e) {
       throw new ResourceConflictException(String.format(
           "invalid project.config: %s", e.getMessage()));
diff --git a/lib/asciidoctor/java/AsciiDoctor.java b/lib/asciidoctor/java/AsciiDoctor.java
index a5bf492..0613ff4 100644
--- a/lib/asciidoctor/java/AsciiDoctor.java
+++ b/lib/asciidoctor/java/AsciiDoctor.java
@@ -29,6 +29,7 @@
 import org.asciidoctor.AttributesBuilder;
 import org.asciidoctor.Options;
 import org.asciidoctor.OptionsBuilder;
+import org.asciidoctor.SafeMode;
 import org.asciidoctor.internal.JRubyAsciidoctor;
 
 import org.kohsuke.args4j.Argument;
@@ -75,15 +76,16 @@
     return basename + outExt;
   }
 
-  private Options createOptions(File tmpFile) {
+  private Options createOptions(File outputFile) {
     OptionsBuilder optionsBuilder = OptionsBuilder.options();
 
-    optionsBuilder.backend(backend).docType(DOCTYPE).eruby(ERUBY);
+    optionsBuilder.backend(backend).docType(DOCTYPE).eruby(ERUBY)
+      .safe(SafeMode.UNSAFE);
     // XXX(fishywang): ideally we should just output to a string and add the
     // content into zip. But asciidoctor will actually ignore all attributes if
     // not output to a file. So we *have* to output to a file then read the
     // content of the file into zip.
-    optionsBuilder.toFile(tmpFile);
+    optionsBuilder.toFile(outputFile);
 
     AttributesBuilder attributesBuilder = AttributesBuilder.attributes();
     attributesBuilder.attributes(getAttributes());
@@ -127,12 +129,18 @@
 
     ZipOutputStream zip = new ZipOutputStream(new FileOutputStream(zipFile));
     for (String inputFile : inputFiles) {
-      File tmp = File.createTempFile("doc", ".html");
-      Options options = createOptions(tmp);
+      if (!inputFile.endsWith(inExt)) {
+        // We have to use UNSAFE mode in order to make embedding work. But in
+        // UNSAFE mode we'll also need css file in the same directory, so we
+        // have to add css files into the SRCS.
+        continue;
+      }
+      String outName = mapInFileToOutFile(inputFile, inExt, outExt);
+      File out = new File(outName);
+      Options options = createOptions(out);
       renderInput(options, inputFile);
 
-      String outputFile = mapInFileToOutFile(inputFile, inExt, outExt);
-      zipFile(tmp, outputFile, zip);
+      zipFile(out, outName, zip);
     }
     zip.close();
   }
diff --git a/website/releases/index.html b/website/releases/index.html
index 0ca0cb4..4a854d6 100644
--- a/website/releases/index.html
+++ b/website/releases/index.html
@@ -44,7 +44,7 @@
   var doc = document;
   var frg = doc.createDocumentFragment();
   var rx = /^gerrit(?:-full)?-([0-9.]+(?:-rc[0-9]+)?)[.]war/;
-  var rel = 'http://gerrit-documentation.googlecode.com/svn/ReleaseNotes/';
+  var docs = 'https://gerrit-documentation.storage.googleapis.com/';
   var src = 'https://gerrit.googlesource.com/gerrit/+/'
 
   data.items.sort(function(a,b) {
@@ -111,14 +111,23 @@
     td.appendChild(doc.createTextNode(sizeText));
     tr.appendChild(td);
 
-    td = doc.createElement('td');
+    td_rel = doc.createElement('td');
+    td_doc = doc.createElement('td');
     if (v && f.name.indexOf('-rc') < 0) {
+      // Release notes link
       a = doc.createElement('a');
-      a.href = rel + 'ReleaseNotes-' + v[1] + '.html';
+      a.href = docs + 'ReleaseNotes/ReleaseNotes-' + v[1] + '.html';
       a.appendChild(doc.createTextNode('Release Notes'));
-      td.appendChild(a);
+      td_rel.appendChild(a);
+
+      // Documentation link
+      a = doc.createElement('a');
+      a.href = docs + 'Documentation/' + v[1] + '/index.html';
+      a.appendChild(doc.createTextNode('Documentation'));
+      td_doc.appendChild(a);
     }
-    tr.appendChild(td);
+    tr.appendChild(td_rel);
+    tr.appendChild(td_doc);
 
     td = doc.createElement('td');
     if (v) {