Class: RSCM::Base

Inherits:
Object
  • Object
show all
Defined in:
lib/rscm/base.rb,
lib/rscm/revision_poller.rb

Overview

This class defines the RSCM API, which offers access to an SCM working copy as well as a ‘central’ repository.

Concrete subclasses of this class (concrete adapters) implement the integration with the respective SCMs.

Most of the methods take an optional options Hash (named parameters), allowing the following options:

  • :stdout: Path to file name where stdout of SCM operations are written.

  • :stdout: Path to file name where stderr of SCM operations are written.

In stead of specifying the options parameters for every API method, it’s possible to assign default options via the default_options attribute.

Some of the methods in this API use from_identifier and to_identifier. These identifiers can be either a UTC Time (according to the SCM’s clock) or a String or Integer representing a label/revision (according to the SCM’s native label/revision scheme).

If from_identifier or to_identifier are nil they should respectively default to Time.epoch or Time.infinite.

Direct Known Subclasses

ClearCase, Cvs, Darcs, Monotone, Mooky, Perforce, StarTeam, Subversion

Constant Summary collapse

TWO_WEEKS_AGO =
2*7*24*60*60
THIRTY_TWO_WEEKS_AGO =
TWO_WEEKS_AGO * 16
DEFAULT_QUIET_PERIOD =

Default time to wait for scm to be quiet (applies to non-transactional scms only)

15

Instance Attribute Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#default_optionsObject

Returns the value of attribute default_options.



31
32
33
# File 'lib/rscm/base.rb', line 31

def default_options
  @default_options
end

#loggerObject

Returns the value of attribute logger.



3
4
5
# File 'lib/rscm/revision_poller.rb', line 3

def logger
  @logger
end

Instance Method Details

#==(other_scm) ⇒ Object



256
257
258
259
260
261
262
# File 'lib/rscm/base.rb', line 256

def ==(other_scm)
  return false if self.class != other_scm.class
  self.instance_variables.each do |var|
    return false if self.instance_eval(var) != other_scm.instance_eval(var)
  end
  true
end

#add(relative_filename, options = {}) ⇒ Object

Adds relative_filename to the working copy.

Raises:

  • (NotImplementedError)


99
100
101
# File 'lib/rscm/base.rb', line 99

def add(relative_filename, options={})
  raise NotImplementedError
end

#can_create_central?Boolean

Whether a repository can be created.

Returns:

  • (Boolean)


94
95
96
# File 'lib/rscm/base.rb', line 94

def can_create_central?
  false
end

#central_exists?(options = {}) ⇒ Boolean

Whether or not the SCM represented by this instance exists.

Returns:

  • (Boolean)


62
63
64
65
66
# File 'lib/rscm/base.rb', line 62

def central_exists?(options={})
  # The default implementation assumes yes - override if it can be
  # determined programmatically.
  true
end

#checked_out?Boolean

Whether the project is checked out from the central repository or not. Subclasses should override this to check for SCM-specific administrative files if appliccable

Returns:

  • (Boolean)


198
199
200
# File 'lib/rscm/base.rb', line 198

def checked_out?
  File.exists?(@checkout_dir)
end

#checkout(to_identifier = Time.infinity, options = {}) ⇒ Object

Checks out or updates contents from a central SCM to checkout_dir - a local working copy. If this is a distributed SCM, this method should create a ‘working copy’ repository if one doesn’t already exist. Then the contents of the central SCM should be pulled into the working copy.

The to_identifier parameter may be optionally specified to obtain files up to a particular time or label. to_identifier should either be a Time (in UTC - according to the clock on the SCM machine) or a String - reprsenting a label or revision.

This method will yield the relative file name of each checked out file, and also return them in an array. Only files, not directories, should be yielded/returned.

This method should be overridden for SCMs that are able to yield checkouts as they happen. For some SCMs this is not possible, or at least very hard. In that case, just override the checkout_silent method instead of this method (should be protected).



141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# File 'lib/rscm/base.rb', line 141

def checkout(to_identifier=Time.infinity, options={}) # :yield: file
  to_identifier=Time.infinity if to_identifier.nil?

  # the OS doesn't store file timestamps with fractions.
  before_checkout_time = Time.now.utc - 1

  # We expect subclasses to implement this as a protected method (unless this whole method is overridden).
  checkout_silent(to_identifier, options)
  files = Dir["#{@checkout_dir}/**/*"]
  added = []
  files.each do |file|
    added << file if File.mtime(file).utc > before_checkout_time
  end
  ignore_paths.each do |regex|
    added.delete_if{|path| path =~ regex}
  end
  added_file_paths = added.find_all do |path|
    File.file?(path)
  end
  relative_added_file_paths = to_relative(checkout_dir, added_file_paths)
  relative_added_file_paths
end

#checkout_commandline(to_identifier = Time.infinity) ⇒ Object

The command line to run in order to check out a fresh working copy.

Raises:

  • (NotImplementedError)


240
241
242
# File 'lib/rscm/base.rb', line 240

def checkout_commandline(to_identifier=Time.infinity)
  raise NotImplementedError
end

#checkout_dirObject

Gets the working copy directory.



