Merge "Parse and record remote review host in .gitattributes"
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/ManifestParserTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/ManifestParserTest.java
index 0949d04..d8ac28c 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/ManifestParserTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/ManifestParserTest.java
@@ -254,4 +254,26 @@ public void testXXE() throws Exception {
 		assertTrue(e.getCause().getMessage().contains("DOCTYPE"));
 	}
 
+	@Test
+	public void testManifestParserWithReview() throws Exception {
+		String baseUrl = "https://git.google.com/";
+		StringBuilder xmlContent = new StringBuilder();
+		xmlContent.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
+				.append("<manifest>")
+				.append("<remote name=\"remote1\" fetch=\".\" review=\"https://review-1.com\" />")
+				.append("<remote name=\"remote2\" fetch=\".\" review=\"https://review-2.com\" />")
+				.append("<default revision=\"master\" remote=\"remote1\" />")
+				.append("<project path=\"foo\" name=\"foo\" />")
+				.append("<project path=\"bar\" name=\"bar\" remote=\"remote2\" />")
+				.append("</manifest>");
+
+		ManifestParser parser = new ManifestParser(null, null, "master", baseUrl, null, null);
+		parser.read(new ByteArrayInputStream(xmlContent.toString().getBytes(UTF_8)));
+
+		Map<String, RepoProject> map = parser.getProjects().stream()
+				.collect(Collectors.toMap(RepoProject::getPath, Function.identity()));
+		assertEquals("https://review-1.com", map.get("foo").getReview());
+		assertEquals("https://review-2.com", map.get("bar").getReview());
+	}
+
 }
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/RepoCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/RepoCommandTest.java
index 3162e79..ceb18e6 100644
--- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/RepoCommandTest.java
+++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/gitrepo/RepoCommandTest.java
@@ -1302,6 +1302,104 @@ public void testRecordSubmoduleLabels() throws Exception {
 	}
 
 	@Test
+	public void testRecordSubmoduleLabelsWithRemoteReview() throws Exception {
+		Repository remoteDb = createBareRepository();
+		Repository tempDb = createWorkRepository();
+
+		StringBuilder xmlContent = new StringBuilder();
+		xmlContent
+				.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
+				.append("<manifest>")
+				.append(
+						"<remote name=\"remote1\" fetch=\".\""
+								+ " review=\"https://googleplex-android-review.git.corp.google.com\" />")
+				.append("<default revision=\"master\" remote=\"remote1\" />")
+				.append("<project path=\"test\" ")
+				.append("revision=\"master\" ")
+				.append("name=\"")
+				.append(notDefaultUri)
+				.append("\" ")
+				.append("groups=\"a1,a2\" />")
+				.append("</manifest>");
+		JGitTestUtil.writeTrashFile(tempDb, "manifest.xml", xmlContent.toString());
+
+		RepoCommand command = new RepoCommand(remoteDb);
+		command
+				.setPath(tempDb.getWorkTree().getAbsolutePath() + "/manifest.xml")
+				.setURI(rootUri)
+				.setRecordSubmoduleLabels(true)
+				.call();
+		// Clone it
+		File directory = createTempDirectory("testBareRepo");
+		try (Repository localDb =
+				Git.cloneRepository()
+						.setDirectory(directory)
+						.setURI(remoteDb.getDirectory().toURI().toString())
+						.call()
+						.getRepository(); ) {
+			// The .gitattributes file should exist
+			File gitattributes = new File(localDb.getWorkTree(), ".gitattributes");
+			assertTrue("The .gitattributes file should exist", gitattributes.exists());
+			try (BufferedReader reader = Files.newBufferedReader(gitattributes.toPath(), UTF_8)) {
+				String content = reader.readLine();
+				assertEquals(
+						".gitattributes content should be as expected",
+						"/test a1 a2 remote-review=googleplex-android",
+						content);
+			}
+		}
+	}
+
+	@Test
+	public void testRecordSubmoduleLabelsWithDefaultReview() throws Exception {
+		Repository remoteDb = createBareRepository();
+		Repository tempDb = createWorkRepository();
+
+		StringBuilder xmlContent = new StringBuilder();
+		xmlContent
+				.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
+				.append("<manifest>")
+				.append("<remote name=\"remote1\" fetch=\".\" />")
+				.append(
+						"<default revision=\"master\" remote=\"remote1\""
+								+ " review=\"https://googleplex-android-review.git.corp.google.com\" />")
+				.append("<project path=\"test\" ")
+				.append("revision=\"master\" ")
+				.append("name=\"")
+				.append(notDefaultUri)
+				.append("\" ")
+				.append("groups=\"a1,a2\" />")
+				.append("</manifest>");
+		JGitTestUtil.writeTrashFile(tempDb, "manifest.xml", xmlContent.toString());
+
+		RepoCommand command = new RepoCommand(remoteDb);
+		command
+				.setPath(tempDb.getWorkTree().getAbsolutePath() + "/manifest.xml")
+				.setURI(rootUri)
+				.setRecordSubmoduleLabels(true)
+				.call();
+		// Clone it
+		File directory = createTempDirectory("testBareRepo");
+		try (Repository localDb =
+				Git.cloneRepository()
+						.setDirectory(directory)
+						.setURI(remoteDb.getDirectory().toURI().toString())
+						.call()
+						.getRepository(); ) {
+			// The .gitattributes file should exist
+			File gitattributes = new File(localDb.getWorkTree(), ".gitattributes");
+			assertTrue("The .gitattributes file should exist", gitattributes.exists());
+			try (BufferedReader reader = Files.newBufferedReader(gitattributes.toPath(), UTF_8)) {
+				String content = reader.readLine();
+				assertEquals(
+						".gitattributes content should be as expected",
+						"/test a1 a2 remote-review=googleplex-android",
+						content);
+			}
+		}
+	}
+
+	@Test
 	public void testRecordShallowRecommendation() throws Exception {
 		Repository remoteDb = createBareRepository();
 		Repository tempDb = createWorkRepository();
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/BareSuperprojectWriter.java b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/BareSuperprojectWriter.java
index e511a68..bfc7c9c 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/BareSuperprojectWriter.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/BareSuperprojectWriter.java
@@ -192,6 +192,11 @@ private void prepareIndex(List<RepoProject> projects, DirCache index,
 					rec.append(" "); //$NON-NLS-1$
 					rec.append(group);
 				}
+				String reviewHost = getPlainHost(proj.getReview());
+				if (reviewHost != null && !reviewHost.isEmpty()) {
+					rec.append(" remote-review="); //$NON-NLS-1$
+					rec.append(reviewHost);
+				}
 				rec.append("\n"); //$NON-NLS-1$
 				attributes.append(rec.toString());
 			}
