blob: d19f7929c3207fd96a20479e7ada4e67e65b8b78 [file] [log] [blame]
// Copyright (C) 2018 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.renameproject.database;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ServerInitiated;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.account.AccountsUpdate;
import com.google.gerrit.server.account.ProjectWatches.NotifyType;
import com.google.gerrit.server.account.ProjectWatches.ProjectWatchKey;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ChangeNotes.Factory.ChangeNotesResult;
import com.google.gerrit.server.notedb.NotesMigration;
import com.google.gerrit.server.query.account.InternalAccountQuery;
import com.google.gwtorm.jdbc.JdbcSchema;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.SchemaFactory;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import com.googlesource.gerrit.plugins.renameproject.RenameRevertException;
import com.googlesource.gerrit.plugins.renameproject.monitor.ProgressMonitor;
import java.io.IOException;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Stream;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Singleton
public class DatabaseRenameHandler {
private static final Logger log = LoggerFactory.getLogger(DatabaseRenameHandler.class);
private final SchemaFactory<ReviewDb> schemaFactory;
private final ChangeNotes.Factory schemaFactoryNoteDb;
private final GitRepositoryManager repoManager;
private final Provider<InternalAccountQuery> accountQueryProvider;
private final Provider<AccountsUpdate> accountsUpdateProvider;
private NotesMigration migration;
@Inject
public DatabaseRenameHandler(
SchemaFactory<ReviewDb> schemaFactory,
ChangeNotes.Factory schemaFactoryNoteDb,
GitRepositoryManager repoManager,
NotesMigration migration,
Provider<InternalAccountQuery> accountQueryProvider,
@ServerInitiated Provider<AccountsUpdate> accountsUpdateProvider) {
this.accountQueryProvider = accountQueryProvider;
this.schemaFactory = schemaFactory;
this.schemaFactoryNoteDb = schemaFactoryNoteDb;
this.repoManager = repoManager;
this.accountsUpdateProvider = accountsUpdateProvider;
this.migration = migration;
}
public List<Change.Id> getChangeIds(Project.NameKey oldProjectKey)
throws OrmException, IOException {
log.debug("Starting to retrieve changes from the DB for project {}", oldProjectKey.get());
ReviewDb db = schemaFactory.open();
return (isNoteDb())
? getChangeIdsFromNoteDb(oldProjectKey, db)
: getChangeIdsFromReviewDb(oldProjectKey, db);
}
private List<Change.Id> getChangeIdsFromReviewDb(Project.NameKey oldProjectKey, ReviewDb db)
throws OrmException {
List<Change.Id> changeIds = new ArrayList<>();
String query =
"select change_id from changes where dest_project_name ='" + oldProjectKey.get() + "';";
try (Connection conn = ((JdbcSchema) db).getConnection();
Statement stmt = conn.createStatement();
ResultSet changes = stmt.executeQuery(query)) {
while (changes != null && changes.next()) {
Change.Id changeId = new Change.Id(changes.getInt(1));
changeIds.add(changeId);
}
log.debug(
"Number of changes in reviewDb related to project {} are {}",
oldProjectKey.get(),
changeIds.size());
return changeIds;
} catch (SQLException e) {
throw new OrmException(e);
}
}
private List<Change.Id> getChangeIdsFromNoteDb(Project.NameKey oldProjectKey, ReviewDb db)
throws IOException {
List<Change.Id> changeIds = new ArrayList<>();
Stream<ChangeNotesResult> changes =
schemaFactoryNoteDb.scan(repoManager.openRepository(oldProjectKey), db, oldProjectKey);
Iterator<ChangeNotesResult> iterator = changes.iterator();
while (iterator.hasNext()) {
ChangeNotesResult change = iterator.next();
changeIds.add(change.id());
}
log.debug(
"Number of changes in noteDb related to project {} are {}",
oldProjectKey.get(),
changeIds.size());
return changeIds;
}
private boolean isNoteDb() {
return migration.disableChangeReviewDb();
}
public List<Change.Id> rename(
List<Change.Id> changes,
Project.NameKey oldProjectKey,
Project.NameKey newProjectKey,
Optional<ProgressMonitor> opm)
throws OrmException, RenameRevertException {
opm.ifPresent(pm -> pm.beginTask("Updating changes in the database"));
ReviewDb db = schemaFactory.open();
return (isNoteDb())
? renameInNoteDb(changes, oldProjectKey, newProjectKey)
: renameInReviewDb(changes, oldProjectKey, newProjectKey, db);
}
private List<Change.Id> renameInReviewDb(
List<Change.Id> changes,
Project.NameKey oldProjectKey,
Project.NameKey newProjectKey,
ReviewDb db)
throws OrmException {
try (Connection conn = ((JdbcSchema) db).getConnection();
Statement stmt = conn.createStatement()) {
conn.setAutoCommit(false);
try {
try {
log.debug("Updating the changes in reviewDb related to project {}", oldProjectKey.get());
for (Change.Id cd : changes) {
stmt.addBatch(
"update changes set dest_project_name='"
+ newProjectKey.get()
+ "' where change_id ="
+ cd.id
+ ";");
}
stmt.executeBatch();
conn.commit();
} catch (SQLException e) {
throw new OrmException(e);
}
updateWatchEntries(oldProjectKey, newProjectKey);
log.debug(
"Successfully updated the changes in reviewDb related to project {}",
oldProjectKey.get());
return changes;
} catch (OrmException e) {
try {
log.error(
"Failed to update changes in reviewDb for project {}, exception caught: {}. Rolling back the operation.",
oldProjectKey.get(),
e.toString());
conn.rollback();
} catch (SQLException revertEx) {
log.error(
"Failed to rollback changes in reviewDb from project {} to project {}, exception caught: {}",
newProjectKey.get(),
oldProjectKey.get(),
revertEx.toString());
throw new RenameRevertException(revertEx, e);
}
try {
updateWatchEntries(newProjectKey, oldProjectKey);
} catch (OrmException revertEx) {
log.error(
"Failed to update watched changes in reviewDb from project {} to project {}, exception caught: {}",
newProjectKey.get(),
oldProjectKey.get(),
revertEx.toString());
throw new RenameRevertException(revertEx, e);
}
throw e;
} finally {
conn.setAutoCommit(true);
}
} catch (SQLException e) {
throw new OrmException(e);
}
}
private List<Change.Id> renameInNoteDb(
List<Change.Id> changes, Project.NameKey oldProjectKey, Project.NameKey newProjectKey)
throws OrmException {
log.debug("Updating the changes in noteDb related to project {}", oldProjectKey.get());
try {
updateWatchEntries(oldProjectKey, newProjectKey);
} catch (OrmException e) {
log.error(
"Failed to update changes in noteDb for project {}, exception caught: {}. Rolling back the operation.",
oldProjectKey.get(),
e.toString());
try {
updateWatchEntries(newProjectKey, oldProjectKey);
} catch (OrmException revertEx) {
log.error(
"Failed to rollback changes in noteDb from project {} to project {}, exception caught: {}",
newProjectKey.get(),
oldProjectKey.get(),
revertEx.toString());
throw new RenameRevertException(revertEx, e);
}
throw e;
}
log.debug(
"Successfully updated the changes in noteDb related to project {}", oldProjectKey.get());
return changes;
}
private void updateWatchEntries(Project.NameKey oldProjectKey, Project.NameKey newProjectKey)
throws OrmException {
for (AccountState a : accountQueryProvider.get().byWatchedProject(oldProjectKey)) {
Account.Id accountId = a.getAccount().getId();
ImmutableMap<ProjectWatchKey, ImmutableSet<NotifyType>> projectWatches =
a.getProjectWatches();
Map<ProjectWatchKey, Set<NotifyType>> newProjectWatches = new HashMap<>();
List<ProjectWatchKey> oldProjectWatches = new ArrayList<>();
for (ProjectWatchKey watchKey : a.getProjectWatches().keySet()) {
if (oldProjectKey.equals(watchKey.project())) {
newProjectWatches.put(
ProjectWatchKey.create(newProjectKey, watchKey.filter()),
projectWatches.get(watchKey));
oldProjectWatches.add(watchKey);
try {
accountsUpdateProvider
.get()
.update(
"Add watch entry",
accountId,
(accountState, update) ->
update.updateProjectWatches(newProjectWatches).build());
accountsUpdateProvider
.get()
.update(
"Remove watch entry",
accountId,
(accountState, update) ->
update.deleteProjectWatches(oldProjectWatches).build());
} catch (ConfigInvalidException e) {
log.error(
"Updating watch entry for user {} in project {} failed. Watch config found invalid.",
a.getUserName(),
newProjectKey.get(),
e);
throw new OrmException(e);
} catch (IOException e) {
log.error(
"Updating watch entry for user {} in project {} failed.",
a.getUserName(),
newProjectKey.get(),
e);
throw new OrmException(e);
}
}
}
}
}
}