View Javadoc
1   /*
2    * Copyright (C) 2016, Google Inc.
3    * and other copyright owners as documented in the project's IP log.
4    *
5    * This program and the accompanying materials are made available
6    * under the terms of the Eclipse Distribution License v1.0 which
7    * accompanies this distribution, is reproduced below, and is
8    * available at http://www.eclipse.org/org/documents/edl-v10.php
9    *
10   * All rights reserved.
11   *
12   * Redistribution and use in source and binary forms, with or
13   * without modification, are permitted provided that the following
14   * conditions are met:
15   *
16   * - Redistributions of source code must retain the above copyright
17   *   notice, this list of conditions and the following disclaimer.
18   *
19   * - Redistributions in binary form must reproduce the above
20   *   copyright notice, this list of conditions and the following
21   *   disclaimer in the documentation and/or other materials provided
22   *   with the distribution.
23   *
24   * - Neither the name of the Eclipse Foundation, Inc. nor the
25   *   names of its contributors may be used to endorse or promote
26   *   products derived from this software without specific prior
27   *   written permission.
28   *
29   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
30   * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
31   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
32   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
33   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
34   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
35   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
36   * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
37   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
38   * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
39   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
40   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
41   * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
42   */
43  
44  package org.eclipse.jgit.internal.storage.reftree;
45  
46  import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
47  import static org.eclipse.jgit.lib.Constants.encode;
48  import static org.eclipse.jgit.lib.FileMode.TYPE_GITLINK;
49  import static org.eclipse.jgit.lib.FileMode.TYPE_SYMLINK;
50  import static org.eclipse.jgit.lib.Ref.Storage.NETWORK;
51  import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED;
52  import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON;
53  
54  import java.io.IOException;
55  
56  import org.eclipse.jgit.annotations.Nullable;
57  import org.eclipse.jgit.dircache.DirCacheEntry;
58  import org.eclipse.jgit.errors.MissingObjectException;
59  import org.eclipse.jgit.internal.JGitText;
60  import org.eclipse.jgit.lib.ObjectId;
61  import org.eclipse.jgit.lib.ObjectIdRef;
62  import org.eclipse.jgit.lib.ObjectInserter;
63  import org.eclipse.jgit.lib.Ref;
64  import org.eclipse.jgit.lib.SymbolicRef;
65  import org.eclipse.jgit.revwalk.RevObject;
66  import org.eclipse.jgit.revwalk.RevTag;
67  import org.eclipse.jgit.revwalk.RevWalk;
68  import org.eclipse.jgit.transport.ReceiveCommand;
69  import org.eclipse.jgit.transport.ReceiveCommand.Result;
70  
71  /**
72   * Command to create, update or delete an entry inside a
73   * {@link org.eclipse.jgit.internal.storage.reftree.RefTree}.
74   * <p>
75   * Unlike {@link org.eclipse.jgit.transport.ReceiveCommand} (which can only
76   * update a reference to an {@link org.eclipse.jgit.lib.ObjectId}), a RefTree
77   * Command can also create, modify or delete symbolic references to a target
78   * reference.
79   * <p>
80   * RefTree Commands may wrap a {@code ReceiveCommand} to allow callers to
81   * process an existing ReceiveCommand against a RefTree.
82   * <p>
83   * Commands should be passed into
84   * {@link org.eclipse.jgit.internal.storage.reftree.RefTree#apply(java.util.Collection)}
85   * for processing.
86   */
87  public class Command {
88  	/**
89  	 * Set unprocessed commands as failed due to transaction aborted.
90  	 * <p>
91  	 * If a command is still
92  	 * {@link org.eclipse.jgit.transport.ReceiveCommand.Result#NOT_ATTEMPTED} it
93  	 * will be set to
94  	 * {@link org.eclipse.jgit.transport.ReceiveCommand.Result#REJECTED_OTHER_REASON}.
95  	 * If {@code why} is non-null its contents will be used as the message for
96  	 * the first command status.
97  	 *
98  	 * @param commands
99  	 *            commands to mark as failed.
100 	 * @param why
101 	 *            optional message to set on the first aborted command.
102 	 */
103 	public static void abort(Iterable<Command> commands, @Nullable String why) {
104 		if (why == null || why.isEmpty()) {
105 			why = JGitText.get().transactionAborted;
106 		}
107 		for (Command c : commands) {
108 			if (c.getResult() == NOT_ATTEMPTED) {
109 				c.setResult(REJECTED_OTHER_REASON, why);
110 				why = JGitText.get().transactionAborted;
111 			}
112 		}
113 	}
114 
115 	private final Ref oldRef;
116 	private final Ref newRef;
117 	private final ReceiveCommand cmd;
118 	private Result result;
119 
120 	/**
121 	 * Create a command to create, update or delete a reference.
122 	 * <p>
123 	 * At least one of {@code oldRef} or {@code newRef} must be supplied.
124 	 *
125 	 * @param oldRef
126 	 *            expected value. Null if the ref should not exist.
127 	 * @param newRef
128 	 *            desired value, must be peeled if not null and not symbolic.
129 	 *            Null to delete the ref.
130 	 */
131 	public Command(@Nullable Ref oldRef, @Nullable Ref newRef) {
132 		this.oldRef = oldRef;
133 		this.newRef = newRef;
134 		this.cmd = null;
135 		this.result = NOT_ATTEMPTED;
136 
137 		if (oldRef == null && newRef == null) {
138 			throw new IllegalArgumentException();
139 		}
140 		if (newRef != null && !newRef.isPeeled() && !newRef.isSymbolic()) {
141 			throw new IllegalArgumentException();
142 		}
143 		if (oldRef != null && newRef != null
144 				&& !oldRef.getName().equals(newRef.getName())) {
145 			throw new IllegalArgumentException();
146 		}
147 	}
148 
149 	/**
150 	 * Construct a RefTree command wrapped around a ReceiveCommand.
151 	 *
152 	 * @param rw
153 	 *            walk instance to peel the {@code newId}.
154 	 * @param cmd
155 	 *            command received from a push client.
156 	 * @throws org.eclipse.jgit.errors.MissingObjectException
157 	 *             {@code oldId} or {@code newId} is missing.
158 	 * @throws java.io.IOException
159 	 *             {@code oldId} or {@code newId} cannot be peeled.
160 	 */
161 	public Command(RevWalk rw, ReceiveCommand cmd)
162 			throws MissingObjectException, IOException {
163 		this.oldRef = toRef(rw, cmd.getOldId(), cmd.getOldSymref(),
164 				cmd.getRefName(), false);
165 		this.newRef = toRef(rw, cmd.getNewId(), cmd.getNewSymref(),
166 				cmd.getRefName(), true);
167 		this.cmd = cmd;
168 	}
169 
170 	static Ref toRef(RevWalk rw, ObjectId id, @Nullable String target,
171 			String name, boolean mustExist)
172 			throws MissingObjectException, IOException {
173 		if (target != null) {
174 			return new SymbolicRef(name,
175 					new ObjectIdRef.Unpeeled(NETWORK, target, id));
176 		} else if (ObjectId.zeroId().equals(id)) {
177 			return null;
178 		}
179 
180 		try {
181 			RevObject o = rw.parseAny(id);
182 			if (o instanceof RevTag) {
183 				RevObject p = rw.peel(o);
184 				return new ObjectIdRef.PeeledTag(NETWORK, name, id, p.copy());
185 			}
186 			return new ObjectIdRef.PeeledNonTag(NETWORK, name, id);
187 		} catch (MissingObjectException e) {
188 			if (mustExist) {
189 				throw e;
190 			}
191 			return new ObjectIdRef.Unpeeled(NETWORK, name, id);
192 		}
193 	}
194 
195 	/**
196 	 * Get name of the reference affected by this command.
197 	 *
198 	 * @return name of the reference affected by this command.
199 	 */
200 	public String getRefName() {
201 		if (cmd != null) {
202 			return cmd.getRefName();
203 		} else if (newRef != null) {
204 			return newRef.getName();
205 		}
206 		return oldRef.getName();
207 	}
208 
209 	/**
210 	 * Set the result of this command.
211 	 *
212 	 * @param result
213 	 *            the command result.
214 	 */
215 	public void setResult(Result result) {
216 		setResult(result, null);
217 	}
218 
219 	/**
220 	 * Set the result of this command.
221 	 *
222 	 * @param result
223 	 *            the command result.
224 	 * @param why
225 	 *            optional message explaining the result status.
226 	 */
227 	public void setResult(Result result, @Nullable String why) {
228 		if (cmd != null) {
229 			cmd.setResult(result, why);
230 		} else {
231 			this.result = result;
232 		}
233 	}
234 
235 	/**
236 	 * Get result of executing this command.
237 	 *
238 	 * @return result of executing this command.
239 	 */
240 	public Result getResult() {
241 		return cmd != null ? cmd.getResult() : result;
242 	}
243 
244 	/**
245 	 * Get optional message explaining command failure.
246 	 *
247 	 * @return optional message explaining command failure.
248 	 */
249 	@Nullable
250 	public String getMessage() {
251 		return cmd != null ? cmd.getMessage() : null;
252 	}
253 
254 	/**
255 	 * Old peeled reference.
256 	 *
257 	 * @return the old reference; null if the command is creating the reference.
258 	 */
259 	@Nullable
260 	public Ref getOldRef() {
261 		return oldRef;
262 	}
263 
264 	/**
265 	 * New peeled reference.
266 	 *
267 	 * @return the new reference; null if the command is deleting the reference.
268 	 */
269 	@Nullable
270 	public Ref getNewRef() {
271 		return newRef;
272 	}
273 
274 	/** {@inheritDoc} */
275 	@Override
276 	public String toString() {
277 		StringBuilder s = new StringBuilder();
278 		append(s, oldRef, "CREATE"); //$NON-NLS-1$
279 		s.append(' ');
280 		append(s, newRef, "DELETE"); //$NON-NLS-1$
281 		s.append(' ').append(getRefName());
282 		s.append(' ').append(getResult());
283 		if (getMessage() != null) {
284 			s.append(' ').append(getMessage());
285 		}
286 		return s.toString();
287 	}
288 
289 	private static void append(StringBuilder s, Ref r, String nullName) {
290 		if (r == null) {
291 			s.append(nullName);
292 		} else if (r.isSymbolic()) {
293 			s.append(r.getTarget().getName());
294 		} else {
295 			ObjectId id = r.getObjectId();
296 			if (id != null) {
297 				s.append(id.name());
298 			}
299 		}
300 	}
301 
302 	/**
303 	 * Check the entry is consistent with either the old or the new ref.
304 	 *
305 	 * @param entry
306 	 *            current entry; null if the entry does not exist.
307 	 * @return true if entry matches {@link #getOldRef()} or
308 	 *         {@link #getNewRef()}; otherwise false.
309 	 */
310 	boolean checkRef(@Nullable DirCacheEntry entry) {
311 		if (entry != null && entry.getRawMode() == 0) {
312 			entry = null;
313 		}
314 		return check(entry, oldRef) || check(entry, newRef);
315 	}
316 
317 	private static boolean check(@Nullable DirCacheEntry cur,
318 			@Nullable Ref exp) {
319 		if (cur == null) {
320 			// Does not exist, ok if oldRef does not exist.
321 			return exp == null;
322 		} else if (exp == null) {
323 			// Expected to not exist, but currently exists, fail.
324 			return false;
325 		}
326 
327 		if (exp.isSymbolic()) {
328 			String dst = exp.getTarget().getName();
329 			return cur.getRawMode() == TYPE_SYMLINK
330 					&& cur.getObjectId().equals(symref(dst));
331 		}
332 
333 		return cur.getRawMode() == TYPE_GITLINK
334 				&& cur.getObjectId().equals(exp.getObjectId());
335 	}
336 
337 	static ObjectId symref(String s) {
338 		@SuppressWarnings("resource")
339 		ObjectInserter.Formatter fmt = new ObjectInserter.Formatter();
340 		return fmt.idFor(OBJ_BLOB, encode(s));
341 	}
342 }