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 |
|