xref: /JGit/org.eclipse.jgit.lfs/src/org/eclipse/jgit/lfs/LfsPrePushHook.java (revision 5c5f7c6b146b24f2bd4afae1902df85ad6e57ea3)
1c0bb9928SMarkus Duft /*
2*5c5f7c6bSMatthias Sohn  * Copyright (C) 2017, Markus Duft <markus.duft@ssi-schaefer.com> and others
3c0bb9928SMarkus Duft  *
4*5c5f7c6bSMatthias Sohn  * This program and the accompanying materials are made available under the
5*5c5f7c6bSMatthias Sohn  * terms of the Eclipse Distribution License v. 1.0 which is available at
6*5c5f7c6bSMatthias Sohn  * https://www.eclipse.org/org/documents/edl-v10.php.
7c0bb9928SMarkus Duft  *
8*5c5f7c6bSMatthias Sohn  * SPDX-License-Identifier: BSD-3-Clause
9c0bb9928SMarkus Duft  */
10c0bb9928SMarkus Duft package org.eclipse.jgit.lfs;
11c0bb9928SMarkus Duft 
1230c6c754SDavid Pursehouse import static java.nio.charset.StandardCharsets.UTF_8;
13c0bb9928SMarkus Duft import static org.eclipse.jgit.lfs.Protocol.OPERATION_UPLOAD;
14ea2f7e93SMarkus Duft import static org.eclipse.jgit.lfs.internal.LfsConnectionFactory.toRequest;
15c0bb9928SMarkus Duft import static org.eclipse.jgit.transport.http.HttpConnection.HTTP_OK;
16c0bb9928SMarkus Duft import static org.eclipse.jgit.util.HttpSupport.METHOD_POST;
17c0bb9928SMarkus Duft import static org.eclipse.jgit.util.HttpSupport.METHOD_PUT;
18c0bb9928SMarkus Duft 
19c0bb9928SMarkus Duft import java.io.IOException;
20c0bb9928SMarkus Duft import java.io.InputStream;
21c0bb9928SMarkus Duft import java.io.InputStreamReader;
22c0bb9928SMarkus Duft import java.io.OutputStream;
23c0bb9928SMarkus Duft import java.io.PrintStream;
24c0bb9928SMarkus Duft import java.nio.file.Files;
25c0bb9928SMarkus Duft import java.nio.file.Path;
26c0bb9928SMarkus Duft import java.text.MessageFormat;
27c0bb9928SMarkus Duft import java.util.Collection;
28c0bb9928SMarkus Duft import java.util.HashMap;
29c0bb9928SMarkus Duft import java.util.List;
30c0bb9928SMarkus Duft import java.util.Map;
31c0bb9928SMarkus Duft import java.util.Set;
32c0bb9928SMarkus Duft import java.util.TreeSet;
33c0bb9928SMarkus Duft 
34c0bb9928SMarkus Duft import org.eclipse.jgit.api.errors.AbortedByHookException;
35c0bb9928SMarkus Duft import org.eclipse.jgit.errors.IncorrectObjectTypeException;
36c0bb9928SMarkus Duft import org.eclipse.jgit.errors.MissingObjectException;
37c0bb9928SMarkus Duft import org.eclipse.jgit.hooks.PrePushHook;
38c0bb9928SMarkus Duft import org.eclipse.jgit.lfs.Protocol.ObjectInfo;
39c0bb9928SMarkus Duft import org.eclipse.jgit.lfs.errors.CorruptMediaFile;
40c0bb9928SMarkus Duft import org.eclipse.jgit.lfs.internal.LfsConnectionFactory;
41c0bb9928SMarkus Duft import org.eclipse.jgit.lfs.internal.LfsText;
42c0bb9928SMarkus Duft import org.eclipse.jgit.lib.AnyObjectId;
43c0bb9928SMarkus Duft import org.eclipse.jgit.lib.Constants;
44c0bb9928SMarkus Duft import org.eclipse.jgit.lib.ObjectId;
45c0bb9928SMarkus Duft import org.eclipse.jgit.lib.ObjectReader;
46c0bb9928SMarkus Duft import org.eclipse.jgit.lib.Ref;
47c0bb9928SMarkus Duft import org.eclipse.jgit.lib.RefDatabase;
48c0bb9928SMarkus Duft import org.eclipse.jgit.lib.Repository;
49c0bb9928SMarkus Duft import org.eclipse.jgit.revwalk.ObjectWalk;
50c0bb9928SMarkus Duft import org.eclipse.jgit.revwalk.RevObject;
51c0bb9928SMarkus Duft import org.eclipse.jgit.transport.RemoteRefUpdate;
52c0bb9928SMarkus Duft import org.eclipse.jgit.transport.http.HttpConnection;
53c0bb9928SMarkus Duft 
54c0bb9928SMarkus Duft import com.google.gson.Gson;
55c0bb9928SMarkus Duft import com.google.gson.stream.JsonReader;
56c0bb9928SMarkus Duft 
57c0bb9928SMarkus Duft /**
58c0bb9928SMarkus Duft  * Pre-push hook that handles uploading LFS artefacts.
59c0bb9928SMarkus Duft  *
60c0bb9928SMarkus Duft  * @since 4.11
61c0bb9928SMarkus Duft  */
62c0bb9928SMarkus Duft public class LfsPrePushHook extends PrePushHook {
63c0bb9928SMarkus Duft 
64c0bb9928SMarkus Duft 	private static final String EMPTY = ""; //$NON-NLS-1$
65c0bb9928SMarkus Duft 	private Collection<RemoteRefUpdate> refs;
66c0bb9928SMarkus Duft 
67c0bb9928SMarkus Duft 	/**
68c0bb9928SMarkus Duft 	 * @param repo
69c0bb9928SMarkus Duft 	 *            the repository
70c0bb9928SMarkus Duft 	 * @param outputStream
71c0bb9928SMarkus Duft 	 *            not used by this implementation
72c0bb9928SMarkus Duft 	 */
LfsPrePushHook(Repository repo, PrintStream outputStream)73c0bb9928SMarkus Duft 	public LfsPrePushHook(Repository repo, PrintStream outputStream) {
74c0bb9928SMarkus Duft 		super(repo, outputStream);
75c0bb9928SMarkus Duft 	}
76c0bb9928SMarkus Duft 
7723125abcSTim Neumann 	/**
7823125abcSTim Neumann 	 * @param repo
7923125abcSTim Neumann 	 *            the repository
8023125abcSTim Neumann 	 * @param outputStream
8123125abcSTim Neumann 	 *            not used by this implementation
8223125abcSTim Neumann 	 * @param errorStream
8323125abcSTim Neumann 	 *            not used by this implementation
8423125abcSTim Neumann 	 * @since 5.6
8523125abcSTim Neumann 	 */
LfsPrePushHook(Repository repo, PrintStream outputStream, PrintStream errorStream)8623125abcSTim Neumann 	public LfsPrePushHook(Repository repo, PrintStream outputStream,
8723125abcSTim Neumann 			PrintStream errorStream) {
8823125abcSTim Neumann 		super(repo, outputStream, errorStream);
8923125abcSTim Neumann 	}
9023125abcSTim Neumann 
91c0bb9928SMarkus Duft 	@Override
setRefs(Collection<RemoteRefUpdate> toRefs)92c0bb9928SMarkus Duft 	public void setRefs(Collection<RemoteRefUpdate> toRefs) {
93c0bb9928SMarkus Duft 		this.refs = toRefs;
94c0bb9928SMarkus Duft 	}
95c0bb9928SMarkus Duft 
96c0bb9928SMarkus Duft 	@Override
call()97c0bb9928SMarkus Duft 	public String call() throws IOException, AbortedByHookException {
98c0bb9928SMarkus Duft 		Set<LfsPointer> toPush = findObjectsToPush();
99c0bb9928SMarkus Duft 		if (toPush.isEmpty()) {
100c0bb9928SMarkus Duft 			return EMPTY;
101c0bb9928SMarkus Duft 		}
102c0bb9928SMarkus Duft 		HttpConnection api = LfsConnectionFactory.getLfsConnection(
103c0bb9928SMarkus Duft 				getRepository(), METHOD_POST, OPERATION_UPLOAD);
104c0bb9928SMarkus Duft 		Map<String, LfsPointer> oid2ptr = requestBatchUpload(api, toPush);
105c0bb9928SMarkus Duft 		uploadContents(api, oid2ptr);
106c0bb9928SMarkus Duft 		return EMPTY;
107ea2f7e93SMarkus Duft 
108c0bb9928SMarkus Duft 	}
109c0bb9928SMarkus Duft 
findObjectsToPush()110c0bb9928SMarkus Duft 	private Set<LfsPointer> findObjectsToPush() throws IOException,
111c0bb9928SMarkus Duft 			MissingObjectException, IncorrectObjectTypeException {
112c0bb9928SMarkus Duft 		Set<LfsPointer> toPush = new TreeSet<>();
113c0bb9928SMarkus Duft 
114c0bb9928SMarkus Duft 		try (ObjectWalk walk = new ObjectWalk(getRepository())) {
115c0bb9928SMarkus Duft 			for (RemoteRefUpdate up : refs) {
116c0bb9928SMarkus Duft 				walk.setRewriteParents(false);
117c0bb9928SMarkus Duft 				excludeRemoteRefs(walk);
118c0bb9928SMarkus Duft 				walk.markStart(walk.parseCommit(up.getNewObjectId()));
119c0bb9928SMarkus Duft 				while (walk.next() != null) {
120c0bb9928SMarkus Duft 					// walk all commits to populate objects
121c0bb9928SMarkus Duft 				}
122c0bb9928SMarkus Duft 				findLfsPointers(toPush, walk);
123c0bb9928SMarkus Duft 			}
124c0bb9928SMarkus Duft 		}
125c0bb9928SMarkus Duft 		return toPush;
126c0bb9928SMarkus Duft 	}
127c0bb9928SMarkus Duft 
findLfsPointers(Set<LfsPointer> toPush, ObjectWalk walk)128c0bb9928SMarkus Duft 	private static void findLfsPointers(Set<LfsPointer> toPush, ObjectWalk walk)
129c0bb9928SMarkus Duft 			throws MissingObjectException, IncorrectObjectTypeException,
130c0bb9928SMarkus Duft 			IOException {
131c0bb9928SMarkus Duft 		RevObject obj;
132c0bb9928SMarkus Duft 		ObjectReader r = walk.getObjectReader();
133c0bb9928SMarkus Duft 		while ((obj = walk.nextObject()) != null) {
134c0bb9928SMarkus Duft 			if (obj.getType() == Constants.OBJ_BLOB
135c0bb9928SMarkus Duft 					&& getObjectSize(r, obj) < LfsPointer.SIZE_THRESHOLD) {
136c0bb9928SMarkus Duft 				LfsPointer ptr = loadLfsPointer(r, obj);
137c0bb9928SMarkus Duft 				if (ptr != null) {
138c0bb9928SMarkus Duft 					toPush.add(ptr);
139c0bb9928SMarkus Duft 				}
140c0bb9928SMarkus Duft 			}
141c0bb9928SMarkus Duft 		}
142c0bb9928SMarkus Duft 	}
143c0bb9928SMarkus Duft 
getObjectSize(ObjectReader r, RevObject obj)144c0bb9928SMarkus Duft 	private static long getObjectSize(ObjectReader r, RevObject obj)
145c0bb9928SMarkus Duft 			throws IOException {
146c0bb9928SMarkus Duft 		return r.getObjectSize(obj.getId(), Constants.OBJ_BLOB);
147c0bb9928SMarkus Duft 	}
148c0bb9928SMarkus Duft 
loadLfsPointer(ObjectReader r, AnyObjectId obj)149c0bb9928SMarkus Duft 	private static LfsPointer loadLfsPointer(ObjectReader r, AnyObjectId obj)
150c0bb9928SMarkus Duft 			throws IOException {
151c0bb9928SMarkus Duft 		try (InputStream is = r.open(obj, Constants.OBJ_BLOB).openStream()) {
152c0bb9928SMarkus Duft 			return LfsPointer.parseLfsPointer(is);
153c0bb9928SMarkus Duft 		}
154c0bb9928SMarkus Duft 	}
155c0bb9928SMarkus Duft 
excludeRemoteRefs(ObjectWalk walk)156c0bb9928SMarkus Duft 	private void excludeRemoteRefs(ObjectWalk walk) throws IOException {
157c0bb9928SMarkus Duft 		RefDatabase refDatabase = getRepository().getRefDatabase();
1589edf9bf2SMatthias Sohn 		List<Ref> remoteRefs = refDatabase.getRefsByPrefix(remote());
1599edf9bf2SMatthias Sohn 		for (Ref r : remoteRefs) {
160c0bb9928SMarkus Duft 			ObjectId oid = r.getPeeledObjectId();
161c0bb9928SMarkus Duft 			if (oid == null) {
162c0bb9928SMarkus Duft 				oid = r.getObjectId();
163c0bb9928SMarkus Duft 			}
164312e61a3SMarkus Duft 			if (oid == null) {
165312e61a3SMarkus Duft 				// ignore (e.g. symbolic, ...)
166312e61a3SMarkus Duft 				continue;
167312e61a3SMarkus Duft 			}
168c0bb9928SMarkus Duft 			RevObject o = walk.parseAny(oid);
169c0bb9928SMarkus Duft 			if (o.getType() == Constants.OBJ_COMMIT
170c0bb9928SMarkus Duft 					|| o.getType() == Constants.OBJ_TAG) {
171c0bb9928SMarkus Duft 				walk.markUninteresting(o);
172c0bb9928SMarkus Duft 			}
173c0bb9928SMarkus Duft 		}
174c0bb9928SMarkus Duft 	}
175c0bb9928SMarkus Duft 
remote()176c0bb9928SMarkus Duft 	private String remote() {
177c0bb9928SMarkus Duft 		String remoteName = getRemoteName() == null
178c0bb9928SMarkus Duft 				? Constants.DEFAULT_REMOTE_NAME
179c0bb9928SMarkus Duft 				: getRemoteName();
180c0bb9928SMarkus Duft 		return Constants.R_REMOTES + remoteName;
181c0bb9928SMarkus Duft 	}
182c0bb9928SMarkus Duft 
requestBatchUpload(HttpConnection api, Set<LfsPointer> toPush)183c0bb9928SMarkus Duft 	private Map<String, LfsPointer> requestBatchUpload(HttpConnection api,
184c0bb9928SMarkus Duft 			Set<LfsPointer> toPush) throws IOException {
1852fc00af4SMichael Keppler 		LfsPointer[] res = toPush.toArray(new LfsPointer[0]);
186c0bb9928SMarkus Duft 		Map<String, LfsPointer> oidStr2ptr = new HashMap<>();
187c0bb9928SMarkus Duft 		for (LfsPointer p : res) {
188c0bb9928SMarkus Duft 			oidStr2ptr.put(p.getOid().name(), p);
189c0bb9928SMarkus Duft 		}
190ea2f7e93SMarkus Duft 		Gson gson = Protocol.gson();
191c0bb9928SMarkus Duft 		api.getOutputStream().write(
19230c6c754SDavid Pursehouse 				gson.toJson(toRequest(OPERATION_UPLOAD, res)).getBytes(UTF_8));
193c0bb9928SMarkus Duft 		int responseCode = api.getResponseCode();
194c0bb9928SMarkus Duft 		if (responseCode != HTTP_OK) {
195c0bb9928SMarkus Duft 			throw new IOException(
196c0bb9928SMarkus Duft 					MessageFormat.format(LfsText.get().serverFailure,
197c0bb9928SMarkus Duft 							api.getURL(), Integer.valueOf(responseCode)));
198c0bb9928SMarkus Duft 		}
199c0bb9928SMarkus Duft 		return oidStr2ptr;
200c0bb9928SMarkus Duft 	}
201c0bb9928SMarkus Duft 
uploadContents(HttpConnection api, Map<String, LfsPointer> oid2ptr)202c0bb9928SMarkus Duft 	private void uploadContents(HttpConnection api,
203c0bb9928SMarkus Duft 			Map<String, LfsPointer> oid2ptr) throws IOException {
204c0bb9928SMarkus Duft 		try (JsonReader reader = new JsonReader(
20530c6c754SDavid Pursehouse 				new InputStreamReader(api.getInputStream(), UTF_8))) {
206c0bb9928SMarkus Duft 			for (Protocol.ObjectInfo o : parseObjects(reader)) {
207c0bb9928SMarkus Duft 				if (o.actions == null) {
208c0bb9928SMarkus Duft 					continue;
209c0bb9928SMarkus Duft 				}
210c0bb9928SMarkus Duft 				LfsPointer ptr = oid2ptr.get(o.oid);
211c0bb9928SMarkus Duft 				if (ptr == null) {
212c0bb9928SMarkus Duft 					// received an object we didn't request
213c0bb9928SMarkus Duft 					continue;
214c0bb9928SMarkus Duft 				}
215c0bb9928SMarkus Duft 				Protocol.Action uploadAction = o.actions.get(OPERATION_UPLOAD);
216c0bb9928SMarkus Duft 				if (uploadAction == null || uploadAction.href == null) {
217c0bb9928SMarkus Duft 					continue;
218c0bb9928SMarkus Duft 				}
219c0bb9928SMarkus Duft 
220c0bb9928SMarkus Duft 				Lfs lfs = new Lfs(getRepository());
221c0bb9928SMarkus Duft 				Path path = lfs.getMediaFile(ptr.getOid());
222c0bb9928SMarkus Duft 				if (!Files.exists(path)) {
223c0bb9928SMarkus Duft 					throw new IOException(MessageFormat
224c0bb9928SMarkus Duft 							.format(LfsText.get().missingLocalObject, path));
225c0bb9928SMarkus Duft 				}
226c0bb9928SMarkus Duft 				uploadFile(o, uploadAction, path);
227c0bb9928SMarkus Duft 			}
228c0bb9928SMarkus Duft 		}
229c0bb9928SMarkus Duft 	}
230c0bb9928SMarkus Duft 
parseObjects(JsonReader reader)231c0bb9928SMarkus Duft 	private List<ObjectInfo> parseObjects(JsonReader reader) {
232c0bb9928SMarkus Duft 		Gson gson = new Gson();
233c0bb9928SMarkus Duft 		Protocol.Response resp = gson.fromJson(reader, Protocol.Response.class);
234c0bb9928SMarkus Duft 		return resp.objects;
235c0bb9928SMarkus Duft 	}
236c0bb9928SMarkus Duft 
uploadFile(Protocol.ObjectInfo o, Protocol.Action uploadAction, Path path)237c0bb9928SMarkus Duft 	private void uploadFile(Protocol.ObjectInfo o,
238c0bb9928SMarkus Duft 			Protocol.Action uploadAction, Path path)
239c0bb9928SMarkus Duft 			throws IOException, CorruptMediaFile {
240c0bb9928SMarkus Duft 		HttpConnection contentServer = LfsConnectionFactory
241c0bb9928SMarkus Duft 				.getLfsContentConnection(getRepository(), uploadAction,
242c0bb9928SMarkus Duft 						METHOD_PUT);
243c0bb9928SMarkus Duft 		contentServer.setDoOutput(true);
244c0bb9928SMarkus Duft 		try (OutputStream out = contentServer
245c0bb9928SMarkus Duft 				.getOutputStream()) {
246c0bb9928SMarkus Duft 			long size = Files.copy(path, out);
247c0bb9928SMarkus Duft 			if (size != o.size) {
248c0bb9928SMarkus Duft 				throw new CorruptMediaFile(path, o.size, size);
249c0bb9928SMarkus Duft 			}
250c0bb9928SMarkus Duft 		}
251c0bb9928SMarkus Duft 		int responseCode = contentServer.getResponseCode();
252c0bb9928SMarkus Duft 		if (responseCode != HTTP_OK) {
253c0bb9928SMarkus Duft 			throw new IOException(MessageFormat.format(
254c0bb9928SMarkus Duft 					LfsText.get().serverFailure, contentServer.getURL(),
255c0bb9928SMarkus Duft 					Integer.valueOf(responseCode)));
256c0bb9928SMarkus Duft 		}
257c0bb9928SMarkus Duft 	}
258c0bb9928SMarkus Duft }
259