Make hooks/commit-msg available over HTTP

We have to move the old scproot assets into gerrit-server so they
are commonly available to both the SSH and HTTP daemon packages.

Bug: issue 392
Change-Id: Ie0dc95529f26b14535c2e1041863a441333516b3
Signed-off-by: Shawn O. Pearce <sop@google.com>
Reviewed-by: Nico Sallembien <nsallembien@google.com>
This commit is contained in:
Shawn O. Pearce 2010-01-29 12:48:21 -08:00
parent 733791be38
commit 3e4e804248
15 changed files with 445 additions and 145 deletions

View File

@ -35,11 +35,13 @@ each commit message.
OBTAINING
---------
To obtain the 'gerrit-cherry-pick' script use scp to copy it to
your local system:
To obtain the 'gerrit-cherry-pick' script use scp, curl or wget to
copy it to your local system:
$ scp -p -P 29418 gerrit.example.com:bin/gerrit-cherry-pick ~/bin/
$ curl http://gerrit.example.com/tools/bin/gerrit-cherry-pick
GERRIT
------
Part of link:index.html[Gerrit Code Review]

View File

@ -53,11 +53,13 @@ change viewed on the web.
OBTAINING
---------
To obtain the 'commit-msg' script use scp to copy it to your
local system:
To obtain the 'commit-msg' script use scp, wget or curl to copy it
to your local system:
$ scp -p -P 29418 review.example.com:hooks/commit-msg .git/hooks/
$ curl http://review.example.com/tools/hooks/commit-msg
SEE ALSO
--------

View File

@ -4,14 +4,17 @@ Gerrit Code Review - Command Line Tools
Client
------
Client commands and hooks can be downloaded via scp from Gerrit's
SSH daemon, and then executed on the client system.
Client commands and hooks can be downloaded via scp, wget or curl
from Gerrit's daemon, and then executed on the client system.
To download a client command or hook, use scp:
To download a client command or hook, use scp or an http client:
$ scp -p -P 29418 review.example.com:bin/gerrit-cherry-pick ~/bin/
$ scp -p -P 29418 review.example.com:hooks/commit-msg .git/hooks/
$ curl http://review.example.com/tools/bin/gerrit-cherry-pick
$ curl http://review.example.com/tools/hooks/commit-msg
For more details on how to determine the correct SSH port number,
see link:user-upload.html#test_ssh[Testing Your SSH Connection].

View File

@ -46,10 +46,12 @@ Creation
Gerrit Code Review provides a standard 'commit-msg' hook which
can be installed in the local Git repository to automatically
create and insert a unique Change-Id line during `git commit`.
To install the hook, copy it from Gerrit's SSH daemon:
To install the hook, copy it from Gerrit's daemon:
$ scp -p -P 29418 review.example.com:hooks/commit-msg .git/hooks/
$ curl http://review.example.com/tools/hooks/commit-msg
For more details, see link:cmd-hook-commit-msg.html[commit-msg].
Change Upload

View File

