Project

General

Profile

Feature #4455 » redmine-mercurial.patch

Mercurial overhaul - Peter Fern, 2009-12-21 01:27

View differences:

app/models/repository/mercurial.rb
28 28
  def self.scm_name
29 29
    'Mercurial'
30 30
  end
31
  
32
  def entries(path=nil, identifier=nil)
33
    entries=scm.entries(path, identifier)
34
    if entries
35
      entries.each do |entry|
36
        next unless entry.is_file?
37
        # Set the filesize unless browsing a specific revision
38
        if identifier.nil?
39
          full_path = File.join(root_url, entry.path)
40
          entry.size = File.stat(full_path).size if File.file?(full_path)
41
        end
42
        # Search the DB for the entry's last change
43
        change = changes.find(:first, :conditions => ["path = ?", scm.with_leading_slash(entry.path)], :order => "#{Changeset.table_name}.committed_on DESC")
44
        if change
45
          entry.lastrev.identifier = change.changeset.revision
46
          entry.lastrev.name = change.changeset.revision
47
          entry.lastrev.author = change.changeset.committer
48
          entry.lastrev.revision = change.revision
49
        end
50
      end
51
    end
52
    entries
31

  
32
  def branches
33
    scm.branches
34
  end
35

  
36
  def tags
37
    scm.tags
53 38
  end
54 39

  
40
  # Sequential changesets are brittle in Mercurial, so we take
41
  # a leaf out of Git's book, but run two passes to take 
42
  # advantage of the 'lite' log speed to build our sync list
55 43
  def fetch_changesets
56 44
    scm_info = scm.info
57
    if scm_info
58
      # latest revision found in database
59
      db_revision = latest_changeset ? latest_changeset.revision.to_i : -1
60
      # latest revision in the repository
61
      latest_revision = scm_info.lastrev
62
      return if latest_revision.nil?
63
      scm_revision = latest_revision.identifier.to_i
64
      if db_revision < scm_revision
65
        logger.debug "Fetching changesets for repository #{url}" if logger && logger.debug?
66
        identifier_from = db_revision + 1
67
        while (identifier_from <= scm_revision)
68
          # loads changesets by batches of 100
69
          identifier_to = [identifier_from + 99, scm_revision].min
70
          revisions = scm.revisions('', identifier_from, identifier_to, :with_paths => true)
71
          transaction do
72
            revisions.each do |revision|
73
              changeset = Changeset.create(:repository => self,
74
                                           :revision => revision.identifier,
75
                                           :scmid => revision.scmid,
76
                                           :committer => revision.author, 
77
                                           :committed_on => revision.time,
78
                                           :comments => revision.message)
79
              
80
              revision.paths.each do |change|
81
                Change.create(:changeset => changeset,
82
                              :action => change[:action],
83
                              :path => change[:path],
84
                              :from_path => change[:from_path],
85
                              :from_revision => change[:from_revision])
86
              end
87
            end
88
          end unless revisions.nil?
89
          identifier_from = identifier_to + 1
90
        end
91
      end
92
    end
45
    return unless scm_info or scm_info.lastrev.nil?
46
    
47
    db_revision = latest_changeset ? latest_changeset.scmid.to_s : 0
48
    scm_revision = scm_info.lastrev.scmid.to_s
49
    # Save ourselves an expensive operation if we're already up to date
50
    scm_revcount = scm.num_revisions
51
    db_revcount = changesets.count
52
    return if scm.num_revisions == changesets.count and db_revision == scm_revision
53
   
54
    lite_revisions = scm.revisions(nil, nil, scm_revision, :lite => true)
55
    return if lite_revisions.nil? or lite_revisions.empty?
56

  
57
    # Find revisions that redmine knows about already
58
    existing_revisions = changesets.find(:all).map!{|c| c.scmid}
59

  
60
    # Clean out revisions that are no longer in Mercurial
61
    Changeset.delete_all(["scmid NOT IN (?) AND repository_id = (?)", lite_revisions.map{|r| r.scmid}, self.id])
62

  
63
    # Subtract revisions that redmine already knows about
64
    lite_revisions.reject!{|r| existing_revisions.include?(r.scmid)}
65
    return if lite_revisions.nil? or lite_revisions.empty?
66
   
67
    # Retrieve full revisions for the remainder
68
    revisions = []
69
    lite_revisions.each {|r| revisions += scm.revisions(nil, r.scmid, r.scmid)}
70
    return if revisions.nil? or revisions.empty?
71

  
72
    # Save the results to the database
73
    revisions.each{|r| r.save(self)} unless revisions.nil?
