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