@ -125,6 +125,15 @@ public class HtmlDomUtil {
form.appendChild(in);
}
/** Construct a new empty document. */
public static Document newDocument() {
try {
return newBuilder().newDocument();
} catch (ParserConfigurationException e) {
throw new RuntimeException("Cannot create new document", e);
}
}
/** Clone a document so it can be safely modified on a per-request basis. */
public static Document clone(final Document doc) throws IOException {
final Document d;

View File

@ -22,6 +22,7 @@ import com.google.gerrit.httpd.raw.HostPageServlet;
import com.google.gerrit.httpd.raw.LegacyGerritServlet;
import com.google.gerrit.httpd.raw.SshInfoServlet;
import com.google.gerrit.httpd.raw.StaticServlet;
import com.google.gerrit.httpd.raw.ToolServlet;
import com.google.gerrit.reviewdb.RevId;
import com.google.gwtexpui.server.CacheControlFilter;
import com.google.inject.Key;
@ -49,6 +50,7 @@ class UrlModule extends ServletModule {
serve("/signout").with(HttpLogoutServlet.class);
serve("/ssh_info").with(SshInfoServlet.class);
serve("/static/*").with(StaticServlet.class);
serve("/tools/*").with(ToolServlet.class);
filter("/p/*").through(ProjectAccessPathFilter.class);
filter("/p/*").through(ProjectDigestFilter.class);

View File

@ -0,0 +1,162 @@
// Copyright (C) 2010 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.httpd.raw;
import static com.google.gerrit.httpd.HtmlDomUtil.compress;
import static com.google.gerrit.httpd.HtmlDomUtil.newDocument;
import static com.google.gerrit.httpd.HtmlDomUtil.toUTF8;
import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
import static org.eclipse.jgit.util.HttpSupport.HDR_CACHE_CONTROL;
import static org.eclipse.jgit.util.HttpSupport.HDR_EXPIRES;
import static org.eclipse.jgit.util.HttpSupport.HDR_PRAGMA;
import com.google.gerrit.common.Version;
import com.google.gerrit.server.tools.ToolsCatalog;
import com.google.gerrit.server.tools.ToolsCatalog.Entry;
import com.google.gwt.user.server.rpc.RPCServletUtils;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import java.io.IOException;
import java.io.OutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/** Sends the client side tools we keep within our software. */
@Singleton
public class ToolServlet extends HttpServlet {
private final ToolsCatalog toc;
@Inject
ToolServlet(ToolsCatalog toc) {
this.toc = toc;
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse rsp)
throws IOException {
Entry ent = toc.get(req.getPathInfo());
if (ent == null) {
rsp.sendError(SC_NOT_FOUND);
return;
}
switch (ent.getType()) {
case FILE:
doGetFile(ent, req, rsp);
break;
case DIR:
doGetDirectory(ent, req, rsp);
break;
default:
rsp.sendError(SC_NOT_FOUND);
break;
}
}
private void doGetFile(Entry ent, HttpServletRequest req,
HttpServletResponse rsp) throws IOException {
byte[] tosend = ent.getBytes();
rsp.setDateHeader(HDR_EXPIRES, 0L);
rsp.setHeader(HDR_PRAGMA, "no-cache");
rsp.setHeader(HDR_CACHE_CONTROL, "no-cache, must-revalidate");
if (false) {
rsp.setHeader("Content-Disposition", "attachment; filename=\""
+ ent.getName() + "\"");
}
rsp.setContentType("application/octet-stream");
rsp.setContentLength(tosend.length);
final OutputStream out = rsp.getOutputStream();
try {
out.write(tosend);
} finally {
out.close();
}
}
private void doGetDirectory(Entry ent, HttpServletRequest req,
HttpServletResponse rsp) throws IOException {
String path = "/tools/" + ent.getPath();
Document page = newDocument();
Element html = page.createElement("html");
Element head = page.createElement("head");
Element title = page.createElement("title");
Element body = page.createElement("body");
page.appendChild(html);
html.appendChild(head);
html.appendChild(body);
head.appendChild(title);
title.setTextContent("Gerrit Code Review - " + path);
Element h1 = page.createElement("h1");
h1.setTextContent(title.getTextContent());
body.appendChild(h1);
Element ul = page.createElement("ul");
body.appendChild(ul);
for (Entry e : ent.getChildren()) {
String name = e.getName();
if (e.getType() == Entry.Type.DIR && !name.endsWith("/")) {
name += "/";
}
Element li = page.createElement("li");
Element a = page.createElement("a");
a.setAttribute("href", name);
a.setTextContent(name);
li.appendChild(a);
ul.appendChild(li);
}
body.appendChild(page.createElement("hr"));
Element footer = page.createElement("p");
footer.setAttribute("style", "text-align: right; font-style: italic");
footer.setTextContent("Powered by Gerrit Code Review "
+ Version.getVersion());
body.appendChild(footer);
byte[] tosend = toUTF8(page);
if (RPCServletUtils.acceptsGzipEncoding(req)) {
rsp.setHeader("Content-Encoding", "gzip");
tosend = compress(tosend);
}
rsp.setDateHeader(HDR_EXPIRES, 0L);
rsp.setHeader(HDR_PRAGMA, "no-cache");
rsp.setHeader(HDR_CACHE_CONTROL, "no-cache, must-revalidate");
rsp.setContentType("text/html");
rsp.setCharacterEncoding("UTF-8");
rsp.setContentLength(tosend.length);
final OutputStream out = rsp.getOutputStream();
try {
out.write(tosend);
} finally {
out.close();
}
}
}

View File

@ -59,6 +59,7 @@ import com.google.gerrit.server.patch.PatchListCacheImpl;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
import com.google.gerrit.server.project.ProjectCacheImpl;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.tools.ToolsCatalog;
import com.google.gerrit.server.util.IdGenerator;
import com.google.gerrit.server.workflow.FunctionState;
import com.google.inject.Inject;
@ -114,6 +115,7 @@ public class GerritGlobalModule extends FactoryModule {
bind(GitRepositoryManager.class).to(LocalDiskRepositoryManager.class);
bind(FileTypeRegistry.class).to(MimeUtilFileTypeRegistry.class);
bind(WorkQueue.class);
bind(ToolsCatalog.class);
bind(ReplicationQueue.class).to(PushReplication.class).in(SINGLETON);
factory(PushAllProjectsOp.Factory.class);

View File

@ -0,0 +1,229 @@
// Copyright (C) 2010 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.tools;
import com.google.gerrit.common.Version;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.util.RawParseUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.SortedMap;
import java.util.TreeMap;
/**
* Listing of all client side tools stored on this server.
* <p>
* Clients may download these tools through our file server, as they are
* packaged with our own software releases.
*/
@Singleton
public class ToolsCatalog {
private static final Logger log = LoggerFactory.getLogger(ToolsCatalog.class);
private final SortedMap<String, Entry> toc;
@Inject
ToolsCatalog() throws IOException {
this.toc = readToc();
}
/**
* Lookup an entry in the tools catalog.
*
* @param name path of the item, relative to the root of the catalog.
* @return the entry; null if the item is not part of the catalog.
*/
public Entry get(String name) {
if (name.startsWith("/")) {
name = name.substring(1);
}
if (name.endsWith("/")) {
name = name.substring(0, name.length() - 1);
}
return toc.get(name);
}
private static SortedMap<String, Entry> readToc() throws IOException {
SortedMap<String, Entry> toc = new TreeMap<String, Entry>();
final BufferedReader br =
new BufferedReader(new InputStreamReader(new ByteArrayInputStream(
read("TOC")), "UTF-8"));
String line;
while ((line = br.readLine()) != null) {
if (line.length() > 0 && !line.startsWith("#")) {
final Entry e = new Entry(Entry.Type.FILE, line);
toc.put(e.getPath(), e);
}
}
final List<Entry> all = new ArrayList<Entry>(toc.values());
for (Entry e : all) {
String path = dirOf(e.getPath());
while (path != null) {
Entry d = toc.get(path);
if (d == null) {
d = new Entry(Entry.Type.DIR, 0755, path);
toc.put(d.getPath(), d);
}
d.children.add(e);
path = dirOf(path);
e = d;
}
}
final Entry top = new Entry(Entry.Type.DIR, 0755, "");
for (Entry e : toc.values()) {
if (dirOf(e.getPath()) == null) {
top.children.add(e);
}
}
toc.put(top.getPath(), top);
return Collections.unmodifiableSortedMap(toc);
}
private static byte[] read(String path) {
String name = "root/" + path;
InputStream in = ToolsCatalog.class.getResourceAsStream(name);
if (in == null) {
return null;
}
try {
final ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
final byte[] buf = new byte[8192];
int n;
while ((n = in.read(buf, 0, buf.length)) > 0) {
out.write(buf, 0, n);
}
} finally {
in.close();
}
return out.toByteArray();
} catch (Exception e) {
log.debug("Cannot read " + path, e);
return null;
}
}
private static String dirOf(String path) {
final int s = path.lastIndexOf('/');
return s < 0 ? null : path.substring(0, s);
}
/** A file served out of the tools root directory. */
public static class Entry {
public static enum Type {
DIR, FILE;
}
private final Type type;
private final int mode;
private final String path;
private final List<Entry> children;
Entry(Type type, String line) {
int s = line.indexOf(' ');
String mode = line.substring(0, s);
String path = line.substring(s + 1);
this.type = type;
this.mode = Integer.parseInt(mode, 8);
this.path = path;
if (type == Type.FILE) {
this.children = Collections.emptyList();
} else {
this.children = new ArrayList<Entry>();
}
}
Entry(Type type, int mode, String path) {
this.type = type;
this.mode = mode;
this.path = path;
this.children = new ArrayList<Entry>();
}
public Type getType() {
return type;
}
/** @return the preferred UNIX file mode, e.g. {@code 0755}. */
public int getMode() {
return mode;
}
/** @return path of the entry, relative to the catalog root. */
public String getPath() {
return path;
}
/** @return name of the entry, within its parent directory. */
public String getName() {
final int s = path.lastIndexOf('/');
return s < 0 ? path : path.substring(s + 1);
}
/** @return collection of entries below this one, if this is a directory. */
public List<Entry> getChildren() {
return Collections.unmodifiableList(children);
}
/** @return a copy of the file's contents. */
public byte[] getBytes() {
byte[] data = read(getPath());
if (isScript(data)) {
// Embed Gerrit's version number into the top of the script.
//
final String version = Version.getVersion();
final int lf = RawParseUtils.nextLF(data, 0);
if (version != null && lf < data.length) {
byte[] versionHeader =
Constants.encode("# From Gerrit Code Review " + version + "\n");
ByteArrayOutputStream buf = new ByteArrayOutputStream();
buf.write(data, 0, lf);
buf.write(versionHeader, 0, versionHeader.length);
buf.write(data, lf, data.length - lf);
data = buf.toByteArray();
}
}
return data;
}
private boolean isScript(byte[] data) {
return data != null && data.length > 3 //
&& data[0] == '#' //
&& data[1] == '!' //
&& data[2] == '/';
}
}
}

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.sshd.scproot.hooks;
package com.google.gerrit.server.tools.hooks;
import com.google.gerrit.server.util.HostPlatform;

View File

@ -48,7 +48,7 @@
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package com.google.gerrit.sshd.scproot.hooks;
package com.google.gerrit.server.tools.hooks;
import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase;
import org.eclipse.jgit.lib.Repository;
@ -67,7 +67,7 @@ public abstract class HookTestCase extends LocalDiskRepositoryTestCase {
}
protected File getHook(final String name) {
final String scproot = "com/google/gerrit/sshd/scproot";
final String scproot = "com/google/gerrit/server/tools/root";
final String path = scproot + "/hooks/" + name;
final URL url = cl().getResource(path);
if (url == null) {

View File

@ -22,25 +22,19 @@
*/
package com.google.gerrit.sshd.commands;
import com.google.gerrit.common.Version;
import com.google.gerrit.server.tools.ToolsCatalog;
import com.google.gerrit.server.tools.ToolsCatalog.Entry;
import com.google.gerrit.sshd.BaseCommand;
import com.google.inject.Inject;
import org.apache.sshd.server.Environment;
import org.eclipse.jgit.util.RawParseUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
import java.util.TreeMap;
final class ScpCommand extends BaseCommand {
private static final String TYPE_DIR = "D";
@ -52,7 +46,8 @@ final class ScpCommand extends BaseCommand {
private boolean opt_f;
private String root;
private TreeMap<String, Entry> toc;
@Inject
private ToolsCatalog toc;
private IOException error;
@Override
@ -101,7 +96,6 @@ final class ScpCommand extends BaseCommand {
throw error;
}
readToc();
if (opt_f) {
if (root.startsWith("/")) {
root = root.substring(1);
@ -117,10 +111,10 @@ final class ScpCommand extends BaseCommand {
if (ent == null) {
throw new IOException(root + " not found");
} else if (TYPE_FILE.equals(ent.type)) {
} else if (Entry.Type.FILE == ent.getType()) {
readFile(ent);
} else if (TYPE_DIR.equals(ent.type)) {
} else if (Entry.Type.DIR == ent.getType()) {
if (!opt_r) {
throw new IOException(root + " not a regular file");
}
@ -152,43 +146,6 @@ final class ScpCommand extends BaseCommand {
}
}
private void readToc() throws IOException {
toc = new TreeMap<String, Entry>();
final BufferedReader br =
new BufferedReader(new InputStreamReader(new ByteArrayInputStream(
read("TOC")), "UTF-8"));
String line;
while ((line = br.readLine()) != null) {
if (line.length() > 0 && !line.startsWith("#")) {
final Entry e = new Entry(TYPE_FILE, line);
toc.put(e.path, e);
}
}
final List<Entry> all = new ArrayList<Entry>(toc.values());
for (Entry e : all) {
String path = dirOf(e.path);
while (path != null) {
Entry d = toc.get(path);
if (d == null) {
d = new Entry(TYPE_DIR, 0755, path);
toc.put(d.path, d);
}
d.children.add(e);
path = dirOf(path);
e = d;
}
}
final Entry top = new Entry(TYPE_DIR, 0755, "");
for (Entry e : toc.values()) {
if (dirOf(e.path) == null) {
top.children.add(e);
}
}
toc.put(top.path, top);
}
private String readLine() throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
for (;;) {
@ -203,62 +160,10 @@ final class ScpCommand extends BaseCommand {
}
}
private static String nameOf(String path) {
final int s = path.lastIndexOf('/');
return s < 0 ? path : path.substring(s + 1);
}
private static String dirOf(String path) {
final int s = path.lastIndexOf('/');
return s < 0 ? null : path.substring(0, s);
}
private static byte[] read(String path) {
final InputStream in =
ScpCommand.class.getClassLoader().getResourceAsStream(
"com/google/gerrit/sshd/scproot/" + path);
if (in == null) {
return null;
}
try {
final ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
final byte[] buf = new byte[8192];
int n;
while ((n = in.read(buf, 0, buf.length)) > 0) {
out.write(buf, 0, n);
}
} finally {
in.close();
}
return out.toByteArray();
} catch (Exception e) {
log.debug("Cannot read " + path, e);
return null;
}
}
private void readFile(final Entry ent) throws IOException {
byte[] data = read(ent.path);
byte[] data = ent.getBytes();
if (data == null) {
throw new FileNotFoundException(ent.path);
}
if (data.length > 3 && data[0] == '#' && data[1] == '!' && data[2] == '/') {
// Embed Gerrit's version number into the top of the script.
//
final String version = Version.getVersion();
final int lf = RawParseUtils.nextLF(data, 0);
if (version != null && lf < data.length) {
final byte[] versionHeader =
("# From Gerrit Code Review " + version + "\n").getBytes("UTF-8");
final ByteArrayOutputStream buf;
buf = new ByteArrayOutputStream(data.length + versionHeader.length);
buf.write(data, 0, lf);
buf.write(versionHeader);
buf.write(data, lf, data.length - lf);
data = buf.toByteArray();
}
throw new FileNotFoundException(ent.getPath());
}
header(ent, data.length);
@ -273,8 +178,8 @@ final class ScpCommand extends BaseCommand {
header(dir, 0);
readAck();
for (Entry e : dir.children) {
if (TYPE_DIR.equals(e.type)) {
for (Entry e : dir.getChildren()) {
if (Entry.Type.DIR == e.getType()) {
readDir(e);
} else {
readFile(e);
@ -289,12 +194,19 @@ final class ScpCommand extends BaseCommand {
private void header(final Entry dir, final int len) throws IOException,
UnsupportedEncodingException {
final StringBuilder buf = new StringBuilder();
buf.append(dir.type);
buf.append(dir.mode); // perms
switch(dir.getType()){
case DIR:
buf.append(TYPE_DIR);
break;
case FILE:
buf.append(TYPE_FILE);
break;
}
buf.append("0" + Integer.toOctalString(dir.getMode())); // perms
buf.append(" ");
buf.append(len); // length
buf.append(" ");
buf.append(nameOf(dir.path));
buf.append(dir.getName());
buf.append("\n");
out.write(buf.toString().getBytes("UTF-8"));
out.flush();
@ -316,29 +228,4 @@ final class ScpCommand extends BaseCommand {
throw new IOException("Received nack: " + readLine());
}
}
private static class Entry {
String type;
String mode;
String path;
List<Entry> children;
Entry(String type, String line) {
this.type = type;
int s = line.indexOf(' ');
mode = line.substring(0, s);
path = line.substring(s + 1);
if (!mode.startsWith("0")) {
mode = "0" + mode;
}
}
Entry(String type, int mode, String path) {
this.type = type;
this.mode = "0" + Integer.toOctalString(mode);
this.path = path;
this.children = new ArrayList<Entry>();
}
}
}