subcheat 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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