]> source.dussan.org Git - gitblit.git/commitdiff
Implement mirror executor (issue-5)
authorJames Moger <james.moger@gitblit.com>
Wed, 13 Nov 2013 22:56:50 +0000 (17:56 -0500)
committerJames Moger <james.moger@gitblit.com>
Wed, 13 Nov 2013 22:56:50 +0000 (17:56 -0500)
The mirror executor will fetch ref updates for repository mirrors.  This
feature is disabled by default and can be enabled by setting
git.enableMirroring=true.  The period between update checks is
configurable, but it is global.  An individual rpeository may not set
it's own update schedule.

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.

Change-Id: I4bbe3fb2df106366ae4c2313596d0fab0dfcac46

21 files changed:
build.xml
releases.moxie
src/main/distrib/data/gitblit.properties
src/main/java/com/gitblit/GitBlit.java
src/main/java/com/gitblit/MirrorExecutor.java [new file with mode: 0644]
src/main/java/com/gitblit/client/IndicatorsRenderer.java
src/main/java/com/gitblit/git/GitblitReceivePack.java
src/main/java/com/gitblit/models/RepositoryModel.java
src/main/java/com/gitblit/models/TeamModel.java
src/main/java/com/gitblit/models/UserModel.java
src/main/java/com/gitblit/utils/JGitUtils.java
src/main/java/com/gitblit/wicket/GitBlitWebApp.properties
src/main/java/com/gitblit/wicket/pages/RepositoryPage.html
src/main/java/com/gitblit/wicket/pages/RepositoryPage.java
src/main/java/com/gitblit/wicket/panels/ProjectRepositoryPanel.html
src/main/java/com/gitblit/wicket/panels/ProjectRepositoryPanel.java
src/main/java/com/gitblit/wicket/panels/RepositoriesPanel.html
src/main/java/com/gitblit/wicket/panels/RepositoriesPanel.java
src/main/resources/mirror_16x16.png [new file with mode: 0644]
src/site/features.mkd
src/test/java/com/gitblit/tests/PermissionsTest.java

index 57b7ca2471c4aa9f4451455ed14f74fa919baee1..e4af1ef24c63e1c9402372f533a540efe7614de8 100644 (file)
--- a/build.xml
+++ b/build.xml
                        <resource file="${project.resources.dir}/commit_merge_16x16.png" />\r
                        <resource file="${project.resources.dir}/commit_divide_16x16.png" />\r
                        <resource file="${project.resources.dir}/star_16x16.png" />\r
+                       <resource file="${project.resources.dir}/mirror_16x16.png" />\r
                        <resource file="${project.resources.dir}/blank.png" />\r
                        <resource file="${project.src.dir}/log4j.properties" />\r
                        <resource>\r
index 1832284f98b4ca201824324837e4b34f0ada9d83..a3b5ee4b974f70cd2057b2f151c6719e0438ef17 100644 (file)
@@ -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' }
index a791c1fd7bae8a7abfd442e1e2b8742a5ed33f16..2823c4de2ea0172163080525c11ee411a90fed97 100644 (file)
@@ -276,6 +276,34 @@ git.defaultGarbageCollectionThreshold = 500k
 # SINCE 1.2.0\r
 git.defaultGarbageCollectionPeriod = 7\r
 \r
+# Gitblit can automatically fetch ref updates for a properly configured mirror\r
+# repository.\r
+#\r
+# Requirements:\r
+# 1. you must manually clone the repository using native git\r
+#    git clone --mirror git://somewhere.com/myrepo.git\r
+# 2. the "origin" remote must be the mirror source\r
+# 3. the "origin" repository must be accessible without authentication OR the\r
+#    credentials must be embedded in the origin url (not recommended)\r
+#\r
+# Notes:\r
+# 1. "origin" SSH urls are untested and not likely to work\r
+# 2. mirrors cloned while Gitblit is running are likely to require clearing the\r
+#    gitblit cache (link on the repositories page of an administrator account)\r
+# 3. Gitblit will automatically repair any invalid fetch refspecs with a "//"\r
+#    sequence.\r
+#\r
+# SINCE 1.4.0\r
+# RESTART REQUIRED\r
+git.enableMirroring = false\r
+\r
+# Specify the period between update checks for mirrored repositories.\r
+# The shortest period you may specify between mirror update checks is 5 mins.\r
+#\r
+# SINCE 1.4.0\r
+# RESTART REQUIRED\r
+git.mirrorPeriod = 30 mins\r
+\r
 # Number of bytes of a pack file to load into memory in a single read operation.\r
 # This is the "page size" of the JGit buffer cache, used for all pack access\r
 # operations. All disk IO occurs as single window reads. Setting this too large\r
