blob: 7e82ff22c7e35f1987d0b9806806a33b226632ad [file] [log] [blame]
// Copyright (C) 2015 Advanced Micro Devices, Inc. All rights reserved.
//
// 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.amd.gerrit.plugins.manifestsubscription;
import static com.google.gerrit.reviewdb.client.RefNames.REFS_CONFIG;
import com.amd.gerrit.plugins.manifestsubscription.manifest.Manifest;
import com.google.common.collect.*;
import com.google.gerrit.extensions.annotations.PluginName;
import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.MetaDataUpdate;
import com.google.gerrit.server.git.ProjectConfig;
import com.google.gerrit.server.project.ProjectCache;
import com.google.inject.Inject;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.xml.bind.JAXBException;
import java.io.IOException;
import java.util.*;
public class ManifestSubscription implements
GitReferenceUpdatedListener, LifecycleListener {
private static final Logger log =
LoggerFactory.getLogger(ManifestSubscription.class);
private static final String KEY_BRANCH = "branch";
private static final String KEY_STORE = "store";
static final String STORE_BRANCH_PREFIX = "refs/heads/m/";
private final String pluginName;
private final MetaDataUpdate.Server metaDataUpdateFactory;
private final GitRepositoryManager gitRepoManager;
private final ProjectCache projectCache;
private final GitReferenceUpdated gitRefUpdated;
/**
* source project lookup
* manifest source project name, plugin config
**/
private Map<String, PluginProjectConfig> enabledManifestSource = Maps.newHashMap();
/**
* manifest store lookup
* repo name, branch (branchPath), Manifest
*/
private Table<String, String, Manifest> manifestStores = HashBasedTable.create();
/**
* manifest source lookup
* repo name, branch (branchPath), manifest source repo
*/
private Table<String, String, String> manifestSource = HashBasedTable.create();
/**
* lookup
* subscribed project name and branch, manifest dest store, <manifest dest branch Project>
**/
private Table<ProjectBranchKey, String, Map<String, Set<
com.amd.gerrit.plugins.manifestsubscription.manifest.Project>>> subscribedRepos = HashBasedTable.create();
public Set<String> getEnabledManifestSource() {
return ImmutableSet.copyOf(enabledManifestSource.keySet());
}
public Set<ProjectBranchKey> getSubscribedProjects() {
return ImmutableSet.copyOf(subscribedRepos.rowKeySet());
}
public ImmutableTable<ProjectBranchKey, String, Map<String, Set<
com.amd.gerrit.plugins.manifestsubscription.manifest.Project>>> getSubscribedRepos() {
return new ImmutableTable.Builder<ProjectBranchKey, String, Map<String, Set<
com.amd.gerrit.plugins.manifestsubscription.manifest.Project>>>().putAll(subscribedRepos).build();
}
@Override
public void start() {
ProjectConfig config;
for (Project.NameKey p : projectCache.all()) {
//TODO parallelize parsing/load up?
try {
config = ProjectConfig.read(metaDataUpdateFactory.create(p));
loadStoreFromProjectConfig(p.toString(), config);
} catch (IOException | ConfigInvalidException | JAXBException e) {
log.error(e.toString());
e.printStackTrace();
}
}
}
@Override
public void stop() {
}
@Inject
ManifestSubscription(MetaDataUpdate.Server metaDataUpdateFactory,
GitRepositoryManager gitRepoManager,
@PluginName String pluginName,
ProjectCache projectCache,
GitReferenceUpdated gitRefUpdated) {
this.metaDataUpdateFactory = metaDataUpdateFactory;
this.gitRepoManager = gitRepoManager;
this.pluginName = pluginName;
this.projectCache = projectCache;
this.gitRefUpdated = gitRefUpdated;
}
@Override
public void onGitReferenceUpdated(Event event) {
String projectName = event.getProjectName();
String refName = event.getRefName();
String branchName = refName.startsWith("refs/heads/") ?
refName.substring(11) : refName;
ProjectBranchKey pbKey = new ProjectBranchKey(projectName, branchName);
if (event.getNewObjectId().equals(ObjectId.zeroId().toString())) {
// This happens when there's a branch deletion and possibly other events
log.info("Project: " + projectName +
"\nrefName: " + refName);
} else if (REFS_CONFIG.equals(refName)) {
// possible change in enabled repos
processProjectConfigChange(event);
} else if (enabledManifestSource.containsKey(projectName) &&
enabledManifestSource.get(projectName)
.getBranches().contains(branchName)) {
processManifestChange(event, projectName, branchName);
} else if (subscribedRepos.containsRow(pbKey)) {
//updates in subscribed repos
processRepoChange(event.getNewObjectId(), projectName, pbKey);
}
}
void processRepoChange(String refUpdatedHash, String projectName,
ProjectBranchKey pbKey) {
// Manifest store and branch
Map<String, Map<String, Set<
com.amd.gerrit.plugins.manifestsubscription.manifest.Project>>>
destinations = subscribedRepos.row(pbKey);
for (String store : destinations.keySet()) {
for (String storeBranch : destinations.get(store).keySet()) {
Set<com.amd.gerrit.plugins.manifestsubscription.manifest.Project> ps
= destinations.get(store).get(storeBranch);
Manifest manifest = manifestStores.get(store, storeBranch);
String manifestSrc = manifestSource.get(store, storeBranch);
StringBuilder extraCommitMsg = new StringBuilder();
Project.NameKey p = new Project.NameKey(projectName);
try (Repository r = gitRepoManager.openRepository(p);
RevWalk walk = new RevWalk(r)) {
RevCommit c = walk.parseCommit(
ObjectId.fromString(refUpdatedHash));
extraCommitMsg.append(refUpdatedHash.substring(0,7));
extraCommitMsg.append(" ");
extraCommitMsg.append(projectName);
extraCommitMsg.append(" ");
extraCommitMsg.append(c.getShortMessage());
} catch (IOException e) {
e.printStackTrace();
}
// these are project from the above manifest previously
// cached in the lookup table
for (com.amd.gerrit.plugins.manifestsubscription.manifest.Project
updateProject : ps) {
updateProject.setRevision(refUpdatedHash);
}
try {
Utilities.updateManifest(gitRepoManager, metaDataUpdateFactory,
gitRefUpdated, store, STORE_BRANCH_PREFIX + storeBranch,
manifest, manifestSrc, extraCommitMsg.toString(), null);
} catch (JAXBException | IOException e) {
e.printStackTrace();
}
}
}
}
private void updateProjectRev(String projectName, String branch, String rev,
List<com.amd.gerrit.plugins.
manifestsubscription.manifest.Project> projects) {
//TODO optimize to not have to iterate through manifest?
for (com.amd.gerrit.plugins.manifestsubscription.manifest.Project project : projects) {
if (Objects.equals(projectName, project.getName()) &&
Objects.equals(branch, project.getUpstream())) {
project.setRevision(rev);
}
if (project.getProject().size() > 0) {
updateProjectRev(projectName, branch, rev, project.getProject());
}
}
}
private void processManifestChange(Event event,
String projectName, String branchName) {
try {
VersionedManifests versionedManifests = parseManifests(event);
processManifestChange(versionedManifests, projectName, branchName);
} catch (JAXBException | IOException | ConfigInvalidException e) {
e.printStackTrace();
}
}
private void processManifestChange(VersionedManifests versionedManifests,
String projectName, String branchName) {
//possible manifest update in subscribing repos
//TODO Fix, right now update all manifest every time
//TODO even when only one of the manifest has changed
try {
if (versionedManifests != null) {
CanonicalManifest cManifest = new CanonicalManifest(versionedManifests);
Set<String> manifests = versionedManifests.getManifestPaths();
Manifest manifest;
String store = enabledManifestSource.get(projectName).getStore();
Table<String, String, String> lookup = HashBasedTable.create();
// Remove old manifest from subscription if destination store and branch
// matches manifest source being updated
// TODO again, this assume 1-1 map between store and manifest store
Map<String,
Set<com.amd.gerrit.plugins.manifestsubscription.manifest.Project>> branchPaths;
for (Table.Cell<ProjectBranchKey, String,
Map<String,
Set<com.amd.gerrit.plugins.manifestsubscription.manifest.Project>>> cell :
subscribedRepos.cellSet()) {
if (store.equals(cell.getColumnKey())) {
branchPaths = cell.getValue();
Iterator<String> iter = branchPaths.keySet().iterator();
String branchPath;
while (iter.hasNext()) {
branchPath = iter.next();
if (branchPath.startsWith(branchName+"/")) {
iter.remove();
manifestStores.remove(store, branchPath);
manifestSource.remove(store, branchPath);
}
}
}
}
//TODO need to make sure remote is pointing to this server?
//TODO this may be impossible
//TODO only monitor projects without 'remote' attribute / only using default?
for (String path : manifests) {
String bp = branchName + "/" + path;
try {
manifest = cManifest.getCanonicalManifest(path);
watchCanonicalManifest(manifest, store, bp, projectName);
VersionedManifests.affixManifest(gitRepoManager, manifest, lookup);
//save manifest
//TODO added the m/ to the ref to to work around LOCK_FAILURE error of creating master/bla/bla
//TODO (because default master ref already exists) better solution?
updateManifest(store, STORE_BRANCH_PREFIX + bp,
manifest, projectName);
} catch (ManifestReadException | GitAPIException e) {
e.printStackTrace();
}
}
}
} catch (JAXBException | IOException e) {
e.printStackTrace();
}
}
private void watchCanonicalManifest(Manifest manifest, String store,
String branchPath, String manifestSrc) {
String defaultBranch;
if (manifest.getDefault() != null &&
manifest.getDefault().getRevision() != null) {
defaultBranch = manifest.getDefault().getRevision();
} else {
defaultBranch = "";
}
manifestStores.put(store, branchPath, manifest);
manifestSource.put(store, branchPath, manifestSrc);
List<com.amd.gerrit.plugins.manifestsubscription.manifest.Project> projects
= manifest.getProject();
watchProjectsInCanonicalManifest(store, branchPath, defaultBranch, projects);
}
private void watchProjectsInCanonicalManifest(String store, String branchPath,
String defaultBranch,
List<com.amd.gerrit.plugins.manifestsubscription.manifest.Project> projects) {
ProjectBranchKey pbKey;
for (com.amd.gerrit.plugins.manifestsubscription.manifest.Project project : projects) {
if (manifestStores.containsRow(project.getName()) &&
!manifestStores.row(project.getName()).isEmpty()) {
// Skip if it's one of the repo for storing
// manifest to avoid infinite loop
// This is a bit too general, but it's done to avoid the complexity
// of actually tracing out the loop
// i.e. manifest1->store2 --> manifest2->store1
continue;
}
String branch = project.getRevision() == null ?
defaultBranch : project.getRevision();
pbKey = new ProjectBranchKey(project.getName(),
Repository.shortenRefName(branch));
//TODO only update manifests that changed
if (!subscribedRepos.contains(pbKey, store)) {
subscribedRepos.put(pbKey, store,
Maps.<String, Set<com.amd.gerrit.plugins.manifestsubscription.manifest.Project>>newHashMap());
}
Map<String,
Set<com.amd.gerrit.plugins.manifestsubscription.manifest.Project>> ps;
ps = subscribedRepos.get(pbKey, store);
if (!ps.containsKey(branchPath)) {
ps.put(branchPath,
Sets.<com.amd.gerrit.plugins.manifestsubscription.manifest.Project>newHashSet());
}
ps.get(branchPath).add(project);
if (project.getProject().size() > 0) {
watchProjectsInCanonicalManifest(store, branchPath, defaultBranch,
project.getProject());
}
}
}
private void processProjectConfigChange(Event event) {
Project.NameKey p = new Project.NameKey(event.getProjectName());
//TODO test two separate project configured to the same store
try {
ProjectConfig oldCfg = parseConfig(p, event.getOldObjectId());
ProjectConfig newCfg = parseConfig(p, event.getNewObjectId());
//TODO selectively update changes instead of complete reset
if (oldCfg != null) {
String oldStore =
oldCfg.getPluginConfig(pluginName).getString(KEY_STORE);
if (oldStore != null && !oldStore.isEmpty()) {
//TODO FIX assume unique store for each manifest source (1-1 map)
manifestStores.row(oldStore).clear();
manifestSource.row(oldStore).clear();
enabledManifestSource.remove(event.getProjectName());
Iterator<Table.Cell<ProjectBranchKey, String, Map<String,
Set<com.amd.gerrit.plugins.manifestsubscription.manifest.Project>>>> iter =
subscribedRepos.cellSet().iterator();
while (iter.hasNext()) {
if (oldStore.equals(iter.next().getColumnKey())) {
iter.remove();
}
}
}
}
if (newCfg != null) {
loadStoreFromProjectConfig(event.getProjectName(), newCfg);
}
} catch (IOException | ConfigInvalidException | JAXBException e) {
e.printStackTrace();
}
}
private void loadStoreFromProjectConfig(String projectName,
ProjectConfig config)
throws JAXBException, IOException, ConfigInvalidException {
String newStore =
config.getPluginConfig(pluginName).getString(KEY_STORE);
if (newStore != null) {
newStore = newStore.trim();
if (!newStore.isEmpty()) {
Set<String> branches = Sets.newHashSet(
config.getPluginConfig(pluginName)
.getStringList(KEY_BRANCH));
if (branches.size() > 0) {
PluginProjectConfig ppc = new PluginProjectConfig(newStore, branches);
enabledManifestSource.put(projectName, ppc);
Project.NameKey nameKey = new Project.NameKey(projectName);
VersionedManifests versionedManifests;
for (String branch : branches) {
versionedManifests = parseManifests(nameKey, branch);
processManifestChange(versionedManifests, projectName, branch);
}
}
}
}
}
private ProjectConfig parseConfig(Project.NameKey p, String idStr)
throws IOException, ConfigInvalidException {
ObjectId id = ObjectId.fromString(idStr);
if (ObjectId.zeroId().equals(id)) {
return null;
}
return ProjectConfig.read(metaDataUpdateFactory.create(p), id);
}
private VersionedManifests parseManifests(Event event)
throws JAXBException, IOException, ConfigInvalidException {
Project.NameKey p = new Project.NameKey(event.getProjectName());
return parseManifests(p, event.getRefName());
}
private VersionedManifests parseManifests(Project.NameKey p, String refName)
throws IOException, JAXBException, ConfigInvalidException {
Repository repo = gitRepoManager.openRepository(p);
ObjectId commitId = repo.resolve(refName);
MetaDataUpdate update = metaDataUpdateFactory.create(p);
VersionedManifests vManifests = new VersionedManifests(refName);
vManifests.load(update, commitId);
return vManifests;
}
private void updateManifest(String projectName, String refName,
Manifest manifest, String manifestSrc)
throws JAXBException, IOException {
Utilities.updateManifest(gitRepoManager, metaDataUpdateFactory, gitRefUpdated,
projectName, refName, manifest, manifestSrc, "", null);
}
}