1# vcs
2
3require 'time'
4
5ENV.delete('PWD')
6
7unless File.respond_to? :realpath
8  require 'pathname'
9  def File.realpath(arg)
10    Pathname(arg).realpath.to_s
11  end
12end
13
14class VCS
15  class NotFoundError < RuntimeError; end
16
17  @@dirs = []
18  def self.register(dir)
19    @@dirs << [dir, self]
20  end
21
22  def self.detect(path)
23    @@dirs.each do |dir, klass|
24      return klass.new(path) if File.directory?(File.join(path, dir))
25      prev = path
26      loop {
27        curr = File.realpath(File.join(prev, '..'))
28        break if curr == prev	# stop at the root directory
29        return klass.new(path) if File.directory?(File.join(curr, dir))
30        prev = curr
31      }
32    end
33    raise VCS::NotFoundError, "does not seem to be under a vcs: #{path}"
34  end
35
36  def initialize(path)
37    @srcdir = path
38    super()
39  end
40
41  # return a pair of strings, the last revision and the last revision in which
42  # +path+ was modified.
43  def get_revisions(path)
44    path = relative_to(path)
45    last, changed, modified, *rest = Dir.chdir(@srcdir) {self.class.get_revisions(path)}
46    last or raise "last revision not found"
47    changed or raise "changed revision not found"
48    modified &&= Time.parse(modified)
49    return last, changed, modified, *rest
50  end
51
52  def relative_to(path)
53    if path
54      srcdir = File.realpath(@srcdir)
55      path = File.realpath(path)
56      list1 = srcdir.split(%r{/})
57      list2 = path.split(%r{/})
58      while !list1.empty? && !list2.empty? && list1.first == list2.first
59        list1.shift
60        list2.shift
61      end
62      if list1.empty? && list2.empty?
63        "."
64      else
65        ([".."] * list1.length + list2).join("/")
66      end
67    else
68      '.'
69    end
70  end
71
72  class SVN < self
73    register(".svn")
74
75    def self.get_revisions(path)
76      begin
77        nulldevice = %w[/dev/null NUL NIL: NL:].find {|dev| File.exist?(dev)}
78        if nulldevice
79          save_stderr = STDERR.dup
80          STDERR.reopen nulldevice, 'w'
81        end
82        info_xml = `svn info --xml "#{path}"`
83      ensure
84        if save_stderr
85          STDERR.reopen save_stderr
86          save_stderr.close
87        end
88      end
89      _, last, _, changed, _ = info_xml.split(/revision="(\d+)"/)
90      modified = info_xml[/<date>([^<>]*)/, 1]
91      [last, changed, modified]
92    end
93  end
94
95  class GIT < self
96    register(".git")
97
98    def self.get_revisions(path)
99      logcmd = %Q[git log -n1 --grep="^ *git-svn-id: .*@[0-9][0-9]* "]
100      idpat = /git-svn-id: .*?@(\d+) \S+\Z/
101      last = `#{logcmd}`[idpat, 1]
102      if path
103        log = `#{logcmd} "#{path}"`
104        changed = log[idpat, 1]
105        modified = `git log --format=%ai -- #{path}`
106      else
107        changed = last
108      end
109      [last, changed, modified]
110    end
111  end
112end
113