<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
- 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)
- 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)
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' }
# 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
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>());
private GCExecutor gcExecutor;
+ private MirrorExecutor mirrorExecutor;
+
private TimeZone timezone;
private FileBasedConfig projectConfigs;
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")));
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, "~");
configureMailExecutor();
configureLuceneIndexing();
configureGarbageCollector();
+ configureMirrorExecutor();
if (startFederation) {
configureFederation();
}
}
}
+ 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();
scheduledExecutor.shutdownNow();
luceneExecutor.close();
gcExecutor.close();
+ mirrorExecutor.close();
if (fanoutService != null) {
fanoutService.stop();
}
--- /dev/null
+/*\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
\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
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
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
@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
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
\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
\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
}\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
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
<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
// 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
<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
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
<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
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
- 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
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);
+ }
}