subcheat 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Arjan van der Gaag
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.
data/README.rdoc ADDED
@@ -0,0 +1,96 @@
1
+ = subcheat
2
+
3
+ Subcheat is a simple wrapper around Subversion's svn command-line client.
4
+
5
+ <b>This is hobby project I'm hacking away on. Poke around at your own peril.</b>
6
+
7
+ == Description
8
+
9
+ <tt>subcheat</tt> functions the same way svn does. You could alias <tt>subcheat</tt> to <tt>svn</tt> and use <tt>subcheat</tt> from now on without ever noticing it.
10
+
11
+ <tt>subcheat</tt> adds some subcommands that can make your life a little easier:
12
+
13
+ * <tt>subcheat undo</tt>: roll-back a commit or range of commits.
14
+ * <tt>subcheat tag</tt>: create, show or delete tags
15
+ * <tt>subcheat branch</tt>: create, show or delete branches
16
+ * <tt>subcheat reintegrate</tt>: merge changes from a branch back into trunk
17
+ * <tt>subcheat rebase</tt>: merge changes from trunk into current branch
18
+ * <tt>subcheat url</tt>: output the current working copy URL
19
+ * <tt>subcheat root</tt>: output the current project root folder
20
+ * <tt>subcheat path</tt>: output the current path in the repository
21
+ * <tt>subcheat revision</tt>: output the current revision number
22
+
23
+ Also, some existing subcommands are enhanced:
24
+
25
+ * <tt>subcheat export</tt>: now expands simple tag names to tag URLs
26
+ * <tt>subcheat switch</tt>: now expands simple branch names to branch URLs
27
+
28
+ === Examples
29
+
30
+ Rolling back a commit is basically reverse-merging a revision into the current working copy. The following are equivalent:
31
+
32
+ subcheat undo 5000
33
+ svn merge -r 5000:4999 url/to/current/repo
34
+
35
+ Managing branches and tags are basic <tt>copy</tt> and <tt>list</tt> operations. The following are equivalent:
36
+
37
+ # assume we're in /svn/project/trunk
38
+ subcheat branch foo
39
+ svn copy /svn/project/trunk /svn/project/branches/foo
40
+
41
+ subcheat branch -d foo
42
+ svn delete /svn/project/branches/foo
43
+
44
+ subcheat branch
45
+ svn list /svn/project/branches
46
+
47
+ Note that tags and branches work the same but operate on the +tags+ and +branches+ subdirectories respectively.
48
+
49
+ +reintegrate+ and +rebase+ are two similar tools for managing feature branches. These basically merge changes from a branch into trunk, or the other way around. These commands first determine the revision number that created the branch and then merge from that revision to +HEAD+. So, the following are equivalent:
50
+
51
+ # Subcheat
52
+ subcheat reintegrate foo
53
+
54
+ # Regular
55
+ svn log /svn/project/branches/foo --stop-on-copy
56
+ # note that revision number that created the branch is 5000
57
+ svn merge -r 5000:HEAD /svn/project/branches/foo .
58
+
59
+ Both +reintegrate+ and +rebase+ can accept a revision number as an argument to start the revision range to merge somewhere other than the branch starting point.
60
+
61
+ == Installation
62
+
63
+ This project will some day be released as a gem, so you can install it as easily as <tt>sudo gem install subcheat</tt>, but for now you will have to clone the project itself and include <tt>./bin/subcheat</tt> in your path in some way.
64
+
65
+ Once you've got it set up, you should really alias <tt>svn</tt> to subcheat in your shell.
66
+
67
+ == Assumptions
68
+
69
+ Subcheat assumes a particular layout for your repository:
70
+
71
+ [root]
72
+ `- project1
73
+ `- trunk
74
+ `- branches
75
+ `- tags
76
+ `- project2
77
+ `- trunk
78
+ `- branches
79
+ `- tags
80
+ `- ...
81
+
82
+ It might some day be made more flexible, but this works for me right now.
83
+
84
+ == Note on Patches/Pull Requests
85
+
86
+ * Fork the project.
87
+ * Make your feature addition.
88
+ * Add tests for it. This is important so I don't break it in a
89
+ future version unintentionally.
90
+ * Commit, do not mess with rakefile, version, or history.
91
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
92
+ * Send me a pull request. Bonus points for topic branches.
93
+
94
+ == Copyright
95
+
96
+ Copyright (c) 2009 Arjan van der Gaag. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,66 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "subcheat"
8
+ gem.summary = %Q{Wrapper for Subversion CLI}
9
+ gem.description = %Q{Wrap the Subversion CLI to extract some usage patterns into commands}
10
+ gem.email = "arjan@arjanvandergaag.nl"
11
+ gem.homepage = "http://github.com/avdgaag/subcheat"
12
+ gem.authors = ["Arjan van der Gaag"]
13
+ gem.add_development_dependency "thoughtbot-shoulda", ">= 0"
14
+ gem.add_development_dependency "cucumber", ">= 0"
15
+ gem.add_development_dependency "mocha", ">= 0.9.8"
16
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
17
+ end
18
+ Jeweler::GemcutterTasks.new
19
+ rescue LoadError
20
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
21
+ end
22
+
23
+ require 'rake/testtask'
24
+ Rake::TestTask.new(:test) do |test|
25
+ test.libs << 'lib' << 'test'
26
+ test.pattern = 'test/**/test_*.rb'
27
+ test.verbose = true
28
+ end
29
+
30
+ begin
31
+ require 'rcov/rcovtask'
32
+ Rcov::RcovTask.new do |test|
33
+ test.libs << 'test'
34
+ test.pattern = 'test/**/test_*.rb'
35
+ test.verbose = true
36
+ end
37
+ rescue LoadError
38
+ task :rcov do
39
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
40
+ end
41
+ end
42
+
43
+ task :test => :check_dependencies
44
+
45
+ begin
46
+ require 'cucumber/rake/task'
47
+ Cucumber::Rake::Task.new(:features)
48
+
49
+ task :features => :check_dependencies
50
+ rescue LoadError
51
+ task :features do
52
+ abort "Cucumber is not available. In order to run features, you must: sudo gem install cucumber"
53
+ end
54
+ end
55
+
56
+ task :default => :test
57
+
58
+ require 'rake/rdoctask'
59
+ Rake::RDocTask.new do |rdoc|
60
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
61
+
62
+ rdoc.rdoc_dir = 'rdoc'
63
+ rdoc.title = "subcheat #{version}"
64
+ rdoc.rdoc_files.include('README*')
65
+ rdoc.rdoc_files.include('lib/**/*.rb')
66
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.3.0
data/bin/subcheat ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ require File.join(File.dirname(__FILE__), *%w{.. lib subcheat})
3
+ Subcheat::Runner.new(*ARGV)
@@ -0,0 +1,14 @@
1
+ Feature: Subversion shortcuts
2
+ In order to speed up development
3
+ As a user
4
+ I want to enter common subversion commands quicker
5
+
6
+ Scenario: updating without externals
7
+ Given a working copy
8
+ When I run "subcheat uie"
9
+ Then subcheat should run "svn update --ignore-externals"
10
+
11
+ Scenario: get common working copy information
12
+ Given a working copy with attribute Revision: 54
13
+ When I run "subcheat revision"
14
+ Then subcheat should output "54"
@@ -0,0 +1,24 @@
1
+ Before do
2
+ Subcheat::Runner.output = StringIO.new
3
+ Subcheat::Runner.perform_run = false
4
+ end
5
+
6
+ Given /^a working copy$/ do
7
+ @wc ||= {}
8
+ end
9
+
10
+ Given /^(?:a working copy )?with attribute ([^:]+?): (.+?)$/ do |attribute, value|
11
+ Subcheat::Svn.any_instance.expects(:attr).with(attribute).returns(value)
12
+ end
13
+
14
+ When /^I run "subcheat([^\"]*)"$/ do |arguments|
15
+ Subcheat::Runner.new(*arguments.strip.split(/\s+/))
16
+ end
17
+
18
+ Then /^subcheat should run "([^\"]*)"$/ do |command|
19
+ assert_equal command, Subcheat::Runner.output.string.gsub(/\s*$/, '')
20
+ end
21
+
22
+ Then /^subcheat should output "([^\"]*)"$/ do |output|
23
+ assert_match(/#{output}/, Subcheat::Runner.output.string)
24
+ end
@@ -0,0 +1,19 @@
1
+ Feature: new subcheat subcommands
2
+ In order to make working with subversion a little easier
3
+ As a user
4
+ I want to use simple commands for complex patterns
5
+
6
+ Scenario: rolling back a commit
7
+ Given a working copy with attribute URL: foo
8
+ When I run "subcheat undo 50"
9
+ Then subcheat should run "svn merge -r 50:49 foo"
10
+
11
+ Scenario: rolling back a commit from a different URL
12
+ Given a working copy with attribute URL: foo
13
+ When I run "subcheat undo 50 bar"
14
+ Then subcheat should run "svn merge -r 50:49 bar"
15
+
16
+ Scenario: rolling back a range of commits
17
+ Given a working copy with attribute URL: foo
18
+ When I run "subcheat undo 50:60"
19
+ Then subcheat should run "svn merge -r 60:50 foo"
@@ -0,0 +1,5 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__) + '/../../lib')
2
+ require 'subcheat'
3
+ require 'mocha'
4
+ require 'test/unit/assertions'
5
+ World(Test::Unit::Assertions)
@@ -0,0 +1,19 @@
1
+ Feature: invisibly wrap svn
2
+ In order to transparently replace svn
3
+ As a user
4
+ I want to use normal svn commands
5
+
6
+ Scenario: pass through commands
7
+ Given a working copy
8
+ When I run "subcheat update"
9
+ Then subcheat should run "svn update"
10
+
11
+ Scenario: pass through commands with arguments
12
+ Given a working copy
13
+ When I run "subcheat update . --force"
14
+ Then subcheat should run "svn update . --force"
15
+
16
+ Scenario: pass through default command
17
+ Given a working copy
18
+ When I run "subcheat"
19
+ Then subcheat should run "svn help"
@@ -0,0 +1,108 @@
1
+ module Subcheat
2
+
3
+ # == Introduction
4
+ # A command is a simple combination of a subcommand name and a proc object
5
+ # that generates a subversion command. When invoking subcheat with a
6
+ # subcommand the +Command+ object with by that name is looked up and executed
7
+ # in the context of current directory (usually a working copy.)
8
+ #
9
+ # == Usage
10
+ #
11
+ # === Command Creation
12
+ #
13
+ # Commands are created with a special shorthand syntax:
14
+ #
15
+ # Command.define('subcommand-name') do
16
+ # # do something useful here
17
+ # end
18
+ #
19
+ # Commands are then stored in the +Command+ class, which can be queried:
20
+ #
21
+ # Command.on('subcommand-name')
22
+ #
23
+ # The returning subcommand can then be executed given a specific
24
+ # Subversion context (an instance of <tt>Subcheat::Svn</tt>):
25
+ #
26
+ # Command.on('subcommand-name').call(Svn.new)
27
+ #
28
+ # === Writing Commands
29
+ #
30
+ # Because commands get executed in a <ttr>Subcheat::SVN</tt> context, they
31
+ # have access to all its methods and instance variables. See
32
+ # <tt>Subcheat::SVN</tt> for more information.
33
+ #
34
+ # Note that a command should always return either a Subversion command
35
+ # statement as a string (e.g. "svn status"). If it returns nothing,
36
+ # nothing will be done.
37
+ #
38
+ # Commands can be stored in the <tt>lib/subcheat/commands</tt> directory,
39
+ # so they will be automatically loaded together with this class. Filenames
40
+ # are irrelevant.
41
+ #
42
+ # === Exceptions
43
+ #
44
+ # When something goes wrong, commands can raise a +CommandException+
45
+ # exception. When querying Command for a non-existant subcommand a
46
+ # +NoSuchCommand+ exception will be raised.
47
+ class Command
48
+ # Name of the subcommand to which this command should respond.
49
+ attr_reader :subcommand
50
+
51
+ # Should the output be executed as a shell command, or printed?
52
+ attr_reader :execute
53
+
54
+ # List of available commands that can be invoked.
55
+ @commands = []
56
+
57
+ class << self
58
+ # List of all available commands
59
+ attr_reader :commands
60
+
61
+ #:call-seq: define(subcommand, &block)
62
+ #
63
+ # Shortcut method to creating and registering a new +Command+ object.
64
+ #
65
+ # This will instantiate a new +Command+ with the given subcommand name,
66
+ # and the given block as its method to execute when invoked.
67
+ #
68
+ # Example usage:
69
+ #
70
+ # Command.define('nuke') do
71
+ # exec "rm -Rf"
72
+ # end
73
+ #
74
+ def define(*args, &block)
75
+ @commands << new(*args, &block)
76
+ end
77
+
78
+ # Query for a +Command+ object by the given subcommand name.
79
+ #
80
+ # This will return either a +Command+ object to be invoked, or it will
81
+ # raise a +NoSuchCommand+ exception.
82
+ def on(subcommand)
83
+ command = @commands.select { |c| c.subcommand == subcommand }.first
84
+ raise NoSuchCommand if command.nil?
85
+ command
86
+ end
87
+ end
88
+
89
+ def initialize(subcommand, execute = true, &block) #:nodoc:
90
+ @subcommand, @execute, @method = subcommand, execute, block
91
+ end
92
+
93
+ # Invoke the +Command+'s method to generate and return a subversion CLI
94
+ # statement.
95
+ #
96
+ # This requires an instance of <tt>Subcheat::Svn</tt> to be passed in,
97
+ # which will be used as context to execute the method in.
98
+ def call(svn)
99
+ Subcheat::Runner.perform_run = false unless execute
100
+ svn.instance_eval(&@method)
101
+ end
102
+ end
103
+ end
104
+
105
+ # Load command library from the /commands dir
106
+ Dir[File.join(File.dirname(__FILE__), 'commands', '*.rb')].each do |filename|
107
+ require filename
108
+ end
@@ -0,0 +1,63 @@
1
+ # Manage branches
2
+ #
3
+ # > svn branch -l
4
+ #
5
+ # List all branches for the current project.
6
+ #
7
+ # > svn branch -d FB-refactor
8
+ #
9
+ # Remove branch 'FB-refactor'
10
+ #
11
+ # > svn branch FB-refactor
12
+ #
13
+ # Create branch 'FB-refactor'
14
+ Subcheat::Command.define('branch') do
15
+ if delete = arguments.delete("-d")
16
+ raise Subcheat::CommandException, 'No URL to delete given.' unless arguments[0]
17
+ "svn delete %sbranches/%s %s" % [
18
+ attr('URL'),
19
+ arguments[0],
20
+ arguments[1..-1].join(' ')
21
+ ]
22
+ elsif list = arguments.delete('-l') || !arguments.any?
23
+ "svn list #{base_url}branches/"
24
+ else
25
+ "svn copy %s %s %s" % [
26
+ attr('URL'),
27
+ base_url + "branches/#{arguments[0]}",
28
+ arguments[1..-1].join(' ')
29
+ ]
30
+ end
31
+ end
32
+
33
+ # Manage tags
34
+ #
35
+ # > svn tag -l
36
+ #
37
+ # List all tags for the current project.
38
+ #
39
+ # > svn tag -d REL-1.0
40
+ #
41
+ # Remove tag 'REL-1.0'
42
+ #
43
+ # > svn tag REL-1.0
44
+ #
45
+ # Create tag 'REL-1.0'
46
+ Subcheat::Command.define('tag') do
47
+ if delete = arguments.delete("-d")
48
+ raise Subcheat::CommandException, 'No URL to delete given.' unless arguments[0]
49
+ "svn delete %tags/%s %s" % [
50
+ attr('URL'),
51
+ arguments[0],
52
+ arguments[1..-1].join(' ')
53
+ ]
54
+ elsif list = arguments.delete('-l') || !arguments.any?
55
+ "svn list #{base_url}tags/"
56
+ else
57
+ "svn copy %s %s %s" % [
58
+ attr('URL'),
59
+ base_url + "tags/#{arguments[0]}",
60
+ arguments[1..-1].join(' ')
61
+ ]
62
+ end
63
+ end
@@ -0,0 +1,48 @@
1
+ # Merge changes from trunk into a branch.
2
+ #
3
+ # > svn rebase
4
+ #
5
+ # This will merge all changes from trunk to the current working copy from its
6
+ # branch point to the HEAD revision. This will only work when you're inside a
7
+ # branch working copy.
8
+ #
9
+ # You can optionally specify the revision number to merge from:
10
+ #
11
+ # > svn rebase 5032
12
+ #
13
+ # This will merge from 5032:HEAD.
14
+ Subcheat::Command.define('rebase') do
15
+ raise Subcheat::CommandException, 'You can only rebase a branch working copy.' unless attr('URL') =~ /branches/
16
+ logs = log('.', '--stop-on-copy') unless arguments[0]
17
+ if logs
18
+ branch_point = logs.scan(/^r(\d+) \|/).flatten.last
19
+ else
20
+ raise Subcheat::CommandException, 'Could not calculate branch starting point. Please provide explicitly.' unless arguments[0]
21
+ end
22
+ "svn merge -r #{(arguments[0] || branch_point)}:HEAD #{base_url}trunk ."
23
+ end
24
+
25
+ # Merge changes from a branch back into trunk.
26
+ #
27
+ # > svn reintegrate FB-refactor
28
+ #
29
+ # This will merge all changes from /branches/FB-refactor from its starting point
30
+ # to the HEAD revision back into the current working copy. This is intended to be used
31
+ # inside a /trunk working copy.
32
+ #
33
+ # Optionally, you can specify the starting point of the merge, rather than using the
34
+ # branch starting point:
35
+ #
36
+ # > svn reintegrate FB-refactor 5032
37
+ #
38
+ # This will merge in changes from the branch from range 5032:HEAD.
39
+ Subcheat::Command.define('reintegrate') do
40
+ branch_url = "#{base_url}branches/#{arguments[0]}"
41
+ logs = log(branch_url, '--stop-on-copy') unless arguments[1]
42
+ if logs
43
+ branch_point = logs.scan(/^r(\d+) \|/).flatten.last
44
+ else
45
+ raise Subcheat::CommandException, 'Could not calculate branch starting point. Please provide explicitly.' unless arguments[1]
46
+ end
47
+ "svn merge -r #{(arguments[1] || branch_point)}:HEAD #{branch_url} ."
48
+ end
@@ -0,0 +1,20 @@
1
+ Subcheat::Command.define('url', false) do
2
+ attr('URL')
3
+ end
4
+
5
+ Subcheat::Command.define('revision', false) do
6
+ attr('Revision')
7
+ end
8
+
9
+ Subcheat::Command.define('path', false) do
10
+ attr('URL').sub(attr('Repository Root'), '')
11
+ end
12
+
13
+ Subcheat::Command.define('root', false) do
14
+ attr('Repository Root')
15
+ end
16
+
17
+ Subcheat::Command.define('--version') do
18
+ puts 'Subcheat ' + Subcheat.version
19
+ 'svn --version'
20
+ end
@@ -0,0 +1,57 @@
1
+ Subcheat::Command.define('uie') do
2
+ "svn update --ignore-externals #{arguments.join(' ')}"
3
+ end
4
+
5
+ # Check Out Project: shortcut to check out a working copy from the repository
6
+ #
7
+ # > svn cop my-project
8
+ #
9
+ # This will checkout the ^/my-project/trunk folder to the my-project dir in
10
+ # the current directory. You may specify a specific branch or tags:
11
+ #
12
+ # > svn cop my-project/tags/REL-1.0
13
+ #
14
+ # You may also specify the directory name to create the new working copy in:
15
+ #
16
+ # > svn cop my-project new-dir
17
+ Subcheat::Command.define('cop') do
18
+ raise 'NYI'
19
+ url = 'http://repo/' + arguments[0]
20
+ url += '/trunk' unless url =~ /trunk|tags|branches/
21
+ dir = arguments[0].gsub(/^www\.|\.(?:nl|fr|be|com).*$/i, '')
22
+ arguments.insert(1, dir) if (arguments[1] && arguments[1] =~ /^-+/) || arguments[1].nil?
23
+ "svn checkout #{url} #{arguments[1..-1].join(' ')}"
24
+ end
25
+
26
+ # Enable exporting of tags
27
+ #
28
+ # > svn export REL-1.0 ~/Desktop/export
29
+ #
30
+ # This will now export the 'REL-1.0' tag from the 'tags' directory of the repo.
31
+ # def export(args)
32
+ # args[1] = project_root_url + '/tags/' + args[1] if args[1] =~ /^[a-zA-Z\-_0-9]+$/
33
+ # end
34
+ Subcheat::Command.define('export') do
35
+ if arguments[0] =~ /^[a-zA-Z\-_0-9]+$/
36
+ "svn export %stags/%s %s" % [
37
+ base_url,
38
+ arguments[0],
39
+ arguments[1..-1].join(' ')
40
+ ]
41
+ end
42
+ end
43
+
44
+ # Enable switching to branch names.
45
+ #
46
+ # > svn switch FB-refactor
47
+ #
48
+ # This will now switch the current working copy to the 'FB-refactor' branch.
49
+ Subcheat::Command.define('switch') do
50
+ if arguments[0] =~ /^[a-zA-Z\-_0-9]+$/
51
+ "svn switch %sbranches/%s %s" % [
52
+ base_url,
53
+ arguments[0],
54
+ arguments[1..-1].join(' ')
55
+ ]
56
+ end
57
+ end
@@ -0,0 +1,22 @@
1
+ # Undo a commit or range of commits.
2
+ #
3
+ # This reverse-merges one or more revision into the current working copy.
4
+ #
5
+ # > svn undo 45
6
+ # > svn undo 45:50
7
+ #
8
+ # This will merge in 45:44 and 50:45 respectively. The source to merge from
9
+ # is the current working copy URL by default, but you may specify your own:
10
+ #
11
+ # > svn undo 5034 ^/my-project/trunk
12
+ Subcheat::Command.define('undo') do
13
+ revision, url = arguments[0], arguments[1]
14
+ revision = case revision
15
+ when /^\d+$/: "#{revision}:#{revision.to_i - 1}"
16
+ when /^\d+:\d+$/: revision.split(':').reverse.join(':')
17
+ else
18
+ raise Subcheat::CommandException, "Bad revision: #{revision}"
19
+ end
20
+ url ||= attr('URL')
21
+ "svn merge -r #{revision} #{url}"
22
+ end
@@ -0,0 +1,14 @@
1
+ module Subcheat
2
+ # General-purpose program-specific exception.
3
+ Exception = Class.new(Exception)
4
+
5
+ # Raised when looking for a custom command that does not exist.
6
+ NoSuchCommand = Class.new(Exception)
7
+
8
+ # Raised when trying to have subversion work on a directory
9
+ # that is not a working copy.
10
+ NotAWorkingCopy = Class.new(Exception)
11
+
12
+ # General-purpose exception that commands can raise to halt execution.
13
+ CommandException = Class.new(Exception)
14
+ end
@@ -0,0 +1,71 @@
1
+ module Subcheat
2
+ # the Runner handles this program's input and output, and selects and
3
+ # invokes the commands to run.
4
+ #
5
+ # == Program flow
6
+ #
7
+ # 1. The user invokes subcheat via the command line: <tt>subcheat foo</tt>
8
+ # 2. The CLI creates a new runner.
9
+ # 3. The +Runner+ finds a custom Command by the name +foo+ and invokes it.
10
+ # 4. The +Command+ returns a Subversion command to be executed.
11
+ # 5. The +Runner+ executes the command and exits.
12
+ #
13
+ # If no custom command for the given subcommand name was found, it will
14
+ # be passed along to +svn+ itself. This way, subcheat is a transparent
15
+ # wrapper around +svn+.
16
+ #
17
+ # == Testing
18
+ #
19
+ # You can control where output is sent by overriding +output+, which
20
+ # defaults to <tt>$stdin</tt>. You can also prevent the actual
21
+ # execution of commands by setting +perform_run+ to +false+.
22
+ class Runner
23
+ class << self
24
+ # Usually <tt>$stdin</tt>, but might be overridden
25
+ attr_accessor :output
26
+
27
+ # Switch that controls whether commands are executed in the system,
28
+ # or simply sent to the output stream.
29
+ attr_accessor :perform_run
30
+
31
+ # Print something to the output stream
32
+ def write(msg)
33
+ self.output.puts(msg)
34
+ end
35
+
36
+ # Run a command in the system.
37
+ # Setting +perform_run+ to +false+ will make it just output the command,
38
+ # rather than executing it.
39
+ def run(command)
40
+ (perform_run.nil? || perform_run) ? exec(command) : self.write(command)
41
+ end
42
+ end
43
+
44
+ # Create a new runner by passing it all the arguments that the
45
+ # command-line client received.
46
+ #
47
+ # The first argument is the name of the subcommand, and defaults to
48
+ # 'help'. The rest are arguments passed to the subcommand.
49
+ #
50
+ # Creating a new runner will immediately run the given subcommand.
51
+ def initialize(*args)
52
+ # Default to $stdout
53
+ self.class.output = $stdout if self.class.output.nil?
54
+
55
+ # Gather subcommand and arguments
56
+ subcommand, *arguments = args
57
+ subcommand ||= 'help'
58
+ arguments ||= []
59
+
60
+ begin
61
+ self.class.run Command.on(subcommand).call(Svn.new(arguments))
62
+ rescue NotAWorkingCopy
63
+ # ...
64
+ rescue NoSuchCommand
65
+ self.class.run "svn #{subcommand} #{arguments.join(' ')}".strip
66
+ rescue CommandException
67
+ puts $!
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,51 @@
1
+ module Subcheat
2
+ class Svn
3
+
4
+ # The arguments passed to the subcommand
5
+ attr_accessor :arguments
6
+
7
+ def initialize(arguments)
8
+ @arguments = arguments
9
+ end
10
+
11
+ # Shortcut method to the base url for the current project in the current repo.
12
+ def base_url
13
+ attr('URL').split(/branches|tags|trunk/).first
14
+ end
15
+
16
+ # Interact with Subversion through the command-line interface +svn+.
17
+ module Cli
18
+ # Extract a working copy attribute, like URL or revision number.
19
+ def attr(name)
20
+ info[/^#{name}: (.+?)$/, 1]
21
+ end
22
+
23
+ # Read the Subversion logs for a given path.
24
+ def log(repo, *arguments)
25
+ svn("log #{repo} #{[*arguments].join(' ')}")
26
+ end
27
+
28
+ # Retrieve information about the working copy.
29
+ def info
30
+ @info ||= svn('info')
31
+ end
32
+
33
+ private
34
+
35
+ # Execute a subversion command in the shell, or raise an exception if
36
+ # the target path is not actually a subversion working copy.
37
+ #--
38
+ # TODO: make this customizable, since users might sometimes ask for
39
+ # information about other paths than the current path.
40
+ def svn(subcommand)
41
+ output = `svn #{subcommand}`
42
+ raise Subcheat::NotAWorkingCopy if output.empty?
43
+ output
44
+ end
45
+ end
46
+
47
+ # other modules may implement other ways of working with subversion
48
+ # (like using the ruby bindings) but we choose the command-line client.
49
+ include Cli
50
+ end
51
+ end
data/lib/subcheat.rb ADDED
@@ -0,0 +1,12 @@
1
+ # Require all subcheat components
2
+ %w{exceptions runner svn command}.each do |filename|
3
+ require File.join(File.dirname(__FILE__), 'subcheat', filename)
4
+ end
5
+
6
+ module Subcheat
7
+ # Report the version number from /VERSION
8
+ def version
9
+ File.read(File.join(File.dirname(__FILE__), *%w{.. VERSION}))
10
+ end
11
+ extend self
12
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,19 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'shoulda'
4
+ require 'mocha'
5
+
6
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
7
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
8
+ require 'subcheat'
9
+
10
+ class Test::Unit::TestCase
11
+ def disable_running_of_commands
12
+ Subcheat::Runner.output = StringIO.new
13
+ Subcheat::Runner.perform_run = false
14
+ end
15
+
16
+ def subcheat_output
17
+ Subcheat::Runner.output.string.chomp
18
+ end
19
+ end
@@ -0,0 +1,21 @@
1
+ require 'helper'
2
+
3
+ class TestRunner < Test::Unit::TestCase
4
+ context "creating a command" do
5
+ should "create a command" do
6
+ old_count = Subcheat::Command.commands.size
7
+ Subcheat::Command.define('name') { puts 'foo' }
8
+ assert_equal(old_count + 1, Subcheat::Command.commands.size)
9
+ end
10
+ end
11
+
12
+ context 'finding commands' do
13
+ should 'return a command for a name' do
14
+ assert_instance_of(Subcheat::Command, Subcheat::Command.on('undo'))
15
+ end
16
+
17
+ should 'raise when loading non-exstant commands' do
18
+ assert_raise(Subcheat::NoSuchCommand) { Subcheat::Command.on('foo') }
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,42 @@
1
+ require 'helper'
2
+
3
+ class TestRunner < Test::Unit::TestCase
4
+ context 'No arguments' do
5
+ setup do
6
+ disable_running_of_commands
7
+ end
8
+
9
+ should 'run help by default' do
10
+ Subcheat::Runner.new
11
+ assert_equal('svn help', subcheat_output)
12
+ end
13
+ end
14
+
15
+ context 'unwrapped commands' do
16
+ setup do
17
+ disable_running_of_commands
18
+ end
19
+
20
+ should 'pass through commands' do
21
+ Subcheat::Runner.new('status')
22
+ assert_equal('svn status', subcheat_output)
23
+ end
24
+
25
+ should 'pass through arguments' do
26
+ Subcheat::Runner.new('status --ignore-externals')
27
+ assert_equal('svn status --ignore-externals', subcheat_output)
28
+ end
29
+ end
30
+
31
+ context 'wrapped commands' do
32
+ setup do
33
+ disable_running_of_commands
34
+ end
35
+
36
+ should 'find and call associated command' do
37
+ Subcheat::Command.expects(:on).with('undo').returns(stub(:call => 'foo'))
38
+ Subcheat::Runner.new('undo', '55')
39
+ assert_equal('foo', subcheat_output)
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,8 @@
1
+ require 'helper'
2
+
3
+ class TestSubcheat < Test::Unit::TestCase
4
+ should 'report version number' do
5
+ File.expects(:read).returns('foo')
6
+ assert_equal 'foo', Subcheat.version
7
+ end
8
+ end
data/test/test_svn.rb ADDED
@@ -0,0 +1,32 @@
1
+ require 'helper'
2
+
3
+ class TestRunner < Test::Unit::TestCase
4
+ context 'a working copy' do
5
+ setup do
6
+ @info = <<-EOS
7
+ Path: .
8
+ URL: https://some-kind-of-svn.com/svn/project_name/branches/languages
9
+ Repository Root: https://some-kind-of-svn.com/svn
10
+ Repository UUID: 11af7ba9-5ee7-4c51-9542-47637e3bfceb
11
+ Revision: 8049
12
+ Node Kind: directory
13
+ Schedule: normal
14
+ Last Changed Author: Andy
15
+ Last Changed Rev: 5019
16
+ Last Changed Date: 2009-12-11 15:12:57 +0100 (vr, 11 dec 2009)
17
+
18
+ EOS
19
+ @svn = Subcheat::Svn.new([])
20
+ @svn.stubs(:info).returns(@info)
21
+ end
22
+
23
+ should "read attributes" do
24
+ assert_equal('https://some-kind-of-svn.com/svn/project_name/branches/languages', @svn.attr('URL'))
25
+ assert_equal('8049', @svn.attr('Revision'))
26
+ end
27
+
28
+ should 'find project base URL' do
29
+ assert_equal('https://some-kind-of-svn.com/svn/project_name/', @svn.base_url)
30
+ end
31
+ end
32
+ end
metadata ADDED
@@ -0,0 +1,130 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: subcheat
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 3
8
+ - 0
9
+ version: 0.3.0
10
+ platform: ruby
11
+ authors:
12
+ - Arjan van der Gaag
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-03-10 00:00:00 +01:00
18
+ default_executable: subcheat
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: thoughtbot-shoulda
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 0
29
+ version: "0"
30
+ type: :development
31
+ version_requirements: *id001
32
+ - !ruby/object:Gem::Dependency
33
+ name: cucumber
34
+ prerelease: false
35
+ requirement: &id002 !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ segments:
40
+ - 0
41
+ version: "0"
42
+ type: :development
43
+ version_requirements: *id002
44
+ - !ruby/object:Gem::Dependency
45
+ name: mocha
46
+ prerelease: false
47
+ requirement: &id003 !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ segments:
52
+ - 0
53
+ - 9
54
+ - 8
55
+ version: 0.9.8
56
+ type: :development
57
+ version_requirements: *id003
58
+ description: Wrap the Subversion CLI to extract some usage patterns into commands
59
+ email: arjan@arjanvandergaag.nl
60
+ executables:
61
+ - subcheat
62
+ extensions: []
63
+
64
+ extra_rdoc_files:
65
+ - LICENSE
66
+ - README.rdoc
67
+ files:
68
+ - .document
69
+ - .gitignore
70
+ - LICENSE
71
+ - README.rdoc
72
+ - Rakefile
73
+ - VERSION
74
+ - bin/subcheat
75
+ - features/shortcuts.feature
76
+ - features/step_definitions/wrapper_steps.rb
77
+ - features/subcommands.feature
78
+ - features/support/env.rb
79
+ - features/wrapper.feature
80
+ - lib/subcheat.rb
81
+ - lib/subcheat/command.rb
82
+ - lib/subcheat/commands/branch_and_tag.rb
83
+ - lib/subcheat/commands/branching.rb
84
+ - lib/subcheat/commands/info.rb
85
+ - lib/subcheat/commands/shortcuts.rb
86
+ - lib/subcheat/commands/undo.rb
87
+ - lib/subcheat/exceptions.rb
88
+ - lib/subcheat/runner.rb
89
+ - lib/subcheat/svn.rb
90
+ - test/helper.rb
91
+ - test/test_command.rb
92
+ - test/test_runner.rb
93
+ - test/test_subcheat.rb
94
+ - test/test_svn.rb
95
+ has_rdoc: true
96
+ homepage: http://github.com/avdgaag/subcheat
97
+ licenses: []
98
+
99
+ post_install_message:
100
+ rdoc_options:
101
+ - --charset=UTF-8
102
+ require_paths:
103
+ - lib
104
+ required_ruby_version: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - ">="
107
+ - !ruby/object:Gem::Version
108
+ segments:
109
+ - 0
110
+ version: "0"
111
+ required_rubygems_version: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - ">="
114
+ - !ruby/object:Gem::Version
115
+ segments:
116
+ - 0
117
+ version: "0"
118
+ requirements: []
119
+
120
+ rubyforge_project:
121
+ rubygems_version: 1.3.6
122
+ signing_key:
123
+ specification_version: 3
124
+ summary: Wrapper for Subversion CLI
125
+ test_files:
126
+ - test/helper.rb
127
+ - test/test_command.rb
128
+ - test/test_runner.rb
129
+ - test/test_subcheat.rb
130
+ - test/test_svn.rb