View Javadoc
1   /*
2    * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
3    * Copyright (C) 2008, 2020 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 java.nio.charset.StandardCharsets.UTF_8;
15  import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED;
16  import static org.eclipse.jgit.transport.ReceiveCommand.Result.OK;
17  import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_NONFASTFORWARD;
18  import static org.eclipse.jgit.transport.ReceiveCommand.Type.UPDATE_NONFASTFORWARD;
19  
20  import java.io.File;
21  import java.io.IOException;
22  import java.io.OutputStreamWriter;
23  import java.io.Writer;
24  import java.text.MessageFormat;
25  import java.util.ArrayList;
26  import java.util.Collection;
27  import java.util.Collections;
28  import java.util.HashMap;
29  import java.util.HashSet;
30  import java.util.Iterator;
31  import java.util.Map;
32  import java.util.Set;
33  import java.util.concurrent.TimeUnit;
34  import java.util.stream.Collectors;
35  
36  import org.eclipse.jgit.errors.MissingObjectException;
37  import org.eclipse.jgit.errors.NotSupportedException;
38  import org.eclipse.jgit.errors.TransportException;
39  import org.eclipse.jgit.internal.JGitText;
40  import org.eclipse.jgit.internal.storage.file.LockFile;
41  import org.eclipse.jgit.lib.BatchRefUpdate;
42  import org.eclipse.jgit.lib.BatchingProgressMonitor;
43  import org.eclipse.jgit.lib.Constants;
44  import org.eclipse.jgit.lib.ObjectId;
45  import org.eclipse.jgit.lib.ObjectIdRef;
46  import org.eclipse.jgit.lib.ProgressMonitor;
47  import org.eclipse.jgit.lib.Ref;
48  import org.eclipse.jgit.lib.RefDatabase;
49  import org.eclipse.jgit.revwalk.ObjectWalk;
50  import org.eclipse.jgit.revwalk.RevWalk;
51  import org.eclipse.jgit.util.StringUtils;
52  
53  class FetchProcess {
54  	/** Transport we will fetch over. */
55  	private final Transport transport;
56  
57  	/** List of things we want to fetch from the remote repository. */
58  	private final Collection<RefSpec> toFetch;
59  
60  	/**
61  	 * List of things we don't want to fetch from the remote repository or to
62  	 * the local repository.
63  	 */
64  	private final Collection<RefSpec> negativeRefSpecs;
65  
66  	/** Set of refs we will actually wind up asking to obtain. */
67  	private final HashMap<ObjectId, Ref> askFor = new HashMap<>();
68  
69  	/** Objects we know we have locally. */
70  	private final HashSet<ObjectId> have = new HashSet<>();
71  
72  	/** Updates to local tracking branches (if any). */
73  	private final ArrayList<TrackingRefUpdate> localUpdates = new ArrayList<>();
74  
75  	/** Records to be recorded into FETCH_HEAD. */
76  	private final ArrayList<FetchHeadRecord> fetchHeadUpdates = new ArrayList<>();
77  
78  	private final ArrayList<PackLock> packLocks = new ArrayList<>();
79  
80  	private FetchConnection conn;
81  
82  	private Map<String, Ref> localRefs;
83  
84  	FetchProcess(Transport t, Collection<RefSpec> refSpecs) {
85  		transport = t;
86  		toFetch = refSpecs.stream().filter(refSpec -> !refSpec.isNegative())
87  				.collect(Collectors.toList());
88  		negativeRefSpecs = refSpecs.stream().filter(RefSpec::isNegative)
89  				.collect(Collectors.toList());
90  	}
91  
92  	void execute(ProgressMonitor monitor, FetchResult result,
93  			String initialBranch)
94  			throws NotSupportedException, TransportException {
95  		askFor.clear();
96  		localUpdates.clear();
97  		fetchHeadUpdates.clear();
98  		packLocks.clear();
99  		localRefs = null;
100 
101 		Throwable e1 = null;
102 		try {
103 			executeImp(monitor, result, initialBranch);
104 		} catch (NotSupportedException | TransportException err) {
105 			e1 = err;
106 			throw err;
107 		} finally {
108 			try {
109 				for (PackLock lock : packLocks) {
110 					lock.unlock();
111 				}
112 			} catch (IOException e) {
113 				if (e1 != null) {
114 					e.addSuppressed(e1);
115 				}
116 				throw new TransportException(e.getMessage(), e);
117 			}
118 		}
119 	}
120 
121 	private boolean isInitialBranchMissing(Map<String, Ref> refsMap,
122 			String initialBranch) {
123 		if (StringUtils.isEmptyOrNull(initialBranch) || refsMap.isEmpty()) {
124 			return false;
125 		}
126 		if (refsMap.containsKey(initialBranch)
127 				|| refsMap.containsKey(Constants.R_HEADS + initialBranch)
128 				|| refsMap.containsKey(Constants.R_TAGS + initialBranch)) {
129 			return false;
130 		}
131 		return true;
132 	}
133 
134 	private void executeImp(final ProgressMonitor monitor,
135 			final FetchResult result, String initialBranch)
136 			throws NotSupportedException, TransportException {
137 		final TagOpt tagopt = transport.getTagOpt();
138 		String getTags = (tagopt == TagOpt.NO_TAGS) ? null : Constants.R_TAGS;
139 		String getHead = null;
140 		try {
141 			// If we don't have a HEAD yet, we're cloning and need to get the
142 			// upstream HEAD, too.
143 			Ref head = transport.local.exactRef(Constants.HEAD);
144 			ObjectId id = head != null ? head.getObjectId() : null;
145 			if (id == null || id.equals(ObjectId.zeroId())) {
146 				getHead = Constants.HEAD;
147 			}
148 		} catch (IOException e) {
149 			// Ignore
150 		}
151 		conn = transport.openFetch(toFetch, getTags, getHead);
152 		try {
153 			Map<String, Ref> refsMap = conn.getRefsMap();
154 			if (isInitialBranchMissing(refsMap, initialBranch)) {
155 				throw new TransportException(MessageFormat.format(
156 						JGitText.get().remoteBranchNotFound, initialBranch));
157 			}
158 			result.setAdvertisedRefs(transport.getURI(), refsMap);
159 			result.peerUserAgent = conn.getPeerUserAgent();
160 			final Set<Ref> matched = new HashSet<>();
161 			for (RefSpec spec : toFetch) {
162 				if (spec.getSource() == null)
163 					throw new TransportException(MessageFormat.format(
164 							JGitText.get().sourceRefNotSpecifiedForRefspec, spec));
165 
166 				if (spec.isWildcard())
167 					expandWildcard(spec, matched);
168 				else
169 					expandSingle(spec, matched);
170 			}
171 
172 			Collection<Ref> additionalTags = Collections.<Ref> emptyList();
173 			if (tagopt == TagOpt.AUTO_FOLLOW)
174 				additionalTags = expandAutoFollowTags();
175 			else if (tagopt == TagOpt.FETCH_TAGS)
176 				expandFetchTags();
177 
178 			final boolean includedTags;
179 			if (!askFor.isEmpty() && !askForIsComplete()) {
180 				fetchObjects(monitor);
181 				includedTags = conn.didFetchIncludeTags();
182 
183 				// Connection was used for object transfer. If we
184 				// do another fetch we must open a new connection.
185 				//
186 				closeConnection(result);
187 			} else {
188 				includedTags = false;
189 			}
190 
191 			if (tagopt == TagOpt.AUTO_FOLLOW && !additionalTags.isEmpty()) {
192 				// There are more tags that we want to follow, but
193 				// not all were asked for on the initial request.
194 				//
195 				have.addAll(askFor.keySet());
196 				askFor.clear();
197 				for (Ref r : additionalTags) {
198 					ObjectId id = r.getPeeledObjectId();
199 					if (id == null)
200 						id = r.getObjectId();
201 					if (localHasObject(id))
202 						wantTag(r);
203 				}
204 
205 				if (!askFor.isEmpty() && (!includedTags || !askForIsComplete())) {
206 					reopenConnection();
207 					if (!askFor.isEmpty())
208 						fetchObjects(monitor);
209 				}
210 			}
211 		} finally {
212 			closeConnection(result);
213 		}
214 
215 		BatchRefUpdate batch = transport.local.getRefDatabase()
216 				.newBatchUpdate()
217 				.setAllowNonFastForwards(true)
218 				.setRefLogMessage("fetch", true); //$NON-NLS-1$
219 		try (RevWalk walk = new RevWalk(transport.local)) {
220 			walk.setRetainBody(false);
221 			if (monitor instanceof BatchingProgressMonitor) {
222 				((BatchingProgressMonitor) monitor).setDelayStart(
223 						250, TimeUnit.MILLISECONDS);
224 			}
225 			if (transport.isRemoveDeletedRefs()) {
226 				deleteStaleTrackingRefs(result, batch);
227 			}
228 			addUpdateBatchCommands(result, batch);
229 			for (ReceiveCommand cmd : batch.getCommands()) {
230 				cmd.updateType(walk);
231 				if (cmd.getType() == UPDATE_NONFASTFORWARD
232 						&& cmd instanceof TrackingRefUpdate.Command
233 						&& !((TrackingRefUpdate.Command) cmd).canForceUpdate())
234 					cmd.setResult(REJECTED_NONFASTFORWARD);
235 			}
236 			if (transport.isDryRun()) {
237 				for (ReceiveCommand cmd : batch.getCommands()) {
238 					if (cmd.getResult() == NOT_ATTEMPTED)
239 						cmd.setResult(OK);
240 				}
241 			} else {
242 				batch.execute(walk, monitor);
243 			}
244 		} catch (TransportException e) {
245 			throw e;
246 		} catch (IOException err) {
247 			throw new TransportException(MessageFormat.format(
248 					JGitText.get().failureUpdatingTrackingRef,
249 					getFirstFailedRefName(batch), err.getMessage()), err);
250 		}
251 
252 		if (!fetchHeadUpdates.isEmpty()) {
253 			try {
254 				updateFETCH_HEAD(result);
255 			} catch (IOException err) {
256 				throw new TransportException(MessageFormat.format(
257 						JGitText.get().failureUpdatingFETCH_HEAD, err.getMessage()), err);
258 			}
259 		}
260 	}
261 
262 	private void addUpdateBatchCommands(FetchResult result,
263 			BatchRefUpdate batch) throws TransportException {
264 		Map<String, ObjectId> refs = new HashMap<>();
265 		for (TrackingRefUpdate u : localUpdates) {
266 			// Try to skip duplicates if they'd update to the same object ID
267 			ObjectId existing = refs.get(u.getLocalName());
268 			if (existing == null) {
269 				refs.put(u.getLocalName(), u.getNewObjectId());
270 				result.add(u);
271 				batch.addCommand(u.asReceiveCommand());
272 			} else if (!existing.equals(u.getNewObjectId())) {
273 				throw new TransportException(MessageFormat
274 						.format(JGitText.get().duplicateRef, u.getLocalName()));
275 			}
276 		}
277 	}
278 
279 	private void fetchObjects(ProgressMonitor monitor)
280 			throws TransportException {
281 		try {
282 			conn.setPackLockMessage("jgit fetch " + transport.uri); //$NON-NLS-1$
283 			conn.fetch(monitor, askFor.values(), have);
284 		} finally {
285 			packLocks.addAll(conn.getPackLocks());
286 		}
287 		if (transport.isCheckFetchedObjects()
288 				&& !conn.didFetchTestConnectivity() && !askForIsComplete())
289 			throw new TransportException(transport.getURI(),
290 					JGitText.get().peerDidNotSupplyACompleteObjectGraph);
291 	}
292 
293 	private void closeConnection(FetchResult result) {
294 		if (conn != null) {
295 			conn.close();
296 			result.addMessages(conn.getMessages());
297 			conn = null;
298 		}
299 	}
300 
301 	private void reopenConnection() throws NotSupportedException,
302 			TransportException {
303 		if (conn != null)
304 			return;
305 
306 		// Build prefixes
307 		Set<String> prefixes = new HashSet<>();
308 		for (Ref toGet : askFor.values()) {
309 			String src = toGet.getName();
310 			prefixes.add(src);
311 			prefixes.add(Constants.R_REFS + src);
312 			prefixes.add(Constants.R_HEADS + src);
313 			prefixes.add(Constants.R_TAGS + src);
314 		}
315 		conn = transport.openFetch(Collections.emptyList(),
316 				prefixes.toArray(new String[0]));
317 
318 		// Since we opened a new connection we cannot be certain
319 		// that the system we connected to has the same exact set
320 		// of objects available (think round-robin DNS and mirrors
321 		// that aren't updated at the same time).
322 		//
323 		// We rebuild our askFor list using only the refs that the
324 		// new connection has offered to us.
325 		//
326 		final HashMap<ObjectId, Ref> avail = new HashMap<>();
327 		for (Ref r : conn.getRefs())
328 			avail.put(r.getObjectId(), r);
329 
330 		final Collection<Ref> wants = new ArrayList<>(askFor.values());
331 		askFor.clear();
332 		for (Ref want : wants) {
333 			final Ref newRef = avail.get(want.getObjectId());
334 			if (newRef != null) {
335 				askFor.put(newRef.getObjectId(), newRef);
336 			} else {
337 				removeFetchHeadRecord(want.getObjectId());
338 				removeTrackingRefUpdate(want.getObjectId());
339 			}
340 		}
341 	}
342 
343 	private void removeTrackingRefUpdate(ObjectId want) {
344 		final Iterator<TrackingRefUpdate> i = localUpdates.iterator();
345 		while (i.hasNext()) {
346 			final TrackingRefUpdate u = i.next();
347 			if (u.getNewObjectId().equals(want))
348 				i.remove();
349 		}
350 	}
351 
352 	private void removeFetchHeadRecord(ObjectId want) {
353 		final Iterator<FetchHeadRecord> i = fetchHeadUpdates.iterator();
354 		while (i.hasNext()) {
355 			final FetchHeadRecord fh = i.next();
356 			if (fh.newValue.equals(want))
357 				i.remove();
358 		}
359 	}
360 
361 	private void updateFETCH_HEAD(FetchResult result) throws IOException {
362 		File meta = transport.local.getDirectory();
363 		if (meta == null)
364 			return;
365 		final LockFile lock = new LockFile(new File(meta, "FETCH_HEAD")); //$NON-NLS-1$
366 		try {
367 			if (lock.lock()) {
368 				try (Writer w = new OutputStreamWriter(
369 						lock.getOutputStream(), UTF_8)) {
370 					for (FetchHeadRecord h : fetchHeadUpdates) {
371 						h.write(w);
372 						result.add(h);
373 					}
374 				}
375 				lock.commit();
376 			}
377 		} finally {
378 			lock.unlock();
379 		}
380 	}
381 
382 	private boolean askForIsComplete() throws TransportException {
383 		try {
384 			try (ObjectWalk ow = new ObjectWalk(transport.local)) {
385 				for (ObjectId want : askFor.keySet())
386 					ow.markStart(ow.parseAny(want));
387 				for (Ref ref : localRefs().values())
388 					ow.markUninteresting(ow.parseAny(ref.getObjectId()));
389 				ow.checkConnectivity();
390 			}
391 			return true;
392 		} catch (MissingObjectException e) {
393 			return false;
394 		} catch (IOException e) {
395 			throw new TransportException(JGitText.get().unableToCheckConnectivity, e);
396 		}
397 	}
398 
399 	private void expandWildcard(RefSpec spec, Set<Ref> matched)
400 			throws TransportException {
401 		for (Ref src : conn.getRefs()) {
402 			if (spec.matchSource(src)) {
403 				RefSpec expandedRefSpec = spec.expandFromSource(src);
404 				if (!matchNegativeRefSpec(expandedRefSpec)
405 						&& matched.add(src)) {
406 					want(src, expandedRefSpec);
407 				}
408 			}
409 		}
410 	}
411 
412 	private void expandSingle(RefSpec spec, Set<Ref> matched)
413 			throws TransportException {
414 		String want = spec.getSource();
415 		if (ObjectId.isId(want)) {
416 			want(ObjectId.fromString(want));
417 			return;
418 		}
419 
420 		Ref src = conn.getRef(want);
421 		if (src == null) {
422 			throw new TransportException(MessageFormat.format(JGitText.get().remoteDoesNotHaveSpec, want));
423 		}
424 		if (!matchNegativeRefSpec(spec) && matched.add(src)) {
425 			want(src, spec);
426 		}
427 	}
428 
429 	private boolean matchNegativeRefSpec(RefSpec spec) {
430 		for (RefSpec negativeRefSpec : negativeRefSpecs) {
431 			if (negativeRefSpec.getSource() != null && spec.getSource() != null
432 					&& negativeRefSpec.matchSource(spec.getSource())) {
433 				return true;
434 			}
435 
436 			if (negativeRefSpec.getDestination() != null
437 					&& spec.getDestination() != null && negativeRefSpec
438 							.matchDestination(spec.getDestination())) {
439 				return true;
440 			}
441 		}
442 		return false;
443 	}
444 
445 	private boolean localHasObject(ObjectId id) throws TransportException {
446 		try {
447 			return transport.local.getObjectDatabase().has(id);
448 		} catch (IOException err) {
449 			throw new TransportException(
450 					MessageFormat.format(
451 							JGitText.get().readingObjectsFromLocalRepositoryFailed,
452 							err.getMessage()),
453 					err);
454 		}
455 	}
456 
457 	private Collection<Ref> expandAutoFollowTags() throws TransportException {
458 		final Collection<Ref> additionalTags = new ArrayList<>();
459 		final Map<String, Ref> haveRefs = localRefs();
460 		for (Ref r : conn.getRefs()) {
461 			if (!isTag(r))
462 				continue;
463 
464 			Ref local = haveRefs.get(r.getName());
465 			if (local != null)
466 				// We already have a tag with this name, don't fetch it (even if
467 				// the local is different).
468 				continue;
469 
470 			ObjectId obj = r.getPeeledObjectId();
471 			if (obj == null)
472 				obj = r.getObjectId();
473 
474 			if (askFor.containsKey(obj) || localHasObject(obj))
475 				wantTag(r);
476 			else
477 				additionalTags.add(r);
478 		}
479 		return additionalTags;
480 	}
481 
482 	private void expandFetchTags() throws TransportException {
483 		final Map<String, Ref> haveRefs = localRefs();
484 		for (Ref r : conn.getRefs()) {
485 			if (!isTag(r)) {
486 				continue;
487 			}
488 			ObjectId id = r.getObjectId();
489 			if (id == null) {
490 				continue;
491 			}
492 			final Ref local = haveRefs.get(r.getName());
493 			if (local == null || !id.equals(local.getObjectId())) {
494 				wantTag(r);
495 			}
496 		}
497 	}
498 
499 	private void wantTag(Ref r) throws TransportException {
500 		want(r, new RefSpec().setSource(r.getName())
501 				.setDestination(r.getName()).setForceUpdate(true));
502 	}
503 
504 	private void want(Ref src, RefSpec spec)
505 			throws TransportException {
506 		final ObjectId newId = src.getObjectId();
507 		if (newId == null) {
508 			throw new NullPointerException(MessageFormat.format(
509 					JGitText.get().transportProvidedRefWithNoObjectId,
510 					src.getName()));
511 		}
512 		if (spec.getDestination() != null) {
513 			final TrackingRefUpdate tru = createUpdate(spec, newId);
514 			if (newId.equals(tru.getOldObjectId()))
515 				return;
516 			localUpdates.add(tru);
517 		}
518 
519 		askFor.put(newId, src);
520 
521 		final FetchHeadRecord fhr = new FetchHeadRecord();
522 		fhr.newValue = newId;
523 		fhr.notForMerge = spec.getDestination() != null;
524 		fhr.sourceName = src.getName();
525 		fhr.sourceURI = transport.getURI();
526 		fetchHeadUpdates.add(fhr);
527 	}
528 
529 	private void want(ObjectId id) {
530 		askFor.put(id,
531 				new ObjectIdRef.Unpeeled(Ref.Storage.NETWORK, id.name(), id));
532 	}
533 
534 	private TrackingRefUpdate createUpdate(RefSpec spec, ObjectId newId)
535 			throws TransportException {
536 		Ref ref = localRefs().get(spec.getDestination());
537 		ObjectId oldId = ref != null && ref.getObjectId() != null
538 				? ref.getObjectId()
539 				: ObjectId.zeroId();
540 		return new TrackingRefUpdate(
541 				spec.isForceUpdate(),
542 				spec.getSource(),
543 				spec.getDestination(),
544 				oldId,
545 				newId);
546 	}
547 
548 	private Map<String, Ref> localRefs() throws TransportException {
549 		if (localRefs == null) {
550 			try {
551 				localRefs = transport.local.getRefDatabase()
552 						.getRefs(RefDatabase.ALL);
553 			} catch (IOException err) {
554 				throw new TransportException(JGitText.get().cannotListRefs, err);
555 			}
556 		}
557 		return localRefs;
558 	}
559 
560 	private void deleteStaleTrackingRefs(FetchResult result,
561 			BatchRefUpdate batch) throws IOException {
562 		Set<Ref> processed = new HashSet<>();
563 		for (Ref ref : localRefs().values()) {
564 			if (ref.isSymbolic()) {
565 				continue;
566 			}
567 			String refname = ref.getName();
568 			for (RefSpec spec : toFetch) {
569 				if (spec.matchDestination(refname)) {
570 					RefSpec s = spec.expandFromDestination(refname);
571 					if (result.getAdvertisedRef(s.getSource()) == null
572 							&& processed.add(ref)) {
573 						deleteTrackingRef(result, batch, s, ref);
574 					}
575 				}
576 			}
577 		}
578 	}
579 
580 	private void deleteTrackingRef(final FetchResult result,
581 			final BatchRefUpdate batch, final RefSpec spec, final Ref localRef) {
582 		if (localRef.getObjectId() == null)
583 			return;
584 		TrackingRefUpdate update = new TrackingRefUpdate(
585 				true,
586 				spec.getSource(),
587 				localRef.getName(),
588 				localRef.getObjectId(),
589 				ObjectId.zeroId());
590 		result.add(update);
591 		batch.addCommand(update.asReceiveCommand());
592 	}
593 
594 	private static boolean isTag(Ref r) {
595 		return isTag(r.getName());
596 	}
597 
598 	private static boolean isTag(String name) {
599 		return name.startsWith(Constants.R_TAGS);
600 	}
601 
602 	private static String getFirstFailedRefName(BatchRefUpdate batch) {
603 		for (ReceiveCommand cmd : batch.getCommands()) {
604 			if (cmd.getResult() != ReceiveCommand.Result.OK)
605 				return cmd.getRefName();
606 		}
607 		return ""; //$NON-NLS-1$
608 	}
609 }