rscm 0.1.0.1338 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README +13 -28
- data/Rakefile +25 -24
- data/lib/rscm.rb +1 -6
- data/lib/rscm/abstract_scm.rb +36 -10
- data/lib/rscm/annotations.rb +50 -0
- data/lib/rscm/changes.rb +2 -5
- data/lib/rscm/logging.rb +1 -0
- data/lib/rscm/path_converter.rb +3 -2
- data/lib/rscm/{cvs → scm}/cvs.rb +16 -9
- data/lib/rscm/{cvs → scm}/cvs_log_parser.rb +4 -4
- data/lib/rscm/{darcs → scm}/darcs.rb +6 -3
- data/lib/rscm/scm/monotone.rb +162 -0
- data/lib/rscm/scm/monotone_log_parser.rb +95 -0
- data/lib/rscm/scm/mooky.rb +21 -0
- data/lib/rscm/{perforce → scm}/perforce.rb +7 -4
- data/lib/rscm/{starteam/starteam.rb → scm/star_team.rb} +23 -3
- data/lib/rscm/{svn/svn.rb → scm/subversion.rb} +17 -10
- data/lib/rscm/{svn/svn_log_parser.rb → scm/subversion_log_parser.rb} +8 -7
- data/test/rscm/abstract_scm_test.rb +21 -0
- data/test/rscm/annotations_test.rb +57 -0
- data/test/rscm/changes_fixture.rb +7 -7
- data/test/rscm/changes_test.rb +3 -3
- data/test/rscm/generic_scm_tests.rb +2 -2
- data/test/rscm/{cvs → scm}/cvs-dataforge.log +0 -0
- data/test/rscm/{cvs → scm}/cvs-test.log +0 -0
- data/test/rscm/{cvs → scm}/cvs_log_parser_test.rb +12 -13
- data/test/rscm/{cvs → scm}/cvs_test.rb +7 -7
- data/test/rscm/{darcs → scm}/darcs_test.rb +1 -1
- data/test/rscm/{monotone → scm}/keys +0 -0
- data/test/rscm/scm/monotone_log_parser_test.rb +109 -0
- data/test/rscm/{monotone → scm}/monotone_test.rb +1 -1
- data/test/rscm/{mooky → scm}/mooky_test.rb +1 -1
- data/test/rscm/{perforce → scm}/perforce_test.rb +1 -1
- data/test/rscm/{starteam/starteam_test.rb → scm/star_team.rb} +1 -1
- data/test/rscm/{svn/svn_log_parser_test.rb → scm/subversion_log_parser_test.rb} +25 -10
- data/test/rscm/{svn/svn_test.rb → scm/subversion_test.rb} +4 -5
- data/test/rscm/{svn/cargo-svn.log → scm/svn-cargo.log} +0 -0
- data/test/rscm/scm/svn-growl.log +875 -0
- data/test/rscm/scm/svn-growl2.log +30 -0
- data/test/rscm/{svn/proxytoys-svn.log → scm/svn-proxytoys.log} +0 -0
- metadata +35 -44
- data/lib/rscm/attr_attr.rb +0 -36
- data/lib/rscm/monotone/monotone.rb +0 -107
- data/lib/rscm/mooky/mooky.rb +0 -13
- data/test/actual +0 -3
- data/test/expected +0 -3
- data/test/rscm/attr_attr_test.rb +0 -32
@@ -1,12 +1,15 @@
|
|
1
|
-
require 'rscm/abstract_scm'
|
2
1
|
require 'tempfile'
|
3
|
-
require 'rscm/path_converter'
|
4
2
|
require 'fileutils'
|
3
|
+
require 'rscm'
|
5
4
|
|
6
5
|
module RSCM
|
7
6
|
class Darcs < AbstractSCM
|
7
|
+
register self
|
8
8
|
|
9
|
-
|
9
|
+
ann :description => "Directory"
|
10
|
+
attr_accessor :dir
|
11
|
+
|
12
|
+
def initialize(dir=".")
|
10
13
|
@dir = File.expand_path(dir)
|
11
14
|
end
|
12
15
|
|
@@ -0,0 +1,162 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'rscm'
|
3
|
+
|
4
|
+
module RSCM
|
5
|
+
class Monotone < AbstractSCM
|
6
|
+
register self
|
7
|
+
|
8
|
+
ann :description => "Database file"
|
9
|
+
attr_accessor :db_file
|
10
|
+
|
11
|
+
ann :description => "Branch"
|
12
|
+
attr_accessor :branch
|
13
|
+
|
14
|
+
ann :description => "Key"
|
15
|
+
attr_accessor :key
|
16
|
+
|
17
|
+
ann :description => "Passphrase"
|
18
|
+
attr_accessor :passphrase
|
19
|
+
|
20
|
+
ann :description => "Keys file"
|
21
|
+
attr_accessor :keys_file
|
22
|
+
|
23
|
+
def initialize(db_file="MT.db", branch="", key="", passphrase="", keys_file="")
|
24
|
+
@db_file = File.expand_path(db_file)
|
25
|
+
@branch = branch
|
26
|
+
@key = key
|
27
|
+
@passphrase = passphrase
|
28
|
+
@keys_file = keys_file
|
29
|
+
end
|
30
|
+
|
31
|
+
def name
|
32
|
+
"Monotone"
|
33
|
+
end
|
34
|
+
|
35
|
+
def create
|
36
|
+
FileUtils.mkdir_p(File.dirname(@db_file))
|
37
|
+
monotone("db init")
|
38
|
+
monotone("read") do |io|
|
39
|
+
io.write(File.open(@keys_file).read)
|
40
|
+
io.close_write
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def transactional?
|
45
|
+
true
|
46
|
+
end
|
47
|
+
|
48
|
+
def import(dir, message)
|
49
|
+
dir = File.expand_path(dir)
|
50
|
+
|
51
|
+
# post 0.17, this can be "cd dir && cmd add ."
|
52
|
+
|
53
|
+
files = Dir["#{dir}/*"]
|
54
|
+
relative_paths_to_add = to_relative(dir, files)
|
55
|
+
|
56
|
+
with_working_dir(dir) do
|
57
|
+
monotone("add #{relative_paths_to_add.join(' ')}")
|
58
|
+
monotone("commit '#{message}'", @branch, @key) do |io|
|
59
|
+
io.puts(@passphrase)
|
60
|
+
io.close_write
|
61
|
+
io.read
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def checked_out?(checkout_dir)
|
67
|
+
File.exists?("#{checkout_dir}/MT")
|
68
|
+
end
|
69
|
+
|
70
|
+
def uptodate?(checkout_dir, from_identifier)
|
71
|
+
if (!checked_out?(checkout_dir))
|
72
|
+
false
|
73
|
+
else
|
74
|
+
lr = local_revision(checkout_dir)
|
75
|
+
hr = head_revision(checkout_dir)
|
76
|
+
lr == hr
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def local_revision(checkout_dir)
|
81
|
+
local_revision = nil
|
82
|
+
rev_file = File.expand_path("#{checkout_dir}/MT/revision")
|
83
|
+
local_revision = File.open(rev_file).read.strip
|
84
|
+
local_revision
|
85
|
+
end
|
86
|
+
|
87
|
+
def head_revision(checkout_dir)
|
88
|
+
# FIXME: this will grab last head if heads are not merged.
|
89
|
+
head_revision = nil
|
90
|
+
monotone("heads", @branch) do |stdout|
|
91
|
+
stdout.each_line do |line|
|
92
|
+
next if (line =~ /^monotone:/)
|
93
|
+
head_revision = line.split(" ")[0]
|
94
|
+
end
|
95
|
+
end
|
96
|
+
head_revision
|
97
|
+
end
|
98
|
+
|
99
|
+
def changesets(checkout_dir, from_identifier, to_identifier=Time.infinity)
|
100
|
+
from_identifier = Time.epoch if from_identifier.nil?
|
101
|
+
to_identifier = Time.infinity if to_identifier.nil?
|
102
|
+
with_working_dir(checkout_dir) do
|
103
|
+
monotone("log", @branch, @key) do |stdout|
|
104
|
+
MonotoneLogParser.new.parse_changesets(stdout, from_identifier, to_identifier)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def commit(checkout_dir, message)
|
110
|
+
with_working_dir(checkout_dir) do
|
111
|
+
monotone("commit '#{message}'", @branch, @key) do |io|
|
112
|
+
io.puts(@passphrase)
|
113
|
+
io.close_write
|
114
|
+
io.read
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
protected
|
120
|
+
|
121
|
+
# Checks out silently. Called by superclass' checkout.
|
122
|
+
def checkout_silent(checkout_dir, to_identifier)
|
123
|
+
checkout_dir = PathConverter.filepath_to_nativepath(checkout_dir, false)
|
124
|
+
if checked_out?(checkout_dir)
|
125
|
+
with_working_dir(checkout_dir) do
|
126
|
+
monotone("update")
|
127
|
+
end
|
128
|
+
else
|
129
|
+
monotone("checkout #{checkout_dir}", @branch, @key) do |stdout|
|
130
|
+
stdout.each_line do |line|
|
131
|
+
# TODO: checkout prints nothing to stdout - may be fixed in a future monotone.
|
132
|
+
# When/if it happens we may want to do a kosher implementation of checkout
|
133
|
+
# to get yields as checkouts happen.
|
134
|
+
yield line if block_given?
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
# Administrative files that should be ignored when counting files.
|
141
|
+
def ignore_paths
|
142
|
+
return [/MT/, /\.mt-attrs/]
|
143
|
+
end
|
144
|
+
|
145
|
+
private
|
146
|
+
|
147
|
+
def monotone(monotone_cmd, branch=nil, key=nil)
|
148
|
+
branch_opt = branch ? "--branch=\"#{branch}\"" : ""
|
149
|
+
key_opt = key ? "--key=\"#{key}\"" : ""
|
150
|
+
cmd = "monotone --db=\"#{@db_file}\" #{branch_opt} #{key_opt} #{monotone_cmd}"
|
151
|
+
safer_popen(cmd, "r+") do |io|
|
152
|
+
if(block_given?)
|
153
|
+
return(yield(io))
|
154
|
+
else
|
155
|
+
# just read stdout so we can exit
|
156
|
+
io.read
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
end
|
162
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require 'rscm'
|
2
|
+
require 'time'
|
3
|
+
require 'stringio'
|
4
|
+
|
5
|
+
module RSCM
|
6
|
+
|
7
|
+
class MonotoneLogParser
|
8
|
+
|
9
|
+
def parse_changesets(io, from_identifier=Time.epoch, to_identifier=Time.infinity)
|
10
|
+
# skip first separator
|
11
|
+
io.readline
|
12
|
+
|
13
|
+
changesets = ChangeSets.new
|
14
|
+
changeset_string = ""
|
15
|
+
|
16
|
+
# hash of path => [array of revisions]
|
17
|
+
path_revisions = {}
|
18
|
+
io.each_line do |line|
|
19
|
+
if(line =~ /-----------------------------------------------------------------/)
|
20
|
+
changeset = parse_changeset(StringIO.new(changeset_string), path_revisions)
|
21
|
+
changesets.add(changeset)
|
22
|
+
changeset_string = ""
|
23
|
+
else
|
24
|
+
changeset_string << line
|
25
|
+
end
|
26
|
+
end
|
27
|
+
changeset = parse_changeset(StringIO.new(changeset_string), path_revisions)
|
28
|
+
if((from_identifier <= changeset.time) && (changeset.time <= to_identifier))
|
29
|
+
changesets.add(changeset)
|
30
|
+
end
|
31
|
+
|
32
|
+
# set the previous revisions. most recent is at index 0.
|
33
|
+
changesets.each do |changeset|
|
34
|
+
changeset.each do |change|
|
35
|
+
current_index = path_revisions[change.path].index(change.revision)
|
36
|
+
change.previous_revision = path_revisions[change.path][current_index + 1]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
changesets
|
40
|
+
end
|
41
|
+
|
42
|
+
def parse_changeset(changeset_io, path_revisions)
|
43
|
+
changeset = ChangeSet.new
|
44
|
+
state = nil
|
45
|
+
changeset_io.each_line do |line|
|
46
|
+
if(line =~ /^Revision: (.*)$/ && changeset.revision.nil?)
|
47
|
+
changeset.revision = $1
|
48
|
+
elsif(line =~ /^Author: (.*)$/ && changeset.developer.nil?)
|
49
|
+
changeset.developer = $1
|
50
|
+
elsif(line =~ /^Date: (.*)$/ && changeset.time.nil?)
|
51
|
+
changeset.time = Time.utc(
|
52
|
+
$1[0..3].to_i,
|
53
|
+
$1[5..6].to_i,
|
54
|
+
$1[8..9].to_i,
|
55
|
+
$1[11..12].to_i,
|
56
|
+
$1[14..15].to_i,
|
57
|
+
$1[17..18].to_i
|
58
|
+
)
|
59
|
+
elsif(line =~ /^ChangeLog:$/ && changeset.message.nil?)
|
60
|
+
state = :message
|
61
|
+
elsif(state == :message && changeset.message.nil?)
|
62
|
+
changeset.message = ""
|
63
|
+
elsif(state == :message && changeset.message)
|
64
|
+
changeset.message << line
|
65
|
+
elsif(line =~ /^Added files:$/)
|
66
|
+
state = :added
|
67
|
+
elsif(state == :added)
|
68
|
+
add_changes(changeset, line, Change::ADDED, path_revisions)
|
69
|
+
elsif(line =~ /^Modified files:$/)
|
70
|
+
state = :modified
|
71
|
+
elsif(state == :modified)
|
72
|
+
add_changes(changeset, line, Change::MODIFIED, path_revisions)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
changeset.message.chomp!
|
76
|
+
changeset
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def add_changes(changeset, line, state, path_revisions)
|
82
|
+
paths = line.split(" ")
|
83
|
+
paths.each do |path|
|
84
|
+
changeset << Change.new(path, state, changeset.developer, nil, changeset.revision, changeset.time)
|
85
|
+
|
86
|
+
# now record path revisions so we can keep track of previous rev for each path
|
87
|
+
# doesn't work for moved files, and have no idea how to make it work either.
|
88
|
+
path_revisions[path] ||= []
|
89
|
+
path_revisions[path] << changeset.revision
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'rscm/abstract_scm'
|
2
|
+
|
3
|
+
module RSCM
|
4
|
+
class Mooky < AbstractSCM
|
5
|
+
register self
|
6
|
+
|
7
|
+
ann :description => "The Foo", :tip => "Foo is nonsense"
|
8
|
+
attr_accessor :foo
|
9
|
+
|
10
|
+
ann :description => "Le Bar", :tip => "Bar toi!"
|
11
|
+
attr_accessor :bar
|
12
|
+
|
13
|
+
def initialize(foo="", bar="chocolate bar")
|
14
|
+
end
|
15
|
+
|
16
|
+
def name
|
17
|
+
"Mooky"
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
@@ -15,11 +15,14 @@ module RSCM
|
|
15
15
|
# You need the p4/p4d executable on the PATH in order for it to work.
|
16
16
|
#
|
17
17
|
class Perforce < AbstractSCM
|
18
|
+
register self
|
19
|
+
|
18
20
|
include FileUtils
|
19
21
|
|
22
|
+
ann :description => "Depot path", :tip => "The path to the Perforce depot"
|
20
23
|
attr_accessor :depotpath
|
21
24
|
|
22
|
-
def initialize(repository_root_dir =
|
25
|
+
def initialize(repository_root_dir = "")
|
23
26
|
@clients = {}
|
24
27
|
@depotpath = repository_root_dir
|
25
28
|
end
|
@@ -55,8 +58,8 @@ module RSCM
|
|
55
58
|
client(checkout_dir).submit(message, &proc)
|
56
59
|
end
|
57
60
|
|
58
|
-
def changesets(checkout_dir, from_identifier, to_identifier
|
59
|
-
client(checkout_dir).changesets(from_identifier, to_identifier
|
61
|
+
def changesets(checkout_dir, from_identifier, to_identifier=Time.infinity)
|
62
|
+
client(checkout_dir).changesets(from_identifier, to_identifier)
|
60
63
|
end
|
61
64
|
|
62
65
|
def uptodate?(checkout_dir, from_identifier)
|
@@ -251,7 +254,7 @@ module RSCM
|
|
251
254
|
p4("sync -n").empty?
|
252
255
|
end
|
253
256
|
|
254
|
-
def changesets(from_identifier, to_identifier
|
257
|
+
def changesets(from_identifier, to_identifier)
|
255
258
|
changesets = changelists(from_identifier, to_identifier).collect {|changelist| to_changeset(changelist)}
|
256
259
|
ChangeSets.new(changesets)
|
257
260
|
end
|
@@ -18,10 +18,30 @@ module RSCM
|
|
18
18
|
# * Apache Ant (http://ant.apache.org/)
|
19
19
|
#
|
20
20
|
class StarTeam < AbstractSCM
|
21
|
+
register self
|
21
22
|
|
22
|
-
|
23
|
+
ann :description => "User name"
|
24
|
+
attr_accessor :user_name
|
23
25
|
|
24
|
-
|
26
|
+
ann :description => "Password"
|
27
|
+
attr_accessor :password
|
28
|
+
|
29
|
+
ann :description => "Server name"
|
30
|
+
attr_accessor :server_name
|
31
|
+
|
32
|
+
ann :description => "Server port"
|
33
|
+
attr_accessor :server_port
|
34
|
+
|
35
|
+
ann :description => "Project name"
|
36
|
+
attr_accessor :project_name
|
37
|
+
|
38
|
+
ann :description => "View name"
|
39
|
+
attr_accessor :view_name
|
40
|
+
|
41
|
+
ann :description => "Folder name"
|
42
|
+
attr_accessor :folder_name
|
43
|
+
|
44
|
+
def initialize(user_name="", password="", server_name="", server_port="", project_name="", view_name="", folder_name="")
|
25
45
|
@user_name, @password, @server_name, @server_port, @project_name, @view_name, @folder_name = user_name, password, server_name, server_port, project_name, view_name, folder_name
|
26
46
|
end
|
27
47
|
|
@@ -29,7 +49,7 @@ module RSCM
|
|
29
49
|
"StarTeam"
|
30
50
|
end
|
31
51
|
|
32
|
-
def changesets(checkout_dir, from_identifier=Time.epoch, to_identifier=Time.infinity,
|
52
|
+
def changesets(checkout_dir, from_identifier=Time.epoch, to_identifier=Time.infinity, &proc)
|
33
53
|
# just assuming it is a Time for now, may support labels later.
|
34
54
|
# the java class really wants rfc822 and not rfc2822, but this works ok anyway.
|
35
55
|
from = from_identifier.to_rfc2822
|
@@ -1,7 +1,7 @@
|
|
1
1
|
require 'rscm/abstract_scm'
|
2
2
|
require 'rscm/path_converter'
|
3
3
|
require 'rscm/line_editor'
|
4
|
-
require 'rscm/
|
4
|
+
require 'rscm/scm/subversion_log_parser'
|
5
5
|
|
6
6
|
module RSCM
|
7
7
|
|
@@ -10,14 +10,21 @@ module RSCM
|
|
10
10
|
# You need the svn/svnadmin executable on the PATH in order for it to work.
|
11
11
|
#
|
12
12
|
# NOTE: On Cygwin these have to be the win32 builds of svn/svnadmin and not the Cygwin ones.
|
13
|
-
class
|
13
|
+
class Subversion < AbstractSCM
|
14
|
+
register self
|
15
|
+
|
14
16
|
include FileUtils
|
15
17
|
include PathConverter
|
16
18
|
|
19
|
+
ann :description => "Repository URL"
|
20
|
+
ann :tip => "If you specify a local URL (starting with file://) DamageControl can create the repository for you after you save (unless the repository already exists).<br>Using a file:// URL will also give you the option to have DamageControl install a trigger in Subversion, so that you don't have to use polling to detect changes.<br>On Windows, file URLs must look like file:///C:/jupiter/mars"
|
17
21
|
attr_accessor :url
|
22
|
+
|
23
|
+
ann :description => "Path"
|
24
|
+
ann :tip => "This is the relative path from the start of the repository <br>to the end of the URL. For example, if your URL is <br>svn://your.server/path/to/repository/path/within/repository <br>then this value should be path/within/repository."
|
18
25
|
attr_accessor :path
|
19
26
|
|
20
|
-
def initialize(url=
|
27
|
+
def initialize(url="", path="trunk")
|
21
28
|
@url, @path = url, path
|
22
29
|
end
|
23
30
|
|
@@ -98,7 +105,7 @@ module RSCM
|
|
98
105
|
cmd = "svn log #{repourl} -r HEAD"
|
99
106
|
with_working_dir(checkout_dir) do
|
100
107
|
safer_popen(cmd) do |stdout|
|
101
|
-
parser =
|
108
|
+
parser = SubversionLogParser.new(stdout, path, checkout_dir)
|
102
109
|
changesets = parser.parse_changesets
|
103
110
|
changesets[0].revision.to_i
|
104
111
|
end
|
@@ -117,7 +124,7 @@ module RSCM
|
|
117
124
|
|
118
125
|
def diff(checkout_dir, change, &block)
|
119
126
|
with_working_dir(checkout_dir) do
|
120
|
-
cmd = "svn diff -r #{change.previous_revision}:#{change.revision} #{url}/#{change.path}"
|
127
|
+
cmd = "svn diff -r #{change.previous_revision}:#{change.revision} \"#{url}/#{change.path}\""
|
121
128
|
safer_popen(cmd) do |io|
|
122
129
|
return(yield(io))
|
123
130
|
end
|
@@ -179,18 +186,18 @@ module RSCM
|
|
179
186
|
svn(dir, import_cmd)
|
180
187
|
end
|
181
188
|
|
182
|
-
def changesets(checkout_dir, from_identifier, to_identifier=Time.infinity
|
189
|
+
def changesets(checkout_dir, from_identifier, to_identifier=Time.infinity)
|
183
190
|
# Return empty changeset if the requested revision doesn't exist yet.
|
184
191
|
return ChangeSets.new if(from_identifier.is_a?(Integer) && head_revision(checkout_dir) < from_identifier)
|
185
192
|
|
186
193
|
checkout_dir = PathConverter.filepath_to_nativepath(checkout_dir, false)
|
187
194
|
changesets = nil
|
188
|
-
command = "svn #{changes_command(from_identifier, to_identifier
|
195
|
+
command = "svn #{changes_command(from_identifier, to_identifier)}"
|
189
196
|
yield command if block_given?
|
190
197
|
|
191
198
|
with_working_dir(checkout_dir) do
|
192
199
|
safer_popen(command) do |stdout|
|
193
|
-
parser =
|
200
|
+
parser = SubversionLogParser.new(stdout, path, checkout_dir)
|
194
201
|
changesets = parser.parse_changesets
|
195
202
|
end
|
196
203
|
end
|
@@ -274,7 +281,7 @@ module RSCM
|
|
274
281
|
"update #{revision_option(nil,to_identifier)}"
|
275
282
|
end
|
276
283
|
|
277
|
-
def changes_command(from_identifier, to_identifier
|
284
|
+
def changes_command(from_identifier, to_identifier)
|
278
285
|
# http://svnbook.red-bean.com/svnbook-1.1/svn-book.html#svn-ch-3-sect-3.3
|
279
286
|
# file_list = files.join('\n')
|
280
287
|
# WEIRD cygwin bug garbles this!?!?!?!
|
@@ -312,7 +319,7 @@ module RSCM
|
|
312
319
|
|
313
320
|
def svndate(time)
|
314
321
|
return nil unless time
|
315
|
-
time.utc.strftime("\"
|
322
|
+
time.utc.strftime("{\"%Y-%m-%d %H:%M:%S\"}")
|
316
323
|
end
|
317
324
|
|
318
325
|
def commit_command(message)
|