45
46
47
# File 'lib/rscm/base.rb', line 45

def checkout_dir
  @checkout_dir
end

#checkout_dir=(dir) ⇒ Object

Sets the checkout dir (working copy). Should be set prior to most other method invocations (depending on the implementation).



40
41
42
# File 'lib/rscm/base.rb', line 40

def checkout_dir=(dir)
  @checkout_dir = PathConverter.filepath_to_nativepath(dir, false)
end

#commit(message, options = {}) ⇒ Object

Commit (check in) modified files.

Raises:

  • (NotImplementedError)


121
122
123
# File 'lib/rscm/base.rb', line 121

def commit(message, options={})
  raise NotImplementedError
end

#create_central(options = {}) ⇒ Object

Creates a new ‘central’ repository. This is intended only for creation of ‘central’ repositories (not for working copies). You shouldn’t have to call this method if a central repository already exists. This method is used primarily for testing of RSCM, but can also be used if you really want to use RSCM to create a central repository.

This method should throw an exception if the repository cannot be created (for example if the repository is ‘remote’ or if it already exists).

Raises:

  • (NotImplementedError)


82
83
84
# File 'lib/rscm/base.rb', line 82

def create_central(options={})
  raise NotImplementedError
end

#destroy_centralObject

Destroys the central repository. Shuts down any server processes and deletes the repository. WARNING: calling this may result in loss of data. Only call this if you really want to wipe it out for good!

Raises:

  • (NotImplementedError)


89
90
91
# File 'lib/rscm/base.rb', line 89

def destroy_central
  raise NotImplementedError
end

#destroy_working_copy(options = {}) ⇒ Object

Destroys the working copy



57
58
59
# File 'lib/rscm/base.rb', line 57

def destroy_working_copy(options={})
  FileUtils.rm_rf(checkout_dir) unless checkout_dir.nil?
end

#diff(change, &block) ⇒ Object

Returns/yields an IO containing the unified diff of the change. Also see RevisionFile#diff

Raises:

  • (NotImplementedError)


252
253
254
# File 'lib/rscm/base.rb', line 252

def diff(change, &block)
  raise NotImplementedError
end

#edit(file, options = {}) ⇒ Object

Open a file for edit - required by scms that check out files in read-only mode e.g. perforce



117
118
# File 'lib/rscm/base.rb', line 117

def edit(file, options={})
end

#file(relative_path, dir) ⇒ Object

Returns a HistoricFile for relative_path



178
179
180
# File 'lib/rscm/base.rb', line 178

def file(relative_path, dir)
  HistoricFile.new(relative_path, dir, self)
end

#import_central(options) ⇒ Object

Recursively imports files from :dir into the central scm, using commit message :message

Raises:

  • (NotImplementedError)


112
113
114
# File 'lib/rscm/base.rb', line 112

def import_central(options)
  raise NotImplementedError
end

#install_trigger(trigger_command, install_dir) ⇒ Object

Installs trigger_command in the SCM. The install_dir parameter should be an empty local directory that the SCM can use for temporary files if necessary (CVS needs this to check out its administrative files). Most implementations will ignore this parameter.

Raises:

  • (NotImplementedError)


222
223
224
# File 'lib/rscm/base.rb', line 222

def install_trigger(trigger_command, install_dir)
  raise NotImplementedError
end

#move(relative_src, relative_dest, options = {}) ⇒ Object

Schedules a move of relative_src to relative_dest Should not take effect in the central repository until commit is invoked.

Raises:

  • (NotImplementedError)


106
107
108
# File 'lib/rscm/base.rb', line 106

def move(relative_src, relative_dest, options={})
  raise NotImplementedError
end

#open(revision_file, &block) ⇒ Object

Opens a revision_file

Raises:

  • (NotImplementedError)


183
184
185
# File 'lib/rscm/base.rb', line 183

def open(revision_file, &block) #:yield: io
  raise NotImplementedError
end

#poll_new_revisions(options) ⇒ Object

Polls new revisions for since last_revision, or if last_revision is nil, polls since ‘now’ - options[:seconds_before_now]. If no revisions are found AND the poll was using options[:seconds_before_now] (i.e. it’s the first poll, and no revisions were found), calls itself recursively with twice the options[:seconds_before_now]. This happens until revisions are found, ot until the options[:seconds_before_now] Exceeds 32 weeks, which means it’s probably not worth looking further in the past, the scm is either completely idle or not yet active.



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
# File 'lib/rscm/revision_poller.rb', line 19

