chris@217: #!/usr/bin/env ruby chris@217: chris@217: # == Synopsis chris@217: # chris@241: # convert-external-repos: Update local Mercurial mirrors of external repos, chris@241: # by running an external command for each project requiring an update. chris@217: # chris@217: # == Usage chris@217: # chris@217: # convert-external-repos [OPTIONS...] -s [DIR] -r [HOST] chris@217: # chris@217: # == Arguments (mandatory) chris@217: # chris@241: # -s, --scm-dir=DIR use DIR as base directory for repositories chris@217: # -r, --redmine-host=HOST assume Redmine is hosted on HOST. Examples: chris@217: # -r redmine.example.net chris@217: # -r http://redmine.example.net chris@217: # -r https://example.net/redmine chris@217: # -k, --key=KEY use KEY as the Redmine API key chris@241: # -c, --command=COMMAND use this command to update each external chris@241: # repository: command is called with the name chris@241: # of the project, the path to its repo, and chris@241: # its external repo url as its three args chris@217: # chris@217: # == Options chris@217: # chris@217: # --http-user=USER User for HTTP Basic authentication with Redmine WS chris@217: # --http-pass=PASSWORD Password for Basic authentication with Redmine WS chris@217: # -t, --test only show what should be done chris@217: # -h, --help show help and exit chris@217: # -v, --verbose verbose chris@217: # -V, --version print version and exit chris@217: # -q, --quiet no log chris@217: chris@217: chris@217: require 'getoptlong' chris@217: require 'find' chris@217: require 'etc' chris@217: chris@217: Version = "1.0" chris@217: chris@217: opts = GetoptLong.new( chris@241: ['--scm-dir', '-s', GetoptLong::REQUIRED_ARGUMENT], chris@217: ['--redmine-host', '-r', GetoptLong::REQUIRED_ARGUMENT], chris@217: ['--key', '-k', GetoptLong::REQUIRED_ARGUMENT], chris@217: ['--http-user', GetoptLong::REQUIRED_ARGUMENT], chris@217: ['--http-pass', GetoptLong::REQUIRED_ARGUMENT], chris@241: ['--command' , '-c', GetoptLong::REQUIRED_ARGUMENT], chris@217: ['--test', '-t', GetoptLong::NO_ARGUMENT], chris@217: ['--verbose', '-v', GetoptLong::NO_ARGUMENT], chris@217: ['--version', '-V', GetoptLong::NO_ARGUMENT], chris@217: ['--help' , '-h', GetoptLong::NO_ARGUMENT], chris@217: ['--quiet' , '-q', GetoptLong::NO_ARGUMENT] chris@217: ) chris@217: chris@217: $verbose = 0 chris@217: $quiet = false chris@217: $redmine_host = '' chris@217: $repos_base = '' chris@217: $http_user = '' chris@217: $http_pass = '' chris@217: $test = false chris@217: chris@437: $mirrordir = '/var/mirror' chris@437: chris@217: def log(text, options={}) chris@217: level = options[:level] || 0 chris@217: puts text unless $quiet or level > $verbose chris@217: exit 1 if options[:exit] chris@217: end chris@217: chris@217: def system_or_raise(command) chris@217: raise "\"#{command}\" failed" unless system command chris@217: end chris@217: chris@217: begin chris@217: opts.each do |opt, arg| chris@217: case opt chris@241: when '--scm-dir'; $repos_base = arg.dup chris@217: when '--redmine-host'; $redmine_host = arg.dup chris@217: when '--key'; $api_key = arg.dup chris@217: when '--http-user'; $http_user = arg.dup chris@217: when '--http-pass'; $http_pass = arg.dup chris@241: when '--command'; $command = arg.dup chris@217: when '--verbose'; $verbose += 1 chris@217: when '--test'; $test = true chris@217: when '--version'; puts Version; exit Chris@1336: when '--help'; puts "Read source for documentation"; exit chris@217: when '--quiet'; $quiet = true chris@217: end chris@217: end chris@217: rescue chris@217: exit 1 chris@217: end chris@217: chris@217: if $test chris@217: log("running in test mode") chris@217: end chris@217: chris@241: if ($redmine_host.empty? or $repos_base.empty? or $command.empty?) Chris@1336: puts "Read source for documentation"; exit chris@217: end chris@217: chris@217: unless File.directory?($repos_base) chris@217: log("directory '#{$repos_base}' doesn't exist", :exit => true) chris@217: end chris@217: chris@217: begin chris@217: require 'active_resource' chris@217: rescue LoadError chris@217: log("This script requires activeresource.\nRun 'gem install activeresource' to install it.", :exit => true) chris@217: end chris@217: chris@217: class Project < ActiveResource::Base chris@217: self.headers["User-agent"] = "SoundSoftware external repository converter/#{Version}" Chris@1336: self.format = :xml chris@217: end chris@217: chris@217: log("querying Redmine for projects...", :level => 1); chris@217: chris@217: $redmine_host.gsub!(/^/, "http://") unless $redmine_host.match("^https?://") chris@217: $redmine_host.gsub!(/\/$/, '') chris@217: chris@217: Project.site = "#{$redmine_host}/sys"; chris@217: Project.user = $http_user; chris@217: Project.password = $http_pass; chris@217: chris@217: begin chris@217: # Get all active projects that have the Repository module enabled chris@217: projects = Project.find(:all, :params => {:key => $api_key}) Chris@1336: rescue ActiveResource::ForbiddenAccess Chris@1336: log("Request was denied by your Redmine server. Make sure that 'WS for repository management' is enabled in application settings and that you provided the correct API key.") chris@217: rescue => e chris@217: log("Unable to connect to #{Project.site}: #{e}", :exit => true) chris@217: end chris@217: chris@217: if projects.nil? chris@217: log('no project found, perhaps you forgot to "Enable WS for repository management"', :exit => true) chris@217: end chris@217: chris@217: log("retrieved #{projects.size} projects", :level => 1) chris@217: chris@217: projects.each do |project| chris@217: log("treating project #{project.name}", :level => 1) chris@217: chris@217: if project.identifier.empty? chris@217: log("\tno identifier for project #{project.name}") chris@217: next chris@217: elsif not project.identifier.match(/^[a-z0-9\-]+$/) chris@217: log("\tinvalid identifier for project #{project.name} : #{project.identifier}"); chris@217: next chris@217: end chris@217: chris@217: if !project.respond_to?(:repository) or !project.repository.is_external? chris@217: log("\tproject #{project.identifier} does not use an external repository"); chris@217: next chris@217: end chris@217: chris@217: external_url = project.repository.external_url; chris@217: log("\tproject #{project.identifier} has external repository url #{external_url}"); chris@217: chris@217: if !external_url.match(/^[a-z][a-z+]{0,8}[a-z]:\/\//) chris@217: log("\tthis doesn't look like a plausible url to me, skipping") chris@217: next chris@217: end chris@217: chris@217: repos_path = File.join($repos_base, project.identifier).gsub(File::SEPARATOR, File::ALT_SEPARATOR || File::SEPARATOR) chris@217: chris@241: unless File.directory?(repos_path) chris@241: log("\tproject repo directory '#{repos_path}' doesn't exist") chris@217: next chris@217: end chris@217: chris@241: system($command, project.identifier, repos_path, external_url) chris@437: chris@437: $cache_clearance_file = File.join($mirrordir, project.identifier, 'url_changed') chris@437: if File.file?($cache_clearance_file) chris@437: log("\tproject repo url has changed, requesting cache clearance") chris@437: if project.post(:repository_cache, :key => $api_key) chris@437: File.delete($cache_clearance_file) chris@437: end chris@437: end chris@217: chris@217: end chris@217: