blob: 887a464b579cd6b169ba263bc83c316af2cf824d [file]
// Copyright (C) 2026 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.replication;
import static com.googlesource.gerrit.plugins.replication.ReplicationQueue.repLog;
import com.google.common.base.Strings;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Project;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.googlesource.gerrit.plugins.replication.api.ReplicationConfig;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.transport.URIish;
import org.eclipse.jgit.util.QuotedString;
import org.eclipse.jgit.util.io.StreamCopyThread;
@Singleton
public class ProjectRepairer {
public enum Action {
COPY_LOOSE_OBJECTS,
COPY_PACKS;
public static List<Action> all() {
return List.of(values());
}
}
private static final String OBJECTS_DIR = "objects/";
private static final String PACK_DIR = OBJECTS_DIR + "pack/";
private final GitRepositoryManager gitManager;
private final ReplicationConfig replicationConfig;
@Inject
ProjectRepairer(GitRepositoryManager gitManager, ReplicationConfig replicationConfig) {
this.gitManager = gitManager;
this.replicationConfig = replicationConfig;
}
public boolean repair(
Project.NameKey project, URIish uri, OutputStream out, Collection<Action> actions)
throws InterruptedIOException {
if (actions.isEmpty()) {
return true;
}
Path objectsDir = objectsDir(project, uri);
if (objectsDir == null) {
return false;
}
boolean isRepaired = true;
for (Action action : actions) {
isRepaired &= repair(project, uri, out, objectsDir, action);
}
return isRepaired;
}
private boolean repair(
Project.NameKey project, URIish uri, OutputStream out, Path objectsDir, Action action)
throws InterruptedIOException {
boolean isRepaired =
switch (action) {
case COPY_LOOSE_OBJECTS -> copyLooseObjectsTo(objectsDir, uri, out);
case COPY_PACKS -> copyPacksTo(objectsDir.resolve("pack"), uri, out);
};
if (!isRepaired) {
repLog.atSevere().log("Repair (%s) failed for %s on %s", action, project.get(), uri);
}
return isRepaired;
}
public static boolean canCopy(URIish uri) {
return AdminApiFactory.isSSH(uri) && !AdminApiFactory.isGerrit(uri);
}
@Nullable
private Path objectsDir(Project.NameKey project, URIish uri) {
if (Strings.isNullOrEmpty(uri.getHost())) {
repLog.atSevere().log("Cannot repair %s: URI has no host: %s", project.get(), uri);
return null;
}
if (Strings.isNullOrEmpty(uri.getPath())) {
repLog.atSevere().log("Cannot repair %s: URI has no path: %s", project.get(), uri);
return null;
}
try (Repository repo = gitManager.openRepository(project)) {
return repo.getDirectory().toPath().resolve("objects");
} catch (IOException e) {
repLog.atSevere().withCause(e).log("Cannot open repository %s for repair", project.get());
return null;
}
}
private boolean copyLooseObjectsTo(Path objectsDir, URIish uri, OutputStream out)
throws InterruptedIOException {
if (!Files.isDirectory(objectsDir)) {
repLog.atSevere().log("No objects directory %s", objectsDir);
return false;
}
return copy(objectsDir, uri, out, OBJECTS_DIR, "/??/", "/??/*") == 0;
}
private boolean copyPacksTo(Path packDir, URIish uri, OutputStream out)
throws InterruptedIOException {
if (!Files.isDirectory(packDir)) {
repLog.atSevere().log("No objects/pack directory %s", packDir);
return false;
}
return copy(packDir, uri, out, PACK_DIR, "*.pack") == 0
&& copy(packDir, uri, out, PACK_DIR, "*.idx", "*.bitmap", "*.rev") == 0;
}
private int copy(Path src, URIish uri, OutputStream out, String destDir, String... includes)
throws InterruptedIOException {
List<String> cmd = new ArrayList<>();
cmd.add(replicationConfig.getRsyncPath());
cmd.add("-av");
cmd.add("--progress");
cmd.add("-e");
cmd.add(buildSshTransport(uri));
for (String inc : includes) {
cmd.add("--include=" + inc);
}
cmd.add("--exclude=*");
cmd.add(src.toAbsolutePath().normalize() + "/");
cmd.add(buildCopyDestination(uri, destDir));
repLog.atInfo().log("Running repair cmd: %s", String.join(" ", cmd));
ProcessBuilder pb = new ProcessBuilder(cmd);
pb.redirectErrorStream(true);
Process p;
try {
p = pb.start();
} catch (IOException e) {
repLog.atWarning().withCause(e).log("Copy to %s failed", uri);
return -1;
}
StreamCopyThread outStream = new StreamCopyThread(p.getInputStream(), out);
outStream.setName("repair-copy-output");
outStream.start();
try {
int code = p.waitFor();
outStream.join();
if (code != 0) {
repLog.atWarning().log("Copy to %s failed with exit code %d", uri, code);
}
return code;
} catch (InterruptedException e) {
p.destroyForcibly();
try {
outStream.halt();
} catch (InterruptedException ignored) {
// ignore
}
throw (InterruptedIOException)
new InterruptedIOException("Interrupted during copy to " + uri).initCause(e);
}
}
private static String buildCopyDestination(URIish uri, String destDir) {
String host = uri.getHost();
String path = uri.getPath();
String remotePath = QuotedString.BOURNE.quote(path + "/" + destDir);
String user = uri.getUser();
if (user != null && !user.isEmpty()) {
return user + "@" + host + ":" + remotePath;
}
return host + ":" + remotePath;
}
private static String buildSshTransport(URIish uri) {
StringBuilder sb = new StringBuilder("ssh -o BatchMode=yes");
int port = uri.getPort();
if (port > 0) {
sb.append(" -p ").append(port);
}
return sb.toString();
}
}