summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--build.xml1
-rw-r--r--releases.moxie8
-rw-r--r--src/main/distrib/data/gitblit.properties28
-rw-r--r--src/main/java/com/gitblit/GitBlit.java21
-rw-r--r--src/main/java/com/gitblit/MirrorExecutor.java169
-rw-r--r--src/main/java/com/gitblit/client/IndicatorsRenderer.java8
-rw-r--r--src/main/java/com/gitblit/git/GitblitReceivePack.java8
-rw-r--r--src/main/java/com/gitblit/models/RepositoryModel.java1
-rw-r--r--src/main/java/com/gitblit/models/TeamModel.java2
-rw-r--r--src/main/java/com/gitblit/models/UserModel.java2
-rw-r--r--src/main/java/com/gitblit/utils/JGitUtils.java50
-rw-r--r--src/main/java/com/gitblit/wicket/GitBlitWebApp.properties5
-rw-r--r--src/main/java/com/gitblit/wicket/pages/RepositoryPage.html4
-rw-r--r--src/main/java/com/gitblit/wicket/pages/RepositoryPage.java9
-rw-r--r--src/main/java/com/gitblit/wicket/panels/ProjectRepositoryPanel.html1
-rw-r--r--src/main/java/com/gitblit/wicket/panels/ProjectRepositoryPanel.java6
-rw-r--r--src/main/java/com/gitblit/wicket/panels/RepositoriesPanel.html2
-rw-r--r--src/main/java/com/gitblit/wicket/panels/RepositoriesPanel.java7
-rw-r--r--src/main/resources/mirror_16x16.pngbin0 -> 630 bytes
-rw-r--r--src/site/features.mkd1
-rw-r--r--src/test/java/com/gitblit/tests/PermissionsTest.java18
21 files changed, 343 insertions, 8 deletions
diff --git a/build.xml b/build.xml
index 57b7ca24..e4af1ef2 100644
--- a/build.xml
+++ b/build.xml
@@ -451,6 +451,7 @@
<resource file="${project.resources.dir}/commit_merge_16x16.png" />
<resource file="${project.resources.dir}/commit_divide_16x16.png" />
<resource file="${project.resources.dir}/star_16x16.png" />
+ <resource file="${project.resources.dir}/mirror_16x16.png" />
<resource file="${project.resources.dir}/blank.png" />
<resource file="${project.src.dir}/log4j.properties" />
<resource>
diff --git a/releases.moxie b/releases.moxie
index 1832284f..a3b5ee4b 100644
--- a/releases.moxie
+++ b/releases.moxie
@@ -16,6 +16,7 @@ r20: {
- Fix error on generating activity page when there is no activity
- Fix raw page content type of binaries when running behind a reverse proxy
changes:
+ - Gitblit now rejects pushes to mirror repositories (issue-5)
- Personal repository prefix (~) is now configurable (issue-265)
- Reversed line links in blob view (issue-309)
- Dashboard and Activity pages now obey the web.generateActivityGraph setting (issue-310)
@@ -24,11 +25,12 @@ r20: {
- Change the WAR baseFolder context parameter to a JNDI env-entry to improve enterprise deployments
- Removed internal Gitblit ref exclusions in the upload pack
- Removed "show readme" setting in favor of automatic detection
- - Support plain text "readme" files
+ - Support plain text, markdown, confluence, mediawiki, textile, tracwiki, or twiki "readme" files
- Determine best commit id (e.g. "master") for the tree and docs pages and use that in links
- - By default GO will now bind to all interfaces for both http and https connectors. This simplifies setup for first-time users.
+ - By default GO will now bind to all interfaces for both http and https connectors. This simplifies setup for first-time users.
- Removed docs indicator on the repositories page
additions:
+ - Added an optional MirrorExecutor which will periodically fetch ref updates from source repositories for mirrors (issue-5). Repositories must be manually cloned using native git and "--mirror".
- Added branch graph image servlet based on EGit's branch graph renderer (issue-194)
- Added option to render Markdown commit messages (issue-203)
- Added setting to control creating a repository as --shared on Unix servers (issue-263)
@@ -47,7 +49,9 @@ r20: {
settings:
- { name: 'git.createRepositoriesShared', defaultValue: 'false' }
- { name: 'git.allowAnonymousPushes', defaultValue: 'false' }
+ - { name: 'git.enableMirroring', defaultValue: 'false' }
- { name: 'git.defaultAccessRestriction', defaultValue: 'PUSH' }
+ - { name: 'git.mirrorPeriod', defaultValue: '30 mins' }
- { name: 'web.commitMessageRenderer', defaultValue: 'plain' }
- { name: 'web.showBranchGraph', defaultValue: 'true' }
- { name: 'server.redirectToHttpsPort', defaultValue: 'true' }
diff --git a/src/main/distrib/data/gitblit.properties b/src/main/distrib/data/gitblit.properties
index a791c1fd..2823c4de 100644
--- a/src/main/distrib/data/gitblit.properties
+++ b/src/main/distrib/data/gitblit.properties
@@ -276,6 +276,34 @@ git.defaultGarbageCollectionThreshold = 500k
# SINCE 1.2.0
git.defaultGarbageCollectionPeriod = 7
+# Gitblit can automatically fetch ref updates for a properly configured mirror
+# repository.
+#
+# Requirements:
+# 1. you must manually clone the repository using native git
+# git clone --mirror git://somewhere.com/myrepo.git
+# 2. the "origin" remote must be the mirror source
+# 3. the "origin" repository must be accessible without authentication OR the
+# credentials must be embedded in the origin url (not recommended)
+#
+# Notes:
+# 1. "origin" SSH urls are untested and not likely to work
+# 2. mirrors cloned while Gitblit is running are likely to require clearing the
+# gitblit cache (link on the repositories page of an administrator account)
+# 3. Gitblit will automatically repair any invalid fetch refspecs with a "//"
+# sequence.
+#
+# SINCE 1.4.0
+# RESTART REQUIRED
+git.enableMirroring = false
+
+# Specify the period between update checks for mirrored repositories.
+# The shortest period you may specify between mirror update checks is 5 mins.
+#
+# SINCE 1.4.0
+# RESTART REQUIRED
+git.mirrorPeriod = 30 mins
+
# Number of bytes of a pack file to load into memory in a single read operation.
# This is the "page size" of the JGit buffer cache, used for all pack access
# operations. All disk IO occurs as single window reads. Setting this too large
diff --git a/src/main/java/com/gitblit/GitBlit.java b/src/main/java/com/gitblit/GitBlit.java
index f313b6e3..a0e8b0a3 100644
--- a/src/main/java/com/gitblit/GitBlit.java
+++ b/src/main/java/com/gitblit/GitBlit.java
@@ -164,7 +164,7 @@ public class GitBlit implements ServletContextListener {
private final Logger logger = LoggerFactory.getLogger(GitBlit.class);
- private final ScheduledExecutorService scheduledExecutor = Executors.newScheduledThreadPool(5);
+ private final ScheduledExecutorService scheduledExecutor = Executors.newScheduledThreadPool(10);
private final List<FederationModel> federationRegistrations = Collections
.synchronizedList(new ArrayList<FederationModel>());
@@ -207,6 +207,8 @@ public class GitBlit implements ServletContextListener {
private GCExecutor gcExecutor;
+ private MirrorExecutor mirrorExecutor;
+
private TimeZone timezone;
private FileBasedConfig projectConfigs;
@@ -2035,6 +2037,7 @@ public class GitBlit implements ServletContextListener {
model.origin = config.getString("remote", "origin", "url");
if (model.origin != null) {
model.origin = model.origin.replace('\\', '/');
+ model.isMirror = config.getBoolean("remote", "origin", "mirror", false);
}
model.preReceiveScripts = new ArrayList<String>(Arrays.asList(config.getStringList(
Constants.CONFIG_GITBLIT, null, "preReceiveScript")));
@@ -3505,6 +3508,7 @@ public class GitBlit implements ServletContextListener {
mailExecutor = new MailExecutor(settings);
luceneExecutor = new LuceneExecutor(settings, repositoriesFolder);
gcExecutor = new GCExecutor(settings);
+ mirrorExecutor = new MirrorExecutor(settings);
// initialize utilities
String prefix = settings.getString(Keys.git.userRepositoryPrefix, "~");
@@ -3544,6 +3548,7 @@ public class GitBlit implements ServletContextListener {
configureMailExecutor();
configureLuceneIndexing();
configureGarbageCollector();
+ configureMirrorExecutor();
if (startFederation) {
configureFederation();
}
@@ -3595,6 +3600,19 @@ public class GitBlit implements ServletContextListener {
}
}
+ protected void configureMirrorExecutor() {
+ if (mirrorExecutor.isReady()) {
+ int mins = TimeUtils.convertFrequencyToMinutes(settings.getString(Keys.git.mirrorPeriod, "30 mins"));
+ if (mins < 5) {
+ mins = 5;
+ }
+ int delay = 1;
+ scheduledExecutor.scheduleAtFixedRate(mirrorExecutor, delay, mins, TimeUnit.MINUTES);
+ logger.info("Mirror executor is scheduled to fetch updates every {} minutes.", mins);
+ logger.info("Next scheduled mirror fetch is in {} minutes", delay);
+ }
+ }
+
protected void configureJGit() {
// Configure JGit
WindowCacheConfig cfg = new WindowCacheConfig();
@@ -3864,6 +3882,7 @@ public class GitBlit implements ServletContextListener {
scheduledExecutor.shutdownNow();
luceneExecutor.close();
gcExecutor.close();
+ mirrorExecutor.close();
if (fanoutService != null) {
fanoutService.stop();
}
diff --git a/src/main/java/com/gitblit/MirrorExecutor.java b/src/main/java/com/gitblit/MirrorExecutor.java
new file mode 100644
index 00000000..21c194f6
--- /dev/null
+++ b/src/main/java/com/gitblit/MirrorExecutor.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright 2013 gitblit.com.
+ *
+ * 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.gitblit;
+
+import java.text.MessageFormat;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.lib.RefUpdate.Result;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.StoredConfig;
+import org.eclipse.jgit.transport.FetchResult;
+import org.eclipse.jgit.transport.RemoteConfig;
+import org.eclipse.jgit.transport.TrackingRefUpdate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.models.UserModel;
+import com.gitblit.utils.JGitUtils;
+
+/**
+ * The Mirror executor handles periodic fetching of mirrored repositories.
+ *
+ * @author James Moger
+ *
+ */
+public class MirrorExecutor implements Runnable {
+
+ private final Logger logger = LoggerFactory.getLogger(MirrorExecutor.class);
+
+ private final Set<String> repairAttempted = Collections.synchronizedSet(new HashSet<String>());
+
+ private final IStoredSettings settings;
+
+ private AtomicBoolean running = new AtomicBoolean(false);
+
+ private AtomicBoolean forceClose = new AtomicBoolean(false);
+
+ private final UserModel gitblitUser;
+
+ public MirrorExecutor(IStoredSettings settings) {
+ this.settings = settings;
+ this.gitblitUser = new UserModel("gitblit");
+ this.gitblitUser.displayName = "Gitblit";
+ }
+
+ public boolean isReady() {
+ return settings.getBoolean(Keys.git.enableMirroring, false);
+ }
+
+ public boolean isRunning() {
+ return running.get();
+ }
+
+ public void close() {
+ forceClose.set(true);
+ }
+
+ @Override
+ public void run() {
+ if (!isReady()) {
+ return;
+ }
+
+ running.set(true);
+
+ for (String repositoryName : GitBlit.self().getRepositoryList()) {
+ if (forceClose.get()) {
+ break;
+ }
+ if (GitBlit.self().isCollectingGarbage(repositoryName)) {
+ logger.debug("mirror is skipping {} garbagecollection", repositoryName);
+ continue;
+ }
+ RepositoryModel model = null;
+ Repository repository = null;
+ try {
+ model = GitBlit.self().getRepositoryModel(repositoryName);
+ if (!model.isMirror && !model.isBare) {
+ // repository must be a valid bare git mirror
+ logger.debug("mirror is skipping {} !mirror !bare", repositoryName);
+ continue;
+ }
+
+ repository = GitBlit.self().getRepository(repositoryName);
+ if (repository == null) {
+ logger.warn(MessageFormat.format("MirrorExecutor is missing repository {0}?!?", repositoryName));
+ continue;
+ }
+
+ // automatically repair (some) invalid fetch ref specs
+ if (!repairAttempted.contains(repositoryName)) {
+ repairAttempted.add(repositoryName);
+ JGitUtils.repairFetchSpecs(repository);
+ }
+
+ // find the first mirror remote - there should only be one
+ StoredConfig rc = repository.getConfig();
+ RemoteConfig mirror = null;
+ List<RemoteConfig> configs = RemoteConfig.getAllRemoteConfigs(rc);
+ for (RemoteConfig config : configs) {
+ if (config.isMirror()) {
+ mirror = config;
+ break;
+ }
+ }
+
+ if (mirror == null) {
+ // repository does not have a mirror remote
+ logger.debug("mirror is skipping {} no mirror remote found", repositoryName);
+ continue;
+ }
+
+ logger.debug("checking {} remote {} for ref updates", repositoryName, mirror.getName());
+ final boolean testing = false;
+ Git git = new Git(repository);
+ FetchResult result = git.fetch().setRemote(mirror.getName()).setDryRun(testing).call();
+ Collection<TrackingRefUpdate> refUpdates = result.getTrackingRefUpdates();
+ if (refUpdates.size() > 0) {
+ for (TrackingRefUpdate ru : refUpdates) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("updated mirror ");
+ sb.append(repositoryName);
+ sb.append(" ");
+ sb.append(ru.getRemoteName());
+ sb.append(" -> ");
+ sb.append(ru.getLocalName());
+ if (ru.getResult() == Result.FORCED) {
+ sb.append(" (forced)");
+ }
+ sb.append(" ");
+ sb.append(ru.getOldObjectId() == null ? "" : ru.getOldObjectId().abbreviate(7).name());
+ sb.append("..");
+ sb.append(ru.getNewObjectId() == null ? "" : ru.getNewObjectId().abbreviate(7).name());
+ logger.info(sb.toString());
+ }
+ }
+ } catch (Exception e) {
+ logger.error("Error updating mirror " + repositoryName, e);
+ } finally {
+ // cleanup
+ if (repository != null) {
+ repository.close();
+ }
+ }
+ }
+
+ running.set(false);
+ }
+}
diff --git a/src/main/java/com/gitblit/client/IndicatorsRenderer.java b/src/main/java/com/gitblit/client/IndicatorsRenderer.java
index 5b61df66..5883ab04 100644
--- a/src/main/java/com/gitblit/client/IndicatorsRenderer.java
+++ b/src/main/java/com/gitblit/client/IndicatorsRenderer.java
@@ -56,6 +56,8 @@ public class IndicatorsRenderer extends JPanel implements TableCellRenderer, Ser
private final ImageIcon sparkleshareIcon;
+ private final ImageIcon mirrorIcon;
+
public IndicatorsRenderer() {
super(new FlowLayout(FlowLayout.RIGHT, 1, 0));
blankIcon = new ImageIcon(getClass().getResource("/blank.png"));
@@ -67,6 +69,7 @@ public class IndicatorsRenderer extends JPanel implements TableCellRenderer, Ser
federatedIcon = new ImageIcon(getClass().getResource("/federated_16x16.png"));
forkIcon = new ImageIcon(getClass().getResource("/commit_divide_16x16.png"));
sparkleshareIcon = new ImageIcon(getClass().getResource("/star_16x16.png"));
+ mirrorIcon = new ImageIcon(getClass().getResource("/mirror_16x16.png"));
}
@Override
@@ -85,6 +88,11 @@ public class IndicatorsRenderer extends JPanel implements TableCellRenderer, Ser
tooltip.append(Translation.get("gb.isSparkleshared")).append("<br/>");
add(icon);
}
+ if (model.isMirror) {
+ JLabel icon = new JLabel(mirrorIcon);
+ tooltip.append(Translation.get("gb.isMirror")).append("<br/>");
+ add(icon);
+ }
if (model.isFork()) {
JLabel icon = new JLabel(forkIcon);
tooltip.append(Translation.get("gb.isFork")).append("<br/>");
diff --git a/src/main/java/com/gitblit/git/GitblitReceivePack.java b/src/main/java/com/gitblit/git/GitblitReceivePack.java
index e6ff5721..ba200b25 100644
--- a/src/main/java/com/gitblit/git/GitblitReceivePack.java
+++ b/src/main/java/com/gitblit/git/GitblitReceivePack.java
@@ -120,6 +120,14 @@ public class GitblitReceivePack extends ReceivePack implements PreReceiveHook, P
@Override
public void onPreReceive(ReceivePack rp, Collection<ReceiveCommand> commands) {
+ if (repository.isMirror) {
+ // repository is a mirror
+ for (ReceiveCommand cmd : commands) {
+ sendRejection(cmd, "Gitblit does not allow pushes to \"{0}\" because it is a mirror!", repository.name);
+ }
+ return;
+ }
+
if (repository.isFrozen) {
// repository is frozen/readonly
for (ReceiveCommand cmd : commands) {
diff --git a/src/main/java/com/gitblit/models/RepositoryModel.java b/src/main/java/com/gitblit/models/RepositoryModel.java
index 88854652..40a5acd2 100644
--- a/src/main/java/com/gitblit/models/RepositoryModel.java
+++ b/src/main/java/com/gitblit/models/RepositoryModel.java
@@ -65,6 +65,7 @@ public class RepositoryModel implements Serializable, Comparable<RepositoryModel
public boolean skipSummaryMetrics;
public String frequency;
public boolean isBare;
+ public boolean isMirror;
public String origin;
public String HEAD;
public List<String> availableRefs;
diff --git a/src/main/java/com/gitblit/models/TeamModel.java b/src/main/java/com/gitblit/models/TeamModel.java
index 54f194b1..a1928283 100644
--- a/src/main/java/com/gitblit/models/TeamModel.java
+++ b/src/main/java/com/gitblit/models/TeamModel.java
@@ -206,7 +206,7 @@ public class TeamModel implements Serializable, Comparable<TeamModel> {
// determine maximum permission for the repository
final AccessPermission maxPermission =
- (repository.isFrozen || !repository.isBare) ?
+ (repository.isFrozen || !repository.isBare || repository.isMirror) ?
AccessPermission.CLONE : AccessPermission.REWIND;
if (AccessRestrictionType.NONE.equals(repository.accessRestriction)) {
diff --git a/src/main/java/com/gitblit/models/UserModel.java b/src/main/java/com/gitblit/models/UserModel.java
index b4fdb66f..446db3ab 100644
--- a/src/main/java/com/gitblit/models/UserModel.java
+++ b/src/main/java/com/gitblit/models/UserModel.java
@@ -292,7 +292,7 @@ public class UserModel implements Principal, Serializable, Comparable<UserModel>
// determine maximum permission for the repository
final AccessPermission maxPermission =
- (repository.isFrozen || !repository.isBare) ?
+ (repository.isFrozen || !repository.isBare || repository.isMirror) ?
AccessPermission.CLONE : AccessPermission.REWIND;
if (AccessRestrictionType.NONE.equals(repository.accessRestriction)) {
diff --git a/src/main/java/com/gitblit/utils/JGitUtils.java b/src/main/java/com/gitblit/utils/JGitUtils.java
index be2860de..5584fb5c 100644
--- a/src/main/java/com/gitblit/utils/JGitUtils.java
+++ b/src/main/java/com/gitblit/utils/JGitUtils.java
@@ -2096,4 +2096,54 @@ public class JGitUtils {
}
return StringUtils.decodeString(content);
}
+
+ /**
+ * Automatic repair of (some) invalid refspecs. These are the result of a
+ * bug in JGit cloning where a double forward-slash was injected. :(
+ *
+ * @param repository
+ * @return true, if the refspecs were repaired
+ */
+ public static boolean repairFetchSpecs(Repository repository) {
+ StoredConfig rc = repository.getConfig();
+
+ // auto-repair broken fetch ref specs
+ for (String name : rc.getSubsections("remote")) {
+ int invalidSpecs = 0;
+ int repairedSpecs = 0;
+ List<String> specs = new ArrayList<String>();
+ for (String spec : rc.getStringList("remote", name, "fetch")) {
+ try {
+ RefSpec rs = new RefSpec(spec);
+ // valid spec
+ specs.add(spec);
+ } catch (IllegalArgumentException e) {
+ // invalid spec
+ invalidSpecs++;
+ if (spec.contains("//")) {
+ // auto-repair this known spec bug
+ spec = spec.replace("//", "/");
+ specs.add(spec);
+ repairedSpecs++;
+ }
+ }
+ }
+
+ if (invalidSpecs == repairedSpecs && repairedSpecs > 0) {
+ // the fetch specs were automatically repaired
+ rc.setStringList("remote", name, "fetch", specs);
+ try {
+ rc.save();
+ rc.load();
+ LOGGER.debug("repaired {} invalid fetch refspecs for {}", repairedSpecs, repository.getDirectory());
+ return true;
+ } catch (Exception e) {
+ LOGGER.error(null, e);
+ }
+ } else if (invalidSpecs > 0) {
+ LOGGER.error("mirror executor found {} invalid fetch refspecs for {}", invalidSpecs, repository.getDirectory());
+ }
+ }
+ return false;
+ }
}
diff --git a/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties b/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties
index 526093ab..feaa9c6e 100644
--- a/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties
+++ b/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties
@@ -504,4 +504,7 @@ gb.noActivityToday = there has been no activity today
gb.anonymousUser= anonymous
gb.commitMessageRenderer = commit message renderer
gb.diffStat = {0} insertions & {1} deletions
-gb.home = home \ No newline at end of file
+gb.home = home
+gb.isMirror = this repository is a mirror
+gb.mirrorOf = mirror of {0}
+gb.mirrorWarning = this repository is a mirror and can not receive pushes \ No newline at end of file
diff --git a/src/main/java/com/gitblit/wicket/pages/RepositoryPage.html b/src/main/java/com/gitblit/wicket/pages/RepositoryPage.html
index 1af9127e..0acc6dbc 100644
--- a/src/main/java/com/gitblit/wicket/pages/RepositoryPage.html
+++ b/src/main/java/com/gitblit/wicket/pages/RepositoryPage.html
@@ -62,6 +62,10 @@
<wicket:fragment wicket:id="originFragment">
<p class="originRepository"><wicket:message key="gb.forkedFrom">[forked from]</wicket:message> <span wicket:id="originRepository">[origin repository]</span></p>
</wicket:fragment>
+
+ <wicket:fragment wicket:id="mirrorFragment">
+ <p class="originRepository"><span wicket:id="originRepository">[origin repository]</span></p>
+ </wicket:fragment>
</wicket:extend>
</body>
diff --git a/src/main/java/com/gitblit/wicket/pages/RepositoryPage.java b/src/main/java/com/gitblit/wicket/pages/RepositoryPage.java
index d0d801ef..70a2b9eb 100644
--- a/src/main/java/com/gitblit/wicket/pages/RepositoryPage.java
+++ b/src/main/java/com/gitblit/wicket/pages/RepositoryPage.java
@@ -260,7 +260,14 @@ public abstract class RepositoryPage extends RootPage {
// indicate origin repository
RepositoryModel model = getRepositoryModel();
if (StringUtils.isEmpty(model.originRepository)) {
- add(new Label("originRepository").setVisible(false));
+ if (model.isMirror) {
+ Fragment mirrorFrag = new Fragment("originRepository", "mirrorFragment", this);
+ Label lbl = new Label("originRepository", MessageFormat.format(getString("gb.mirrorOf"), "<b>" + model.origin + "</b>"));
+ mirrorFrag.add(lbl.setEscapeModelStrings(false));
+ add(mirrorFrag);
+ } else {
+ add(new Label("originRepository").setVisible(false));
+ }
} else {
RepositoryModel origin = GitBlit.self().getRepositoryModel(model.originRepository);
if (origin == null) {
diff --git a/src/main/java/com/gitblit/wicket/panels/ProjectRepositoryPanel.html b/src/main/java/com/gitblit/wicket/panels/ProjectRepositoryPanel.html
index 54c1e92b..d5a87d3f 100644
--- a/src/main/java/com/gitblit/wicket/panels/ProjectRepositoryPanel.html
+++ b/src/main/java/com/gitblit/wicket/panels/ProjectRepositoryPanel.html
@@ -39,6 +39,7 @@
<span wicket:id="repositoryLinks"></span>
<div>
<img class="inlineIcon" wicket:id="sparkleshareIcon" />
+ <img class="inlineIcon" wicket:id="mirrorIcon" />
<img class="inlineIcon" wicket:id="frozenIcon" />
<img class="inlineIcon" wicket:id="federatedIcon" />
diff --git a/src/main/java/com/gitblit/wicket/panels/ProjectRepositoryPanel.java b/src/main/java/com/gitblit/wicket/panels/ProjectRepositoryPanel.java
index f6c80bd4..ed5780f5 100644
--- a/src/main/java/com/gitblit/wicket/panels/ProjectRepositoryPanel.java
+++ b/src/main/java/com/gitblit/wicket/panels/ProjectRepositoryPanel.java
@@ -87,6 +87,12 @@ public class ProjectRepositoryPanel extends BasePanel {
add(WicketUtils.newClearPixel("sparkleshareIcon").setVisible(false));
}
+ if (entry.isMirror) {
+ add(WicketUtils.newImage("mirrorIcon", "mirror_16x16.png", localizer.getString("gb.isMirror", parent)));
+ } else {
+ add(WicketUtils.newClearPixel("mirrorIcon").setVisible(false));
+ }
+
if (entry.isFrozen) {
add(WicketUtils.newImage("frozenIcon", "cold_16x16.png", localizer.getString("gb.isFrozen", parent)));
} else {
diff --git a/src/main/java/com/gitblit/wicket/panels/RepositoriesPanel.html b/src/main/java/com/gitblit/wicket/panels/RepositoriesPanel.html
index e080103b..0cf3ef29 100644
--- a/src/main/java/com/gitblit/wicket/panels/RepositoriesPanel.html
+++ b/src/main/java/com/gitblit/wicket/panels/RepositoriesPanel.html
@@ -89,7 +89,7 @@
<td class="left" style="padding-left:3px;" ><b><span class="repositorySwatch" wicket:id="repositorySwatch"></span></b> <span style="padding-left:3px;" wicket:id="repositoryName">[repository name]</span></td>
<td class="hidden-phone"><span class="list" wicket:id="repositoryDescription">[repository description]</span></td>
<td class="hidden-tablet hidden-phone author"><span wicket:id="repositoryOwner">[repository owner]</span></td>
- <td class="hidden-phone" style="text-align: right;padding-right:10px;"><img class="inlineIcon" wicket:id="sparkleshareIcon" /><img class="inlineIcon" wicket:id="forkIcon" /><img class="inlineIcon" wicket:id="frozenIcon" /><img class="inlineIcon" wicket:id="federatedIcon" /><img class="inlineIcon" wicket:id="accessRestrictionIcon" /></td>
+ <td class="hidden-phone" style="text-align: right;padding-right:10px;"><img class="inlineIcon" wicket:id="sparkleshareIcon" /><img class="inlineIcon" wicket:id="mirrorIcon" /><img class="inlineIcon" wicket:id="forkIcon" /><img class="inlineIcon" wicket:id="frozenIcon" /><img class="inlineIcon" wicket:id="federatedIcon" /><img class="inlineIcon" wicket:id="accessRestrictionIcon" /></td>
<td><span wicket:id="repositoryLastChange">[last change]</span></td>
<td class="hidden-phone" style="text-align: right;padding-right:15px;"><span style="font-size:0.8em;" wicket:id="repositorySize">[repository size]</span></td>
<td class="rightAlign">
diff --git a/src/main/java/com/gitblit/wicket/panels/RepositoriesPanel.java b/src/main/java/com/gitblit/wicket/panels/RepositoriesPanel.java
index cb271504..9de387a8 100644
--- a/src/main/java/com/gitblit/wicket/panels/RepositoriesPanel.java
+++ b/src/main/java/com/gitblit/wicket/panels/RepositoriesPanel.java
@@ -243,6 +243,13 @@ public class RepositoriesPanel extends BasePanel {
row.add(WicketUtils.newClearPixel("sparkleshareIcon").setVisible(false));
}
+ if (entry.isMirror) {
+ row.add(WicketUtils.newImage("mirrorIcon", "mirror_16x16.png",
+ getString("gb.isMirror")));
+ } else {
+ row.add(WicketUtils.newClearPixel("mirrorIcon").setVisible(false));
+ }
+
if (entry.isFork()) {
row.add(WicketUtils.newImage("forkIcon", "commit_divide_16x16.png",
getString("gb.isFork")));
diff --git a/src/main/resources/mirror_16x16.png b/src/main/resources/mirror_16x16.png
new file mode 100644
index 00000000..a0c25e97
--- /dev/null
+++ b/src/main/resources/mirror_16x16.png
Binary files differ
diff --git a/src/site/features.mkd b/src/site/features.mkd
index 31ad007a..c426abf1 100644
--- a/src/site/features.mkd
+++ b/src/site/features.mkd
@@ -19,6 +19,7 @@
- Optional feature to allow users to create personal repositories
- Optional feature to fork a repository to a personal repository
- Optional feature to create a repository on push
+- Optional feature to automatically fetch ref updates for repository mirrors
- *Experimental* built-in Garbage Collection
- Ability to federate with one or more other Gitblit instances
- RSS/JSON RPC interface
diff --git a/src/test/java/com/gitblit/tests/PermissionsTest.java b/src/test/java/com/gitblit/tests/PermissionsTest.java
index 942811d5..42ef9935 100644
--- a/src/test/java/com/gitblit/tests/PermissionsTest.java
+++ b/src/test/java/com/gitblit/tests/PermissionsTest.java
@@ -2878,4 +2878,22 @@ public class PermissionsTest extends Assert {
assertEquals("user has wrong permission!", AccessPermission.CLONE, user.getRepositoryPermission(repo).permission);
assertEquals("team has wrong permission!", AccessPermission.CLONE, team.getRepositoryPermission(repo).permission);
}
+
+ @Test
+ public void testIsMirror() throws Exception {
+ RepositoryModel repo = new RepositoryModel("somerepo.git", null, null, new Date());
+ repo.authorizationControl = AuthorizationControl.NAMED;
+ repo.accessRestriction = AccessRestrictionType.NONE;
+
+ UserModel user = new UserModel("test");
+ TeamModel team = new TeamModel("team");
+
+ assertEquals("user has wrong permission!", AccessPermission.REWIND, user.getRepositoryPermission(repo).permission);
+ assertEquals("team has wrong permission!", AccessPermission.REWIND, team.getRepositoryPermission(repo).permission);
+
+ // set repo to be a mirror, pushes prohibited
+ repo.isMirror = true;
+ assertEquals("user has wrong permission!", AccessPermission.CLONE, user.getRepositoryPermission(repo).permission);
+ assertEquals("team has wrong permission!", AccessPermission.CLONE, team.getRepositoryPermission(repo).permission);
+ }
}