Feature #4455 » hg-overhaul-0.9.3.patch
| .gitignore | ||
|---|---|---|
| 17 | 17 | /tmp/sockets/* | 
| 18 | 18 | /tmp/test/* | 
| 19 | 19 | /vendor/rails | 
| 20 | ||
| 21 | /.hg/ | |
| 22 | /.hgignore | |
| 23 | /.hgtags | |
| app/helpers/repositories_helper.rb | ||
|---|---|---|
| 158 | 158 | def darcs_field_tags(form, repository) | 
| 159 | 159 |       content_tag('p', form.text_field(:url, :label => 'Root directory', :size => 60, :required => true, :disabled => (repository && !repository.new_record?))) | 
| 160 | 160 | end | 
| 161 |  | |
| 161 | ||
| 162 | 162 | def mercurial_field_tags(form, repository) | 
| 163 |       content_tag('p', form.text_field(:url, :label => 'Root directory', :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?))) | |
| 163 |       content_tag('p', form.text_field( | |
| 164 | :url, | |
| 165 | :label => 'Root directory', | |
| 166 | :size => 60, | |
| 167 | :required => true, | |
| 168 | ## Mercurial repository is removable. | |
| 169 | # :disabled => (repository && !repository.root_url.blank?) | |
| 170 | :disabled => false | |
| 171 | )) | |
| 164 | 172 | end | 
| 165 | 173 | |
| 166 | 174 | def git_field_tags(form, repository) | 
| 167 |       content_tag('p', form.text_field(:url, :label => 'Path to .git directory', :size => 60, :required => true, :disabled => (repository && !repository.root_url.blank?))) | |
| 175 |       content_tag('p', form.text_field( | |
| 176 | :url, | |
| 177 | :label => 'Path to .git directory', | |
| 178 | :size => 60, | |
| 179 | :required => true, | |
| 180 | :disabled => (repository && !repository.root_url.blank?) | |
| 181 | )) | |
| 168 | 182 | end | 
| 169 | 183 | |
| 170 | 184 | def cvs_field_tags(form, repository) | 
| app/models/repository.rb | ||
|---|---|---|
| 17 | 17 | |
| 18 | 18 | class Repository < ActiveRecord::Base | 
| 19 | 19 | belongs_to :project | 
| 20 |   has_many :changesets, :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC" | |
| 20 |   has_many :changesets, :order => "#{Changeset.table_name}.scm_order DESC, #{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC" | |
| 21 | 21 | has_many :changes, :through => :changesets | 
| 22 | 22 |  | 
| 23 | 23 | # Raw SQL to delete changesets and changes in the database | 
| ... | ... | |
| 96 | 96 | def find_changeset_by_name(name) | 
| 97 | 97 | changesets.find(:first, :conditions => (name.match(/^\d*$/) ? ["revision = ?", name.to_s] : ["revision LIKE ?", name + '%'])) | 
| 98 | 98 | end | 
| 99 |  | |
| 99 | ||
| 100 | 100 | def latest_changeset | 
| 101 | 101 | @latest_changeset ||= changesets.find(:first) | 
| 102 | 102 | end | 
| ... | ... | |
| 105 | 105 | # Default behaviour is to search in cached changesets | 
| 106 | 106 | def latest_changesets(path, rev, limit=10) | 
| 107 | 107 | if path.blank? | 
| 108 | # this is defined at "has_many" | |
| 109 |        # :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC", | |
| 108 | 110 | changesets.find(:all, :include => :user, | 
| 109 |                             :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC", | |
| 110 | 111 | :limit => limit) | 
| 111 | 112 | else | 
| 112 | 113 |       changes.find(:all, :include => {:changeset => :user},  | 
| 113 | 114 | :conditions => ["path = ?", path.with_leading_slash], | 
| 114 |                          :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC", | |
| 115 |                          :order => "#{Changeset.table_name}.scm_order DESC, #{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC", | |
| 115 | 116 | :limit => limit).collect(&:changeset) | 
| 116 | 117 | end | 
| 117 | 118 | end | 
| 118 |  | |
| 119 | ||
| 119 | 120 | def scan_changesets_for_issue_ids | 
| 120 | 121 | self.changesets.each(&:scan_comment_for_issue_ids) | 
| 121 | 122 | end | 
| app/models/repository/mercurial.rb | ||
|---|---|---|
| 18 | 18 | require 'redmine/scm/adapters/mercurial_adapter' | 
| 19 | 19 | |
| 20 | 20 | class Repository::Mercurial < Repository | 
| 21 | attr_protected :root_url | |
| 21 |   attr_protected        :root_url | |
| 22 | 22 | validates_presence_of :url | 
| 23 | 23 | |
| 24 | @@limit_check_strip = 100 | |
| 25 | @@num_convert_redmine_0_9 = 20 | |
| 26 | ||
| 24 | 27 | def scm_adapter | 
| 25 | 28 | Redmine::Scm::Adapters::MercurialAdapter | 
| 26 | 29 | end | 
| 27 |  | |
| 30 | ||
| 28 | 31 | def self.scm_name | 
| 29 | 32 | 'Mercurial' | 
| 30 | 33 | end | 
| 31 |  | |
| 34 | ||
| 35 | def branches | |
| 36 | scm.branches | |
| 37 | end | |
| 38 | ||
| 39 | def tags | |
| 40 | scm.tags | |
| 41 | end | |
| 42 | ||
| 32 | 43 | def entries(path=nil, identifier=nil) | 
| 33 | entries=scm.entries(path, identifier) | |
| 44 |     entries=scm.entries(path, identifier,:include_file_revs => true ) | |
| 34 | 45 | if entries | 
| 35 | 46 | 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 | |
| 47 | if entry && entry.lastrev && entry.lastrev.identifier | |
| 48 | rev = changesets.find( | |
| 49 | :first, | |
| 50 | :conditions => [ "revision = ?" , entry.lastrev.identifier ] | |
| 51 | ) | |
| 52 | next if rev | |
| 53 | rev = changesets.find( | |
| 54 | :first, | |
| 55 | :conditions => ["scmid LIKE ?", entry.lastrev.identifier + '%'] | |
| 56 | ) | |
| 57 | entry.lastrev.identifier = rev.revision if rev | |
| 49 | 58 | end | 
| 50 | 59 | end | 
| 51 | 60 | end | 
| 52 | 61 | entries | 
| 53 | 62 | end | 
| 54 | 63 | |
| 64 | # TODO: | |
| 65 | # This logic fails in following case. | |
| 66 | # | |
| 67 | # Before | |
| 68 | # | |
| 69 | # /-C | |
| 70 | # A-------B | |
| 71 | # | |
| 72 | # After | |
| 73 | # | |
| 74 | # /-D | |
| 75 | # A-------B | |
| 76 | # | |
| 77 | # This is very very rare case. | |
| 78 | # | |
| 79 | # For this case, we need to store HEAD info on DB? | |
| 80 | # http://www.redmine.org/issues/4773#note-11 | |
| 81 | # | |
| 55 | 82 | def fetch_changesets | 
| 56 | 83 | 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]) | |
| 84 | # Backout Redmine 0.9.x | |
| 85 | # return unless scm_info or scm_info.lastrev.nil? | |
| 86 | return unless scm_info | |
| 87 | ||
| 88 | transaction do | |
| 89 | Changeset.update_all( | |
| 90 | "scm_order = -1" , | |
| 91 | ["repository_id = ? AND scm_order is null", id] | |
| 92 | ) | |
| 93 | end | |
| 94 | ||
| 95 | identifier_from = 0 | |
| 96 | ||
| 97 | transaction do | |
| 98 | tip_on_db = changesets.find(:first, :order => 'scm_order DESC') | |
| 99 | tip_on_db = convert_changeset(tip_on_db) if ( tip_on_db && ( tip_on_db.scm_order == -1 ) ) | |
| 100 | tip_revno_on_db = -1 | |
| 101 | hg_revno_dbtip = -1 | |
| 102 | if tip_on_db | |
| 103 | tip_revno_on_db = tip_on_db.scm_order | |
| 104 | revs = scm.revisions( nil, tip_on_db.scmid, tip_on_db.scmid, | |
| 105 | :lite => true) | |
| 106 | hg_revno_dbtip = revs.first.scm_order if revs && revs.first | |
| 107 | end | |
| 108 | ||
| 109 | # Redmine cannot check changeset.count == scm.num_revisions | |
| 110 | # because Redmine ver.0.9.x has stripped revision on DB | |
| 111 | # and 'revision' is uniq. | |
| 112 | if ( tip_revno_on_db == hg_revno_dbtip ) | |
| 113 | identifier_from = tip_revno_on_db + 1 | |
| 114 | else | |
| 115 | # At Redmine SVN r3394, git check history editing for only one week. | |
| 116 | # Mercurial has revision number, | |
| 117 | # so Redmine can check from big revision number. | |
| 118 | # And strip revision in middle of history on shared repository | |
| 119 | # is very rare case. | |
| 120 | ver09x_changeset_first = | |
| 121 | changesets.find(:first,:conditions =>[ "scm_order = -1" ] ) | |
| 122 | if ( ver09x_changeset_first ) | |
| 123 | convert_changesets(@@num_convert_redmine_0_9) | |
| 124 | end | |
| 125 | converted_cs = nil | |
| 126 | flag_loop_break = false | |
| 127 | convert_limit = @@limit_check_strip | |
| 128 | changesets.find( | |
| 129 | :all, | |
| 130 | :order => 'scm_order DESC', | |
| 131 | :limit => convert_limit).each do |cs| | |
| 132 | prev_no = cs.scm_order | |
| 133 | converted_cs = convert_changeset(cs) | |
| 134 | if ( converted_cs && converted_cs.scm_order ) | |
| 135 | if ( converted_cs.scm_order == prev_no ) | |
| 136 | changesets.find(:all,:conditions => | |
| 137 | [ "scm_order = ? AND scmid != ?" , cs.scm_order , cs.scmid ] | |
| 138 | ).each do |cs1| | |
| 139 | convert_changeset(cs1) | |
| 140 | end | |
| 141 | flag_loop_break = true | |
| 142 | break | |
| 143 | end | |
| 144 | end | |
| 145 | end | |
| 146 | if ( flag_loop_break ) | |
| 147 | identifier_from = converted_cs.scm_order + 1 | |
| 148 | else | |
| 149 | identifier_from = [scm.num_revisions - convert_limit , 0].max | |
| 150 | end | |
| 151 | end | |
| 152 | end | |
| 153 | ||
| 154 | scm_revision = scm.num_revisions - 1 | |
| 155 | # Reffered from Subversion logic. | |
| 156 | while (identifier_from <= scm_revision) | |
| 157 | transaction do | |
| 158 | identifier_to = [identifier_from + 19, scm_revision].min | |
| 159 | revisions = scm.revisions( | |
| 160 | nil, identifier_from, identifier_to ) | |
| 161 | if revisions | |
| 162 | revisions.each do |rev| | |
| 163 | dups = changesets.find( | |
| 164 | :all, | |
| 165 | :conditions =>["scmid = ? or scmid = ? " , | |
| 166 | rev.scmid , rev.identifier] | |
| 167 | ) | |
| 168 | unless dups.empty? | |
| 169 | dups.each do |cs1| | |
| 170 | ## There is no way to store | |
| 171 | ## Redmine 0.9.x original revision number. | |
| 172 | cs1.scmid = rev.scmid | |
| 173 | cs1.scm_order = rev.scm_order.to_i | |
| 174 | cs1.save | |
| 175 | end | |
| 176 | next | |
| 86 | 177 | end | 
| 178 | rev.save(self) | |
| 87 | 179 | end | 
| 88 |           end unless revisions.nil? | |
| 180 | end | |
| 89 | 181 | identifier_from = identifier_to + 1 | 
| 90 | 182 | end | 
| 183 | end | |
| 184 | end | |
| 185 | ||
| 186 | # TODO: | |
| 187 | # 1. Mercurial has Named branches. | |
| 188 | # http://mercurial.selenic.com/wiki/NamedBranches | |
| 189 | # a) | |
| 190 | # Mercurial has --only-branch option. | |
| 191 | # But, this is show this branch only. | |
| 192 | # TortoiseHg 0.9 Repository Explorer is different. | |
| 193 | # How does TortoiseHg handle branches? | |
| 194 | # b) | |
| 195 | # If no changeset on this named branch, | |
| 196 | # no revisons show on repository tab. | |
| 197 | # c) | |
| 198 | # Mercurial version 1.2 introduced the ability to close a branch. | |
| 199 | # http://mercurial.selenic.com/wiki/PruningDeadBranches#Closing_branches | |
| 200 | # | |
| 201 | # 2. | |
| 202 | # If Setting.autofetch_changesets is off, | |
| 203 | # no revisons show on repository tab. | |
| 204 | # | |
| 205 | def latest_changesets(path, rev, limit=10) | |
| 206 | branch = nil | |
| 207 | if branches.index(rev) | |
| 208 | branch = rev | |
| 209 | end | |
| 210 | revisions = scm.revisions( | |
| 211 | path, rev, 0, :limit => limit, | |
| 212 | :lite => true , :branch => branch | |
| 213 | ) | |
| 214 | ||
| 215 | return [] if revisions.nil? or revisions.empty? | |
| 216 | ||
| 217 | # Redmine 0.9.x changeset scmid is short id. | |
| 218 | changesets.find( | |
| 219 | :all, | |
| 220 | :conditions => [ | |
| 221 | "scmid IN (?) or scmid IN (?)", | |
| 222 |         # revisions.map!{|c| c.scmid} , | |
| 223 |         revisions.map{|c| c.scmid} , | |
| 224 |         revisions.map{|c| c.identifier} | |
| 225 | ] | |
| 226 | ) | |
| 227 | end | |
| 228 | ||
| 229 | def diff(path, rev, rev_to) | |
| 230 | from_scmid = rev | |
| 231 | if ( rev =~ /^\d*$/ ) | |
| 232 | from_rev = find_changeset_by_name(rev) | |
| 233 | from_scmid = from_rev.scmid if from_rev | |
| 234 | end | |
| 235 | return nil if from_scmid.nil? | |
| 236 | to_scmid = rev_to | |
| 237 | if ( rev_to =~ /^\d*$/ ) | |
| 238 | to_rev = find_changeset_by_name(rev_to) | |
| 239 | to_scmid = to_rev.scmid if to_rev | |
| 240 | end | |
| 241 | scm.diff(path, from_scmid, to_scmid) | |
| 242 | end | |
| 243 | ||
| 244 | # Redmine 0.9.x has revision with revision number. | |
| 245 | # TODO: | |
| 246 | # In case of strip, revision number is renumberd, | |
| 247 | # this logic fails? | |
| 248 | # We need to test with fixtures? | |
| 249 | # And tag and branch can use '\d'??? | |
| 250 | def find_changeset_by_name(name) | |
| 251 | if name | |
| 252 | changesets.find(:first, :conditions => (name.match(/^\d*$/) ? ["revision = ?", name.to_s] : ["scmid LIKE ?", name + '%'])) | |
| 253 | else | |
| 254 | nil | |
| 255 | end | |
| 256 | end | |
| 257 | ||
| 258 | # This method is not used now. | |
| 259 | # But, I plan to use in rake task. | |
| 260 | def convert_changesets_all | |
| 261 | changesets.find_each( | |
| 262 | :conditions => [ "scm_order = -1 or scm_order is null" ], | |
| 263 | :batch_size => 100) do |cs| | |
| 264 | convert_changeset(cs) | |
| 265 | end | |
| 266 | end | |
| 267 | ||
| 268 | def convert_changesets(limit=10) | |
| 269 | changesets.find( | |
| 270 | :all, | |
| 271 | :conditions =>[ "scm_order = -1 or scm_order is null" ], | |
| 272 | :limit => limit).each do |cs| | |
| 273 | convert_changeset(cs) | |
| 274 | end | |
| 275 | end | |
| 276 | ||
| 277 | # Mercurial revision number is sequential from 0. | |
| 278 | # And Mercurial has multipile heads. | |
| 279 | # If one head in middle of history was stripped, | |
| 280 | # revision number was renumbered. | |
| 281 | # In this case, Redmine 0.9.x had duplicate scmid on DB. | |
| 282 | # | |
| 283 | # Because revision is uniq on table, convert fails. | |
| 284 | # And "rNN" is written in Wiki, issue message, etc. | |
| 285 | def convert_changeset(cs) | |
| 286 | ret = cs | |
| 287 | revs = scm.revisions(nil, cs.scmid, cs.scmid, :lite => true) | |
| 288 | rev = nil | |
| 289 | rev = revs.first if revs | |
| 290 | if rev | |
| 291 | ret = convert_changeset_with_revision(cs, rev) | |
| 292 | else | |
| 293 | cs.delete | |
| 294 | ret = nil | |
| 295 | end | |
| 296 | ret | |
| 297 | end | |
| 298 | ||
| 299 | def convert_changeset_with_revision(cs, rev) | |
| 300 | cs.scm_order = rev.scm_order.to_i | |
| 301 | if false | |
| 302 | dup_first1 = changesets.find( | |
| 303 | :first, | |
| 304 | :conditions =>["revision = ?" , rev.identifier ] | |
| 305 | ) | |
| 306 | if dup_first1.nil? | |
| 307 | then | |
| 308 | ## There is no way to store | |
| 309 | ## Redmine 0.9.x original revision number. | |
| 310 | # cs.revision = rev.identifier if cs.revision != rev.identifier | |
| 91 | 311 | end | 
| 92 | 312 | end | 
| 313 | dup_first = changesets.find( | |
| 314 | :first, | |
| 315 | :conditions =>["scmid = ?" , rev.scmid ] | |
| 316 | ) | |
| 317 | if dup_first.nil? | |
| 318 | then | |
| 319 | cs.scmid = rev.scmid | |
| 320 | end | |
| 321 | ||
| 322 | cs.save | |
| 323 | ret = cs | |
| 324 | ret | |
| 93 | 325 | end | 
| 94 | 326 | end | 
| app/views/repositories/revision.rhtml | ||
|---|---|---|
| 21 | 21 | |
| 22 | 22 | <h2><%= l(:label_revision) %> <%= format_revision(@changeset.revision) %></h2> | 
| 23 | 23 | |
| 24 | <p><% if @changeset.scmid %>ID: <%= @changeset.scmid %><br /><% end %> | |
| 24 | <p> | |
| 25 | <% if @changeset.scmid %>ID: <%= @changeset.scmid %><br /><% end %> | |
| 26 | <% if @changeset.scm_order %>SCM Order: <%= @changeset.scm_order %><br /><% end %> | |
| 25 | 27 | <span class="author"><%= authoring(@changeset.committed_on, @changeset.author) %></span></p> | 
| 26 | 28 | |
| 27 | 29 | <%= textilizable @changeset.comments %> | 
| db/migrate/20100222000000_add_scm_order_to_changesets.rb | ||
|---|---|---|
| 1 | ||
| 2 | class AddScmOrderToChangesets < ActiveRecord::Migration | |
| 3 | def self.up | |
| 4 | add_column :changesets, :scm_order, :integer, :null => true | |
| 5 | add_index :changesets, :scm_order | |
| 6 | end | |
| 7 | ||
| 8 | def self.down | |
| 9 | remove_index :changesets, :scm_order | |
| 10 | remove_column :changesets, :scm_order | |
| 11 | end | |
| 12 | end | |
| lib/redmine/scm/adapters/abstract_adapter.rb | ||
|---|---|---|
| 269 | 269 | }.last | 
| 270 | 270 | end | 
| 271 | 271 | end | 
| 272 |  | |
| 272 | ||
| 273 | ||
| 273 | 274 | class Revision | 
| 274 | attr_accessor :identifier, :scmid, :name, :author, :time, :message, :paths, :revision, :branch | |
| 275 |         attr_accessor :identifier, :scmid, :name, :author, :time, :message, :paths, :revision, :branch, :scm_order | |
| 275 | 276 | |
| 276 | 277 |         def initialize(attributes={}) | 
| 277 | 278 | self.identifier = attributes[:identifier] | 
| ... | ... | |
| 283 | 284 | self.paths = attributes[:paths] | 
| 284 | 285 | self.revision = attributes[:revision] | 
| 285 | 286 | self.branch = attributes[:branch] | 
| 287 | self.scm_order = attributes[:scm_order] | |
| 286 | 288 | end | 
| 287 | 289 | |
| 288 | 290 | def save(repo) | 
| ... | ... | |
| 293 | 295 | :scmid => scmid, | 
| 294 | 296 | :committer => author, | 
| 295 | 297 | :committed_on => time, | 
| 296 | :comments => message) | |
| 297 |  | |
| 298 | :comments => message , | |
| 299 | :scm_order => scm_order | |
| 300 | ) | |
| 298 | 301 | if changeset.save | 
| 299 | 302 | paths.each do |file| | 
| 300 | 303 | Change.create( | 
| ... | ... | |
| 306 | 309 | end | 
| 307 | 310 | end | 
| 308 | 311 | end | 
| 309 |  | |
| 312 | ||
| 310 | 313 | class Annotate | 
| 311 | 314 | attr_reader :lines, :revisions | 
| 312 | 315 |  | 
| 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 | ||
|---|---|---|
| 16 | 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | 
| 17 | 17 | |
| 18 | 18 | require 'redmine/scm/adapters/abstract_adapter' | 
| 19 | require 'rexml/document' | |
| 19 | 20 | |
| 20 | 21 | module Redmine | 
| 21 | 22 | module Scm | 
| 22 |     module Adapters     | |
| 23 | module Adapters | |
| 23 | 24 | class MercurialAdapter < AbstractAdapter | 
| 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 | @@limit_include_file_revs = 20 | |
| 33 | @@has_size_ext = true | |
| 34 | ||
| 32 | 35 | def client_version | 
| 33 | @@client_version ||= (hgversion || []) | |
| 36 |             @@client_version        ||= (hgversion || []) | |
| 34 | 37 | end | 
| 35 |  | |
| 36 | def hgversion | |
| 38 | ||
| 39 | # TODO: | |
| 40 | # Mercurial version 1.2 introduced the ability to close a branch. | |
| 41 | # http://mercurial.selenic.com/wiki/PruningDeadBranches#Closing_branches | |
| 42 | def hgversion | |
| 37 | 43 | # The hg version is expressed either as a | 
| 38 | 44 | # release number (eg 0.9.5 or 1.0) or as a revision | 
| 39 | 45 | # id composed of 12 hexa characters. | 
| ... | ... | |
| 42 | 48 |               theversion.split(".").collect(&:to_i) | 
| 43 | 49 | end | 
| 44 | 50 | end | 
| 45 |  | |
| 51 | ||
| 46 | 52 | def hgversion_from_command_line | 
| 47 | 53 |             %x{#{HG_BIN} --version}.match(/\(version (.*)\)/)[1] | 
| 48 | 54 | end | 
| 49 |  | |
| 55 | ||
| 50 | 56 | def template_path | 
| 51 | 57 | @@template_path ||= template_path_for(client_version) | 
| 52 | 58 | end | 
| 53 |  | |
| 54 | def template_path_for(version) | |
| 59 | ||
| 60 | def lite_template_path | |
| 61 | @@lite_template_path ||= template_path_for(client_version,'lite') | |
| 62 | end | |
| 63 | ||
| 64 | def template_path_for(version,style=nil) | |
| 55 | 65 | if ((version <=> [0,9,5]) > 0) || version.empty? | 
| 56 | 66 | ver = "1.0" | 
| 57 | 67 | else | 
| 58 | 68 | ver = "0.9.5" | 
| 59 | 69 | end | 
| 60 |             "#{TEMPLATES_DIR}/#{TEMPLATE_NAME}-#{ver}.#{TEMPLATE_EXTENSION}" | |
| 70 | if style | |
| 71 |               tmpl = "#{TEMPLATES_DIR}/#{TEMPLATE_NAME}-#{ver}-#{style}.#{TEMPLATE_EXTENSION}" | |
| 72 | else | |
| 73 |               tmpl = "#{TEMPLATES_DIR}/#{TEMPLATE_NAME}-#{ver}.#{TEMPLATE_EXTENSION}" | |
| 74 | end | |
| 75 | tmpl | |
| 76 | end | |
| 77 | end | |
| 78 | ||
| 79 | # Mercurial default branch is "default". | |
| 80 | # But, Mercurial has multipile heads. | |
| 81 | def default_branch | |
| 82 | @default_branch ||= 'tip' | |
| 83 | end | |
| 84 | ||
| 85 | def branches | |
| 86 | @branches ||= get_branches | |
| 87 | end | |
| 88 | ||
| 89 | # TODO: | |
| 90 | # Mercurial version 1.2 introduced the ability to close a branch. | |
| 91 | # http://mercurial.selenic.com/wiki/PruningDeadBranches#Closing_branches | |
| 92 | def get_branches | |
| 93 | branches = [] | |
| 94 |           cmd = "#{HG_BIN} -R #{target('')} branches" | |
| 95 | shellout(cmd) do |io| | |
| 96 | io.each_line do |line| | |
| 97 |               branches << line.chomp.match('^([^:]+[^\s]+)[\s]+[\d]+:.*$')[1] | |
| 98 | end | |
| 61 | 99 | end | 
| 100 | branches | |
| 101 | end | |
| 102 | ||
| 103 | def tags | |
| 104 | @tags ||= get_tags | |
| 62 | 105 | end | 
| 63 |  | |
| 106 | ||
| 107 | def get_tags | |
| 108 | tags = [] | |
| 109 |           cmd = "#{HG_BIN} -R #{target('')} tags -v" | |
| 110 | shellout(cmd) do |io| | |
| 111 | io.each_line do |line| | |
| 112 |               strs = line.chomp.match('^([^:]+[^\s]+)[\s]+[\d]+:(.*)$') | |
| 113 | if strs[2] !~ /[\s]+local/ | |
| 114 | tags << strs[1] | |
| 115 | end | |
| 116 | end | |
| 117 | end | |
| 118 | tags | |
| 119 | end | |
| 120 | ||
| 121 | def tip | |
| 122 | @tip ||= get_tip | |
| 123 | end | |
| 124 | ||
| 125 | def get_tip | |
| 126 | tip = nil | |
| 127 |           cmd = "#{HG_BIN} -R #{target('')} id -i -r tip" | |
| 128 | shellout(cmd) do |io| | |
| 129 | tip = io.gets.chomp | |
| 130 | end | |
| 131 | return nil if $? && $?.exitstatus != 0 | |
| 132 | tip | |
| 133 | end | |
| 134 | ||
| 64 | 135 | def info | 
| 65 | 136 |           cmd = "#{HG_BIN} -R #{target('')} root" | 
| 66 | 137 | root_url = nil | 
| 67 | 138 | shellout(cmd) do |io| | 
| 68 | root_url = io.gets | |
| 139 |             root_url = io.gets.chomp | |
| 69 | 140 | end | 
| 70 | 141 | return nil if $? && $?.exitstatus != 0 | 
| 71 |           info = Info.new({:root_url => root_url.chomp, | |
| 72 |                             :lastrev => revisions(nil,nil,nil,{:limit => 1}).last | |
| 73 | }) | |
| 142 | info = Info.new( | |
| 143 |               { | |
| 144 | :root_url => root_url.chomp, | |
| 145 |                 # :lastrev => revisions(nil,nil,nil,{:limit => 1}).last | |
| 146 | }) | |
| 74 | 147 | info | 
| 75 | 148 | rescue CommandFailed | 
| 76 | 149 | return nil | 
| 77 | 150 | end | 
| 78 |  | |
| 79 | def entries(path=nil, identifier=nil) | |
| 151 | ||
| 152 | def lastrev(path=nil, identifier=nil) | |
| 153 | lastrev = revisions(path,identifier,0,:limit => 1, :lite => true) | |
| 154 | return nil if lastrev.nil? or lastrev.empty? | |
| 155 | lastrev.last | |
| 156 | end | |
| 157 | ||
| 158 | def num_revisions | |
| 159 | num = 0 | |
| 160 |           cmd = "#{HG_BIN} -R #{target('')} log -r tip --template=#{shell_quote('{rev}\n')}" | |
| 161 | shellout(cmd) do |io| | |
| 162 | line = io.gets | |
| 163 | if line.nil? | |
| 164 | num = 0 | |
| 165 | else | |
| 166 | num = line.chomp.to_i + 1 | |
| 167 | end | |
| 168 | break | |
| 169 | end | |
| 170 | num | |
| 171 | end | |
| 172 | ||
| 173 |         def entries(path=nil, identifier=nil, options={}) | |
| 80 | 174 | path ||= '' | 
| 81 | 175 | entries = Entries.new | 
| 82 | 176 |           cmd = "#{HG_BIN} -R #{target('')} --cwd #{target('')} locate" | 
| ... | ... | |
| 89 | 183 |               if path.empty? or e = line.gsub!(%r{^#{with_trailling_slash(path)}},'') | 
| 90 | 184 | e ||= line | 
| 91 | 185 |                 e = e.chomp.split(%r{[\/\\]}) | 
| 92 |                 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 | |
| 96 |                                      }) unless e.empty? || entries.detect{|entry| entry.name == e.first} | |
| 186 | unless e.empty? || | |
| 187 |                      entries.detect{|entry| entry.name == e.first} | |
| 188 | kind = (e.size > 1 ? 'dir' : 'file') | |
| 189 |                   ent_path = (path.nil? or path.empty? ? e.first : "#{with_trailling_slash(path)}#{e.first}") | |
| 190 | lastrev = nil | |
| 191 | s = nil | |
| 192 | entries << Entry.new( | |
| 193 |                                     { | |
| 194 | :name => e.first , | |
| 195 | :path => ent_path , | |
| 196 | :kind => kind , | |
| 197 | :size => s , | |
| 198 | :lastrev => lastrev | |
| 199 | } | |
| 200 | ) | |
| 201 | end | |
| 97 | 202 | end | 
| 98 | 203 | end | 
| 99 | 204 | end | 
| 100 | 205 | return nil if $? && $?.exitstatus != 0 | 
| 206 | ||
| 207 | file_cnt = 0 | |
| 208 | entries.each do |ent| | |
| 209 | if ( ent.kind == 'file' ) | |
| 210 | file_cnt += 1 | |
| 211 | end | |
| 212 | end | |
| 213 | entries.each do |ent| | |
| 214 | if ( ent.kind == 'file' ) | |
| 215 | if options[:include_file_revs] && file_cnt < @@limit_include_file_revs | |
| 216 | # Following process is very heavy. | |
| 217 | ent.lastrev = lastrev(ent.path,identifier) | |
| 218 | s = nil | |
| 219 | s = size(path,identifier) if @@has_size_ext | |
| 220 | if s.nil? and (identifier.to_s == default_branch or identifier.to_s == 'tip') | |
| 221 | full_path = info.root_url + '/' + ent.path | |
| 222 | ent.size = File.stat(full_path).size if File.file?(full_path) | |
| 223 | end | |
| 224 | else | |
| 225 | ent.lastrev = Revision.new | |
| 226 | end | |
| 227 | else | |
| 228 | # "hg log -l1 DIR" is VERY VERY HEAVY!! | |
| 229 | ent.lastrev = Revision.new | |
| 230 | end | |
| 231 | end | |
| 232 | ||
| 101 | 233 | entries.sort_by_name | 
| 102 | 234 | end | 
| 103 |  | |
| 235 | ||
| 104 | 236 | # Fetch the revisions by using a template file that | 
| 105 | 237 | # makes Mercurial produce a xml output. | 
| 106 |         def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})   | |
| 238 | # | |
| 239 | # TODO: | |
| 240 | # Mercurial version 1.2 introduced the ability to close a branch. | |
| 241 | # http://mercurial.selenic.com/wiki/PruningDeadBranches#Closing_branches | |
| 242 |         def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}) | |
| 107 | 243 | revisions = Revisions.new | 
| 108 |           cmd = "#{HG_BIN} --debug --encoding utf8 -R #{target('')} log -C --style #{shell_quote self.class.template_path}" | |
| 244 |           cmd = "#{HG_BIN} --debug --encoding utf8 -R #{target('')} --cwd #{target('')} log" | |
| 245 | if options[:lite] | |
| 246 |             cmd << " --style #{shell_quote self.class.lite_template_path}"  | |
| 247 | else | |
| 248 |           	cmd << " -C --style #{shell_quote self.class.template_path}" | |
| 249 | end | |
| 109 | 250 | if identifier_from && identifier_to | 
| 110 |             cmd << " -r #{identifier_from.to_i}:#{identifier_to.to_i}" | |
| 251 |             cmd << " -r #{shell_quote(identifier_from.to_s)}:#{shell_quote(identifier_to.to_s)}" | |
| 111 | 252 | elsif identifier_from | 
| 112 |             cmd << " -r #{identifier_from.to_i}:" | |
| 253 |             cmd << " -r #{shell_quote(identifier_from.to_s)}:" | |
| 254 | elsif identifier_to | |
| 255 |             cmd << " -r :#{shell_quote(identifier_to.to_s)}" | |
| 113 | 256 | end | 
| 114 | 257 |           cmd << " --limit #{options[:limit].to_i}" if options[:limit] | 
| 258 |           cmd << " --only-branch #{options[:branch]}" if options[:branch] | |
| 115 | 259 |           cmd << " #{path}" if path | 
| 116 | 260 | shellout(cmd) do |io| | 
| 117 | 261 | begin | 
| 118 | 262 | # HG doesn't close the XML Document... | 
| 119 | doc = REXML::Document.new(io.read << "</log>") | |
| 263 | output = io.read | |
| 264 | return nil if output.empty? | |
| 265 | doc = REXML::Document.new(output << "</log>") | |
| 120 | 266 |               doc.elements.each("log/logentry") do |logentry| | 
| 121 | 267 | paths = [] | 
| 122 | 268 |                 copies = logentry.get_elements('paths/path-copied') | 
| ... | ... | |
| 124 | 270 | # Detect if the added file is a copy | 
| 125 | 271 |                   if path.attributes['action'] == 'A' and c = copies.find{ |e| e.text == path.text } | 
| 126 | 272 | from_path = c.attributes['copyfrom-path'] | 
| 127 |                     from_rev = logentry.attributes['revision'] | |
| 273 |                     from_rev = logentry.attributes['shortnode'] | |
| 128 | 274 | end | 
| 129 | 275 |                   paths << {:action => path.attributes['action'], | 
| 130 | 276 |                     :path => "/#{path.text}", | 
| ... | ... | |
| 132 | 278 | :from_revision => from_rev ? from_rev : nil | 
| 133 | 279 | } | 
| 134 | 280 | end | 
| 135 |                 paths.sort! { |x,y| x[:path] <=> y[:path] } | |
| 136 |  | |
| 137 |                 revisions << Revision.new({:identifier => logentry.attributes['revision'], | |
| 138 | :scmid => logentry.attributes['node'], | |
| 139 | :author => (logentry.elements['author'] ? logentry.elements['author'].text : ""), | |
| 140 | :time => Time.parse(logentry.elements['date'].text).localtime, | |
| 141 | :message => logentry.elements['msg'].text, | |
| 142 | :paths => paths | |
| 143 | }) | |
| 281 |                 paths.sort! { |x,y| x[:path] <=> y[:path] } unless paths.empty? | |
| 282 | revisions << Revision.new( | |
| 283 |                       { | |
| 284 | :identifier => logentry.attributes['shortnode'], | |
| 285 | :scmid => logentry.attributes['node'], | |
| 286 | :author => (logentry.elements['author'] ? logentry.elements['author'].text : ""), | |
| 287 | :time => Time.parse(logentry.elements['date'].text).localtime, | |
| 288 | :message => logentry.elements['msg'].text, | |
| 289 | :paths => paths, | |
| 290 | :scm_order => logentry.attributes['revision'].to_i , | |
| 291 | } | |
| 292 | ) | |
| 144 | 293 | end | 
| 145 | 294 | rescue | 
| 146 | 295 | logger.debug($!) | 
| ... | ... | |
| 149 | 298 | return nil if $? && $?.exitstatus != 0 | 
| 150 | 299 | revisions | 
| 151 | 300 | end | 
| 152 |  | |
| 301 | ||
| 153 | 302 | def diff(path, identifier_from, identifier_to=nil) | 
| 154 | 303 | path ||= '' | 
| 155 | 304 | if identifier_to | 
| 156 |             identifier_to = identifier_to.to_i  | |
| 305 |             cmd = "#{HG_BIN} -R #{target('')} diff -r #{shell_quote(identifier_to.to_s)} -r #{shell_quote(identifier_from.to_s)} --nodates" | |
| 157 | 306 | else | 
| 158 |             identifier_to = identifier_from.to_i - 1 | |
| 307 |             cmd = "#{HG_BIN} -R #{target('')} diff -c #{identifier_from} --nodates" | |
| 159 | 308 | end | 
| 160 |           cmd = "#{HG_BIN} -R #{target('')} diff -r #{identifier_to} -r #{identifier_from} --nodates" | |
| 161 | 309 |           cmd << " -I #{target(path)}" unless path.empty? | 
| 162 | 310 | diff = [] | 
| 163 | 311 | shellout(cmd) do |io| | 
| ... | ... | |
| 168 | 316 | return nil if $? && $?.exitstatus != 0 | 
| 169 | 317 | diff | 
| 170 | 318 | end | 
| 171 |  | |
| 319 | ||
| 172 | 320 | def cat(path, identifier=nil) | 
| 173 | 321 |           cmd = "#{HG_BIN} -R #{target('')} cat" | 
| 174 |           cmd << " -r " + (identifier ? identifier.to_s : "tip") | |
| 175 |           cmd << " #{target(path)}" | |
| 322 |           cmd << " -r " + shell_quote((identifier ? identifier.to_s : "tip")) | |
| 323 |           cmd << " #{target(path)}" unless path.empty? | |
| 176 | 324 | cat = nil | 
| 177 | 325 | shellout(cmd) do |io| | 
| 178 | 326 | io.binmode | 
| ... | ... | |
| 181 | 329 | return nil if $? && $?.exitstatus != 0 | 
| 182 | 330 | cat | 
| 183 | 331 | end | 
| 184 |  | |
| 332 | ||
| 333 | def size(path, identifier=nil) | |
| 334 | # return nil | |
| 335 | ||
| 336 |           cmd = "#{HG_BIN} --cwd #{target('')} size" | |
| 337 | cmd << " -r " + shell_quote((identifier ? identifier.to_s : "tip")) | |
| 338 |           cmd << " #{path}" unless path.empty? | |
| 339 | size = nil | |
| 340 | shellout(cmd) do |io| | |
| 341 | size = io.read | |
| 342 | end | |
| 343 | return nil if $? && $?.exitstatus != 0 | |
| 344 | size.to_i | |
| 345 | end | |
| 346 | ||
| 347 | # TODO: | |
| 348 | # hg annotate behavior small changes at Ver.1.5. | |
| 349 | # http://mercurial.selenic.com/wiki/UpgradeNotes#A1.5:_Small_behavior_changes | |
| 350 | # hg annotate now follows copies and renames by default, | |
| 351 | # use --no-follow for old behavior. | |
| 185 | 352 | def annotate(path, identifier=nil) | 
| 186 | 353 | path ||= '' | 
| 354 | identifier = 'tip' if identifier.blank? | |
| 187 | 355 |           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)}" | |
| 356 | cmd << " annotate -c -u" | |
| 357 |           cmd << " -r #{shell_quote(identifier.to_s)}" | |
| 358 |           cmd << " #{target(path)}" unless path.empty? | |
| 192 | 359 | blame = Annotate.new | 
| 193 | 360 | shellout(cmd) do |io| | 
| 194 | 361 | 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)) | |
| 362 |               next unless line =~ %r{^([^:]+)\s(\w+):(.*)$} | |
| 363 |               blame.add_line($3.rstrip, Revision.new(:identifier => $2.to_s, :author => $1.strip)) | |
| 197 | 364 | end | 
| 198 | 365 | end | 
| 199 | 366 | return nil if $? && $?.exitstatus != 0 | 
| lib/tasks/testing.rake | ||
|---|---|---|
| 26 | 26 |         system "svnadmin create #{repo_path}" | 
| 27 | 27 |         system "gunzip < test/fixtures/repositories/subversion_repository.dump.gz | svnadmin load #{repo_path}" | 
| 28 | 28 | end | 
| 29 |  | |
| 30 | (supported_scms - [:subversion]).each do |scm| | |
| 29 | ||
| 30 | desc "Creates a test mercurial repository" | |
| 31 | task :mercurial => :create_dir do | |
| 32 | repo_path = "tmp/test/mercurial_repository" | |
| 33 |         system "hg init #{repo_path}" | |
| 34 |         system "cp -f test/fixtures/repositories/mercurial/hgrc #{repo_path}/.hg/hgrc " | |
| 35 |         system "hg -R #{repo_path} pull test/fixtures/repositories/mercurial/default.r0.bundle" | |
| 36 |         system "hg -R #{repo_path} pull test/fixtures/repositories/mercurial/default.r1-r5.bundle" | |
| 37 |         system "hg -R #{repo_path} pull test/fixtures/repositories/mercurial/branch00.r1.bundle" | |
| 38 | ||
| 39 | end | |
| 40 | ||
| 41 | (supported_scms - [:subversion, :mercurial]).each do |scm| | |
| 31 | 42 |         desc "Creates a test #{scm} repository" | 
| 32 | 43 | task scm => :create_dir do | 
| 33 |           system "gunzip < test/fixtures/repositories/#{scm}_repository.tar.gz | tar -xv -C tmp/test" | |
| 44 |           # system "gunzip < test/fixtures/repositories/#{scm}_repository.tar.gz | tar -xv -C tmp/test" | |
| 45 |           system "tar -xvz -C tmp/test -f test/fixtures/repositories/#{scm}_repository.tar.gz" | |
| 34 | 46 | end | 
| 35 | 47 | end | 
| 36 | 48 |  | 
| test/fixtures/repositories/mercurial/hgrc | ||
|---|---|---|
| 1 | ||
| 2 | [ui] | |
| 3 | username = test00 <test00@example.com> | |
| 4 | ||
| 5 | [extensions] | |
| 6 | # MQ extention needs for unit test of check history editing. | |
| 7 | hgext.mq = | |
| 8 | ||
| 9 | # share = | |
| 10 | # hgext.convert = | |
| 11 | # hgext.graphlog = | |
| 12 | # extdiff = | |
| 13 | # hgext.hgk = | |
| 14 | # hgext.bookmarks = | |
| 15 | # rebase= | |
| 16 | # hgext.purge= | |
| 17 | ||
| 18 | # hggit = | |
| 19 | # svn = | |
| 20 | ||
| test/functional/repositories_mercurial_controller_test.rb | ||
|---|---|---|
| 33 | 33 | @response = ActionController::TestResponse.new | 
| 34 | 34 | User.current = nil | 
| 35 | 35 | Repository::Mercurial.create(:project => Project.find(3), :url => REPOSITORY_PATH) | 
| 36 | ||
| 37 |     %x{hg -R #{REPOSITORY_PATH} update null} | |
| 38 |     %x{hg -R #{REPOSITORY_PATH} strip 0} | |
| 39 |     %x{hg -R #{REPOSITORY_PATH} verify} | |
| 40 |     %x{hg -R #{REPOSITORY_PATH} pull test/fixtures/repositories/mercurial/default.r0.bundle} | |
| 41 |     %x{hg -R #{REPOSITORY_PATH} pull test/fixtures/repositories/mercurial/branch00.r1.bundle} | |
| 42 |     %x{hg -R #{REPOSITORY_PATH} pull test/fixtures/repositories/mercurial/default.r1-r5.bundle} | |
| 43 | ||
| 36 | 44 | end | 
| 37 | 45 |  | 
| 38 | 46 | if File.directory?(REPOSITORY_PATH) | 
| ... | ... | |
| 92 | 100 |                  :attributes => { :class => /line-num/ }, | 
| 93 | 101 |                  :sibling => { :tag => 'td', :content => /WITHOUT ANY WARRANTY/ } | 
| 94 | 102 | end | 
| 95 |  | |
| 103 | ||
| 96 | 104 | def test_entry_download | 
| 97 | 105 | get :entry, :id => 3, :path => ['sources', 'watchers_controller.rb'], :format => 'raw' | 
| 98 | 106 | assert_response :success | 
| ... | ... | |
| 100 | 108 |       assert @response.body.include?('WITHOUT ANY WARRANTY') | 
| 101 | 109 | end | 
| 102 | 110 | |
| 111 | # This shows "default branch". | |
| 112 | # git shows "master", but Mercurial shows "tip". | |
| 103 | 113 | def test_directory_entry | 
| 104 | 114 | get :entry, :id => 3, :path => ['sources'] | 
| 105 | 115 | assert_response :success | 
| ... | ... | |
| 107 | 117 | assert_not_nil assigns(:entry) | 
| 108 | 118 | assert_equal 'sources', assigns(:entry).name | 
| 109 | 119 | end | 
| 110 |  | |
| 120 | ||
| 121 | def test_browse_branch | |
| 122 | get :show, :id => 3, :rev => 'branch00' | |
| 123 | assert_response :success | |
| 124 | assert_template 'show' | |
| 125 | assert_not_nil assigns(:entries) | |
| 126 | assert_equal 4, assigns(:entries).size | |
| 127 |       assert assigns(:entries).detect {|e| e.name == 'branch00-dir' && e.kind == 'dir'} | |
| 128 |       assert assigns(:entries).detect {|e| e.name == 'images' && e.kind == 'dir'} | |
| 129 |       assert assigns(:entries).detect {|e| e.name == 'sources' && e.kind == 'dir'} | |
| 130 |       assert assigns(:entries).detect {|e| e.name == 'README' && e.kind == 'file'} | |
| 131 | end | |
| 132 | ||
| 111 | 133 | def test_diff | 
| 112 | 134 | # Full diff of changeset 4 | 
| 113 |       get :diff, :id => 3, :rev => 4 | |
| 135 |       get :diff, :id => 3, :rev => 'def6d2f1254a' | |
| 114 | 136 | assert_response :success | 
| 115 | 137 | assert_template 'diff' | 
| 116 | 138 | # Line 22 removed | 
| ... | ... | |
| 120 | 142 |                                :attributes => { :class => /diff_out/ }, | 
| 121 | 143 | :content => /def remove/ } | 
| 122 | 144 | end | 
| 123 |  | |
| 145 | ||
| 124 | 146 | def test_annotate | 
| 125 | 147 | get :annotate, :id => 3, :path => ['sources', 'watchers_controller.rb'] | 
| 126 | 148 | assert_response :success | 
| ... | ... | |
| 131 | 153 |                  :sibling => { :tag => 'td', :content => /jsmith/ }, | 
| 132 | 154 |                  :sibling => { :tag => 'td', :content => /watcher =/ } | 
| 133 | 155 | end | 
| 156 | ||
| 134 | 157 | else | 
| 135 | 158 | puts "Mercurial test repository NOT FOUND. Skipping functional tests !!!" | 
| 136 | 159 | def test_fake; assert true end | 
| test/unit/repository_mercurial_test.rb | ||
|---|---|---|
| 26 | 26 | def setup | 
| 27 | 27 | @project = Project.find(1) | 
| 28 | 28 | assert @repository = Repository::Mercurial.create(:project => @project, :url => REPOSITORY_PATH) | 
| 29 | ||
| 30 |     %x{hg -R #{REPOSITORY_PATH} update null} | |
| 31 |     %x{hg -R #{REPOSITORY_PATH} strip 0} | |
| 32 |     %x{hg -R #{REPOSITORY_PATH} verify} | |
| 33 | ||
| 34 |     %x{hg -R #{REPOSITORY_PATH} pull test/fixtures/repositories/mercurial/default.r0.bundle} | |
| 35 |     %x{hg -R #{REPOSITORY_PATH} pull test/fixtures/repositories/mercurial/branch00.r1.bundle} | |
| 36 |     %x{hg -R #{REPOSITORY_PATH} pull test/fixtures/repositories/mercurial/default.r1-r5.bundle} | |
| 29 | 37 | end | 
| 30 |  | |
| 31 | if File.directory?(REPOSITORY_PATH) | |
| 38 | ||
| 39 | if File.directory?(REPOSITORY_PATH) | |
| 40 | ||
| 32 | 41 | def test_fetch_changesets_from_scratch | 
| 33 | 42 | @repository.fetch_changesets | 
| 34 | 43 | @repository.reload | 
| 35 |  | |
| 36 |       assert_equal 6, @repository.changesets.count | |
| 37 |       assert_equal 11, @repository.changes.count | |
| 38 |       assert_equal "Initial import.\nThe repository contains 3 files.", @repository.changesets.find_by_revision('0').comments | |
| 44 | ||
| 45 |       assert_equal 7, @repository.changesets.count | |
| 46 |       assert_equal 12, @repository.changes.count | |
| 47 |       assert_equal "Initial import.\nThe repository contains 3 files.", @repository.changesets.find_by_revision('0885933ad4f6').comments | |
| 39 | 48 | end | 
| 40 | 49 |  | 
| 41 | 50 | def test_fetch_changesets_incremental | 
| 42 | 51 | @repository.fetch_changesets | 
| 43 | # Remove changesets with revision > 2 | |
| 44 |       @repository.changesets.find(:all).each {|c| c.destroy if c.revision.to_i > 2} | |
| 52 | ## Mercurial can set commit date, | |
| 53 | # @repository.changesets.find(:all, :order => 'committed_on DESC', :limit => 3).each(&:destroy) | |
| 54 | @repository.changesets.find(:all, :order => 'scm_order DESC', :limit => 3).each(&:destroy) | |
| 55 | ||
| 45 | 56 | @repository.reload | 
| 46 |       assert_equal 3, @repository.changesets.count | |
| 47 |  | |
| 57 |       assert_equal 4, @repository.changesets.count | |
| 58 | ||
| 48 | 59 | @repository.fetch_changesets | 
| 49 |       assert_equal 6, @repository.changesets.count | |
| 60 |       assert_equal 7, @repository.changesets.count | |
| 50 | 61 | end | 
| 51 | 62 |  | 
| 52 | 63 | def test_entries | 
| 53 |       assert_equal 2, @repository.entries("sources", 2).size | |
| 54 |       assert_equal 1, @repository.entries("sources", 3).size | |
| 64 |       %x{hg -R #{REPOSITORY_PATH} up null} | |
| 65 |       assert_equal 2, @repository.entries("sources", '400bb8672109').size | |
| 66 |       assert_equal 1, @repository.entries("sources", 'b3a615152df8').size | |
| 55 | 67 | end | 
| 56 | 68 | |
| 57 | 69 | def test_locate_on_outdated_repository | 
| 58 | 70 | # Change the working dir state | 
| 59 |       %x{hg -R #{REPOSITORY_PATH} up -r 0} | |
| 60 |       assert_equal 1, @repository.entries("images", 0).size | |
| 71 |       %x{hg -R #{REPOSITORY_PATH} up null} | |
| 72 |       assert_equal 1, @repository.entries("images", '0885933ad4f6').size | |
| 61 | 73 |       assert_equal 2, @repository.entries("images").size | 
| 62 |       assert_equal 2, @repository.entries("images", 2).size | |
| 74 |       assert_equal 2, @repository.entries("images", '400bb8672109').size | |
| 63 | 75 | end | 
| 64 | 76 | |
| 65 | 77 | |
| 66 | 78 | def test_cat | 
| 67 |       assert @repository.scm.cat("sources/welcome_controller.rb", 2) | |
| 79 |       assert @repository.scm.cat("sources/welcome_controller.rb", '400bb8672109') | |
| 68 | 80 |       assert_nil @repository.scm.cat("sources/welcome_controller.rb") | 
| 69 | 81 | end | 
| 70 | 82 | |
| 83 | def test_simple_strip | |