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