blob: 638c267e6d718fa65151b324f94a919ab68d502c [file] [log] [blame]
// Copyright (C) 2015 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.googlesource.gerrit.plugins.repositoryuse;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.config.CanonicalWebUrl;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.diff.DiffFormatter;
import org.eclipse.jgit.diff.RawTextComparator;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.ObjectLoader;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.submodule.SubmoduleWalk;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.util.io.DisabledOutputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class RefUpdateHandlerImpl implements RefUpdateHandler {
private static final Logger log =
LoggerFactory.getLogger(RefUpdateHandlerImpl.class);
private RefUpdate event;
private final GitRepositoryManager repoManager;
private final String serverName;
@Inject
public RefUpdateHandlerImpl(@Assisted RefUpdate event,
GitRepositoryManager repoManager,
@CanonicalWebUrl String canonicalWebUrl) {
this.event = event;
this.repoManager = repoManager;
if (canonicalWebUrl != null) {
try {
URL url = new URL(canonicalWebUrl);
canonicalWebUrl = url.getHost();
} catch (MalformedURLException e) {
log.warn("Could not parse canonicalWebUrl", e);
}
}
this.serverName = canonicalWebUrl;
}
@Override
public void run() {
if (event.isDelete() && event.getRefName().startsWith(Constants.R_HEADS)
|| event.getRefName().startsWith(Constants.R_TAGS)) {
// Ref was deleted... clean up any references
Ref ref = Ref.fetchByRef(event.getProjectName(), event.getRefName());
if (ref != null) {
ref.delete();
}
if (event.getRefName().startsWith(Constants.R_HEADS)) {
// Also clean up uses from this ref
Usage.deleteByBranch(getCanonicalProject(event.getProjectName()),
event.getRefName());
}
} else if (event.getRefName().startsWith(Constants.R_TAGS)) {
Ref updatedRef = new Ref(event.getProjectName(), event.getRefName(),
event.getNewObjectId());
updatedRef.save();
} else if (event.getRefName().startsWith(Constants.R_HEADS)) {
Ref updatedRef = new Ref(event.getProjectName(), event.getRefName(),
event.getNewObjectId());
updatedRef.save();
Project.NameKey nameKey = new Project.NameKey(event.getProjectName());
try {
if (Config.refreshAllSubmodules() || event.isCreate()
|| isSubmoduleUpdate(event, nameKey)) {
Map<String, String> submodules = getSubmodules(event, nameKey);
updateProjects(event.getProjectName(), event.getRefName(),
submodules);
}
if (Config.parseManifests()) {
parseManifests(event, nameKey);
}
} catch (IOException e) {
log.error(e.getMessage(), e);
}
}
}
private void parseManifests(RefUpdate event, Project.NameKey project)
throws RepositoryNotFoundException, IOException {
if (event.isDelete()) {
return;
}
try (Repository repo = repoManager.openRepository(project)) {
try (RevWalk walk = new RevWalk(repo); TreeWalk tw = new TreeWalk(repo)) {
RevCommit commit =
walk.parseCommit(repo.resolve(event.getNewObjectId()));
tw.setRecursive(false);
tw.addTree(commit.getTree());
ObjectReader or = tw.getObjectReader();
while (tw.next()) {
String path = tw.getPathString();
if (path.endsWith(".xml")) {
ManifestParser mp = new ManifestParser();
ObjectLoader ol = or.open(tw.getObjectId(0));
if (!ol.isLarge()) {
Map<String, String> tmp = mp.parseManifest(ol.getBytes());
HashMap<String, String> projects = new HashMap<>();
for (String key : tmp.keySet()) {
projects
.put(
normalizePath(String.format("%s:%s",
event.getProjectName(), path), key, true),
tmp.get(key));
}
updateProjects(
String.format("%s:%s", event.getProjectName(), path),
event.getRefName(), projects);
} else {
log.warn(String.format("%s is too large, skipping manifest parse",
tw.getPathString()));
}
}
}
}
}
}
/**
* Has a submodule been updated?
*
* @param event the Event
* @return True if a submodule update occurred, otherwise False.
*/
private boolean isSubmoduleUpdate(RefUpdate event, Project.NameKey project)
throws RepositoryNotFoundException, IOException {
if (event.isDelete()) {
return false;
}
try (Repository repo = repoManager.openRepository(project)) {
try (RevWalk walk = new RevWalk(repo);
DiffFormatter df = new DiffFormatter(DisabledOutputStream.INSTANCE)) {
RevTree aTree = null;
if (!event.isCreate()) {
// If this is a new ref, we can't get the original commit.
// We can still use the DiffFormatter to give us what changed
// by passing null, however.
RevCommit aCommit =
walk.parseCommit(repo.resolve(event.getOldObjectId()));
aTree = aCommit.getTree();
}
RevCommit bCommit =
walk.parseCommit(repo.resolve(event.getNewObjectId()));
RevTree bTree = bCommit.getTree();
df.setRepository(repo);
df.setDiffComparator(RawTextComparator.DEFAULT);
df.setDetectRenames(true);
List<DiffEntry> diffEntries = df.scan(aTree, bTree);
for (DiffEntry de : diffEntries) {
FileMode oldMode = de.getOldMode();
FileMode newMode = de.getNewMode();
if ((oldMode != null && oldMode == FileMode.GITLINK)
|| (newMode != null && newMode == FileMode.GITLINK)) {
return true;
}
}
}
}
return false;
}
private Map<String, String> getSubmodules(RefUpdate event,
Project.NameKey project) throws RepositoryNotFoundException, IOException {
HashMap<String, String> submodules = new HashMap<>();
try (Repository repo = repoManager.openRepository(project)) {
try (RevWalk walk = new RevWalk(repo);
SubmoduleWalk sw = new SubmoduleWalk(repo)) {
RevCommit commit =
walk.parseCommit(repo.resolve(event.getNewObjectId()));
sw.setTree(commit.getTree());
sw.setRootTree(commit.getTree());
while (sw.next()) {
submodules.put(
normalizePath(event.getProjectName(), sw.getModulesUrl(), false),
sw.getObjectId().name());
}
} catch (ConfigInvalidException e) {
log.warn("Invalid .gitmodules configuration while parsing "
+ event.getProjectName());
}
}
return submodules;
}
private void updateProjects(String project, String branch,
Map<String, String> projects) {
String canonicalProject = getCanonicalProject(project);
List<Usage> uses = Usage.fetchByProject(canonicalProject, branch);
for (Usage use : uses) {
if (!projects.containsKey(use.getDestination())) {
// No longer exists; delete.
use.delete();
} else {
// Update SHA1 here.
use.setRef(projects.get(use.getDestination()));
use.save();
projects.remove(use.getDestination());
}
}
// At this point, submodules only contains new elements.
// Create them.
for (String key : projects.keySet()) {
Usage use = new Usage(canonicalProject, branch, key, projects.get(key));
use.save();
}
}
private String getCanonicalProject(String project) {
String canonicalProject =
String.format("https://%s/%s", serverName, project);
try {
URL url = new URL(canonicalProject);
canonicalProject = url.getHost() + url.getPath();
} catch (MalformedURLException e) {
log.warn("Could not parse project as URL: " + canonicalProject);
}
return canonicalProject;
}
private String normalizePath(String project, String destination,
boolean isManifest) {
String originalProject =
isManifest ? project.substring(0, project.lastIndexOf(":")) : project;
// Handle relative and absolute paths on the same server
if (destination.startsWith("/")) {
if (serverName != null) {
destination = serverName + destination;
} else {
log.warn("Could not parse absolute path; canonicalWebUrl not set");
}
} else if (destination.startsWith(".")) {
if (serverName != null) {
Path path = Paths.get(String.format("/%s/%s", project, destination));
destination = serverName + path.normalize().toString();
} else {
log.warn("Could not parse relative path; canonicalWebUrl not set");
}
} else if (!destination.matches("^[^:]+://.*")) {
if (serverName != null) {
destination = serverName + "/" + originalProject + "/" + destination;
} else {
log.warn("Could not parse relative path; canonicalWebURl not set");
}
}
try {
// Replace the protocol with a known scheme, to avoid angering URL
destination = destination.replaceFirst("^[^:]+://", "");
URL url = new URL("https://" + destination);
destination = url.getHost() + url.getPath();
} catch (MalformedURLException e) {
log.warn("Could not parse destination as URL: " + destination);
}
return destination;
}
}