6 STDERR.puts "#{$0} filename -- fast-export filename's RCS history"
10 STDERR.puts "Could not find #{arg}"
13 # returns a hash that maps usernames to author names & emails
14 def load_authors_file(fn)
17 File.open(File.expand_path fn) do |io|
18 io.each_line do |line|
19 uname, author = line.split('=', 2)
22 STDERR.puts "Username #{uname} redefined to #{author}" if hash.has_key? uname
34 fields = string.split('.')
35 raise ArgumentError, "wrong number of fields for RCS date #{string}" unless fields.length == 6
41 # strip an optional final ;
46 # strip the first and last @, and de-double @@s
51 raise 'malformed first line' unless ret.first[0,1] == '@'
52 raise 'malformed last line' unless ret.last[-1,1] == '@'
53 ret.first.sub!(/^@/,'')
54 ret.last.sub!(/@$/,'')
55 ret.map { |l| l.gsub('@@','@') }
57 arg.chomp('@').sub(/^@/,'').gsub('@@','@')
65 RCS.sanitize RCS.clean(arg)
69 arg.gsub('.', '0') + ('90'*5)
73 arg.gsub('.', '0') + ('09'*5)
77 attr_accessor :head, :comment, :desc, :revision
83 @revision = Hash.new { |h, r| h[r] = Revision.new(r) }
86 def has_revision?(rev)
87 @revision.has_key?(rev) and not @revision[rev].author.nil?
90 def export_commits(opts={})
93 until @revision.empty?
96 # a string sort is a very good candidate for
97 # export order, getting a miss only for
98 # multi-digit revision components
99 keys = @revision.keys.sort
101 STDERR.puts "commit export loop ##{counter}"
102 STDERR.puts "\t#{exported.length} commits exported so far: #{exported.join(', ')}" unless exported.empty?
103 STDERR.puts "\t#{keys.size} to export: #{keys.join(', ')}"
107 # the parent commit is rev.next if we're on the
108 # master branch (rev.branch is nil) or
109 # rev.diff_base otherwise
110 from = rev.branch.nil? ? rev.next : rev.diff_base
111 # A commit can only be exported if it has no
112 # parent, or if the parent has been exported
113 # already. Skip this commit otherwise
114 if from and not exported.include? from
118 branch = rev.branch || 'master'
119 author = opts[:authors][rev.author] || "#{rev.author} <empty>"
120 date = "#{rev.date.tv_sec} +0000"
123 puts "commit refs/heads/#{branch}"
124 puts "mark :#{RCS.commit key}"
125 puts "committer #{author} #{date}"
126 puts "data #{log.length}"
127 puts log unless log.empty?
128 puts "from :#{RCS.commit from}" if rev.branch_point
129 puts "M 644 :#{RCS.blob key} #{@fname}"
131 rev.symbols.each do |sym|
132 puts "reset refs/tags/#{sym}"
133 puts "from :#{RCS.commit key}"
135 if opts[:tag_each_rev]
136 puts "reset refs/tags/#{key}"
137 puts "from :#{RCS.commit key}"
142 exported.each { |k| @revision.delete(k) }
148 attr_accessor :rev, :author, :date, :state, :next
149 attr_accessor :branches, :log, :text, :symbols
150 attr_accessor :branch, :diff_base, :branch_point
167 @date = Time.rcs(str)
172 ret = "blob\nmark :#{RCS.blob @rev}\ndata #{str.length}\n#{str}\n"
177 def RCS.parse(fname, rcsfile, opts={})
178 rcs = RCS::File.new(fname)
180 ::File.open(rcsfile, 'r') do |file|
185 file.each_line do |line|
188 command, args = line.split($;,2)
189 next if command.empty?
193 rcs.head = RCS.clean(args.chomp)
197 rcs.comment = RCS.at_clean(args.chomp)
200 if rcs.has_revision?(rev)
201 status.push :revision_data
203 status.push :new_revision
208 status.push :read_lines
210 STDERR.puts "Skipping unhandled command #{command.inspect}"
213 sym, rev = line.strip.split(':',2);
214 status.pop if rev.chomp!(';')
215 rcs.revision[rev].symbols << sym
217 rcs.desc.replace lines.dup
220 # we sanitize lines as we read them
222 actual_line = line.dup
224 # the first line must begin with a @, which we strip
226 ats = line.match(/^@+/)
227 raise 'malformed line' unless ats
228 actual_line.replace line.sub(/^@/,'')
231 # if the line ends with an ODD number of @, it's the
232 # last line -- we work on actual_line so that content
233 # such as @\n or @ work correctly (they would be
234 # encoded respectively as ['@@@\n','@\n'] and
236 ats = actual_line.chomp.match(/@+$/)
237 if nomore = (ats && Regexp.last_match(0).length.odd?)
238 actual_line.replace actual_line.chomp.sub(/@$/,'')
240 lines << actual_line.gsub('@@','@')
247 when /^date\s+(\S+);\s+author\s+(\S+);\sstate\s(\S+);$/
248 rcs.revision[rev].date = $1
249 rcs.revision[rev].author = $2
250 rcs.revision[rev].state = $3
252 status.push :branches
255 when /^next\s+(\S+)?;$/
256 nxt = rcs.revision[rev].next = $1
258 raise "multiple diff_bases for #{nxt}" unless rcs.revision[nxt].diff_base.nil?
259 rcs.revision[nxt].diff_base = rev
260 rcs.revision[nxt].branch = rcs.revision[rev].branch
265 candidate = line.split(';',2)
266 branch = candidate.first.strip
267 rcs.revision[rev].branches.push branch
268 raise "multiple diff_bases for #{branch}" unless rcs.revision[branch].diff_base.nil?
269 rcs.revision[branch].diff_base = rev
270 # we drop the last number from the branch name
271 rcs.revision[branch].branch = branch.sub(/\.\d+$/,'.x')
272 rcs.revision[branch].branch_point = rev
273 status.pop if candidate.length > 1
279 status.push :read_lines
287 status.push :read_lines
292 rcs.revision[rev].log.replace lines.dup
295 rcs.revision[rev].text.replace lines.dup
296 puts rcs.revision[rev].blob
299 difflines.replace lines.dup
300 difflines.pop if difflines.last.empty?
301 base = rcs.revision[rev].diff_base
302 unless rcs.revision[base].text
305 raise 'no diff base!'
309 rcs.revision[base].text.each { |l| buffer << l.dup }
315 while l = difflines.shift
319 adding = false unless count > 0
324 raise 'malformed diff' unless l =~ /^([ad])(\d+) (\d+)$/
329 # we replace them with empty string so that 'a' commands
330 # referring to the same line work properly
332 buffer[index].replace ''
342 buffer.delete_if { |l| l.empty? }
344 rcs.revision[rev].text = buffer
345 puts rcs.revision[rev].blob
348 STDERR.puts "Unknown status #{status.last}"
353 rcs.export_commits(opts)
359 opts = GetoptLong.new(
360 # Authors file, like git-svn and git-cvsimport, more than one can be
362 ['--authors-file', '-A', GetoptLong::REQUIRED_ARGUMENT],
363 # RCS file suffix, like RCS
364 ['--rcs-suffixes', '-x', GetoptLong::REQUIRED_ARGUMENT],
366 ['--tag-each-rev', GetoptLong::NO_ARGUMENT],
367 ['--no-tag-each-rev', GetoptLong::NO_ARGUMENT]
370 # We read options in order, but they apply to all passed parameters.
371 # TODO maybe they should only apply to the following, unless there's only one
373 opts.ordering = GetoptLong::RETURN_IN_ORDER
377 :authors => Hash.new,
380 opts.each do |opt, arg|
382 when '--authors-file'
383 authors = load_authors_file(arg)
384 redef = parse_options[:authors].keys & authors.keys
385 STDERR.puts "Authors file #{arg} redefines #{redef.join(', ')}" unless redef.empty?
386 parse_options[:authors].merge!(authors)
387 when '--rcs-suffixes'
389 when '--tag-each-rev'
390 parse_options[:tag_each_rev] = true
391 when '--no-tag-each-rev'
392 # this is the default, which is fine since the missing key
393 # (default) returns nil which is false in Ruby
394 parse_options[:tag_each_rev] = false
409 # Read config options
410 `git config --get-all rcs.authorsfile`.each_line do |fn|
411 authors = load_authors_file(fn.chomp)
412 # Add but don't overwrite
413 authors.each do |k, v|
414 parse_options[:authors][k] ||= v
418 file_list.each do |arg|
423 not_found "RCS file #{arg}"
426 filename = File.basename(arg, SFX)
428 filename = File.basename(arg)
429 path = File.dirname(arg)
430 rcsfile = File.join(path, 'RCS', filename) + SFX
431 unless File.exists? rcsfile
432 rcsfile.replace File.join(path, filename) + SFX
433 unless File.exists? rcsfile
434 not_found "RCS file for #{filename} in #{path}"
439 RCS.parse(filename, rcsfile, parse_options)