You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

FederationPullExecutor.java 20KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523
  1. /*
  2. * Copyright 2011 gitblit.com.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. package com.gitblit;
  17. import static org.eclipse.jgit.lib.Constants.DOT_GIT_EXT;
  18. import java.io.File;
  19. import java.io.FileOutputStream;
  20. import java.io.IOException;
  21. import java.net.InetAddress;
  22. import java.text.MessageFormat;
  23. import java.util.ArrayList;
  24. import java.util.Arrays;
  25. import java.util.Collection;
  26. import java.util.Date;
  27. import java.util.HashMap;
  28. import java.util.HashSet;
  29. import java.util.List;
  30. import java.util.Map;
  31. import java.util.Properties;
  32. import java.util.Set;
  33. import java.util.concurrent.TimeUnit;
  34. import org.eclipse.jgit.lib.Repository;
  35. import org.eclipse.jgit.lib.StoredConfig;
  36. import org.eclipse.jgit.revwalk.RevCommit;
  37. import org.eclipse.jgit.transport.CredentialsProvider;
  38. import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
  39. import org.slf4j.Logger;
  40. import org.slf4j.LoggerFactory;
  41. import com.gitblit.Constants.AccessPermission;
  42. import com.gitblit.Constants.FederationPullStatus;
  43. import com.gitblit.Constants.FederationStrategy;
  44. import com.gitblit.GitBlitException.ForbiddenException;
  45. import com.gitblit.models.FederationModel;
  46. import com.gitblit.models.RefModel;
  47. import com.gitblit.models.RepositoryModel;
  48. import com.gitblit.models.TeamModel;
  49. import com.gitblit.models.UserModel;
  50. import com.gitblit.utils.ArrayUtils;
  51. import com.gitblit.utils.FederationUtils;
  52. import com.gitblit.utils.FileUtils;
  53. import com.gitblit.utils.JGitUtils;
  54. import com.gitblit.utils.JGitUtils.CloneResult;
  55. import com.gitblit.utils.StringUtils;
  56. import com.gitblit.utils.TimeUtils;
  57. /**
  58. * FederationPullExecutor pulls repository updates and, optionally, user
  59. * accounts and server settings from registered Gitblit instances.
  60. */
  61. public class FederationPullExecutor implements Runnable {
  62. private final Logger logger = LoggerFactory.getLogger(FederationPullExecutor.class);
  63. private final List<FederationModel> registrations;
  64. private final boolean isDaemon;
  65. /**
  66. * Constructor for specifying a single federation registration. This
  67. * constructor is used to schedule the next pull execution.
  68. *
  69. * @param registration
  70. */
  71. private FederationPullExecutor(FederationModel registration) {
  72. this(Arrays.asList(registration), true);
  73. }
  74. /**
  75. * Constructor to specify a group of federation registrations. This is
  76. * normally used at startup to pull and then schedule the next update based
  77. * on each registrations frequency setting.
  78. *
  79. * @param registrations
  80. * @param isDaemon
  81. * if true, registrations are rescheduled in perpetuity. if
  82. * false, the federation pull operation is executed once.
  83. */
  84. public FederationPullExecutor(List<FederationModel> registrations, boolean isDaemon) {
  85. this.registrations = registrations;
  86. this.isDaemon = isDaemon;
  87. }
  88. /**
  89. * Run method for this pull executor.
  90. */
  91. @Override
  92. public void run() {
  93. for (FederationModel registration : registrations) {
  94. FederationPullStatus was = registration.getLowestStatus();
  95. try {
  96. Date now = new Date(System.currentTimeMillis());
  97. pull(registration);
  98. sendStatusAcknowledgment(registration);
  99. registration.lastPull = now;
  100. FederationPullStatus is = registration.getLowestStatus();
  101. if (is.ordinal() < was.ordinal()) {
  102. // the status for this registration has downgraded
  103. logger.warn("Federation pull status of {0} is now {1}", registration.name,
  104. is.name());
  105. if (registration.notifyOnError) {
  106. String message = "Federation pull of " + registration.name + " @ "
  107. + registration.url + " is now at " + is.name();
  108. GitBlit.self()
  109. .sendMailToAdministrators(
  110. "Pull Status of " + registration.name + " is " + is.name(),
  111. message);
  112. }
  113. }
  114. } catch (Throwable t) {
  115. logger.error(MessageFormat.format(
  116. "Failed to pull from federated gitblit ({0} @ {1})", registration.name,
  117. registration.url), t);
  118. } finally {
  119. if (isDaemon) {
  120. schedule(registration);
  121. }
  122. }
  123. }
  124. }
  125. /**
  126. * Mirrors a repository and, optionally, the server's users, and/or
  127. * configuration settings from a origin Gitblit instance.
  128. *
  129. * @param registration
  130. * @throws Exception
  131. */
  132. private void pull(FederationModel registration) throws Exception {
  133. Map<String, RepositoryModel> repositories = FederationUtils.getRepositories(registration,
  134. true);
  135. String registrationFolder = registration.folder.toLowerCase().trim();
  136. // confirm valid characters in server alias
  137. Character c = StringUtils.findInvalidCharacter(registrationFolder);
  138. if (c != null) {
  139. logger.error(MessageFormat
  140. .format("Illegal character ''{0}'' in folder name ''{1}'' of federation registration {2}!",
  141. c, registrationFolder, registration.name));
  142. return;
  143. }
  144. File repositoriesFolder = new File(GitBlit.getString(Keys.git.repositoriesFolder, "git"));
  145. File registrationFolderFile = new File(repositoriesFolder, registrationFolder);
  146. registrationFolderFile.mkdirs();
  147. // Clone/Pull the repository
  148. for (Map.Entry<String, RepositoryModel> entry : repositories.entrySet()) {
  149. String cloneUrl = entry.getKey();
  150. RepositoryModel repository = entry.getValue();
  151. if (!repository.hasCommits) {
  152. logger.warn(MessageFormat.format(
  153. "Skipping federated repository {0} from {1} @ {2}. Repository is EMPTY.",
  154. repository.name, registration.name, registration.url));
  155. registration.updateStatus(repository, FederationPullStatus.SKIPPED);
  156. continue;
  157. }
  158. // Determine local repository name
  159. String repositoryName;
  160. if (StringUtils.isEmpty(registrationFolder)) {
  161. repositoryName = repository.name;
  162. } else {
  163. repositoryName = registrationFolder + "/" + repository.name;
  164. }
  165. if (registration.bare) {
  166. // bare repository, ensure .git suffix
  167. if (!repositoryName.toLowerCase().endsWith(DOT_GIT_EXT)) {
  168. repositoryName += DOT_GIT_EXT;
  169. }
  170. } else {
  171. // normal repository, strip .git suffix
  172. if (repositoryName.toLowerCase().endsWith(DOT_GIT_EXT)) {
  173. repositoryName = repositoryName.substring(0,
  174. repositoryName.indexOf(DOT_GIT_EXT));
  175. }
  176. }
  177. // confirm that the origin of any pre-existing repository matches
  178. // the clone url
  179. String fetchHead = null;
  180. Repository existingRepository = GitBlit.self().getRepository(repositoryName);
  181. if (existingRepository == null && GitBlit.self().isCollectingGarbage(repositoryName)) {
  182. logger.warn(MessageFormat.format("Skipping local repository {0}, busy collecting garbage", repositoryName));
  183. continue;
  184. }
  185. if (existingRepository != null) {
  186. StoredConfig config = existingRepository.getConfig();
  187. config.load();
  188. String origin = config.getString("remote", "origin", "url");
  189. RevCommit commit = JGitUtils.getCommit(existingRepository,
  190. org.eclipse.jgit.lib.Constants.FETCH_HEAD);
  191. if (commit != null) {
  192. fetchHead = commit.getName();
  193. }
  194. existingRepository.close();
  195. if (!origin.startsWith(registration.url)) {
  196. logger.warn(MessageFormat
  197. .format("Skipping federated repository {0} from {1} @ {2}. Origin does not match, consider EXCLUDING.",
  198. repository.name, registration.name, registration.url));
  199. registration.updateStatus(repository, FederationPullStatus.SKIPPED);
  200. continue;
  201. }
  202. }
  203. // clone/pull this repository
  204. CredentialsProvider credentials = new UsernamePasswordCredentialsProvider(
  205. Constants.FEDERATION_USER, registration.token);
  206. logger.info(MessageFormat.format("Pulling federated repository {0} from {1} @ {2}",
  207. repository.name, registration.name, registration.url));
  208. CloneResult result = JGitUtils.cloneRepository(registrationFolderFile, repository.name,
  209. cloneUrl, registration.bare, credentials);
  210. Repository r = GitBlit.self().getRepository(repositoryName);
  211. RepositoryModel rm = GitBlit.self().getRepositoryModel(repositoryName);
  212. repository.isFrozen = registration.mirror;
  213. if (result.createdRepository) {
  214. // default local settings
  215. repository.federationStrategy = FederationStrategy.EXCLUDE;
  216. repository.isFrozen = registration.mirror;
  217. repository.showRemoteBranches = !registration.mirror;
  218. logger.info(MessageFormat.format(" cloning {0}", repository.name));
  219. registration.updateStatus(repository, FederationPullStatus.MIRRORED);
  220. } else {
  221. // fetch and update
  222. boolean fetched = false;
  223. RevCommit commit = JGitUtils.getCommit(r, org.eclipse.jgit.lib.Constants.FETCH_HEAD);
  224. String newFetchHead = commit.getName();
  225. fetched = fetchHead == null || !fetchHead.equals(newFetchHead);
  226. if (registration.mirror) {
  227. // mirror
  228. if (fetched) {
  229. // update local branches to match the remote tracking branches
  230. for (RefModel ref : JGitUtils.getRemoteBranches(r, false, -1)) {
  231. if (ref.displayName.startsWith("origin/")) {
  232. String branch = org.eclipse.jgit.lib.Constants.R_HEADS
  233. + ref.displayName.substring(ref.displayName.indexOf('/') + 1);
  234. String hash = ref.getReferencedObjectId().getName();
  235. JGitUtils.setBranchRef(r, branch, hash);
  236. logger.info(MessageFormat.format(" resetting {0} of {1} to {2}", branch,
  237. repository.name, hash));
  238. }
  239. }
  240. String newHead;
  241. if (StringUtils.isEmpty(repository.HEAD)) {
  242. newHead = newFetchHead;
  243. } else {
  244. newHead = repository.HEAD;
  245. }
  246. JGitUtils.setHEADtoRef(r, newHead);
  247. logger.info(MessageFormat.format(" resetting HEAD of {0} to {1}",
  248. repository.name, newHead));
  249. registration.updateStatus(repository, FederationPullStatus.MIRRORED);
  250. } else {
  251. // indicate no commits pulled
  252. registration.updateStatus(repository, FederationPullStatus.NOCHANGE);
  253. }
  254. } else {
  255. // non-mirror
  256. if (fetched) {
  257. // indicate commits pulled to origin/master
  258. registration.updateStatus(repository, FederationPullStatus.PULLED);
  259. } else {
  260. // indicate no commits pulled
  261. registration.updateStatus(repository, FederationPullStatus.NOCHANGE);
  262. }
  263. }
  264. // preserve local settings
  265. repository.isFrozen = rm.isFrozen;
  266. repository.federationStrategy = rm.federationStrategy;
  267. // merge federation sets
  268. Set<String> federationSets = new HashSet<String>();
  269. if (rm.federationSets != null) {
  270. federationSets.addAll(rm.federationSets);
  271. }
  272. if (repository.federationSets != null) {
  273. federationSets.addAll(repository.federationSets);
  274. }
  275. repository.federationSets = new ArrayList<String>(federationSets);
  276. // merge indexed branches
  277. Set<String> indexedBranches = new HashSet<String>();
  278. if (rm.indexedBranches != null) {
  279. indexedBranches.addAll(rm.indexedBranches);
  280. }
  281. if (repository.indexedBranches != null) {
  282. indexedBranches.addAll(repository.indexedBranches);
  283. }
  284. repository.indexedBranches = new ArrayList<String>(indexedBranches);
  285. }
  286. // only repositories that are actually _cloned_ from the origin
  287. // Gitblit repository are marked as federated. If the origin
  288. // is from somewhere else, these repositories are not considered
  289. // "federated" repositories.
  290. repository.isFederated = cloneUrl.startsWith(registration.url);
  291. GitBlit.self().updateConfiguration(r, repository);
  292. r.close();
  293. }
  294. IUserService userService = null;
  295. try {
  296. // Pull USERS
  297. // TeamModels are automatically pulled because they are contained
  298. // within the UserModel. The UserService creates unknown teams
  299. // and updates existing teams.
  300. Collection<UserModel> users = FederationUtils.getUsers(registration);
  301. if (users != null && users.size() > 0) {
  302. File realmFile = new File(registrationFolderFile, registration.name + "_users.conf");
  303. realmFile.delete();
  304. userService = new ConfigUserService(realmFile);
  305. for (UserModel user : users) {
  306. userService.updateUserModel(user.username, user);
  307. // merge the origin permissions and origin accounts into
  308. // the user accounts of this Gitblit instance
  309. if (registration.mergeAccounts) {
  310. // reparent all repository permissions if the local
  311. // repositories are stored within subfolders
  312. if (!StringUtils.isEmpty(registrationFolder)) {
  313. if (user.permissions != null) {
  314. // pulling from >= 1.2 version
  315. Map<String, AccessPermission> copy = new HashMap<String, AccessPermission>(user.permissions);
  316. user.permissions.clear();
  317. for (Map.Entry<String, AccessPermission> entry : copy.entrySet()) {
  318. user.setRepositoryPermission(registrationFolder + "/" + entry.getKey(), entry.getValue());
  319. }
  320. } else {
  321. // pulling from <= 1.1 version
  322. List<String> permissions = new ArrayList<String>(user.repositories);
  323. user.repositories.clear();
  324. for (String permission : permissions) {
  325. user.addRepositoryPermission(registrationFolder + "/" + permission);
  326. }
  327. }
  328. }
  329. // insert new user or update local user
  330. UserModel localUser = GitBlit.self().getUserModel(user.username);
  331. if (localUser == null) {
  332. // create new local user
  333. GitBlit.self().updateUserModel(user.username, user, true);
  334. } else {
  335. // update repository permissions of local user
  336. if (user.permissions != null) {
  337. // pulling from >= 1.2 version
  338. Map<String, AccessPermission> copy = new HashMap<String, AccessPermission>(user.permissions);
  339. for (Map.Entry<String, AccessPermission> entry : copy.entrySet()) {
  340. localUser.setRepositoryPermission(entry.getKey(), entry.getValue());
  341. }
  342. } else {
  343. // pulling from <= 1.1 version
  344. for (String repository : user.repositories) {
  345. localUser.addRepositoryPermission(repository);
  346. }
  347. }
  348. localUser.password = user.password;
  349. localUser.canAdmin = user.canAdmin;
  350. GitBlit.self().updateUserModel(localUser.username, localUser, false);
  351. }
  352. for (String teamname : GitBlit.self().getAllTeamnames()) {
  353. TeamModel team = GitBlit.self().getTeamModel(teamname);
  354. if (user.isTeamMember(teamname) && !team.hasUser(user.username)) {
  355. // new team member
  356. team.addUser(user.username);
  357. GitBlit.self().updateTeamModel(teamname, team, false);
  358. } else if (!user.isTeamMember(teamname) && team.hasUser(user.username)) {
  359. // remove team member
  360. team.removeUser(user.username);
  361. GitBlit.self().updateTeamModel(teamname, team, false);
  362. }
  363. // update team repositories
  364. TeamModel remoteTeam = user.getTeam(teamname);
  365. if (remoteTeam != null) {
  366. if (remoteTeam.permissions != null) {
  367. // pulling from >= 1.2
  368. for (Map.Entry<String, AccessPermission> entry : remoteTeam.permissions.entrySet()){
  369. team.setRepositoryPermission(entry.getKey(), entry.getValue());
  370. }
  371. GitBlit.self().updateTeamModel(teamname, team, false);
  372. } else if(!ArrayUtils.isEmpty(remoteTeam.repositories)) {
  373. // pulling from <= 1.1
  374. team.addRepositoryPermissions(remoteTeam.repositories);
  375. GitBlit.self().updateTeamModel(teamname, team, false);
  376. }
  377. }
  378. }
  379. }
  380. }
  381. }
  382. } catch (ForbiddenException e) {
  383. // ignore forbidden exceptions
  384. } catch (IOException e) {
  385. logger.warn(MessageFormat.format(
  386. "Failed to retrieve USERS from federated gitblit ({0} @ {1})",
  387. registration.name, registration.url), e);
  388. }
  389. try {
  390. // Pull TEAMS
  391. // We explicitly pull these even though they are embedded in
  392. // UserModels because it is possible to use teams to specify
  393. // mailing lists or push scripts without specifying users.
  394. if (userService != null) {
  395. Collection<TeamModel> teams = FederationUtils.getTeams(registration);
  396. if (teams != null && teams.size() > 0) {
  397. for (TeamModel team : teams) {
  398. userService.updateTeamModel(team);
  399. }
  400. }
  401. }
  402. } catch (ForbiddenException e) {
  403. // ignore forbidden exceptions
  404. } catch (IOException e) {
  405. logger.warn(MessageFormat.format(
  406. "Failed to retrieve TEAMS from federated gitblit ({0} @ {1})",
  407. registration.name, registration.url), e);
  408. }
  409. try {
  410. // Pull SETTINGS
  411. Map<String, String> settings = FederationUtils.getSettings(registration);
  412. if (settings != null && settings.size() > 0) {
  413. Properties properties = new Properties();
  414. properties.putAll(settings);
  415. FileOutputStream os = new FileOutputStream(new File(registrationFolderFile,
  416. registration.name + "_" + Constants.PROPERTIES_FILE));
  417. properties.store(os, null);
  418. os.close();
  419. }
  420. } catch (ForbiddenException e) {
  421. // ignore forbidden exceptions
  422. } catch (IOException e) {
  423. logger.warn(MessageFormat.format(
  424. "Failed to retrieve SETTINGS from federated gitblit ({0} @ {1})",
  425. registration.name, registration.url), e);
  426. }
  427. try {
  428. // Pull SCRIPTS
  429. Map<String, String> scripts = FederationUtils.getScripts(registration);
  430. if (scripts != null && scripts.size() > 0) {
  431. for (Map.Entry<String, String> script : scripts.entrySet()) {
  432. String scriptName = script.getKey();
  433. if (scriptName.endsWith(".groovy")) {
  434. scriptName = scriptName.substring(0, scriptName.indexOf(".groovy"));
  435. }
  436. File file = new File(registrationFolderFile, registration.name + "_"
  437. + scriptName + ".groovy");
  438. FileUtils.writeContent(file, script.getValue());
  439. }
  440. }
  441. } catch (ForbiddenException e) {
  442. // ignore forbidden exceptions
  443. } catch (IOException e) {
  444. logger.warn(MessageFormat.format(
  445. "Failed to retrieve SCRIPTS from federated gitblit ({0} @ {1})",
  446. registration.name, registration.url), e);
  447. }
  448. }
  449. /**
  450. * Sends a status acknowledgment to the origin Gitblit instance. This
  451. * includes the results of the federated pull.
  452. *
  453. * @param registration
  454. * @throws Exception
  455. */
  456. private void sendStatusAcknowledgment(FederationModel registration) throws Exception {
  457. if (!registration.sendStatus) {
  458. // skip status acknowledgment
  459. return;
  460. }
  461. InetAddress addr = InetAddress.getLocalHost();
  462. String federationName = GitBlit.getString(Keys.federation.name, null);
  463. if (StringUtils.isEmpty(federationName)) {
  464. federationName = addr.getHostName();
  465. }
  466. FederationUtils.acknowledgeStatus(addr.getHostAddress(), registration);
  467. logger.info(MessageFormat.format("Pull status sent to {0}", registration.url));
  468. }
  469. /**
  470. * Schedules the next check of the federated Gitblit instance.
  471. *
  472. * @param registration
  473. */
  474. private void schedule(FederationModel registration) {
  475. // schedule the next pull
  476. int mins = TimeUtils.convertFrequencyToMinutes(registration.frequency);
  477. registration.nextPull = new Date(System.currentTimeMillis() + (mins * 60 * 1000L));
  478. GitBlit.self().executor()
  479. .schedule(new FederationPullExecutor(registration), mins, TimeUnit.MINUTES);
  480. logger.info(MessageFormat.format(
  481. "Next pull of {0} @ {1} scheduled for {2,date,yyyy-MM-dd HH:mm}",
  482. registration.name, registration.url, registration.nextPull));
  483. }
  484. }