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.InputStream;
51  import java.io.OutputStream;
52  import java.text.MessageFormat;
53  import java.util.Collection;
54  import java.util.HashSet;
55  import java.util.List;
56  import java.util.Map;
57  import java.util.Set;
58  
59  import org.eclipse.jgit.errors.NoRemoteRepositoryException;
60  import org.eclipse.jgit.errors.NotSupportedException;
61  import org.eclipse.jgit.errors.PackProtocolException;
62  import org.eclipse.jgit.errors.TooLargeObjectInPackException;
63  import org.eclipse.jgit.errors.TooLargePackException;
64  import org.eclipse.jgit.errors.TransportException;
65  import org.eclipse.jgit.internal.JGitText;
66  import org.eclipse.jgit.internal.storage.pack.PackWriter;
67  import org.eclipse.jgit.lib.ObjectId;
68  import org.eclipse.jgit.lib.ProgressMonitor;
69  import org.eclipse.jgit.lib.Ref;
70  import org.eclipse.jgit.transport.RemoteRefUpdate.Status;
71  
72  /**
73   * Push implementation using the native Git pack transfer service.
74   * <p>
75   * This is the canonical implementation for transferring objects to the remote
76   * repository from the local repository by talking to the 'git-receive-pack'
77   * service. Objects are packed on the local side into a pack file and then sent
78   * to the remote repository.
79   * <p>
80   * This connection requires only a bi-directional pipe or socket, and thus is
81   * easily wrapped up into a local process pipe, anonymous TCP socket, or a
82   * command executed through an SSH tunnel.
83   * <p>
84   * This implementation honors
85   * {@link org.eclipse.jgit.transport.Transport#isPushThin()} option.
86   * <p>
87   * Concrete implementations should just call
88   * {@link #init(java.io.InputStream, java.io.OutputStream)} and
89   * {@link #readAdvertisedRefs()} methods in constructor or before any use. They
90   * should also handle resources releasing in {@link #close()} method if needed.
91   */
92  public abstract class BasePackPushConnection extends BasePackConnection implements
93  		PushConnection {
94  	/**
95  	 * The client expects a status report after the server processes the pack.
96  	 * @since 2.0
97  	 */
98  	public static final String CAPABILITY_REPORT_STATUS = GitProtocolConstants.CAPABILITY_REPORT_STATUS;
99  
100 	/**
101 	 * The server supports deleting refs.
102 	 * @since 2.0
103 	 */
104 	public static final String CAPABILITY_DELETE_REFS = GitProtocolConstants.CAPABILITY_DELETE_REFS;
105 
106 	/**
107 	 * The server supports packs with OFS deltas.
108 	 * @since 2.0
109 	 */
110 	public static final String CAPABILITY_OFS_DELTA = GitProtocolConstants.CAPABILITY_OFS_DELTA;
111 
112 	/**
113 	 * The client supports using the 64K side-band for progress messages.
114 	 * @since 2.0
115 	 */
116 	public static final String CAPABILITY_SIDE_BAND_64K = GitProtocolConstants.CAPABILITY_SIDE_BAND_64K;
117 
118 	/**
119 	 * The server supports the receiving of push options.
120 	 * @since 4.5
121 	 */
122 	public static final String CAPABILITY_PUSH_OPTIONS = GitProtocolConstants.CAPABILITY_PUSH_OPTIONS;
123 
124 	private final boolean thinPack;
125 	private final boolean atomic;
126 
127 	/** A list of option strings associated with this push. */
128 	private List<String> pushOptions;
129 
130 	private boolean capableAtomic;
131 	private boolean capableDeleteRefs;
132 	private boolean capableReport;
133 	private boolean capableSideBand;
134 	private boolean capableOfsDelta;
135 	private boolean capablePushOptions;
136 
137 	private boolean sentCommand;
138 	private boolean writePack;
139 
140 	/** Time in milliseconds spent transferring the pack data. */
141 	private long packTransferTime;
142 
143 	/**
144 	 * Create a new connection to push using the native git transport.
145 	 *
146 	 * @param packTransport
147 	 *            the transport.
148 	 */
149 	public BasePackPushConnection(PackTransport packTransport) {
150 		super(packTransport);
151 		thinPack = transport.isPushThin();
152 		atomic = transport.isPushAtomic();
153 		pushOptions = transport.getPushOptions();
154 	}
155 
156 	/** {@inheritDoc} */
157 	@Override
158 	public void push(final ProgressMonitor monitor,
159 			final Map<String, RemoteRefUpdate> refUpdates)
160 			throws TransportException {
161 		push(monitor, refUpdates, null);
162 	}
163 
164 	/** {@inheritDoc} */
165 	@Override
166 	public void push(final ProgressMonitor monitor,
167 			final Map<String, RemoteRefUpdate> refUpdates, OutputStream outputStream)
168 			throws TransportException {
169 		markStartedOperation();
170 		doPush(monitor, refUpdates, outputStream);
171 	}
172 
173 	/** {@inheritDoc} */
174 	@Override
175 	protected TransportException noRepository() {
176 		// Sadly we cannot tell the "invalid URI" case from "push not allowed".
177 		// Opening a fetch connection can help us tell the difference, as any
178 		// useful repository is going to support fetch if it also would allow
179 		// push. So if fetch throws NoRemoteRepositoryException we know the
180 		// URI is wrong. Otherwise we can correctly state push isn't allowed
181 		// as the fetch connection opened successfully.
182 		//
183 		try {
184 			transport.openFetch().close();
185 		} catch (NotSupportedException e) {
186 			// Fall through.
187 		} catch (NoRemoteRepositoryException e) {
188 			// Fetch concluded the repository doesn't exist.
189 			//
190 			return e;
191 		} catch (TransportException e) {
192 			// Fall through.
193 		}
194 		return new TransportException(uri, JGitText.get().pushNotPermitted);
195 	}
196 
197 	/**
198 	 * Push one or more objects and update the remote repository.
199 	 *
200 	 * @param monitor
201 	 *            progress monitor to receive status updates.
202 	 * @param refUpdates
203 	 *            update commands to be applied to the remote repository.
204 	 * @param outputStream
205 	 *            output stream to write sideband messages to
206 	 * @throws org.eclipse.jgit.errors.TransportException
207 	 *             if any exception occurs.
208 	 * @since 3.0
209 	 */
210 	protected void doPush(final ProgressMonitor monitor,
211 			final Map<String, RemoteRefUpdate> refUpdates,
212 			OutputStream outputStream) throws TransportException {
213 		try {
214 			writeCommands(refUpdates.values(), monitor, outputStream);
215 
216 			if (pushOptions != null && capablePushOptions)
217 				transmitOptions();
218 			if (writePack)
219 				writePack(refUpdates, monitor);
220 			if (sentCommand) {
221 				if (capableReport)
222 					readStatusReport(refUpdates);
223 				if (capableSideBand) {
224 					// Ensure the data channel is at EOF, so we know we have
225 					// read all side-band data from all channels and have a
226 					// complete copy of the messages (if any) buffered from
227 					// the other data channels.
228 					//
229 					int b = in.read();
230 					if (0 <= b)
231 						throw new TransportException(uri, MessageFormat.format(
232 								JGitText.get().expectedEOFReceived,
233 								Character.valueOf((char) b)));
234 				}
235 			}
236 		} catch (TransportException e) {
237 			throw e;
238 		} catch (Exception e) {
239 			throw new TransportException(uri, e.getMessage(), e);
240 		} finally {
241 			close();
242 		}
243 	}
244 
245 	private void writeCommands(final Collection<RemoteRefUpdate> refUpdates,
246 			final ProgressMonitor monitor, OutputStream outputStream) throws IOException {
247 		final String capabilities = enableCapabilities(monitor, outputStream);
248 		if (atomic && !capableAtomic) {
249 			throw new TransportException(uri,
250 					JGitText.get().atomicPushNotSupported);
251 		}
252 
253 		if (pushOptions != null && !capablePushOptions) {
254 			throw new TransportException(uri,
255 					MessageFormat.format(JGitText.get().pushOptionsNotSupported,
256 							pushOptions.toString()));
257 		}
258 
259 		for (RemoteRefUpdate rru : refUpdates) {
260 			if (!capableDeleteRefs && rru.isDelete()) {
261 				rru.setStatus(Status.REJECTED_NODELETE);
262 				continue;
263 			}
264 
265 			final StringBuilder sb = new StringBuilder();
266 			ObjectId oldId = rru.getExpectedOldObjectId();
267 			if (oldId == null) {
268 				final Ref advertised = getRef(rru.getRemoteName());
269 				oldId = advertised != null ? advertised.getObjectId() : null;
270 				if (oldId == null) {
271 					oldId = ObjectId.zeroId();
272 				}
273 			}
274 			sb.append(oldId.name());
275 			sb.append(' ');
276 			sb.append(rru.getNewObjectId().name());
277 			sb.append(' ');
278 			sb.append(rru.getRemoteName());
279 			if (!sentCommand) {
280 				sentCommand = true;
281 				sb.append(capabilities);
282 			}
283 
284 			pckOut.writeString(sb.toString());
285 			rru.setStatus(Status.AWAITING_REPORT);
286 			if (!rru.isDelete())
287 				writePack = true;
288 		}
289 
290 		if (monitor.isCancelled())
291 			throw new TransportException(uri, JGitText.get().pushCancelled);
292 		pckOut.end();
293 		outNeedsEnd = false;
294 	}
295 
296 	private void transmitOptions() throws IOException {
297 		for (String pushOption : pushOptions) {
298 			pckOut.writeString(pushOption);
299 		}
300 
301 		pckOut.end();
302 	}
303 
304 	private String enableCapabilities(final ProgressMonitor monitor,
305 			OutputStream outputStream) {
306 		final StringBuilder line = new StringBuilder();
307 		if (atomic)
308 			capableAtomic = wantCapability(line, CAPABILITY_ATOMIC);
309 		capableReport = wantCapability(line, CAPABILITY_REPORT_STATUS);
310 		capableDeleteRefs = wantCapability(line, CAPABILITY_DELETE_REFS);
311 		capableOfsDelta = wantCapability(line, CAPABILITY_OFS_DELTA);
312 
313 		if (pushOptions != null) {
314 			capablePushOptions = wantCapability(line, CAPABILITY_PUSH_OPTIONS);
315 		}
316 
317 		capableSideBand = wantCapability(line, CAPABILITY_SIDE_BAND_64K);
318 		if (capableSideBand) {
319 			in = new SideBandInputStream(in, monitor, getMessageWriter(),
320 					outputStream);
321 			pckIn = new PacketLineIn(in);
322 		}
323 		addUserAgentCapability(line);
324 
325 		if (line.length() > 0)
326 			line.setCharAt(0, '\0');
327 		return line.toString();
328 	}
329 
330 	private void writePack(final Map<String, RemoteRefUpdate> refUpdates,
331 			final ProgressMonitor monitor) throws IOException {
332 		Set<ObjectId> remoteObjects = new HashSet<>();
333 		Set<ObjectId> newObjects = new HashSet<>();
334 
335 		try (PackWritere/pack/PackWriter.html#PackWriter">PackWriter writer = new PackWriter(transport.getPackConfig(),
336 				local.newObjectReader())) {
337 
338 			for (Ref r : getRefs()) {
339 				// only add objects that we actually have
340 				ObjectId oid = r.getObjectId();
341 				if (local.getObjectDatabase().has(oid))
342 					remoteObjects.add(oid);
343 			}
344 			remoteObjects.addAll(additionalHaves);
345 			for (RemoteRefUpdate r : refUpdates.values()) {
346 				if (!ObjectId.zeroId().equals(r.getNewObjectId()))
347 					newObjects.add(r.getNewObjectId());
348 			}
349 
350 			writer.setIndexDisabled(true);
351 			writer.setUseCachedPacks(true);
352 			writer.setUseBitmaps(true);
353 			writer.setThin(thinPack);
354 			writer.setReuseValidatingObjects(false);
355 			writer.setDeltaBaseAsOffset(capableOfsDelta);
356 			writer.preparePack(monitor, newObjects, remoteObjects);
357 
358 			OutputStream packOut = out;
359 			if (capableSideBand) {
360 				packOut = new CheckingSideBandOutputStream(in, out);
361 			}
362 			writer.writePack(monitor, monitor, packOut);
363 
364 			packTransferTime = writer.getStatistics().getTimeWriting();
365 		}
366 	}
367 
368 	private void readStatusReport(Map<String, RemoteRefUpdate> refUpdates)
369 			throws IOException {
370 		final String unpackLine = readStringLongTimeout();
371 		if (!unpackLine.startsWith("unpack ")) //$NON-NLS-1$
372 			throw new PackProtocolException(uri, MessageFormat
373 					.format(JGitText.get().unexpectedReportLine, unpackLine));
374 		final String unpackStatus = unpackLine.substring("unpack ".length()); //$NON-NLS-1$
375 		if (unpackStatus.startsWith("error Pack exceeds the limit of")) {//$NON-NLS-1$
376 			throw new TooLargePackException(uri,
377 					unpackStatus.substring("error ".length())); //$NON-NLS-1$
378 		} else if (unpackStatus.startsWith("error Object too large")) {//$NON-NLS-1$
379 			throw new TooLargeObjectInPackException(uri,
380 					unpackStatus.substring("error ".length())); //$NON-NLS-1$
381 		} else if (!unpackStatus.equals("ok")) { //$NON-NLS-1$
382 			throw new TransportException(uri, MessageFormat.format(
383 					JGitText.get().errorOccurredDuringUnpackingOnTheRemoteEnd, unpackStatus));
384 		}
385 
386 		for (String refLine : pckIn.readStrings()) {
387 			boolean ok = false;
388 			int refNameEnd = -1;
389 			if (refLine.startsWith("ok ")) { //$NON-NLS-1$
390 				ok = true;
391 				refNameEnd = refLine.length();
392 			} else if (refLine.startsWith("ng ")) { //$NON-NLS-1$
393 				ok = false;
394 				refNameEnd = refLine.indexOf(" ", 3); //$NON-NLS-1$
395 			}
396 			if (refNameEnd == -1)
397 				throw new PackProtocolException(MessageFormat.format(JGitText.get().unexpectedReportLine2
398 						, uri, refLine));
399 			final String refName = refLine.substring(3, refNameEnd);
400 			final String message = (ok ? null : refLine
401 					.substring(refNameEnd + 1));
402 
403 			final RemoteRefUpdate rru = refUpdates.get(refName);
404 			if (rru == null)
405 				throw new PackProtocolException(MessageFormat.format(JGitText.get().unexpectedRefReport, uri, refName));
406 			if (ok) {
407 				rru.setStatus(Status.OK);
408 			} else {
409 				rru.setStatus(Status.REJECTED_OTHER_REASON);
410 				rru.setMessage(message);
411 			}
412 		}
413 		for (RemoteRefUpdate rru : refUpdates.values()) {
414 			if (rru.getStatus() == Status.AWAITING_REPORT)
415 				throw new PackProtocolException(MessageFormat.format(
416 						JGitText.get().expectedReportForRefNotReceived , uri, rru.getRemoteName()));
417 		}
418 	}
419 
420 	private String readStringLongTimeout() throws IOException {
421 		if (timeoutIn == null)
422 			return pckIn.readString();
423 
424 		// The remote side may need a lot of time to choke down the pack
425 		// we just sent them. There may be many deltas that need to be
426 		// resolved by the remote. Its hard to say how long the other
427 		// end is going to be silent. Taking 10x the configured timeout
428 		// or the time spent transferring the pack, whichever is larger,
429 		// gives the other side some reasonable window to process the data,
430 		// but this is just a wild guess.
431 		//
432 		final int oldTimeout = timeoutIn.getTimeout();
433 		final int sendTime = (int) Math.min(packTransferTime, 28800000L);
434 		try {
435 			int timeout = 10 * Math.max(sendTime, oldTimeout);
436 			timeoutIn.setTimeout((timeout < 0) ? Integer.MAX_VALUE : timeout);
437 			return pckIn.readString();
438 		} finally {
439 			timeoutIn.setTimeout(oldTimeout);
440 		}
441 	}
442 
443 	/**
444 	 * Gets the list of option strings associated with this push.
445 	 *
446 	 * @return pushOptions
447 	 * @since 4.5
448 	 */
449 	public List<String> getPushOptions() {
450 		return pushOptions;
451 	}
452 
453 	private static class CheckingSideBandOutputStream extends OutputStream {
454 		private final InputStream in;
455 		private final OutputStream out;
456 
457 		CheckingSideBandOutputStream(InputStream in, OutputStream out) {
458 			this.in = in;
459 			this.out = out;
460 		}
461 
462 		@Override
463 		public void write(int b) throws IOException {
464 			write(new byte[] { (byte) b });
465 		}
466 
467 		@Override
468 		public void write(byte[] buf, int ptr, int cnt) throws IOException {
469 			try {
470 				out.write(buf, ptr, cnt);
471 			} catch (IOException e) {
472 				throw checkError(e);
473 			}
474 		}
475 
476 		@Override
477 		public void flush() throws IOException {
478 			try {
479 				out.flush();
480 			} catch (IOException e) {
481 				throw checkError(e);
482 			}
483 		}
484 
485 		private IOException checkError(IOException e1) {
486 			try {
487 				in.read();
488 			} catch (TransportException e2) {
489 				return e2;
490 			} catch (IOException e2) {
491 				return e1;
492 			}
493 			return e1;
494 		}
495 	}
496 }