├── .document ├── .gitignore ├── .rspec ├── .yardopts ├── ChangeLog.md ├── LICENSE.txt ├── README.md ├── Rakefile ├── gemspec.yml ├── lib ├── scm.rb └── scm │ ├── commits │ ├── commit.rb │ ├── git.rb │ ├── hg.rb │ └── svn.rb │ ├── git.rb │ ├── hg.rb │ ├── repository.rb │ ├── scm.rb │ ├── svn.rb │ ├── util.rb │ └── version.rb ├── scm.gemspec └── spec ├── git_spec.rb ├── helpers └── scm.rb ├── hg_spec.rb ├── scm_spec.rb └── spec_helper.rb /.document: -------------------------------------------------------------------------------- 1 | - 2 | ChangeLog.* 3 | LICENSE.txt 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | doc/ 2 | pkg/ 3 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --colour --format documentation 2 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --markup markdown --title "SCM Documentation" --protected 2 | -------------------------------------------------------------------------------- /ChangeLog.md: -------------------------------------------------------------------------------- 1 | ### 0.1.0 / 2011-06-16 2 | 3 | * Initial release: 4 | * Supports {SCM::Git}. 5 | * Supports {SCM::Hg}. 6 | * Supports {SCM::SVN}. 7 | 8 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Hal Brodigan 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SCM 2 | 3 | * [Source](http://github.com/postmodern/scm) 4 | * [Issues](http://github.com/postmodern/scm/issues) 5 | * [Documentation](http://rubydoc.info/gems/scm/frames) 6 | * [Email](mailto:postmodern.mod3 at gmail.com) 7 | 8 | ## Description 9 | 10 | {SCM} is a simple Ruby library for interacting with common SCMs, 11 | such as Git, Mercurial (Hg) and SubVersion (SVN). 12 | 13 | ## Features 14 | 15 | * Supports: 16 | * [Git](http://www.git-scm.org/) 17 | * [Mercurial (Hg)](http://mercurial.selenic.com/) 18 | * [SubVersion (SVN)](http://subversion.tigris.org/) 19 | * Provides a basic {SCM::Repository API} for each SCM. 20 | 21 | ## Examples 22 | 23 | require 'scm' 24 | 25 | repo = SCM::Git.new('path/to/repo') 26 | 27 | repo.branches 28 | # => [...] 29 | 30 | repo.tags 31 | # => [...] 32 | 33 | repo.status 34 | # => {...} 35 | 36 | repo.log 37 | 38 | ## Install 39 | 40 | $ gem install scm 41 | 42 | ## Copyright 43 | 44 | Copyright (c) 2011 Hal Brodigan 45 | 46 | See {file:LICENSE.txt} for details. 47 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'rubygems' 4 | require 'rake' 5 | 6 | begin 7 | gem 'rubygems-tasks', '~> 0.1' 8 | require 'rubygems/tasks' 9 | 10 | Gem::Tasks.new 11 | rescue LoadError => e 12 | warn e.message 13 | warn "Run `gem install rubygems-tasks` to install 'rubygems/tasks'." 14 | end 15 | 16 | begin 17 | gem 'rspec', '~> 2.4' 18 | require 'rspec/core/rake_task' 19 | 20 | RSpec::Core::RakeTask.new 21 | rescue LoadError => e 22 | task :spec do 23 | abort "Please run `gem install rspec` to install RSpec." 24 | end 25 | end 26 | 27 | task :test => :spec 28 | task :default => :spec 29 | 30 | begin 31 | gem 'yard', '~> 0.7.0' 32 | require 'yard' 33 | 34 | YARD::Rake::YardocTask.new 35 | rescue LoadError => e 36 | task :yard do 37 | abort "Please run `gem install yard` to install YARD." 38 | end 39 | end 40 | task :doc => :yard 41 | -------------------------------------------------------------------------------- /gemspec.yml: -------------------------------------------------------------------------------- 1 | name: scm 2 | summary: Ruby interface to common SCMs 3 | description: 4 | SCM is a simple Ruby library for interacting with common SCMs, 5 | such as Git, Mercurial (Hg) and SubVersion (SVN). 6 | 7 | license: MIT 8 | authors: Postmodern 9 | email: postmodern.mod3@gmail.com 10 | homepage: http://github.com/postmodern/scm 11 | has_yard: true 12 | 13 | development_dependencies: 14 | rubygems-tasks: ~> 0.1 15 | rspec: ~> 2.4 16 | yard: ~> 0.7.0 17 | -------------------------------------------------------------------------------- /lib/scm.rb: -------------------------------------------------------------------------------- 1 | require 'scm/version' 2 | require 'scm/git' 3 | require 'scm/svn' 4 | require 'scm/scm' 5 | -------------------------------------------------------------------------------- /lib/scm/commits/commit.rb: -------------------------------------------------------------------------------- 1 | require 'time' 2 | 3 | module SCM 4 | module Commits 5 | # 6 | # Base-class for other SCM Commit classes. 7 | # 8 | class Commit 9 | 10 | # The commit hash or revision number 11 | attr_reader :commit 12 | 13 | # The date of the commit 14 | attr_reader :date 15 | 16 | # The author of the commit 17 | attr_reader :author 18 | 19 | # The summary of the commit 20 | attr_reader :summary 21 | 22 | # The full commit message of the commit 23 | attr_reader :message 24 | 25 | # The files changed by the commit 26 | attr_reader :files 27 | 28 | # 29 | # Creates a new commit object. 30 | # 31 | # @param [String, Integer] commit 32 | # The commit hash or revision number. 33 | # 34 | # @param [Time] date 35 | # The date of the commit. 36 | # 37 | # @param [String] author 38 | # The author of the commit. 39 | # 40 | # @param [String] summary 41 | # The summary of the commit. 42 | # 43 | # @param [String] message 44 | # The full commit message of the commit. 45 | # 46 | # @param [String] files 47 | # The files changed in the commit. 48 | # 49 | def initialize(commit,date,author,summary,message,files=[]) 50 | @commit = commit 51 | @date = date 52 | @author = author 53 | @summary = summary 54 | @message = message 55 | @files = files 56 | end 57 | 58 | # 59 | # Inspects the commit. 60 | # 61 | # @return [String] 62 | # The inspected commit. 63 | # 64 | def inspect 65 | "#<#{self.class}: #{@commit}>" 66 | end 67 | 68 | # 69 | # Converts the commit to a String. 70 | # 71 | # @return [String] 72 | # The commit hash or revision. 73 | # 74 | def to_s 75 | @commit.to_s 76 | end 77 | 78 | # 79 | # Coerces the commit into an Array. 80 | # 81 | # @return [Array] 82 | # The commit components. 83 | # 84 | def to_ary 85 | [@commit, @date, @author, @summary] 86 | end 87 | 88 | end 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /lib/scm/commits/git.rb: -------------------------------------------------------------------------------- 1 | require 'scm/commits/commit' 2 | 3 | module SCM 4 | module Commits 5 | class Git < Commit 6 | 7 | # The parent of the commit 8 | attr_reader :parent 9 | 10 | # The tree of the commit 11 | attr_reader :tree 12 | 13 | # The email of the author 14 | attr_reader :email 15 | 16 | # 17 | # Creates a new Git commit. 18 | # 19 | # @param [String] commit 20 | # The SHA1 hash of the commit. 21 | # 22 | # @param [String] parent 23 | # The SHA1 hash of the parent commit. 24 | # 25 | # @param [String] tree 26 | # The SHA1 hash of the tree. 27 | # 28 | # @param [Time] date 29 | # The date the commit was made. 30 | # 31 | # @param [String] author 32 | # The author of the commit. 33 | # 34 | # @param [String] email 35 | # The email for the author. 36 | # 37 | # @param [String] summary 38 | # The summary of the commit. 39 | # 40 | def initialize(commit,parent,tree,date,author,email,summary,message,files) 41 | super(commit,date,author,summary,message,files) 42 | 43 | @parent = parent 44 | @tree = tree 45 | @email = email 46 | end 47 | 48 | alias sha1 commit 49 | 50 | # 51 | # Coerces the Git commit into an Array. 52 | # 53 | # @return [Array] 54 | # The commit components. 55 | # 56 | def to_ary 57 | [@commit, @parent, @tree, @date, @author, @email, @summary, @message, @files] 58 | end 59 | 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/scm/commits/hg.rb: -------------------------------------------------------------------------------- 1 | require 'scm/commits/commit' 2 | 3 | module SCM 4 | module Commits 5 | # 6 | # Represents a commit in an {SCM::Hg Hg Repository}. 7 | # 8 | class Hg < Commit 9 | 10 | # The Hash of the commit 11 | attr_reader :hash 12 | 13 | # The branch of the commit 14 | attr_reader :branch 15 | 16 | # 17 | # Creates a new Hg commit. 18 | # 19 | # @param [String, Integer] revision 20 | # The revision of the commit. 21 | # 22 | # @param [String] hash 23 | # The hash of the commit. 24 | # 25 | # @param [String] branch 26 | # The branch the commit belongs to. 27 | # 28 | # @param [String] user 29 | # The Hg user that made the commit. 30 | # 31 | # @param [Time] date 32 | # The date the commit was made on. 33 | # 34 | # @param [String] summary 35 | # The summary of the commit. 36 | # 37 | def initialize(revision,hash,branch,user,date,summary,message,files) 38 | super(revision,date,user,summary,message,files) 39 | 40 | @hash = hash 41 | @branch = branch 42 | end 43 | 44 | alias revision commit 45 | alias user author 46 | 47 | # 48 | # Converts the commit to an Integer. 49 | # 50 | # @return [Integer] 51 | # The commit revision. 52 | # 53 | def to_i 54 | @commit.to_i 55 | end 56 | 57 | # 58 | # Coerces the Hg commit into an Array. 59 | # 60 | # @return [Array] 61 | # The commit components. 62 | # 63 | def to_ary 64 | [@commit, @hash, @branch, @date, @user, @summary] 65 | end 66 | 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/scm/commits/svn.rb: -------------------------------------------------------------------------------- 1 | require 'scm/commits/commit' 2 | 3 | module SCM 4 | module Commits 5 | class SVN < Commit 6 | 7 | alias revision commit 8 | alias user author 9 | 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/scm/git.rb: -------------------------------------------------------------------------------- 1 | require 'scm/repository' 2 | require 'scm/commits/git' 3 | 4 | module SCM 5 | # 6 | # Interacts with Git repositories. 7 | # 8 | class Git < Repository 9 | 10 | # The two-letter Git status codes 11 | STATUSES = { 12 | ' M' => :modified, 13 | 'M ' => :staged, 14 | 'A ' => :added, 15 | 'D ' => :deleted, 16 | 'R ' => :renamed, 17 | 'C ' => :copied, 18 | 'U ' => :unmerged, 19 | '??' => :untracked 20 | } 21 | 22 | # 23 | # Creates a Git repository. 24 | # 25 | # @param [String] path 26 | # The path to the repository. 27 | # 28 | # @param [Hash] options 29 | # Additional options. 30 | # 31 | # @option options [Boolean] :bare 32 | # Specifies whether to create a bare repository. 33 | # 34 | # @return [Git] 35 | # The initialized Git repository. 36 | # 37 | # @raise [RuntimeError] 38 | # Could not initialize the Git repository. 39 | # 40 | def self.create(path,options={}) 41 | path = File.expand_path(path) 42 | 43 | FileUtils.mkdir_p(path) 44 | 45 | arguments = [path] 46 | 47 | unless run('init',arguments,options) 48 | raise("unable to initialize Git repository #{path.dump}") 49 | end 50 | 51 | return new(path) 52 | end 53 | 54 | # 55 | # Clones a remote Git repository. 56 | # 57 | # @param [URI, String] uri 58 | # The URI of the remote repository. 59 | # 60 | # @param [Hash] options 61 | # Additional options. 62 | # 63 | # @option options [Boolean] :bare 64 | # Performs a bare clone of the repository. 65 | # 66 | # @option options [Boolean] :mirror 67 | # Mirrors the remote repository. 68 | # 69 | # @option options [Integer] :depth 70 | # Performs a shallow clone. 71 | # 72 | # @option options [Boolean] :submodules 73 | # Recursively initialize each sub-module. 74 | # 75 | # @option options [String, Symbol] :branch 76 | # The branch to specifically clone. 77 | # 78 | # @option options [String] :dest 79 | # The destination directory to clone into. 80 | # 81 | # @return [Boolean] 82 | # Specifies whether the clone was successful. 83 | # 84 | def self.clone(uri,options={}) 85 | arguments = [] 86 | 87 | arguments << '--mirror' if options.delete(:mirror) 88 | 89 | if (depth = options.delete(:depth)) 90 | arguments << '--depth' << depth 91 | end 92 | 93 | if (branch = options.delete(:branch)) 94 | arguments << '--branch' << branch 95 | end 96 | 97 | arguments << '--recurse-submodules' if options.delete(:submodules) 98 | arguments << '--' unless arguments.empty? 99 | 100 | arguments << uri 101 | 102 | if (dest = options.delete(:dest)) 103 | arguments << dest 104 | end 105 | 106 | return run('clone',arguments,options) 107 | end 108 | 109 | # 110 | # Queries the status of the repository. 111 | # 112 | # @param [Array] paths 113 | # The optional paths to query. 114 | # 115 | # @return [Hash{String => Symbol}] 116 | # The file paths and their statuses. 117 | # 118 | def status(*paths) 119 | statuses = {} 120 | 121 | popen('status','--porcelain',*paths) do |line| 122 | status = line[0,2] 123 | path = line[3..-1] 124 | 125 | statuses[path] = STATUSES[status] 126 | end 127 | 128 | return statuses 129 | end 130 | 131 | # 132 | # Adds paths to the repository. 133 | # 134 | # @param [Array] paths 135 | # The paths to add to the repository. 136 | # 137 | def add(*paths) 138 | run('add',*paths) 139 | end 140 | 141 | # 142 | # Moves a file or directory. 143 | # 144 | # @param [String] source 145 | # The path of the source file/directory. 146 | # 147 | # @param [String] dest 148 | # The new destination path. 149 | # 150 | # @param [Boolean] force 151 | # Specifies whether to force the move. 152 | # 153 | def move(source,dest,force=false) 154 | arguments = [] 155 | 156 | arguments << '-f' if force 157 | arguments << source << dest 158 | 159 | return run('mv',*arguments) 160 | end 161 | 162 | # 163 | # Removes files or directories. 164 | # 165 | # @param [String, Array] paths 166 | # The path(s) to remove. 167 | # 168 | # @param [Hash] options 169 | # Additional options. 170 | # 171 | # @option options [Boolean] :force (false) 172 | # Specifies whether to forcibly remove the files/directories. 173 | # 174 | # @option options [Boolean] :recursive (false) 175 | # Specifies whether to recursively remove the files/directories. 176 | # 177 | def remove(paths,options={}) 178 | arguments = [] 179 | 180 | arguments << '-f' if options[:force] 181 | arguments << '-r' if options[:recursive] 182 | arguments += ['--', *paths] 183 | 184 | return run('rm',*arguments) 185 | end 186 | 187 | # 188 | # Makes a Git commit. 189 | # 190 | # @param [String] message 191 | # The message for the commit. 192 | # 193 | # @param [Hash] options 194 | # Commit options. 195 | # 196 | # @option options [String] :paths 197 | # The path of the file to commit. 198 | # 199 | # @return [Boolean] 200 | # Specifies whether the commit was successfully made. 201 | # 202 | def commit(message=nil,options={}) 203 | arguments = [] 204 | 205 | if message 206 | arguments << '-m' << message 207 | end 208 | 209 | if options[:paths] 210 | arguments += ['--', *options[:paths]] 211 | end 212 | 213 | return run('commit',*arguments) 214 | end 215 | 216 | # 217 | # Lists Git branches. 218 | # 219 | # @return [Array] 220 | # The branch names. 221 | # 222 | def branches 223 | branches = [] 224 | 225 | popen('branch') do |line| 226 | branches << line[2..-1] 227 | end 228 | 229 | return branches 230 | end 231 | 232 | # 233 | # The current branch. 234 | # 235 | # @return [String] 236 | # The name of the current branch. 237 | # 238 | def current_branch 239 | popen('branch') do |line| 240 | return line[2..-1] if line[0,1] == '*' 241 | end 242 | end 243 | 244 | # 245 | # Swtiches to another Git branch. 246 | # 247 | # @param [String, Symbol] name 248 | # The name of the branch to switch to. 249 | # 250 | # @param [Hash] options 251 | # Additional options. 252 | # 253 | # @option options [Boolean] :quiet 254 | # Switch branch quietly. 255 | # 256 | # @return [Boolean] 257 | # Specifies whether the branch was successfully switched. 258 | # 259 | def switch_branch(name,options={}) 260 | arguments = [] 261 | arguments << '-q' if options[:quiet] 262 | arguments << name 263 | 264 | return run('checkout',*arguments) 265 | end 266 | 267 | # 268 | # Deletes a branch. 269 | # 270 | # @param [String] name 271 | # The name of the branch to delete. 272 | # 273 | # @return [Boolean] 274 | # Specifies whether the branch was successfully deleted. 275 | # 276 | def delete_branch(name) 277 | run('branch','-d',name) 278 | end 279 | 280 | # 281 | # Lists Git tags. 282 | # 283 | # @return [Array] 284 | # The tag names. 285 | # 286 | def tags 287 | enum_for(:popen,'tag').to_a 288 | end 289 | 290 | # 291 | # Creates a Git tag. 292 | # 293 | # @param [String] name 294 | # The name for the tag. 295 | # 296 | # @param [String] commit 297 | # The commit to create the tag at. 298 | # 299 | # @return [Boolean] 300 | # Specifies whether the tag was successfully created. 301 | # 302 | def tag(name,commit=nil) 303 | arguments = [] 304 | arguments << commit if commit 305 | 306 | return run('tag',name,*arguments) 307 | end 308 | 309 | # 310 | # Deletes a Git tag. 311 | # 312 | # @param [String] name 313 | # The name of the tag. 314 | # 315 | # @return [Boolean] 316 | # Specifies whether the tag was successfully deleted. 317 | # 318 | def delete_tag(name) 319 | run('tag','-d',name) 320 | end 321 | 322 | # 323 | # Prints the Git log. 324 | # 325 | # @param [String] :commit 326 | # Commit to begin the log at. 327 | # 328 | # @param [String] :paths 329 | # File to list commits for. 330 | # 331 | def log(options={}) 332 | arguments = [] 333 | 334 | arguments << options[:commit] if options[:commit] 335 | 336 | if options[:paths] 337 | arguments += ['--', *options[:paths]] 338 | end 339 | 340 | return run('log',*arguments) 341 | end 342 | 343 | # 344 | # Pushes changes to the remote Git repository. 345 | # 346 | # @param [Hash] options 347 | # Additional options. 348 | # 349 | # @option options [Boolean] :mirror 350 | # Specifies to push all refs under `.git/refs/`. 351 | # 352 | # @option options [Boolean] :all 353 | # Specifies to push all refs under `.git/refs/heads/`. 354 | # 355 | # @option options [Boolean] :tags 356 | # Specifies to push all tags. 357 | # 358 | # @option options [Boolean] :force 359 | # Specifies whether to force pushing the changes. 360 | # 361 | # @option options [String, Symbol] :repository 362 | # The remote repository to push to. 363 | # 364 | # @option options [String, Symbol] :branch 365 | # The specific branch to push. 366 | # 367 | # @return [Boolean] 368 | # Specifies whether the changes were successfully pushed. 369 | # 370 | def push(options={}) 371 | arguments = [] 372 | 373 | if options[:mirror] 374 | arguments << '--mirror' 375 | elsif options[:all] 376 | arguments << '--all' 377 | elsif options[:tags] 378 | arguments << '--tags' 379 | end 380 | 381 | arguments << '-f' if options[:force] 382 | arguments << options[:repository] if options[:repository] 383 | 384 | if options[:branch] 385 | arguments << 'origin' unless options[:repository] 386 | arguments << options[:branch] 387 | end 388 | 389 | return run('push',*arguments) 390 | end 391 | 392 | # 393 | # Pulls changes from the remote Git repository. 394 | # 395 | # @param [Hash] options 396 | # Additional options. 397 | # 398 | # @option options [Boolean] :force 399 | # Specifies whether to force pushing the changes. 400 | # 401 | # @option options [String, Symbol] :repository 402 | # The remote repository to push to. 403 | # 404 | # @return [Boolean] 405 | # Specifies whether the changes were successfully pulled. 406 | # 407 | def pull(options={}) 408 | arguments = [] 409 | 410 | arguments << '-f' if options[:force] 411 | arguments << options[:repository] if options[:repository] 412 | 413 | return run('pull',*arguments) 414 | end 415 | 416 | # 417 | # Lists the commits in the Git repository. 418 | # 419 | # @param [Hash] options 420 | # Additional options. 421 | # 422 | # @option options [String] :commit 423 | # Commit to start at. 424 | # 425 | # @option options [Symbol, String] :branch 426 | # The branch to list commits within. 427 | # 428 | # @option options [Integer] :limit 429 | # The number of commits to list. 430 | # 431 | # @option options [String, Array] :paths 432 | # The path(s) to list commits for. 433 | # 434 | # @yield [commit] 435 | # The given block will be passed each commit. 436 | # 437 | # @yieldparam [Commits::Git] commit 438 | # A commit from the repository. 439 | # 440 | # @return [Enumerator] 441 | # The commits in the repository. 442 | # 443 | def commits(options={}) 444 | return enum_for(:commits,options) unless block_given? 445 | 446 | arguments = [ 447 | '--name-only', 448 | '--pretty=format:%H~|~%P~|~%T~|~%at~|~%an~|~%ae~|~%s~|~%b~|~' 449 | ] 450 | 451 | if options[:limit] 452 | arguments << "-#{options[:limit]}" 453 | end 454 | 455 | if (options[:commit] || options[:branch]) 456 | arguments << (options[:commit] || options[:branch]) 457 | end 458 | 459 | if options[:paths] 460 | arguments += ['--', *options[:paths]] 461 | end 462 | 463 | commit = nil 464 | parent = nil 465 | tree = nil 466 | date = nil 467 | author = nil 468 | email = nil 469 | summary = nil 470 | 471 | io = popen('log',*arguments) 472 | 473 | until io.eof? 474 | line = io.readline.chomp 475 | 476 | commit, parent, tree, date, author, email, summary, body, files = line.split('~|~',9) 477 | 478 | message = [summary, '', body].join($/) 479 | files = readlines_until(io) 480 | 481 | yield Commits::Git.new( 482 | commit, 483 | parent, 484 | tree, 485 | Time.at(date.to_i), 486 | author, 487 | email, 488 | summary, 489 | message, 490 | files 491 | ) 492 | end 493 | end 494 | 495 | # 496 | # Lists the files of the Git repository. 497 | # 498 | # @param [String] pattern 499 | # Optional glob pattern to filter the files by. 500 | # 501 | # @yield [file] 502 | # The given block will be passed each file. 503 | # 504 | # @yieldparam [String] file 505 | # A path of a file tracked by Git. 506 | # 507 | # @return [Enumerator] 508 | # If no block is given, an Enumerator will be returned. 509 | # 510 | def files(pattern=nil,&block) 511 | return enum_for(:files,pattern) unless block 512 | 513 | arguments = [] 514 | 515 | if pattern 516 | arguments << '--' << pattern 517 | end 518 | 519 | popen('ls-files',*arguments,&block) 520 | return nil 521 | end 522 | 523 | protected 524 | 525 | # 526 | # Builds arguments for common `git` options. 527 | # 528 | # @param [Hash] options 529 | # Common options for `git`. 530 | # 531 | # @option options [String] :exec_path 532 | # Path to wherever your core git programs are installed. 533 | # 534 | # @option options [Boolean] :paginate 535 | # Pipe all output into the pager. 536 | # 537 | # @option options [Boolean] :no_pager 538 | # Do not pipe git output into a paper. 539 | # 540 | # @option options [Boolean] :no_replace_objects 541 | # Do not use replacement refs to replace git objects. 542 | # 543 | # @option options [Boolean] :bare 544 | # Treats the repository as a bare repository. 545 | # 546 | # @option options [String] :git_dir 547 | # Set the path to the repository. 548 | # 549 | # @option options [String] :work_tree 550 | # Sets the path to the working tree. 551 | # 552 | # @option options [Hash{String => String}] :config 553 | # Additional configuration parameters for `git`. 554 | # 555 | # @return [Array] 556 | # Arguments for the `git` command. 557 | # 558 | def self.options(options) 559 | arguments = [] 560 | 561 | if options[:exec_path] 562 | arguments << "--exec-path=#{options[:exec_path]}" 563 | end 564 | 565 | if options[:paginate] 566 | arguments << '--paginate' 567 | elsif options[:no_pager] 568 | arguments << '--no-pager' 569 | end 570 | 571 | arguments << '--no-replace-objects' if options[:no_replace_objects] 572 | arguments << '--bare' if options[:bare] 573 | 574 | arguments << "--git-dir=#{options[:git_dir]}" if options[:git_dir] 575 | arguments << "--work-tree=#{options[:work_tree]}" if options[:work_tree] 576 | 577 | if options[:config] 578 | options[:config].each do |name,value| 579 | arguments << "-c" << "#{name}=#{value}" 580 | end 581 | end 582 | 583 | return arguments 584 | end 585 | 586 | end 587 | end 588 | -------------------------------------------------------------------------------- /lib/scm/hg.rb: -------------------------------------------------------------------------------- 1 | require 'scm/repository' 2 | require 'scm/commits/hg' 3 | 4 | require 'uri' 5 | 6 | module SCM 7 | # 8 | # Interacts with Mercurial (Hg) repositories. 9 | # 10 | class Hg < Repository 11 | 12 | # Hg status codes 13 | STATUSES = { 14 | 'M' => :modified, 15 | 'A' => :added, 16 | 'R' => :removed, 17 | 'C' => :clean, 18 | '!' => :missing, 19 | '?' => :untracked, 20 | 'I' => :ignored, 21 | ' ' => :origin 22 | } 23 | 24 | # 25 | # Creates a Hg repository. 26 | # 27 | # @param [String] path 28 | # The path to the repository. 29 | # 30 | # @return [Hg, URI::Generic] 31 | # The initialized local Hg repository or the URI to the remote 32 | # repository. 33 | # 34 | # @raise [RuntimeError] 35 | # Could not initialize the Hg repository. 36 | # 37 | def self.create(path,options={}) 38 | if options[:bare] 39 | raise("Hg does not support creating bare repositories") 40 | end 41 | 42 | unless path.start_with?('ssh://') 43 | FileUtils.mkdir_p(path) 44 | end 45 | 46 | arguments = [path] 47 | 48 | unless (result = run('init',arguments,options)) 49 | raise("unable to initialize Hg repository #{path.dump}") 50 | end 51 | 52 | if path.start_with?('ssh://') 53 | return URI(path) 54 | else 55 | return new(path) 56 | end 57 | end 58 | 59 | # 60 | # Clones a remote Hg repository. 61 | # 62 | # @param [URI, String] uri 63 | # The URI of the remote repository. 64 | # 65 | # @param [Hash] options 66 | # Additional options. 67 | # 68 | # @option options [String, Integer] :commits 69 | # The commits to include. 70 | # 71 | # @option options [String, Symbol] :branch 72 | # The branch to specifically clone. 73 | # 74 | # @option options [String] :dest 75 | # The destination directory to clone into. 76 | # 77 | # @return [Boolean] 78 | # Specifies whether the clone was successful. 79 | # 80 | def self.clone(uri,options={}) 81 | arguments = [] 82 | 83 | if (commits = options.delete(:commits)) 84 | arguments << '--rev' << commits 85 | end 86 | 87 | if (branch = options.delete(:branch)) 88 | arguments << '--branch' << branch 89 | end 90 | 91 | arguments << uri 92 | 93 | if (dest = options.delete(:dest)) 94 | arguments << dest 95 | end 96 | 97 | return run('clone',arguments,options) 98 | end 99 | 100 | # 101 | # Queries the status of the repository. 102 | # 103 | # @param [Array] paths 104 | # Optional paths to query the statuses of. 105 | # 106 | # @return [Hash{String => Symbol}] 107 | # The paths and their repsective statuses. 108 | # 109 | def status(*paths) 110 | statuses = {} 111 | 112 | popen('status',*paths) do |line| 113 | status, path = line.split(' ',2) 114 | 115 | statuses[path] = STATUSES[status] 116 | end 117 | 118 | return statuses 119 | end 120 | 121 | # 122 | # Adds paths to the repository. 123 | # 124 | # @param [Array] paths 125 | # The paths to add to the repository. 126 | # 127 | def add(*paths) 128 | run('add',*paths) 129 | end 130 | 131 | # 132 | # Moves a file or directory. 133 | # 134 | # @param [String] source 135 | # The path of the source file/directory. 136 | # 137 | # @param [String] dest 138 | # The new destination path. 139 | # 140 | # @param [Boolean] force 141 | # Specifies whether to force the move. 142 | # 143 | def move(source,dest,force=false) 144 | arguments = [] 145 | 146 | arguments << '--force' if force 147 | arguments << source << dest 148 | 149 | return run('mv',*arguments) 150 | end 151 | 152 | # 153 | # Removes files or directories. 154 | # 155 | # @param [String, Array] paths 156 | # The path(s) to remove. 157 | # 158 | # @param [Hash] options 159 | # Additional options. 160 | # 161 | # @option options [Boolean] :force (false) 162 | # Specifies whether to forcibly remove the files/directories. 163 | # 164 | # @note 165 | # {#remove} does not respond to the `:recursive` option. 166 | # Hg removes directories recursively by default. 167 | # 168 | def remove(paths,options={}) 169 | arguments = [] 170 | 171 | arguments << '--force' if options[:force] 172 | arguments += ['--', *paths] 173 | 174 | return run('rm',*arguments) 175 | end 176 | 177 | # 178 | # Makes a Hg commit. 179 | # 180 | # @param [String] message 181 | # The message for the commit. 182 | # 183 | # @param [Hash] options 184 | # Commit options. 185 | # 186 | # @option options [String] :paths 187 | # The path of the file to commit. 188 | # 189 | # @return [Boolean] 190 | # Specifies whether the commit was successfully made. 191 | # 192 | def commit(message=nil,options={}) 193 | arguments = [] 194 | 195 | if message 196 | arguments << '-m' << message 197 | end 198 | 199 | if options[:paths] 200 | arguments += [*options[:paths]] 201 | end 202 | 203 | return run('commit',*arguments) 204 | end 205 | 206 | # 207 | # Lists branches in the SVN repository. 208 | # 209 | # @return [Array] 210 | # The branch names. 211 | # 212 | def branches 213 | branches = [] 214 | 215 | popen('branches') do |line| 216 | branches << line[2..-1] 217 | end 218 | 219 | return branches 220 | end 221 | 222 | # 223 | # The current branch. 224 | # 225 | # @return [String] 226 | # The name of the current branch. 227 | # 228 | def branch 229 | popen('branch').chomp 230 | end 231 | 232 | # 233 | # Swtiches to another Hg branch. 234 | # 235 | # @param [String, Symbol] name 236 | # The name of the branch to switch to. 237 | # 238 | # @return [Boolean] 239 | # Specifies whether the branch was successfully switched. 240 | # 241 | def switch_branch(name) 242 | run('update',name) 243 | end 244 | 245 | # 246 | # Deletes a branch. 247 | # 248 | # @param [String] name 249 | # The name of the branch to delete. 250 | # 251 | # @return [Boolean] 252 | # Specifies whether the branch was successfully deleted. 253 | # 254 | def delete_branch(name) 255 | run('commit','--close-branch','-m',"Closing #{name}") 256 | end 257 | 258 | # 259 | # Lists Hg tags. 260 | # 261 | # @return [Array] 262 | # The tag names. 263 | # 264 | def tags 265 | tags = [] 266 | 267 | popen('tags') do |line| 268 | tags << line[2..-1] 269 | end 270 | 271 | return tags 272 | end 273 | 274 | # 275 | # Creates a Hg tag. 276 | # 277 | # @param [String] name 278 | # The name for the tag. 279 | # 280 | # @param [String] commit 281 | # The commit to create the tag at. 282 | # 283 | # @return [Boolean] 284 | # Specifies whether the tag was successfully created. 285 | # 286 | def tag(name,commit=nil) 287 | arguments = [] 288 | 289 | if commit 290 | arguments << '-r' << commit 291 | end 292 | 293 | return run('tag',name,*arguments) 294 | end 295 | 296 | # 297 | # Deletes a Hg tag. 298 | # 299 | # @param [String] name 300 | # The name of the tag. 301 | # 302 | # @return [Boolean] 303 | # Specifies whether the tag was successfully deleted. 304 | # 305 | def delete_tag(name) 306 | run('tag','--remove',name) 307 | end 308 | 309 | # 310 | # Prints the Hg log. 311 | # 312 | # @param [String] :commit 313 | # Commit to begin the log at. 314 | # 315 | # @param [String] :paths 316 | # File to list commits for. 317 | # 318 | def log(options={}) 319 | arguments = [] 320 | 321 | if options[:commit] 322 | arguments << '-r' << options[:commit] 323 | end 324 | 325 | if options[:paths] 326 | arguments += [*options[:paths]] 327 | end 328 | 329 | return run('log',*arguments) 330 | end 331 | 332 | # 333 | # Pushes changes to the remote Hg repository. 334 | # 335 | # @param [Hash] options 336 | # Additional options. 337 | # 338 | # @option options [Boolean] :force 339 | # Specifies whether to force pushing the changes. 340 | # 341 | # @option options [String, Symbol] :repository 342 | # The remote repository to push to. 343 | # 344 | # @return [Boolean] 345 | # Specifies whether the changes were successfully pushed. 346 | # 347 | def push(options={}) 348 | arguments = [] 349 | 350 | arguments << '-f' if options[:force] 351 | arguments << options[:repository] if options[:repository] 352 | 353 | return run('push',*arguments) 354 | end 355 | 356 | # 357 | # Pulls changes from the remote Hg repository. 358 | # 359 | # @param [Hash] options 360 | # Additional options. 361 | # 362 | # @option options [Boolean] :force 363 | # Specifies whether to force pushing the changes. 364 | # 365 | # @option options [String, Symbol] :repository 366 | # The remote repository to push to. 367 | # 368 | # @return [Boolean] 369 | # Specifies whether the changes were successfully pulled. 370 | # 371 | def pull(options={}) 372 | arguments = [] 373 | 374 | arguments << '-f' if options[:force] 375 | arguments << options[:repository] if options[:repository] 376 | 377 | return run('pull',*arguments) 378 | end 379 | 380 | # 381 | # Lists the commits in the Hg repository. 382 | # 383 | # @param [Hash] options 384 | # Additional options. 385 | # 386 | # @option options [String] :commit 387 | # Commit to start at. 388 | # 389 | # @option options [Symbol, String] :branch 390 | # The branch to list commits within. 391 | # 392 | # @option options [Integer] :limit 393 | # The number of commits to list. 394 | # 395 | # @option options [String, Array] :paths 396 | # The path(s) to list commits for. 397 | # 398 | # @yield [commit] 399 | # The given block will be passed each commit. 400 | # 401 | # @yieldparam [Commits::Hg] commit 402 | # A commit from the repository. 403 | # 404 | # @return [Enumerator] 405 | # The commits in the repository. 406 | # 407 | def commits(options={}) 408 | return enum_for(:commits,options) unless block_given? 409 | 410 | arguments = ['-v'] 411 | 412 | if options[:commit] 413 | arguments << '--rev' << options[:commit] 414 | end 415 | 416 | if options[:branch] 417 | arguments << '--branch' << options[:branch] 418 | end 419 | 420 | if options[:limit] 421 | arguments << '--limit' << options[:limit] 422 | end 423 | 424 | if options[:paths] 425 | arguments.push(*options[:paths]) 426 | end 427 | 428 | revision = nil 429 | hash = nil 430 | branch = nil 431 | user = nil 432 | date = nil 433 | summary = nil 434 | message = nil 435 | files = nil 436 | 437 | io = popen('log',*arguments) 438 | 439 | until io.eof? 440 | line = io.readline.chomp 441 | 442 | if line.empty? 443 | yield Commits::Hg.new(revision,hash,branch,user,date,summary,message,files) 444 | 445 | revision = hash = branch = user = date = summary = message = files = nil 446 | else 447 | key, value = line.split(' ',2) 448 | 449 | case key 450 | when 'changeset:' 451 | revision, hash = value.split(':',2) 452 | when 'branch:' 453 | branch = value 454 | when 'user:' 455 | user = value 456 | when 'date:' 457 | date = Time.parse(value) 458 | when 'description:' 459 | description = readlines_until(io) 460 | summary = description[0] 461 | message = description.join($/) 462 | when 'files:' 463 | files = value.split(' ') 464 | end 465 | end 466 | end 467 | end 468 | 469 | # 470 | # Lists the files of the Hg repository. 471 | # 472 | # @yield [file] 473 | # The given block will be passed each file. 474 | # 475 | # @yieldparam [String] file 476 | # A path of a file tracked by Hg. 477 | # 478 | # @return [Enumerator] 479 | # If no block is given, an Enumerator will be returned. 480 | # 481 | def files(&block) 482 | return enum_for(:files) unless block 483 | 484 | popen('manifest',&block) 485 | return nil 486 | end 487 | 488 | end 489 | end 490 | -------------------------------------------------------------------------------- /lib/scm/repository.rb: -------------------------------------------------------------------------------- 1 | require 'scm/util' 2 | 3 | require 'pathname' 4 | 5 | module SCM 6 | class Repository 7 | 8 | # The path of the repository 9 | attr_reader :path 10 | 11 | # SCM specification options for the repository 12 | attr_reader :options 13 | 14 | # 15 | # Creates a new repository. 16 | # 17 | # @param [String] path 18 | # The path to the repository. 19 | # 20 | # @param [Hash] options 21 | # SCM specific options for the repository. 22 | # 23 | def initialize(path,options={}) 24 | @path = Pathname.new(File.expand_path(path)) 25 | @options = options 26 | end 27 | 28 | # 29 | # The path to the SCM binary. 30 | # 31 | # @return [String, nil] 32 | # The binary path. 33 | # 34 | def self.path 35 | @path ||= nil 36 | end 37 | 38 | # 39 | # Sets the path to the SCM binary. 40 | # 41 | # @param [String, nil] new_path 42 | # The new path to the SCM binary. 43 | # 44 | # @return [String, nil] 45 | # The new SCM binary path. 46 | # 47 | def self.path=(new_path) 48 | @path = if new_path 49 | File.expand_path(new_path) 50 | end 51 | end 52 | 53 | # 54 | # Creates a new repository. 55 | # 56 | # @param [String] path 57 | # Path to the repository. 58 | # 59 | # @param [Hash] options 60 | # Additional options. 61 | # 62 | # @return [Repository] 63 | # The newly created repository. 64 | # 65 | # @abstract 66 | # 67 | def self.create(path,options={}) 68 | new(path,options) 69 | end 70 | 71 | # 72 | # Clones a remote repository. 73 | # 74 | # @param [URI, String] uri 75 | # The URI of the remote repository. 76 | # 77 | # @param [Hash] options 78 | # Additional options. 79 | # 80 | # @return [Boolean] 81 | # Specifies whether the clone was successful. 82 | # 83 | # @abstract 84 | # 85 | def self.clone(uri,options={}) 86 | false 87 | end 88 | 89 | # 90 | # Queries the status of the files. 91 | # 92 | # @param [Array] paths 93 | # The optional paths to query. 94 | # 95 | # @return [Hash{String => Symbol}] 96 | # The file paths and their statuses. 97 | # 98 | # @abstract 99 | # 100 | def status(*paths) 101 | {} 102 | end 103 | 104 | # 105 | # Adds files or directories to the repository. 106 | # 107 | # @param [Array] paths 108 | # The paths of the files/directories to add. 109 | # 110 | # @abstract 111 | # 112 | def add(*paths) 113 | end 114 | 115 | # 116 | # Moves a file or directory. 117 | # 118 | # @param [String] source 119 | # The path of the source file/directory. 120 | # 121 | # @param [String] dest 122 | # The new destination path. 123 | # 124 | # @param [Boolean] force 125 | # Specifies whether to force the move. 126 | # 127 | # @abstract 128 | # 129 | def move(source,dest,force=false) 130 | end 131 | 132 | # 133 | # Removes files or directories. 134 | # 135 | # @param [String, Array] paths 136 | # The path(s) to remove. 137 | # 138 | # @param [Hash] options 139 | # Additional options. 140 | # 141 | # @option options [Boolean] :force (false) 142 | # Specifies whether to forcibly remove the files/directories. 143 | # 144 | # @option options [Boolean] :recursive (false) 145 | # Specifies whether to recursively remove the files/directories. 146 | # 147 | # @abstract 148 | # 149 | def remove(paths,options={}) 150 | end 151 | 152 | # 153 | # Makes a commit. 154 | # 155 | # @param [String] message 156 | # The message for the commit. 157 | # 158 | # @param [Hash] options 159 | # Commit options. 160 | # 161 | # @option options [String] :paths 162 | # The path of the file to commit. 163 | # 164 | # @return [Boolean] 165 | # Specifies whether the commit was successfully made. 166 | # 167 | # @abstract 168 | # 169 | def commit(message=nil,options={}) 170 | false 171 | end 172 | 173 | # 174 | # Lists branches. 175 | # 176 | # @return [Array] 177 | # The branch names. 178 | # 179 | # @abstract 180 | # 181 | def branches 182 | [] 183 | end 184 | 185 | # 186 | # The current branch. 187 | # 188 | # @return [String] 189 | # The name of the current branch. 190 | # 191 | # @abstract 192 | # 193 | def current_branch 194 | end 195 | 196 | # 197 | # Swtiches to a branch. 198 | # 199 | # @param [String, Symbol] name 200 | # The name of the branch to switch to. 201 | # 202 | # @return [Boolean] 203 | # Specifies whether the branch was successfully switched. 204 | # 205 | # @abstract 206 | # 207 | def switch_branch(name) 208 | false 209 | end 210 | 211 | # 212 | # Deletes a branch. 213 | # 214 | # @param [String] name 215 | # The name of the branch to delete. 216 | # 217 | # @return [Boolean] 218 | # Specifies whether the branch was successfully deleted. 219 | # 220 | # @abstract 221 | # 222 | def delete_branch(name) 223 | false 224 | end 225 | 226 | # 227 | # Lists tags. 228 | # 229 | # @return [Array] 230 | # The tag names. 231 | # 232 | # @abstract 233 | # 234 | def tags 235 | [] 236 | end 237 | 238 | # 239 | # Tags a release. 240 | # 241 | # @param [String] name 242 | # The name for the tag. 243 | # 244 | # @param [String] commit 245 | # The specific commit to make the tag at. 246 | # 247 | # @return [Boolean] 248 | # Specifies whether the tag was successfully created. 249 | # 250 | # @abstract 251 | # 252 | def tag(name,commit=nil) 253 | false 254 | end 255 | 256 | # 257 | # Deletes a tag. 258 | # 259 | # @param [String] name 260 | # The name of the tag. 261 | # 262 | # @return [Boolean] 263 | # Specifies whether the tag was successfully deleted. 264 | # 265 | # @abstract 266 | # 267 | def delete_tag(name) 268 | false 269 | end 270 | 271 | # 272 | # Prints a log of commits. 273 | # 274 | # @param [String] :commit 275 | # Commit to begin the log at. 276 | # 277 | # @param [String] :paths 278 | # File to list commits for. 279 | # 280 | # @abstract 281 | # 282 | def log(options={}) 283 | false 284 | end 285 | 286 | # 287 | # Pushes changes to the remote repository. 288 | # 289 | # @param [Hash] options 290 | # Additional options. 291 | # 292 | # @return [Boolean] 293 | # Specifies whether the changes were successfully pushed. 294 | # 295 | # @abstract 296 | # 297 | def push(options={}) 298 | false 299 | end 300 | 301 | # 302 | # Pulls changes from the remote repository. 303 | # 304 | # @param [Hash] options 305 | # Additional options. 306 | # 307 | # @return [Boolean] 308 | # Specifies whether the changes were successfully pulled. 309 | # 310 | # @abstract 311 | # 312 | def pull(options={}) 313 | false 314 | end 315 | 316 | # 317 | # Lists commits. 318 | # 319 | # @param [Hash] options 320 | # Additional options. 321 | # 322 | # @return [Enumerator] 323 | # The commits within the repository. 324 | # 325 | # @raise [NotImplementedError] 326 | # If a subclass does not provide its own implementation. 327 | # 328 | # @abstract 329 | # 330 | def commits(options={}) 331 | raise(NotImplementedError,"This method is not implemented for #{self.class}") 332 | end 333 | 334 | # 335 | # Converts the repository to a String. 336 | # 337 | # @return [String] 338 | # The path of the repository. 339 | # 340 | def to_s 341 | @path.to_s 342 | end 343 | 344 | # 345 | # Inspects the Repository. 346 | # 347 | # @return [String] 348 | # The repository class name and path. 349 | # 350 | def inspect 351 | "#<#{self.class}: #{@path}>" 352 | end 353 | 354 | # 355 | # Lists the files of the repository. 356 | # 357 | # @yield [file] 358 | # The given block will be passed each file. 359 | # 360 | # @yieldparam [String] file 361 | # A path of a file within the repository. 362 | # 363 | # @return [Enumerator] 364 | # If no block is given, an Enumerator will be returned. 365 | # 366 | # @abstract 367 | # 368 | def files(&block) 369 | end 370 | 371 | protected 372 | 373 | extend Util 374 | 375 | # 376 | # Formats SCM specific options. 377 | # 378 | # @param [Hash] options 379 | # The SCM specific options to format. 380 | # 381 | # @return [Array] 382 | # SCM specific arguments. 383 | # 384 | # @abstract 385 | # 386 | def self.options(options) 387 | [] 388 | end 389 | 390 | # 391 | # Builds a command for the SCM executable. 392 | # 393 | # @param [String] sub_command 394 | # The SCM sub-command to invoke. 395 | # 396 | # @param [Array] arguments 397 | # Additional arguments for the command. 398 | # 399 | # @return [Array] 400 | # The arguments for the SCM command. 401 | # 402 | def self.command(sub_command,arguments,options=nil) 403 | program = (path || self.name.split('::').last.downcase) 404 | 405 | if options 406 | arguments = self.options(options) + arguments 407 | end 408 | 409 | return [program, sub_command] + arguments 410 | end 411 | 412 | # 413 | # Runs a sub-command of the SCM. 414 | # 415 | # @param [String] sub_command 416 | # The name of the SCM sub_command to run. 417 | # 418 | # @param [Array] arguments 419 | # Additional arguments for the sub-command. 420 | # 421 | # @param [Hash] options 422 | # Additional SCM options. 423 | # 424 | # @see Util#run 425 | # 426 | def self.run(sub_command,arguments,options=nil) 427 | super(*command(sub_command,arguments,options)) 428 | end 429 | 430 | # 431 | # Runs a sub-command of the SCM. 432 | # 433 | # @param [String] sub_command 434 | # The name of the SCM sub_command to run. 435 | # 436 | # @param [Array] arguments 437 | # Additional arguments for the sub-command. 438 | # 439 | # @param [Hash] options 440 | # Additional SCM options. 441 | # 442 | # @see Util#popen 443 | # 444 | def self.popen(sub_command,arguments,options=nil,&block) 445 | super(*command(sub_command,arguments,options),&block) 446 | end 447 | 448 | # 449 | # Runs a command within the repository. 450 | # 451 | # @param [Symbol] sub_command 452 | # The SCM sub-command to run. 453 | # 454 | # @param [Array] arguments 455 | # Additional arguments to pass to the command. 456 | # 457 | # @return [Boolean] 458 | # Specifies whether the SVN command exited successfully. 459 | # 460 | def run(sub_command,*arguments) 461 | Dir.chdir(@path) { self.class.run(sub_command,arguments,@options) } 462 | end 463 | 464 | # 465 | # Runs a command as a separate process. 466 | # 467 | # @param [Symbol] sub_command 468 | # The sub-command to run. 469 | # 470 | # @param [Array] arguments 471 | # Additional arguments to pass to the command. 472 | # 473 | # @yield [line] 474 | # The given block will be passed each line read-in. 475 | # 476 | # @yieldparam [String] line 477 | # A line read from the program. 478 | # 479 | # @return [IO] 480 | # The stdout of the command being ran. 481 | # 482 | def popen(sub_command,*arguments,&block) 483 | Dir.chdir(@path) do 484 | self.class.popen(sub_command,arguments,@options,&block) 485 | end 486 | end 487 | 488 | end 489 | end 490 | -------------------------------------------------------------------------------- /lib/scm/scm.rb: -------------------------------------------------------------------------------- 1 | require 'scm/git' 2 | require 'scm/hg' 3 | require 'scm/svn' 4 | 5 | require 'uri' 6 | 7 | module SCM 8 | # SCM control directories and the SCM classes 9 | DIRS = { 10 | '.git' => Git, 11 | '.hg' => Hg, 12 | '.svn' => SVN 13 | } 14 | 15 | # Common URI schemes used to denote the SCM 16 | SCHEMES = { 17 | 'git' => Git, 18 | 'hg' => Hg, 19 | 'svn' => SVN, 20 | 'svn+ssh' => SVN 21 | } 22 | 23 | # Common file extensions used to denote the SCM of a URI 24 | EXTENSIONS = { 25 | '.git' => Git, 26 | '.hg' => Hg, 27 | '.svn' => SVN 28 | } 29 | 30 | # 31 | # Determines the SCM used for a repository. 32 | # 33 | # @param [String] path 34 | # The path of the repository. 35 | # 36 | # @return [Repository] 37 | # The SCM repository. 38 | # 39 | # @raise [ArgumentError] 40 | # The exact SCM could not be determined. 41 | # 42 | def SCM.new(path) 43 | path = File.expand_path(path) 44 | 45 | DIRS.each do |name,repo| 46 | dir = File.join(path,name) 47 | 48 | return repo.new(path) if File.directory?(dir) 49 | end 50 | 51 | raise(ArgumentError,"could not determine the SCM of #{path.dump}") 52 | end 53 | 54 | # 55 | # Determines the SCM used for a repository URI and clones it. 56 | # 57 | # @param [URI, String] uri 58 | # The URI to the repository. 59 | # 60 | # @param [Hash] options 61 | # Additional SCM specific clone options. 62 | # 63 | # @return [Repository] 64 | # The SCM repository. 65 | # 66 | # @raise [ArgumentError] 67 | # The exact SCM could not be determined. 68 | # 69 | def SCM.clone(uri,options={}) 70 | uri = URI(uri) unless uri.kind_of?(URI) 71 | scm = (SCHEMES[uri.scheme] || EXTENSIONS[File.extname(uri.path)]) 72 | 73 | unless scm 74 | raise(ArgumentError,"could not determine the SCM of #{uri}") 75 | end 76 | 77 | return scm.clone(uri,options) 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/scm/svn.rb: -------------------------------------------------------------------------------- 1 | require 'scm/repository' 2 | require 'scm/commits/svn' 3 | 4 | module SCM 5 | # 6 | # Interacts with SubVersion (SVN) repositories. 7 | # 8 | class SVN < Repository 9 | 10 | # SVN status codes displayed in the First Column 11 | STATUSES = { 12 | 'A' => :added, 13 | 'C' => :conflicted, 14 | 'D' => :deleted, 15 | 'I' => :ignored, 16 | 'M' => :modified, 17 | 'R' => :replaced, 18 | 'X' => :unversioned, 19 | '?' => :untracked, 20 | '!' => :missing, 21 | '~' => :obstructed 22 | } 23 | 24 | LOG_SEPARATOR = '------------------------------------------------------------------------' 25 | 26 | # 27 | # Initializes the SVN repository. 28 | # 29 | # @param [String] path 30 | # The path to the SVN repository. 31 | # 32 | # @param [Hash] options 33 | # SVN specific options. 34 | # 35 | def initialize(path,options={}) 36 | super(File.expand_path(path),options) 37 | 38 | @root = if File.basename(@path) == 'trunk' 39 | File.dirname(@path) 40 | else 41 | @path 42 | end 43 | 44 | @trunk = File.join(@root,'trunk') 45 | @branches = File.join(@root,'branches') 46 | @tags = File.join(@root,'tags') 47 | end 48 | 49 | # 50 | # Creates an SVN repository. 51 | # 52 | # @param [String] path 53 | # The path to the repository. 54 | # 55 | # @return [SVN] 56 | # The new SVN repository. 57 | # 58 | # @raise [RuntimeError] 59 | # 60 | def self.create(path,options={}) 61 | path = File.expand_path(path) 62 | 63 | unless system('svnadmin','create',path) 64 | raise("could not create SVN repository #{path.dump}") 65 | end 66 | 67 | return new(path,options) 68 | end 69 | 70 | # 71 | # Checks out a remote SVN repository. 72 | # 73 | # @param [URI, String] uri 74 | # The URI of the remote repository. 75 | # 76 | # @param [Hash] options 77 | # Additional options. 78 | # 79 | # @option options [String, Integer] :commits 80 | # The commits to include. 81 | # 82 | # @option options [String] :dest 83 | # The destination directory to clone into. 84 | # 85 | # @return [Boolean] 86 | # Specifies whether the clone was successful. 87 | # 88 | def self.checkout(uri,options={}) 89 | arguments = [] 90 | 91 | if options[:commits] 92 | arguments << '--revision' << options[:commits] 93 | end 94 | 95 | arguments << uri 96 | arguments << options[:dest] if options[:dest] 97 | 98 | return run('checkout',arguments,options) 99 | end 100 | 101 | # 102 | # @see checkout 103 | # 104 | def self.clone(uri,options={}) 105 | checkout(uri,options) 106 | end 107 | 108 | # 109 | # Queries the status of the repository. 110 | # 111 | # @param [Array] paths 112 | # Optional paths to query the statuses of. 113 | # 114 | # @return [Hash{String => Symbol}] 115 | # The paths and their repsective statuses. 116 | # 117 | def status(*paths) 118 | statuses = {} 119 | 120 | popen('status',*paths) do |line| 121 | status = line[0,1] 122 | path = line[8..-1] 123 | 124 | statuses[path] = STATUSES[status] 125 | end 126 | 127 | return statuses 128 | end 129 | 130 | # 131 | # Adds paths to the repository. 132 | # 133 | # @param [Array] paths 134 | # The paths to add to the repository. 135 | # 136 | def add(*paths) 137 | run('add',*paths) 138 | end 139 | 140 | # 141 | # Moves a file or directory. 142 | # 143 | # @param [String] source 144 | # The path of the source file/directory. 145 | # 146 | # @param [String] dest 147 | # The new destination path. 148 | # 149 | # @param [Boolean] force 150 | # Specifies whether to force the move. 151 | # 152 | def move(source,dest,force=false) 153 | arguments = [] 154 | 155 | arguments << '--force' if force 156 | arguments << source << dest 157 | 158 | return run('mv',*arguments) 159 | end 160 | 161 | # 162 | # Removes files or directories. 163 | # 164 | # @param [String, Array] paths 165 | # The path(s) to remove. 166 | # 167 | # @param [Hash] options 168 | # Additional options. 169 | # 170 | # @option options [Boolean] :force (false) 171 | # Specifies whether to forcibly remove the files/directories. 172 | # 173 | # @note 174 | # {#remove} does not respond to the `:recursive` option. 175 | # SVN removes directories recursively by default. 176 | # 177 | def remove(paths,options={}) 178 | arguments = [] 179 | 180 | arguments << '--force' if options[:force] 181 | arguments += ['--', *paths] 182 | 183 | return run('rm',*arguments) 184 | end 185 | 186 | # 187 | # Makes a SVN commit. 188 | # 189 | # @param [String] message 190 | # The message for the commit. 191 | # 192 | # @param [Hash] options 193 | # Commit options. 194 | # 195 | # @option options [String] :paths 196 | # The path of the file to commit. 197 | # 198 | # @return [Boolean] 199 | # Specifies whether the commit was successfully made. 200 | # 201 | def commit(message=nil,options={}) 202 | arguments = [] 203 | 204 | if message 205 | arguments << '-m' << message 206 | end 207 | 208 | if options[:paths] 209 | arguments += [*options[:paths]] 210 | end 211 | 212 | return run('commit',*arguments) 213 | end 214 | 215 | # 216 | # Lists branches in the SVN repository. 217 | # 218 | # @return [Array] 219 | # The branch names. 220 | # 221 | def branches 222 | branches = [] 223 | 224 | Dir.glob(File.join(@branches,'*')) do |path| 225 | branches << File.basename(path) if File.directory(path) 226 | end 227 | 228 | return branches 229 | end 230 | 231 | # 232 | # The current branch. 233 | # 234 | # @return [String] 235 | # The name of the current branch. 236 | # 237 | def current_branch 238 | if @path == @trunk 239 | 'trunk' 240 | else 241 | File.basename(@path) 242 | end 243 | end 244 | 245 | # 246 | # Swtiches to another SVN branch. 247 | # 248 | # @param [String, Symbol] name 249 | # The name of the branch to switch to. 250 | # The name may also be `trunk`, to switch back to the `trunk` 251 | # directory. 252 | # 253 | # @return [Boolean] 254 | # Specifies whether the branch was successfully switched. 255 | # 256 | def switch_branch(name) 257 | name = name.to_s 258 | branch_dir = if name == 'trunk' 259 | @trunk 260 | else 261 | File.join(@branches,name) 262 | end 263 | 264 | if File.directory?(branch_dir) 265 | @path = branch_dir 266 | return true 267 | else 268 | return false 269 | end 270 | end 271 | 272 | # 273 | # Deletes a branch. 274 | # 275 | # @param [String] name 276 | # The name of the branch to delete. 277 | # 278 | # @return [Boolean] 279 | # Specifies whether the branch was successfully deleted. 280 | # 281 | def delete_branch(name) 282 | branch_dir = File.join(@branchs,name) 283 | 284 | if File.directory?(branch_dir) 285 | return run('rm',File.join('..','branchs',name)) 286 | else 287 | return false 288 | end 289 | end 290 | 291 | # 292 | # Lists tags in the SVN repository. 293 | # 294 | # @return [Array] 295 | # The tag names. 296 | # 297 | def tags 298 | tags = [] 299 | 300 | Dir.glob(File.join(@tags,'*')) do |path| 301 | tags << File.basename(path) if File.directory(path) 302 | end 303 | 304 | return tags 305 | end 306 | 307 | # 308 | # Creates a SVN tag. 309 | # 310 | # @param [String] name 311 | # The name for the tag. 312 | # 313 | # @param [String] commit 314 | # The commit argument is not supported by {SVN}. 315 | # 316 | # @return [Boolean] 317 | # Specifies whether the tag was successfully created. 318 | # 319 | # @raise [ArgumentError 320 | # The `commit` argument was specified. 321 | # 322 | def tag(name,commit=nil) 323 | if commit 324 | raise(ArgumentError,"the commit argument is not supported by #{SVN}") 325 | end 326 | 327 | if File.directory?(@trunk) 328 | File.mkdir(@tags) unless File.directory?(@tags) 329 | 330 | return run('cp',@trunk,File.join(@tags,name)) 331 | else 332 | return false 333 | end 334 | end 335 | 336 | # 337 | # Deletes a SVN tag. 338 | # 339 | # @param [String] name 340 | # The name of the tag. 341 | # 342 | # @return [Boolean] 343 | # Specifies whether the tag was successfully deleted. 344 | # 345 | def delete_tag(name) 346 | tag_dir = File.join(@tags,name) 347 | 348 | if File.directory?(tag_dir) 349 | return run('rm',tag_dir) 350 | else 351 | return false 352 | end 353 | end 354 | 355 | # 356 | # Prints a SVN log. 357 | # 358 | # @param [String] :commit 359 | # Commit to begin the log at. 360 | # 361 | # @param [String] :paths 362 | # File to list commits for. 363 | # 364 | def log(options={}) 365 | arguments = [] 366 | 367 | if options[:commit] 368 | arguments << '-c' << options[:commit] 369 | end 370 | 371 | if options[:paths] 372 | arguments += [*options[:paths]] 373 | end 374 | 375 | return run('log',*arguments) 376 | end 377 | 378 | # 379 | # @return [true] 380 | # 381 | # @note no-op 382 | # 383 | def push(options={}) 384 | true 385 | end 386 | 387 | # 388 | # Pulls changes from the remote SVN repository. 389 | # 390 | # @param [Hash] options 391 | # Additional options. 392 | # 393 | # @option options [Boolean] :force 394 | # Specifies whether to force pushing the changes. 395 | # 396 | # @return [Boolean] 397 | # Specifies whether the changes were successfully pulled. 398 | # 399 | def pull(options={}) 400 | arguments = [] 401 | arguments << '-f' if options[:force] 402 | 403 | return run('update',*arguments) 404 | end 405 | 406 | # 407 | # Lists the commits in the SVN repository. 408 | # 409 | # @param [Hash] options 410 | # Additional options. 411 | # 412 | # @option options [String] :commit 413 | # Commit to start at. 414 | # 415 | # @option options [Symbol, String] :branch 416 | # The branch to list commits within. 417 | # 418 | # @option options [Integer] :limit 419 | # The number of commits to list. 420 | # 421 | # @option options [String, Array] :paths 422 | # The path(s) to list commits for. 423 | # 424 | # @yield [commit] 425 | # The given block will be passed each commit. 426 | # 427 | # @yieldparam [Commits::SVN] commit 428 | # A commit from the repository. 429 | # 430 | # @return [Enumerator] 431 | # The commits in the repository. 432 | # 433 | def commits(options={}) 434 | return enum_for(:commits,options) unless block_given? 435 | 436 | arguments = ['-v'] 437 | 438 | if options[:commit] 439 | arguments << '--revision' << options[:commit] 440 | end 441 | 442 | if options[:limit] 443 | arguments << '--limit' << options[:limit] 444 | end 445 | 446 | if options[:paths] 447 | arguments.push(*options[:paths]) 448 | end 449 | 450 | revision = nil 451 | date = nil 452 | author = nil 453 | message = '' 454 | files = [] 455 | 456 | io = popen('log',*arguments) 457 | 458 | # eat the first LOG_SEPARATOR 459 | io.readline 460 | 461 | until io.eof? 462 | line = io.readline.chomp 463 | 464 | revision, author, date, changes = line.split(' | ',4) 465 | revision = revision[1..-1].to_i 466 | date = Time.parse(date) 467 | 468 | # eat the next line separating the metadata from the summary 469 | line = io.readline.chomp 470 | 471 | if line == 'Changed paths:' 472 | files = readlines_until(io) 473 | end 474 | 475 | description = readlines_until(io,LOG_SEPARATOR) 476 | summary = description[0] 477 | message = description.join($/) 478 | 479 | yield Commits::SVN.new(revision,date,author,summary,message,files) 480 | 481 | revision = date = author = nil 482 | message = '' 483 | files = [] 484 | end 485 | end 486 | 487 | # 488 | # Lists the files of the SVN repository. 489 | # 490 | # @yield [file] 491 | # The given block will be passed each file. 492 | # 493 | # @yieldparam [String] file 494 | # A path of a file tracked by SVN. 495 | # 496 | # @return [Enumerator] 497 | # If no block is given, an Enumerator will be returned. 498 | # 499 | def files 500 | return enum_for(:files) unless block_given? 501 | 502 | popen('ls','-R') do |file| 503 | yield file if File.file?(File.join(@path,file)) 504 | end 505 | 506 | return nil 507 | end 508 | 509 | end 510 | end 511 | -------------------------------------------------------------------------------- /lib/scm/util.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | require 'shellwords' 3 | 4 | module SCM 5 | module Util 6 | protected 7 | 8 | # 9 | # Runs a program. 10 | # 11 | # @param [String, Symbol] program 12 | # The name or path of the program. 13 | # 14 | # @param [Array] arguments 15 | # Optional arguments for the program. 16 | # 17 | # @return [Boolean] 18 | # Specifies whether the program exited successfully. 19 | # 20 | def run(program,*arguments) 21 | arguments = arguments.map(&:to_s) 22 | 23 | # filter out empty Strings 24 | arguments.reject!(&:empty?) 25 | 26 | $stderr.puts(program,*arguments) if $DEBUG 27 | 28 | system(program.to_s,*arguments) 29 | end 30 | 31 | # 32 | # Runs a command as a separate process. 33 | # 34 | # @param [String] command 35 | # The command to run. 36 | # 37 | # @param [Array] arguments 38 | # Additional arguments for the command. 39 | # 40 | # @yield [line] 41 | # The given block will be passed each line read-in. 42 | # 43 | # @yieldparam [String] line 44 | # A line read from the program. 45 | # 46 | # @return [IO] 47 | # The stdout of the command being ran. 48 | # 49 | def popen(command,*arguments) 50 | unless arguments.empty? 51 | command = command.dup 52 | 53 | arguments.each do |arg| 54 | command << ' ' << Shellwords.shellescape(arg.to_s) 55 | end 56 | end 57 | 58 | $stderr.puts(command) if $DEBUG 59 | 60 | io = IO.popen(command) 61 | 62 | if block_given? 63 | io.each_line do |line| 64 | line.chomp! 65 | yield line 66 | end 67 | end 68 | 69 | return io 70 | end 71 | 72 | # 73 | # Read lines until a separator line is encountered. 74 | # 75 | # @param [IO] io 76 | # The IO stream to read from. 77 | # 78 | # @param [String] separator 79 | # The separator line to stop at. 80 | # 81 | # @return [Array] 82 | # The read lines. 83 | # 84 | def readlines_until(io,separator='') 85 | lines = [] 86 | 87 | until io.eof? 88 | line = io.readline 89 | line.chomp! 90 | 91 | break if line == separator 92 | 93 | lines << line 94 | end 95 | 96 | return lines 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /lib/scm/version.rb: -------------------------------------------------------------------------------- 1 | module SCM 2 | # SCM version 3 | VERSION = "0.1.0.pre2" 4 | end 5 | -------------------------------------------------------------------------------- /scm.gemspec: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'yaml' 4 | 5 | Gem::Specification.new do |gemspec| 6 | root = File.dirname(__FILE__) 7 | lib_dir = File.join(root,'lib') 8 | files = if File.directory?('.git') 9 | `git ls-files`.split($/) 10 | elsif File.directory?('.hg') 11 | `hg manifest`.split($/) 12 | elsif File.directory?('.svn') 13 | `svn ls -R`.split($/).select { |path| File.file?(path) } 14 | else 15 | Dir['{**/}{.*,*}'].select { |path| File.file?(path) } 16 | end 17 | 18 | filter_files = lambda { |paths| 19 | case paths 20 | when Array 21 | (files & paths) 22 | when String 23 | (files & Dir[paths]) 24 | end 25 | } 26 | 27 | version = { 28 | :file => 'scm/version.rb', 29 | :constant => 'SCM::VERSION' 30 | } 31 | 32 | defaults = { 33 | 'name' => File.basename(File.dirname(__FILE__)), 34 | 'files' => files, 35 | 'executables' => filter_files['bin/*'].map { |path| File.basename(path) }, 36 | 'test_files' => filter_files['{test/{**/}*_test.rb,spec/{**/}*_spec.rb}'], 37 | 'extra_doc_files' => filter_files['*.{txt,rdoc,md,markdown,tt,textile}'], 38 | } 39 | 40 | metadata = defaults.merge(YAML.load_file('gemspec.yml')) 41 | 42 | gemspec.name = metadata.fetch('name',defaults[:name]) 43 | gemspec.version = if metadata['version'] 44 | metadata['version'] 45 | else 46 | $LOAD_PATH << lib_dir unless $LOAD_PATH.include?(lib_dir) 47 | 48 | require version[:file] 49 | eval(version[:constant]) 50 | end 51 | 52 | gemspec.summary = metadata.fetch('summary',metadata['description']) 53 | gemspec.description = metadata.fetch('description',metadata['summary']) 54 | 55 | case metadata['license'] 56 | when Array 57 | gemspec.licenses = metadata['license'] 58 | when String 59 | gemspec.license = metadata['license'] 60 | end 61 | 62 | case metadata['authors'] 63 | when Array 64 | gemspec.authors = metadata['authors'] 65 | when String 66 | gemspec.author = metadata['authors'] 67 | end 68 | 69 | gemspec.email = metadata['email'] 70 | gemspec.homepage = metadata['homepage'] 71 | 72 | case metadata['require_paths'] 73 | when Array 74 | gemspec.require_paths = metadata['require_paths'] 75 | when String 76 | gemspec.require_path = metadata['require_paths'] 77 | end 78 | 79 | gemspec.files = filter_files[metadata['files']] 80 | 81 | gemspec.executables = metadata['executables'] 82 | gemspec.extensions = metadata['extensions'] 83 | 84 | if Gem::VERSION < '1.7.' 85 | gemspec.default_executable = gemspec.executables.first 86 | end 87 | 88 | gemspec.test_files = filter_files[metadata['test_files']] 89 | 90 | unless gemspec.files.include?('.document') 91 | gemspec.extra_rdoc_files = metadata['extra_doc_files'] 92 | end 93 | 94 | gemspec.post_install_message = metadata['post_install_message'] 95 | gemspec.requirements = metadata['requirements'] 96 | 97 | if gemspec.respond_to?(:required_ruby_version=) 98 | gemspec.required_ruby_version = metadata['required_ruby_version'] 99 | end 100 | 101 | if gemspec.respond_to?(:required_rubygems_version=) 102 | gemspec.required_rubygems_version = metadata['required_ruby_version'] 103 | end 104 | 105 | parse_versions = lambda { |versions| 106 | case versions 107 | when Array 108 | versions.map { |v| v.to_s } 109 | when String 110 | versions.split(/,\s*/) 111 | end 112 | } 113 | 114 | if metadata['dependencies'] 115 | metadata['dependencies'].each do |name,versions| 116 | gemspec.add_dependency(name,parse_versions[versions]) 117 | end 118 | end 119 | 120 | if metadata['runtime_dependencies'] 121 | metadata['runtime_dependencies'].each do |name,versions| 122 | gemspec.add_runtime_dependency(name,parse_versions[versions]) 123 | end 124 | end 125 | 126 | if metadata['development_dependencies'] 127 | metadata['development_dependencies'].each do |name,versions| 128 | gemspec.add_development_dependency(name,parse_versions[versions]) 129 | end 130 | end 131 | end 132 | -------------------------------------------------------------------------------- /spec/git_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'scm/git' 3 | 4 | describe Git do 5 | describe "create" do 6 | it "should create the directory, if it does not exist" do 7 | repo = Git.create(directory('create_new_git_repo')) 8 | 9 | repo.path.should be_directory 10 | end 11 | 12 | it "should create a git repository" do 13 | repo = Git.create(mkdir('init_git_repo')) 14 | 15 | repo.path.join('.git').should be_directory 16 | end 17 | 18 | it "should allow creating a bare git repository" do 19 | repo = Git.create(mkdir('init_bare_git_repo')) 20 | 21 | repo.path.entries.map(&:to_s).should be =~ %w[ 22 | branches config description HEAD hooks info objects refs 23 | ] 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/helpers/scm.rb: -------------------------------------------------------------------------------- 1 | require 'tmpdir' 2 | require 'fileutils' 3 | 4 | module Helpers 5 | module SCM 6 | ROOT_DIR = File.join(Dir.tmpdir,'scm') 7 | 8 | def directory(name) 9 | File.join(ROOT_DIR,name) 10 | end 11 | 12 | def mkdir(name) 13 | path = directory(name) 14 | 15 | FileUtils.mkdir_p(path) 16 | return path 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/hg_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'scm/hg' 3 | 4 | describe Hg do 5 | describe "create" do 6 | it "should create the directory, if it does not exist" do 7 | repo = Hg.create(directory('create_new_hg_repo')) 8 | 9 | repo.path.should be_directory 10 | end 11 | 12 | it "should create a hg repository" do 13 | repo = Hg.create(mkdir('init_hg_repo')) 14 | 15 | repo.path.join('.hg').should be_directory 16 | end 17 | 18 | it "should raise an exception when :base is specified" do 19 | lambda { 20 | Hg.create(mkdir('init_bare_hg_repo'), :bare => true) 21 | }.should raise_error 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/scm_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'scm' 3 | 4 | describe SCM do 5 | it "should have a VERSION constant" do 6 | subject.const_get('VERSION').should_not be_empty 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rspec' 2 | require 'helpers/scm' 3 | 4 | require 'scm/version' 5 | include SCM 6 | 7 | RSpec.configure do |rspec| 8 | rspec.include Helpers::SCM 9 | 10 | rspec.after(:suite) do 11 | FileUtils.rm_rf(Helpers::SCM::ROOT_DIR) 12 | end 13 | end 14 | --------------------------------------------------------------------------------