View Javadoc
1   /*
2    * Copyright (C) 2019, Google Inc. and others
3    *
4    * This program and the accompanying materials are made available under the
5    * terms of the Eclipse Distribution License v. 1.0 which is available at
6    * https://www.eclipse.org/org/documents/edl-v10.php.
7    *
8    * SPDX-License-Identifier: BSD-3-Clause
9    */
10  
11  package org.eclipse.jgit.internal.storage.reftable;
12  
13  import org.eclipse.jgit.annotations.Nullable;
14  import org.eclipse.jgit.errors.MissingObjectException;
15  import org.eclipse.jgit.internal.JGitText;
16  import org.eclipse.jgit.lib.AnyObjectId;
17  import org.eclipse.jgit.lib.BatchRefUpdate;
18  import org.eclipse.jgit.lib.ObjectId;
19  import org.eclipse.jgit.lib.ObjectIdRef;
20  import org.eclipse.jgit.lib.PersonIdent;
21  import org.eclipse.jgit.lib.ProgressMonitor;
22  import org.eclipse.jgit.lib.Ref;
23  import org.eclipse.jgit.lib.RefDatabase;
24  import org.eclipse.jgit.lib.ReflogEntry;
25  import org.eclipse.jgit.lib.Repository;
26  import org.eclipse.jgit.lib.SymbolicRef;
27  import org.eclipse.jgit.revwalk.RevObject;
28  import org.eclipse.jgit.revwalk.RevTag;
29  import org.eclipse.jgit.revwalk.RevWalk;
30  import org.eclipse.jgit.transport.ReceiveCommand;
31  
32  import java.io.IOException;
33  import java.util.ArrayList;
34  import java.util.Collections;
35  import java.util.HashMap;
36  import java.util.List;
37  import java.util.Map;
38  import java.util.Set;
39  import java.util.TreeSet;
40  import java.util.concurrent.locks.Lock;
41  import java.util.stream.Collectors;
42  
43  import static org.eclipse.jgit.lib.Ref.Storage.NEW;
44  import static org.eclipse.jgit.lib.Ref.Storage.PACKED;
45  
46  import static org.eclipse.jgit.transport.ReceiveCommand.Result.LOCK_FAILURE;
47  import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED;
48  import static org.eclipse.jgit.transport.ReceiveCommand.Result.OK;
49  import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_MISSING_OBJECT;
50  import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_NONFASTFORWARD;
51  import static org.eclipse.jgit.transport.ReceiveCommand.Type.DELETE;
52  import static org.eclipse.jgit.transport.ReceiveCommand.Type.UPDATE_NONFASTFORWARD;
53  
54  /**
55   * {@link org.eclipse.jgit.lib.BatchRefUpdate} for Reftable based RefDatabase.
56   */
57  public abstract class ReftableBatchRefUpdate extends BatchRefUpdate {
58  	private final Lock lock;
59  
60  	private final ReftableDatabase refDb;
61  
62  	private final Repository repository;
63  
64  	/**
65  	 * Initialize.
66  	 *
67  	 * @param refdb
68  	 *            The RefDatabase
69  	 * @param reftableDb
70  	 *            The ReftableDatabase
71  	 * @param lock
72  	 *            A lock protecting the refdatabase's state
73  	 * @param repository
74  	 *            The repository on which this update will run
75  	 */
76  	protected ReftableBatchRefUpdate(RefDatabase refdb, ReftableDatabase reftableDb, Lock lock,
77  			Repository repository) {
78  		super(refdb);
79  		this.refDb = reftableDb;
80  		this.lock = lock;
81  		this.repository = repository;
82  	}
83  
84  	/** {@inheritDoc} */
85  	@Override
86  	public void execute(RevWalk rw, ProgressMonitor pm, List<String> options) {
87  		List<ReceiveCommand> pending = getPending();
88  		if (pending.isEmpty()) {
89  			return;
90  		}
91  		if (options != null) {
92  			setPushOptions(options);
93  		}
94  		try {
95  			if (!checkObjectExistence(rw, pending)) {
96  				return;
97  			}
98  			// if we are here, checkObjectExistence might have flagged some problems
99  			// but the transaction is not atomic, so we should proceed with the other
100 			// pending commands.
101 			pending = getPending();
102 			if (!checkNonFastForwards(rw, pending)) {
103 				return;
104 			}
105 			pending = getPending();
106 
107 			lock.lock();
108 			try {
109 				if (!checkExpected(pending)) {
110 					return;
111 				}
112 				pending = getPending();
113 				if (!checkConflicting(pending)) {
114 					return;
115 				}
116 				pending = getPending();
117 				if (!blockUntilTimestamps(MAX_WAIT)) {
118 					return;
119 				}
120 
121 				List<Ref> newRefs = toNewRefs(rw, pending);
122 				applyUpdates(newRefs, pending);
123 				for (ReceiveCommand cmd : pending) {
124 					if (cmd.getResult() == NOT_ATTEMPTED) {
125 						// XXX this is a bug in DFS ?
126 						cmd.setResult(OK);
127 					}
128 				}
129 			} finally {
130 				lock.unlock();
131 			}
132 		} catch (IOException e) {
133 			pending.get(0).setResult(LOCK_FAILURE, "io error"); //$NON-NLS-1$
134 			ReceiveCommand.abort(pending);
135 		}
136 	}
137 
138 	/**
139 	 * Implements the storage-specific part of the update.
140 	 *
141 	 * @param newRefs
142 	 *            the new refs to create
143 	 * @param pending
144 	 *            the pending receive commands to be executed
145 	 * @throws IOException
146 	 *             if any of the writes fail.
147 	 */
148 	protected abstract void applyUpdates(List<Ref> newRefs,
149 			List<ReceiveCommand> pending) throws IOException;
150 
151 	private List<ReceiveCommand> getPending() {
152 		return ReceiveCommand.filter(getCommands(), NOT_ATTEMPTED);
153 	}
154 
155 	private boolean checkObjectExistence(RevWalk rw,
156 			List<ReceiveCommand> pending) throws IOException {
157 		for (ReceiveCommand cmd : pending) {
158 			try {
159 				if (!cmd.getNewId().equals(ObjectId.zeroId())) {
160 					rw.parseAny(cmd.getNewId());
161 				}
162 			} catch (MissingObjectException e) {
163 				// ReceiveCommand#setResult(Result) converts REJECTED to
164 				// REJECTED_NONFASTFORWARD, even though that result is also
165 				// used for a missing object. Eagerly handle this case so we
166 				// can set the right result.
167 				cmd.setResult(REJECTED_MISSING_OBJECT);
168 				if (isAtomic()) {
169 					ReceiveCommand.abort(pending);
170 					return false;
171 				}
172 			}
173 		}
174 		return true;
175 	}
176 
177 	private boolean checkNonFastForwards(RevWalk rw,
178 			List<ReceiveCommand> pending) throws IOException {
179 		if (isAllowNonFastForwards()) {
180 			return true;
181 		}
182 		for (ReceiveCommand cmd : pending) {
183 			cmd.updateType(rw);
184 			if (cmd.getType() == UPDATE_NONFASTFORWARD) {
185 				cmd.setResult(REJECTED_NONFASTFORWARD);
186 				if (isAtomic()) {
187 					ReceiveCommand.abort(pending);
188 					return false;
189 				}
190 			}
191 		}
192 		return true;
193 	}
194 
195 	private boolean checkConflicting(List<ReceiveCommand> pending)
196 			throws IOException {
197 		TreeSet<String> added = new TreeSet<>();
198 		Set<String> deleted =
199 				pending.stream()
200 						.filter(cmd -> cmd.getType() == DELETE)
201 						.map(c -> c.getRefName())
202 						.collect(Collectors.toSet());
203 
204 		boolean ok = true;
205 		for (ReceiveCommand cmd : pending) {
206 			if (cmd.getType() == DELETE) {
207 				continue;
208 			}
209 
210 			String name = cmd.getRefName();
211 			if (refDb.isNameConflicting(name, added, deleted)) {
212 				if (isAtomic()) {
213 					cmd.setResult(
214 							ReceiveCommand.Result.REJECTED_OTHER_REASON, JGitText.get().transactionAborted);
215 				} else {
216 					cmd.setResult(LOCK_FAILURE);
217 				}
218 
219 				ok = false;
220 			}
221 			added.add(name);
222 		}
223 
224 		if (isAtomic()) {
225 			if (!ok) {
226 				pending.stream()
227 						.filter(cmd -> cmd.getResult() == NOT_ATTEMPTED)
228 						.forEach(cmd -> cmd.setResult(LOCK_FAILURE));
229 			}
230 			return ok;
231 		}
232 
233 		for (ReceiveCommand cmd : pending) {
234 			if (cmd.getResult() == NOT_ATTEMPTED) {
235 				return true;
236 			}
237 		}
238 
239 		return false;
240 	}
241 
242 	private boolean checkExpected(List<ReceiveCommand> pending)
243 			throws IOException {
244 		for (ReceiveCommand cmd : pending) {
245 			if (!matchOld(cmd, refDb.exactRef(cmd.getRefName()))) {
246 				cmd.setResult(LOCK_FAILURE);
247 				if (isAtomic()) {
248 					ReceiveCommand.abort(pending);
249 					return false;
250 				}
251 			}
252 		}
253 		return true;
254 	}
255 
256 	private static boolean matchOld(ReceiveCommand cmd, @Nullable Ref ref) {
257 		if (ref == null) {
258 			return AnyObjectId.isEqual(ObjectId.zeroId(), cmd.getOldId())
259 				&& cmd.getOldSymref() == null;
260 		} else if (ref.isSymbolic()) {
261 			return ref.getTarget().getName().equals(cmd.getOldSymref());
262 		}
263 		ObjectId id = ref.getObjectId();
264 		if (id == null) {
265 			id = ObjectId.zeroId();
266 		}
267 		return cmd.getOldId().equals(id);
268 	}
269 
270 	/**
271 	 * Writes the refs to the writer, and calls finish.
272 	 *
273 	 * @param writer
274 	 *            the writer on which we should write.
275 	 * @param newRefs
276 	 *            the ref data to write..
277 	 * @param pending
278 	 *            the log data to write.
279 	 * @throws IOException
280 	 *            in case of problems.
281 	 */
282 	protected void write(ReftableWriter writer, List<Ref> newRefs,
283 			List<ReceiveCommand> pending) throws IOException {
284 		long updateIndex = refDb.nextUpdateIndex();
285 		writer.setMinUpdateIndex(updateIndex).setMaxUpdateIndex(updateIndex)
286 				.begin().sortAndWriteRefs(newRefs);
287 		if (!isRefLogDisabled()) {
288 			writeLog(writer, updateIndex, pending);
289 		}
290 	}
291 
292 	private void writeLog(ReftableWriter writer, long updateIndex,
293 			List<ReceiveCommand> pending) throws IOException {
294 		Map<String, ReceiveCommand> cmds = new HashMap<>();
295 		List<String> byName = new ArrayList<>(pending.size());
296 		for (ReceiveCommand cmd : pending) {
297 			cmds.put(cmd.getRefName(), cmd);
298 			byName.add(cmd.getRefName());
299 		}
300 		Collections.sort(byName);
301 
302 		PersonIdent ident = getRefLogIdent();
303 		if (ident == null) {
304 			ident = new PersonIdent(repository);
305 		}
306 		for (String name : byName) {
307 			ReceiveCommand cmd = cmds.get(name);
308 			if (isRefLogDisabled(cmd)) {
309 				continue;
310 			}
311 			String msg = getRefLogMessage(cmd);
312 			if (isRefLogIncludingResult(cmd)) {
313 				String strResult = toResultString(cmd);
314 				if (strResult != null) {
315 					msg = msg.isEmpty() ? strResult : msg + ": " + strResult; //$NON-NLS-1$
316 				}
317 			}
318 			writer.writeLog(name, updateIndex, ident, cmd.getOldId(),
319 					cmd.getNewId(), msg);
320 		}
321 	}
322 
323 	private String toResultString(ReceiveCommand cmd) {
324 		switch (cmd.getType()) {
325 		case CREATE:
326 			return ReflogEntry.PREFIX_CREATED;
327 		case UPDATE:
328 			// Match the behavior of a single RefUpdate. In that case, setting
329 			// the force bit completely bypasses the potentially expensive
330 			// isMergedInto check, by design, so the reflog message may be
331 			// inaccurate.
332 			//
333 			// Similarly, this class bypasses the isMergedInto checks when the
334 			// force bit is set, meaning we can't actually distinguish between
335 			// UPDATE and UPDATE_NONFASTFORWARD when isAllowNonFastForwards()
336 			// returns true.
337 			return isAllowNonFastForwards() ? ReflogEntry.PREFIX_FORCED_UPDATE
338 					: ReflogEntry.PREFIX_FAST_FORWARD;
339 		case UPDATE_NONFASTFORWARD:
340 			return ReflogEntry.PREFIX_FORCED_UPDATE;
341 		default:
342 			return null;
343 		}
344 	}
345 
346 	// Extracts and peels the refs out of the ReceiveCommands
347 	private static List<Ref> toNewRefs(RevWalk rw, List<ReceiveCommand> pending)
348 		throws IOException {
349 		List<Ref> refs = new ArrayList<>(pending.size());
350 		for (ReceiveCommand cmd : pending) {
351 			if (cmd.getResult() != NOT_ATTEMPTED) {
352 				continue;
353 			}
354 
355 			String name = cmd.getRefName();
356 			ObjectId newId = cmd.getNewId();
357 			String newSymref = cmd.getNewSymref();
358 			if (AnyObjectId.isEqual(ObjectId.zeroId(), newId)
359 				&& newSymref == null) {
360 				refs.add(new ObjectIdRef.Unpeeled(NEW, name, null));
361 				continue;
362 			} else if (newSymref != null) {
363 				refs.add(new SymbolicRef(name,
364 					new ObjectIdRef.Unpeeled(NEW, newSymref, null)));
365 				continue;
366 			}
367 
368 			RevObject obj = rw.parseAny(newId);
369 			RevObject peel = null;
370 			if (obj instanceof RevTag) {
371 				peel = rw.peel(obj);
372 			}
373 			if (peel != null) {
374 				refs.add(new ObjectIdRef.PeeledTag(PACKED, name, newId,
375 					peel.copy()));
376 			} else {
377 				refs.add(new ObjectIdRef.PeeledNonTag(PACKED, name, newId));
378 			}
379 		}
380 		return refs;
381 	}
382 }