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