chris@217
|
1 #!/usr/bin/env ruby
|
chris@217
|
2
|
chris@217
|
3 # == Synopsis
|
chris@217
|
4 #
|
chris@241
|
5 # convert-external-repos: Update local Mercurial mirrors of external repos,
|
chris@241
|
6 # by running an external command for each project requiring an update.
|
chris@217
|
7 #
|
chris@217
|
8 # == Usage
|
chris@217
|
9 #
|
chris@217
|
10 # convert-external-repos [OPTIONS...] -s [DIR] -r [HOST]
|
chris@217
|
11 #
|
chris@217
|
12 # == Arguments (mandatory)
|
chris@217
|
13 #
|
chris@241
|
14 # -s, --scm-dir=DIR use DIR as base directory for repositories
|
chris@217
|
15 # -r, --redmine-host=HOST assume Redmine is hosted on HOST. Examples:
|
chris@217
|
16 # -r redmine.example.net
|
chris@217
|
17 # -r http://redmine.example.net
|
chris@217
|
18 # -r https://example.net/redmine
|
chris@217
|
19 # -k, --key=KEY use KEY as the Redmine API key
|
chris@241
|
20 # -c, --command=COMMAND use this command to update each external
|
chris@241
|
21 # repository: command is called with the name
|
chris@241
|
22 # of the project, the path to its repo, and
|
chris@241
|
23 # its external repo url as its three args
|
chris@217
|
24 #
|
chris@217
|
25 # == Options
|
chris@217
|
26 #
|
chris@217
|
27 # --http-user=USER User for HTTP Basic authentication with Redmine WS
|
chris@217
|
28 # --http-pass=PASSWORD Password for Basic authentication with Redmine WS
|
chris@217
|
29 # -t, --test only show what should be done
|
chris@217
|
30 # -h, --help show help and exit
|
chris@217
|
31 # -v, --verbose verbose
|
chris@217
|
32 # -V, --version print version and exit
|
chris@217
|
33 # -q, --quiet no log
|
chris@217
|
34
|
chris@217
|
35
|
chris@217
|
36 require 'getoptlong'
|
chris@217
|
37 require 'find'
|
chris@217
|
38 require 'etc'
|
chris@217
|
39
|
chris@217
|
40 Version = "1.0"
|
chris@217
|
41
|
chris@217
|
42 opts = GetoptLong.new(
|
chris@241
|
43 ['--scm-dir', '-s', GetoptLong::REQUIRED_ARGUMENT],
|
chris@217
|
44 ['--redmine-host', '-r', GetoptLong::REQUIRED_ARGUMENT],
|
chris@217
|
45 ['--key', '-k', GetoptLong::REQUIRED_ARGUMENT],
|
chris@217
|
46 ['--http-user', GetoptLong::REQUIRED_ARGUMENT],
|
chris@217
|
47 ['--http-pass', GetoptLong::REQUIRED_ARGUMENT],
|
chris@241
|
48 ['--command' , '-c', GetoptLong::REQUIRED_ARGUMENT],
|
chris@217
|
49 ['--test', '-t', GetoptLong::NO_ARGUMENT],
|
chris@217
|
50 ['--verbose', '-v', GetoptLong::NO_ARGUMENT],
|
chris@217
|
51 ['--version', '-V', GetoptLong::NO_ARGUMENT],
|
chris@217
|
52 ['--help' , '-h', GetoptLong::NO_ARGUMENT],
|
chris@217
|
53 ['--quiet' , '-q', GetoptLong::NO_ARGUMENT]
|
chris@217
|
54 )
|
chris@217
|
55
|
chris@217
|
56 $verbose = 0
|
chris@217
|
57 $quiet = false
|
chris@217
|
58 $redmine_host = ''
|
chris@217
|
59 $repos_base = ''
|
chris@217
|
60 $http_user = ''
|
chris@217
|
61 $http_pass = ''
|
chris@217
|
62 $test = false
|
chris@217
|
63
|
chris@437
|
64 $mirrordir = '/var/mirror'
|
chris@437
|
65
|
chris@217
|
66 def log(text, options={})
|
chris@217
|
67 level = options[:level] || 0
|
chris@217
|
68 puts text unless $quiet or level > $verbose
|
chris@217
|
69 exit 1 if options[:exit]
|
chris@217
|
70 end
|
chris@217
|
71
|
chris@217
|
72 def system_or_raise(command)
|
chris@217
|
73 raise "\"#{command}\" failed" unless system command
|
chris@217
|
74 end
|
chris@217
|
75
|
chris@217
|
76 begin
|
chris@217
|
77 opts.each do |opt, arg|
|
chris@217
|
78 case opt
|
chris@241
|
79 when '--scm-dir'; $repos_base = arg.dup
|
chris@217
|
80 when '--redmine-host'; $redmine_host = arg.dup
|
chris@217
|
81 when '--key'; $api_key = arg.dup
|
chris@217
|
82 when '--http-user'; $http_user = arg.dup
|
chris@217
|
83 when '--http-pass'; $http_pass = arg.dup
|
chris@241
|
84 when '--command'; $command = arg.dup
|
chris@217
|
85 when '--verbose'; $verbose += 1
|
chris@217
|
86 when '--test'; $test = true
|
chris@217
|
87 when '--version'; puts Version; exit
|
Chris@1336
|
88 when '--help'; puts "Read source for documentation"; exit
|
chris@217
|
89 when '--quiet'; $quiet = true
|
chris@217
|
90 end
|
chris@217
|
91 end
|
chris@217
|
92 rescue
|
chris@217
|
93 exit 1
|
chris@217
|
94 end
|
chris@217
|
95
|
chris@217
|
96 if $test
|
chris@217
|
97 log("running in test mode")
|
chris@217
|
98 end
|
chris@217
|
99
|
chris@241
|
100 if ($redmine_host.empty? or $repos_base.empty? or $command.empty?)
|
Chris@1336
|
101 puts "Read source for documentation"; exit
|
chris@217
|
102 end
|
chris@217
|
103
|
chris@217
|
104 unless File.directory?($repos_base)
|
chris@217
|
105 log("directory '#{$repos_base}' doesn't exist", :exit => true)
|
chris@217
|
106 end
|
chris@217
|
107
|
chris@217
|
108 begin
|
chris@217
|
109 require 'active_resource'
|
chris@217
|
110 rescue LoadError
|
chris@217
|
111 log("This script requires activeresource.\nRun 'gem install activeresource' to install it.", :exit => true)
|
chris@217
|
112 end
|
chris@217
|
113
|
chris@217
|
114 class Project < ActiveResource::Base
|
chris@217
|
115 self.headers["User-agent"] = "SoundSoftware external repository converter/#{Version}"
|
Chris@1336
|
116 self.format = :xml
|
chris@217
|
117 end
|
chris@217
|
118
|
chris@217
|
119 log("querying Redmine for projects...", :level => 1);
|
chris@217
|
120
|
chris@217
|
121 $redmine_host.gsub!(/^/, "http://") unless $redmine_host.match("^https?://")
|
chris@217
|
122 $redmine_host.gsub!(/\/$/, '')
|
chris@217
|
123
|
chris@217
|
124 Project.site = "#{$redmine_host}/sys";
|
chris@217
|
125 Project.user = $http_user;
|
chris@217
|
126 Project.password = $http_pass;
|
chris@217
|
127
|
chris@217
|
128 begin
|
chris@217
|
129 # Get all active projects that have the Repository module enabled
|
chris@217
|
130 projects = Project.find(:all, :params => {:key => $api_key})
|
Chris@1336
|
131 rescue ActiveResource::ForbiddenAccess
|
Chris@1336
|
132 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
|
133 rescue => e
|
chris@217
|
134 log("Unable to connect to #{Project.site}: #{e}", :exit => true)
|
chris@217
|
135 end
|
chris@217
|
136
|
chris@217
|
137 if projects.nil?
|
chris@217
|
138 log('no project found, perhaps you forgot to "Enable WS for repository management"', :exit => true)
|
chris@217
|
139 end
|
chris@217
|
140
|
chris@217
|
141 log("retrieved #{projects.size} projects", :level => 1)
|
chris@217
|
142
|
chris@217
|
143 projects.each do |project|
|
chris@217
|
144 log("treating project #{project.name}", :level => 1)
|
chris@217
|
145
|
chris@217
|
146 if project.identifier.empty?
|
chris@217
|
147 log("\tno identifier for project #{project.name}")
|
chris@217
|
148 next
|
Chris@1445
|
149 elsif not project.identifier.match(/^[a-z0-9_\-]+$/)
|
chris@217
|
150 log("\tinvalid identifier for project #{project.name} : #{project.identifier}");
|
chris@217
|
151 next
|
chris@217
|
152 end
|
chris@217
|
153
|
chris@217
|
154 if !project.respond_to?(:repository) or !project.repository.is_external?
|
chris@217
|
155 log("\tproject #{project.identifier} does not use an external repository");
|
chris@217
|
156 next
|
chris@217
|
157 end
|
chris@217
|
158
|
chris@217
|
159 external_url = project.repository.external_url;
|
chris@217
|
160 log("\tproject #{project.identifier} has external repository url #{external_url}");
|
chris@217
|
161
|
chris@217
|
162 if !external_url.match(/^[a-z][a-z+]{0,8}[a-z]:\/\//)
|
chris@217
|
163 log("\tthis doesn't look like a plausible url to me, skipping")
|
chris@217
|
164 next
|
chris@217
|
165 end
|
chris@217
|
166
|
chris@217
|
167 repos_path = File.join($repos_base, project.identifier).gsub(File::SEPARATOR, File::ALT_SEPARATOR || File::SEPARATOR)
|
chris@217
|
168
|
chris@241
|
169 unless File.directory?(repos_path)
|
chris@241
|
170 log("\tproject repo directory '#{repos_path}' doesn't exist")
|
chris@217
|
171 next
|
chris@217
|
172 end
|
chris@217
|
173
|
chris@241
|
174 system($command, project.identifier, repos_path, external_url)
|
chris@437
|
175
|
chris@437
|
176 $cache_clearance_file = File.join($mirrordir, project.identifier, 'url_changed')
|
chris@437
|
177 if File.file?($cache_clearance_file)
|
chris@437
|
178 log("\tproject repo url has changed, requesting cache clearance")
|
chris@437
|
179 if project.post(:repository_cache, :key => $api_key)
|
chris@437
|
180 File.delete($cache_clearance_file)
|
chris@437
|
181 end
|
chris@437
|
182 end
|
chris@217
|
183
|
chris@217
|
184 end
|
chris@217
|
185
|