View Javadoc
1   /*
2    * Copyright (C) 2008-2012, Google Inc.
3    * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
4    * and other copyright owners as documented in the project's IP log.
5    *
6    * This program and the accompanying materials are made available
7    * under the terms of the Eclipse Distribution License v1.0 which
8    * accompanies this distribution, is reproduced below, and is
9    * available at http://www.eclipse.org/org/documents/edl-v10.php
10   *
11   * All rights reserved.
12   *
13   * Redistribution and use in source and binary forms, with or
14   * without modification, are permitted provided that the following
15   * conditions are met:
16   *
17   * - Redistributions of source code must retain the above copyright
18   *   notice, this list of conditions and the following disclaimer.
19   *
20   * - Redistributions in binary form must reproduce the above
21   *   copyright notice, this list of conditions and the following
22   *   disclaimer in the documentation and/or other materials provided
23   *   with the distribution.
24   *
25   * - Neither the name of the Eclipse Foundation, Inc. nor the
26   *   names of its contributors may be used to endorse or promote
27   *   products derived from this software without specific prior
28   *   written permission.
29   *
30   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
31   * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
32   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
33   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
34   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
35   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
36   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
37   * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
38   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
39   * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
40   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
41   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
42   * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
43   */
44  
45  package org.eclipse.jgit.lib;
46  
47  import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED;
48  import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON;
49  
50  import java.io.IOException;
51  import java.text.MessageFormat;
52  import java.util.ArrayList;
53  import java.util.Arrays;
54  import java.util.Collection;
55  import java.util.Collections;
56  import java.util.HashSet;
57  import java.util.List;
58  
59  import org.eclipse.jgit.internal.JGitText;
60  import org.eclipse.jgit.lib.RefUpdate.Result;
61  import org.eclipse.jgit.revwalk.RevWalk;
62  import org.eclipse.jgit.transport.PushCertificate;
63  import org.eclipse.jgit.transport.ReceiveCommand;
64  
65  /**
66   * Batch of reference updates to be applied to a repository.
67   * <p>
68   * The batch update is primarily useful in the transport code, where a client or
69   * server is making changes to more than one reference at a time.
70   */
71  public class BatchRefUpdate {
72  	private final RefDatabase refdb;
73  
74  	/** Commands to apply during this batch. */
75  	private final List<ReceiveCommand> commands;
76  
77  	/** Does the caller permit a forced update on a reference? */
78  	private boolean allowNonFastForwards;
79  
80  	/** Identity to record action as within the reflog. */
81  	private PersonIdent refLogIdent;
82  
83  	/** Message the caller wants included in the reflog. */
84  	private String refLogMessage;
85  
86  	/** Should the result value be appended to {@link #refLogMessage}. */
87  	private boolean refLogIncludeResult;
88  
89  	/** Push certificate associated with this update. */
90  	private PushCertificate pushCert;
91  
92  	/**
93  	 * Initialize a new batch update.
94  	 *
95  	 * @param refdb
96  	 *            the reference database of the repository to be updated.
97  	 */
98  	protected BatchRefUpdate(RefDatabase refdb) {
99  		this.refdb = refdb;
100 		this.commands = new ArrayList<ReceiveCommand>();
101 	}
102 
103 	/**
104 	 * @return true if the batch update will permit a non-fast-forward update to
105 	 *         an existing reference.
106 	 */
107 	public boolean isAllowNonFastForwards() {
108 		return allowNonFastForwards;
109 	}
110 
111 	/**
112 	 * Set if this update wants to permit a forced update.
113 	 *
114 	 * @param allow
115 	 *            true if this update batch should ignore merge tests.
116 	 * @return {@code this}.
117 	 */
118 	public BatchRefUpdate setAllowNonFastForwards(boolean allow) {
119 		allowNonFastForwards = allow;
120 		return this;
121 	}
122 
123 	/** @return identity of the user making the change in the reflog. */
124 	public PersonIdent getRefLogIdent() {
125 		return refLogIdent;
126 	}
127 
128 	/**
129 	 * Set the identity of the user appearing in the reflog.
130 	 * <p>
131 	 * The timestamp portion of the identity is ignored. A new identity with the
132 	 * current timestamp will be created automatically when the update occurs
133 	 * and the log record is written.
134 	 *
135 	 * @param pi
136 	 *            identity of the user. If null the identity will be
137 	 *            automatically determined based on the repository
138 	 *            configuration.
139 	 * @return {@code this}.
140 	 */
141 	public BatchRefUpdate setRefLogIdent(final PersonIdent pi) {
142 		refLogIdent = pi;
143 		return this;
144 	}
145 
146 	/**
147 	 * Get the message to include in the reflog.
148 	 *
149 	 * @return message the caller wants to include in the reflog; null if the
150 	 *         update should not be logged.
151 	 */
152 	public String getRefLogMessage() {
153 		return refLogMessage;
154 	}
155 
156 	/** @return {@code true} if the ref log message should show the result. */
157 	public boolean isRefLogIncludingResult() {
158 		return refLogIncludeResult;
159 	}
160 
161 	/**
162 	 * Set the message to include in the reflog.
163 	 *
164 	 * @param msg
165 	 *            the message to describe this change. It may be null if
166 	 *            appendStatus is null in order not to append to the reflog
167 	 * @param appendStatus
168 	 *            true if the status of the ref change (fast-forward or
169 	 *            forced-update) should be appended to the user supplied
170 	 *            message.
171 	 * @return {@code this}.
172 	 */
173 	public BatchRefUpdate setRefLogMessage(String msg, boolean appendStatus) {
174 		if (msg == null && !appendStatus)
175 			disableRefLog();
176 		else if (msg == null && appendStatus) {
177 			refLogMessage = ""; //$NON-NLS-1$
178 			refLogIncludeResult = true;
179 		} else {
180 			refLogMessage = msg;
181 			refLogIncludeResult = appendStatus;
182 		}
183 		return this;
184 	}
185 
186 	/**
187 	 * Don't record this update in the ref's associated reflog.
188 	 *
189 	 * @return {@code this}.
190 	 */
191 	public BatchRefUpdate disableRefLog() {
192 		refLogMessage = null;
193 		refLogIncludeResult = false;
194 		return this;
195 	}
196 
197 	/** @return true if log has been disabled by {@link #disableRefLog()}. */
198 	public boolean isRefLogDisabled() {
199 		return refLogMessage == null;
200 	}
201 
202 	/**
203 	 * Set a push certificate associated with this update.
204 	 * <p>
205 	 * This usually includes commands to update the refs in this batch, but is not
206 	 * required to.
207 	 *
208 	 * @param cert
209 	 *            push certificate, may be null.
210 	 * @since 4.1
211 	 */
212 	public void setPushCertificate(PushCertificate cert) {
213 		pushCert = cert;
214 	}
215 
216 	/**
217 	 * Set the push certificate associated with this update.
218 	 * <p>
219 	 * This usually includes commands to update the refs in this batch, but is not
220 	 * required to.
221 	 *
222 	 * @return push certificate, may be null.
223 	 * @since 4.1
224 	 */
225 	protected PushCertificate getPushCertificate() {
226 		return pushCert;
227 	}
228 
229 	/** @return commands this update will process. */
230 	public List<ReceiveCommand> getCommands() {
231 		return Collections.unmodifiableList(commands);
232 	}
233 
234 	/**
235 	 * Add a single command to this batch update.
236 	 *
237 	 * @param cmd
238 	 *            the command to add, must not be null.
239 	 * @return {@code this}.
240 	 */
241 	public BatchRefUpdate addCommand(ReceiveCommand cmd) {
242 		commands.add(cmd);
243 		return this;
244 	}
245 
246 	/**
247 	 * Add commands to this batch update.
248 	 *
249 	 * @param cmd
250 	 *            the commands to add, must not be null.
251 	 * @return {@code this}.
252 	 */
253 	public BatchRefUpdate addCommand(ReceiveCommand... cmd) {
254 		return addCommand(Arrays.asList(cmd));
255 	}
256 
257 	/**
258 	 * Add commands to this batch update.
259 	 *
260 	 * @param cmd
261 	 *            the commands to add, must not be null.
262 	 * @return {@code this}.
263 	 */
264 	public BatchRefUpdate addCommand(Collection<ReceiveCommand> cmd) {
265 		commands.addAll(cmd);
266 		return this;
267 	}
268 
269 	/**
270 	 * Execute this batch update.
271 	 * <p>
272 	 * The default implementation of this method performs a sequential reference
273 	 * update over each reference.
274 	 *
275 	 * @param walk
276 	 *            a RevWalk to parse tags in case the storage system wants to
277 	 *            store them pre-peeled, a common performance optimization.
278 	 * @param monitor
279 	 *            progress monitor to receive update status on.
280 	 * @throws IOException
281 	 *             the database is unable to accept the update. Individual
282 	 *             command status must be tested to determine if there is a
283 	 *             partial failure, or a total failure.
284 	 */
285 	public void execute(RevWalk walk, ProgressMonitor monitor)
286 			throws IOException {
287 		monitor.beginTask(JGitText.get().updatingReferences, commands.size());
288 		List<ReceiveCommand> commands2 = new ArrayList<ReceiveCommand>(
289 				commands.size());
290 		List<String> namesToCheck = new ArrayList<String>(commands.size());
291 		// First delete refs. This may free the name space for some of the
292 		// updates.
293 		for (ReceiveCommand cmd : commands) {
294 			try {
295 				if (cmd.getResult() == NOT_ATTEMPTED) {
296 					cmd.updateType(walk);
297 					switch (cmd.getType()) {
298 					case CREATE:
299 						namesToCheck.add(cmd.getRefName());
300 						commands2.add(cmd);
301 						break;
302 					case UPDATE:
303 					case UPDATE_NONFASTFORWARD:
304 						commands2.add(cmd);
305 						break;
306 					case DELETE:
307 						RefUpdate rud = newUpdate(cmd);
308 						monitor.update(1);
309 						cmd.setResult(rud.delete(walk));
310 					}
311 				}
312 			} catch (IOException err) {
313 				cmd.setResult(
314 						REJECTED_OTHER_REASON,
315 						MessageFormat.format(JGitText.get().lockError,
316 								err.getMessage()));
317 			}
318 		}
319 		if (!commands2.isEmpty()) {
320 			// What part of the name space is already taken
321 			Collection<String> takenNames = new HashSet<String>(refdb.getRefs(
322 					RefDatabase.ALL).keySet());
323 			Collection<String> takenPrefixes = getTakenPrefixes(takenNames);
324 
325 			// Now to the update that may require more room in the name space
326 			for (ReceiveCommand cmd : commands2) {
327 				try {
328 					if (cmd.getResult() == NOT_ATTEMPTED) {
329 						cmd.updateType(walk);
330 						RefUpdate ru = newUpdate(cmd);
331 						SWITCH: switch (cmd.getType()) {
332 						case DELETE:
333 							// Performed in the first phase
334 							break;
335 						case UPDATE:
336 						case UPDATE_NONFASTFORWARD:
337 							RefUpdate ruu = newUpdate(cmd);
338 							cmd.setResult(ruu.update(walk));
339 							break;
340 						case CREATE:
341 							for (String prefix : getPrefixes(cmd.getRefName())) {
342 								if (takenNames.contains(prefix)) {
343 									cmd.setResult(Result.LOCK_FAILURE);
344 									break SWITCH;
345 								}
346 							}
347 							if (takenPrefixes.contains(cmd.getRefName())) {
348 								cmd.setResult(Result.LOCK_FAILURE);
349 								break SWITCH;
350 							}
351 							ru.setCheckConflicting(false);
352 							addRefToPrefixes(takenPrefixes, cmd.getRefName());
353 							takenNames.add(cmd.getRefName());
354 							cmd.setResult(ru.update(walk));
355 						}
356 					}
357 				} catch (IOException err) {
358 					cmd.setResult(REJECTED_OTHER_REASON, MessageFormat.format(
359 							JGitText.get().lockError, err.getMessage()));
360 				} finally {
361 					monitor.update(1);
362 				}
363 			}
364 		}
365 		monitor.endTask();
366 	}
367 
368 	private static Collection<String> getTakenPrefixes(
369 			final Collection<String> names) {
370 		Collection<String> ref = new HashSet<String>();
371 		for (String name : names)
372 			ref.addAll(getPrefixes(name));
373 		return ref;
374 	}
375 
376 	private static void addRefToPrefixes(Collection<String> prefixes,
377 			String name) {
378 		for (String prefix : getPrefixes(name)) {
379 			prefixes.add(prefix);
380 		}
381 	}
382 
383 	static Collection<String> getPrefixes(String s) {
384 		Collection<String> ret = new HashSet<String>();
385 		int p1 = s.indexOf('/');
386 		while (p1 > 0) {
387 			ret.add(s.substring(0, p1));
388 			p1 = s.indexOf('/', p1 + 1);
389 		}
390 		return ret;
391 	}
392 
393 	/**
394 	 * Create a new RefUpdate copying the batch settings.
395 	 *
396 	 * @param cmd
397 	 *            specific command the update should be created to copy.
398 	 * @return a single reference update command.
399 	 * @throws IOException
400 	 *             the reference database cannot make a new update object for
401 	 *             the given reference.
402 	 */
403 	protected RefUpdate newUpdate(ReceiveCommand cmd) throws IOException {
404 		RefUpdate ru = refdb.newUpdate(cmd.getRefName(), false);
405 		if (isRefLogDisabled())
406 			ru.disableRefLog();
407 		else {
408 			ru.setRefLogIdent(refLogIdent);
409 			ru.setRefLogMessage(refLogMessage, refLogIncludeResult);
410 		}
411 		ru.setPushCertificate(pushCert);
412 		switch (cmd.getType()) {
413 		case DELETE:
414 			if (!ObjectId.zeroId().equals(cmd.getOldId()))
415 				ru.setExpectedOldObjectId(cmd.getOldId());
416 			ru.setForceUpdate(true);
417 			return ru;
418 
419 		case CREATE:
420 		case UPDATE:
421 		case UPDATE_NONFASTFORWARD:
422 		default:
423 			ru.setForceUpdate(isAllowNonFastForwards());
424 			ru.setExpectedOldObjectId(cmd.getOldId());
425 			ru.setNewObjectId(cmd.getNewId());
426 			return ru;
427 		}
428 	}
429 
430 	@Override
431 	public String toString() {
432 		StringBuilder r = new StringBuilder();
433 		r.append(getClass().getSimpleName()).append('[');
434 		if (commands.isEmpty())
435 			return r.append(']').toString();
436 
437 		r.append('\n');
438 		for (ReceiveCommand cmd : commands) {
439 			r.append("  "); //$NON-NLS-1$
440 			r.append(cmd);
441 			r.append("  (").append(cmd.getResult()).append(")\n"); //$NON-NLS-1$ //$NON-NLS-2$
442 		}
443 		return r.append(']').toString();
444 	}
445 }