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