View Javadoc
1   /*
2    * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com>
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.transport;
45  
46  import java.io.IOException;
47  import java.io.OutputStream;
48  import java.text.MessageFormat;
49  import java.util.Collection;
50  import java.util.Collections;
51  import java.util.LinkedHashMap;
52  import java.util.List;
53  import java.util.Map;
54  
55  import org.eclipse.jgit.errors.MissingObjectException;
56  import org.eclipse.jgit.errors.NotSupportedException;
57  import org.eclipse.jgit.errors.TransportException;
58  import org.eclipse.jgit.internal.JGitText;
59  import org.eclipse.jgit.lib.ObjectId;
60  import org.eclipse.jgit.lib.ProgressMonitor;
61  import org.eclipse.jgit.lib.Ref;
62  import org.eclipse.jgit.revwalk.RevCommit;
63  import org.eclipse.jgit.revwalk.RevObject;
64  import org.eclipse.jgit.revwalk.RevWalk;
65  import org.eclipse.jgit.transport.RemoteRefUpdate.Status;
66  
67  /**
68   * Class performing push operation on remote repository.
69   *
70   * @see Transport#push(ProgressMonitor, Collection, OutputStream)
71   */
72  class PushProcess {
73  	/** Task name for {@link ProgressMonitor} used during opening connection. */
74  	static final String PROGRESS_OPENING_CONNECTION = JGitText.get().openingConnection;
75  
76  	/** Transport used to perform this operation. */
77  	private final Transport transport;
78  
79  	/** Push operation connection created to perform this operation */
80  	private PushConnection connection;
81  
82  	/** Refs to update on remote side. */
83  	private final Map<String, RemoteRefUpdate> toPush;
84  
85  	/** Revision walker for checking some updates properties. */
86  	private final RevWalk walker;
87  
88  	/** an outputstream to write messages to */
89  	private final OutputStream out;
90  
91  	/** A list of option strings associated with this push */
92  	private List<String> pushOptions;
93  
94  	/**
95  	 * Create process for specified transport and refs updates specification.
96  	 *
97  	 * @param transport
98  	 *            transport between remote and local repository, used to create
99  	 *            connection.
100 	 * @param toPush
101 	 *            specification of refs updates (and local tracking branches).
102 	 *
103 	 * @throws TransportException
104 	 */
105 	PushProcess(final Transport transport,
106 			final Collection<RemoteRefUpdate> toPush) throws TransportException {
107 		this(transport, toPush, null);
108 	}
109 
110 	/**
111 	 * Create process for specified transport and refs updates specification.
112 	 *
113 	 * @param transport
114 	 *            transport between remote and local repository, used to create
115 	 *            connection.
116 	 * @param toPush
117 	 *            specification of refs updates (and local tracking branches).
118 	 * @param out
119 	 *            OutputStream to write messages to
120 	 * @throws TransportException
121 	 */
122 	PushProcess(final Transport transport,
123 			final Collection<RemoteRefUpdate> toPush, OutputStream out)
124 			throws TransportException {
125 		this.walker = new RevWalk(transport.local);
126 		this.transport = transport;
127 		this.toPush = new LinkedHashMap<>();
128 		this.out = out;
129 		this.pushOptions = transport.getPushOptions();
130 		for (RemoteRefUpdate rru : toPush) {
131 			if (this.toPush.put(rru.getRemoteName(), rru) != null)
132 				throw new TransportException(MessageFormat.format(
133 						JGitText.get().duplicateRemoteRefUpdateIsIllegal, rru.getRemoteName()));
134 		}
135 	}
136 
137 	/**
138 	 * Perform push operation between local and remote repository - set remote
139 	 * refs appropriately, send needed objects and update local tracking refs.
140 	 * <p>
141 	 * When {@link Transport#isDryRun()} is true, result of this operation is
142 	 * just estimation of real operation result, no real action is performed.
143 	 *
144 	 * @param monitor
145 	 *            progress monitor used for feedback about operation.
146 	 * @return result of push operation with complete status description.
147 	 * @throws NotSupportedException
148 	 *             when push operation is not supported by provided transport.
149 	 * @throws TransportException
150 	 *             when some error occurred during operation, like I/O, protocol
151 	 *             error, or local database consistency error.
152 	 */
153 	PushResult execute(ProgressMonitor monitor)
154 			throws NotSupportedException, TransportException {
155 		try {
156 			monitor.beginTask(PROGRESS_OPENING_CONNECTION,
157 					ProgressMonitor.UNKNOWN);
158 
159 			final PushResult res = new PushResult();
160 			connection = transport.openPush();
161 			try {
162 				res.setAdvertisedRefs(transport.getURI(), connection
163 						.getRefsMap());
164 				res.peerUserAgent = connection.getPeerUserAgent();
165 				res.setRemoteUpdates(toPush);
166 				monitor.endTask();
167 
168 				final Map<String, RemoteRefUpdate> preprocessed = prepareRemoteUpdates();
169 				if (transport.isDryRun())
170 					modifyUpdatesForDryRun();
171 				else if (!preprocessed.isEmpty())
172 					connection.push(monitor, preprocessed, out);
173 			} finally {
174 				connection.close();
175 				res.addMessages(connection.getMessages());
176 			}
177 			if (!transport.isDryRun())
178 				updateTrackingRefs();
179 			for (RemoteRefUpdate rru : toPush.values()) {
180 				final TrackingRefUpdate tru = rru.getTrackingRefUpdate();
181 				if (tru != null)
182 					res.add(tru);
183 			}
184 			return res;
185 		} finally {
186 			walker.close();
187 		}
188 	}
189 
190 	private Map<String, RemoteRefUpdate> prepareRemoteUpdates()
191 			throws TransportException {
192 		boolean atomic = transport.isPushAtomic();
193 		final Map<String, RemoteRefUpdate> result = new LinkedHashMap<>();
194 		for (RemoteRefUpdate rru : toPush.values()) {
195 			final Ref advertisedRef = connection.getRef(rru.getRemoteName());
196 			ObjectId advertisedOld = null;
197 			if (advertisedRef != null) {
198 				advertisedOld = advertisedRef.getObjectId();
199 			}
200 			if (advertisedOld == null) {
201 				advertisedOld = ObjectId.zeroId();
202 			}
203 
204 			if (rru.getNewObjectId().equals(advertisedOld)) {
205 				if (rru.isDelete()) {
206 					// ref does exist neither locally nor remotely
207 					rru.setStatus(Status.NON_EXISTING);
208 				} else {
209 					// same object - nothing to do
210 					rru.setStatus(Status.UP_TO_DATE);
211 				}
212 				continue;
213 			}
214 
215 			// caller has explicitly specified expected old object id, while it
216 			// has been changed in the mean time - reject
217 			if (rru.isExpectingOldObjectId()
218 					&& !rru.getExpectedOldObjectId().equals(advertisedOld)) {
219 				rru.setStatus(Status.REJECTED_REMOTE_CHANGED);
220 				if (atomic) {
221 					return rejectAll();
222 				}
223 				continue;
224 			}
225 			if (!rru.isExpectingOldObjectId()) {
226 				rru.setExpectedOldObjectId(advertisedOld);
227 			}
228 
229 			// create ref (hasn't existed on remote side) and delete ref
230 			// are always fast-forward commands, feasible at this level
231 			if (advertisedOld.equals(ObjectId.zeroId()) || rru.isDelete()) {
232 				rru.setFastForward(true);
233 				result.put(rru.getRemoteName(), rru);
234 				continue;
235 			}
236 
237 			// check for fast-forward:
238 			// - both old and new ref must point to commits, AND
239 			// - both of them must be known for us, exist in repository, AND
240 			// - old commit must be ancestor of new commit
241 			boolean fastForward = true;
242 			try {
243 				RevObject oldRev = walker.parseAny(advertisedOld);
244 				final RevObject newRev = walker.parseAny(rru.getNewObjectId());
245 				if (!(oldRev instanceof RevCommit)
246 						|| !(newRev instanceof RevCommit)
247 						|| !walker.isMergedInto((RevCommit) oldRev,
248 								(RevCommit) newRev))
249 					fastForward = false;
250 			} catch (MissingObjectException x) {
251 				fastForward = false;
252 			} catch (Exception x) {
253 				throw new TransportException(transport.getURI(), MessageFormat.format(
254 						JGitText.get().readingObjectsFromLocalRepositoryFailed, x.getMessage()), x);
255 			}
256 			rru.setFastForward(fastForward);
257 			if (!fastForward && !rru.isForceUpdate()) {
258 				rru.setStatus(Status.REJECTED_NONFASTFORWARD);
259 				if (atomic) {
260 					return rejectAll();
261 				}
262 			} else {
263 				result.put(rru.getRemoteName(), rru);
264 			}
265 		}
266 		return result;
267 	}
268 
269 	private Map<String, RemoteRefUpdate> rejectAll() {
270 		for (RemoteRefUpdate rru : toPush.values()) {
271 			if (rru.getStatus() == Status.NOT_ATTEMPTED) {
272 				rru.setStatus(RemoteRefUpdate.Status.REJECTED_OTHER_REASON);
273 				rru.setMessage(JGitText.get().transactionAborted);
274 			}
275 		}
276 		return Collections.emptyMap();
277 	}
278 
279 	private void modifyUpdatesForDryRun() {
280 		for (RemoteRefUpdate rru : toPush.values())
281 			if (rru.getStatus() == Status.NOT_ATTEMPTED)
282 				rru.setStatus(Status.OK);
283 	}
284 
285 	private void updateTrackingRefs() {
286 		for (RemoteRefUpdate rru : toPush.values()) {
287 			final Status status = rru.getStatus();
288 			if (rru.hasTrackingRefUpdate()
289 					&& (status == Status.UP_TO_DATE || status == Status.OK)) {
290 				// update local tracking branch only when there is a chance that
291 				// it has changed; this is possible for:
292 				// -updated (OK) status,
293 				// -up to date (UP_TO_DATE) status
294 				try {
295 					rru.updateTrackingRef(walker);
296 				} catch (IOException e) {
297 					// ignore as RefUpdate has stored I/O error status
298 				}
299 			}
300 		}
301 	}
302 
303 	/**
304 	 * Gets the list of option strings associated with this push.
305 	 *
306 	 * @return pushOptions
307 	 * @since 4.5
308 	 */
309 	public List<String> getPushOptions() {
310 		return pushOptions;
311 	}
312 }