Don't log TransportGitSsh error stream to JVM stderr
[egit.git] / org.spearce.jgit / src / org / spearce / jgit / transport / TransportGitSsh.java
blobde72d029c64a0c2614bcbe4328aaab867a91eb57
1 /*
2 * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
3 * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
4 * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com>
6 * All rights reserved.
8 * Redistribution and use in source and binary forms, with or
9 * without modification, are permitted provided that the following
10 * conditions are met:
12 * - Redistributions of source code must retain the above copyright
13 * notice, this list of conditions and the following disclaimer.
15 * - Redistributions in binary form must reproduce the above
16 * copyright notice, this list of conditions and the following
17 * disclaimer in the documentation and/or other materials provided
18 * with the distribution.
20 * - Neither the name of the Git Development Community nor the
21 * names of its contributors may be used to endorse or promote
22 * products derived from this software without specific prior
23 * written permission.
25 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
26 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
27 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
28 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
29 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
30 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
31 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
32 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
33 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
34 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
35 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
36 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
37 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
40 package org.spearce.jgit.transport;
42 import java.io.IOException;
43 import java.io.OutputStream;
45 import org.spearce.jgit.errors.NoRemoteRepositoryException;
46 import org.spearce.jgit.errors.TransportException;
47 import org.spearce.jgit.lib.Repository;
48 import org.spearce.jgit.util.QuotedString;
50 import com.jcraft.jsch.ChannelExec;
51 import com.jcraft.jsch.JSchException;
53 /**
54 * Transport through an SSH tunnel.
55 * <p>
56 * The SSH transport requires the remote side to have Git installed, as the
57 * transport logs into the remote system and executes a Git helper program on
58 * the remote side to read (or write) the remote repository's files.
59 * <p>
60 * This transport does not support direct SCP style of copying files, as it
61 * assumes there are Git specific smarts on the remote side to perform object
62 * enumeration, save file modification and hook execution.
64 public class TransportGitSsh extends SshTransport implements PackTransport {
65 static boolean canHandle(final URIish uri) {
66 if (!uri.isRemote())
67 return false;
68 final String scheme = uri.getScheme();
69 if ("ssh".equals(scheme))
70 return true;
71 if ("ssh+git".equals(scheme))
72 return true;
73 if ("git+ssh".equals(scheme))
74 return true;
75 if (scheme == null && uri.getHost() != null && uri.getPath() != null)
76 return true;
77 return false;
80 OutputStream errStream;
82 TransportGitSsh(final Repository local, final URIish uri) {
83 super(local, uri);
86 @Override
87 public FetchConnection openFetch() throws TransportException {
88 return new SshFetchConnection();
91 @Override
92 public PushConnection openPush() throws TransportException {
93 return new SshPushConnection();
96 private static void sqMinimal(final StringBuilder cmd, final String val) {
97 if (val.matches("^[a-zA-Z0-9._/-]*$")) {
98 // If the string matches only generally safe characters
99 // that the shell is not going to evaluate specially we
100 // should leave the string unquoted. Not all systems
101 // actually run a shell and over-quoting confuses them
102 // when it comes to the command name.
104 cmd.append(val);
105 } else {
106 sq(cmd, val);
110 private static void sqAlways(final StringBuilder cmd, final String val) {
111 sq(cmd, val);
114 private static void sq(final StringBuilder cmd, final String val) {
115 if (val.length() > 0)
116 cmd.append(QuotedString.BOURNE.quote(val));
120 ChannelExec exec(final String exe) throws TransportException {
121 initSession();
123 try {
124 final ChannelExec channel = (ChannelExec) sock.openChannel("exec");
125 String path = uri.getPath();
126 if (uri.getScheme() != null && uri.getPath().startsWith("/~"))
127 path = (uri.getPath().substring(1));
129 final StringBuilder cmd = new StringBuilder();
130 final int gitspace = exe.indexOf("git ");
131 if (gitspace >= 0) {
132 sqMinimal(cmd, exe.substring(0, gitspace + 3));
133 cmd.append(' ');
134 sqMinimal(cmd, exe.substring(gitspace + 4));
135 } else
136 sqMinimal(cmd, exe);
137 cmd.append(' ');
138 sqAlways(cmd, path);
139 channel.setCommand(cmd.toString());
140 errStream = createErrorStream();
141 channel.setErrStream(errStream, true);
142 channel.connect();
143 return channel;
144 } catch (JSchException je) {
145 throw new TransportException(uri, je.getMessage(), je);
150 * @return the error stream for the channel, the stream is used to detect
151 * specific error reasons for exceptions.
153 private static OutputStream createErrorStream() {
154 return new OutputStream() {
155 private StringBuilder all = new StringBuilder();
157 private StringBuilder sb = new StringBuilder();
159 public String toString() {
160 String r = all.toString();
161 while (r.endsWith("\n"))
162 r = r.substring(0, r.length() - 1);
163 return r;
166 @Override
167 public void write(final int b) throws IOException {
168 if (b == '\r') {
169 return;
172 sb.append((char) b);
174 if (b == '\n') {
175 all.append(sb);
176 sb.setLength(0);
182 NoRemoteRepositoryException cleanNotFound(NoRemoteRepositoryException nf) {
183 String why = errStream.toString();
184 if (why == null || why.length() == 0)
185 return nf;
187 String path = uri.getPath();
188 if (uri.getScheme() != null && uri.getPath().startsWith("/~"))
189 path = uri.getPath().substring(1);
191 final StringBuilder pfx = new StringBuilder();
192 pfx.append("fatal: ");
193 sqAlways(pfx, path);
194 pfx.append(": ");
195 if (why.startsWith(pfx.toString()))
196 why = why.substring(pfx.length());
198 return new NoRemoteRepositoryException(uri, why);
201 class SshFetchConnection extends BasePackFetchConnection {
202 private ChannelExec channel;
204 SshFetchConnection() throws TransportException {
205 super(TransportGitSsh.this);
206 try {
207 channel = exec(getOptionUploadPack());
209 if (channel.isConnected())
210 init(channel.getInputStream(), channel.getOutputStream());
211 else
212 throw new TransportException(uri, errStream.toString());
214 } catch (TransportException err) {
215 close();
216 throw err;
217 } catch (IOException err) {
218 close();
219 throw new TransportException(uri,
220 "remote hung up unexpectedly", err);
223 try {
224 readAdvertisedRefs();
225 } catch (NoRemoteRepositoryException notFound) {
226 throw cleanNotFound(notFound);
230 @Override
231 public void close() {
232 super.close();
234 if (channel != null) {
235 try {
236 if (channel.isConnected())
237 channel.disconnect();
238 } finally {
239 channel = null;
245 class SshPushConnection extends BasePackPushConnection {
246 private ChannelExec channel;
248 SshPushConnection() throws TransportException {
249 super(TransportGitSsh.this);
250 try {
251 channel = exec(getOptionReceivePack());
253 if (channel.isConnected())
254 init(channel.getInputStream(), channel.getOutputStream());
255 else
256 throw new TransportException(uri, errStream.toString());
258 } catch (TransportException err) {
259 close();
260 throw err;
261 } catch (IOException err) {
262 close();
263 throw new TransportException(uri,
264 "remote hung up unexpectedly", err);
267 try {
268 readAdvertisedRefs();
269 } catch (NoRemoteRepositoryException notFound) {
270 throw cleanNotFound(notFound);
274 @Override
275 public void close() {
276 super.close();
278 if (channel != null) {
279 try {
280 if (channel.isConnected())
281 channel.disconnect();
282 } finally {
283 channel = null;