Rely upon Constants.CHARSET over Constants.CHARACTER_ENCODING
[egit/zawir.git] / org.spearce.jgit / src / org / spearce / jgit / transport / WalkPushConnection.java
blob85bbc14b7f547b79a38de0c36e93e6c730ad6f15
1 /*
2 * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
4 * All rights reserved.
6 * Redistribution and use in source and binary forms, with or
7 * without modification, are permitted provided that the following
8 * conditions are met:
10 * - Redistributions of source code must retain the above copyright
11 * notice, this list of conditions and the following disclaimer.
13 * - Redistributions in binary form must reproduce the above
14 * copyright notice, this list of conditions and the following
15 * disclaimer in the documentation and/or other materials provided
16 * with the distribution.
18 * - Neither the name of the Git Development Community nor the
19 * names of its contributors may be used to endorse or promote
20 * products derived from this software without specific prior
21 * written permission.
23 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
24 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
25 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
26 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
27 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
28 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
29 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
30 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
31 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
32 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
33 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
34 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
35 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
38 package org.spearce.jgit.transport;
40 import java.io.IOException;
41 import java.io.OutputStream;
42 import java.util.ArrayList;
43 import java.util.Collection;
44 import java.util.LinkedHashMap;
45 import java.util.List;
46 import java.util.Map;
47 import java.util.TreeMap;
49 import org.spearce.jgit.errors.TransportException;
50 import org.spearce.jgit.lib.AnyObjectId;
51 import org.spearce.jgit.lib.Constants;
52 import org.spearce.jgit.lib.ObjectId;
53 import org.spearce.jgit.lib.PackWriter;
54 import org.spearce.jgit.lib.ProgressMonitor;
55 import org.spearce.jgit.lib.Ref;
56 import org.spearce.jgit.lib.Repository;
57 import org.spearce.jgit.lib.Ref.Storage;
58 import org.spearce.jgit.transport.RemoteRefUpdate.Status;
60 /**
61 * Generic push support for dumb transport protocols.
62 * <p>
63 * Since there are no Git-specific smarts on the remote side of the connection
64 * the client side must handle everything on its own. The generic push support
65 * requires being able to delete, create and overwrite files on the remote side,
66 * as well as create any missing directories (if necessary). Typically this can
67 * be handled through an FTP style protocol.
68 * <p>
69 * Objects not on the remote side are uploaded as pack files, using one pack
70 * file per invocation. This simplifies the implementation as only two data
71 * files need to be written to the remote repository.
72 * <p>
73 * Push support supplied by this class is not multiuser safe. Concurrent pushes
74 * to the same repository may yield an inconsistent reference database which may
75 * confuse fetch clients.
76 * <p>
77 * A single push is concurrently safe with multiple fetch requests, due to the
78 * careful order of operations used to update the repository. Clients fetching
79 * may receive transient failures due to short reads on certain files if the
80 * protocol does not support atomic file replacement.
82 * @see WalkRemoteObjectDatabase
84 class WalkPushConnection extends BaseConnection implements PushConnection {
85 /** The repository this transport pushes out of. */
86 private final Repository local;
88 /** Location of the remote repository we are writing to. */
89 private final URIish uri;
91 /** Database connection to the remote repository. */
92 private final WalkRemoteObjectDatabase dest;
94 /**
95 * Packs already known to reside in the remote repository.
96 * <p>
97 * This is a LinkedHashMap to maintain the original order.
99 private LinkedHashMap<String, String> packNames;
101 /** Complete listing of refs the remote will have after our push. */
102 private Map<String, Ref> newRefs;
105 * Updates which require altering the packed-refs file to complete.
106 * <p>
107 * If this collection is non-empty then any refs listed in {@link #newRefs}
108 * with a storage class of {@link Storage#PACKED} will be written.
110 private Collection<RemoteRefUpdate> packedRefUpdates;
112 WalkPushConnection(final WalkTransport walkTransport,
113 final WalkRemoteObjectDatabase w) {
114 local = walkTransport.local;
115 uri = walkTransport.getURI();
116 dest = w;
119 public void push(final ProgressMonitor monitor,
120 final Map<String, RemoteRefUpdate> refUpdates)
121 throws TransportException {
122 markStartedOperation();
123 packNames = null;
124 newRefs = new TreeMap<String, Ref>(getRefsMap());
125 packedRefUpdates = new ArrayList<RemoteRefUpdate>(refUpdates.size());
127 // Filter the commands and issue all deletes first. This way we
128 // can correctly handle a directory being cleared out and a new
129 // ref using the directory name being created.
131 final List<RemoteRefUpdate> updates = new ArrayList<RemoteRefUpdate>();
132 for (final RemoteRefUpdate u : refUpdates.values()) {
133 final String n = u.getRemoteName();
134 if (!n.startsWith("refs/") || !Repository.isValidRefName(n)) {
135 u.setStatus(Status.REJECTED_OTHER_REASON);
136 u.setMessage("funny refname");
137 continue;
140 if (AnyObjectId.equals(ObjectId.zeroId(), u.getNewObjectId()))
141 deleteCommand(u);
142 else
143 updates.add(u);
146 // If we have any updates we need to upload the objects first, to
147 // prevent creating refs pointing at non-existant data. Then we
148 // can update the refs, and the info-refs file for dumb transports.
150 if (!updates.isEmpty())
151 sendpack(updates, monitor);
152 for (final RemoteRefUpdate u : updates)
153 updateCommand(u);
155 // Is this a new repository? If so we should create additional
156 // metadata files so it is properly initialized during the push.
158 if (!updates.isEmpty() && isNewRepository())
159 createNewRepository(updates);
161 if (!packedRefUpdates.isEmpty()) {
162 try {
163 dest.writePackedRefs(newRefs.values());
164 for (final RemoteRefUpdate u : packedRefUpdates)
165 u.setStatus(Status.OK);
166 } catch (IOException err) {
167 for (final RemoteRefUpdate u : packedRefUpdates) {
168 u.setStatus(Status.REJECTED_OTHER_REASON);
169 u.setMessage(err.getMessage());
171 throw new TransportException(uri, "failed updating refs", err);
175 try {
176 dest.writeInfoRefs(newRefs.values());
177 } catch (IOException err) {
178 throw new TransportException(uri, "failed updating refs", err);
182 @Override
183 public void close() {
184 dest.close();
187 private void sendpack(final List<RemoteRefUpdate> updates,
188 final ProgressMonitor monitor) throws TransportException {
189 String pathPack = null;
190 String pathIdx = null;
192 try {
193 final PackWriter pw = new PackWriter(local, monitor);
194 final List<ObjectId> need = new ArrayList<ObjectId>();
195 final List<ObjectId> have = new ArrayList<ObjectId>();
196 for (final RemoteRefUpdate r : updates)
197 need.add(r.getNewObjectId());
198 for (final Ref r : getRefs()) {
199 have.add(r.getObjectId());
200 if (r.getPeeledObjectId() != null)
201 have.add(r.getPeeledObjectId());
203 pw.preparePack(need, have, false, true);
205 // We don't have to continue further if the pack will
206 // be an empty pack, as the remote has all objects it
207 // needs to complete this change.
209 if (pw.getObjectsNumber() == 0)
210 return;
212 packNames = new LinkedHashMap<String, String>();
213 for (final String n : dest.getPackNames())
214 packNames.put(n, n);
216 final String base = "pack-" + pw.computeName();
217 final String packName = base + ".pack";
218 pathPack = "pack/" + packName;
219 pathIdx = "pack/" + base + ".idx";
221 if (packNames.remove(packName) != null) {
222 // The remote already contains this pack. We should
223 // remove the index before overwriting to prevent bad
224 // offsets from appearing to clients.
226 dest.writeInfoPacks(packNames.keySet());
227 dest.deleteFile(pathIdx);
230 // Write the pack file, then the index, as readers look the
231 // other direction (index, then pack file).
233 final String wt = "Put " + base.substring(0, 12);
234 OutputStream os = dest.writeFile(pathPack, monitor, wt + "..pack");
235 try {
236 pw.writePack(os);
237 } finally {
238 os.close();
241 os = dest.writeFile(pathIdx, monitor, wt + "..idx");
242 try {
243 pw.writeIndex(os);
244 } finally {
245 os.close();
248 // Record the pack at the start of the pack info list. This
249 // way clients are likely to consult the newest pack first,
250 // and discover the most recent objects there.
252 final ArrayList<String> infoPacks = new ArrayList<String>();
253 infoPacks.add(packName);
254 infoPacks.addAll(packNames.keySet());
255 dest.writeInfoPacks(infoPacks);
257 } catch (IOException err) {
258 safeDelete(pathIdx);
259 safeDelete(pathPack);
261 throw new TransportException(uri, "cannot store objects", err);
265 private void safeDelete(final String path) {
266 if (path != null) {
267 try {
268 dest.deleteFile(path);
269 } catch (IOException cleanupFailure) {
270 // Ignore the deletion failure. We probably are
271 // already failing and were just trying to pick
272 // up after ourselves.
277 private void deleteCommand(final RemoteRefUpdate u) {
278 final Ref r = newRefs.remove(u.getRemoteName());
279 if (r == null) {
280 // Already gone.
282 u.setStatus(Status.OK);
283 return;
286 if (r.getStorage().isPacked())
287 packedRefUpdates.add(u);
289 if (r.getStorage().isLoose()) {
290 try {
291 dest.deleteRef(u.getRemoteName());
292 u.setStatus(Status.OK);
293 } catch (IOException e) {
294 u.setStatus(Status.REJECTED_OTHER_REASON);
295 u.setMessage(e.getMessage());
299 try {
300 dest.deleteRefLog(u.getRemoteName());
301 } catch (IOException e) {
302 u.setStatus(Status.REJECTED_OTHER_REASON);
303 u.setMessage(e.getMessage());
307 private void updateCommand(final RemoteRefUpdate u) {
308 try {
309 dest.writeRef(u.getRemoteName(), u.getNewObjectId());
310 newRefs.put(u.getRemoteName(), new Ref(Storage.LOOSE, u
311 .getRemoteName(), u.getNewObjectId()));
312 u.setStatus(Status.OK);
313 } catch (IOException e) {
314 u.setStatus(Status.REJECTED_OTHER_REASON);
315 u.setMessage(e.getMessage());
319 private boolean isNewRepository() {
320 return getRefsMap().isEmpty() && packNames != null
321 && packNames.isEmpty();
324 private void createNewRepository(final List<RemoteRefUpdate> updates)
325 throws TransportException {
326 try {
327 final String ref = "ref: " + pickHEAD(updates) + "\n";
328 final byte[] bytes = Constants.encode(ref);
329 dest.writeFile("../HEAD", bytes);
330 } catch (IOException e) {
331 throw new TransportException(uri, "cannot create HEAD", e);
334 try {
335 final String config = "[core]\n"
336 + "\trepositoryformatversion = 0\n";
337 final byte[] bytes = Constants.encode(config);
338 dest.writeFile("../config", bytes);
339 } catch (IOException e) {
340 throw new TransportException(uri, "cannot create config", e);
344 private static String pickHEAD(final List<RemoteRefUpdate> updates) {
345 // Try to use master if the user is pushing that, it is the
346 // default branch and is likely what they want to remain as
347 // the default on the new remote.
349 for (final RemoteRefUpdate u : updates) {
350 final String n = u.getRemoteName();
351 if (n.equals(Constants.HEADS_PREFIX + "/" + Constants.MASTER))
352 return n;
355 // Pick any branch, under the assumption the user pushed only
356 // one to the remote side.
358 for (final RemoteRefUpdate u : updates) {
359 final String n = u.getRemoteName();
360 if (n.startsWith(Constants.HEADS_PREFIX + "/"))
361 return n;
363 return updates.get(0).getRemoteName();