def poll_new_revisions(options)
  options = {
    :latest_revision => nil, 
    :quiet_period => DEFAULT_QUIET_PERIOD, 
    :seconds_before_now => TWO_WEEKS_AGO, 
    :max_time_before_now => THIRTY_TWO_WEEKS_AGO,
  }.merge(options)
  max_past = Time.new.utc - options[:max_time_before_now]
  
  # Default value for start time (in case there are no detected revisions yet)
  from = Time.new.utc - options[:seconds_before_now]
  if(options[:latest_revision])
    from = options[:latest_revision].identifier
  else
    if(from < max_past)
      logger.info "Checked for revisions as far back as #{max_past}. There were none, so we give up." if logger
      return []
    else
      logger.info "Latest revision is not known. Checking for revisions since: #{from}" if logger
    end
  end

  logger.info "Polling revisions after #{from} (#{from.class.name})" if logger
  
  revisions = revisions(from, options)
  if(revisions.empty?)
    logger.info "No new revisions after #{from}" if logger
    unless(options[:latest_revision])
      double_seconds_before_now = 2*options[:seconds_before_now]
      logger.info "Last revision still not found, checking since #{double_seconds_before_now.ago}" if logger
      new_opts = options.dup
      new_opts[:seconds_before_now] = double_seconds_before_now
      return poll_new_revisions(new_opts)
    end
  else
    logger.info "There were #{revisions.length} new revision(s) after #{from}" if logger
  end

  if(!revisions.empty? && !transactional?)
    # We're dealing with a non-transactional SCM (like CVS/StarTeam/ClearCase,
    # unlike Subversion/Monotone). Sleep a little, get the revisions again.
    # When the revisions are not changing, we can consider the last commit done
    # and the quiet period elapsed. This is not 100% failsafe, but will work
    # under most circumstances. In the worst case, we'll miss some files in
    # the revisions for really slow commits, but they will be part of the next 
    # revision (on next poll).
    commit_in_progress = true
    while(commit_in_progress)
      logger.info "Sleeping for #{options[:quiet_period]} seconds because #{visual_name} is not transactional." if logger
      
      sleep(options[:quiet_period])
      previous_revisions = revisions
      revisions = revisions(from)
      commit_in_progress = revisions != previous_revisions
      if(commit_in_progress)
        logger.info "Commit still in progress." if logger
      end
    end
    logger.info "Quiet period elapsed" if logger
  end
  return revisions
end

#revisions(from_identifier, options = {}) ⇒ Object

Returns a Revisions object for the period specified by from_identifier (exclusive, i.e. after) and to_identifier (inclusive). If relative_path is specified, the result will only contain revisions pertaining to that path.

Raises:

  • (NotImplementedError)


168
169
170
# File 'lib/rscm/base.rb', line 168

def revisions(from_identifier, options={})
  raise NotImplementedError
end

#rootdirObject

Returns the HistoricFile representing the root of the repo



173
174
175
# File 'lib/rscm/base.rb', line 173

def rootdir
  file("", true)
end

#supports_trigger?Boolean Also known as: can_install_trigger?

Whether triggers are supported by this SCM. A trigger is a command that can be executed upon a completed commit to the SCM.

Returns:

  • (Boolean)


204
205
206
207
208
# File 'lib/rscm/base.rb', line 204

def supports_trigger?
  # The default implementation assumes no - override if it can be
  # determined programmatically.
  false
end

#to_identifier(raw_identifier) ⇒ Object

Transforms raw_identifier into the native rype used for revisions.



34
35
36
# File 'lib/rscm/base.rb', line 34

def to_identifier(raw_identifier)
  raw_identifier.to_s
end

#to_yaml_propertiesObject

:nodoc:



49
50
51
52
53
54
# File 'lib/rscm/base.rb', line 49

def to_yaml_properties #:nodoc:
  props = instance_variables
  props.delete("@checkout_dir")
  props.delete("@default_options")
  props.sort!
end

#transactional?Boolean Also known as: atomic?

Whether or not this SCM is transactional (atomic).

Returns:

  • (Boolean)


69
70
71
# File 'lib/rscm/base.rb', line 69

def transactional?
  false
end

#trigger_installed?(trigger_command, install_dir) ⇒ Boolean

Whether the command denoted by trigger_command is installed in the SCM.

Returns:

  • (Boolean)

Raises:

  • (NotImplementedError)


234
235
236
# File 'lib/rscm/base.rb', line 234

def trigger_installed?(trigger_command, install_dir)
  raise NotImplementedError
end

#trigger_mechanismObject

Descriptive name of the trigger mechanism

Raises:

  • (NotImplementedError)


212
213
214
# File 'lib/rscm/base.rb', line 212

def trigger_mechanism
  raise NotImplementedError
end

#uninstall_trigger(trigger_command, install_dir) ⇒ Object

Uninstalls trigger_command from the SCM.

Raises:

  • (NotImplementedError)


228
229
230
# File 'lib/rscm/base.rb', line 228

def uninstall_trigger(trigger_command, install_dir)
  raise NotImplementedError
end

#update_commandline(to_identifier = Time.infinity) ⇒ Object

The command line to run in order to update a working copy.

Raises:

  • (NotImplementedError)


246
247
248
# File 'lib/rscm/base.rb', line 246

def update_commandline(to_identifier=Time.infinity)
  raise NotImplementedError
end

#uptodate?(identifier) ⇒ Boolean

Whether the working copy is in synch with the central repository’s revision/time identified by identifier. If identifier is nil, ‘HEAD’ of repository should be assumed.

Returns:

  • (Boolean)

Raises:

  • (NotImplementedError)


191
192
193
# File 'lib/rscm/base.rb', line 191

def uptodate?(identifier)
  raise NotImplementedError
end