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