@@ -320,4 +325,37 @@ private RevCommit commitTreeOnCurrentTip(ObjectInserter inserter,
 
 		return rw.parseCommit(commitId);
 	}
+
+	private static String getReviewHost(String reviewUrl) {
+		if (reviewUrl == null) {
+			return null;
+		}
+		try {
+			if (!reviewUrl.contains("://")) { //$NON-NLS-1$
+				reviewUrl = "https://" + reviewUrl; //$NON-NLS-1$
+			}
+			return URI.create(reviewUrl).getHost();
+		} catch (IllegalArgumentException e) {
+			return null;
+		}
+	}
+
+	/**
+	 * Return the host name without "-review" suffix.
+	 *
+	 * @param reviewUrl
+	 *            the review URL
+	 * @return host name without "-review" suffix
+	 */
+	private static String getPlainHost(String reviewUrl) {
+		String host = getReviewHost(reviewUrl);
+		if (host == null) {
+			return null;
+		}
+		int reviewIdx = host.indexOf("-review"); //$NON-NLS-1$
+		if (reviewIdx != -1) {
+			return host.substring(0, reviewIdx);
+		}
+		return host;
+	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/ManifestParser.java b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/ManifestParser.java
index 12ba893..32234a0 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/ManifestParser.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/ManifestParser.java
@@ -61,6 +61,7 @@ public class ManifestParser extends DefaultHandler {
 
 	private String defaultRemote;
 	private String defaultRevision;
+	private String defaultReview;
 	private int xmlInRead;
 	private RepoProject currentProject;
 
@@ -195,7 +196,8 @@ public void startElement(
 			String alias = attributes.getValue("alias");
 			String fetch = attributes.getValue("fetch");
 			String revision = attributes.getValue("revision");
-			Remote remote = new Remote(fetch, revision);
+			String review = attributes.getValue("review");
+			Remote remote = new Remote(fetch, revision, review);
 			remotes.put(attributes.getValue("name"), remote);
 			if (alias != null) {
 				remotes.put(alias, remote);
@@ -204,6 +206,7 @@ public void startElement(
 		case "default":
 			defaultRemote = attributes.getValue("remote");
 			defaultRevision = attributes.getValue("revision");
+			defaultReview = attributes.getValue("review");
 			break;
 		case "copyfile":
 			if (currentProject == null) {
@@ -314,6 +317,12 @@ public void endDocument() throws SAXException {
 			}
 			proj.setUrl(remoteUrl.resolve(proj.getName()).toString())
 				.setDefaultRevision(revision);
+			Remote r = remotes.get(remote);
+			if (r != null && r.review != null) {
+				proj.setReview(r.review);
+			} else if (defaultReview != null) {
+				proj.setReview(defaultReview);
+			}
 		}
 
 		filteredProjects.addAll(projects);
@@ -440,10 +449,12 @@ private boolean isNestedReferencefile(ReferenceFile referencefile) {
 	private static class Remote {
 		final String fetch;
 		final String revision;
+		final String review;
 
-		Remote(String fetch, String revision) {
+		Remote(String fetch, String revision, String review) {
 			this.fetch = fetch;
 			this.revision = revision;
+			this.review = review;
 		}
 	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoCommand.java
index 392ca92..32f2ad1 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoCommand.java
@@ -591,6 +591,7 @@ private List<RepoProject> renameProjects(List<RepoProject> projects) {
 				p.addCopyFiles(proj.getCopyFiles());
 				p.addLinkFiles(proj.getLinkFiles());
 				p.setUpstream(proj.getUpstream());
+				p.setReview(proj.getReview());
 				ret.add(p);
 			}
 		}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoProject.java b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoProject.java
index 07eb12d..97f461c 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoProject.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/gitrepo/RepoProject.java
@@ -41,6 +41,7 @@ public class RepoProject implements Comparable<RepoProject> {
 	private String upstream;
 	private String destBranch;
 	private String recommendShallow;
+	private String review;
 	private String url;
 	private String defaultRevision;
 
@@ -333,6 +334,27 @@ public void setRecommendShallow(String recommendShallow) {
 	}
 
 	/**
+	 * Return the Gerrit review server URL defined by the project's remote.
+	 *
+	 * @return the review server URL
+	 * @since 7.8
+	 */
+	public String getReview() {
+		return review;
+	}
+
+	/**
+	 * Sets the Gerrit review server URL.
+	 *
+	 * @param review the review server URL
+	 * @since 7.8
+	 */
+	public void setReview(String review) {
+		this.review = review;
+	}
+
+
+	/**
 	 * Add a copy file configuration.
 	 *
 	 * @param copyfile a {@link org.eclipse.jgit.gitrepo.RepoProject.CopyFile} object.