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 +5 -0
- data/.gitignore +21 -0
- data/LICENSE +20 -0
- data/README.rdoc +96 -0
- data/Rakefile +66 -0
- data/VERSION +1 -0
- data/bin/subcheat +3 -0
- data/features/shortcuts.feature +14 -0
- data/features/step_definitions/wrapper_steps.rb +24 -0
- data/features/subcommands.feature +19 -0
- data/features/support/env.rb +5 -0
- data/features/wrapper.feature +19 -0
- data/lib/subcheat/command.rb +108 -0
- data/lib/subcheat/commands/branch_and_tag.rb +63 -0
- data/lib/subcheat/commands/branching.rb +48 -0
- data/lib/subcheat/commands/info.rb +20 -0
- data/lib/subcheat/commands/shortcuts.rb +57 -0
- data/lib/subcheat/commands/undo.rb +22 -0
- data/lib/subcheat/exceptions.rb +14 -0
- data/lib/subcheat/runner.rb +71 -0
- data/lib/subcheat/svn.rb +51 -0
- data/lib/subcheat.rb +12 -0
- data/test/helper.rb +19 -0
- data/test/test_command.rb +21 -0
- data/test/test_runner.rb +42 -0
- data/test/test_subcheat.rb +8 -0
- data/test/test_svn.rb +32 -0
- metadata +130 -0
data/.document
ADDED
data/.gitignore
ADDED
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,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,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
|
data/lib/subcheat/svn.rb
ADDED
@@ -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
|
data/test/test_runner.rb
ADDED
@@ -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
|
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
|