7 #{$0} [options] file [file ...]
9 Fast-export the RCS history of one or more file.
12 --help, -h, -? display this help text
13 --authors-file, -A specify a file containing username = Full Name <email> mappings
14 --[no-]tag-each-rev [do not] create a lightweight tag for each RCS revision
20 STDERR.puts "Could not find #{arg}"
23 # returns a hash that maps usernames to author names & emails
24 def load_authors_file(fn)
27 File.open(File.expand_path fn) do |io|
28 io.each_line do |line|
29 uname, author = line.split('=', 2)
32 STDERR.puts "Username #{uname} redefined to #{author}" if hash.has_key? uname
44 fields = string.split('.')
45 raise ArgumentError, "wrong number of fields for RCS date #{string}" unless fields.length == 6
51 # strip an optional final ;
56 # strip the first and last @, and de-double @@s
61 raise 'malformed first line' unless ret.first[0,1] == '@'
62 raise 'malformed last line' unless ret.last[-1,1] == '@'
63 ret.first.sub!(/^@/,'')
64 ret.last.sub!(/@$/,'')
65 ret.map { |l| l.gsub('@@','@') }
67 arg.chomp('@').sub(/^@/,'').gsub('@@','@')
75 RCS.sanitize RCS.clean(arg)
79 arg.gsub('.', '0') + ('90'*5)
83 arg.gsub('.', '0') + ('09'*5)
87 attr_accessor :head, :comment, :desc, :revision
93 @revision = Hash.new { |h, r| h[r] = Revision.new(r) }
96 def has_revision?(rev)
97 @revision.has_key?(rev) and not @revision[rev].author.nil?
100 def export_commits(opts={})
103 until @revision.empty?
106 # a string sort is a very good candidate for
107 # export order, getting a miss only for
108 # multi-digit revision components
109 keys = @revision.keys.sort
111 STDERR.puts "commit export loop ##{counter}"
112 STDERR.puts "\t#{exported.length} commits exported so far: #{exported.join(', ')}" unless exported.empty?
113 STDERR.puts "\t#{keys.size} to export: #{keys.join(', ')}"
117 # the parent commit is rev.next if we're on the
118 # master branch (rev.branch is nil) or
119 # rev.diff_base otherwise
120 from = rev.branch.nil? ? rev.next : rev.diff_base
121 # A commit can only be exported if it has no
122 # parent, or if the parent has been exported
123 # already. Skip this commit otherwise
124 if from and not exported.include? from
128 branch = rev.branch || 'master'
129 author = opts[:authors][rev.author] || "#{rev.author} <empty>"
130 date = "#{rev.date.tv_sec} +0000"
133 puts "commit refs/heads/#{branch}"
134 puts "mark :#{RCS.commit key}"
135 puts "committer #{author} #{date}"
136 puts "data #{log.length}"
137 puts log unless log.empty?
138 puts "from :#{RCS.commit from}" if rev.branch_point
139 puts "M 644 :#{RCS.blob key} #{@fname}"
141 rev.symbols.each do |sym|
142 puts "reset refs/tags/#{sym}"
143 puts "from :#{RCS.commit key}"
145 if opts[:tag_each_rev]
146 puts "reset refs/tags/#{key}"
147 puts "from :#{RCS.commit key}"
152 exported.each { |k| @revision.delete(k) }
158 attr_accessor :rev, :author, :date, :state, :next
159 attr_accessor :branches, :log, :text, :symbols
160 attr_accessor :branch, :diff_base, :branch_point
177 @date = Time.rcs(str)
182 ret = "blob\nmark :#{RCS.blob @rev}\ndata #{str.length}\n#{str}\n"
187 def RCS.parse(fname, rcsfile, opts={})
188 rcs = RCS::File.new(fname)
190 ::File.open(rcsfile, 'r') do |file|
195 file.each_line do |line|
198 command, args = line.split($;,2)
199 next if command.empty?
203 rcs.head = RCS.clean(args.chomp)
207 rcs.comment = RCS.at_clean(args.chomp)
210 if rcs.has_revision?(rev)
211 status.push :revision_data
213 status.push :new_revision
218 status.push :read_lines
220 STDERR.puts "Skipping unhandled command #{command.inspect}"
223 sym, rev = line.strip.split(':',2);
224 status.pop if rev.chomp!(';')
225 rcs.revision[rev].symbols << sym
227 rcs.desc.replace lines.dup
230 # we sanitize lines as we read them
232 actual_line = line.dup
234 # the first line must begin with a @, which we strip
236 ats = line.match(/^@+/)
237 raise 'malformed line' unless ats
238 actual_line.replace line.sub(/^@/,'')
241 # if the line ends with an ODD number of @, it's the
242 # last line -- we work on actual_line so that content
243 # such as @\n or @ work correctly (they would be
244 # encoded respectively as ['@@@\n','@\n'] and
246 ats = actual_line.chomp.match(/@+$/)
247 if nomore = (ats && Regexp.last_match(0).length.odd?)
248 actual_line.replace actual_line.chomp.sub(/@$/,'')
250 lines << actual_line.gsub('@@','@')
257 when /^date\s+(\S+);\s+author\s+(\S+);\sstate\s(\S+);$/
258 rcs.revision[rev].date = $1
259 rcs.revision[rev].author = $2
260 rcs.revision[rev].state = $3
262 status.push :branches
265 when /^next\s+(\S+)?;$/
266 nxt = rcs.revision[rev].next = $1
268 raise "multiple diff_bases for #{nxt}" unless rcs.revision[nxt].diff_base.nil?
269 rcs.revision[nxt].diff_base = rev
270 rcs.revision[nxt].branch = rcs.revision[rev].branch
275 candidate = line.split(';',2)
276 branch = candidate.first.strip
277 rcs.revision[rev].branches.push branch
278 raise "multiple diff_bases for #{branch}" unless rcs.revision[branch].diff_base.nil?
279 rcs.revision[branch].diff_base = rev
280 # we drop the last number from the branch name
281 rcs.revision[branch].branch = branch.sub(/\.\d+$/,'.x')
282 rcs.revision[branch].branch_point = rev
283 status.pop if candidate.length > 1
289 status.push :read_lines
297 status.push :read_lines
302 rcs.revision[rev].log.replace lines.dup
305 rcs.revision[rev].text.replace lines.dup
306 puts rcs.revision[rev].blob
309 difflines.replace lines.dup
310 difflines.pop if difflines.last.empty?
311 base = rcs.revision[rev].diff_base
312 unless rcs.revision[base].text
315 raise 'no diff base!'
319 rcs.revision[base].text.each { |l| buffer << l.dup }
325 while l = difflines.shift
329 adding = false unless count > 0
334 raise 'malformed diff' unless l =~ /^([ad])(\d+) (\d+)$/
339 # we replace them with empty string so that 'a' commands
340 # referring to the same line work properly
342 buffer[index].replace ''
352 buffer.delete_if { |l| l.empty? }
354 rcs.revision[rev].text = buffer
355 puts rcs.revision[rev].blob
358 STDERR.puts "Unknown status #{status.last}"
363 rcs.export_commits(opts)
369 opts = GetoptLong.new(
370 # Authors file, like git-svn and git-cvsimport, more than one can be
372 ['--authors-file', '-A', GetoptLong::REQUIRED_ARGUMENT],
373 # RCS file suffix, like RCS
374 ['--rcs-suffixes', '-x', GetoptLong::REQUIRED_ARGUMENT],
376 ['--tag-each-rev', GetoptLong::NO_ARGUMENT],
377 ['--no-tag-each-rev', GetoptLong::NO_ARGUMENT],
378 ['--help', '-h', '-?', GetoptLong::NO_ARGUMENT]
381 # We read options in order, but they apply to all passed parameters.
382 # TODO maybe they should only apply to the following, unless there's only one
384 opts.ordering = GetoptLong::RETURN_IN_ORDER
388 :authors => Hash.new,
391 # Read config options
392 `git config --get-all rcs.authorsfile`.each_line do |fn|
393 parse_options[:authors].merge! load_authors_file(fn.chomp)
396 parse_options[:tag_each_rev] = (
397 `git config --bool rcs.tageachrev`.chomp == 'true'
400 opts.each do |opt, arg|
402 when '--authors-file'
403 authors = load_authors_file(arg)
404 redef = parse_options[:authors].keys & authors.keys
405 STDERR.puts "Authors file #{arg} redefines #{redef.join(', ')}" unless redef.empty?
406 parse_options[:authors].merge!(authors)
407 when '--rcs-suffixes'
409 when '--tag-each-rev'
410 parse_options[:tag_each_rev] = true
411 when '--no-tag-each-rev'
412 # this is the default, which is fine since the missing key
413 # (default) returns nil which is false in Ruby
414 parse_options[:tag_each_rev] = false
432 file_list.each do |arg|
437 not_found "RCS file #{arg}"
440 filename = File.basename(arg, SFX)
442 filename = File.basename(arg)
443 path = File.dirname(arg)
444 rcsfile = File.join(path, 'RCS', filename) + SFX
445 unless File.exists? rcsfile
446 rcsfile.replace File.join(path, filename) + SFX
447 unless File.exists? rcsfile
448 not_found "RCS file for #{filename} in #{path}"
453 RCS.parse(filename, rcsfile, parse_options)