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.revwalk.RevObject;
65  import org.eclipse.jgit.revwalk.RevTag;
66  import org.eclipse.jgit.revwalk.RevWalk;
67  import org.eclipse.jgit.transport.ReceiveCommand;
68  import org.eclipse.jgit.transport.ReceiveCommand.Result;
69  
70  /**
71   * Command to create, update or delete an entry inside a {@link RefTree}.
72   * <p>
73   * Unlike {@link ReceiveCommand} (which can only update a reference to an
74   * {@link ObjectId}), a RefTree Command can also create, modify or delete
75   * symbolic references to a target reference.
76   * <p>
77   * RefTree Commands may wrap a {@code ReceiveCommand} to allow callers to
78   * process an existing ReceiveCommand against a RefTree.
79   * <p>
80   * Commands should be passed into {@link RefTree#apply(java.util.Collection)}
81   * for processing.
82   */
83  public class Command {
84  	/**
85  	 * Set unprocessed commands as failed due to transaction aborted.
86  	 * <p>
87  	 * If a command is still {@link Result#NOT_ATTEMPTED} it will be set to
88  	 * {@link Result#REJECTED_OTHER_REASON}. If {@code why} is non-null its
89  	 * contents will be used as the message for the first command status.
90  	 *
91  	 * @param commands
92  	 *            commands to mark as failed.
93  	 * @param why
94  	 *            optional message to set on the first aborted command.
95  	 */
96  	public static void abort(Iterable<Command> commands, @Nullable String why) {
97  		if (why == null || why.isEmpty()) {
98  			why = JGitText.get().transactionAborted;
99  		}
100 		for (Command c : commands) {
101 			if (c.getResult() == NOT_ATTEMPTED) {
102 				c.setResult(REJECTED_OTHER_REASON, why);
103 				why = JGitText.get().transactionAborted;
104 			}
105 		}
106 	}
107 
108 	private final Ref oldRef;
109 	private final Ref newRef;
110 	private final ReceiveCommand cmd;
111 	private Result result;
112 
113 	/**
114 	 * Create a command to create, update or delete a reference.
115 	 * <p>
116 	 * At least one of {@code oldRef} or {@code newRef} must be supplied.
117 	 *
118 	 * @param oldRef
119 	 *            expected value. Null if the ref should not exist.
120 	 * @param newRef
121 	 *            desired value, must be peeled if not null and not symbolic.
122 	 *            Null to delete the ref.
123 	 */
124 	public Command(@Nullable Ref oldRef, @Nullable Ref newRef) {
125 		this.oldRef = oldRef;
126 		this.newRef = newRef;
127 		this.cmd = null;
128 		this.result = NOT_ATTEMPTED;
129 
130 		if (oldRef == null && newRef == null) {
131 			throw new IllegalArgumentException();
132 		}
133 		if (newRef != null && !newRef.isPeeled() && !newRef.isSymbolic()) {
134 			throw new IllegalArgumentException();
135 		}
136 		if (oldRef != null && newRef != null
137 				&& !oldRef.getName().equals(newRef.getName())) {
138 			throw new IllegalArgumentException();
139 		}
140 	}
141 
142 	/**
143 	 * Construct a RefTree command wrapped around a ReceiveCommand.
144 	 *
145 	 * @param rw
146 	 *            walk instance to peel the {@code newId}.
147 	 * @param cmd
148 	 *            command received from a push client.
149 	 * @throws MissingObjectException
150 	 *             {@code oldId} or {@code newId} is missing.
151 	 * @throws IOException
152 	 *             {@code oldId} or {@code newId} cannot be peeled.
153 	 */
154 	public Command(RevWalk rw, ReceiveCommand cmd)
155 			throws MissingObjectException, IOException {
156 		this.oldRef = toRef(rw, cmd.getOldId(), cmd.getRefName(), false);
157 		this.newRef = toRef(rw, cmd.getNewId(), cmd.getRefName(), true);
158 		this.cmd = cmd;
159 	}
160 
161 	static Ref toRef(RevWalk rw, ObjectId id, String name,
162 			boolean mustExist) throws MissingObjectException, IOException {
163 		if (ObjectId.zeroId().equals(id)) {
164 			return null;
165 		}
166 
167 		try {
168 			RevObject o = rw.parseAny(id);
169 			if (o instanceof RevTag) {
170 				RevObject p = rw.peel(o);
171 				return new ObjectIdRef.PeeledTag(NETWORK, name, id, p.copy());
172 			}
173 			return new ObjectIdRef.PeeledNonTag(NETWORK, name, id);
174 		} catch (MissingObjectException e) {
175 			if (mustExist) {
176 				throw e;
177 			}
178 			return new ObjectIdRef.Unpeeled(NETWORK, name, id);
179 		}
180 	}
181 
182 	/** @return name of the reference affected by this command. */
183 	public String getRefName() {
184 		if (cmd != null) {
185 			return cmd.getRefName();
186 		} else if (newRef != null) {
187 			return newRef.getName();
188 		}
189 		return oldRef.getName();
190 	}
191 
192 	/**
193 	 * Set the result of this command.
194 	 *
195 	 * @param result
196 	 *            the command result.
197 	 */
198 	public void setResult(Result result) {
199 		setResult(result, null);
200 	}
201 
202 	/**
203 	 * Set the result of this command.
204 	 *
205 	 * @param result
206 	 *            the command result.
207 	 * @param why
208 	 *            optional message explaining the result status.
209 	 */
210 	public void setResult(Result result, @Nullable String why) {
211 		if (cmd != null) {
212 			cmd.setResult(result, why);
213 		} else {
214 			this.result = result;
215 		}
216 	}
217 
218 	/** @return result of executing this command. */
219 	public Result getResult() {
220 		return cmd != null ? cmd.getResult() : result;
221 	}
222 
223 	/** @return optional message explaining command failure. */
224 	@Nullable
225 	public String getMessage() {
226 		return cmd != null ? cmd.getMessage() : null;
227 	}
228 
229 	/**
230 	 * Old peeled reference.
231 	 *
232 	 * @return the old reference; null if the command is creating the reference.
233 	 */
234 	@Nullable
235 	public Ref getOldRef() {
236 		return oldRef;
237 	}
238 
239 	/**
240 	 * New peeled reference.
241 	 *
242 	 * @return the new reference; null if the command is deleting the reference.
243 	 */
244 	@Nullable
245 	public Ref getNewRef() {
246 		return newRef;
247 	}
248 
249 	@Override
250 	public String toString() {
251 		StringBuilder s = new StringBuilder();
252 		append(s, oldRef, "CREATE"); //$NON-NLS-1$
253 		s.append(' ');
254 		append(s, newRef, "DELETE"); //$NON-NLS-1$
255 		s.append(' ').append(getRefName());
256 		s.append(' ').append(getResult());
257 		if (getMessage() != null) {
258 			s.append(' ').append(getMessage());
259 		}
260 		return s.toString();
261 	}
262 
263 	private static void append(StringBuilder s, Ref r, String nullName) {
264 		if (r == null) {
265 			s.append(nullName);
266 		} else if (r.isSymbolic()) {
267 			s.append(r.getTarget().getName());
268 		} else {
269 			ObjectId id = r.getObjectId();
270 			if (id != null) {
271 				s.append(id.name());
272 			}
273 		}
274 	}
275 
276 	/**
277 	 * Check the entry is consistent with either the old or the new ref.
278 	 *
279 	 * @param entry
280 	 *            current entry; null if the entry does not exist.
281 	 * @return true if entry matches {@link #getOldRef()} or
282 	 *         {@link #getNewRef()}; otherwise false.
283 	 */
284 	boolean checkRef(@Nullable DirCacheEntry entry) {
285 		if (entry != null && entry.getRawMode() == 0) {
286 			entry = null;
287 		}
288 		return check(entry, oldRef) || check(entry, newRef);
289 	}
290 
291 	private static boolean check(@Nullable DirCacheEntry cur,
292 			@Nullable Ref exp) {
293 		if (cur == null) {
294 			// Does not exist, ok if oldRef does not exist.
295 			return exp == null;
296 		} else if (exp == null) {
297 			// Expected to not exist, but currently exists, fail.
298 			return false;
299 		}
300 
301 		if (exp.isSymbolic()) {
302 			String dst = exp.getTarget().getName();
303 			return cur.getRawMode() == TYPE_SYMLINK
304 					&& cur.getObjectId().equals(symref(dst));
305 		}
306 
307 		return cur.getRawMode() == TYPE_GITLINK
308 				&& cur.getObjectId().equals(exp.getObjectId());
309 	}
310 
311 	static ObjectId symref(String s) {
312 		@SuppressWarnings("resource")
313 		ObjectInserter.Formatter fmt = new ObjectInserter.Formatter();
314 		return fmt.idFor(OBJ_BLOB, encode(s));
315 	}
316 }