6 STDERR.puts "#{$0} filename -- fast-export filename's RCS history"
10 STDERR.puts "Could not find #{arg}"
15 fields = string.split('.')
16 raise ArgumentError, "wrong number of fields for RCS date #{string}" unless fields.length == 6
22 # strip an optional final ;
27 # strip the first and last @, and de-double @@s
32 raise 'malformed first line' unless ret.first[0,1] == '@'
33 raise 'malformed last line' unless ret.last[-1,1] == '@'
34 ret.first.sub!(/^@/,'')
35 ret.last.sub!(/@$/,'')
36 ret.map { |l| l.gsub('@@','@') }
38 arg.chomp('@').sub(/^@/,'').gsub('@@','@')
46 RCS.sanitize RCS.clean(arg)
50 arg.gsub('.', '0') + ('90'*5)
54 arg.gsub('.', '0') + ('09'*5)
58 attr_accessor :head, :comment, :desc, :revision
64 @revision = Hash.new { |h, r| h[r] = Revision.new(r) }
67 def has_revision?(rev)
68 @revision.has_key?(rev) and not @revision[rev].author.nil?
74 until @revision.empty?
77 # a string sort is a very good candidate for
78 # export order, getting a miss only for
79 # multi-digit revision components
80 keys = @revision.keys.sort
82 STDERR.puts "commit export loop ##{counter}"
83 STDERR.puts "\t#{exported.length} commits exported so far: #{exported.join(', ')}" unless exported.empty?
84 STDERR.puts "\t#{keys.size} to export: #{keys.join(', ')}"
88 # the parent commit is rev.next if we're on the
89 # master branch (rev.branch is nil) or
90 # rev.diff_base otherwise
91 from = rev.branch.nil? ? rev.next : rev.diff_base
92 # A commit can only be exported if it has no
93 # parent, or if the parent has been exported
94 # already. Skip this commit otherwise
95 if from and not exported.include? from
99 branch = rev.branch || 'master'
100 # TODO map authors to author/email
101 author = "#{rev.author} <empty>"
102 date = "#{rev.date.tv_sec} +0000"
105 puts "commit refs/heads/#{branch}"
106 puts "mark :#{RCS.commit key}"
107 puts "committer #{author} #{date}"
108 puts "data #{log.length}"
109 puts log unless log.empty?
110 puts "from :#{RCS.commit from}" if rev.branch_point
111 puts "M 644 :#{RCS.blob key} #{@fname}"
113 rev.symbols.each do |sym|
114 puts "reset refs/tags/#{sym}"
115 puts "from :#{RCS.commit key}"
117 # TODO option to tag every revision with its revision number
121 exported.each { |k| @revision.delete(k) }
127 attr_accessor :rev, :author, :date, :state, :next
128 attr_accessor :branches, :log, :text, :symbols
129 attr_accessor :branch, :diff_base, :branch_point
146 @date = Time.rcs(str)
151 ret = "blob\nmark :#{RCS.blob @rev}\ndata #{str.length}\n#{str}\n"
156 def RCS.parse(fname, rcsfile, opts={})
157 rcs = RCS::File.new(fname)
159 ::File.open(rcsfile, 'r') do |file|
164 file.each_line do |line|
167 command, args = line.split($;,2)
168 next if command.empty?
172 rcs.head = RCS.clean(args.chomp)
176 rcs.comment = RCS.at_clean(args.chomp)
179 if rcs.has_revision?(rev)
180 status.push :revision_data
182 status.push :new_revision
187 status.push :read_lines
189 STDERR.puts "Skipping unhandled command #{command.inspect}"
192 sym, rev = line.strip.split(':',2);
193 status.pop if rev.chomp!(';')
194 rcs.revision[rev].symbols << sym
196 rcs.desc.replace lines.dup
199 # we sanitize lines as we read them
201 actual_line = line.dup
203 # the first line must begin with a @, which we strip
205 ats = line.match(/^@+/)
206 raise 'malformed line' unless ats
207 actual_line.replace line.sub(/^@/,'')
210 # if the line ends with an ODD number of @, it's the
211 # last line -- we work on actual_line so that content
212 # such as @\n or @ work correctly (they would be
213 # encoded respectively as ['@@@\n','@\n'] and
215 ats = actual_line.chomp.match(/@+$/)
216 if nomore = (ats && Regexp.last_match(0).length.odd?)
217 actual_line.replace actual_line.chomp.sub(/@$/,'')
219 lines << actual_line.gsub('@@','@')
226 when /^date\s+(\S+);\s+author\s+(\S+);\sstate\s(\S+);$/
227 rcs.revision[rev].date = $1
228 rcs.revision[rev].author = $2
229 rcs.revision[rev].state = $3
231 status.push :branches
234 when /^next\s+(\S+)?;$/
235 nxt = rcs.revision[rev].next = $1
237 raise "multiple diff_bases for #{nxt}" unless rcs.revision[nxt].diff_base.nil?
238 rcs.revision[nxt].diff_base = rev
239 rcs.revision[nxt].branch = rcs.revision[rev].branch
244 candidate = line.split(';',2)
245 branch = candidate.first.strip
246 rcs.revision[rev].branches.push branch
247 raise "multiple diff_bases for #{branch}" unless rcs.revision[branch].diff_base.nil?
248 rcs.revision[branch].diff_base = rev
249 # we drop the last number from the branch name
250 rcs.revision[branch].branch = branch.sub(/\.\d+$/,'.x')
251 rcs.revision[branch].branch_point = rev
252 status.pop if candidate.length > 1
258 status.push :read_lines
266 status.push :read_lines
271 rcs.revision[rev].log.replace lines.dup
274 rcs.revision[rev].text.replace lines.dup
275 puts rcs.revision[rev].blob
278 difflines.replace lines.dup
279 difflines.pop if difflines.last.empty?
280 base = rcs.revision[rev].diff_base
281 unless rcs.revision[base].text
284 raise 'no diff base!'
288 rcs.revision[base].text.each { |l| buffer << l.dup }
294 while l = difflines.shift
298 adding = false unless count > 0
303 raise 'malformed diff' unless l =~ /^([ad])(\d+) (\d+)$/
308 # we replace them with empty string so that 'a' commands
309 # referring to the same line work properly
311 buffer[index].replace ''
321 buffer.delete_if { |l| l.empty? }
323 rcs.revision[rev].text = buffer
324 puts rcs.revision[rev].blob
327 STDERR.puts "Unknown status #{status.last}"
349 not_found "RCS file #{arg}"
352 filename = File.basename(arg, SFX)
354 filename = File.basename(arg)
355 path = File.dirname(arg)
356 rcsfile = File.join(path, 'RCS', filename) + SFX
357 unless File.exists? rcsfile
358 rcsfile.replace File.join(path, filename) + SFX
359 unless File.exists? rcsfile
360 not_found "RCS file for #{filename} in #{path}"
365 RCS.parse(filename, rcsfile)