74
  end
75
  
76
  def latest_changesets(path, rev, limit=10)
77
    revisions = scm.revisions(path, rev, 0, :limit => limit, :lite => true)
78
    return [] if revisions.nil? or revisions.empty?
79

  
80
    changesets.find(
81
      :all, 
82
      :conditions => [
83
        "scmid IN (?)", 
84
        revisions.map!{|c| c.scmid}
85
      ],
86
      :order => 'committed_on DESC'
87
    )
93 88
  end
94 89
end
lib/redmine/scm/adapters/mercurial/hg-template-0.9.5-lite.tmpl
1
changeset = 'This template must be used with --debug option\n'
2
changeset_quiet =  'This template must be used with --debug option\n'
3
changeset_verbose = 'This template must be used with --debug option\n'
4
changeset_debug = '<logentry revision="{rev}" shortnode="{node|short}" node="{node}">\n<author>{author|escape}</author>\n<date>{date|isodate}</date>\n<msg>{desc|escape}</msg>\n{tags}</logentry>\n\n'
5

  
6
tag = '<tag>{tag|escape}</tag>\n'
7
header='<?xml version="1.0" encoding="UTF-8" ?>\n<log>\n\n'
8
# footer="</log>"
lib/redmine/scm/adapters/mercurial/hg-template-0.9.5.tmpl
1 1
changeset = 'This template must be used with --debug option\n'
2 2
changeset_quiet =  'This template must be used with --debug option\n'
3 3
changeset_verbose = 'This template must be used with --debug option\n'
4
changeset_debug = '<logentry revision="{rev}" node="{node|short}">\n<author>{author|escape}</author>\n<date>{date|isodate}</date>\n<paths>\n{files}{file_adds}{file_dels}{file_copies}</paths>\n<msg>{desc|escape}</msg>\n{tags}</logentry>\n\n'
4
changeset_debug = '<logentry revision="{rev}" shortnode="{node|short}" node="{node}">\n<author>{author|escape}</author>\n<date>{date|isodate}</date>\n<paths>\n{files}{file_adds}{file_dels}{file_copies}</paths>\n<msg>{desc|escape}</msg>\n{tags}</logentry>\n\n'
5 5

  
6 6
file = '<path action="M">{file|escape}</path>\n'
7 7
file_add = '<path action="A">{file_add|escape}</path>\n'
lib/redmine/scm/adapters/mercurial/hg-template-1.0-lite.tmpl
1
changeset = 'This template must be used with --debug option\n'
2
changeset_quiet =  'This template must be used with --debug option\n'
3
changeset_verbose = 'This template must be used with --debug option\n'
4
changeset_debug = '<logentry revision="{rev}" shortnode="{node|short}" node="{node}">\n<author>{author|escape}</author>\n<date>{date|isodate}</date>\n<paths />\n<msg>{desc|escape}</msg>\n{tags}</logentry>\n\n'
5

  
6
tag = '<tag>{tag|escape}</tag>\n'
7
header='<?xml version="1.0" encoding="UTF-8" ?>\n<log>\n\n'
8
# footer="</log>"
lib/redmine/scm/adapters/mercurial/hg-template-1.0.tmpl
1 1
changeset = 'This template must be used with --debug option\n'
2 2
changeset_quiet =  'This template must be used with --debug option\n'
3 3
changeset_verbose = 'This template must be used with --debug option\n'
4
changeset_debug = '<logentry revision="{rev}" node="{node|short}">\n<author>{author|escape}</author>\n<date>{date|isodate}</date>\n<paths>\n{file_mods}{file_adds}{file_dels}{file_copies}</paths>\n<msg>{desc|escape}</msg>\n{tags}</logentry>\n\n'
4
changeset_debug = '<logentry revision="{rev}" shortnode="{node|short}" node="{node}">\n<author>{author|escape}</author>\n<date>{date|isodate}</date>\n<paths>\n{file_mods}{file_adds}{file_dels}{file_copies}</paths>\n<msg>{desc|escape}</msg>\n{tags}</logentry>\n\n'
5 5

  
6 6
file_mod = '<path action="M">{file_mod|escape}</path>\n'
7 7
file_add = '<path action="A">{file_add|escape}</path>\n'
lib/redmine/scm/adapters/mercurial_adapter.rb
21 21
  module Scm
22 22
    module Adapters    
23 23
      class MercurialAdapter < AbstractAdapter
24
        
24

  
25 25
        # Mercurial executable name
26 26
        HG_BIN = "hg"
27 27
        TEMPLATES_DIR = File.dirname(__FILE__) + "/mercurial"
