##// END OF EJS Templates
scm: change key name of configurable command name (#7517, #6159, #7047)....
Toshi MARUYAMA -
r4677:3ee203ef0c2b
parent child
Show More
@@ -1,125 +1,125
1 # = Redmine configuration file
1 # = Redmine configuration file
2 #
2 #
3 # Each environment has it's own configuration options. If you are only
3 # Each environment has it's own configuration options. If you are only
4 # running in production, only the production block needs to be configured.
4 # running in production, only the production block needs to be configured.
5 # Environment specific configuration options override the default ones.
5 # Environment specific configuration options override the default ones.
6 #
6 #
7 # Note that this file needs to be a valid YAML file.
7 # Note that this file needs to be a valid YAML file.
8 #
8 #
9 # == Outgoing email settings (email_delivery setting)
9 # == Outgoing email settings (email_delivery setting)
10 #
10 #
11 # === Common configurations
11 # === Common configurations
12 #
12 #
13 # ==== Sendmail command
13 # ==== Sendmail command
14 #
14 #
15 # production:
15 # production:
16 # email_delivery:
16 # email_delivery:
17 # delivery_method: :sendmail
17 # delivery_method: :sendmail
18 #
18 #
19 # ==== Simple SMTP server at localhost
19 # ==== Simple SMTP server at localhost
20 #
20 #
21 # production:
21 # production:
22 # email_delivery:
22 # email_delivery:
23 # delivery_method: :smtp
23 # delivery_method: :smtp
24 # smtp_settings:
24 # smtp_settings:
25 # address: "localhost"
25 # address: "localhost"
26 # port: 25
26 # port: 25
27 #
27 #
28 # ==== SMTP server at example.com using LOGIN authentication and checking HELO for foo.com
28 # ==== SMTP server at example.com using LOGIN authentication and checking HELO for foo.com
29 #
29 #
30 # production:
30 # production:
31 # email_delivery:
31 # email_delivery:
32 # delivery_method: :smtp
32 # delivery_method: :smtp
33 # smtp_settings:
33 # smtp_settings:
34 # address: "example.com"
34 # address: "example.com"
35 # port: 25
35 # port: 25
36 # authentication: :login
36 # authentication: :login
37 # domain: 'foo.com'
37 # domain: 'foo.com'
38 # user_name: 'myaccount'
38 # user_name: 'myaccount'
39 # password: 'password'
39 # password: 'password'
40 #
40 #
41 # ==== SMTP server at example.com using PLAIN authentication
41 # ==== SMTP server at example.com using PLAIN authentication
42 #
42 #
43 # production:
43 # production:
44 # email_delivery:
44 # email_delivery:
45 # delivery_method: :smtp
45 # delivery_method: :smtp
46 # smtp_settings:
46 # smtp_settings:
47 # address: "example.com"
47 # address: "example.com"
48 # port: 25
48 # port: 25
49 # authentication: :plain
49 # authentication: :plain
50 # domain: 'example.com'
50 # domain: 'example.com'
51 # user_name: 'myaccount'
51 # user_name: 'myaccount'
52 # password: 'password'
52 # password: 'password'
53 #
53 #
54 # ==== SMTP server at using TLS (GMail)
54 # ==== SMTP server at using TLS (GMail)
55 #
55 #
56 # This requires some additional configuration. See the article at:
56 # This requires some additional configuration. See the article at:
57 # http://redmineblog.com/articles/setup-redmine-to-send-email-using-gmail/
57 # http://redmineblog.com/articles/setup-redmine-to-send-email-using-gmail/
58 #
58 #
59 # production:
59 # production:
60 # email_delivery:
60 # email_delivery:
61 # delivery_method: :smtp
61 # delivery_method: :smtp
62 # smtp_settings:
62 # smtp_settings:
63 # tls: true
63 # tls: true
64 # address: "smtp.gmail.com"
64 # address: "smtp.gmail.com"
65 # port: 587
65 # port: 587
66 # domain: "smtp.gmail.com" # 'your.domain.com' for GoogleApps
66 # domain: "smtp.gmail.com" # 'your.domain.com' for GoogleApps
67 # authentication: :plain
67 # authentication: :plain
68 # user_name: "your_email@gmail.com"
68 # user_name: "your_email@gmail.com"
69 # password: "your_password"
69 # password: "your_password"
70 #
70 #
71 #
71 #
72 # === More configuration options
72 # === More configuration options
73 #
73 #
74 # See the "Configuration options" at the following website for a list of the
74 # See the "Configuration options" at the following website for a list of the
75 # full options allowed:
75 # full options allowed:
76 #
76 #
77 # http://wiki.rubyonrails.org/rails/pages/HowToSendEmailsWithActionMailer
77 # http://wiki.rubyonrails.org/rails/pages/HowToSendEmailsWithActionMailer
78
78
79
79
80 # default configuration options for all environments
80 # default configuration options for all environments
81 default:
81 default:
82 # Outgoing emails configuration (see examples above)
82 # Outgoing emails configuration (see examples above)
83 email_delivery:
83 email_delivery:
84 delivery_method: :smtp
84 delivery_method: :smtp
85 smtp_settings:
85 smtp_settings:
86 address: smtp.example.net
86 address: smtp.example.net
87 port: 25
87 port: 25
88 domain: example.net
88 domain: example.net
89 authentication: :login
89 authentication: :login
90 user_name: "redmine@example.net"
90 user_name: "redmine@example.net"
91 password: "redmine"
91 password: "redmine"
92
92
93 # Absolute path to the directory where attachments are stored.
93 # Absolute path to the directory where attachments are stored.
94 # The default is the 'files' directory in your Redmine instance.
94 # The default is the 'files' directory in your Redmine instance.
95 # Your Redmine instance needs to have write permission on this
95 # Your Redmine instance needs to have write permission on this
96 # directory.
96 # directory.
97 # Examples:
97 # Examples:
98 # attachments_storage_path: /var/redmine/files
98 # attachments_storage_path: /var/redmine/files
99 # attachments_storage_path: D:/redmine/files
99 # attachments_storage_path: D:/redmine/files
100 attachments_storage_path:
100 attachments_storage_path:
101
101
102 # Configuration of the autologin cookie.
102 # Configuration of the autologin cookie.
103 # autologin_cookie_name: the name of the cookie (default: autologin)
103 # autologin_cookie_name: the name of the cookie (default: autologin)
104 # autologin_cookie_path: the cookie path (default: /)
104 # autologin_cookie_path: the cookie path (default: /)
105 # autologin_cookie_secure: true sets the cookie secure flag (default: false)
105 # autologin_cookie_secure: true sets the cookie secure flag (default: false)
106 autologin_cookie_name:
106 autologin_cookie_name:
107 autologin_cookie_path:
107 autologin_cookie_path:
108 autologin_cookie_secure:
108 autologin_cookie_secure:
109
109
110 # Configuration of SCM executable command.
110 # Configuration of SCM executable command.
111 # Absolute path (e.g. /usr/local/bin/hg) or command name (e.g. hg.exe, bzr.exe)
111 # Absolute path (e.g. /usr/local/bin/hg) or command name (e.g. hg.exe, bzr.exe)
112 # On Windows, *.cmd, *.bat (e.g. hg.cmd, bzr.bat) does not work.
112 # On Windows, *.cmd, *.bat (e.g. hg.cmd, bzr.bat) does not work.
113 scm_command_subversion: svn # (default: svn)
113 scm_subversion_command: svn # (default: svn)
114 scm_command_mercurial: "\"C:\Program Files\TortoiseHg\hg.exe\"" # (default: hg)
114 scm_mercurial_command: "\"C:\Program Files\TortoiseHg\hg.exe\"" # (default: hg)
115 scm_command_git: /usr/local/bin/git # (default: git)
115 scm_git_command: /usr/local/bin/git # (default: git)
116 scm_command_bazaar: bzr.exe # (default: bzr)
116 scm_bazaar_command: bzr.exe # (default: bzr)
117 scm_command_darcs: darcs-1.0.9-i386-linux # (default: darcs)
117 scm_darcs_command: darcs-1.0.9-i386-linux # (default: darcs)
118
118
119 # specific configuration options for production environment
119 # specific configuration options for production environment
120 # that overrides the default ones
120 # that overrides the default ones
121 production:
121 production:
122
122
123 # specific configuration options for development environment
123 # specific configuration options for development environment
124 # that overrides the default ones
124 # that overrides the default ones
125 development:
125 development:
@@ -1,189 +1,189
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require 'redmine/scm/adapters/abstract_adapter'
18 require 'redmine/scm/adapters/abstract_adapter'
19
19
20 module Redmine
20 module Redmine
21 module Scm
21 module Scm
22 module Adapters
22 module Adapters
23 class BazaarAdapter < AbstractAdapter
23 class BazaarAdapter < AbstractAdapter
24
24
25 # Bazaar executable name
25 # Bazaar executable name
26 BZR_BIN = Redmine::Configuration['scm_command_bazaar'] || "bzr"
26 BZR_BIN = Redmine::Configuration['scm_bazaar_command'] || "bzr"
27
27
28 # Get info about the repository
28 # Get info about the repository
29 def info
29 def info
30 cmd = "#{BZR_BIN} revno #{target('')}"
30 cmd = "#{BZR_BIN} revno #{target('')}"
31 info = nil
31 info = nil
32 shellout(cmd) do |io|
32 shellout(cmd) do |io|
33 if io.read =~ %r{^(\d+)\r?$}
33 if io.read =~ %r{^(\d+)\r?$}
34 info = Info.new({:root_url => url,
34 info = Info.new({:root_url => url,
35 :lastrev => Revision.new({
35 :lastrev => Revision.new({
36 :identifier => $1
36 :identifier => $1
37 })
37 })
38 })
38 })
39 end
39 end
40 end
40 end
41 return nil if $? && $?.exitstatus != 0
41 return nil if $? && $?.exitstatus != 0
42 info
42 info
43 rescue CommandFailed
43 rescue CommandFailed
44 return nil
44 return nil
45 end
45 end
46
46
47 # Returns an Entries collection
47 # Returns an Entries collection
48 # or nil if the given path doesn't exist in the repository
48 # or nil if the given path doesn't exist in the repository
49 def entries(path=nil, identifier=nil)
49 def entries(path=nil, identifier=nil)
50 path ||= ''
50 path ||= ''
51 entries = Entries.new
51 entries = Entries.new
52 cmd = "#{BZR_BIN} ls -v --show-ids"
52 cmd = "#{BZR_BIN} ls -v --show-ids"
53 identifier = -1 unless identifier && identifier.to_i > 0
53 identifier = -1 unless identifier && identifier.to_i > 0
54 cmd << " -r#{identifier.to_i}"
54 cmd << " -r#{identifier.to_i}"
55 cmd << " #{target(path)}"
55 cmd << " #{target(path)}"
56 shellout(cmd) do |io|
56 shellout(cmd) do |io|
57 prefix = "#{url}/#{path}".gsub('\\', '/')
57 prefix = "#{url}/#{path}".gsub('\\', '/')
58 logger.debug "PREFIX: #{prefix}"
58 logger.debug "PREFIX: #{prefix}"
59 re = %r{^V\s+(#{Regexp.escape(prefix)})?(\/?)([^\/]+)(\/?)\s+(\S+)\r?$}
59 re = %r{^V\s+(#{Regexp.escape(prefix)})?(\/?)([^\/]+)(\/?)\s+(\S+)\r?$}
60 io.each_line do |line|
60 io.each_line do |line|
61 next unless line =~ re
61 next unless line =~ re
62 entries << Entry.new({:name => $3.strip,
62 entries << Entry.new({:name => $3.strip,
63 :path => ((path.empty? ? "" : "#{path}/") + $3.strip),
63 :path => ((path.empty? ? "" : "#{path}/") + $3.strip),
64 :kind => ($4.blank? ? 'file' : 'dir'),
64 :kind => ($4.blank? ? 'file' : 'dir'),
65 :size => nil,
65 :size => nil,
66 :lastrev => Revision.new(:revision => $5.strip)
66 :lastrev => Revision.new(:revision => $5.strip)
67 })
67 })
68 end
68 end
69 end
69 end
70 return nil if $? && $?.exitstatus != 0
70 return nil if $? && $?.exitstatus != 0
71 logger.debug("Found #{entries.size} entries in the repository for #{target(path)}") if logger && logger.debug?
71 logger.debug("Found #{entries.size} entries in the repository for #{target(path)}") if logger && logger.debug?
72 entries.sort_by_name
72 entries.sort_by_name
73 end
73 end
74
74
75 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
75 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
76 path ||= ''
76 path ||= ''
77 identifier_from = (identifier_from and identifier_from.to_i > 0) ? identifier_from.to_i : 'last:1'
77 identifier_from = (identifier_from and identifier_from.to_i > 0) ? identifier_from.to_i : 'last:1'
78 identifier_to = (identifier_to and identifier_to.to_i > 0) ? identifier_to.to_i : 1
78 identifier_to = (identifier_to and identifier_to.to_i > 0) ? identifier_to.to_i : 1
79 revisions = Revisions.new
79 revisions = Revisions.new
80 cmd = "#{BZR_BIN} log -v --show-ids -r#{identifier_to}..#{identifier_from} #{target(path)}"
80 cmd = "#{BZR_BIN} log -v --show-ids -r#{identifier_to}..#{identifier_from} #{target(path)}"
81 shellout(cmd) do |io|
81 shellout(cmd) do |io|
82 revision = nil
82 revision = nil
83 parsing = nil
83 parsing = nil
84 io.each_line do |line|
84 io.each_line do |line|
85 if line =~ /^----/
85 if line =~ /^----/
86 revisions << revision if revision
86 revisions << revision if revision
87 revision = Revision.new(:paths => [], :message => '')
87 revision = Revision.new(:paths => [], :message => '')
88 parsing = nil
88 parsing = nil
89 else
89 else
90 next unless revision
90 next unless revision
91
91
92 if line =~ /^revno: (\d+)($|\s\[merge\]$)/
92 if line =~ /^revno: (\d+)($|\s\[merge\]$)/
93 revision.identifier = $1.to_i
93 revision.identifier = $1.to_i
94 elsif line =~ /^committer: (.+)$/
94 elsif line =~ /^committer: (.+)$/
95 revision.author = $1.strip
95 revision.author = $1.strip
96 elsif line =~ /^revision-id:(.+)$/
96 elsif line =~ /^revision-id:(.+)$/
97 revision.scmid = $1.strip
97 revision.scmid = $1.strip
98 elsif line =~ /^timestamp: (.+)$/
98 elsif line =~ /^timestamp: (.+)$/
99 revision.time = Time.parse($1).localtime
99 revision.time = Time.parse($1).localtime
100 elsif line =~ /^ -----/
100 elsif line =~ /^ -----/
101 # partial revisions
101 # partial revisions
102 parsing = nil unless parsing == 'message'
102 parsing = nil unless parsing == 'message'
103 elsif line =~ /^(message|added|modified|removed|renamed):/
103 elsif line =~ /^(message|added|modified|removed|renamed):/
104 parsing = $1
104 parsing = $1
105 elsif line =~ /^ (.*)$/
105 elsif line =~ /^ (.*)$/
106 if parsing == 'message'
106 if parsing == 'message'
107 revision.message << "#{$1}\n"
107 revision.message << "#{$1}\n"
108 else
108 else
109 if $1 =~ /^(.*)\s+(\S+)$/
109 if $1 =~ /^(.*)\s+(\S+)$/
110 path = $1.strip
110 path = $1.strip
111 revid = $2
111 revid = $2
112 case parsing
112 case parsing
113 when 'added'
113 when 'added'
114 revision.paths << {:action => 'A', :path => "/#{path}", :revision => revid}
114 revision.paths << {:action => 'A', :path => "/#{path}", :revision => revid}
115 when 'modified'
115 when 'modified'
116 revision.paths << {:action => 'M', :path => "/#{path}", :revision => revid}
116 revision.paths << {:action => 'M', :path => "/#{path}", :revision => revid}
117 when 'removed'
117 when 'removed'
118 revision.paths << {:action => 'D', :path => "/#{path}", :revision => revid}
118 revision.paths << {:action => 'D', :path => "/#{path}", :revision => revid}
119 when 'renamed'
119 when 'renamed'
120 new_path = path.split('=>').last
120 new_path = path.split('=>').last
121 revision.paths << {:action => 'M', :path => "/#{new_path.strip}", :revision => revid} if new_path
121 revision.paths << {:action => 'M', :path => "/#{new_path.strip}", :revision => revid} if new_path
122 end
122 end
123 end
123 end
124 end
124 end
125 else
125 else
126 parsing = nil
126 parsing = nil
127 end
127 end
128 end
128 end
129 end
129 end
130 revisions << revision if revision
130 revisions << revision if revision
131 end
131 end
132 return nil if $? && $?.exitstatus != 0
132 return nil if $? && $?.exitstatus != 0
133 revisions
133 revisions
134 end
134 end
135
135
136 def diff(path, identifier_from, identifier_to=nil)
136 def diff(path, identifier_from, identifier_to=nil)
137 path ||= ''
137 path ||= ''
138 if identifier_to
138 if identifier_to
139 identifier_to = identifier_to.to_i
139 identifier_to = identifier_to.to_i
140 else
140 else
141 identifier_to = identifier_from.to_i - 1
141 identifier_to = identifier_from.to_i - 1
142 end
142 end
143 if identifier_from
143 if identifier_from
144 identifier_from = identifier_from.to_i
144 identifier_from = identifier_from.to_i
145 end
145 end
146 cmd = "#{BZR_BIN} diff -r#{identifier_to}..#{identifier_from} #{target(path)}"
146 cmd = "#{BZR_BIN} diff -r#{identifier_to}..#{identifier_from} #{target(path)}"
147 diff = []
147 diff = []
148 shellout(cmd) do |io|
148 shellout(cmd) do |io|
149 io.each_line do |line|
149 io.each_line do |line|
150 diff << line
150 diff << line
151 end
151 end
152 end
152 end
153 #return nil if $? && $?.exitstatus != 0
153 #return nil if $? && $?.exitstatus != 0
154 diff
154 diff
155 end
155 end
156
156
157 def cat(path, identifier=nil)
157 def cat(path, identifier=nil)
158 cmd = "#{BZR_BIN} cat"
158 cmd = "#{BZR_BIN} cat"
159 cmd << " -r#{identifier.to_i}" if identifier && identifier.to_i > 0
159 cmd << " -r#{identifier.to_i}" if identifier && identifier.to_i > 0
160 cmd << " #{target(path)}"
160 cmd << " #{target(path)}"
161 cat = nil
161 cat = nil
162 shellout(cmd) do |io|
162 shellout(cmd) do |io|
163 io.binmode
163 io.binmode
164 cat = io.read
164 cat = io.read
165 end
165 end
166 return nil if $? && $?.exitstatus != 0
166 return nil if $? && $?.exitstatus != 0
167 cat
167 cat
168 end
168 end
169
169
170 def annotate(path, identifier=nil)
170 def annotate(path, identifier=nil)
171 cmd = "#{BZR_BIN} annotate --all"
171 cmd = "#{BZR_BIN} annotate --all"
172 cmd << " -r#{identifier.to_i}" if identifier && identifier.to_i > 0
172 cmd << " -r#{identifier.to_i}" if identifier && identifier.to_i > 0
173 cmd << " #{target(path)}"
173 cmd << " #{target(path)}"
174 blame = Annotate.new
174 blame = Annotate.new
175 shellout(cmd) do |io|
175 shellout(cmd) do |io|
176 author = nil
176 author = nil
177 identifier = nil
177 identifier = nil
178 io.each_line do |line|
178 io.each_line do |line|
179 next unless line =~ %r{^(\d+) ([^|]+)\| (.*)$}
179 next unless line =~ %r{^(\d+) ([^|]+)\| (.*)$}
180 blame.add_line($3.rstrip, Revision.new(:identifier => $1.to_i, :author => $2.strip))
180 blame.add_line($3.rstrip, Revision.new(:identifier => $1.to_i, :author => $2.strip))
181 end
181 end
182 end
182 end
183 return nil if $? && $?.exitstatus != 0
183 return nil if $? && $?.exitstatus != 0
184 blame
184 blame
185 end
185 end
186 end
186 end
187 end
187 end
188 end
188 end
189 end
189 end
@@ -1,370 +1,370
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require 'redmine/scm/adapters/abstract_adapter'
18 require 'redmine/scm/adapters/abstract_adapter'
19
19
20 module Redmine
20 module Redmine
21 module Scm
21 module Scm
22 module Adapters
22 module Adapters
23 class CvsAdapter < AbstractAdapter
23 class CvsAdapter < AbstractAdapter
24
24
25 # CVS executable name
25 # CVS executable name
26 CVS_BIN = Redmine::Configuration['scm_command_cvs'] || "cvs"
26 CVS_BIN = Redmine::Configuration['scm_cvs_command'] || "cvs"
27
27
28 # Guidelines for the input:
28 # Guidelines for the input:
29 # url -> the project-path, relative to the cvsroot (eg. module name)
29 # url -> the project-path, relative to the cvsroot (eg. module name)
30 # root_url -> the good old, sometimes damned, CVSROOT
30 # root_url -> the good old, sometimes damned, CVSROOT
31 # login -> unnecessary
31 # login -> unnecessary
32 # password -> unnecessary too
32 # password -> unnecessary too
33 def initialize(url, root_url=nil, login=nil, password=nil)
33 def initialize(url, root_url=nil, login=nil, password=nil)
34 @url = url
34 @url = url
35 @login = login if login && !login.empty?
35 @login = login if login && !login.empty?
36 @password = (password || "") if @login
36 @password = (password || "") if @login
37 #TODO: better Exception here (IllegalArgumentException)
37 #TODO: better Exception here (IllegalArgumentException)
38 raise CommandFailed if root_url.blank?
38 raise CommandFailed if root_url.blank?
39 @root_url = root_url
39 @root_url = root_url
40 end
40 end
41
41
42 def root_url
42 def root_url
43 @root_url
43 @root_url
44 end
44 end
45
45
46 def url
46 def url
47 @url
47 @url
48 end
48 end
49
49
50 def info
50 def info
51 logger.debug "<cvs> info"
51 logger.debug "<cvs> info"
52 Info.new({:root_url => @root_url, :lastrev => nil})
52 Info.new({:root_url => @root_url, :lastrev => nil})
53 end
53 end
54
54
55 def get_previous_revision(revision)
55 def get_previous_revision(revision)
56 CvsRevisionHelper.new(revision).prevRev
56 CvsRevisionHelper.new(revision).prevRev
57 end
57 end
58
58
59 # Returns an Entries collection
59 # Returns an Entries collection
60 # or nil if the given path doesn't exist in the repository
60 # or nil if the given path doesn't exist in the repository
61 # this method is used by the repository-browser (aka LIST)
61 # this method is used by the repository-browser (aka LIST)
62 def entries(path=nil, identifier=nil)
62 def entries(path=nil, identifier=nil)
63 logger.debug "<cvs> entries '#{path}' with identifier '#{identifier}'"
63 logger.debug "<cvs> entries '#{path}' with identifier '#{identifier}'"
64 path_with_project="#{url}#{with_leading_slash(path)}"
64 path_with_project="#{url}#{with_leading_slash(path)}"
65 entries = Entries.new
65 entries = Entries.new
66 cmd = "#{CVS_BIN} -d #{shell_quote root_url} rls -e"
66 cmd = "#{CVS_BIN} -d #{shell_quote root_url} rls -e"
67 cmd << " -D \"#{time_to_cvstime(identifier)}\"" if identifier
67 cmd << " -D \"#{time_to_cvstime(identifier)}\"" if identifier
68 cmd << " #{shell_quote path_with_project}"
68 cmd << " #{shell_quote path_with_project}"
69 shellout(cmd) do |io|
69 shellout(cmd) do |io|
70 io.each_line(){|line|
70 io.each_line(){|line|
71 fields=line.chop.split('/',-1)
71 fields=line.chop.split('/',-1)
72 logger.debug(">>InspectLine #{fields.inspect}")
72 logger.debug(">>InspectLine #{fields.inspect}")
73
73
74 if fields[0]!="D"
74 if fields[0]!="D"
75 entries << Entry.new({:name => fields[-5],
75 entries << Entry.new({:name => fields[-5],
76 #:path => fields[-4].include?(path)?fields[-4]:(path + "/"+ fields[-4]),
76 #:path => fields[-4].include?(path)?fields[-4]:(path + "/"+ fields[-4]),
77 :path => "#{path}/#{fields[-5]}",
77 :path => "#{path}/#{fields[-5]}",
78 :kind => 'file',
78 :kind => 'file',
79 :size => nil,
79 :size => nil,
80 :lastrev => Revision.new({
80 :lastrev => Revision.new({
81 :revision => fields[-4],
81 :revision => fields[-4],
82 :name => fields[-4],
82 :name => fields[-4],
83 :time => Time.parse(fields[-3]),
83 :time => Time.parse(fields[-3]),
84 :author => ''
84 :author => ''
85 })
85 })
86 })
86 })
87 else
87 else
88 entries << Entry.new({:name => fields[1],
88 entries << Entry.new({:name => fields[1],
89 :path => "#{path}/#{fields[1]}",
89 :path => "#{path}/#{fields[1]}",
90 :kind => 'dir',
90 :kind => 'dir',
91 :size => nil,
91 :size => nil,
92 :lastrev => nil
92 :lastrev => nil
93 })
93 })
94 end
94 end
95 }
95 }
96 end
96 end
97 return nil if $? && $?.exitstatus != 0
97 return nil if $? && $?.exitstatus != 0
98 entries.sort_by_name
98 entries.sort_by_name
99 end
99 end
100
100
101 STARTLOG="----------------------------"
101 STARTLOG="----------------------------"
102 ENDLOG ="============================================================================="
102 ENDLOG ="============================================================================="
103
103
104 # Returns all revisions found between identifier_from and identifier_to
104 # Returns all revisions found between identifier_from and identifier_to
105 # in the repository. both identifier have to be dates or nil.
105 # in the repository. both identifier have to be dates or nil.
106 # these method returns nothing but yield every result in block
106 # these method returns nothing but yield every result in block
107 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}, &block)
107 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}, &block)
108 logger.debug "<cvs> revisions path:'#{path}',identifier_from #{identifier_from}, identifier_to #{identifier_to}"
108 logger.debug "<cvs> revisions path:'#{path}',identifier_from #{identifier_from}, identifier_to #{identifier_to}"
109
109
110 path_with_project="#{url}#{with_leading_slash(path)}"
110 path_with_project="#{url}#{with_leading_slash(path)}"
111 cmd = "#{CVS_BIN} -d #{shell_quote root_url} rlog"
111 cmd = "#{CVS_BIN} -d #{shell_quote root_url} rlog"
112 cmd << " -d\">#{time_to_cvstime_rlog(identifier_from)}\"" if identifier_from
112 cmd << " -d\">#{time_to_cvstime_rlog(identifier_from)}\"" if identifier_from
113 cmd << " #{shell_quote path_with_project}"
113 cmd << " #{shell_quote path_with_project}"
114 shellout(cmd) do |io|
114 shellout(cmd) do |io|
115 state="entry_start"
115 state="entry_start"
116
116
117 commit_log=String.new
117 commit_log=String.new
118 revision=nil
118 revision=nil
119 date=nil
119 date=nil
120 author=nil
120 author=nil
121 entry_path=nil
121 entry_path=nil
122 entry_name=nil
122 entry_name=nil
123 file_state=nil
123 file_state=nil
124 branch_map=nil
124 branch_map=nil
125
125
126 io.each_line() do |line|
126 io.each_line() do |line|
127
127
128 if state!="revision" && /^#{ENDLOG}/ =~ line
128 if state!="revision" && /^#{ENDLOG}/ =~ line
129 commit_log=String.new
129 commit_log=String.new
130 revision=nil
130 revision=nil
131 state="entry_start"
131 state="entry_start"
132 end
132 end
133
133
134 if state=="entry_start"
134 if state=="entry_start"
135 branch_map=Hash.new
135 branch_map=Hash.new
136 if /^RCS file: #{Regexp.escape(root_url_path)}\/#{Regexp.escape(path_with_project)}(.+),v$/ =~ line
136 if /^RCS file: #{Regexp.escape(root_url_path)}\/#{Regexp.escape(path_with_project)}(.+),v$/ =~ line
137 entry_path = normalize_cvs_path($1)
137 entry_path = normalize_cvs_path($1)
138 entry_name = normalize_path(File.basename($1))
138 entry_name = normalize_path(File.basename($1))
139 logger.debug("Path #{entry_path} <=> Name #{entry_name}")
139 logger.debug("Path #{entry_path} <=> Name #{entry_name}")
140 elsif /^head: (.+)$/ =~ line
140 elsif /^head: (.+)$/ =~ line
141 entry_headRev = $1 #unless entry.nil?
141 entry_headRev = $1 #unless entry.nil?
142 elsif /^symbolic names:/ =~ line
142 elsif /^symbolic names:/ =~ line
143 state="symbolic" #unless entry.nil?
143 state="symbolic" #unless entry.nil?
144 elsif /^#{STARTLOG}/ =~ line
144 elsif /^#{STARTLOG}/ =~ line
145 commit_log=String.new
145 commit_log=String.new
146 state="revision"
146 state="revision"
147 end
147 end
148 next
148 next
149 elsif state=="symbolic"
149 elsif state=="symbolic"
150 if /^(.*):\s(.*)/ =~ (line.strip)
150 if /^(.*):\s(.*)/ =~ (line.strip)
151 branch_map[$1]=$2
151 branch_map[$1]=$2
152 else
152 else
153 state="tags"
153 state="tags"
154 next
154 next
155 end
155 end
156 elsif state=="tags"
156 elsif state=="tags"
157 if /^#{STARTLOG}/ =~ line
157 if /^#{STARTLOG}/ =~ line
158 commit_log = ""
158 commit_log = ""
159 state="revision"
159 state="revision"
160 elsif /^#{ENDLOG}/ =~ line
160 elsif /^#{ENDLOG}/ =~ line
161 state="head"
161 state="head"
162 end
162 end
163 next
163 next
164 elsif state=="revision"
164 elsif state=="revision"
165 if /^#{ENDLOG}/ =~ line || /^#{STARTLOG}/ =~ line
165 if /^#{ENDLOG}/ =~ line || /^#{STARTLOG}/ =~ line
166 if revision
166 if revision
167
167
168 revHelper=CvsRevisionHelper.new(revision)
168 revHelper=CvsRevisionHelper.new(revision)
169 revBranch="HEAD"
169 revBranch="HEAD"
170
170
171 branch_map.each() do |branch_name,branch_point|
171 branch_map.each() do |branch_name,branch_point|
172 if revHelper.is_in_branch_with_symbol(branch_point)
172 if revHelper.is_in_branch_with_symbol(branch_point)
173 revBranch=branch_name
173 revBranch=branch_name
174 end
174 end
175 end
175 end
176
176
177 logger.debug("********** YIELD Revision #{revision}::#{revBranch}")
177 logger.debug("********** YIELD Revision #{revision}::#{revBranch}")
178
178
179 yield Revision.new({
179 yield Revision.new({
180 :time => date,
180 :time => date,
181 :author => author,
181 :author => author,
182 :message=>commit_log.chomp,
182 :message=>commit_log.chomp,
183 :paths => [{
183 :paths => [{
184 :revision => revision,
184 :revision => revision,
185 :branch=> revBranch,
185 :branch=> revBranch,
186 :path=>entry_path,
186 :path=>entry_path,
187 :name=>entry_name,
187 :name=>entry_name,
188 :kind=>'file',
188 :kind=>'file',
189 :action=>file_state
189 :action=>file_state
190 }]
190 }]
191 })
191 })
192 end
192 end
193
193
194 commit_log=String.new
194 commit_log=String.new
195 revision=nil
195 revision=nil
196
196
197 if /^#{ENDLOG}/ =~ line
197 if /^#{ENDLOG}/ =~ line
198 state="entry_start"
198 state="entry_start"
199 end
199 end
200 next
200 next
201 end
201 end
202
202
203 if /^branches: (.+)$/ =~ line
203 if /^branches: (.+)$/ =~ line
204 #TODO: version.branch = $1
204 #TODO: version.branch = $1
205 elsif /^revision (\d+(?:\.\d+)+).*$/ =~ line
205 elsif /^revision (\d+(?:\.\d+)+).*$/ =~ line
206 revision = $1
206 revision = $1
207 elsif /^date:\s+(\d+.\d+.\d+\s+\d+:\d+:\d+)/ =~ line
207 elsif /^date:\s+(\d+.\d+.\d+\s+\d+:\d+:\d+)/ =~ line
208 date = Time.parse($1)
208 date = Time.parse($1)
209 author = /author: ([^;]+)/.match(line)[1]
209 author = /author: ([^;]+)/.match(line)[1]
210 file_state = /state: ([^;]+)/.match(line)[1]
210 file_state = /state: ([^;]+)/.match(line)[1]
211 #TODO: linechanges only available in CVS.... maybe a feature our SVN implementation. i'm sure, they are
211 #TODO: linechanges only available in CVS.... maybe a feature our SVN implementation. i'm sure, they are
212 # useful for stats or something else
212 # useful for stats or something else
213 # linechanges =/lines: \+(\d+) -(\d+)/.match(line)
213 # linechanges =/lines: \+(\d+) -(\d+)/.match(line)
214 # unless linechanges.nil?
214 # unless linechanges.nil?
215 # version.line_plus = linechanges[1]
215 # version.line_plus = linechanges[1]
216 # version.line_minus = linechanges[2]
216 # version.line_minus = linechanges[2]
217 # else
217 # else
218 # version.line_plus = 0
218 # version.line_plus = 0
219 # version.line_minus = 0
219 # version.line_minus = 0
220 # end
220 # end
221 else
221 else
222 commit_log << line unless line =~ /^\*\*\* empty log message \*\*\*/
222 commit_log << line unless line =~ /^\*\*\* empty log message \*\*\*/
223 end
223 end
224 end
224 end
225 end
225 end
226 end
226 end
227 end
227 end
228
228
229 def diff(path, identifier_from, identifier_to=nil)
229 def diff(path, identifier_from, identifier_to=nil)
230 logger.debug "<cvs> diff path:'#{path}',identifier_from #{identifier_from}, identifier_to #{identifier_to}"
230 logger.debug "<cvs> diff path:'#{path}',identifier_from #{identifier_from}, identifier_to #{identifier_to}"
231 path_with_project="#{url}#{with_leading_slash(path)}"
231 path_with_project="#{url}#{with_leading_slash(path)}"
232 cmd = "#{CVS_BIN} -d #{shell_quote root_url} rdiff -u -r#{identifier_to} -r#{identifier_from} #{shell_quote path_with_project}"
232 cmd = "#{CVS_BIN} -d #{shell_quote root_url} rdiff -u -r#{identifier_to} -r#{identifier_from} #{shell_quote path_with_project}"
233 diff = []
233 diff = []
234 shellout(cmd) do |io|
234 shellout(cmd) do |io|
235 io.each_line do |line|
235 io.each_line do |line|
236 diff << line
236 diff << line
237 end
237 end
238 end
238 end
239 return nil if $? && $?.exitstatus != 0
239 return nil if $? && $?.exitstatus != 0
240 diff
240 diff
241 end
241 end
242
242
243 def cat(path, identifier=nil)
243 def cat(path, identifier=nil)
244 identifier = (identifier) ? identifier : "HEAD"
244 identifier = (identifier) ? identifier : "HEAD"
245 logger.debug "<cvs> cat path:'#{path}',identifier #{identifier}"
245 logger.debug "<cvs> cat path:'#{path}',identifier #{identifier}"
246 path_with_project="#{url}#{with_leading_slash(path)}"
246 path_with_project="#{url}#{with_leading_slash(path)}"
247 cmd = "#{CVS_BIN} -d #{shell_quote root_url} co"
247 cmd = "#{CVS_BIN} -d #{shell_quote root_url} co"
248 cmd << " -D \"#{time_to_cvstime(identifier)}\"" if identifier
248 cmd << " -D \"#{time_to_cvstime(identifier)}\"" if identifier
249 cmd << " -p #{shell_quote path_with_project}"
249 cmd << " -p #{shell_quote path_with_project}"
250 cat = nil
250 cat = nil
251 shellout(cmd) do |io|
251 shellout(cmd) do |io|
252 cat = io.read
252 cat = io.read
253 end
253 end
254 return nil if $? && $?.exitstatus != 0
254 return nil if $? && $?.exitstatus != 0
255 cat
255 cat
256 end
256 end
257
257
258 def annotate(path, identifier=nil)
258 def annotate(path, identifier=nil)
259 identifier = (identifier) ? identifier.to_i : "HEAD"
259 identifier = (identifier) ? identifier.to_i : "HEAD"
260 logger.debug "<cvs> annotate path:'#{path}',identifier #{identifier}"
260 logger.debug "<cvs> annotate path:'#{path}',identifier #{identifier}"
261 path_with_project="#{url}#{with_leading_slash(path)}"
261 path_with_project="#{url}#{with_leading_slash(path)}"
262 cmd = "#{CVS_BIN} -d #{shell_quote root_url} rannotate -r#{identifier} #{shell_quote path_with_project}"
262 cmd = "#{CVS_BIN} -d #{shell_quote root_url} rannotate -r#{identifier} #{shell_quote path_with_project}"
263 blame = Annotate.new
263 blame = Annotate.new
264 shellout(cmd) do |io|
264 shellout(cmd) do |io|
265 io.each_line do |line|
265 io.each_line do |line|
266 next unless line =~ %r{^([\d\.]+)\s+\(([^\)]+)\s+[^\)]+\):\s(.*)$}
266 next unless line =~ %r{^([\d\.]+)\s+\(([^\)]+)\s+[^\)]+\):\s(.*)$}
267 blame.add_line($3.rstrip, Revision.new(:revision => $1, :author => $2.strip))
267 blame.add_line($3.rstrip, Revision.new(:revision => $1, :author => $2.strip))
268 end
268 end
269 end
269 end
270 return nil if $? && $?.exitstatus != 0
270 return nil if $? && $?.exitstatus != 0
271 blame
271 blame
272 end
272 end
273
273
274 private
274 private
275
275
276 # Returns the root url without the connexion string
276 # Returns the root url without the connexion string
277 # :pserver:anonymous@foo.bar:/path => /path
277 # :pserver:anonymous@foo.bar:/path => /path
278 # :ext:cvsservername:/path => /path
278 # :ext:cvsservername:/path => /path
279 def root_url_path
279 def root_url_path
280 root_url.to_s.gsub(/^:.+:\d*/, '')
280 root_url.to_s.gsub(/^:.+:\d*/, '')
281 end
281 end
282
282
283 # convert a date/time into the CVS-format
283 # convert a date/time into the CVS-format
284 def time_to_cvstime(time)
284 def time_to_cvstime(time)
285 return nil if time.nil?
285 return nil if time.nil?
286 return Time.now if time == 'HEAD'
286 return Time.now if time == 'HEAD'
287
287
288 unless time.kind_of? Time
288 unless time.kind_of? Time
289 time = Time.parse(time)
289 time = Time.parse(time)
290 end
290 end
291 return time.strftime("%Y-%m-%d %H:%M:%S")
291 return time.strftime("%Y-%m-%d %H:%M:%S")
292 end
292 end
293
293
294 def time_to_cvstime_rlog(time)
294 def time_to_cvstime_rlog(time)
295 return nil if time.nil?
295 return nil if time.nil?
296 t1 = time.clone.localtime
296 t1 = time.clone.localtime
297 return t1.strftime("%Y-%m-%d %H:%M:%S")
297 return t1.strftime("%Y-%m-%d %H:%M:%S")
298 end
298 end
299
299
300 def normalize_cvs_path(path)
300 def normalize_cvs_path(path)
301 normalize_path(path.gsub(/Attic\//,''))
301 normalize_path(path.gsub(/Attic\//,''))
302 end
302 end
303
303
304 def normalize_path(path)
304 def normalize_path(path)
305 path.sub(/^(\/)*(.*)/,'\2').sub(/(.*)(,v)+/,'\1')
305 path.sub(/^(\/)*(.*)/,'\2').sub(/(.*)(,v)+/,'\1')
306 end
306 end
307 end
307 end
308
308
309 class CvsRevisionHelper
309 class CvsRevisionHelper
310 attr_accessor :complete_rev, :revision, :base, :branchid
310 attr_accessor :complete_rev, :revision, :base, :branchid
311
311
312 def initialize(complete_rev)
312 def initialize(complete_rev)
313 @complete_rev = complete_rev
313 @complete_rev = complete_rev
314 parseRevision()
314 parseRevision()
315 end
315 end
316
316
317 def branchPoint
317 def branchPoint
318 return @base
318 return @base
319 end
319 end
320
320
321 def branchVersion
321 def branchVersion
322 if isBranchRevision
322 if isBranchRevision
323 return @base+"."+@branchid
323 return @base+"."+@branchid
324 end
324 end
325 return @base
325 return @base
326 end
326 end
327
327
328 def isBranchRevision
328 def isBranchRevision
329 !@branchid.nil?
329 !@branchid.nil?
330 end
330 end
331
331
332 def prevRev
332 def prevRev
333 unless @revision==0
333 unless @revision==0
334 return buildRevision(@revision-1)
334 return buildRevision(@revision-1)
335 end
335 end
336 return buildRevision(@revision)
336 return buildRevision(@revision)
337 end
337 end
338
338
339 def is_in_branch_with_symbol(branch_symbol)
339 def is_in_branch_with_symbol(branch_symbol)
340 bpieces=branch_symbol.split(".")
340 bpieces=branch_symbol.split(".")
341 branch_start="#{bpieces[0..-3].join(".")}.#{bpieces[-1]}"
341 branch_start="#{bpieces[0..-3].join(".")}.#{bpieces[-1]}"
342 return (branchVersion==branch_start)
342 return (branchVersion==branch_start)
343 end
343 end
344
344
345 private
345 private
346 def buildRevision(rev)
346 def buildRevision(rev)
347 if rev== 0
347 if rev== 0
348 @base
348 @base
349 elsif @branchid.nil?
349 elsif @branchid.nil?
350 @base+"."+rev.to_s
350 @base+"."+rev.to_s
351 else
351 else
352 @base+"."+@branchid+"."+rev.to_s
352 @base+"."+@branchid+"."+rev.to_s
353 end
353 end
354 end
354 end
355
355
356 # Interpretiert die cvs revisionsnummern wie z.b. 1.14 oder 1.3.0.15
356 # Interpretiert die cvs revisionsnummern wie z.b. 1.14 oder 1.3.0.15
357 def parseRevision()
357 def parseRevision()
358 pieces=@complete_rev.split(".")
358 pieces=@complete_rev.split(".")
359 @revision=pieces.last.to_i
359 @revision=pieces.last.to_i
360 baseSize=1
360 baseSize=1
361 baseSize+=(pieces.size/2)
361 baseSize+=(pieces.size/2)
362 @base=pieces[0..-baseSize].join(".")
362 @base=pieces[0..-baseSize].join(".")
363 if baseSize > 2
363 if baseSize > 2
364 @branchid=pieces[-2]
364 @branchid=pieces[-2]
365 end
365 end
366 end
366 end
367 end
367 end
368 end
368 end
369 end
369 end
370 end
370 end
@@ -1,225 +1,225
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require 'redmine/scm/adapters/abstract_adapter'
18 require 'redmine/scm/adapters/abstract_adapter'
19 require 'rexml/document'
19 require 'rexml/document'
20
20
21 module Redmine
21 module Redmine
22 module Scm
22 module Scm
23 module Adapters
23 module Adapters
24 class DarcsAdapter < AbstractAdapter
24 class DarcsAdapter < AbstractAdapter
25 # Darcs executable name
25 # Darcs executable name
26 DARCS_BIN = Redmine::Configuration['scm_command_darcs'] || "darcs"
26 DARCS_BIN = Redmine::Configuration['scm_darcs_command'] || "darcs"
27
27
28 class << self
28 class << self
29 def client_version
29 def client_version
30 @@client_version ||= (darcs_binary_version || [])
30 @@client_version ||= (darcs_binary_version || [])
31 end
31 end
32
32
33 def darcs_binary_version
33 def darcs_binary_version
34 darcsversion = darcs_binary_version_from_command_line
34 darcsversion = darcs_binary_version_from_command_line
35 if m = darcsversion.match(%r{\A(.*?)((\d+\.)+\d+)})
35 if m = darcsversion.match(%r{\A(.*?)((\d+\.)+\d+)})
36 m[2].scan(%r{\d+}).collect(&:to_i)
36 m[2].scan(%r{\d+}).collect(&:to_i)
37 end
37 end
38 end
38 end
39
39
40 def darcs_binary_version_from_command_line
40 def darcs_binary_version_from_command_line
41 shellout("#{DARCS_BIN} --version") { |io| io.read }.to_s
41 shellout("#{DARCS_BIN} --version") { |io| io.read }.to_s
42 end
42 end
43 end
43 end
44
44
45 def initialize(url, root_url=nil, login=nil, password=nil)
45 def initialize(url, root_url=nil, login=nil, password=nil)
46 @url = url
46 @url = url
47 @root_url = url
47 @root_url = url
48 end
48 end
49
49
50 def supports_cat?
50 def supports_cat?
51 # cat supported in darcs 2.0.0 and higher
51 # cat supported in darcs 2.0.0 and higher
52 self.class.client_version_above?([2, 0, 0])
52 self.class.client_version_above?([2, 0, 0])
53 end
53 end
54
54
55 # Get info about the darcs repository
55 # Get info about the darcs repository
56 def info
56 def info
57 rev = revisions(nil,nil,nil,{:limit => 1})
57 rev = revisions(nil,nil,nil,{:limit => 1})
58 rev ? Info.new({:root_url => @url, :lastrev => rev.last}) : nil
58 rev ? Info.new({:root_url => @url, :lastrev => rev.last}) : nil
59 end
59 end
60
60
61 # Returns an Entries collection
61 # Returns an Entries collection
62 # or nil if the given path doesn't exist in the repository
62 # or nil if the given path doesn't exist in the repository
63 def entries(path=nil, identifier=nil)
63 def entries(path=nil, identifier=nil)
64 path_prefix = (path.blank? ? '' : "#{path}/")
64 path_prefix = (path.blank? ? '' : "#{path}/")
65 if path.blank?
65 if path.blank?
66 path = ( self.class.client_version_above?([2, 2, 0]) ? @url : '.' )
66 path = ( self.class.client_version_above?([2, 2, 0]) ? @url : '.' )
67 end
67 end
68 entries = Entries.new
68 entries = Entries.new
69 cmd = "#{DARCS_BIN} annotate --repodir #{shell_quote @url} --xml-output"
69 cmd = "#{DARCS_BIN} annotate --repodir #{shell_quote @url} --xml-output"
70 cmd << " --match #{shell_quote("hash #{identifier}")}" if identifier
70 cmd << " --match #{shell_quote("hash #{identifier}")}" if identifier
71 cmd << " #{shell_quote path}"
71 cmd << " #{shell_quote path}"
72 shellout(cmd) do |io|
72 shellout(cmd) do |io|
73 begin
73 begin
74 doc = REXML::Document.new(io)
74 doc = REXML::Document.new(io)
75 if doc.root.name == 'directory'
75 if doc.root.name == 'directory'
76 doc.elements.each('directory/*') do |element|
76 doc.elements.each('directory/*') do |element|
77 next unless ['file', 'directory'].include? element.name
77 next unless ['file', 'directory'].include? element.name
78 entries << entry_from_xml(element, path_prefix)
78 entries << entry_from_xml(element, path_prefix)
79 end
79 end
80 elsif doc.root.name == 'file'
80 elsif doc.root.name == 'file'
81 entries << entry_from_xml(doc.root, path_prefix)
81 entries << entry_from_xml(doc.root, path_prefix)
82 end
82 end
83 rescue
83 rescue
84 end
84 end
85 end
85 end
86 return nil if $? && $?.exitstatus != 0
86 return nil if $? && $?.exitstatus != 0
87 entries.compact.sort_by_name
87 entries.compact.sort_by_name
88 end
88 end
89
89
90 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
90 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
91 path = '.' if path.blank?
91 path = '.' if path.blank?
92 revisions = Revisions.new
92 revisions = Revisions.new
93 cmd = "#{DARCS_BIN} changes --repodir #{shell_quote @url} --xml-output"
93 cmd = "#{DARCS_BIN} changes --repodir #{shell_quote @url} --xml-output"
94 cmd << " --from-match #{shell_quote("hash #{identifier_from}")}" if identifier_from
94 cmd << " --from-match #{shell_quote("hash #{identifier_from}")}" if identifier_from
95 cmd << " --last #{options[:limit].to_i}" if options[:limit]
95 cmd << " --last #{options[:limit].to_i}" if options[:limit]
96 shellout(cmd) do |io|
96 shellout(cmd) do |io|
97 begin
97 begin
98 doc = REXML::Document.new(io)
98 doc = REXML::Document.new(io)
99 doc.elements.each("changelog/patch") do |patch|
99 doc.elements.each("changelog/patch") do |patch|
100 message = patch.elements['name'].text
100 message = patch.elements['name'].text
101 message << "\n" + patch.elements['comment'].text.gsub(/\*\*\*END OF DESCRIPTION\*\*\*.*\z/m, '') if patch.elements['comment']
101 message << "\n" + patch.elements['comment'].text.gsub(/\*\*\*END OF DESCRIPTION\*\*\*.*\z/m, '') if patch.elements['comment']
102 revisions << Revision.new({:identifier => nil,
102 revisions << Revision.new({:identifier => nil,
103 :author => patch.attributes['author'],
103 :author => patch.attributes['author'],
104 :scmid => patch.attributes['hash'],
104 :scmid => patch.attributes['hash'],
105 :time => Time.parse(patch.attributes['local_date']),
105 :time => Time.parse(patch.attributes['local_date']),
106 :message => message,
106 :message => message,
107 :paths => (options[:with_path] ? get_paths_for_patch(patch.attributes['hash']) : nil)
107 :paths => (options[:with_path] ? get_paths_for_patch(patch.attributes['hash']) : nil)
108 })
108 })
109 end
109 end
110 rescue
110 rescue
111 end
111 end
112 end
112 end
113 return nil if $? && $?.exitstatus != 0
113 return nil if $? && $?.exitstatus != 0
114 revisions
114 revisions
115 end
115 end
116
116
117 def diff(path, identifier_from, identifier_to=nil)
117 def diff(path, identifier_from, identifier_to=nil)
118 path = '*' if path.blank?
118 path = '*' if path.blank?
119 cmd = "#{DARCS_BIN} diff --repodir #{shell_quote @url}"
119 cmd = "#{DARCS_BIN} diff --repodir #{shell_quote @url}"
120 if identifier_to.nil?
120 if identifier_to.nil?
121 cmd << " --match #{shell_quote("hash #{identifier_from}")}"
121 cmd << " --match #{shell_quote("hash #{identifier_from}")}"
122 else
122 else
123 cmd << " --to-match #{shell_quote("hash #{identifier_from}")}"
123 cmd << " --to-match #{shell_quote("hash #{identifier_from}")}"
124 cmd << " --from-match #{shell_quote("hash #{identifier_to}")}"
124 cmd << " --from-match #{shell_quote("hash #{identifier_to}")}"
125 end
125 end
126 cmd << " -u #{shell_quote path}"
126 cmd << " -u #{shell_quote path}"
127 diff = []
127 diff = []
128 shellout(cmd) do |io|
128 shellout(cmd) do |io|
129 io.each_line do |line|
129 io.each_line do |line|
130 diff << line
130 diff << line
131 end
131 end
132 end
132 end
133 return nil if $? && $?.exitstatus != 0
133 return nil if $? && $?.exitstatus != 0
134 diff
134 diff
135 end
135 end
136
136
137 def cat(path, identifier=nil)
137 def cat(path, identifier=nil)
138 cmd = "#{DARCS_BIN} show content --repodir #{shell_quote @url}"
138 cmd = "#{DARCS_BIN} show content --repodir #{shell_quote @url}"
139 cmd << " --match #{shell_quote("hash #{identifier}")}" if identifier
139 cmd << " --match #{shell_quote("hash #{identifier}")}" if identifier
140 cmd << " #{shell_quote path}"
140 cmd << " #{shell_quote path}"
141 cat = nil
141 cat = nil
142 shellout(cmd) do |io|
142 shellout(cmd) do |io|
143 io.binmode
143 io.binmode
144 cat = io.read
144 cat = io.read
145 end
145 end
146 return nil if $? && $?.exitstatus != 0
146 return nil if $? && $?.exitstatus != 0
147 cat
147 cat
148 end
148 end
149
149
150 private
150 private
151
151
152 # Returns an Entry from the given XML element
152 # Returns an Entry from the given XML element
153 # or nil if the entry was deleted
153 # or nil if the entry was deleted
154 def entry_from_xml(element, path_prefix)
154 def entry_from_xml(element, path_prefix)
155 modified_element = element.elements['modified']
155 modified_element = element.elements['modified']
156 if modified_element.elements['modified_how'].text.match(/removed/)
156 if modified_element.elements['modified_how'].text.match(/removed/)
157 return nil
157 return nil
158 end
158 end
159
159
160 Entry.new({:name => element.attributes['name'],
160 Entry.new({:name => element.attributes['name'],
161 :path => path_prefix + element.attributes['name'],
161 :path => path_prefix + element.attributes['name'],
162 :kind => element.name == 'file' ? 'file' : 'dir',
162 :kind => element.name == 'file' ? 'file' : 'dir',
163 :size => nil,
163 :size => nil,
164 :lastrev => Revision.new({
164 :lastrev => Revision.new({
165 :identifier => nil,
165 :identifier => nil,
166 :scmid => modified_element.elements['patch'].attributes['hash']
166 :scmid => modified_element.elements['patch'].attributes['hash']
167 })
167 })
168 })
168 })
169 end
169 end
170
170
171 def get_paths_for_patch(hash)
171 def get_paths_for_patch(hash)
172 paths = get_paths_for_patch_raw(hash)
172 paths = get_paths_for_patch_raw(hash)
173 if self.class.client_version_above?([2, 4])
173 if self.class.client_version_above?([2, 4])
174 orig_paths = paths
174 orig_paths = paths
175 paths = []
175 paths = []
176 add_paths = []
176 add_paths = []
177 add_paths_name = []
177 add_paths_name = []
178 mod_paths = []
178 mod_paths = []
179 other_paths = []
179 other_paths = []
180 orig_paths.each do |path|
180 orig_paths.each do |path|
181 if path[:action] == 'A'
181 if path[:action] == 'A'
182 add_paths << path
182 add_paths << path
183 add_paths_name << path[:path]
183 add_paths_name << path[:path]
184 elsif path[:action] == 'M'
184 elsif path[:action] == 'M'
185 mod_paths << path
185 mod_paths << path
186 else
186 else
187 other_paths << path
187 other_paths << path
188 end
188 end
189 end
189 end
190 add_paths_name.each do |add_path|
190 add_paths_name.each do |add_path|
191 mod_paths.delete_if { |m| m[:path] == add_path }
191 mod_paths.delete_if { |m| m[:path] == add_path }
192 end
192 end
193 paths.concat add_paths
193 paths.concat add_paths
194 paths.concat mod_paths
194 paths.concat mod_paths
195 paths.concat other_paths
195 paths.concat other_paths
196 end
196 end
197 paths
197 paths
198 end
198 end
199
199
200 # Retrieve changed paths for a single patch
200 # Retrieve changed paths for a single patch
201 def get_paths_for_patch_raw(hash)
201 def get_paths_for_patch_raw(hash)
202 cmd = "#{DARCS_BIN} annotate --repodir #{shell_quote @url} --summary --xml-output"
202 cmd = "#{DARCS_BIN} annotate --repodir #{shell_quote @url} --summary --xml-output"
203 cmd << " --match #{shell_quote("hash #{hash}")} "
203 cmd << " --match #{shell_quote("hash #{hash}")} "
204 paths = []
204 paths = []
205 shellout(cmd) do |io|
205 shellout(cmd) do |io|
206 begin
206 begin
207 # Darcs xml output has multiple root elements in this case (tested with darcs 1.0.7)
207 # Darcs xml output has multiple root elements in this case (tested with darcs 1.0.7)
208 # A root element is added so that REXML doesn't raise an error
208 # A root element is added so that REXML doesn't raise an error
209 doc = REXML::Document.new("<fake_root>" + io.read + "</fake_root>")
209 doc = REXML::Document.new("<fake_root>" + io.read + "</fake_root>")
210 doc.elements.each('fake_root/summary/*') do |modif|
210 doc.elements.each('fake_root/summary/*') do |modif|
211 paths << {:action => modif.name[0,1].upcase,
211 paths << {:action => modif.name[0,1].upcase,
212 :path => "/" + modif.text.chomp.gsub(/^\s*/, '')
212 :path => "/" + modif.text.chomp.gsub(/^\s*/, '')
213 }
213 }
214 end
214 end
215 rescue
215 rescue
216 end
216 end
217 end
217 end
218 paths
218 paths
219 rescue CommandFailed
219 rescue CommandFailed
220 paths
220 paths
221 end
221 end
222 end
222 end
223 end
223 end
224 end
224 end
225 end
225 end
@@ -1,277 +1,277
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require 'redmine/scm/adapters/abstract_adapter'
18 require 'redmine/scm/adapters/abstract_adapter'
19
19
20 module Redmine
20 module Redmine
21 module Scm
21 module Scm
22 module Adapters
22 module Adapters
23 class GitAdapter < AbstractAdapter
23 class GitAdapter < AbstractAdapter
24 # Git executable name
24 # Git executable name
25 GIT_BIN = Redmine::Configuration['scm_command_git'] || "git"
25 GIT_BIN = Redmine::Configuration['scm_git_command'] || "git"
26
26
27 def info
27 def info
28 begin
28 begin
29 Info.new(:root_url => url, :lastrev => lastrev('',nil))
29 Info.new(:root_url => url, :lastrev => lastrev('',nil))
30 rescue
30 rescue
31 nil
31 nil
32 end
32 end
33 end
33 end
34
34
35 def branches
35 def branches
36 return @branches if @branches
36 return @branches if @branches
37 @branches = []
37 @branches = []
38 cmd = "#{GIT_BIN} --git-dir #{target('')} branch --no-color"
38 cmd = "#{GIT_BIN} --git-dir #{target('')} branch --no-color"
39 shellout(cmd) do |io|
39 shellout(cmd) do |io|
40 io.each_line do |line|
40 io.each_line do |line|
41 @branches << line.match('\s*\*?\s*(.*)$')[1]
41 @branches << line.match('\s*\*?\s*(.*)$')[1]
42 end
42 end
43 end
43 end
44 @branches.sort!
44 @branches.sort!
45 end
45 end
46
46
47 def tags
47 def tags
48 return @tags if @tags
48 return @tags if @tags
49 cmd = "#{GIT_BIN} --git-dir #{target('')} tag"
49 cmd = "#{GIT_BIN} --git-dir #{target('')} tag"
50 shellout(cmd) do |io|
50 shellout(cmd) do |io|
51 @tags = io.readlines.sort!.map{|t| t.strip}
51 @tags = io.readlines.sort!.map{|t| t.strip}
52 end
52 end
53 end
53 end
54
54
55 def default_branch
55 def default_branch
56 branches.include?('master') ? 'master' : branches.first
56 branches.include?('master') ? 'master' : branches.first
57 end
57 end
58
58
59 def entries(path=nil, identifier=nil)
59 def entries(path=nil, identifier=nil)
60 path ||= ''
60 path ||= ''
61 entries = Entries.new
61 entries = Entries.new
62 cmd = "#{GIT_BIN} --git-dir #{target('')} ls-tree -l "
62 cmd = "#{GIT_BIN} --git-dir #{target('')} ls-tree -l "
63 cmd << shell_quote("HEAD:" + path) if identifier.nil?
63 cmd << shell_quote("HEAD:" + path) if identifier.nil?
64 cmd << shell_quote(identifier + ":" + path) if identifier
64 cmd << shell_quote(identifier + ":" + path) if identifier
65 shellout(cmd) do |io|
65 shellout(cmd) do |io|
66 io.each_line do |line|
66 io.each_line do |line|
67 e = line.chomp.to_s
67 e = line.chomp.to_s
68 if e =~ /^\d+\s+(\w+)\s+([0-9a-f]{40})\s+([0-9-]+)\t(.+)$/
68 if e =~ /^\d+\s+(\w+)\s+([0-9a-f]{40})\s+([0-9-]+)\t(.+)$/
69 type = $1
69 type = $1
70 sha = $2
70 sha = $2
71 size = $3
71 size = $3
72 name = $4
72 name = $4
73 full_path = path.empty? ? name : "#{path}/#{name}"
73 full_path = path.empty? ? name : "#{path}/#{name}"
74 entries << Entry.new({:name => name,
74 entries << Entry.new({:name => name,
75 :path => full_path,
75 :path => full_path,
76 :kind => (type == "tree") ? 'dir' : 'file',
76 :kind => (type == "tree") ? 'dir' : 'file',
77 :size => (type == "tree") ? nil : size,
77 :size => (type == "tree") ? nil : size,
78 :lastrev => lastrev(full_path,identifier)
78 :lastrev => lastrev(full_path,identifier)
79 }) unless entries.detect{|entry| entry.name == name}
79 }) unless entries.detect{|entry| entry.name == name}
80 end
80 end
81 end
81 end
82 end
82 end
83 return nil if $? && $?.exitstatus != 0
83 return nil if $? && $?.exitstatus != 0
84 entries.sort_by_name
84 entries.sort_by_name
85 end
85 end
86
86
87 def lastrev(path,rev)
87 def lastrev(path,rev)
88 return nil if path.nil?
88 return nil if path.nil?
89 cmd = "#{GIT_BIN} --git-dir #{target('')} log --no-color --date=iso --pretty=fuller --no-merges -n 1 "
89 cmd = "#{GIT_BIN} --git-dir #{target('')} log --no-color --date=iso --pretty=fuller --no-merges -n 1 "
90 cmd << " #{shell_quote rev} " if rev
90 cmd << " #{shell_quote rev} " if rev
91 cmd << "-- #{shell_quote path} " unless path.empty?
91 cmd << "-- #{shell_quote path} " unless path.empty?
92 lines = []
92 lines = []
93 shellout(cmd) { |io| lines = io.readlines }
93 shellout(cmd) { |io| lines = io.readlines }
94 return nil if $? && $?.exitstatus != 0
94 return nil if $? && $?.exitstatus != 0
95 begin
95 begin
96 id = lines[0].split[1]
96 id = lines[0].split[1]
97 author = lines[1].match('Author:\s+(.*)$')[1]
97 author = lines[1].match('Author:\s+(.*)$')[1]
98 time = Time.parse(lines[4].match('CommitDate:\s+(.*)$')[1]).localtime
98 time = Time.parse(lines[4].match('CommitDate:\s+(.*)$')[1]).localtime
99
99
100 Revision.new({
100 Revision.new({
101 :identifier => id,
101 :identifier => id,
102 :scmid => id,
102 :scmid => id,
103 :author => author,
103 :author => author,
104 :time => time,
104 :time => time,
105 :message => nil,
105 :message => nil,
106 :paths => nil
106 :paths => nil
107 })
107 })
108 rescue NoMethodError => e
108 rescue NoMethodError => e
109 logger.error("The revision '#{path}' has a wrong format")
109 logger.error("The revision '#{path}' has a wrong format")
110 return nil
110 return nil
111 end
111 end
112 end
112 end
113
113
114 def revisions(path, identifier_from, identifier_to, options={})
114 def revisions(path, identifier_from, identifier_to, options={})
115 revisions = Revisions.new
115 revisions = Revisions.new
116
116
117 cmd = "#{GIT_BIN} --git-dir #{target('')} log --no-color --raw --date=iso --pretty=fuller "
117 cmd = "#{GIT_BIN} --git-dir #{target('')} log --no-color --raw --date=iso --pretty=fuller "
118 cmd << " --reverse " if options[:reverse]
118 cmd << " --reverse " if options[:reverse]
119 cmd << " --all " if options[:all]
119 cmd << " --all " if options[:all]
120 cmd << " -n #{options[:limit].to_i} " if options[:limit]
120 cmd << " -n #{options[:limit].to_i} " if options[:limit]
121 cmd << "#{shell_quote(identifier_from + '..')}" if identifier_from
121 cmd << "#{shell_quote(identifier_from + '..')}" if identifier_from
122 cmd << "#{shell_quote identifier_to}" if identifier_to
122 cmd << "#{shell_quote identifier_to}" if identifier_to
123 cmd << " --since=#{shell_quote(options[:since].strftime("%Y-%m-%d %H:%M:%S"))}" if options[:since]
123 cmd << " --since=#{shell_quote(options[:since].strftime("%Y-%m-%d %H:%M:%S"))}" if options[:since]
124 cmd << " -- #{shell_quote path}" if path && !path.empty?
124 cmd << " -- #{shell_quote path}" if path && !path.empty?
125
125
126 shellout(cmd) do |io|
126 shellout(cmd) do |io|
127 files=[]
127 files=[]
128 changeset = {}
128 changeset = {}
129 parsing_descr = 0 #0: not parsing desc or files, 1: parsing desc, 2: parsing files
129 parsing_descr = 0 #0: not parsing desc or files, 1: parsing desc, 2: parsing files
130 revno = 1
130 revno = 1
131
131
132 io.each_line do |line|
132 io.each_line do |line|
133 if line =~ /^commit ([0-9a-f]{40})$/
133 if line =~ /^commit ([0-9a-f]{40})$/
134 key = "commit"
134 key = "commit"
135 value = $1
135 value = $1
136 if (parsing_descr == 1 || parsing_descr == 2)
136 if (parsing_descr == 1 || parsing_descr == 2)
137 parsing_descr = 0
137 parsing_descr = 0
138 revision = Revision.new({
138 revision = Revision.new({
139 :identifier => changeset[:commit],
139 :identifier => changeset[:commit],
140 :scmid => changeset[:commit],
140 :scmid => changeset[:commit],
141 :author => changeset[:author],
141 :author => changeset[:author],
142 :time => Time.parse(changeset[:date]),
142 :time => Time.parse(changeset[:date]),
143 :message => changeset[:description],
143 :message => changeset[:description],
144 :paths => files
144 :paths => files
145 })
145 })
146 if block_given?
146 if block_given?
147 yield revision
147 yield revision
148 else
148 else
149 revisions << revision
149 revisions << revision
150 end
150 end
151 changeset = {}
151 changeset = {}
152 files = []
152 files = []
153 revno = revno + 1
153 revno = revno + 1
154 end
154 end
155 changeset[:commit] = $1
155 changeset[:commit] = $1
156 elsif (parsing_descr == 0) && line =~ /^(\w+):\s*(.*)$/
156 elsif (parsing_descr == 0) && line =~ /^(\w+):\s*(.*)$/
157 key = $1
157 key = $1
158 value = $2
158 value = $2
159 if key == "Author"
159 if key == "Author"
160 changeset[:author] = value
160 changeset[:author] = value
161 elsif key == "CommitDate"
161 elsif key == "CommitDate"
162 changeset[:date] = value
162 changeset[:date] = value
163 end
163 end
164 elsif (parsing_descr == 0) && line.chomp.to_s == ""
164 elsif (parsing_descr == 0) && line.chomp.to_s == ""
165 parsing_descr = 1
165 parsing_descr = 1
166 changeset[:description] = ""
166 changeset[:description] = ""
167 elsif (parsing_descr == 1 || parsing_descr == 2) \
167 elsif (parsing_descr == 1 || parsing_descr == 2) \
168 && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\t(.+)$/
168 && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\t(.+)$/
169 parsing_descr = 2
169 parsing_descr = 2
170 fileaction = $1
170 fileaction = $1
171 filepath = $2
171 filepath = $2
172 files << {:action => fileaction, :path => filepath}
172 files << {:action => fileaction, :path => filepath}
173 elsif (parsing_descr == 1 || parsing_descr == 2) \
173 elsif (parsing_descr == 1 || parsing_descr == 2) \
174 && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\d+\s+(\S+)\t(.+)$/
174 && line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\d+\s+(\S+)\t(.+)$/
175 parsing_descr = 2
175 parsing_descr = 2
176 fileaction = $1
176 fileaction = $1
177 filepath = $3
177 filepath = $3
178 files << {:action => fileaction, :path => filepath}
178 files << {:action => fileaction, :path => filepath}
179 elsif (parsing_descr == 1) && line.chomp.to_s == ""
179 elsif (parsing_descr == 1) && line.chomp.to_s == ""
180 parsing_descr = 2
180 parsing_descr = 2
181 elsif (parsing_descr == 1)
181 elsif (parsing_descr == 1)
182 changeset[:description] << line[4..-1]
182 changeset[:description] << line[4..-1]
183 end
183 end
184 end
184 end
185
185
186 if changeset[:commit]
186 if changeset[:commit]
187 revision = Revision.new({
187 revision = Revision.new({
188 :identifier => changeset[:commit],
188 :identifier => changeset[:commit],
189 :scmid => changeset[:commit],
189 :scmid => changeset[:commit],
190 :author => changeset[:author],
190 :author => changeset[:author],
191 :time => Time.parse(changeset[:date]),
191 :time => Time.parse(changeset[:date]),
192 :message => changeset[:description],
192 :message => changeset[:description],
193 :paths => files
193 :paths => files
194 })
194 })
195
195
196 if block_given?
196 if block_given?
197 yield revision
197 yield revision
198 else
198 else
199 revisions << revision
199 revisions << revision
200 end
200 end
201 end
201 end
202 end
202 end
203
203
204 return nil if $? && $?.exitstatus != 0
204 return nil if $? && $?.exitstatus != 0
205 revisions
205 revisions
206 end
206 end
207
207
208 def diff(path, identifier_from, identifier_to=nil)
208 def diff(path, identifier_from, identifier_to=nil)
209 path ||= ''
209 path ||= ''
210
210
211 if identifier_to
211 if identifier_to
212 cmd = "#{GIT_BIN} --git-dir #{target('')} diff --no-color #{shell_quote identifier_to} #{shell_quote identifier_from}"
212 cmd = "#{GIT_BIN} --git-dir #{target('')} diff --no-color #{shell_quote identifier_to} #{shell_quote identifier_from}"
213 else
213 else
214 cmd = "#{GIT_BIN} --git-dir #{target('')} show --no-color #{shell_quote identifier_from}"
214 cmd = "#{GIT_BIN} --git-dir #{target('')} show --no-color #{shell_quote identifier_from}"
215 end
215 end
216
216
217 cmd << " -- #{shell_quote path}" unless path.empty?
217 cmd << " -- #{shell_quote path}" unless path.empty?
218 diff = []
218 diff = []
219 shellout(cmd) do |io|
219 shellout(cmd) do |io|
220 io.each_line do |line|
220 io.each_line do |line|
221 diff << line
221 diff << line
222 end
222 end
223 end
223 end
224 return nil if $? && $?.exitstatus != 0
224 return nil if $? && $?.exitstatus != 0
225 diff
225 diff
226 end
226 end
227
227
228 def annotate(path, identifier=nil)
228 def annotate(path, identifier=nil)
229 identifier = 'HEAD' if identifier.blank?
229 identifier = 'HEAD' if identifier.blank?
230 cmd = "#{GIT_BIN} --git-dir #{target('')} blame -p #{shell_quote identifier} -- #{shell_quote path}"
230 cmd = "#{GIT_BIN} --git-dir #{target('')} blame -p #{shell_quote identifier} -- #{shell_quote path}"
231 blame = Annotate.new
231 blame = Annotate.new
232 content = nil
232 content = nil
233 shellout(cmd) { |io| io.binmode; content = io.read }
233 shellout(cmd) { |io| io.binmode; content = io.read }
234 return nil if $? && $?.exitstatus != 0
234 return nil if $? && $?.exitstatus != 0
235 # git annotates binary files
235 # git annotates binary files
236 return nil if content.is_binary_data?
236 return nil if content.is_binary_data?
237 identifier = ''
237 identifier = ''
238 # git shows commit author on the first occurrence only
238 # git shows commit author on the first occurrence only
239 authors_by_commit = {}
239 authors_by_commit = {}
240 content.split("\n").each do |line|
240 content.split("\n").each do |line|
241 if line =~ /^([0-9a-f]{39,40})\s.*/
241 if line =~ /^([0-9a-f]{39,40})\s.*/
242 identifier = $1
242 identifier = $1
243 elsif line =~ /^author (.+)/
243 elsif line =~ /^author (.+)/
244 authors_by_commit[identifier] = $1.strip
244 authors_by_commit[identifier] = $1.strip
245 elsif line =~ /^\t(.*)/
245 elsif line =~ /^\t(.*)/
246 blame.add_line($1, Revision.new(:identifier => identifier, :author => authors_by_commit[identifier]))
246 blame.add_line($1, Revision.new(:identifier => identifier, :author => authors_by_commit[identifier]))
247 identifier = ''
247 identifier = ''
248 author = ''
248 author = ''
249 end
249 end
250 end
250 end
251 blame
251 blame
252 end
252 end
253
253
254 def cat(path, identifier=nil)
254 def cat(path, identifier=nil)
255 if identifier.nil?
255 if identifier.nil?
256 identifier = 'HEAD'
256 identifier = 'HEAD'
257 end
257 end
258 cmd = "#{GIT_BIN} --git-dir #{target('')} show --no-color #{shell_quote(identifier + ':' + path)}"
258 cmd = "#{GIT_BIN} --git-dir #{target('')} show --no-color #{shell_quote(identifier + ':' + path)}"
259 cat = nil
259 cat = nil
260 shellout(cmd) do |io|
260 shellout(cmd) do |io|
261 io.binmode
261 io.binmode
262 cat = io.read
262 cat = io.read
263 end
263 end
264 return nil if $? && $?.exitstatus != 0
264 return nil if $? && $?.exitstatus != 0
265 cat
265 cat
266 end
266 end
267
267
268 class Revision < Redmine::Scm::Adapters::Revision
268 class Revision < Redmine::Scm::Adapters::Revision
269 # Returns the readable identifier
269 # Returns the readable identifier
270 def format_identifier
270 def format_identifier
271 identifier[0,8]
271 identifier[0,8]
272 end
272 end
273 end
273 end
274 end
274 end
275 end
275 end
276 end
276 end
277 end
277 end
@@ -1,225 +1,225
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require 'redmine/scm/adapters/abstract_adapter'
18 require 'redmine/scm/adapters/abstract_adapter'
19 require 'cgi'
19 require 'cgi'
20
20
21 module Redmine
21 module Redmine
22 module Scm
22 module Scm
23 module Adapters
23 module Adapters
24 class MercurialAdapter < AbstractAdapter
24 class MercurialAdapter < AbstractAdapter
25
25
26 # Mercurial executable name
26 # Mercurial executable name
27 HG_BIN = Redmine::Configuration['scm_command_mercurial'] || "hg"
27 HG_BIN = Redmine::Configuration['scm_mercurial_command'] || "hg"
28 TEMPLATES_DIR = File.dirname(__FILE__) + "/mercurial"
28 TEMPLATES_DIR = File.dirname(__FILE__) + "/mercurial"
29 TEMPLATE_NAME = "hg-template"
29 TEMPLATE_NAME = "hg-template"
30 TEMPLATE_EXTENSION = "tmpl"
30 TEMPLATE_EXTENSION = "tmpl"
31
31
32 class << self
32 class << self
33 def client_version
33 def client_version
34 @@client_version ||= (hgversion || [])
34 @@client_version ||= (hgversion || [])
35 end
35 end
36
36
37 def hgversion
37 def hgversion
38 # The hg version is expressed either as a
38 # The hg version is expressed either as a
39 # release number (eg 0.9.5 or 1.0) or as a revision
39 # release number (eg 0.9.5 or 1.0) or as a revision
40 # id composed of 12 hexa characters.
40 # id composed of 12 hexa characters.
41 theversion = hgversion_from_command_line
41 theversion = hgversion_from_command_line
42 if m = theversion.match(%r{\A(.*?)((\d+\.)+\d+)})
42 if m = theversion.match(%r{\A(.*?)((\d+\.)+\d+)})
43 m[2].scan(%r{\d+}).collect(&:to_i)
43 m[2].scan(%r{\d+}).collect(&:to_i)
44 end
44 end
45 end
45 end
46
46
47 def hgversion_from_command_line
47 def hgversion_from_command_line
48 shellout("#{HG_BIN} --version") { |io| io.read }.to_s
48 shellout("#{HG_BIN} --version") { |io| io.read }.to_s
49 end
49 end
50
50
51 def template_path
51 def template_path
52 @@template_path ||= template_path_for(client_version)
52 @@template_path ||= template_path_for(client_version)
53 end
53 end
54
54
55 def template_path_for(version)
55 def template_path_for(version)
56 if ((version <=> [0,9,5]) > 0) || version.empty?
56 if ((version <=> [0,9,5]) > 0) || version.empty?
57 ver = "1.0"
57 ver = "1.0"
58 else
58 else
59 ver = "0.9.5"
59 ver = "0.9.5"
60 end
60 end
61 "#{TEMPLATES_DIR}/#{TEMPLATE_NAME}-#{ver}.#{TEMPLATE_EXTENSION}"
61 "#{TEMPLATES_DIR}/#{TEMPLATE_NAME}-#{ver}.#{TEMPLATE_EXTENSION}"
62 end
62 end
63 end
63 end
64
64
65 def info
65 def info
66 cmd = "#{HG_BIN} -R #{target('')} root"
66 cmd = "#{HG_BIN} -R #{target('')} root"
67 root_url = nil
67 root_url = nil
68 shellout(cmd) do |io|
68 shellout(cmd) do |io|
69 root_url = io.read
69 root_url = io.read
70 end
70 end
71 return nil if $? && $?.exitstatus != 0
71 return nil if $? && $?.exitstatus != 0
72 info = Info.new({:root_url => root_url.chomp,
72 info = Info.new({:root_url => root_url.chomp,
73 :lastrev => revisions(nil,nil,nil,{:limit => 1}).last
73 :lastrev => revisions(nil,nil,nil,{:limit => 1}).last
74 })
74 })
75 info
75 info
76 rescue CommandFailed
76 rescue CommandFailed
77 return nil
77 return nil
78 end
78 end
79
79
80 def entries(path=nil, identifier=nil)
80 def entries(path=nil, identifier=nil)
81 path ||= ''
81 path ||= ''
82 entries = Entries.new
82 entries = Entries.new
83 cmd = "#{HG_BIN} -R #{target('')} --cwd #{target('')} locate"
83 cmd = "#{HG_BIN} -R #{target('')} --cwd #{target('')} locate"
84 cmd << " -r #{hgrev(identifier)}"
84 cmd << " -r #{hgrev(identifier)}"
85 cmd << " " + shell_quote("path:#{path}") unless path.empty?
85 cmd << " " + shell_quote("path:#{path}") unless path.empty?
86 shellout(cmd) do |io|
86 shellout(cmd) do |io|
87 io.each_line do |line|
87 io.each_line do |line|
88 # HG uses antislashs as separator on Windows
88 # HG uses antislashs as separator on Windows
89 line = line.gsub(/\\/, "/")
89 line = line.gsub(/\\/, "/")
90 if path.empty? or e = line.gsub!(%r{^#{with_trailling_slash(path)}},'')
90 if path.empty? or e = line.gsub!(%r{^#{with_trailling_slash(path)}},'')
91 e ||= line
91 e ||= line
92 e = e.chomp.split(%r{[\/\\]})
92 e = e.chomp.split(%r{[\/\\]})
93 entries << Entry.new({:name => e.first,
93 entries << Entry.new({:name => e.first,
94 :path => (path.nil? or path.empty? ? e.first : "#{with_trailling_slash(path)}#{e.first}"),
94 :path => (path.nil? or path.empty? ? e.first : "#{with_trailling_slash(path)}#{e.first}"),
95 :kind => (e.size > 1 ? 'dir' : 'file'),
95 :kind => (e.size > 1 ? 'dir' : 'file'),
96 :lastrev => Revision.new
96 :lastrev => Revision.new
97 }) unless e.empty? || entries.detect{|entry| entry.name == e.first}
97 }) unless e.empty? || entries.detect{|entry| entry.name == e.first}
98 end
98 end
99 end
99 end
100 end
100 end
101 return nil if $? && $?.exitstatus != 0
101 return nil if $? && $?.exitstatus != 0
102 entries.sort_by_name
102 entries.sort_by_name
103 end
103 end
104
104
105 # Fetch the revisions by using a template file that
105 # Fetch the revisions by using a template file that
106 # makes Mercurial produce a xml output.
106 # makes Mercurial produce a xml output.
107 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
107 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
108 revisions = Revisions.new
108 revisions = Revisions.new
109 cmd = "#{HG_BIN} --debug --encoding utf8 -R #{target('')} log -C --style #{shell_quote self.class.template_path}"
109 cmd = "#{HG_BIN} --debug --encoding utf8 -R #{target('')} log -C --style #{shell_quote self.class.template_path}"
110 if identifier_from && identifier_to
110 if identifier_from && identifier_to
111 cmd << " -r #{hgrev(identifier_from)}:#{hgrev(identifier_to)}"
111 cmd << " -r #{hgrev(identifier_from)}:#{hgrev(identifier_to)}"
112 elsif identifier_from
112 elsif identifier_from
113 cmd << " -r #{hgrev(identifier_from)}:"
113 cmd << " -r #{hgrev(identifier_from)}:"
114 end
114 end
115 cmd << " --limit #{options[:limit].to_i}" if options[:limit]
115 cmd << " --limit #{options[:limit].to_i}" if options[:limit]
116 cmd << " #{shell_quote path}" unless path.blank?
116 cmd << " #{shell_quote path}" unless path.blank?
117 shellout(cmd) do |io|
117 shellout(cmd) do |io|
118 begin
118 begin
119 # HG doesn't close the XML Document...
119 # HG doesn't close the XML Document...
120 doc = REXML::Document.new(io.read << "</log>")
120 doc = REXML::Document.new(io.read << "</log>")
121 doc.elements.each("log/logentry") do |logentry|
121 doc.elements.each("log/logentry") do |logentry|
122 paths = []
122 paths = []
123 copies = logentry.get_elements('paths/path-copied')
123 copies = logentry.get_elements('paths/path-copied')
124 logentry.elements.each("paths/path") do |path|
124 logentry.elements.each("paths/path") do |path|
125 # Detect if the added file is a copy
125 # Detect if the added file is a copy
126 if path.attributes['action'] == 'A' and c = copies.find{ |e| e.text == path.text }
126 if path.attributes['action'] == 'A' and c = copies.find{ |e| e.text == path.text }
127 from_path = c.attributes['copyfrom-path']
127 from_path = c.attributes['copyfrom-path']
128 from_rev = logentry.attributes['revision']
128 from_rev = logentry.attributes['revision']
129 end
129 end
130 paths << {:action => path.attributes['action'],
130 paths << {:action => path.attributes['action'],
131 :path => "/#{CGI.unescape(path.text)}",
131 :path => "/#{CGI.unescape(path.text)}",
132 :from_path => from_path ? "/#{CGI.unescape(from_path)}" : nil,
132 :from_path => from_path ? "/#{CGI.unescape(from_path)}" : nil,
133 :from_revision => from_rev ? from_rev : nil
133 :from_revision => from_rev ? from_rev : nil
134 }
134 }
135 end
135 end
136 paths.sort! { |x,y| x[:path] <=> y[:path] }
136 paths.sort! { |x,y| x[:path] <=> y[:path] }
137
137
138 revisions << Revision.new({:identifier => logentry.attributes['revision'],
138 revisions << Revision.new({:identifier => logentry.attributes['revision'],
139 :scmid => logentry.attributes['node'],
139 :scmid => logentry.attributes['node'],
140 :author => (logentry.elements['author'] ? logentry.elements['author'].text : ""),
140 :author => (logentry.elements['author'] ? logentry.elements['author'].text : ""),
141 :time => Time.parse(logentry.elements['date'].text).localtime,
141 :time => Time.parse(logentry.elements['date'].text).localtime,
142 :message => logentry.elements['msg'].text,
142 :message => logentry.elements['msg'].text,
143 :paths => paths
143 :paths => paths
144 })
144 })
145 end
145 end
146 rescue
146 rescue
147 logger.debug($!)
147 logger.debug($!)
148 end
148 end
149 end
149 end
150 return nil if $? && $?.exitstatus != 0
150 return nil if $? && $?.exitstatus != 0
151 revisions
151 revisions
152 end
152 end
153
153
154 def diff(path, identifier_from, identifier_to=nil)
154 def diff(path, identifier_from, identifier_to=nil)
155 path ||= ''
155 path ||= ''
156 diff_args = ''
156 diff_args = ''
157 diff = []
157 diff = []
158 if identifier_to
158 if identifier_to
159 diff_args = "-r #{hgrev(identifier_to)} -r #{hgrev(identifier_from)}"
159 diff_args = "-r #{hgrev(identifier_to)} -r #{hgrev(identifier_from)}"
160 else
160 else
161 if self.class.client_version_above?([1, 2])
161 if self.class.client_version_above?([1, 2])
162 diff_args = "-c #{hgrev(identifier_from)}"
162 diff_args = "-c #{hgrev(identifier_from)}"
163 else
163 else
164 return []
164 return []
165 end
165 end
166 end
166 end
167 cmd = "#{HG_BIN} -R #{target('')} --config diff.git=false diff --nodates #{diff_args}"
167 cmd = "#{HG_BIN} -R #{target('')} --config diff.git=false diff --nodates #{diff_args}"
168 cmd << " -I #{target(path)}" unless path.empty?
168 cmd << " -I #{target(path)}" unless path.empty?
169 shellout(cmd) do |io|
169 shellout(cmd) do |io|
170 io.each_line do |line|
170 io.each_line do |line|
171 diff << line
171 diff << line
172 end
172 end
173 end
173 end
174 return nil if $? && $?.exitstatus != 0
174 return nil if $? && $?.exitstatus != 0
175 diff
175 diff
176 end
176 end
177
177
178 def cat(path, identifier=nil)
178 def cat(path, identifier=nil)
179 cmd = "#{HG_BIN} -R #{target('')} cat"
179 cmd = "#{HG_BIN} -R #{target('')} cat"
180 cmd << " -r #{hgrev(identifier)}"
180 cmd << " -r #{hgrev(identifier)}"
181 cmd << " #{target(path)}"
181 cmd << " #{target(path)}"
182 cat = nil
182 cat = nil
183 shellout(cmd) do |io|
183 shellout(cmd) do |io|
184 io.binmode
184 io.binmode
185 cat = io.read
185 cat = io.read
186 end
186 end
187 return nil if $? && $?.exitstatus != 0
187 return nil if $? && $?.exitstatus != 0
188 cat
188 cat
189 end
189 end
190
190
191 def annotate(path, identifier=nil)
191 def annotate(path, identifier=nil)
192 path ||= ''
192 path ||= ''
193 cmd = "#{HG_BIN} -R #{target('')}"
193 cmd = "#{HG_BIN} -R #{target('')}"
194 cmd << " annotate -ncu"
194 cmd << " annotate -ncu"
195 cmd << " -r #{hgrev(identifier)}"
195 cmd << " -r #{hgrev(identifier)}"
196 cmd << " #{target(path)}"
196 cmd << " #{target(path)}"
197 blame = Annotate.new
197 blame = Annotate.new
198 shellout(cmd) do |io|
198 shellout(cmd) do |io|
199 io.each_line do |line|
199 io.each_line do |line|
200 next unless line =~ %r{^([^:]+)\s(\d+)\s([0-9a-f]+):\s(.*)$}
200 next unless line =~ %r{^([^:]+)\s(\d+)\s([0-9a-f]+):\s(.*)$}
201 r = Revision.new(:author => $1.strip, :revision => $2, :scmid => $3,
201 r = Revision.new(:author => $1.strip, :revision => $2, :scmid => $3,
202 :identifier => $3)
202 :identifier => $3)
203 blame.add_line($4.rstrip, r)
203 blame.add_line($4.rstrip, r)
204 end
204 end
205 end
205 end
206 return nil if $? && $?.exitstatus != 0
206 return nil if $? && $?.exitstatus != 0
207 blame
207 blame
208 end
208 end
209
209
210 class Revision < Redmine::Scm::Adapters::Revision
210 class Revision < Redmine::Scm::Adapters::Revision
211 # Returns the readable identifier
211 # Returns the readable identifier
212 def format_identifier
212 def format_identifier
213 "#{revision}:#{scmid}"
213 "#{revision}:#{scmid}"
214 end
214 end
215 end
215 end
216
216
217 # Returns correct revision identifier
217 # Returns correct revision identifier
218 def hgrev(identifier)
218 def hgrev(identifier)
219 shell_quote(identifier.blank? ? 'tip' : identifier.to_s)
219 shell_quote(identifier.blank? ? 'tip' : identifier.to_s)
220 end
220 end
221 private :hgrev
221 private :hgrev
222 end
222 end
223 end
223 end
224 end
224 end
225 end
225 end
@@ -1,256 +1,256
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2010 Jean-Philippe Lang
2 # Copyright (C) 2006-2010 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require 'redmine/scm/adapters/abstract_adapter'
18 require 'redmine/scm/adapters/abstract_adapter'
19 require 'uri'
19 require 'uri'
20
20
21 module Redmine
21 module Redmine
22 module Scm
22 module Scm
23 module Adapters
23 module Adapters
24 class SubversionAdapter < AbstractAdapter
24 class SubversionAdapter < AbstractAdapter
25
25
26 # SVN executable name
26 # SVN executable name
27 SVN_BIN = Redmine::Configuration['scm_command_subversion'] || "svn"
27 SVN_BIN = Redmine::Configuration['scm_subversion_command'] || "svn"
28
28
29 class << self
29 class << self
30 def client_version
30 def client_version
31 @@client_version ||= (svn_binary_version || [])
31 @@client_version ||= (svn_binary_version || [])
32 end
32 end
33
33
34 def svn_binary_version
34 def svn_binary_version
35 cmd = "#{SVN_BIN} --version"
35 cmd = "#{SVN_BIN} --version"
36 version = nil
36 version = nil
37 shellout(cmd) do |io|
37 shellout(cmd) do |io|
38 # Read svn version in first returned line
38 # Read svn version in first returned line
39 if m = io.read.to_s.match(%r{\A(.*?)((\d+\.)+\d+)})
39 if m = io.read.to_s.match(%r{\A(.*?)((\d+\.)+\d+)})
40 version = m[2].scan(%r{\d+}).collect(&:to_i)
40 version = m[2].scan(%r{\d+}).collect(&:to_i)
41 end
41 end
42 end
42 end
43 return nil if $? && $?.exitstatus != 0
43 return nil if $? && $?.exitstatus != 0
44 version
44 version
45 end
45 end
46 end
46 end
47
47
48 # Get info about the svn repository
48 # Get info about the svn repository
49 def info
49 def info
50 cmd = "#{SVN_BIN} info --xml #{target}"
50 cmd = "#{SVN_BIN} info --xml #{target}"
51 cmd << credentials_string
51 cmd << credentials_string
52 info = nil
52 info = nil
53 shellout(cmd) do |io|
53 shellout(cmd) do |io|
54 output = io.read
54 output = io.read
55 begin
55 begin
56 doc = ActiveSupport::XmlMini.parse(output)
56 doc = ActiveSupport::XmlMini.parse(output)
57 #root_url = doc.elements["info/entry/repository/root"].text
57 #root_url = doc.elements["info/entry/repository/root"].text
58 info = Info.new({:root_url => doc['info']['entry']['repository']['root']['__content__'],
58 info = Info.new({:root_url => doc['info']['entry']['repository']['root']['__content__'],
59 :lastrev => Revision.new({
59 :lastrev => Revision.new({
60 :identifier => doc['info']['entry']['commit']['revision'],
60 :identifier => doc['info']['entry']['commit']['revision'],
61 :time => Time.parse(doc['info']['entry']['commit']['date']['__content__']).localtime,
61 :time => Time.parse(doc['info']['entry']['commit']['date']['__content__']).localtime,
62 :author => (doc['info']['entry']['commit']['author'] ? doc['info']['entry']['commit']['author']['__content__'] : "")
62 :author => (doc['info']['entry']['commit']['author'] ? doc['info']['entry']['commit']['author']['__content__'] : "")
63 })
63 })
64 })
64 })
65 rescue
65 rescue
66 end
66 end
67 end
67 end
68 return nil if $? && $?.exitstatus != 0
68 return nil if $? && $?.exitstatus != 0
69 info
69 info
70 rescue CommandFailed
70 rescue CommandFailed
71 return nil
71 return nil
72 end
72 end
73
73
74 # Returns an Entries collection
74 # Returns an Entries collection
75 # or nil if the given path doesn't exist in the repository
75 # or nil if the given path doesn't exist in the repository
76 def entries(path=nil, identifier=nil)
76 def entries(path=nil, identifier=nil)
77 path ||= ''
77 path ||= ''
78 identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
78 identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
79 entries = Entries.new
79 entries = Entries.new
80 cmd = "#{SVN_BIN} list --xml #{target(path)}@#{identifier}"
80 cmd = "#{SVN_BIN} list --xml #{target(path)}@#{identifier}"
81 cmd << credentials_string
81 cmd << credentials_string
82 shellout(cmd) do |io|
82 shellout(cmd) do |io|
83 output = io.read
83 output = io.read
84 begin
84 begin
85 doc = ActiveSupport::XmlMini.parse(output)
85 doc = ActiveSupport::XmlMini.parse(output)
86 each_xml_element(doc['lists']['list'], 'entry') do |entry|
86 each_xml_element(doc['lists']['list'], 'entry') do |entry|
87 commit = entry['commit']
87 commit = entry['commit']
88 commit_date = commit['date']
88 commit_date = commit['date']
89 # Skip directory if there is no commit date (usually that
89 # Skip directory if there is no commit date (usually that
90 # means that we don't have read access to it)
90 # means that we don't have read access to it)
91 next if entry['kind'] == 'dir' && commit_date.nil?
91 next if entry['kind'] == 'dir' && commit_date.nil?
92 name = entry['name']['__content__']
92 name = entry['name']['__content__']
93 entries << Entry.new({:name => URI.unescape(name),
93 entries << Entry.new({:name => URI.unescape(name),
94 :path => ((path.empty? ? "" : "#{path}/") + name),
94 :path => ((path.empty? ? "" : "#{path}/") + name),
95 :kind => entry['kind'],
95 :kind => entry['kind'],
96 :size => ((s = entry['size']) ? s['__content__'].to_i : nil),
96 :size => ((s = entry['size']) ? s['__content__'].to_i : nil),
97 :lastrev => Revision.new({
97 :lastrev => Revision.new({
98 :identifier => commit['revision'],
98 :identifier => commit['revision'],
99 :time => Time.parse(commit_date['__content__'].to_s).localtime,
99 :time => Time.parse(commit_date['__content__'].to_s).localtime,
100 :author => ((a = commit['author']) ? a['__content__'] : nil)
100 :author => ((a = commit['author']) ? a['__content__'] : nil)
101 })
101 })
102 })
102 })
103 end
103 end
104 rescue Exception => e
104 rescue Exception => e
105 logger.error("Error parsing svn output: #{e.message}")
105 logger.error("Error parsing svn output: #{e.message}")
106 logger.error("Output was:\n #{output}")
106 logger.error("Output was:\n #{output}")
107 end
107 end
108 end
108 end
109 return nil if $? && $?.exitstatus != 0
109 return nil if $? && $?.exitstatus != 0
110 logger.debug("Found #{entries.size} entries in the repository for #{target(path)}") if logger && logger.debug?
110 logger.debug("Found #{entries.size} entries in the repository for #{target(path)}") if logger && logger.debug?
111 entries.sort_by_name
111 entries.sort_by_name
112 end
112 end
113
113
114 def properties(path, identifier=nil)
114 def properties(path, identifier=nil)
115 # proplist xml output supported in svn 1.5.0 and higher
115 # proplist xml output supported in svn 1.5.0 and higher
116 return nil unless self.class.client_version_above?([1, 5, 0])
116 return nil unless self.class.client_version_above?([1, 5, 0])
117
117
118 identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
118 identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
119 cmd = "#{SVN_BIN} proplist --verbose --xml #{target(path)}@#{identifier}"
119 cmd = "#{SVN_BIN} proplist --verbose --xml #{target(path)}@#{identifier}"
120 cmd << credentials_string
120 cmd << credentials_string
121 properties = {}
121 properties = {}
122 shellout(cmd) do |io|
122 shellout(cmd) do |io|
123 output = io.read
123 output = io.read
124 begin
124 begin
125 doc = ActiveSupport::XmlMini.parse(output)
125 doc = ActiveSupport::XmlMini.parse(output)
126 each_xml_element(doc['properties']['target'], 'property') do |property|
126 each_xml_element(doc['properties']['target'], 'property') do |property|
127 properties[ property['name'] ] = property['__content__'].to_s
127 properties[ property['name'] ] = property['__content__'].to_s
128 end
128 end
129 rescue
129 rescue
130 end
130 end
131 end
131 end
132 return nil if $? && $?.exitstatus != 0
132 return nil if $? && $?.exitstatus != 0
133 properties
133 properties
134 end
134 end
135
135
136 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
136 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
137 path ||= ''
137 path ||= ''
138 identifier_from = (identifier_from && identifier_from.to_i > 0) ? identifier_from.to_i : "HEAD"
138 identifier_from = (identifier_from && identifier_from.to_i > 0) ? identifier_from.to_i : "HEAD"
139 identifier_to = (identifier_to && identifier_to.to_i > 0) ? identifier_to.to_i : 1
139 identifier_to = (identifier_to && identifier_to.to_i > 0) ? identifier_to.to_i : 1
140 revisions = Revisions.new
140 revisions = Revisions.new
141 cmd = "#{SVN_BIN} log --xml -r #{identifier_from}:#{identifier_to}"
141 cmd = "#{SVN_BIN} log --xml -r #{identifier_from}:#{identifier_to}"
142 cmd << credentials_string
142 cmd << credentials_string
143 cmd << " --verbose " if options[:with_paths]
143 cmd << " --verbose " if options[:with_paths]
144 cmd << " --limit #{options[:limit].to_i}" if options[:limit]
144 cmd << " --limit #{options[:limit].to_i}" if options[:limit]
145 cmd << ' ' + target(path)
145 cmd << ' ' + target(path)
146 shellout(cmd) do |io|
146 shellout(cmd) do |io|
147 output = io.read
147 output = io.read
148 begin
148 begin
149 doc = ActiveSupport::XmlMini.parse(output)
149 doc = ActiveSupport::XmlMini.parse(output)
150 each_xml_element(doc['log'], 'logentry') do |logentry|
150 each_xml_element(doc['log'], 'logentry') do |logentry|
151 paths = []
151 paths = []
152 each_xml_element(logentry['paths'], 'path') do |path|
152 each_xml_element(logentry['paths'], 'path') do |path|
153 paths << {:action => path['action'],
153 paths << {:action => path['action'],
154 :path => path['__content__'],
154 :path => path['__content__'],
155 :from_path => path['copyfrom-path'],
155 :from_path => path['copyfrom-path'],
156 :from_revision => path['copyfrom-rev']
156 :from_revision => path['copyfrom-rev']
157 }
157 }
158 end if logentry['paths'] && logentry['paths']['path']
158 end if logentry['paths'] && logentry['paths']['path']
159 paths.sort! { |x,y| x[:path] <=> y[:path] }
159 paths.sort! { |x,y| x[:path] <=> y[:path] }
160
160
161 revisions << Revision.new({:identifier => logentry['revision'],
161 revisions << Revision.new({:identifier => logentry['revision'],
162 :author => (logentry['author'] ? logentry['author']['__content__'] : ""),
162 :author => (logentry['author'] ? logentry['author']['__content__'] : ""),
163 :time => Time.parse(logentry['date']['__content__'].to_s).localtime,
163 :time => Time.parse(logentry['date']['__content__'].to_s).localtime,
164 :message => logentry['msg']['__content__'],
164 :message => logentry['msg']['__content__'],
165 :paths => paths
165 :paths => paths
166 })
166 })
167 end
167 end
168 rescue
168 rescue
169 end
169 end
170 end
170 end
171 return nil if $? && $?.exitstatus != 0
171 return nil if $? && $?.exitstatus != 0
172 revisions
172 revisions
173 end
173 end
174
174
175 def diff(path, identifier_from, identifier_to=nil, type="inline")
175 def diff(path, identifier_from, identifier_to=nil, type="inline")
176 path ||= ''
176 path ||= ''
177 identifier_from = (identifier_from and identifier_from.to_i > 0) ? identifier_from.to_i : ''
177 identifier_from = (identifier_from and identifier_from.to_i > 0) ? identifier_from.to_i : ''
178 identifier_to = (identifier_to and identifier_to.to_i > 0) ? identifier_to.to_i : (identifier_from.to_i - 1)
178 identifier_to = (identifier_to and identifier_to.to_i > 0) ? identifier_to.to_i : (identifier_from.to_i - 1)
179
179
180 cmd = "#{SVN_BIN} diff -r "
180 cmd = "#{SVN_BIN} diff -r "
181 cmd << "#{identifier_to}:"
181 cmd << "#{identifier_to}:"
182 cmd << "#{identifier_from}"
182 cmd << "#{identifier_from}"
183 cmd << " #{target(path)}@#{identifier_from}"
183 cmd << " #{target(path)}@#{identifier_from}"
184 cmd << credentials_string
184 cmd << credentials_string
185 diff = []
185 diff = []
186 shellout(cmd) do |io|
186 shellout(cmd) do |io|
187 io.each_line do |line|
187 io.each_line do |line|
188 diff << line
188 diff << line
189 end
189 end
190 end
190 end
191 return nil if $? && $?.exitstatus != 0
191 return nil if $? && $?.exitstatus != 0
192 diff
192 diff
193 end
193 end
194
194
195 def cat(path, identifier=nil)
195 def cat(path, identifier=nil)
196 identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
196 identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
197 cmd = "#{SVN_BIN} cat #{target(path)}@#{identifier}"
197 cmd = "#{SVN_BIN} cat #{target(path)}@#{identifier}"
198 cmd << credentials_string
198 cmd << credentials_string
199 cat = nil
199 cat = nil
200 shellout(cmd) do |io|
200 shellout(cmd) do |io|
201 io.binmode
201 io.binmode
202 cat = io.read
202 cat = io.read
203 end
203 end
204 return nil if $? && $?.exitstatus != 0
204 return nil if $? && $?.exitstatus != 0
205 cat
205 cat
206 end
206 end
207
207
208 def annotate(path, identifier=nil)
208 def annotate(path, identifier=nil)
209 identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
209 identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
210 cmd = "#{SVN_BIN} blame #{target(path)}@#{identifier}"
210 cmd = "#{SVN_BIN} blame #{target(path)}@#{identifier}"
211 cmd << credentials_string
211 cmd << credentials_string
212 blame = Annotate.new
212 blame = Annotate.new
213 shellout(cmd) do |io|
213 shellout(cmd) do |io|
214 io.each_line do |line|
214 io.each_line do |line|
215 next unless line =~ %r{^\s*(\d+)\s*(\S+)\s(.*)$}
215 next unless line =~ %r{^\s*(\d+)\s*(\S+)\s(.*)$}
216 blame.add_line($3.rstrip, Revision.new(:identifier => $1.to_i, :author => $2.strip))
216 blame.add_line($3.rstrip, Revision.new(:identifier => $1.to_i, :author => $2.strip))
217 end
217 end
218 end
218 end
219 return nil if $? && $?.exitstatus != 0
219 return nil if $? && $?.exitstatus != 0
220 blame
220 blame
221 end
221 end
222
222
223 private
223 private
224
224
225 def credentials_string
225 def credentials_string
226 str = ''
226 str = ''
227 str << " --username #{shell_quote(@login)}" unless @login.blank?
227 str << " --username #{shell_quote(@login)}" unless @login.blank?
228 str << " --password #{shell_quote(@password)}" unless @login.blank? || @password.blank?
228 str << " --password #{shell_quote(@password)}" unless @login.blank? || @password.blank?
229 str << " --no-auth-cache --non-interactive"
229 str << " --no-auth-cache --non-interactive"
230 str
230 str
231 end
231 end
232
232
233 # Helper that iterates over the child elements of a xml node
233 # Helper that iterates over the child elements of a xml node
234 # MiniXml returns a hash when a single child is found or an array of hashes for multiple children
234 # MiniXml returns a hash when a single child is found or an array of hashes for multiple children
235 def each_xml_element(node, name)
235 def each_xml_element(node, name)
236 if node && node[name]
236 if node && node[name]
237 if node[name].is_a?(Hash)
237 if node[name].is_a?(Hash)
238 yield node[name]
238 yield node[name]
239 else
239 else
240 node[name].each do |element|
240 node[name].each do |element|
241 yield element
241 yield element
242 end
242 end
243 end
243 end
244 end
244 end
245 end
245 end
246
246
247 def target(path = '')
247 def target(path = '')
248 base = path.match(/^\//) ? root_url : url
248 base = path.match(/^\//) ? root_url : url
249 uri = "#{base}/#{path}"
249 uri = "#{base}/#{path}"
250 uri = URI.escape(URI.escape(uri), '[]')
250 uri = URI.escape(URI.escape(uri), '[]')
251 shell_quote(uri.gsub(/[?<>\*]/, ''))
251 shell_quote(uri.gsub(/[?<>\*]/, ''))
252 end
252 end
253 end
253 end
254 end
254 end
255 end
255 end
256 end
256 end
General Comments 0
You need to be logged in to leave comments. Login now