View Javadoc
1   /*
2    * Copyright (C) 2007, Dave Watson <dwatson@mimvista.com>
3    * Copyright (C) 2008, 2010 Google Inc.
4    * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com>
5    * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
6    * Copyright (C) 2008, 2020 Shawn O. Pearce <spearce@spearce.org> and others
7    *
8    * This program and the accompanying materials are made available under the
9    * terms of the Eclipse Distribution License v. 1.0 which is available at
10   * https://www.eclipse.org/org/documents/edl-v10.php.
11   *
12   * SPDX-License-Identifier: BSD-3-Clause
13   */
14  
15  package org.eclipse.jgit.transport;
16  
17  import java.io.BufferedInputStream;
18  import java.io.BufferedOutputStream;
19  import java.io.File;
20  import java.io.IOException;
21  import java.io.InputStream;
22  import java.io.OutputStream;
23  import java.util.Collection;
24  import java.util.Collections;
25  import java.util.Map;
26  import java.util.Set;
27  
28  import org.eclipse.jgit.errors.NoRemoteRepositoryException;
29  import org.eclipse.jgit.errors.NotSupportedException;
30  import org.eclipse.jgit.errors.TransportException;
31  import org.eclipse.jgit.internal.JGitText;
32  import org.eclipse.jgit.lib.Repository;
33  import org.eclipse.jgit.lib.RepositoryBuilder;
34  import org.eclipse.jgit.lib.RepositoryCache;
35  import org.eclipse.jgit.transport.resolver.ReceivePackFactory;
36  import org.eclipse.jgit.transport.resolver.UploadPackFactory;
37  import org.eclipse.jgit.util.FS;
38  import org.eclipse.jgit.util.io.MessageWriter;
39  import org.eclipse.jgit.util.io.StreamCopyThread;
40  
41  /**
42   * Transport to access a local directory as though it were a remote peer.
43   * <p>
44   * This transport is suitable for use on the local system, where the caller has
45   * direct read or write access to the "remote" repository.
46   * <p>
47   * By default this transport works by spawning a helper thread within the same
48   * JVM, and processes the data transfer using a shared memory buffer between the
49   * calling thread and the helper thread. This is a pure-Java implementation
50   * which does not require forking an external process.
51   * <p>
52   * However, during {@link #openFetch()}, if the Transport has configured
53   * {@link Transport#getOptionUploadPack()} to be anything other than
54   * <code>"git-upload-pack"</code> or <code>"git upload-pack"</code>, this
55   * implementation will fork and execute the external process, using an operating
56   * system pipe to transfer data.
57   * <p>
58   * Similarly, during {@link #openPush()}, if the Transport has configured
59   * {@link Transport#getOptionReceivePack()} to be anything other than
60   * <code>"git-receive-pack"</code> or <code>"git receive-pack"</code>, this
61   * implementation will fork and execute the external process, using an operating
62   * system pipe to transfer data.
63   */
64  class TransportLocal extends Transport implements PackTransport {
65  	static final TransportProtocol PROTO_LOCAL = new TransportProtocol() {
66  		@Override
67  		public String getName() {
68  			return JGitText.get().transportProtoLocal;
69  		}
70  
71  		@Override
72  		public Set<String> getSchemes() {
73  			return Collections.singleton("file"); //$NON-NLS-1$
74  		}
75  
76  		@Override
77  		public boolean canHandle(URIish uri, Repository local, String remoteName) {
78  			if (uri.getPath() == null
79  					|| uri.getPort() > 0
80  					|| uri.getUser() != null
81  					|| uri.getPass() != null
82  					|| uri.getHost() != null
83  					|| (uri.getScheme() != null && !getSchemes().contains(uri.getScheme())))
84  				return false;
85  			return true;
86  		}
87  
88  		@Override
89  		public Transport open(URIish uri, Repository local, String remoteName)
90  				throws NoRemoteRepositoryException {
91  			File localPath = local.isBare() ? local.getDirectory() : local.getWorkTree();
92  			File path = local.getFS().resolve(localPath, uri.getPath());
93  			// If the reference is to a local file, C Git behavior says
94  			// assume this is a bundle, since repositories are directories.
95  			if (path.isFile())
96  				return new TransportBundleFile(local, uri, path);
97  
98  			File gitDir = RepositoryCache.FileKey.resolve(path, local.getFS());
99  			if (gitDir == null)
100 				throw new NoRemoteRepositoryException(uri, JGitText.get().notFound);
101 			return new TransportLocal(local, uri, gitDir);
102 		}
103 
104 		@Override
105 		public Transport open(URIish uri) throws NotSupportedException,
106 				TransportException {
107 			File path = FS.DETECTED.resolve(new File("."), uri.getPath()); //$NON-NLS-1$
108 			// If the reference is to a local file, C Git behavior says
109 			// assume this is a bundle, since repositories are directories.
110 			if (path.isFile())
111 				return new TransportBundleFile(uri, path);
112 
113 			File gitDir = RepositoryCache.FileKey.resolve(path, FS.DETECTED);
114 			if (gitDir == null)
115 				throw new NoRemoteRepositoryException(uri,
116 						JGitText.get().notFound);
117 			return new TransportLocal(uri, gitDir);
118 		}
119 	};
120 
121 	private final File remoteGitDir;
122 
123 	TransportLocal(Repository local, URIish uri, File gitDir) {
124 		super(local, uri);
125 		remoteGitDir = gitDir;
126 	}
127 
128 	TransportLocal(URIish uri, File gitDir) {
129 		super(uri);
130 		remoteGitDir = gitDir;
131 	}
132 
133 	UploadPack createUploadPack(Repository dst) {
134 		return new UploadPack(dst);
135 	}
136 
137 	ReceivePack createReceivePack(Repository dst) {
138 		return new ReceivePack(dst);
139 	}
140 
141 	private Repository openRepo() throws TransportException {
142 		try {
143 			return new RepositoryBuilder()
144 					.setFS(local != null ? local.getFS() : FS.DETECTED)
145 					.setGitDir(remoteGitDir).build();
146 		} catch (IOException err) {
147 			TransportException te = new TransportException(uri,
148 					JGitText.get().notAGitDirectory);
149 			te.initCause(err);
150 			throw te;
151 		}
152 	}
153 
154 	/** {@inheritDoc} */
155 	@Override
156 	public FetchConnection openFetch() throws TransportException {
157 		return openFetch(Collections.emptyList());
158 	}
159 
160 	@Override
161 	public FetchConnection openFetch(Collection<RefSpec> refSpecs,
162 			String... additionalPatterns) throws TransportException {
163 		final String up = getOptionUploadPack();
164 		if (!"git-upload-pack".equals(up) //$NON-NLS-1$
165 				&& !"git upload-pack".equals(up)) {//$NON-NLS-1$
166 			return new ForkLocalFetchConnection(refSpecs, additionalPatterns);
167 		}
168 		UploadPackFactory<Void> upf = (Void req,
169 				Repository db) -> createUploadPack(db);
170 		return new InternalFetchConnection<>(this, upf, null, openRepo());
171 	}
172 
173 	/** {@inheritDoc} */
174 	@Override
175 	public PushConnection openPush() throws TransportException {
176 		final String rp = getOptionReceivePack();
177 		if (!"git-receive-pack".equals(rp) //$NON-NLS-1$
178 				&& !"git receive-pack".equals(rp)) //$NON-NLS-1$
179 			return new ForkLocalPushConnection();
180 
181 		ReceivePackFactory<Void> rpf = (Void req,
182 				Repository db) -> createReceivePack(db);
183 		return new InternalPushConnection<>(this, rpf, null, openRepo());
184 	}
185 
186 	/** {@inheritDoc} */
187 	@Override
188 	public void close() {
189 		// Resources must be established per-connection.
190 	}
191 
192 	/**
193 	 * Spawn process
194 	 *
195 	 * @param cmd
196 	 *            command
197 	 * @return a {@link java.lang.Process} object.
198 	 * @throws org.eclipse.jgit.errors.TransportException
199 	 *             if any.
200 	 */
201 	protected Process spawn(String cmd)
202 			throws TransportException {
203 		return spawn(cmd, null);
204 	}
205 
206 	/**
207 	 * Spawn process
208 	 *
209 	 * @param cmd
210 	 *            command
211 	 * @param protocolVersion
212 	 *            to use
213 	 * @return a {@link java.lang.Process} object.
214 	 * @throws org.eclipse.jgit.errors.TransportException
215 	 *             if any.
216 	 */
217 	private Process spawn(String cmd,
218 			TransferConfig.ProtocolVersion protocolVersion)
219 			throws TransportException {
220 		try {
221 			String[] args = { "." }; //$NON-NLS-1$
222 			ProcessBuilder proc = local.getFS().runInShell(cmd, args);
223 			proc.directory(remoteGitDir);
224 
225 			// Remove the same variables CGit does.
226 			Map<String, String> env = proc.environment();
227 			env.remove("GIT_ALTERNATE_OBJECT_DIRECTORIES"); //$NON-NLS-1$
228 			env.remove("GIT_CONFIG"); //$NON-NLS-1$
229 			env.remove("GIT_CONFIG_PARAMETERS"); //$NON-NLS-1$
230 			env.remove("GIT_DIR"); //$NON-NLS-1$
231 			env.remove("GIT_WORK_TREE"); //$NON-NLS-1$
232 			env.remove("GIT_GRAFT_FILE"); //$NON-NLS-1$
233 			env.remove("GIT_INDEX_FILE"); //$NON-NLS-1$
234 			env.remove("GIT_NO_REPLACE_OBJECTS"); //$NON-NLS-1$
235 			if (TransferConfig.ProtocolVersion.V2.equals(protocolVersion)) {
236 				env.put(GitProtocolConstants.PROTOCOL_ENVIRONMENT_VARIABLE,
237 						GitProtocolConstants.VERSION_2_REQUEST);
238 			}
239 			return proc.start();
240 		} catch (IOException err) {
241 			throw new TransportException(uri, err.getMessage(), err);
242 		}
243 	}
244 
245 	class ForkLocalFetchConnection extends BasePackFetchConnection {
246 		private Process uploadPack;
247 
248 		private Thread errorReaderThread;
249 
250 		ForkLocalFetchConnection() throws TransportException {
251 			this(Collections.emptyList());
252 		}
253 
254 		ForkLocalFetchConnection(Collection<RefSpec> refSpecs,
255 				String... additionalPatterns) throws TransportException {
256 			super(TransportLocal.this);
257 
258 			final MessageWriter msg = new MessageWriter();
259 			setMessageWriter(msg);
260 
261 			TransferConfig.ProtocolVersion gitProtocol = protocol;
262 			if (gitProtocol == null) {
263 				gitProtocol = TransferConfig.ProtocolVersion.V2;
264 			}
265 			uploadPack = spawn(getOptionUploadPack(), gitProtocol);
266 
267 			final InputStream upErr = uploadPack.getErrorStream();
268 			errorReaderThread = new StreamCopyThread(upErr, msg.getRawStream());
269 			errorReaderThread.start();
270 
271 			InputStream upIn = uploadPack.getInputStream();
272 			OutputStream upOut = uploadPack.getOutputStream();
273 
274 			upIn = new BufferedInputStream(upIn);
275 			upOut = new BufferedOutputStream(upOut);
276 
277 			init(upIn, upOut);
278 			if (!readAdvertisedRefs()) {
279 				lsRefs(refSpecs, additionalPatterns);
280 			}
281 		}
282 
283 		@Override
284 		public void close() {
285 			super.close();
286 
287 			if (uploadPack != null) {
288 				try {
289 					uploadPack.waitFor();
290 				} catch (InterruptedException ie) {
291 					// Stop waiting and return anyway.
292 				} finally {
293 					uploadPack = null;
294 				}
295 			}
296 
297 			if (errorReaderThread != null) {
298 				try {
299 					errorReaderThread.join();
300 				} catch (InterruptedException e) {
301 					// Stop waiting and return anyway.
302 				} finally {
303 					errorReaderThread = null;
304 				}
305 			}
306 		}
307 	}
308 
309 	class ForkLocalPushConnection extends BasePackPushConnection {
310 		private Process receivePack;
311 
312 		private Thread errorReaderThread;
313 
314 		ForkLocalPushConnection() throws TransportException {
315 			super(TransportLocal.this);
316 
317 			final MessageWriter msg = new MessageWriter();
318 			setMessageWriter(msg);
319 
320 			receivePack = spawn(getOptionReceivePack());
321 
322 			final InputStream rpErr = receivePack.getErrorStream();
323 			errorReaderThread = new StreamCopyThread(rpErr, msg.getRawStream());
324 			errorReaderThread.start();
325 
326 			InputStream rpIn = receivePack.getInputStream();
327 			OutputStream rpOut = receivePack.getOutputStream();
328 
329 			rpIn = new BufferedInputStream(rpIn);
330 			rpOut = new BufferedOutputStream(rpOut);
331 
332 			init(rpIn, rpOut);
333 			readAdvertisedRefs();
334 		}
335 
336 		@Override
337 		public void close() {
338 			super.close();
339 
340 			if (receivePack != null) {
341 				try {
342 					receivePack.waitFor();
343 				} catch (InterruptedException ie) {
344 					// Stop waiting and return anyway.
345 				} finally {
346 					receivePack = null;
347 				}
348 			}
349 
350 			if (errorReaderThread != null) {
351 				try {
352 					errorReaderThread.join();
353 				} catch (InterruptedException e) {
354 					// Stop waiting and return anyway.
355 				} finally {
356 					errorReaderThread = null;
357 				}
358 			}
359 		}
360 	}
361 }