28 28
        TEMPLATE_NAME = "hg-template"
29 29
        TEMPLATE_EXTENSION = "tmpl"
30
        
30

  
31 31
        class << self
32 32
          def client_version
33 33
            @@client_version ||= (hgversion || [])
34 34
          end
35
          
35

  
36 36
          def hgversion  
37 37
            # The hg version is expressed either as a
38 38
            # release number (eg 0.9.5 or 1.0) or as a revision
......
42 42
              theversion.split(".").collect(&:to_i)
43 43
            end
44 44
          end
45
          
45

  
46 46
          def hgversion_from_command_line
47 47
            %x{#{HG_BIN} --version}.match(/\(version (.*)\)/)[1]
48 48
          end
49
          
49

  
50 50
          def template_path
51 51
            @@template_path ||= template_path_for(client_version)
52 52
          end
53
          
54
          def template_path_for(version)
53

  
54
          def lite_template_path
55
            @@lite_template_path ||= template_path_for(client_version,'lite')
56
          end
57

  
58
          def template_path_for(version,style=nil)
55 59
            if ((version <=> [0,9,5]) > 0) || version.empty?
56 60
              ver = "1.0"
57 61
            else
58 62
              ver = "0.9.5"
59 63
            end
60
            "#{TEMPLATES_DIR}/#{TEMPLATE_NAME}-#{ver}.#{TEMPLATE_EXTENSION}"
64
            if style
65
              tmpl = "#{TEMPLATES_DIR}/#{TEMPLATE_NAME}-#{ver}-#{style}.#{TEMPLATE_EXTENSION}"
66
            else
67
            	tmpl = "#{TEMPLATES_DIR}/#{TEMPLATE_NAME}-#{ver}.#{TEMPLATE_EXTENSION}"
68
            end
69
            tmpl
70
          end
71
        end
72

  
73
        def default_branch
74
          @default_branch ||= 'tip'
75
        end
76

  
77
        def branches
78
          @branches ||= get_branches
79
        end
80

  
81
        def get_branches
82
          branches = []
83
          cmd = "#{HG_BIN} -R #{target('')} branches"
84
          shellout(cmd) do |io|
85
            io.each_line do |line|
86
              branches << line.chomp.match('^([^\s]+).*$')[1]
87
            end
88
          end
89
          branches.sort!
90
        end
91

  
92
        def tags
93
          @tags ||= get_tags
94
        end
95

  
96
        def get_tags
97
          tags = []
98
          cmd = "#{HG_BIN} -R #{target('')} tags"
99
          shellout(cmd) do |io|
100
            io.each_line do |line|
101
              tags << line.chomp.match('^([\w]+).*$')[1]
102
            end
61 103
          end
104
          tags.sort!
105
        end
106

  
107
        def tip
108
          @tip ||= get_tip
62 109
        end
63 110
        
111
        def get_tip
112
          tip = nil
113
          cmd = "#{HG_BIN} -R #{target('')} tip"
114
          shellout(cmd) do |io|
115
            tip = io.gets.chomp.match('^changeset:\s+\d+:(\w+)$')[1]
116
          end
117
          return nil if $? && $?.exitstatus != 0
118
          tip
119
        end
120

  
64 121
        def info
65 122
          cmd = "#{HG_BIN} -R #{target('')} root"
66 123
          root_url = nil
......
69 126
          end
70 127
          return nil if $? && $?.exitstatus != 0
71 128
          info = Info.new({:root_url => root_url.chomp,
72
                            :lastrev => revisions(nil,nil,nil,{:limit => 1}).last
129
                            :lastrev => lastrev(nil,tip)
73 130
                          })
74 131
          info
75 132
        rescue CommandFailed
76 133
          return nil
77 134
        end
78 135
        
79
        def entries(path=nil, identifier=nil)
136
        def lastrev(path=nil, identifier=nil)
137
          lastrev = revisions(path,identifier,0,:limit => 1, :lite => true)
138
          return nil if lastrev.nil? or lastrev.empty?
139
          lastrev.last
140
        end
141

  
142
        def num_revisions
143
          cmd = "#{HG_BIN} -R #{target('')} log -r :tip --template='\n' | wc -l"
144
          shellout(cmd) {|io| io.gets.chomp.to_i}
145
        end
146
        
147
        # Returns the entry identified by path and revision identifier
148
        # or nil if entry doesn't exist in the repository
149
        def entry(path=nil, identifier=nil)
150
          parts = path.to_s.split(%r{[\/\\]}).select {|n| !n.blank?}
151
          search_path = parts[0..-2].join('/')
152
          search_name = parts[-1]
153
          if search_path.blank? && search_name.blank?
154
            # Root entry
155
            Entry.new(:path => '', :kind => 'dir')
156
          else
157
            # Search for the entry in the parent directory
158
            es = entries(search_path, identifier, :search => search_name)
159
            es ? es.detect {|e| e.name == search_name} : nil
160
          end
161
        end
162

  
163
        def entries(path=nil, identifier=nil, options={})  
80 164
          path ||= ''
165
          identifier ||= 'tip'
81 166
          entries = Entries.new
82 167
          cmd = "#{HG_BIN} -R #{target('')} --cwd #{target('')} locate"
83
          cmd << " -r " + (identifier ? identifier.to_s : "tip")
168
          cmd << " -r #{shell_quote(identifier.to_s)}"
169
          cmd << " -I" if options[:search] unless path.empty?
84 170
          cmd << " " + shell_quote("path:#{path}") unless path.empty?
171
          cmd << " " + shell_quote(options[:search]) if options[:search]
85 172
          shellout(cmd) do |io|
86 173
            io.each_line do |line|
87 174
              # HG uses antislashs as separator on Windows
......
89 176
              if path.empty? or e = line.gsub!(%r{^#{with_trailling_slash(path)}},'')
90 177
                e ||= line
91 178
                e = e.chomp.split(%r{[\/\\]})
179
                k = (e.size > 1 ? 'dir' : 'file')
180
                p = (path.nil? or path.empty? ? e.first : "#{with_trailling_slash(path)}#{e.first}")
181
                # Always set the file size if we have the 'size' extension for 
182
                # Mercurial, otherwise set from the filesystem if we're browsing 
183
                # the default 'branch' (tip)
184
                s = nil
185
                if (k == 'file')
186
                  s = size(p,identifier)
187
                  if s.nil? and (identifier.to_s == default_branch or identifier.to_s == 'tip')
188
                    full_path = info.root_url + '/' + p
189
                    s = File.stat(full_path).size if File.file?(full_path)
190
                  end
191
                end
92 192
                entries << Entry.new({:name => e.first,
93
                                       :path => (path.nil? or path.empty? ? e.first : "#{with_trailling_slash(path)}#{e.first}"),
94
                                       :kind => (e.size > 1 ? 'dir' : 'file'),
95
                                       :lastrev => Revision.new
193
                                       :path => p,
194
                                       :kind => k,
195
                                       :size => s,
196
                                       :lastrev => lastrev(p,identifier)
96 197
                                     }) unless e.empty? || entries.detect{|entry| entry.name == e.first}
97 198
              end
98 199
            end
......
100 201
          return nil if $? && $?.exitstatus != 0
101 202
          entries.sort_by_name
102 203
        end
103
        
204

  
104 205
        # Fetch the revisions by using a template file that 
105 206
        # makes Mercurial produce a xml output.
106 207
        def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})  
107 208
          revisions = Revisions.new
108
          cmd = "#{HG_BIN} --debug --encoding utf8 -R #{target('')} log -C --style #{shell_quote self.class.template_path}"
209
          cmd = "#{HG_BIN} --debug --encoding utf8 -R #{target('')} --cwd #{target('')} log"
210
          if options[:lite]
211
            cmd << " --style #{shell_quote self.class.lite_template_path}" 
212
          else
213
          	cmd << " -C --style #{shell_quote self.class.template_path}"
214
          end
109 215
          if identifier_from && identifier_to
110
            cmd << " -r #{identifier_from.to_i}:#{identifier_to.to_i}"
216
            cmd << " -r #{shell_quote(identifier_from.to_s)}:#{shell_quote(identifier_to.to_s)}"
111 217
          elsif identifier_from
112
            cmd << " -r #{identifier_from.to_i}:"
218
            cmd << " -r #{shell_quote(identifier_from.to_s)}:"
219
          elsif identifier_to
220
            cmd << " -r :#{shell_quote(identifier_to.to_s)}"
113 221
          end
114 222
          cmd << " --limit #{options[:limit].to_i}" if options[:limit]
115 223
          cmd << " #{path}" if path
116 224
          shellout(cmd) do |io|
117 225
            begin
118 226
              # HG doesn't close the XML Document...
119
              doc = REXML::Document.new(io.read << "</log>")
227
              output = io.read
228
              return nil if output.empty?
229
              doc = REXML::Document.new(output << "</log>")
120 230
              doc.elements.each("log/logentry") do |logentry|
121 231
                paths = []
122 232
                copies = logentry.get_elements('paths/path-copied')
......
124 234
                  # Detect if the added file is a copy
125 235
                  if path.attributes['action'] == 'A' and c = copies.find{ |e| e.text == path.text }
126 236
                    from_path = c.attributes['copyfrom-path']
127
                    from_rev = logentry.attributes['revision']
237
                    from_rev = logentry.attributes['shortnode']
128 238
                  end
129 239
                  paths << {:action => path.attributes['action'],
130 240
                    :path => "/#{path.text}",
......
132 242
                    :from_revision => from_rev ? from_rev : nil
133 243
                  }
134 244
                end
135
                paths.sort! { |x,y| x[:path] <=> y[:path] }
136
                
137
                revisions << Revision.new({:identifier => logentry.attributes['revision'],
245
                paths.sort! { |x,y| x[:path] <=> y[:path] } unless paths.empty?
246

  
247
                revisions << Revision.new({:identifier => logentry.attributes['shortnode'],
138 248
                                            :scmid => logentry.attributes['node'],
139 249
                                            :author => (logentry.elements['author'] ? logentry.elements['author'].text : ""),
140 250
                                            :time => Time.parse(logentry.elements['date'].text).localtime,
......
149 259
          return nil if $? && $?.exitstatus != 0
150 260
          revisions
151 261
        end
152
        
262

  
153 263
        def diff(path, identifier_from, identifier_to=nil)
154 264
          path ||= ''
155 265
          if identifier_to
156
            identifier_to = identifier_to.to_i 
266
            cmd = "#{HG_BIN} -R #{target('')} diff -r #{shell_quote(identifier_to.to_s)} -r #{shell_quote(identifier_from.to_s)} --nodates"
157 267
          else
158
            identifier_to = identifier_from.to_i - 1
268
            cmd = "#{HG_BIN} -R #{target('')} diff -c #{identifier_from} --nodates"
159 269
          end
160
          cmd = "#{HG_BIN} -R #{target('')} diff -r #{identifier_to} -r #{identifier_from} --nodates"
161 270
          cmd << " -I #{target(path)}" unless path.empty?
162 271
          diff = []
163 272
          shellout(cmd) do |io|
......
168 277
          return nil if $? && $?.exitstatus != 0
169 278
          diff
170 279
        end
171
        
280

  
172 281
        def cat(path, identifier=nil)
173 282
          cmd = "#{HG_BIN} -R #{target('')} cat"
174
          cmd << " -r " + (identifier ? identifier.to_s : "tip")
175
          cmd << " #{target(path)}"
283
          cmd << " -r " + shell_quote((identifier ? identifier.to_s : "tip"))
284
          cmd << " #{target(path)}" unless path.empty?
176 285
          cat = nil
177 286
          shellout(cmd) do |io|
178 287
            io.binmode
......
181 290
          return nil if $? && $?.exitstatus != 0
182 291
          cat
183 292
        end
184
        
293

  
294
        def size(path, identifier=nil)
295
          cmd = "#{HG_BIN} --cwd #{target('')} size"
296
          cmd << " -r " + shell_quote((identifier ? identifier.to_s : "tip"))
297
          cmd << " #{path}" unless path.empty?
298
          size = nil
299
          shellout(cmd) do |io|
300
            size = io.read
301
          end
302
          return nil if $? && $?.exitstatus != 0
303
          size.to_i
304
        end
305

  
185 306
        def annotate(path, identifier=nil)
186 307
          path ||= ''
187 308
          cmd = "#{HG_BIN} -R #{target('')}"
188
          cmd << " annotate -n -u"
189
          cmd << " -r " + (identifier ? identifier.to_s : "tip")
190
          cmd << " -r #{identifier.to_i}" if identifier
191
          cmd << " #{target(path)}"
309
          cmd << " annotate -c -u"
310
          cmd << " -r #{shell_quote(identifier.to_s)}" if identifier
311
          cmd << " #{target(path)}" unless path.empty?
192 312
          blame = Annotate.new
193 313
          shellout(cmd) do |io|
194 314
            io.each_line do |line|
195
              next unless line =~ %r{^([^:]+)\s(\d+):(.*)$}
196
              blame.add_line($3.rstrip, Revision.new(:identifier => $2.to_i, :author => $1.strip))
315
              next unless line =~ %r{^([^:]+)\s(\w+):(.*)$}
316
              blame.add_line($3.rstrip, Revision.new(:identifier => $2.to_s, :author => $1.strip))
197 317
            end
198 318
          end
199 319
          return nil if $? && $?.exitstatus != 0
(1-1/24)