index f313b6e3ccb3bafb6219d761d1c2662ccf655caf..a0e8b0a3236c7b3d00fe139e80bbe42b65cb326b 100644 (file)
@@ -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 (file)
index 0000000..21c194f
--- /dev/null
@@ -0,0 +1,169 @@
+/*\r
+ * Copyright 2013 gitblit.com.\r
+ *\r
+ * Licensed under the Apache License, Version 2.0 (the "License");\r
+ * you may not use this file except in compliance with the License.\r
+ * You may obtain a copy of the License at\r
+ *\r
+ *     http://www.apache.org/licenses/LICENSE-2.0\r
+ *\r
+ * Unless required by applicable law or agreed to in writing, software\r
+ * distributed under the License is distributed on an "AS IS" BASIS,\r
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ * See the License for the specific language governing permissions and\r
+ * limitations under the License.\r
+ */\r
+package com.gitblit;\r
+\r
+import java.text.MessageFormat;\r
+import java.util.Collection;\r
+import java.util.Collections;\r
+import java.util.HashSet;\r
+import java.util.List;\r
+import java.util.Set;\r
+import java.util.concurrent.atomic.AtomicBoolean;\r
+\r
+import org.eclipse.jgit.api.Git;\r
+import org.eclipse.jgit.lib.RefUpdate.Result;\r
+import org.eclipse.jgit.lib.Repository;\r
+import org.eclipse.jgit.lib.StoredConfig;\r
+import org.eclipse.jgit.transport.FetchResult;\r
+import org.eclipse.jgit.transport.RemoteConfig;\r
+import org.eclipse.jgit.transport.TrackingRefUpdate;\r
+import org.slf4j.Logger;\r
+import org.slf4j.LoggerFactory;\r
+\r
+import com.gitblit.models.RepositoryModel;\r
+import com.gitblit.models.UserModel;\r
+import com.gitblit.utils.JGitUtils;\r
+\r
+/**\r
+ * The Mirror executor handles periodic fetching of mirrored repositories.\r
+ *\r
+ * @author James Moger\r
+ *\r
+ */\r
+public class MirrorExecutor implements Runnable {\r
+\r
+       private final Logger logger = LoggerFactory.getLogger(MirrorExecutor.class);\r
+\r
+       private final Set<String> repairAttempted = Collections.synchronizedSet(new HashSet<String>());\r
+\r
+       private final IStoredSettings settings;\r
+\r
+       private AtomicBoolean running = new AtomicBoolean(false);\r
+\r
+       private AtomicBoolean forceClose = new AtomicBoolean(false);\r
+\r
+       private final UserModel gitblitUser;\r
+\r
+       public MirrorExecutor(IStoredSettings settings) {\r
+               this.settings = settings;\r
+               this.gitblitUser = new UserModel("gitblit");\r
+               this.gitblitUser.displayName = "Gitblit";\r
+       }\r
+\r
+       public boolean isReady() {\r
+               return settings.getBoolean(Keys.git.enableMirroring, false);\r
+       }\r
+\r
+       public boolean isRunning() {\r
+               return running.get();\r
+       }\r
+\r
+       public void close() {\r
+               forceClose.set(true);\r
+       }\r
+\r
+       @Override\r
+       public void run() {\r
+               if (!isReady()) {\r
+                       return;\r
+               }\r
+\r
+               running.set(true);\r
+\r
+               for (String repositoryName : GitBlit.self().getRepositoryList()) {\r
+                       if (forceClose.get()) {\r
+                               break;\r
+                       }\r
+                       if (GitBlit.self().isCollectingGarbage(repositoryName)) {\r
+                               logger.debug("mirror is skipping {} garbagecollection", repositoryName);\r
+                               continue;\r
+                       }\r
+                       RepositoryModel model = null;\r
+                       Repository repository = null;\r
+                       try {\r
+                               model = GitBlit.self().getRepositoryModel(repositoryName);\r
+                               if (!model.isMirror && !model.isBare) {\r
+                                       // repository must be a valid bare git mirror\r
+                                       logger.debug("mirror is skipping {} !mirror !bare", repositoryName);\r
+                                       continue;\r
+                               }\r
+\r
+                               repository = GitBlit.self().getRepository(repositoryName);\r
+                               if (repository == null) {\r
+                                       logger.warn(MessageFormat.format("MirrorExecutor is missing repository {0}?!?", repositoryName));\r
+                                       continue;\r
+                               }\r
+\r
+                               // automatically repair (some) invalid fetch ref specs\r
+                               if (!repairAttempted.contains(repositoryName)) {\r
+                                       repairAttempted.add(repositoryName);\r
+                                       JGitUtils.repairFetchSpecs(repository);\r
+                               }\r
+\r
+                               // find the first mirror remote - there should only be one\r
+                               StoredConfig rc = repository.getConfig();\r
+                               RemoteConfig mirror = null;\r
+                               List<RemoteConfig> configs = RemoteConfig.getAllRemoteConfigs(rc);\r
+                               for (RemoteConfig config : configs) {\r
+                                       if (config.isMirror()) {\r
+                                               mirror = config;\r
+                                               break;\r
+                                       }\r
+                               }\r
+\r
+                               if (mirror == null) {\r
+                                       // repository does not have a mirror remote\r
+                                       logger.debug("mirror is skipping {} no mirror remote found", repositoryName);\r
+                                       continue;\r
+                               }\r
+\r
+                               logger.debug("checking {} remote {} for ref updates", repositoryName, mirror.getName());\r
+                               final boolean testing = false;\r
+                               Git git = new Git(repository);\r
+                               FetchResult result = git.fetch().setRemote(mirror.getName()).setDryRun(testing).call();\r
+                               Collection<TrackingRefUpdate> refUpdates = result.getTrackingRefUpdates();\r
+                               if (refUpdates.size() > 0) {\r
+                                       for (TrackingRefUpdate ru : refUpdates) {\r
+                                               StringBuilder sb = new StringBuilder();\r
+                                               sb.append("updated mirror ");\r
+                                               sb.append(repositoryName);\r
+                                               sb.append(" ");\r
+                                               sb.append(ru.getRemoteName());\r
+                                               sb.append(" -> ");\r
+                                               sb.append(ru.getLocalName());\r
+                                               if (ru.getResult() == Result.FORCED) {\r
+                                                       sb.append(" (forced)");\r
+                                               }\r
+                                               sb.append(" ");\r
+                                               sb.append(ru.getOldObjectId() == null ? "" : ru.getOldObjectId().abbreviate(7).name());\r
+                                               sb.append("..");\r
+                                               sb.append(ru.getNewObjectId() == null ? "" : ru.getNewObjectId().abbreviate(7).name());\r
+                                               logger.info(sb.toString());\r
+                                       }\r
+                               }\r
+                       } catch (Exception e) {\r
+                               logger.error("Error updating mirror " + repositoryName, e);\r
+                       } finally {\r
+                               // cleanup\r
+                               if (repository != null) {\r
+                                       repository.close();\r
+                               }\r
+                       }\r
+               }\r
+\r
+               running.set(false);\r
+       }\r
+}\r
index 5b61df6623804d50104586b03a38007bbb0b9c3b..5883ab04545750d4fed1133eaf0e41a85d91cefb 100644 (file)
@@ -56,6 +56,8 @@ public class IndicatorsRenderer extends JPanel implements TableCellRenderer, Ser
 \r
        private final ImageIcon sparkleshareIcon;\r
 \r
