View Javadoc
1   /*
2    * Copyright (C) 2014, Google Inc.
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  package org.eclipse.jgit.gitrepo;
44  
45  import static java.nio.charset.StandardCharsets.UTF_8;
46  import static org.eclipse.jgit.lib.Constants.DEFAULT_REMOTE_NAME;
47  import static org.eclipse.jgit.lib.Constants.R_REMOTES;
48  
49  import java.io.File;
50  import java.io.FileInputStream;
51  import java.io.IOException;
52  import java.io.InputStream;
53  import java.net.URI;
54  import java.text.MessageFormat;
55  import java.util.ArrayList;
56  import java.util.List;
57  import java.util.Map;
58  import java.util.Objects;
59  import java.util.StringJoiner;
60  import java.util.TreeMap;
61  
62  import org.eclipse.jgit.annotations.NonNull;
63  import org.eclipse.jgit.annotations.Nullable;
64  import org.eclipse.jgit.api.Git;
65  import org.eclipse.jgit.api.GitCommand;
66  import org.eclipse.jgit.api.SubmoduleAddCommand;
67  import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
68  import org.eclipse.jgit.api.errors.GitAPIException;
69  import org.eclipse.jgit.api.errors.InvalidRefNameException;
70  import org.eclipse.jgit.api.errors.JGitInternalException;
71  import org.eclipse.jgit.dircache.DirCache;
72  import org.eclipse.jgit.dircache.DirCacheBuilder;
73  import org.eclipse.jgit.dircache.DirCacheEntry;
74  import org.eclipse.jgit.gitrepo.ManifestParser.IncludedFileReader;
75  import org.eclipse.jgit.gitrepo.RepoProject.CopyFile;
76  import org.eclipse.jgit.gitrepo.RepoProject.LinkFile;
77  import org.eclipse.jgit.gitrepo.internal.RepoText;
78  import org.eclipse.jgit.internal.JGitText;
79  import org.eclipse.jgit.lib.CommitBuilder;
80  import org.eclipse.jgit.lib.Config;
81  import org.eclipse.jgit.lib.Constants;
82  import org.eclipse.jgit.lib.FileMode;
83  import org.eclipse.jgit.lib.ObjectId;
84  import org.eclipse.jgit.lib.ObjectInserter;
85  import org.eclipse.jgit.lib.PersonIdent;
86  import org.eclipse.jgit.lib.ProgressMonitor;
87  import org.eclipse.jgit.lib.Ref;
88  import org.eclipse.jgit.lib.RefDatabase;
89  import org.eclipse.jgit.lib.RefUpdate;
90  import org.eclipse.jgit.lib.RefUpdate.Result;
91  import org.eclipse.jgit.lib.Repository;
92  import org.eclipse.jgit.revwalk.RevCommit;
93  import org.eclipse.jgit.revwalk.RevWalk;
94  import org.eclipse.jgit.treewalk.TreeWalk;
95  import org.eclipse.jgit.util.FileUtils;
96  
97  /**
98   * A class used to execute a repo command.
99   *
100  * This will parse a repo XML manifest, convert it into .gitmodules file and the
101  * repository config file.
102  *
103  * If called against a bare repository, it will replace all the existing content
104  * of the repository with the contents populated from the manifest.
105  *
106  * repo manifest allows projects overlapping, e.g. one project's manifestPath is
107  * "foo" and another project's manifestPath is "foo/bar". This won't
108  * work in git submodule, so we'll skip all the sub projects
109  * ("foo/bar" in the example) while converting.
110  *
111  * @see <a href="https://code.google.com/p/git-repo/">git-repo project page</a>
112  * @since 3.4
113  */
114 public class RepoCommand extends GitCommand<RevCommit> {
115 	private String manifestPath;
116 	private String baseUri;
117 	private URI targetUri;
118 	private String groupsParam;
119 	private String branch;
120 	private String targetBranch = Constants.HEAD;
121 	private boolean recordRemoteBranch = true;
122 	private boolean recordSubmoduleLabels = true;
123 	private boolean recordShallowSubmodules = true;
124 	private PersonIdent author;
125 	private RemoteReader callback;
126 	private InputStream inputStream;
127 	private IncludedFileReader includedReader;
128 	private boolean ignoreRemoteFailures = false;
129 
130 	private ProgressMonitor monitor;
131 
132 	/**
133 	 * A callback to get ref sha1 of a repository from its uri.
134 	 *
135 	 * We provided a default implementation {@link DefaultRemoteReader} to
136 	 * use ls-remote command to read the sha1 from the repository and clone the
137 	 * repository to read the file. Callers may have their own quicker
138 	 * implementation.
139 	 *
140 	 * @since 3.4
141 	 */
142 	public interface RemoteReader {
143 		/**
144 		 * Read a remote ref sha1.
145 		 *
146 		 * @param uri
147 		 *            The URI of the remote repository
148 		 * @param ref
149 		 *            Name of the ref to lookup. May be a short-hand form, e.g.
150 		 *            "master" which is automatically expanded to
151 		 *            "refs/heads/master" if "refs/heads/master" already exists.
152 		 * @return the sha1 of the remote repository, or null if the ref does
153 		 *         not exist.
154 		 * @throws GitAPIException
155 		 */
156 		@Nullable
157 		public ObjectId sha1(String uri, String ref) throws GitAPIException;
158 
159 		/**
160 		 * Read a file from a remote repository.
161 		 *
162 		 * @param uri
163 		 *            The URI of the remote repository
164 		 * @param ref
165 		 *            The ref (branch/tag/etc.) to read
166 		 * @param path
167 		 *            The relative path (inside the repo) to the file to read
168 		 * @return the file content.
169 		 * @throws GitAPIException
170 		 * @throws IOException
171 		 * @since 3.5
172 		 *
173 		 * @deprecated Use {@link #readFileWithMode(String, String, String)}
174 		 *             instead
175 		 */
176 		@Deprecated
177 		public default byte[] readFile(String uri, String ref, String path)
178 				throws GitAPIException, IOException {
179 			return readFileWithMode(uri, ref, path).getContents();
180 		}
181 
182 		/**
183 		 * Read contents and mode (i.e. permissions) of the file from a remote
184 		 * repository.
185 		 *
186 		 * @param uri
187 		 *            The URI of the remote repository
188 		 * @param ref
189 		 *            Name of the ref to lookup. May be a short-hand form, e.g.
190 		 *            "master" which is automatically expanded to
191 		 *            "refs/heads/master" if "refs/heads/master" already exists.
192 		 * @param path
193 		 *            The relative path (inside the repo) to the file to read
194 		 * @return The contents and file mode of the file in the given
195 		 *         repository and branch. Never null.
196 		 * @throws GitAPIException
197 		 *             If the ref have an invalid or ambiguous name, or it does
198 		 *             not exist in the repository,
199 		 * @throws IOException
200 		 *             If the object does not exist or is too large
201 		 * @since 5.2
202 		 */
203 		@NonNull
204 		public RemoteFile readFileWithMode(String uri, String ref, String path)
205 				throws GitAPIException, IOException;
206 	}
207 
208 	/**
209 	 * Read-only view of contents and file mode (i.e. permissions) for a file in
210 	 * a remote repository.
211 	 *
212 	 * @since 5.2
213 	 */
214 	public static final class RemoteFile {
215 		@NonNull
216 		private final byte[] contents;
217 
218 		@NonNull
219 		private final FileMode fileMode;
220 
221 		/**
222 		 * @param contents
223 		 *            Raw contents of the file.
224 		 * @param fileMode
225 		 *            Git file mode for this file (e.g. executable or regular)
226 		 */
227 		public RemoteFile(@NonNull byte[] contents,
228 				@NonNull FileMode fileMode) {
229 			this.contents = Objects.requireNonNull(contents);
230 			this.fileMode = Objects.requireNonNull(fileMode);
231 		}
232 
233 		/**
234 		 * Contents of the file.
235 		 * <p>
236 		 * Callers who receive this reference must not modify its contents (as
237 		 * it can point to internal cached data).
238 		 *
239 		 * @return Raw contents of the file. Do not modify it.
240 		 */
241 		@NonNull
242 		public byte[] getContents() {
243 			return contents;
244 		}
245 
246 		/**
247 		 * @return Git file mode for this file (e.g. executable or regular)
248 		 */
249 		@NonNull
250 		public FileMode getFileMode() {
251 			return fileMode;
252 		}
253 
254 	}
255 
256 	/** A default implementation of {@link RemoteReader} callback. */
257 	public static class DefaultRemoteReader implements RemoteReader {
258 
259 		@Override
260 		public ObjectId sha1(String uri, String ref) throws GitAPIException {
261 			Map<String, Ref> map = Git
262 					.lsRemoteRepository()
263 					.setRemote(uri)
264 					.callAsMap();
265 			Ref r = RefDatabase.findRef(map, ref);
266 			return r != null ? r.getObjectId() : null;
267 		}
268 
269 		@Override
270 		public RemoteFile readFileWithMode(String uri, String ref, String path)
271 				throws GitAPIException, IOException {
272 			File dir = FileUtils.createTempDir("jgit_", ".git", null); //$NON-NLS-1$ //$NON-NLS-2$
273 			try (Git git = Git.cloneRepository().setBare(true).setDirectory(dir)
274 					.setURI(uri).call()) {
275 				Repository repo = git.getRepository();
276 				ObjectId refCommitId = sha1(uri, ref);
277 				if (refCommitId == null) {
278 					throw new InvalidRefNameException(MessageFormat
279 							.format(JGitText.get().refNotResolved, ref));
280 				}
281 				RevCommit commit = repo.parseCommit(refCommitId);
282 				TreeWalk tw = TreeWalk.forPath(repo, path, commit.getTree());
283 
284 				// TODO(ifrade): Cope better with big files (e.g. using
285 				// InputStream instead of byte[])
286 				return new RemoteFile(
287 						tw.getObjectReader().open(tw.getObjectId(0))
288 								.getCachedBytes(Integer.MAX_VALUE),
289 						tw.getFileMode(0));
290 			} finally {
291 				FileUtils.delete(dir, FileUtils.RECURSIVE);
292 			}
293 		}
294 	}
295 
296 	@SuppressWarnings("serial")
297 	private static class ManifestErrorException extends GitAPIException {
298 		ManifestErrorException(Throwable cause) {
299 			super(RepoText.get().invalidManifest, cause);
300 		}
301 	}
302 
303 	@SuppressWarnings("serial")
304 	private static class RemoteUnavailableException extends GitAPIException {
305 		RemoteUnavailableException(String uri) {
306 			super(MessageFormat.format(RepoText.get().errorRemoteUnavailable, uri));
307 		}
308 	}
309 
310 	/**
311 	 * Constructor for RepoCommand
312 	 *
313 	 * @param repo
314 	 *            the {@link org.eclipse.jgit.lib.Repository}
315 	 */
316 	public RepoCommand(Repository repo) {
317 		super(repo);
318 	}
319 
320 	/**
321 	 * Set path to the manifest XML file.
322 	 * <p>
323 	 * Calling {@link #setInputStream} will ignore the path set here.
324 	 *
325 	 * @param path
326 	 *            (with <code>/</code> as separator)
327 	 * @return this command
328 	 */
329 	public RepoCommand setPath(String path) {
330 		this.manifestPath = path;
331 		return this;
332 	}
333 
334 	/**
335 	 * Set the input stream to the manifest XML.
336 	 * <p>
337 	 * Setting inputStream will ignore the path set. It will be closed in
338 	 * {@link #call}.
339 	 *
340 	 * @param inputStream a {@link java.io.InputStream} object.
341 	 * @return this command
342 	 * @since 3.5
343 	 */
344 	public RepoCommand setInputStream(InputStream inputStream) {
345 		this.inputStream = inputStream;
346 		return this;
347 	}
348 
349 	/**
350 	 * Set base URI of the paths inside the XML. This is typically the name of
351 	 * the directory holding the manifest repository, eg. for
352 	 * https://android.googlesource.com/platform/manifest, this should be
353 	 * /platform (if you would run this on android.googlesource.com) or
354 	 * https://android.googlesource.com/platform elsewhere.
355 	 *
356 	 * @param uri
357 	 *            the base URI
358 	 * @return this command
359 	 */
360 	public RepoCommand setURI(String uri) {
361 		this.baseUri = uri;
362 		return this;
363 	}
364 
365 	/**
366 	 * Set the URI of the superproject (this repository), so the .gitmodules
367 	 * file can specify the submodule URLs relative to the superproject.
368 	 *
369 	 * @param uri
370 	 *            the URI of the repository holding the superproject.
371 	 * @return this command
372 	 * @since 4.8
373 	 */
374 	public RepoCommand setTargetURI(String uri) {
375 		// The repo name is interpreted as a directory, for example
376 		// Gerrit (http://gerrit.googlesource.com/gerrit) has a
377 		// .gitmodules referencing ../plugins/hooks, which is
378 		// on http://gerrit.googlesource.com/plugins/hooks,
379 		this.targetUri = URI.create(uri + "/"); //$NON-NLS-1$
380 		return this;
381 	}
382 
383 	/**
384 	 * Set groups to sync
385 	 *
386 	 * @param groups groups separated by comma, examples: default|all|G1,-G2,-G3
387 	 * @return this command
388 	 */
389 	public RepoCommand setGroups(String groups) {
390 		this.groupsParam = groups;
391 		return this;
392 	}
393 
394 	/**
395 	 * Set default branch.
396 	 * <p>
397 	 * This is generally the name of the branch the manifest file was in. If
398 	 * there's no default revision (branch) specified in manifest and no
399 	 * revision specified in project, this branch will be used.
400 	 *
401 	 * @param branch
402 	 *            a branch name
403 	 * @return this command
404 	 */
405 	public RepoCommand setBranch(String branch) {
406 		this.branch = branch;
407 		return this;
408 	}
409 
410 	/**
411 	 * Set target branch.
412 	 * <p>
413 	 * This is the target branch of the super project to be updated. If not set,
414 	 * default is HEAD.
415 	 * <p>
416 	 * For non-bare repositories, HEAD will always be used and this will be
417 	 * ignored.
418 	 *
419 	 * @param branch
420 	 *            branch name
421 	 * @return this command
422 	 * @since 4.1
423 	 */
424 	public RepoCommand setTargetBranch(String branch) {
425 		this.targetBranch = Constants.R_HEADS + branch;
426 		return this;
427 	}
428 
429 	/**
430 	 * Set whether the branch name should be recorded in .gitmodules.
431 	 * <p>
432 	 * Submodule entries in .gitmodules can include a "branch" field
433 	 * to indicate what remote branch each submodule tracks.
434 	 * <p>
435 	 * That field is used by "git submodule update --remote" to update
436 	 * to the tip of the tracked branch when asked and by Gerrit to
437 	 * update the superproject when a change on that branch is merged.
438 	 * <p>
439 	 * Subprojects that request a specific commit or tag will not have
440 	 * a branch name recorded.
441 	 * <p>
442 	 * Not implemented for non-bare repositories.
443 	 *
444 	 * @param enable Whether to record the branch name
445 	 * @return this command
446 	 * @since 4.2
447 	 */
448 	public RepoCommand setRecordRemoteBranch(boolean enable) {
449 		this.recordRemoteBranch = enable;
450 		return this;
451 	}
452 
453 	/**
454 	 * Set whether the labels field should be recorded as a label in
455 	 * .gitattributes.
456 	 * <p>
457 	 * Not implemented for non-bare repositories.
458 	 *
459 	 * @param enable Whether to record the labels in the .gitattributes
460 	 * @return this command
461 	 * @since 4.4
462 	 */
463 	public RepoCommand setRecordSubmoduleLabels(boolean enable) {
464 		this.recordSubmoduleLabels = enable;
465 		return this;
466 	}
467 
468 	/**
469 	 * Set whether the clone-depth field should be recorded as a shallow
470 	 * recommendation in .gitmodules.
471 	 * <p>
472 	 * Not implemented for non-bare repositories.
473 	 *
474 	 * @param enable Whether to record the shallow recommendation.
475 	 * @return this command
476 	 * @since 4.4
477 	 */
478 	public RepoCommand setRecommendShallow(boolean enable) {
479 		this.recordShallowSubmodules = enable;
480 		return this;
481 	}
482 
483 	/**
484 	 * The progress monitor associated with the clone operation. By default,
485 	 * this is set to <code>NullProgressMonitor</code>
486 	 *
487 	 * @see org.eclipse.jgit.lib.NullProgressMonitor
488 	 * @param monitor
489 	 *            a {@link org.eclipse.jgit.lib.ProgressMonitor}
490 	 * @return this command
491 	 */
492 	public RepoCommand setProgressMonitor(ProgressMonitor monitor) {
493 		this.monitor = monitor;
494 		return this;
495 	}
496 
497 	/**
498 	 * Set whether to skip projects whose commits don't exist remotely.
499 	 * <p>
500 	 * When set to true, we'll just skip the manifest entry and continue
501 	 * on to the next one.
502 	 * <p>
503 	 * When set to false (default), we'll throw an error when remote
504 	 * failures occur.
505 	 * <p>
506 	 * Not implemented for non-bare repositories.
507 	 *
508 	 * @param ignore Whether to ignore the remote failures.
509 	 * @return this command
510 	 * @since 4.3
511 	 */
512 	public RepoCommand setIgnoreRemoteFailures(boolean ignore) {
513 		this.ignoreRemoteFailures = ignore;
514 		return this;
515 	}
516 
517 	/**
518 	 * Set the author/committer for the bare repository commit.
519 	 * <p>
520 	 * For non-bare repositories, the current user will be used and this will be
521 	 * ignored.
522 	 *
523 	 * @param author
524 	 *            the author's {@link org.eclipse.jgit.lib.PersonIdent}
525 	 * @return this command
526 	 */
527 	public RepoCommand setAuthor(PersonIdent author) {
528 		this.author = author;
529 		return this;
530 	}
531 
532 	/**
533 	 * Set the GetHeadFromUri callback.
534 	 *
535 	 * This is only used in bare repositories.
536 	 *
537 	 * @param callback
538 	 *            a {@link org.eclipse.jgit.gitrepo.RepoCommand.RemoteReader}
539 	 *            object.
540 	 * @return this command
541 	 */
542 	public RepoCommand setRemoteReader(RemoteReader callback) {
543 		this.callback = callback;
544 		return this;
545 	}
546 
547 	/**
548 	 * Set the IncludedFileReader callback.
549 	 *
550 	 * @param reader
551 	 *            a
552 	 *            {@link org.eclipse.jgit.gitrepo.ManifestParser.IncludedFileReader}
553 	 *            object.
554 	 * @return this command
555 	 * @since 4.0
556 	 */
557 	public RepoCommand setIncludedFileReader(IncludedFileReader reader) {
558 		this.includedReader = reader;
559 		return this;
560 	}
561 
562 	/** {@inheritDoc} */
563 	@Override
564 	public RevCommit call() throws GitAPIException {
565 		checkCallable();
566 		if (baseUri == null) {
567 			baseUri = ""; //$NON-NLS-1$
568 		}
569 		if (inputStream == null) {
570 			if (manifestPath == null || manifestPath.length() == 0)
571 				throw new IllegalArgumentException(
572 						JGitText.get().pathNotConfigured);
573 			try {
574 				inputStream = new FileInputStream(manifestPath);
575 			} catch (IOException e) {
576 				throw new IllegalArgumentException(
577 						JGitText.get().pathNotConfigured);
578 			}
579 		}
580 
581 		List<RepoProject> filteredProjects;
582 		try {
583 			ManifestParser parser = new ManifestParser(includedReader,
584 					manifestPath, branch, baseUri, groupsParam, repo);
585 			parser.read(inputStream);
586 			filteredProjects = parser.getFilteredProjects();
587 		} catch (IOException e) {
588 			throw new ManifestErrorException(e);
589 		} finally {
590 			try {
591 				inputStream.close();
592 			} catch (IOException e) {
593 				// Just ignore it, it's not important.
594 			}
595 		}
596 
597 		if (repo.isBare()) {
598 			if (author == null)
599 				author = new PersonIdent(repo);
600 			if (callback == null)
601 				callback = new DefaultRemoteReader();
602 			List<RepoProject> renamedProjects = renameProjects(filteredProjects);
603 
604 			DirCache index = DirCache.newInCore();
605 			DirCacheBuilder builder = index.builder();
606 			ObjectInserter inserter = repo.newObjectInserter();
607 			try (RevWalkRevWalk.html#RevWalk">RevWalk rw = new RevWalk(repo)) {
608 				Config cfg = new Config();
609 				StringBuilder attributes = new StringBuilder();
610 				for (RepoProject proj : renamedProjects) {
611 					String name = proj.getName();
612 					String path = proj.getPath();
613 					String url = proj.getUrl();
614 					ObjectId objectId;
615 					if (ObjectId.isId(proj.getRevision())) {
616 						objectId = ObjectId.fromString(proj.getRevision());
617 					} else {
618 						objectId = callback.sha1(url, proj.getRevision());
619 						if (objectId == null && !ignoreRemoteFailures) {
620 							throw new RemoteUnavailableException(url);
621 						}
622 						if (recordRemoteBranch) {
623 							// can be branch or tag
624 							cfg.setString("submodule", name, "branch", //$NON-NLS-1$ //$NON-NLS-2$
625 									proj.getRevision());
626 						}
627 
628 						if (recordShallowSubmodules && proj.getRecommendShallow() != null) {
629 							// The shallow recommendation is losing information.
630 							// As the repo manifests stores the recommended
631 							// depth in the 'clone-depth' field, while
632 							// git core only uses a binary 'shallow = true/false'
633 							// hint, we'll map any depth to 'shallow = true'
634 							cfg.setBoolean("submodule", name, "shallow", //$NON-NLS-1$ //$NON-NLS-2$
635 									true);
636 						}
637 					}
638 					if (recordSubmoduleLabels) {
639 						StringBuilder rec = new StringBuilder();
640 						rec.append("/"); //$NON-NLS-1$
641 						rec.append(path);
642 						for (String group : proj.getGroups()) {
643 							rec.append(" "); //$NON-NLS-1$
644 							rec.append(group);
645 						}
646 						rec.append("\n"); //$NON-NLS-1$
647 						attributes.append(rec.toString());
648 					}
649 
650 					URI submodUrl = URI.create(url);
651 					if (targetUri != null) {
652 						submodUrl = relativize(targetUri, submodUrl);
653 					}
654 					cfg.setString("submodule", name, "path", path); //$NON-NLS-1$ //$NON-NLS-2$
655 					cfg.setString("submodule", name, "url", //$NON-NLS-1$ //$NON-NLS-2$
656 							submodUrl.toString());
657 
658 					// create gitlink
659 					if (objectId != null) {
660 						DirCacheEntry dcEntry = new DirCacheEntry(path);
661 						dcEntry.setObjectId(objectId);
662 						dcEntry.setFileMode(FileMode.GITLINK);
663 						builder.add(dcEntry);
664 
665 						for (CopyFile copyfile : proj.getCopyFiles()) {
666 							RemoteFile rf = callback.readFileWithMode(
667 								url, proj.getRevision(), copyfile.src);
668 							objectId = inserter.insert(Constants.OBJ_BLOB,
669 									rf.getContents());
670 							dcEntry = new DirCacheEntry(copyfile.dest);
671 							dcEntry.setObjectId(objectId);
672 							dcEntry.setFileMode(rf.getFileMode());
673 							builder.add(dcEntry);
674 						}
675 						for (LinkFile linkfile : proj.getLinkFiles()) {
676 							String link;
677 							if (linkfile.dest.contains("/")) { //$NON-NLS-1$
678 								link = FileUtils.relativizeGitPath(
679 									linkfile.dest.substring(0,
680 										linkfile.dest.lastIndexOf('/')),
681 									proj.getPath() + "/" + linkfile.src); //$NON-NLS-1$
682 							} else {
683 								link = proj.getPath() + "/" + linkfile.src; //$NON-NLS-1$
684 							}
685 
686 							objectId = inserter.insert(Constants.OBJ_BLOB,
687 									link.getBytes(UTF_8));
688 							dcEntry = new DirCacheEntry(linkfile.dest);
689 							dcEntry.setObjectId(objectId);
690 							dcEntry.setFileMode(FileMode.SYMLINK);
691 							builder.add(dcEntry);
692 						}
693 					}
694 				}
695 				String content = cfg.toText();
696 
697 				// create a new DirCacheEntry for .gitmodules file.
698 				final DirCacheEntrytry.html#DirCacheEntry">DirCacheEntry dcEntry = new DirCacheEntry(Constants.DOT_GIT_MODULES);
699 				ObjectId objectId = inserter.insert(Constants.OBJ_BLOB,
700 						content.getBytes(UTF_8));
701 				dcEntry.setObjectId(objectId);
702 				dcEntry.setFileMode(FileMode.REGULAR_FILE);
703 				builder.add(dcEntry);
704 
705 				if (recordSubmoduleLabels) {
706 					// create a new DirCacheEntry for .gitattributes file.
707 					final DirCacheEntryhtml#DirCacheEntry">DirCacheEntry dcEntryAttr = new DirCacheEntry(Constants.DOT_GIT_ATTRIBUTES);
708 					ObjectId attrId = inserter.insert(Constants.OBJ_BLOB,
709 							attributes.toString().getBytes(UTF_8));
710 					dcEntryAttr.setObjectId(attrId);
711 					dcEntryAttr.setFileMode(FileMode.REGULAR_FILE);
712 					builder.add(dcEntryAttr);
713 				}
714 
715 				builder.finish();
716 				ObjectId treeId = index.writeTree(inserter);
717 
718 				// Create a Commit object, populate it and write it
719 				ObjectId headId = repo.resolve(targetBranch + "^{commit}"); //$NON-NLS-1$
720 				if (headId != null && rw.parseCommit(headId).getTree().getId().equals(treeId)) {
721 					// No change. Do nothing.
722 					return rw.parseCommit(headId);
723 				}
724 
725 				CommitBuilder commit = new CommitBuilder();
726 				commit.setTreeId(treeId);
727 				if (headId != null)
728 					commit.setParentIds(headId);
729 				commit.setAuthor(author);
730 				commit.setCommitter(author);
731 				commit.setMessage(RepoText.get().repoCommitMessage);
732 
733 				ObjectId commitId = inserter.insert(commit);
734 				inserter.flush();
735 
736 				RefUpdate ru = repo.updateRef(targetBranch);
737 				ru.setNewObjectId(commitId);
738 				ru.setExpectedOldObjectId(headId != null ? headId : ObjectId.zeroId());
739 				Result rc = ru.update(rw);
740 
741 				switch (rc) {
742 					case NEW:
743 					case FORCED:
744 					case FAST_FORWARD:
745 						// Successful. Do nothing.
746 						break;
747 					case REJECTED:
748 					case LOCK_FAILURE:
749 						throw new ConcurrentRefUpdateException(
750 								MessageFormat.format(
751 										JGitText.get().cannotLock, targetBranch),
752 								ru.getRef(),
753 								rc);
754 					default:
755 						throw new JGitInternalException(MessageFormat.format(
756 								JGitText.get().updatingRefFailed,
757 								targetBranch, commitId.name(), rc));
758 				}
759 
760 				return rw.parseCommit(commitId);
761 			} catch (GitAPIException | IOException e) {
762 				throw new ManifestErrorException(e);
763 			}
764 		} else {
765 			try (Gitit.html#Git">Git git = new Git(repo)) {
766 				for (RepoProject proj : filteredProjects) {
767 					addSubmodule(proj.getName(), proj.getUrl(), proj.getPath(),
768 							proj.getRevision(), proj.getCopyFiles(),
769 							proj.getLinkFiles(), git);
770 				}
771 				return git.commit().setMessage(RepoText.get().repoCommitMessage)
772 						.call();
773 			} catch (GitAPIException | IOException e) {
774 				throw new ManifestErrorException(e);
775 			}
776 		}
777 	}
778 
779 	private void addSubmodule(String name, String url, String path,
780 			String revision, List<CopyFile> copyfiles, List<LinkFile> linkfiles,
781 			Git git) throws GitAPIException, IOException {
782 		assert (!repo.isBare());
783 		assert (git != null);
784 		if (!linkfiles.isEmpty()) {
785 			throw new UnsupportedOperationException(
786 					JGitText.get().nonBareLinkFilesNotSupported);
787 		}
788 
789 		SubmoduleAddCommand add = git.submoduleAdd().setName(name).setPath(path)
790 				.setURI(url);
791 		if (monitor != null)
792 			add.setProgressMonitor(monitor);
793 
794 		Repository subRepo = add.call();
795 		if (revision != null) {
796 			try (Gitit.html#Git">Git sub = new Git(subRepo)) {
797 				sub.checkout().setName(findRef(revision, subRepo)).call();
798 			}
799 			subRepo.close();
800 			git.add().addFilepattern(path).call();
801 		}
802 		for (CopyFile copyfile : copyfiles) {
803 			copyfile.copy();
804 			git.add().addFilepattern(copyfile.dest).call();
805 		}
806 	}
807 
808 	/**
809 	 * Rename the projects if there's a conflict when converted to submodules.
810 	 *
811 	 * @param projects
812 	 *            parsed projects
813 	 * @return projects that are renamed if necessary
814 	 */
815 	private List<RepoProject> renameProjects(List<RepoProject> projects) {
816 		Map<String, List<RepoProject>> m = new TreeMap<>();
817 		for (RepoProject proj : projects) {
818 			List<RepoProject> l = m.get(proj.getName());
819 			if (l == null) {
820 				l = new ArrayList<>();
821 				m.put(proj.getName(), l);
822 			}
823 			l.add(proj);
824 		}
825 
826 		List<RepoProject> ret = new ArrayList<>();
827 		for (List<RepoProject> ps : m.values()) {
828 			boolean nameConflict = ps.size() != 1;
829 			for (RepoProject proj : ps) {
830 				String name = proj.getName();
831 				if (nameConflict) {
832 					name += SLASH + proj.getPath();
833 				}
834 				RepoProject p = new RepoProject(name,
835 						proj.getPath(), proj.getRevision(), null,
836 						proj.getGroups(), proj.getRecommendShallow());
837 				p.setUrl(proj.getUrl());
838 				p.addCopyFiles(proj.getCopyFiles());
839 				p.addLinkFiles(proj.getLinkFiles());
840 				ret.add(p);
841 			}
842 		}
843 		return ret;
844 	}
845 
846 	/*
847 	 * Assume we are document "a/b/index.html", what should we put in a href to get to "a/" ?
848 	 * Returns the child if either base or child is not a bare path. This provides a missing feature in
849 	 * java.net.URI (see http://bugs.java.com/view_bug.do?bug_id=6226081).
850 	 */
851 	private static final String SLASH = "/"; //$NON-NLS-1$
852 	static URI relativize(URI current, URI target) {
853 		if (!Objects.equals(current.getHost(), target.getHost())) {
854 			return target;
855 		}
856 
857 		String cur = current.normalize().getPath();
858 		String dest = target.normalize().getPath();
859 
860 		// TODO(hanwen): maybe (absolute, relative) should throw an exception.
861 		if (cur.startsWith(SLASH) != dest.startsWith(SLASH)) {
862 			return target;
863 		}
864 
865 		while (cur.startsWith(SLASH)) {
866 			cur = cur.substring(1);
867 		}
868 		while (dest.startsWith(SLASH)) {
869 			dest = dest.substring(1);
870 		}
871 
872 		if (cur.indexOf('/') == -1 || dest.indexOf('/') == -1) {
873 			// Avoid having to special-casing in the next two ifs.
874 			String prefix = "prefix/"; //$NON-NLS-1$
875 			cur = prefix + cur;
876 			dest = prefix + dest;
877 		}
878 
879 		if (!cur.endsWith(SLASH)) {
880 			// The current file doesn't matter.
881 			int lastSlash = cur.lastIndexOf('/');
882 			cur = cur.substring(0, lastSlash);
883 		}
884 		String destFile = ""; //$NON-NLS-1$
885 		if (!dest.endsWith(SLASH)) {
886 			// We always have to provide the destination file.
887 			int lastSlash = dest.lastIndexOf('/');
888 			destFile = dest.substring(lastSlash + 1, dest.length());
889 			dest = dest.substring(0, dest.lastIndexOf('/'));
890 		}
891 
892 		String[] cs = cur.split(SLASH);
893 		String[] ds = dest.split(SLASH);
894 
895 		int common = 0;
896 		while (common < cs.length && common < ds.length && cs[common].equals(ds[common])) {
897 			common++;
898 		}
899 
900 		StringJoiner j = new StringJoiner(SLASH);
901 		for (int i = common; i < cs.length; i++) {
902 			j.add(".."); //$NON-NLS-1$
903 		}
904 		for (int i = common; i < ds.length; i++) {
905 			j.add(ds[i]);
906 		}
907 
908 		j.add(destFile);
909 		return URI.create(j.toString());
910 	}
911 
912 	private static String findRef(String ref, Repository repo)
913 			throws IOException {
914 		if (!ObjectId.isId(ref)) {
915 			Ref r = repo.exactRef(R_REMOTES + DEFAULT_REMOTE_NAME + "/" + ref); //$NON-NLS-1$
916 			if (r != null)
917 				return r.getName();
918 		}
919 		return ref;
920 	}
921 }