summaryrefslogtreecommitdiffstats
path: root/app/models/repository/cvs.rb
blob: 3cf5b70360b64f762c2fa38cb1b3ddd52612cba4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# Redmine - project management software
# Copyright (C) 2006-2012  Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

require 'redmine/scm/adapters/cvs_adapter'
require 'digest/sha1'

class Repository::Cvs < Repository
  validates_presence_of :url, :root_url, :log_encoding

  safe_attributes 'root_url',
    :if => lambda {|repository, user| repository.new_record?}

  def self.human_attribute_name(attribute_key_name, *args)
    attr_name = attribute_key_name.to_s
    if attr_name == "root_url"
      attr_name = "cvsroot"
    elsif attr_name == "url"
      attr_name = "cvs_module"
    end
    super(attr_name, *args)
  end

  def self.scm_adapter_class
    Redmine::Scm::Adapters::CvsAdapter
  end

  def self.scm_name
    'CVS'
  end

  def entry(path=nil, identifier=nil)
    rev = identifier.nil? ? nil : changesets.find_by_revision(identifier)
    scm.entry(path, rev.nil? ? nil : rev.committed_on)
  end

  def entries(path=nil, identifier=nil)
    rev = nil
    if ! identifier.nil?
      rev = changesets.find_by_revision(identifier)
      return nil if rev.nil?
    end
    entries = scm.entries(path, rev.nil? ? nil : rev.committed_on)
    if entries
      entries.each() do |entry|
        if ( ! entry.lastrev.nil? ) && ( ! entry.lastrev.revision.nil? )
          change = filechanges.find_by_revision_and_path(
                     entry.lastrev.revision,
                     scm.with_leading_slash(entry.path) )
          if change
            entry.lastrev.identifier = change.changeset.revision
            entry.lastrev.revision   = change.changeset.revision
            entry.lastrev.author     = change.changeset.committer
            # entry.lastrev.branch     = change.branch
          end
        end
      end
    end
    load_entries_changesets(entries)
    entries
  end

  def cat(path, identifier=nil)
    rev = nil
    if ! identifier.nil?
      rev = changesets.find_by_revision(identifier)
      return nil if rev.nil?
    end
    scm.cat(path, rev.nil? ? nil : rev.committed_on)
  end

  def annotate(path, identifier=nil)
    rev = nil
    if ! identifier.nil?
      rev = changesets.find_by_revision(identifier)
      return nil if rev.nil?
    end
    scm.annotate(path, rev.nil? ? nil : rev.committed_on)
  end

  def diff(path, rev, rev_to)
    # convert rev to revision. CVS can't handle changesets here
    diff=[]
    changeset_from = changesets.find_by_revision(rev)
    if rev_to.to_i > 0
      changeset_to = changesets.find_by_revision(rev_to)
    end
    changeset_from.filechanges.each() do |change_from|
      revision_from = nil
      revision_to   = nil
      if path.nil? || (change_from.path.starts_with? scm.with_leading_slash(path))
        revision_from = change_from.revision
      end
      if revision_from
        if changeset_to
          changeset_to.filechanges.each() do |change_to|
            revision_to = change_to.revision if change_to.path == change_from.path
          end
        end
        unless revision_to
          revision_to = scm.get_previous_revision(revision_from)
        end
        file_diff = scm.diff(change_from.path, revision_from, revision_to)
        diff = diff + file_diff unless file_diff.nil?
      end
    end
    return diff
  end

  def fetch_changesets
    # some nifty bits to introduce a commit-id with cvs
    # natively cvs doesn't provide any kind of changesets,
    # there is only a revision per file.
    # we now take a guess using the author, the commitlog and the commit-date.

    # last one is the next step to take. the commit-date is not equal for all
    # commits in one changeset. cvs update the commit-date when the *,v file was touched. so
    # we use a small delta here, to merge all changes belonging to _one_ changeset
    time_delta  = 10.seconds
    fetch_since = latest_changeset ? latest_changeset.committed_on : nil
    transaction do
      tmp_rev_num = 1
      scm.revisions('', fetch_since, nil, :log_encoding => repo_log_encoding) do |revision|
        # only add the change to the database, if it doen't exists. the cvs log
        # is not exclusive at all.
        tmp_time = revision.time.clone
        unless filechanges.find_by_path_and_revision(
	                         scm.with_leading_slash(revision.paths[0][:path]),
	                         revision.paths[0][:revision]
	                           )
          cmt = Changeset.normalize_comments(revision.message, repo_log_encoding)
          author_utf8 = Changeset.to_utf8(revision.author, repo_log_encoding)
          cs  = changesets.find(
            :first,
            :conditions => {
                :committed_on => tmp_time - time_delta .. tmp_time + time_delta,
                :committer    => author_utf8,
                :comments     => cmt
                }
             )
          # create a new changeset....
          unless cs
            # we use a temporaray revision number here (just for inserting)
            # later on, we calculate a continous positive number
            tmp_time2 = tmp_time.clone.gmtime
            branch    = revision.paths[0][:branch]
            scmid     = branch + "-" + tmp_time2.strftime("%Y%m%d-%H%M%S")
            cs = Changeset.create(:repository   => self,
                                  :revision     => "tmp#{tmp_rev_num}",
                                  :scmid        => scmid,
                                  :committer    => revision.author,
                                  :committed_on => tmp_time,
                                  :comments     => revision.message)
            tmp_rev_num += 1
          end
          # convert CVS-File-States to internal Action-abbrevations
          # default action is (M)odified
          action = "M"
          if revision.paths[0][:action] == "Exp" && revision.paths[0][:revision] == "1.1"
            action = "A" # add-action always at first revision (= 1.1)
          elsif revision.paths[0][:action] == "dead"
            action = "D" # dead-state is similar to Delete
          end
          Change.create(
             :changeset => cs,
             :action    => action,
             :path      => scm.with_leading_slash(revision.paths[0][:path]),
             :revision  => revision.paths[0][:revision],
             :branch    => revision.paths[0][:branch]
              )
        end
      end

      # Renumber new changesets in chronological order
      Changeset.all(
              :order => 'committed_on ASC, id ASC',
              :conditions => ["repository_id = ? AND revision LIKE 'tmp%'", id]
           ).each do |changeset|
        changeset.update_attribute :revision, next_revision_number
      end
    end # transaction
    @current_revision_number = nil
  end

  private

  # Returns the next revision number to assign to a CVS changeset
  def next_revision_number
    # Need to retrieve existing revision numbers to sort them as integers
    sql = "SELECT revision FROM #{Changeset.table_name} "
    sql << "WHERE repository_id = #{id} AND revision NOT LIKE 'tmp%'"
    @current_revision_number ||= (connection.select_values(sql).collect(&:to_i).max || 0)
    @current_revision_number += 1
  end
end