View Javadoc
1   /*
2    * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com>
3    * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
4    * and other copyright owners as documented in the project's IP log.
5    *
6    * This program and the accompanying materials are made available
7    * under the terms of the Eclipse Distribution License v1.0 which
8    * accompanies this distribution, is reproduced below, and is
9    * available at http://www.eclipse.org/org/documents/edl-v10.php
10   *
11   * All rights reserved.
12   *
13   * Redistribution and use in source and binary forms, with or
14   * without modification, are permitted provided that the following
15   * conditions are met:
16   *
17   * - Redistributions of source code must retain the above copyright
18   *   notice, this list of conditions and the following disclaimer.
19   *
20   * - Redistributions in binary form must reproduce the above
21   *   copyright notice, this list of conditions and the following
22   *   disclaimer in the documentation and/or other materials provided
23   *   with the distribution.
24   *
25   * - Neither the name of the Eclipse Foundation, Inc. nor the
26   *   names of its contributors may be used to endorse or promote
27   *   products derived from this software without specific prior
28   *   written permission.
29   *
30   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
31   * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
32   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
33   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
34   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
35   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
36   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
37   * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
38   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
39   * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
40   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
41   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
42   * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
43   */
44  
45  package org.eclipse.jgit.transport;
46  
47  import static org.eclipse.jgit.transport.GitProtocolConstants.CAPABILITY_ATOMIC;
48  
49  import java.io.IOException;
50  import java.io.OutputStream;
51  import java.text.MessageFormat;
52  import java.util.Collection;
53  import java.util.HashSet;
54  import java.util.Map;
55  import java.util.Set;
56  
57  import org.eclipse.jgit.errors.NoRemoteRepositoryException;
58  import org.eclipse.jgit.errors.NotSupportedException;
59  import org.eclipse.jgit.errors.PackProtocolException;
60  import org.eclipse.jgit.errors.TooLargeObjectInPackException;
61  import org.eclipse.jgit.errors.TooLargePackException;
62  import org.eclipse.jgit.errors.TransportException;
63  import org.eclipse.jgit.internal.JGitText;
64  import org.eclipse.jgit.internal.storage.pack.PackWriter;
65  import org.eclipse.jgit.lib.ObjectId;
66  import org.eclipse.jgit.lib.ProgressMonitor;
67  import org.eclipse.jgit.lib.Ref;
68  import org.eclipse.jgit.transport.RemoteRefUpdate.Status;
69  
70  /**
71   * Push implementation using the native Git pack transfer service.
72   * <p>
73   * This is the canonical implementation for transferring objects to the remote
74   * repository from the local repository by talking to the 'git-receive-pack'
75   * service. Objects are packed on the local side into a pack file and then sent
76   * to the remote repository.
77   * <p>
78   * This connection requires only a bi-directional pipe or socket, and thus is
79   * easily wrapped up into a local process pipe, anonymous TCP socket, or a
80   * command executed through an SSH tunnel.
81   * <p>
82   * This implementation honors {@link Transport#isPushThin()} option.
83   * <p>
84   * Concrete implementations should just call
85   * {@link #init(java.io.InputStream, java.io.OutputStream)} and
86   * {@link #readAdvertisedRefs()} methods in constructor or before any use. They
87   * should also handle resources releasing in {@link #close()} method if needed.
88   */
89  public abstract class BasePackPushConnection extends BasePackConnection implements
90  		PushConnection {
91  	/**
92  	 * The client expects a status report after the server processes the pack.
93  	 * @since 2.0
94  	 */
95  	public static final String CAPABILITY_REPORT_STATUS = GitProtocolConstants.CAPABILITY_REPORT_STATUS;
96  
97  	/**
98  	 * The server supports deleting refs.
99  	 * @since 2.0
100 	 */
101 	public static final String CAPABILITY_DELETE_REFS = GitProtocolConstants.CAPABILITY_DELETE_REFS;
102 
103 	/**
104 	 * The server supports packs with OFS deltas.
105 	 * @since 2.0
106 	 */
107 	public static final String CAPABILITY_OFS_DELTA = GitProtocolConstants.CAPABILITY_OFS_DELTA;
108 
109 	/**
110 	 * The client supports using the 64K side-band for progress messages.
111 	 * @since 2.0
112 	 */
113 	public static final String CAPABILITY_SIDE_BAND_64K = GitProtocolConstants.CAPABILITY_SIDE_BAND_64K;
114 
115 	private final boolean thinPack;
116 	private final boolean atomic;
117 
118 	private boolean capableAtomic;
119 	private boolean capableDeleteRefs;
120 	private boolean capableReport;
121 	private boolean capableSideBand;
122 	private boolean capableOfsDelta;
123 
124 	private boolean sentCommand;
125 	private boolean writePack;
126 
127 	/** Time in milliseconds spent transferring the pack data. */
128 	private long packTransferTime;
129 
130 	/**
131 	 * Create a new connection to push using the native git transport.
132 	 *
133 	 * @param packTransport
134 	 *            the transport.
135 	 */
136 	public BasePackPushConnection(final PackTransport packTransport) {
137 		super(packTransport);
138 		thinPack = transport.isPushThin();
139 		atomic = transport.isPushAtomic();
140 	}
141 
142 	public void push(final ProgressMonitor monitor,
143 			final Map<String, RemoteRefUpdate> refUpdates)
144 			throws TransportException {
145 		push(monitor, refUpdates, null);
146 	}
147 
148 	/**
149 	 * @since 3.0
150 	 */
151 	public void push(final ProgressMonitor monitor,
152 			final Map<String, RemoteRefUpdate> refUpdates, OutputStream outputStream)
153 			throws TransportException {
154 		markStartedOperation();
155 		doPush(monitor, refUpdates, outputStream);
156 	}
157 
158 	@Override
159 	protected TransportException noRepository() {
160 		// Sadly we cannot tell the "invalid URI" case from "push not allowed".
161 		// Opening a fetch connection can help us tell the difference, as any
162 		// useful repository is going to support fetch if it also would allow
163 		// push. So if fetch throws NoRemoteRepositoryException we know the
164 		// URI is wrong. Otherwise we can correctly state push isn't allowed
165 		// as the fetch connection opened successfully.
166 		//
167 		try {
168 			transport.openFetch().close();
169 		} catch (NotSupportedException e) {
170 			// Fall through.
171 		} catch (NoRemoteRepositoryException e) {
172 			// Fetch concluded the repository doesn't exist.
173 			//
174 			return e;
175 		} catch (TransportException e) {
176 			// Fall through.
177 		}
178 		return new TransportException(uri, JGitText.get().pushNotPermitted);
179 	}
180 
181 	/**
182 	 * Push one or more objects and update the remote repository.
183 	 *
184 	 * @param monitor
185 	 *            progress monitor to receive status updates.
186 	 * @param refUpdates
187 	 *            update commands to be applied to the remote repository.
188 	 * @param outputStream
189 	 *            output stream to write sideband messages to
190 	 * @throws TransportException
191 	 *             if any exception occurs.
192 	 * @since 3.0
193 	 */
194 	protected void doPush(final ProgressMonitor monitor,
195 			final Map<String, RemoteRefUpdate> refUpdates,
196 			OutputStream outputStream) throws TransportException {
197 		try {
198 			writeCommands(refUpdates.values(), monitor, outputStream);
199 			if (writePack)
200 				writePack(refUpdates, monitor);
201 			if (sentCommand) {
202 				if (capableReport)
203 					readStatusReport(refUpdates);
204 				if (capableSideBand) {
205 					// Ensure the data channel is at EOF, so we know we have
206 					// read all side-band data from all channels and have a
207 					// complete copy of the messages (if any) buffered from
208 					// the other data channels.
209 					//
210 					int b = in.read();
211 					if (0 <= b)
212 						throw new TransportException(uri, MessageFormat.format(
213 								JGitText.get().expectedEOFReceived,
214 								Character.valueOf((char) b)));
215 				}
216 			}
217 		} catch (TransportException e) {
218 			throw e;
219 		} catch (Exception e) {
220 			throw new TransportException(uri, e.getMessage(), e);
221 		} finally {
222 			close();
223 		}
224 	}
225 
226 	private void writeCommands(final Collection<RemoteRefUpdate> refUpdates,
227 			final ProgressMonitor monitor, OutputStream outputStream) throws IOException {
228 		final String capabilities = enableCapabilities(monitor, outputStream);
229 		if (atomic && !capableAtomic) {
230 			throw new TransportException(uri,
231 					JGitText.get().atomicPushNotSupported);
232 		}
233 
234 		for (final RemoteRefUpdate rru : refUpdates) {
235 			if (!capableDeleteRefs && rru.isDelete()) {
236 				rru.setStatus(Status.REJECTED_NODELETE);
237 				continue;
238 			}
239 
240 			final StringBuilder sb = new StringBuilder();
241 			ObjectId oldId = rru.getExpectedOldObjectId();
242 			if (oldId == null) {
243 				final Ref advertised = getRef(rru.getRemoteName());
244 				oldId = advertised != null ? advertised.getObjectId() : null;
245 				if (oldId == null) {
246 					oldId = ObjectId.zeroId();
247 				}
248 			}
249 			sb.append(oldId.name());
250 			sb.append(' ');
251 			sb.append(rru.getNewObjectId().name());
252 			sb.append(' ');
253 			sb.append(rru.getRemoteName());
254 			if (!sentCommand) {
255 				sentCommand = true;
256 				sb.append(capabilities);
257 			}
258 
259 			pckOut.writeString(sb.toString());
260 			rru.setStatus(Status.AWAITING_REPORT);
261 			if (!rru.isDelete())
262 				writePack = true;
263 		}
264 
265 		if (monitor.isCancelled())
266 			throw new TransportException(uri, JGitText.get().pushCancelled);
267 		pckOut.end();
268 		outNeedsEnd = false;
269 	}
270 
271 	private String enableCapabilities(final ProgressMonitor monitor,
272 			OutputStream outputStream) {
273 		final StringBuilder line = new StringBuilder();
274 		if (atomic)
275 			capableAtomic = wantCapability(line, CAPABILITY_ATOMIC);
276 		capableReport = wantCapability(line, CAPABILITY_REPORT_STATUS);
277 		capableDeleteRefs = wantCapability(line, CAPABILITY_DELETE_REFS);
278 		capableOfsDelta = wantCapability(line, CAPABILITY_OFS_DELTA);
279 
280 		capableSideBand = wantCapability(line, CAPABILITY_SIDE_BAND_64K);
281 		if (capableSideBand) {
282 			in = new SideBandInputStream(in, monitor, getMessageWriter(),
283 					outputStream);
284 			pckIn = new PacketLineIn(in);
285 		}
286 		addUserAgentCapability(line);
287 
288 		if (line.length() > 0)
289 			line.setCharAt(0, '\0');
290 		return line.toString();
291 	}
292 
293 	private void writePack(final Map<String, RemoteRefUpdate> refUpdates,
294 			final ProgressMonitor monitor) throws IOException {
295 		Set<ObjectId> remoteObjects = new HashSet<ObjectId>();
296 		Set<ObjectId> newObjects = new HashSet<ObjectId>();
297 
298 		try (final PackWriter writer = new PackWriter(transport.getPackConfig(),
299 				local.newObjectReader())) {
300 
301 			for (final Ref r : getRefs()) {
302 				// only add objects that we actually have
303 				ObjectId oid = r.getObjectId();
304 				if (local.hasObject(oid))
305 					remoteObjects.add(oid);
306 			}
307 			remoteObjects.addAll(additionalHaves);
308 			for (final RemoteRefUpdate r : refUpdates.values()) {
309 				if (!ObjectId.zeroId().equals(r.getNewObjectId()))
310 					newObjects.add(r.getNewObjectId());
311 			}
312 
313 			writer.setIndexDisabled(true);
314 			writer.setUseCachedPacks(true);
315 			writer.setUseBitmaps(true);
316 			writer.setThin(thinPack);
317 			writer.setReuseValidatingObjects(false);
318 			writer.setDeltaBaseAsOffset(capableOfsDelta);
319 			writer.preparePack(monitor, newObjects, remoteObjects);
320 			writer.writePack(monitor, monitor, out);
321 
322 			packTransferTime = writer.getStatistics().getTimeWriting();
323 		}
324 	}
325 
326 	private void readStatusReport(final Map<String, RemoteRefUpdate> refUpdates)
327 			throws IOException {
328 		final String unpackLine = readStringLongTimeout();
329 		if (!unpackLine.startsWith("unpack ")) //$NON-NLS-1$
330 			throw new PackProtocolException(uri, MessageFormat.format(JGitText.get().unexpectedReportLine, unpackLine));
331 		final String unpackStatus = unpackLine.substring("unpack ".length()); //$NON-NLS-1$
332 		if (unpackStatus.startsWith("error Pack exceeds the limit of")) {//$NON-NLS-1$
333 			throw new TooLargePackException(uri,
334 					unpackStatus.substring("error ".length())); //$NON-NLS-1$
335 		} else if (unpackStatus.startsWith("error Object too large")) {//$NON-NLS-1$
336 			throw new TooLargeObjectInPackException(uri,
337 					unpackStatus.substring("error ".length())); //$NON-NLS-1$
338 		} else if (!unpackStatus.equals("ok")) { //$NON-NLS-1$
339 			throw new TransportException(uri, MessageFormat.format(
340 					JGitText.get().errorOccurredDuringUnpackingOnTheRemoteEnd, unpackStatus));
341 		}
342 
343 		String refLine;
344 		while ((refLine = pckIn.readString()) != PacketLineIn.END) {
345 			boolean ok = false;
346 			int refNameEnd = -1;
347 			if (refLine.startsWith("ok ")) { //$NON-NLS-1$
348 				ok = true;
349 				refNameEnd = refLine.length();
350 			} else if (refLine.startsWith("ng ")) { //$NON-NLS-1$
351 				ok = false;
352 				refNameEnd = refLine.indexOf(" ", 3); //$NON-NLS-1$
353 			}
354 			if (refNameEnd == -1)
355 				throw new PackProtocolException(MessageFormat.format(JGitText.get().unexpectedReportLine2
356 						, uri, refLine));
357 			final String refName = refLine.substring(3, refNameEnd);
358 			final String message = (ok ? null : refLine
359 					.substring(refNameEnd + 1));
360 
361 			final RemoteRefUpdate rru = refUpdates.get(refName);
362 			if (rru == null)
363 				throw new PackProtocolException(MessageFormat.format(JGitText.get().unexpectedRefReport, uri, refName));
364 			if (ok) {
365 				rru.setStatus(Status.OK);
366 			} else {
367 				rru.setStatus(Status.REJECTED_OTHER_REASON);
368 				rru.setMessage(message);
369 			}
370 		}
371 		for (final RemoteRefUpdate rru : refUpdates.values()) {
372 			if (rru.getStatus() == Status.AWAITING_REPORT)
373 				throw new PackProtocolException(MessageFormat.format(
374 						JGitText.get().expectedReportForRefNotReceived , uri, rru.getRemoteName()));
375 		}
376 	}
377 
378 	private String readStringLongTimeout() throws IOException {
379 		if (timeoutIn == null)
380 			return pckIn.readString();
381 
382 		// The remote side may need a lot of time to choke down the pack
383 		// we just sent them. There may be many deltas that need to be
384 		// resolved by the remote. Its hard to say how long the other
385 		// end is going to be silent. Taking 10x the configured timeout
386 		// or the time spent transferring the pack, whichever is larger,
387 		// gives the other side some reasonable window to process the data,
388 		// but this is just a wild guess.
389 		//
390 		final int oldTimeout = timeoutIn.getTimeout();
391 		final int sendTime = (int) Math.min(packTransferTime, 28800000L);
392 		try {
393 			int timeout = 10 * Math.max(sendTime, oldTimeout);
394 			timeoutIn.setTimeout((timeout < 0) ? Integer.MAX_VALUE : timeout);
395 			return pckIn.readString();
396 		} finally {
397 			timeoutIn.setTimeout(oldTimeout);
398 		}
399 	}
400 }