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.

FederationPullService.java 17KB

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