blob: b28b83968ac0bc428bb4ccedbfc88221a8bf51ee [file] [log] [blame]
// Copyright (C) 2009 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.google.gerrit.server.git;
import com.google.gerrit.reviewdb.Project;
import com.google.gerrit.reviewdb.Project.NameKey;
import com.google.gerrit.reviewdb.ReviewDb;
import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gerrit.server.project.ProjectControl;
import com.google.gwtorm.client.OrmException;
import com.google.gwtorm.client.SchemaFactory;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import com.jcraft.jsch.JSchException;
import org.eclipse.jgit.errors.NoRemoteRepositoryException;
import org.eclipse.jgit.errors.NotSupportedException;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.errors.TransportException;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.NullProgressMonitor;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.transport.FetchConnection;
import org.eclipse.jgit.transport.PushResult;
import org.eclipse.jgit.transport.RefSpec;
import org.eclipse.jgit.transport.RemoteConfig;
import org.eclipse.jgit.transport.RemoteRefUpdate;
import org.eclipse.jgit.transport.Transport;
import org.eclipse.jgit.transport.URIish;
import org.slf4j.Logger;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* A push to remote operation started by {@link ReplicationQueue}.
* <p>
* Instance members are protected by the lock within PushQueue. Callers must
* take that lock to ensure they are working with a current view of the object.
*/
class PushOp implements ProjectRunnable {
interface Factory {
PushOp create(Project.NameKey d, URIish u);
}
private static final Logger log = PushReplication.log;
static final String MIRROR_ALL = "..all..";
private final GitRepositoryManager repoManager;
private final SchemaFactory<ReviewDb> schema;
private final PushReplication.ReplicationConfig pool;
private final RemoteConfig config;
private final CredentialsProvider credentialsProvider;
private final TagCache tagCache;
private final Set<String> delta = new HashSet<String>();
private final Project.NameKey projectName;
private final URIish uri;
private boolean mirror;
private Repository db;
/**
* It indicates if the current instance is in fact retrying to push.
*/
private boolean retrying;
private boolean canceled;
@Inject
PushOp(final GitRepositoryManager grm, final SchemaFactory<ReviewDb> s,
final PushReplication.ReplicationConfig p, final RemoteConfig c,
final SecureCredentialsProvider.Factory cpFactory,
final TagCache tc,
@Assisted final Project.NameKey d, @Assisted final URIish u) {
repoManager = grm;
schema = s;
pool = p;
config = c;
credentialsProvider = cpFactory.create(c.getName());
tagCache = tc;
projectName = d;
uri = u;
}
public boolean isRetrying() {
return retrying;
}
public void setToRetry() {
retrying = true;
}
public void cancel() {
canceled = true;
}
public boolean wasCanceled() {
return canceled;
}
URIish getURI() {
return uri;
}
void addRef(final String ref) {
if (MIRROR_ALL.equals(ref)) {
delta.clear();
mirror = true;
} else if (!mirror) {
delta.add(ref);
}
}
public Set<String> getRefs() {
final Set<String> refs;
if (mirror) {
refs = new HashSet<String>(1);
refs.add(MIRROR_ALL);
} else {
refs = delta;
}
return refs;
}
public void addRefs(Set<String> refs) {
if (!mirror) {
for (String ref : refs) {
addRef(ref);
}
}
}
public void run() {
// Lock the queue, and remove ourselves, so we can't be modified once
// we start replication (instead a new instance, with the same URI, is
// created and scheduled for a future point in time.)
//
pool.notifyStarting(this);
// It should only verify if it was canceled after calling notifyStarting,
// since the canceled flag would be set locking the queue.
if (!canceled) {
try {
db = repoManager.openRepository(projectName);
runImpl();
} catch (RepositoryNotFoundException e) {
log.error("Cannot replicate " + projectName + "; " + e.getMessage());
} catch (NoRemoteRepositoryException e) {
log.error("Cannot replicate to " + uri + "; repository not found");
} catch (NotSupportedException e) {
log.error("Cannot replicate to " + uri, e);
} catch (TransportException e) {
final Throwable cause = e.getCause();
if (cause instanceof JSchException
&& cause.getMessage().startsWith("UnknownHostKey:")) {
log.error("Cannot replicate to " + uri + ": " + cause.getMessage());
} else {
log.error("Cannot replicate to " + uri, e);
}
// The remote push operation should be retried.
pool.reschedule(this);
} catch (IOException e) {
log.error("Cannot replicate to " + uri, e);
} catch (RuntimeException e) {
log.error("Unexpected error during replication to " + uri, e);
} catch (Error e) {
log.error("Unexpected error during replication to " + uri, e);
} finally {
if (db != null) {
db.close();
}
}
}
}
@Override
public String toString() {
return (mirror ? "mirror " : "push ") + uri;
}
private void runImpl() throws IOException {
final Transport tn = Transport.open(db, uri);
final PushResult res;
try {
res = pushVia(tn);
} finally {
try {
tn.close();
} catch (Throwable e2) {
log.warn("Unexpected error while closing " + uri, e2);
}
}
for (final RemoteRefUpdate u : res.getRemoteUpdates()) {
switch (u.getStatus()) {
case OK:
case UP_TO_DATE:
case NON_EXISTING:
break;
case NOT_ATTEMPTED:
case AWAITING_REPORT:
case REJECTED_NODELETE:
case REJECTED_NONFASTFORWARD:
case REJECTED_REMOTE_CHANGED:
log.error("Failed replicate of " + u.getRemoteName() + " to " + uri
+ ": status " + u.getStatus().name());
break;
case REJECTED_OTHER_REASON:
if ("non-fast-forward".equals(u.getMessage())) {
log.error("Failed replicate of " + u.getRemoteName() + " to " + uri
+ ", remote rejected non-fast-forward push."
+ " Check receive.denyNonFastForwards variable in config file"
+ " of destination repository.");
} else {
log.error("Failed replicate of " + u.getRemoteName() + " to " + uri
+ ", reason: " + u.getMessage());
}
break;
}
}
}
private PushResult pushVia(final Transport tn) throws IOException,
NotSupportedException, TransportException {
tn.applyConfig(config);
tn.setCredentialsProvider(credentialsProvider);
final List<RemoteRefUpdate> todo = generateUpdates(tn);
if (todo.isEmpty()) {
// If we have no commands selected, we have nothing to do.
// Calling JGit at this point would just redo the work we
// already did, and come up with the same answer. Instead
// send back an empty result.
//
return new PushResult();
}
return tn.push(NullProgressMonitor.INSTANCE, todo);
}
private List<RemoteRefUpdate> generateUpdates(final Transport tn)
throws IOException {
final ProjectControl pc;
try {
pc = pool.controlFor(projectName);
} catch (NoSuchProjectException e) {
return Collections.emptyList();
}
Map<String, Ref> local = db.getAllRefs();
if (!pc.allRefsAreVisible()) {
if (!mirror) {
// If we aren't mirroring, reduce the space we need to filter
// to only the references we will update during this operation.
//
Map<String, Ref> n = new HashMap<String, Ref>();
for (String src : delta) {
Ref r = local.get(src);
if (r != null) {
n.put(src, r);
}
}
local = n;
}
final ReviewDb meta;
try {
meta = schema.open();
} catch (OrmException e) {
log.error("Cannot read database to replicate to " + projectName, e);
return Collections.emptyList();
}
try {
local = new VisibleRefFilter(tagCache, db, pc, meta, true).filter(local);
} finally {
meta.close();
}
}
final List<RemoteRefUpdate> cmds = new ArrayList<RemoteRefUpdate>();
if (mirror) {
final Map<String, Ref> remote = listRemote(tn);
for (final Ref src : local.values()) {
final RefSpec spec = matchSrc(src.getName());
if (spec != null) {
final Ref dst = remote.get(spec.getDestination());
if (dst == null || !src.getObjectId().equals(dst.getObjectId())) {
// Doesn't exist yet, or isn't the same value, request to push.
//
send(cmds, spec, src);
}
}
}
for (final Ref ref : remote.values()) {
if (!Constants.HEAD.equals(ref.getName())) {
final RefSpec spec = matchDst(ref.getName());
if (spec != null && !local.containsKey(spec.getSource())) {
// No longer on local side, request removal.
//
delete(cmds, spec);
}
}
}
} else {
for (final String src : delta) {
final RefSpec spec = matchSrc(src);
if (spec != null) {
// If the ref still exists locally, send it, otherwise delete it.
//
Ref srcRef = local.get(src);
if (srcRef != null) {
send(cmds, spec, srcRef);
} else {
delete(cmds, spec);
}
}
}
}
return cmds;
}
private Map<String, Ref> listRemote(final Transport tn)
throws NotSupportedException, TransportException {
final FetchConnection fc = tn.openFetch();
try {
return fc.getRefsMap();
} finally {
fc.close();
}
}
private RefSpec matchSrc(final String ref) {
for (final RefSpec s : config.getPushRefSpecs()) {
if (s.matchSource(ref)) {
return s.expandFromSource(ref);
}
}
return null;
}
private RefSpec matchDst(final String ref) {
for (final RefSpec s : config.getPushRefSpecs()) {
if (s.matchDestination(ref)) {
return s.expandFromDestination(ref);
}
}
return null;
}
private void send(final List<RemoteRefUpdate> cmds, final RefSpec spec,
final Ref src) throws IOException {
final String dst = spec.getDestination();
final boolean force = spec.isForceUpdate();
cmds.add(new RemoteRefUpdate(db, src, dst, force, null, null));
}
private void delete(final List<RemoteRefUpdate> cmds, final RefSpec spec)
throws IOException {
final String dst = spec.getDestination();
final boolean force = spec.isForceUpdate();
cmds.add(new RemoteRefUpdate(db, (Ref) null, dst, force, null, null));
}
@Override
public NameKey getProjectNameKey() {
return projectName;
}
@Override
public String getRemoteName() {
return config.getName();
}
@Override
public boolean hasCustomizedPrint() {
return true;
}
}