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