##// END OF EJS Templates
scm: git: move saving changesets from adapter to model (#3396)....
Toshi MARUYAMA -
r4839:d7aa303a5ca0
parent child
Show More
@@ -1,98 +1,119
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 # Copyright (C) 2007 Patrick Aljord patcito@ŋmail.com
3 # Copyright (C) 2007 Patrick Aljord patcito@ŋmail.com
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/git_adapter'
18 require 'redmine/scm/adapters/git_adapter'
19
19
20 class Repository::Git < Repository
20 class Repository::Git < Repository
21 attr_protected :root_url
21 attr_protected :root_url
22 validates_presence_of :url
22 validates_presence_of :url
23
23
24 def self.scm_adapter_class
24 def self.scm_adapter_class
25 Redmine::Scm::Adapters::GitAdapter
25 Redmine::Scm::Adapters::GitAdapter
26 end
26 end
27
27
28 def self.scm_name
28 def self.scm_name
29 'Git'
29 'Git'
30 end
30 end
31
31
32 # Returns the identifier for the given git changeset
32 # Returns the identifier for the given git changeset
33 def self.changeset_identifier(changeset)
33 def self.changeset_identifier(changeset)
34 changeset.scmid
34 changeset.scmid
35 end
35 end
36
36
37 # Returns the readable identifier for the given git changeset
37 # Returns the readable identifier for the given git changeset
38 def self.format_changeset_identifier(changeset)
38 def self.format_changeset_identifier(changeset)
39 changeset.revision[0, 8]
39 changeset.revision[0, 8]
40 end
40 end
41
41
42 def branches
42 def branches
43 scm.branches
43 scm.branches
44 end
44 end
45
45
46 def tags
46 def tags
47 scm.tags
47 scm.tags
48 end
48 end
49
49
50 def find_changeset_by_name(name)
50 def find_changeset_by_name(name)
51 return nil if name.nil? || name.empty?
51 return nil if name.nil? || name.empty?
52 e = changesets.find(:first, :conditions => ['revision = ?', name.to_s])
52 e = changesets.find(:first, :conditions => ['revision = ?', name.to_s])
53 return e if e
53 return e if e
54 changesets.find(:first, :conditions => ['scmid LIKE ?', "#{name}%"])
54 changesets.find(:first, :conditions => ['scmid LIKE ?', "#{name}%"])
55 end
55 end
56
56
57 # With SCM's that have a sequential commit numbering, redmine is able to be
57 # With SCM's that have a sequential commit numbering, redmine is able to be
58 # clever and only fetch changesets going forward from the most recent one
58 # clever and only fetch changesets going forward from the most recent one
59 # it knows about. However, with git, you never know if people have merged
59 # it knows about. However, with git, you never know if people have merged
60 # commits into the middle of the repository history, so we should parse
60 # commits into the middle of the repository history, so we should parse
61 # the entire log. Since it's way too slow for large repositories, we only
61 # the entire log. Since it's way too slow for large repositories, we only
62 # parse 1 week before the last known commit.
62 # parse 1 week before the last known commit.
63 # The repository can still be fully reloaded by calling #clear_changesets
63 # The repository can still be fully reloaded by calling #clear_changesets
64 # before fetching changesets (eg. for offline resync)
64 # before fetching changesets (eg. for offline resync)
65 def fetch_changesets
65 def fetch_changesets
66 c = changesets.find(:first, :order => 'committed_on DESC')
66 c = changesets.find(:first, :order => 'committed_on DESC')
67 since = (c ? c.committed_on - 7.days : nil)
67 since = (c ? c.committed_on - 7.days : nil)
68
68
69 revisions = scm.revisions('', nil, nil, :all => true, :since => since)
69 revisions = scm.revisions('', nil, nil, :all => true, :since => since)
70 return if revisions.nil? || revisions.empty?
70 return if revisions.nil? || revisions.empty?
71
71
72 recent_changesets = changesets.find(:all, :conditions => ['committed_on >= ?', since])
72 recent_changesets = changesets.find(:all, :conditions => ['committed_on >= ?', since])
73
73
74 # Clean out revisions that are no longer in git
74 # Clean out revisions that are no longer in git
75 recent_changesets.each {|c| c.destroy unless revisions.detect {|r| r.scmid.to_s == c.scmid.to_s }}
75 recent_changesets.each {|c| c.destroy unless revisions.detect {|r| r.scmid.to_s == c.scmid.to_s }}
76
76
77 # Subtract revisions that redmine already knows about
77 # Subtract revisions that redmine already knows about
78 recent_revisions = recent_changesets.map{|c| c.scmid}
78 recent_revisions = recent_changesets.map{|c| c.scmid}
79 revisions.reject!{|r| recent_revisions.include?(r.scmid)}
79 revisions.reject!{|r| recent_revisions.include?(r.scmid)}
80
80
81 # Save the remaining ones to the database
81 # Save the remaining ones to the database
82 revisions.each{|r| r.save(self)} unless revisions.nil?
82 unless revisions.nil?
83 revisions.each do |rev|
84 transaction do
85 changeset = Changeset.new(
86 :repository => self,
87 :revision => rev.identifier,
88 :scmid => rev.scmid,
89 :committer => rev.author,
90 :committed_on => rev.time,
91 :comments => rev.message)
92
93 if changeset.save
94 rev.paths.each do |file|
95 Change.create(
96 :changeset => changeset,
97 :action => file[:action],
98 :path => file[:path])
99 end
100 end
101 end
102 end
103 end
83 end
104 end
84
105
85 def latest_changesets(path,rev,limit=10)
106 def latest_changesets(path,rev,limit=10)
86 revisions = scm.revisions(path, nil, rev, :limit => limit, :all => false)
107 revisions = scm.revisions(path, nil, rev, :limit => limit, :all => false)
87 return [] if revisions.nil? || revisions.empty?
108 return [] if revisions.nil? || revisions.empty?
88
109
89 changesets.find(
110 changesets.find(
90 :all,
111 :all,
91 :conditions => [
112 :conditions => [
92 "scmid IN (?)",
113 "scmid IN (?)",
93 revisions.map!{|c| c.scmid}
114 revisions.map!{|c| c.scmid}
94 ],
115 ],
95 :order => 'committed_on DESC'
116 :order => 'committed_on DESC'
96 )
117 )
97 end
118 end
98 end
119 end
@@ -1,373 +1,352
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 'cgi'
18 require 'cgi'
19
19
20 module Redmine
20 module Redmine
21 module Scm
21 module Scm
22 module Adapters
22 module Adapters
23 class CommandFailed < StandardError #:nodoc:
23 class CommandFailed < StandardError #:nodoc:
24 end
24 end
25
25
26 class AbstractAdapter #:nodoc:
26 class AbstractAdapter #:nodoc:
27 class << self
27 class << self
28 def client_command
28 def client_command
29 ""
29 ""
30 end
30 end
31
31
32 # Returns the version of the scm client
32 # Returns the version of the scm client
33 # Eg: [1, 5, 0] or [] if unknown
33 # Eg: [1, 5, 0] or [] if unknown
34 def client_version
34 def client_version
35 []
35 []
36 end
36 end
37
37
38 # Returns the version string of the scm client
38 # Returns the version string of the scm client
39 # Eg: '1.5.0' or 'Unknown version' if unknown
39 # Eg: '1.5.0' or 'Unknown version' if unknown
40 def client_version_string
40 def client_version_string
41 v = client_version || 'Unknown version'
41 v = client_version || 'Unknown version'
42 v.is_a?(Array) ? v.join('.') : v.to_s
42 v.is_a?(Array) ? v.join('.') : v.to_s
43 end
43 end
44
44
45 # Returns true if the current client version is above
45 # Returns true if the current client version is above
46 # or equals the given one
46 # or equals the given one
47 # If option is :unknown is set to true, it will return
47 # If option is :unknown is set to true, it will return
48 # true if the client version is unknown
48 # true if the client version is unknown
49 def client_version_above?(v, options={})
49 def client_version_above?(v, options={})
50 ((client_version <=> v) >= 0) || (client_version.empty? && options[:unknown])
50 ((client_version <=> v) >= 0) || (client_version.empty? && options[:unknown])
51 end
51 end
52
52
53 def client_available
53 def client_available
54 true
54 true
55 end
55 end
56
56
57 def shell_quote(str)
57 def shell_quote(str)
58 if Redmine::Platform.mswin?
58 if Redmine::Platform.mswin?
59 '"' + str.gsub(/"/, '\\"') + '"'
59 '"' + str.gsub(/"/, '\\"') + '"'
60 else
60 else
61 "'" + str.gsub(/'/, "'\"'\"'") + "'"
61 "'" + str.gsub(/'/, "'\"'\"'") + "'"
62 end
62 end
63 end
63 end
64 end
64 end
65
65
66 def initialize(url, root_url=nil, login=nil, password=nil,
66 def initialize(url, root_url=nil, login=nil, password=nil,
67 path_encoding=nil)
67 path_encoding=nil)
68 @url = url
68 @url = url
69 @login = login if login && !login.empty?
69 @login = login if login && !login.empty?
70 @password = (password || "") if @login
70 @password = (password || "") if @login
71 @root_url = root_url.blank? ? retrieve_root_url : root_url
71 @root_url = root_url.blank? ? retrieve_root_url : root_url
72 end
72 end
73
73
74 def adapter_name
74 def adapter_name
75 'Abstract'
75 'Abstract'
76 end
76 end
77
77
78 def supports_cat?
78 def supports_cat?
79 true
79 true
80 end
80 end
81
81
82 def supports_annotate?
82 def supports_annotate?
83 respond_to?('annotate')
83 respond_to?('annotate')
84 end
84 end
85
85
86 def root_url
86 def root_url
87 @root_url
87 @root_url
88 end
88 end
89
89
90 def url
90 def url
91 @url
91 @url
92 end
92 end
93
93
94 # get info about the svn repository
94 # get info about the svn repository
95 def info
95 def info
96 return nil
96 return nil
97 end
97 end
98
98
99 # Returns the entry identified by path and revision identifier
99 # Returns the entry identified by path and revision identifier
100 # or nil if entry doesn't exist in the repository
100 # or nil if entry doesn't exist in the repository
101 def entry(path=nil, identifier=nil)
101 def entry(path=nil, identifier=nil)
102 parts = path.to_s.split(%r{[\/\\]}).select {|n| !n.blank?}
102 parts = path.to_s.split(%r{[\/\\]}).select {|n| !n.blank?}
103 search_path = parts[0..-2].join('/')
103 search_path = parts[0..-2].join('/')
104 search_name = parts[-1]
104 search_name = parts[-1]
105 if search_path.blank? && search_name.blank?
105 if search_path.blank? && search_name.blank?
106 # Root entry
106 # Root entry
107 Entry.new(:path => '', :kind => 'dir')
107 Entry.new(:path => '', :kind => 'dir')
108 else
108 else
109 # Search for the entry in the parent directory
109 # Search for the entry in the parent directory
110 es = entries(search_path, identifier)
110 es = entries(search_path, identifier)
111 es ? es.detect {|e| e.name == search_name} : nil
111 es ? es.detect {|e| e.name == search_name} : nil
112 end
112 end
113 end
113 end
114
114
115 # Returns an Entries collection
115 # Returns an Entries collection
116 # or nil if the given path doesn't exist in the repository
116 # or nil if the given path doesn't exist in the repository
117 def entries(path=nil, identifier=nil)
117 def entries(path=nil, identifier=nil)
118 return nil
118 return nil
119 end
119 end
120
120
121 def branches
121 def branches
122 return nil
122 return nil
123 end
123 end
124
124
125 def tags
125 def tags
126 return nil
126 return nil
127 end
127 end
128
128
129 def default_branch
129 def default_branch
130 return nil
130 return nil
131 end
131 end
132
132
133 def properties(path, identifier=nil)
133 def properties(path, identifier=nil)
134 return nil
134 return nil
135 end
135 end
136
136
137 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
137 def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
138 return nil
138 return nil
139 end
139 end
140
140
141 def diff(path, identifier_from, identifier_to=nil)
141 def diff(path, identifier_from, identifier_to=nil)
142 return nil
142 return nil
143 end
143 end
144
144
145 def cat(path, identifier=nil)
145 def cat(path, identifier=nil)
146 return nil
146 return nil
147 end
147 end
148
148
149 def with_leading_slash(path)
149 def with_leading_slash(path)
150 path ||= ''
150 path ||= ''
151 (path[0,1]!="/") ? "/#{path}" : path
151 (path[0,1]!="/") ? "/#{path}" : path
152 end
152 end
153
153
154 def with_trailling_slash(path)
154 def with_trailling_slash(path)
155 path ||= ''
155 path ||= ''
156 (path[-1,1] == "/") ? path : "#{path}/"
156 (path[-1,1] == "/") ? path : "#{path}/"
157 end
157 end
158
158
159 def without_leading_slash(path)
159 def without_leading_slash(path)
160 path ||= ''
160 path ||= ''
161 path.gsub(%r{^/+}, '')
161 path.gsub(%r{^/+}, '')
162 end
162 end
163
163
164 def without_trailling_slash(path)
164 def without_trailling_slash(path)
165 path ||= ''
165 path ||= ''
166 (path[-1,1] == "/") ? path[0..-2] : path
166 (path[-1,1] == "/") ? path[0..-2] : path
167 end
167 end
168
168
169 def shell_quote(str)
169 def shell_quote(str)
170 self.class.shell_quote(str)
170 self.class.shell_quote(str)
171 end
171 end
172
172
173 private
173 private
174 def retrieve_root_url
174 def retrieve_root_url
175 info = self.info
175 info = self.info
176 info ? info.root_url : nil
176 info ? info.root_url : nil
177 end
177 end
178
178
179 def target(path)
179 def target(path)
180 path ||= ''
180 path ||= ''
181 base = path.match(/^\//) ? root_url : url
181 base = path.match(/^\//) ? root_url : url
182 shell_quote("#{base}/#{path}".gsub(/[?<>\*]/, ''))
182 shell_quote("#{base}/#{path}".gsub(/[?<>\*]/, ''))
183 end
183 end
184
184
185 def logger
185 def logger
186 self.class.logger
186 self.class.logger
187 end
187 end
188
188
189 def shellout(cmd, &block)
189 def shellout(cmd, &block)
190 self.class.shellout(cmd, &block)
190 self.class.shellout(cmd, &block)
191 end
191 end
192
192
193 def self.logger
193 def self.logger
194 RAILS_DEFAULT_LOGGER
194 RAILS_DEFAULT_LOGGER
195 end
195 end
196
196
197 def self.shellout(cmd, &block)
197 def self.shellout(cmd, &block)
198 logger.debug "Shelling out: #{strip_credential(cmd)}" if logger && logger.debug?
198 logger.debug "Shelling out: #{strip_credential(cmd)}" if logger && logger.debug?
199 if Rails.env == 'development'
199 if Rails.env == 'development'
200 # Capture stderr when running in dev environment
200 # Capture stderr when running in dev environment
201 cmd = "#{cmd} 2>>#{RAILS_ROOT}/log/scm.stderr.log"
201 cmd = "#{cmd} 2>>#{RAILS_ROOT}/log/scm.stderr.log"
202 end
202 end
203 begin
203 begin
204 if RUBY_VERSION < '1.9'
204 if RUBY_VERSION < '1.9'
205 mode = "r+"
205 mode = "r+"
206 else
206 else
207 mode = "r+:ASCII-8BIT"
207 mode = "r+:ASCII-8BIT"
208 end
208 end
209 IO.popen(cmd, mode) do |io|
209 IO.popen(cmd, mode) do |io|
210 io.close_write
210 io.close_write
211 block.call(io) if block_given?
211 block.call(io) if block_given?
212 end
212 end
213 rescue Errno::ENOENT => e
213 rescue Errno::ENOENT => e
214 msg = strip_credential(e.message)
214 msg = strip_credential(e.message)
215 # The command failed, log it and re-raise
215 # The command failed, log it and re-raise
216 logger.error("SCM command failed, make sure that your SCM binary (eg. svn) is in PATH (#{ENV['PATH']}): #{strip_credential(cmd)}\n with: #{msg}")
216 logger.error("SCM command failed, make sure that your SCM binary (eg. svn) is in PATH (#{ENV['PATH']}): #{strip_credential(cmd)}\n with: #{msg}")
217 raise CommandFailed.new(msg)
217 raise CommandFailed.new(msg)
218 end
218 end
219 end
219 end
220
220
221 # Hides username/password in a given command
221 # Hides username/password in a given command
222 def self.strip_credential(cmd)
222 def self.strip_credential(cmd)
223 q = (Redmine::Platform.mswin? ? '"' : "'")
223 q = (Redmine::Platform.mswin? ? '"' : "'")
224 cmd.to_s.gsub(/(\-\-(password|username))\s+(#{q}[^#{q}]+#{q}|[^#{q}]\S+)/, '\\1 xxxx')
224 cmd.to_s.gsub(/(\-\-(password|username))\s+(#{q}[^#{q}]+#{q}|[^#{q}]\S+)/, '\\1 xxxx')
225 end
225 end
226
226
227 def strip_credential(cmd)
227 def strip_credential(cmd)
228 self.class.strip_credential(cmd)
228 self.class.strip_credential(cmd)
229 end
229 end
230
230
231 def scm_iconv(to, from, str)
231 def scm_iconv(to, from, str)
232 return nil if str.nil?
232 return nil if str.nil?
233 return str if to == from
233 return str if to == from
234 begin
234 begin
235 Iconv.conv(to, from, str)
235 Iconv.conv(to, from, str)
236 rescue Iconv::Failure => err
236 rescue Iconv::Failure => err
237 logger.error("failed to convert from #{from} to #{to}. #{err}")
237 logger.error("failed to convert from #{from} to #{to}. #{err}")
238 nil
238 nil
239 end
239 end
240 end
240 end
241 end
241 end
242
242
243 class Entries < Array
243 class Entries < Array
244 def sort_by_name
244 def sort_by_name
245 sort {|x,y|
245 sort {|x,y|
246 if x.kind == y.kind
246 if x.kind == y.kind
247 x.name.to_s <=> y.name.to_s
247 x.name.to_s <=> y.name.to_s
248 else
248 else
249 x.kind <=> y.kind
249 x.kind <=> y.kind
250 end
250 end
251 }
251 }
252 end
252 end
253
253
254 def revisions
254 def revisions
255 revisions ||= Revisions.new(collect{|entry| entry.lastrev}.compact)
255 revisions ||= Revisions.new(collect{|entry| entry.lastrev}.compact)
256 end
256 end
257 end
257 end
258
258
259 class Info
259 class Info
260 attr_accessor :root_url, :lastrev
260 attr_accessor :root_url, :lastrev
261 def initialize(attributes={})
261 def initialize(attributes={})
262 self.root_url = attributes[:root_url] if attributes[:root_url]
262 self.root_url = attributes[:root_url] if attributes[:root_url]
263 self.lastrev = attributes[:lastrev]
263 self.lastrev = attributes[:lastrev]
264 end
264 end
265 end
265 end
266
266
267 class Entry
267 class Entry
268 attr_accessor :name, :path, :kind, :size, :lastrev
268 attr_accessor :name, :path, :kind, :size, :lastrev
269 def initialize(attributes={})
269 def initialize(attributes={})
270 self.name = attributes[:name] if attributes[:name]
270 self.name = attributes[:name] if attributes[:name]
271 self.path = attributes[:path] if attributes[:path]
271 self.path = attributes[:path] if attributes[:path]
272 self.kind = attributes[:kind] if attributes[:kind]
272 self.kind = attributes[:kind] if attributes[:kind]
273 self.size = attributes[:size].to_i if attributes[:size]
273 self.size = attributes[:size].to_i if attributes[:size]
274 self.lastrev = attributes[:lastrev]
274 self.lastrev = attributes[:lastrev]
275 end
275 end
276
276
277 def is_file?
277 def is_file?
278 'file' == self.kind
278 'file' == self.kind
279 end
279 end
280
280
281 def is_dir?
281 def is_dir?
282 'dir' == self.kind
282 'dir' == self.kind
283 end
283 end
284
284
285 def is_text?
285 def is_text?
286 Redmine::MimeType.is_type?('text', name)
286 Redmine::MimeType.is_type?('text', name)
287 end
287 end
288 end
288 end
289
289
290 class Revisions < Array
290 class Revisions < Array
291 def latest
291 def latest
292 sort {|x,y|
292 sort {|x,y|
293 unless x.time.nil? or y.time.nil?
293 unless x.time.nil? or y.time.nil?
294 x.time <=> y.time
294 x.time <=> y.time
295 else
295 else
296 0
296 0
297 end
297 end
298 }.last
298 }.last
299 end
299 end
300 end
300 end
301
301
302 class Revision
302 class Revision
303 attr_accessor :scmid, :name, :author, :time, :message, :paths, :revision, :branch
303 attr_accessor :scmid, :name, :author, :time, :message, :paths, :revision, :branch
304 attr_writer :identifier
304 attr_writer :identifier
305
305
306 def initialize(attributes={})
306 def initialize(attributes={})
307 self.identifier = attributes[:identifier]
307 self.identifier = attributes[:identifier]
308 self.scmid = attributes[:scmid]
308 self.scmid = attributes[:scmid]
309 self.name = attributes[:name] || self.identifier
309 self.name = attributes[:name] || self.identifier
310 self.author = attributes[:author]
310 self.author = attributes[:author]
311 self.time = attributes[:time]
311 self.time = attributes[:time]
312 self.message = attributes[:message] || ""
312 self.message = attributes[:message] || ""
313 self.paths = attributes[:paths]
313 self.paths = attributes[:paths]
314 self.revision = attributes[:revision]
314 self.revision = attributes[:revision]
315 self.branch = attributes[:branch]
315 self.branch = attributes[:branch]
316 end
316 end
317
317
318 # Returns the identifier of this revision; see also Changeset model
318 # Returns the identifier of this revision; see also Changeset model
319 def identifier
319 def identifier
320 (@identifier || revision).to_s
320 (@identifier || revision).to_s
321 end
321 end
322
322
323 # Returns the readable identifier.
323 # Returns the readable identifier.
324 def format_identifier
324 def format_identifier
325 identifier
325 identifier
326 end
326 end
327
328 def save(repo)
329 Changeset.transaction do
330 changeset = Changeset.new(
331 :repository => repo,
332 :revision => identifier,
333 :scmid => scmid,
334 :committer => author,
335 :committed_on => time,
336 :comments => message)
337
338 if changeset.save
339 paths.each do |file|
340 Change.create(
341 :changeset => changeset,
342 :action => file[:action],
343 :path => file[:path])
344 end
345 end
346 end
347 end
348 end
327 end
349
328
350 class Annotate
329 class Annotate
351 attr_reader :lines, :revisions
330 attr_reader :lines, :revisions
352
331
353 def initialize
332 def initialize
354 @lines = []
333 @lines = []
355 @revisions = []
334 @revisions = []
356 end
335 end
357
336
358 def add_line(line, revision)
337 def add_line(line, revision)
359 @lines << line
338 @lines << line
360 @revisions << revision
339 @revisions << revision
361 end
340 end
362
341
363 def content
342 def content
364 content = lines.join("\n")
343 content = lines.join("\n")
365 end
344 end
366
345
367 def empty?
346 def empty?
368 lines.empty?
347 lines.empty?
369 end
348 end
370 end
349 end
371 end
350 end
372 end
351 end
373 end
352 end
General Comments 0
You need to be logged in to leave comments. Login now