subcheat 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|