+       private final ImageIcon mirrorIcon;\r
+\r
        public IndicatorsRenderer() {\r
                super(new FlowLayout(FlowLayout.RIGHT, 1, 0));\r
                blankIcon = new ImageIcon(getClass().getResource("/blank.png"));\r
@@ -67,6 +69,7 @@ public class IndicatorsRenderer extends JPanel implements TableCellRenderer, Ser
                federatedIcon = new ImageIcon(getClass().getResource("/federated_16x16.png"));\r
                forkIcon = new ImageIcon(getClass().getResource("/commit_divide_16x16.png"));\r
                sparkleshareIcon = new ImageIcon(getClass().getResource("/star_16x16.png"));\r
+               mirrorIcon = new ImageIcon(getClass().getResource("/mirror_16x16.png"));\r
        }\r
 \r
        @Override\r
@@ -85,6 +88,11 @@ public class IndicatorsRenderer extends JPanel implements TableCellRenderer, Ser
                                tooltip.append(Translation.get("gb.isSparkleshared")).append("<br/>");\r
                                add(icon);\r
                        }\r
+                       if (model.isMirror) {\r
+                               JLabel icon = new JLabel(mirrorIcon);\r
+                               tooltip.append(Translation.get("gb.isMirror")).append("<br/>");\r
+                               add(icon);\r
+                       }\r
                        if (model.isFork()) {\r
                                JLabel icon = new JLabel(forkIcon);\r
                                tooltip.append(Translation.get("gb.isFork")).append("<br/>");\r
index e6ff5721c0051e66ef3acf373cfa5ae8a1ab22b4..ba200b25768e02b25799a317c763c24451e2ba16 100644 (file)
@@ -120,6 +120,14 @@ public class GitblitReceivePack extends ReceivePack implements PreReceiveHook, P
        @Override\r
        public void onPreReceive(ReceivePack rp, Collection<ReceiveCommand> commands) {\r
 \r
+               if (repository.isMirror) {\r
+                       // repository is a mirror\r
+                       for (ReceiveCommand cmd : commands) {\r
+                               sendRejection(cmd, "Gitblit does not allow pushes to \"{0}\" because it is a mirror!", repository.name);\r
+                       }\r
+                       return;\r
+               }\r
+\r
                if (repository.isFrozen) {\r
                        // repository is frozen/readonly\r
                        for (ReceiveCommand cmd : commands) {\r
index 888546523d3c1ef68bf794fa194ddbf62a0ec0e6..40a5acd244743b1ab95d3d32d8ffb07f21fea006 100644 (file)
@@ -65,6 +65,7 @@ public class RepositoryModel implements Serializable, Comparable<RepositoryModel
        public boolean skipSummaryMetrics;\r
        public String frequency;\r
        public boolean isBare;\r
+       public boolean isMirror;\r
        public String origin;\r
        public String HEAD;\r
        public List<String> availableRefs;\r
index 54f194b11359fc3a071478dfbb4698529e7c487d..a1928283db76a3a0eadb14938435deaf034437d7 100644 (file)
@@ -206,7 +206,7 @@ public class TeamModel implements Serializable, Comparable<TeamModel> {
 \r
                // determine maximum permission for the repository\r
                final AccessPermission maxPermission =\r
-                               (repository.isFrozen || !repository.isBare) ?\r
+                               (repository.isFrozen || !repository.isBare || repository.isMirror) ?\r
                                                AccessPermission.CLONE : AccessPermission.REWIND;\r
 \r
                if (AccessRestrictionType.NONE.equals(repository.accessRestriction)) {\r
index b4fdb66f5eb015f4e68cdf8031c2f1e93dbf82ab..446db3ab2d2cc0a3436a9202a1e13ecf1471c42b 100644 (file)
@@ -292,7 +292,7 @@ public class UserModel implements Principal, Serializable, Comparable<UserModel>
 \r
                // determine maximum permission for the repository\r
                final AccessPermission maxPermission =\r
-                               (repository.isFrozen || !repository.isBare) ?\r
+                               (repository.isFrozen || !repository.isBare || repository.isMirror) ?\r
                                                AccessPermission.CLONE : AccessPermission.REWIND;\r
 \r
                if (AccessRestrictionType.NONE.equals(repository.accessRestriction)) {\r
index be2860de9f764ceaddcb11b1e8eef4ac2574fdbf..5584fb5c651676e406f1d05278975182df85523e 100644 (file)
@@ -2096,4 +2096,54 @@ public class JGitUtils {
                }\r
                return StringUtils.decodeString(content);\r
        }\r
+\r
+       /**\r
+        * Automatic repair of (some) invalid refspecs.  These are the result of a\r
+        * bug in JGit cloning where a double forward-slash was injected.  :(\r
+        *\r
+        * @param repository\r
+        * @return true, if the refspecs were repaired\r
+        */\r
+       public static boolean repairFetchSpecs(Repository repository) {\r
+               StoredConfig rc = repository.getConfig();\r
+\r
+               // auto-repair broken fetch ref specs\r
+               for (String name : rc.getSubsections("remote")) {\r
+                       int invalidSpecs = 0;\r
+                       int repairedSpecs = 0;\r
+                       List<String> specs = new ArrayList<String>();\r
+                       for (String spec : rc.getStringList("remote", name, "fetch")) {\r
+                               try {\r
+                                       RefSpec rs = new RefSpec(spec);\r
+                                       // valid spec\r
+                                       specs.add(spec);\r
+                               } catch (IllegalArgumentException e) {\r
+                                       // invalid spec\r
+                                       invalidSpecs++;\r
+                                       if (spec.contains("//")) {\r
+                                               // auto-repair this known spec bug\r
+                                               spec = spec.replace("//", "/");\r
+                                               specs.add(spec);\r
+                                               repairedSpecs++;\r
+                                       }\r
+                               }\r
+                       }\r
+\r
+                       if (invalidSpecs == repairedSpecs && repairedSpecs > 0) {\r
+                               // the fetch specs were automatically repaired\r
+                               rc.setStringList("remote", name, "fetch", specs);\r
+                               try {\r
+                                       rc.save();\r
+                                       rc.load();\r
+                                       LOGGER.debug("repaired {} invalid fetch refspecs for {}", repairedSpecs, repository.getDirectory());\r
+                                       return true;\r
+                               } catch (Exception e) {\r
+                                       LOGGER.error(null, e);\r
+                               }\r
+                       } else if (invalidSpecs > 0) {\r
+                               LOGGER.error("mirror executor found {} invalid fetch refspecs for {}", invalidSpecs, repository.getDirectory());\r
+                       }\r
+               }\r
+               return false;\r
+       }\r
 }\r
index 526093ab99651ee52c8500210da007b145781629..feaa9c6e7b3ab16995c58c6fdb173e921234f6bc 100644 (file)
@@ -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
index 1af9127ed87eaeb035f842e279043f09aa880346..0acc6dbc1b0b3dd1b22c0affbe2736ee903700ff 100644 (file)
                <wicket:fragment wicket:id="originFragment">\r
                        <p class="originRepository"><wicket:message key="gb.forkedFrom">[forked from]</wicket:message> <span wicket:id="originRepository">[origin repository]</span></p>\r
                </wicket:fragment>\r
+\r
+               <wicket:fragment wicket:id="mirrorFragment">\r
+                       <p class="originRepository"><span wicket:id="originRepository">[origin repository]</span></p>\r
+               </wicket:fragment>\r
                                \r
        </wicket:extend>\r
 </body>\r
index d0d801efe795e04dbf0fb7bbdc9d240e0f36bcd0..70a2b9eb5de4742cb7f147cbe87d230e8c54df75 100644 (file)
@@ -260,7 +260,14 @@ public abstract class RepositoryPage extends RootPage {
                // indicate origin repository\r
                RepositoryModel model = getRepositoryModel();\r
                if (StringUtils.isEmpty(model.originRepository)) {\r
-                       add(new Label("originRepository").setVisible(false));\r
+                       if (model.isMirror) {\r
+                               Fragment mirrorFrag = new Fragment("originRepository", "mirrorFragment", this);\r
+                               Label lbl = new Label("originRepository", MessageFormat.format(getString("gb.mirrorOf"), "<b>" + model.origin + "</b>"));\r
+                               mirrorFrag.add(lbl.setEscapeModelStrings(false));\r
+                               add(mirrorFrag);\r
+                       } else {\r
+                               add(new Label("originRepository").setVisible(false));\r
+                       }\r
                } else {\r
                        RepositoryModel origin = GitBlit.self().getRepositoryModel(model.originRepository);\r
                        if (origin == null) {\r
index 54c1e92b669d2c7d72db7a584057b577eb42f08a..d5a87d3ff26eb3c270d4e85e1c787a688382d940 100644 (file)
@@ -39,6 +39,7 @@
                                <span wicket:id="repositoryLinks"></span>\r
                                <div>\r
                                        <img class="inlineIcon" wicket:id="sparkleshareIcon" />\r
+                                       <img class="inlineIcon" wicket:id="mirrorIcon" />\r
                                        <img class="inlineIcon" wicket:id="frozenIcon" />\r
                                        <img class="inlineIcon" wicket:id="federatedIcon" />\r
                                                        \r
index f6c80bd4851fba0bd966dd37cdb7414bc6b03daa..ed5780f59851f5f012e128a397a4bcaabcd222dd 100644 (file)
@@ -87,6 +87,12 @@ public class ProjectRepositoryPanel extends BasePanel {
                        add(WicketUtils.newClearPixel("sparkleshareIcon").setVisible(false));\r
                }\r
 \r
+               if (entry.isMirror) {\r
+                       add(WicketUtils.newImage("mirrorIcon", "mirror_16x16.png", localizer.getString("gb.isMirror", parent)));\r
+               } else {\r
+                       add(WicketUtils.newClearPixel("mirrorIcon").setVisible(false));\r
+               }\r
+\r
                if (entry.isFrozen) {\r
                        add(WicketUtils.newImage("frozenIcon", "cold_16x16.png", localizer.getString("gb.isFrozen", parent)));\r
                } else {\r
index e080103b7aa3895c51aac46541bd0ec329540140..0cf3ef29397963bd950aa97b6cc7d7e9fc735b07 100644 (file)
@@ -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>\r
         <td class="hidden-phone"><span class="list" wicket:id="repositoryDescription">[repository description]</span></td>\r
         <td class="hidden-tablet hidden-phone author"><span wicket:id="repositoryOwner">[repository owner]</span></td>\r
-        <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>\r
+        <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>\r
         <td><span wicket:id="repositoryLastChange">[last change]</span></td>\r
         <td class="hidden-phone" style="text-align: right;padding-right:15px;"><span style="font-size:0.8em;" wicket:id="repositorySize">[repository size]</span></td>\r
         <td class="rightAlign">\r
index cb271504ed80c58906b379fab481132ba3a7d76f..9de387a8222028897851c174a51accc94734191f 100644 (file)
@@ -243,6 +243,13 @@ public class RepositoriesPanel extends BasePanel {
                                        row.add(WicketUtils.newClearPixel("sparkleshareIcon").setVisible(false));\r
                                }\r
 \r
+                               if (entry.isMirror) {\r
+                                       row.add(WicketUtils.newImage("mirrorIcon", "mirror_16x16.png",\r
+                                                       getString("gb.isMirror")));\r
+                               } else {\r
+                                       row.add(WicketUtils.newClearPixel("mirrorIcon").setVisible(false));\r
+                               }\r
+\r
                                if (entry.isFork()) {\r
                                        row.add(WicketUtils.newImage("forkIcon", "commit_divide_16x16.png",\r
                                                        getString("gb.isFork")));\r
diff --git a/src/main/resources/mirror_16x16.png b/src/main/resources/mirror_16x16.png
new file mode 100644 (file)
index 0000000..a0c25e9
Binary files /dev/null and b/src/main/resources/mirror_16x16.png differ
index 31ad007a94789e3b8614d515ff0dff89d909d23f..c426abf184bb8cfc4e1710ae5c7bc41057eebdcb 100644 (file)
@@ -19,6 +19,7 @@
 - Optional feature to allow users to create personal repositories\r
 - Optional feature to fork a repository to a personal repository\r
 - Optional feature to create a repository on push\r
+- Optional feature to automatically fetch ref updates for repository mirrors\r
 - *Experimental* built-in Garbage Collection\r
 - Ability to federate with one or more other Gitblit instances\r
 - RSS/JSON RPC interface\r
index 942811d520fe16d749669bd7c50d1c7490d9b870..42ef9935b2ce1d32ae50f0526a962c540563ba6f 100644 (file)
@@ -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);
+       }
 }