ext 0.0.5
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/MIT_LICENSE.txt +22 -0
- data/README +108 -0
- data/Rakefile +64 -0
- data/bin/ext +5 -0
- data/bin/ext.bat +6 -0
- data/lib/ext/string.rb +9 -0
- data/lib/ext/symbol.rb +7 -0
- data/lib/externals/command.rb +35 -0
- data/lib/externals/configuration/configuration.rb +159 -0
- data/lib/externals/ext.rb +557 -0
- data/lib/externals/project.rb +110 -0
- data/lib/externals/project_types/rails.rb +28 -0
- data/lib/externals/scms/git_project.rb +114 -0
- data/lib/externals/scms/svn_project.rb +98 -0
- data/lib/externals/test_case.rb +97 -0
- data/test/test_checkout_git.rb +35 -0
- data/test/test_checkout_with_subprojects_git.rb +77 -0
- data/test/test_checkout_with_subprojects_svn.rb +153 -0
- data/test/test_init_git.rb +47 -0
- data/test/test_rails_detection.rb +26 -0
- data/test/test_string_extensions.rb +15 -0
- data/test/test_touch_emptydirs.rb +52 -0
- metadata +72 -0
data/MIT_LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2008 Miles Georgi, Azimux.com, nopugs.com Consulting
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person
|
4
|
+
obtaining a copy of this software and associated documentation
|
5
|
+
files (the "Software"), to deal in the Software without
|
6
|
+
restriction, including without limitation the rights to use,
|
7
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
8
|
+
copies of the Software, and to permit persons to whom the
|
9
|
+
Software is furnished to do so, subject to the following
|
10
|
+
conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be
|
13
|
+
included in all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
17
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
19
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
20
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
21
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
data/README
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
Externals is a project that allows you to use the workflow normally made
|
2
|
+
possible by svn:externals in an SCM independent manner.
|
3
|
+
|
4
|
+
I was inspired to create this project because I had several projects that had
|
5
|
+
a mix of plugins managed by git and by svn. Git's submodule feature is
|
6
|
+
not exactly like svn:externals. Basically, I don't like how you have to manually
|
7
|
+
checkout a branch, and I don't like git status not propagating through the
|
8
|
+
submodules. Also, the branch tip doesn't automatically move with git-submodule.
|
9
|
+
Subversion always checks out the branch tip for subprojects when performing a
|
10
|
+
checkout or update.
|
11
|
+
|
12
|
+
Externals is designed such that adding support for a new SCM, or new project
|
13
|
+
types is easy.
|
14
|
+
|
15
|
+
The externals executable is called ext. Commands come in a long form and a
|
16
|
+
short form. The longer form applies the action to the main project. The short
|
17
|
+
forms apply the action to all sub projects.
|
18
|
+
|
19
|
+
The commands and usage are as follows (from 'ext help'):
|
20
|
+
|
21
|
+
ext [OPTIONS] <command> [repository[:branch]] [path]
|
22
|
+
-g, --git same as '--scm git' Uses git to checkout/export the main project
|
23
|
+
-s, --svn, --subversion same as '--scm svn' Uses subversion to checkout/export the main project
|
24
|
+
-t, --type TYPE The type of project the main project is. For example, 'rails'.
|
25
|
+
--scm SCM The SCM used to manage the main project. For example, '--scm svn'.
|
26
|
+
-w, --workdir DIR The working directory to execute commands from. Use this if for some reason you
|
27
|
+
cannot execute ext from the main project's directory (or if it's just inconvenient, such as in a script
|
28
|
+
or in a Capistrano task)
|
29
|
+
--help does the same as 'ext help' If you use this with a command
|
30
|
+
it will ignore the command and run help instead.
|
31
|
+
|
32
|
+
|
33
|
+
|
34
|
+
Commands that apply to the main project or the .externals file:
|
35
|
+
update_ignore, install, init, touch_emptydirs, help
|
36
|
+
|
37
|
+
update_ignore Adds all paths to subprojects that are
|
38
|
+
registered in .externals to the ignore feature of the
|
39
|
+
main project. This is automatically performed by install,
|
40
|
+
and so you probably only will run this if you are manually
|
41
|
+
maintaining .externals
|
42
|
+
|
43
|
+
install Usage: ext install <repository[:branch]> [path]
|
44
|
+
Registers <repository> in .externals under the appropriate
|
45
|
+
SCM. Checks out the project, and also adds it to the ignore
|
46
|
+
feature offered by the SCM of the main project. If the SCM type
|
47
|
+
is not obvious from the repository URL, use the --scm, --git,
|
48
|
+
or --svn flags.
|
49
|
+
|
50
|
+
init Creates a .externals file containing only [main]
|
51
|
+
It will try to determine the SCM used by the main project,
|
52
|
+
as well as the project type. You don't have to specify
|
53
|
+
a project type if you don't want to or if your project type
|
54
|
+
isn't supported. It just means that when using 'install'
|
55
|
+
that you'll want to specify the path.
|
56
|
+
|
57
|
+
touch_emptydirs Recurses through all directories from the
|
58
|
+
top and adds a .emptydir file to any empty directories it
|
59
|
+
comes across. Useful for dealing with SCMs that refuse to
|
60
|
+
track empty directories (such as git, for example)
|
61
|
+
|
62
|
+
help You probably just ran this command just now.
|
63
|
+
|
64
|
+
|
65
|
+
|
66
|
+
Commands that apply to the main project and all subprojects:
|
67
|
+
checkout, export, status, update
|
68
|
+
|
69
|
+
checkout Usage: ext checkout <repository>
|
70
|
+
|
71
|
+
Checks out <repository>, and checks out any subprojects
|
72
|
+
registered in <repository>'s .externals file.
|
73
|
+
|
74
|
+
export Usage: ext export <repository>
|
75
|
+
|
76
|
+
Like checkout except this command fetches as little
|
77
|
+
history as possible.
|
78
|
+
|
79
|
+
status Usage: ext status
|
80
|
+
|
81
|
+
Prints out the status of the main project, followed by
|
82
|
+
the status of each subproject.
|
83
|
+
|
84
|
+
update Usage: ext update
|
85
|
+
|
86
|
+
Brings the main project, and all subprojects, up to the
|
87
|
+
latest version.
|
88
|
+
|
89
|
+
|
90
|
+
|
91
|
+
Commands that only apply to the subprojects:
|
92
|
+
co, ex, st, up
|
93
|
+
|
94
|
+
co Like checkout, but skips the main project and
|
95
|
+
only checks out subprojects.
|
96
|
+
|
97
|
+
ex Like export, but skips the main project.
|
98
|
+
|
99
|
+
st Like status, but skips the main project.
|
100
|
+
|
101
|
+
up Like update, but skips the main project.
|
102
|
+
|
103
|
+
|
104
|
+
The externals project is copyright 2008 by Miles Georgi, nopugs.com, azimux.com
|
105
|
+
and is released under the MIT license.
|
106
|
+
|
107
|
+
The license is available in the same directory as this README
|
108
|
+
file and is named MIT_LICENSE.txt
|
data/Rakefile
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/testtask'
|
3
|
+
require 'rake/rdoctask'
|
4
|
+
require 'rake/gempackagetask'
|
5
|
+
|
6
|
+
Rake::TestTask.new('test') do |task|
|
7
|
+
task.libs = [File.expand_path('lib'),File.expand_path('test')]
|
8
|
+
task.pattern = './test/test_*.rb'
|
9
|
+
task.warning = true
|
10
|
+
end
|
11
|
+
|
12
|
+
gem_specification = Gem::Specification.new do |specification|
|
13
|
+
specification.name = 'ext'
|
14
|
+
specification.version = '0.0.5'
|
15
|
+
specification.platform = Gem::Platform::RUBY
|
16
|
+
specification.rubyforge_project = 'ext'
|
17
|
+
|
18
|
+
specification.summary =
|
19
|
+
%{Provides an SCM agnostic way to manage subprojects with a workflow similar
|
20
|
+
to the svn:externals feature of subversion. It's particularly useful for rails
|
21
|
+
projects that have some plugins managed by svn and some managed by git.}
|
22
|
+
specification.description =
|
23
|
+
%{Provides an SCM agnostic way to manage subprojects with a workflow similar
|
24
|
+
to the scm:externals feature of subversion. It's particularly useful for rails
|
25
|
+
projects that have some plugins managed by svn and some managed by git.
|
26
|
+
|
27
|
+
For example, "ext install git://github.com/rails/rails.git" from within a rails
|
28
|
+
application directory will realize that this belongs in the vendor/rails folder.
|
29
|
+
It will also realize that this URL is a git repository and clone it into that
|
30
|
+
folder.
|
31
|
+
|
32
|
+
It will also add the vendor/rails folder to the ignore feature for the SCM of
|
33
|
+
the main project. Let's say that the main project is being managed by
|
34
|
+
subversion. In that case it adds "rails" to the svn:ignore property of the
|
35
|
+
vendor folder. It also adds the URL to the .externals file so that when this
|
36
|
+
project is checked out via "ext checkout" it knows where to fetch the
|
37
|
+
subprojects.
|
38
|
+
|
39
|
+
There are several other useful commands, such as init, touch_emptydirs, add_all,
|
40
|
+
export, status. I plan to put up a tutorial at http://nopugs.com/ext-tutorial
|
41
|
+
|
42
|
+
The reason I made this project is that I was frustrated by two things:
|
43
|
+
|
44
|
+
1. In my opinion, the workflow for svn:externals is far superior to
|
45
|
+
git-submodule.
|
46
|
+
|
47
|
+
2. Even if git-submodule was as useful as svn:externals, I would still like a
|
48
|
+
uniform way to fetch all of the subprojects regardless of the SCM used to manage
|
49
|
+
the main project.}
|
50
|
+
|
51
|
+
specification.author = "Miles Georgi"
|
52
|
+
specification.email = "azimux@gmail.com"
|
53
|
+
specification.homepage = "http://nopugs.com/ext-tutorial"
|
54
|
+
|
55
|
+
specification.test_files = FileList['test/test_*.rb']
|
56
|
+
specification.executables = ['ext', 'ext.bat']
|
57
|
+
specification.files = ['Rakefile', 'README', 'MIT_LICENSE.txt'] +
|
58
|
+
FileList['lib/**/*.rb']
|
59
|
+
#specification.require_path = 'lib'
|
60
|
+
end
|
61
|
+
|
62
|
+
Rake::GemPackageTask.new(gem_specification) do |package|
|
63
|
+
package.need_zip = package.need_tar = false
|
64
|
+
end
|
data/bin/ext
ADDED
data/bin/ext.bat
ADDED
data/lib/ext/string.rb
ADDED
data/lib/ext/symbol.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
module Externals
|
2
|
+
class Command
|
3
|
+
attr_reader :name, :usage, :summary
|
4
|
+
def initialize name, usage, summary = nil
|
5
|
+
@name = name
|
6
|
+
@usage = usage
|
7
|
+
@summary = summary
|
8
|
+
|
9
|
+
if !@summary
|
10
|
+
@summary, @usage = @usage, @summary
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_s
|
15
|
+
retval = StringIO.new
|
16
|
+
retval.printf "%-16s", name
|
17
|
+
if usage
|
18
|
+
retval.printf "Usage: #{usage}\n"
|
19
|
+
else
|
20
|
+
dont_pad_first = true
|
21
|
+
end
|
22
|
+
|
23
|
+
summary.split(/\n/).each_with_index do |line, index|
|
24
|
+
if index == 0 && dont_pad_first
|
25
|
+
retval.printf "%s\n", line.strip
|
26
|
+
else
|
27
|
+
retval.printf "%16s%s\n", '', line.strip
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
retval.printf "\n"
|
32
|
+
retval.string
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,159 @@
|
|
1
|
+
module Externals
|
2
|
+
module Configuration
|
3
|
+
SECTION_TITLE_REGEX = /^\s*\[(\w+)\]\s*$/
|
4
|
+
SECTION_TITLE_REGEX_NO_GROUPS = /^\s*\[(?:\w+)\]\s*$/
|
5
|
+
|
6
|
+
|
7
|
+
class Section
|
8
|
+
attr_accessor :title_string, :body_string, :title, :rows, :scm
|
9
|
+
|
10
|
+
def main?
|
11
|
+
title == 'main'
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize title_string, body_string, scm = nil
|
15
|
+
self.title_string = title_string
|
16
|
+
self.body_string = body_string
|
17
|
+
self.scm = scm
|
18
|
+
|
19
|
+
self.title = SECTION_TITLE_REGEX.match(title_string)[1]
|
20
|
+
|
21
|
+
self.scm ||= self.title
|
22
|
+
|
23
|
+
raise "Invalid section title: #{title_string}" unless title
|
24
|
+
|
25
|
+
self.rows = body_string.split(/\n/)
|
26
|
+
end
|
27
|
+
|
28
|
+
def setting key
|
29
|
+
if !main?
|
30
|
+
raise "this isn't a section of the configuration that can contain settings"
|
31
|
+
end
|
32
|
+
|
33
|
+
rows.each do |row|
|
34
|
+
if row =~ /\s*(\w+)\s*=\s*([^#]*)(?:#.*)?$/ && key.to_s == $1
|
35
|
+
return $2
|
36
|
+
end
|
37
|
+
end
|
38
|
+
nil
|
39
|
+
end
|
40
|
+
|
41
|
+
def [] key
|
42
|
+
setting(key)
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
def projects
|
47
|
+
return @projects if @projects
|
48
|
+
|
49
|
+
@projects = []
|
50
|
+
|
51
|
+
if main?
|
52
|
+
@projects = [Ext.project_class(self['scm']).new(".", :is_main)]
|
53
|
+
else
|
54
|
+
rows.each do |row|
|
55
|
+
if Project.project_line?(row)
|
56
|
+
@projects << Ext.project_class(title).new(row)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
@projects
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def add_row(row)
|
64
|
+
rows << row
|
65
|
+
|
66
|
+
self.body_string = body_string.chomp + "\n#{row}\n"
|
67
|
+
clear_caches
|
68
|
+
end
|
69
|
+
|
70
|
+
def clear_caches
|
71
|
+
@projects = nil
|
72
|
+
end
|
73
|
+
|
74
|
+
def to_s
|
75
|
+
"#{title_string}#{body_string}"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
class Configuration
|
80
|
+
attr_accessor :file_string
|
81
|
+
|
82
|
+
def sections
|
83
|
+
@sections ||= []
|
84
|
+
end
|
85
|
+
|
86
|
+
def [] title
|
87
|
+
title = title.to_s
|
88
|
+
sections.detect {|section| section.title == title}
|
89
|
+
end
|
90
|
+
|
91
|
+
def add_empty_section title
|
92
|
+
raise "Section already exists" if self[title]
|
93
|
+
sections << Section.new("\n\n[#{title.to_s}]\n", "")
|
94
|
+
end
|
95
|
+
|
96
|
+
def self.new_empty
|
97
|
+
new nil, true
|
98
|
+
end
|
99
|
+
|
100
|
+
def initialize externals_file = nil, empty = false
|
101
|
+
if empty
|
102
|
+
self.file_string = ''
|
103
|
+
return
|
104
|
+
end
|
105
|
+
|
106
|
+
if !externals_file && File.exists?('.externals')
|
107
|
+
open('.externals', 'r') do |f|
|
108
|
+
externals_file = f.read
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
externals_file ||= ""
|
113
|
+
|
114
|
+
self.file_string = externals_file
|
115
|
+
|
116
|
+
titles = externals_file.grep SECTION_TITLE_REGEX
|
117
|
+
bodies = externals_file.split SECTION_TITLE_REGEX_NO_GROUPS
|
118
|
+
|
119
|
+
if titles.size > 0 && bodies.size > 0
|
120
|
+
if titles.size + 1 != bodies.size
|
121
|
+
raise "bodies and sections do not match up"
|
122
|
+
end
|
123
|
+
|
124
|
+
bodies = bodies[1..(bodies.size - 1)]
|
125
|
+
|
126
|
+
(0...(bodies.size)).each do |index|
|
127
|
+
sections << Section.new(titles[index], bodies[index])
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def projects
|
133
|
+
retval = []
|
134
|
+
sections.each do |section|
|
135
|
+
retval += section.projects
|
136
|
+
end
|
137
|
+
|
138
|
+
retval
|
139
|
+
end
|
140
|
+
|
141
|
+
def subprojects
|
142
|
+
retval = []
|
143
|
+
sections.each do |section|
|
144
|
+
retval += section.projects unless section.main?
|
145
|
+
end
|
146
|
+
|
147
|
+
retval
|
148
|
+
end
|
149
|
+
|
150
|
+
def write path = ".externals"
|
151
|
+
open(path, 'w') do |f|
|
152
|
+
sections.each do |section|
|
153
|
+
f.write section.to_s
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
@@ -0,0 +1,557 @@
|
|
1
|
+
require 'externals/project'
|
2
|
+
require 'externals/configuration/configuration'
|
3
|
+
require 'optparse'
|
4
|
+
require 'externals/command'
|
5
|
+
require 'ext/symbol'
|
6
|
+
|
7
|
+
module Externals
|
8
|
+
PROJECT_TYPES_DIRECTORY = File.join(File.dirname(__FILE__), '..', 'externals','project_types')
|
9
|
+
|
10
|
+
# Full commands operate on the main project as well as the externals
|
11
|
+
# short commands only operate on the externals
|
12
|
+
# Main commands only operate on the main project
|
13
|
+
FULL_COMMANDS_HASH = [
|
14
|
+
[:checkout, "ext checkout <repository>", %{
|
15
|
+
Checks out <repository>, and checks out any subprojects
|
16
|
+
registered in <repository>'s .externals file.}],
|
17
|
+
[:export, "ext export <repository>", %{
|
18
|
+
Like checkout except this command fetches as little
|
19
|
+
history as possible.}],
|
20
|
+
[:status, "ext status", %{
|
21
|
+
Prints out the status of the main project, followed by
|
22
|
+
the status of each subproject.}],
|
23
|
+
[:update, "ext update", %{
|
24
|
+
Brings the main project, and all subprojects, up to the
|
25
|
+
latest version.}]
|
26
|
+
]
|
27
|
+
SHORT_COMMANDS_HASH = [
|
28
|
+
[:co, "Like checkout, but skips the main project and
|
29
|
+
only checks out subprojects."],
|
30
|
+
[:ex, "Like export, but skips the main project."],
|
31
|
+
[:st, "Like status, but skips the main project."],
|
32
|
+
[:up, "Like update, but skips the main project."]
|
33
|
+
]
|
34
|
+
MAIN_COMMANDS_HASH = [
|
35
|
+
[:update_ignore, "Adds all paths to subprojects that are
|
36
|
+
registered in .externals to the ignore feature of the
|
37
|
+
main project. This is automatically performed by install,
|
38
|
+
and so you probably only will run this if you are manually
|
39
|
+
maintaining .externals"],
|
40
|
+
[:install, "ext install <repository[:branch]> [path]",
|
41
|
+
"Registers <repository> in .externals under the appropriate
|
42
|
+
SCM. Checks out the project, and also adds it to the ignore
|
43
|
+
feature offered by the SCM of the main project. If the SCM
|
44
|
+
type is not obvious from the repository URL, use the --scm,
|
45
|
+
--git, or --svn flags."],
|
46
|
+
[:init, "Creates a .externals file containing only [main]
|
47
|
+
It will try to determine the SCM used by the main project,
|
48
|
+
as well as the project type. You don't have to specify
|
49
|
+
a project type if you don't want to or if your project type
|
50
|
+
isn't supported. It just means that when using 'install'
|
51
|
+
that you'll want to specify the path."],
|
52
|
+
[:touch_emptydirs, "Recurses through all directories from the
|
53
|
+
top and adds a .emptydir file to any empty directories it
|
54
|
+
comes across. Useful for dealing with SCMs that refuse to
|
55
|
+
track empty directories (such as git, for example)"],
|
56
|
+
[:help, "You probably just ran this command just now."]
|
57
|
+
]
|
58
|
+
|
59
|
+
|
60
|
+
FULL_COMMANDS = FULL_COMMANDS_HASH.map(&:first)
|
61
|
+
SHORT_COMMANDS = SHORT_COMMANDS_HASH.map(&:first)
|
62
|
+
MAIN_COMMANDS = MAIN_COMMANDS_HASH.map(&:first)
|
63
|
+
|
64
|
+
COMMANDS = FULL_COMMANDS + SHORT_COMMANDS + MAIN_COMMANDS
|
65
|
+
|
66
|
+
|
67
|
+
class Ext
|
68
|
+
Dir.entries(File.join(File.dirname(__FILE__), '..', 'ext')).each do |extension|
|
69
|
+
require "ext/#{extension}" if extension =~ /.rb$/
|
70
|
+
end
|
71
|
+
|
72
|
+
Dir.entries(File.join(File.dirname(__FILE__), '..', 'externals','scms')).each do |project|
|
73
|
+
require "externals/scms/#{project}" if project =~ /_project.rb$/
|
74
|
+
end
|
75
|
+
|
76
|
+
|
77
|
+
def self.project_types
|
78
|
+
types = Dir.entries(PROJECT_TYPES_DIRECTORY).select do |file|
|
79
|
+
file =~ /\.rb$/
|
80
|
+
end
|
81
|
+
|
82
|
+
types.map do |type|
|
83
|
+
/^(.*)\.rb$/.match(type)[1]
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
#puts "Project types available: #{project_types.join(' ')}"
|
88
|
+
|
89
|
+
def self.project_type_files
|
90
|
+
project_types.map do |project_type|
|
91
|
+
"#{File.join(PROJECT_TYPES_DIRECTORY, project_type)}.rb"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
project_type_files.each do |file|
|
96
|
+
require file
|
97
|
+
end
|
98
|
+
|
99
|
+
def self.new_opts main_options, sub_options
|
100
|
+
opts = OptionParser.new
|
101
|
+
|
102
|
+
opts.banner = "ext [OPTIONS] <command> [repository[:branch]] [path]"
|
103
|
+
|
104
|
+
project_classes.each do |project_class|
|
105
|
+
project_class.fill_in_opts(opts, main_options, sub_options)
|
106
|
+
end
|
107
|
+
|
108
|
+
opts.on("--type TYPE", "-t TYPE", "The type of project the main project is. For example, 'rails'.",
|
109
|
+
Integer) {|type| sub_options[:scm] = main_options[:type] = type}
|
110
|
+
opts.on("--scm SCM", "-s SCM", "The SCM used to manage the main project. For example, '--scm svn'.",
|
111
|
+
Integer) {|scm| sub_options[:scm] = main_options[:scm] = scm}
|
112
|
+
opts.on("--workdir DIR", "-w DIR", "The working directory to execute commands from. Use this if for some reason you
|
113
|
+
cannot execute ext from the main project's directory (or if it's just inconvenient, such as in a script
|
114
|
+
or in a Capistrano task)",
|
115
|
+
String) {|dir|
|
116
|
+
raise "No such directory: #{dir}" unless File.exists?(dir) && File.directory?(dir)
|
117
|
+
main_options[:workdir] = dir
|
118
|
+
}
|
119
|
+
opts.on("--help", "does the same as 'ext help' If you use this with a command
|
120
|
+
it will ignore the command and run help instead.") {main_options[:help] = true}
|
121
|
+
end
|
122
|
+
|
123
|
+
def self.run *arguments
|
124
|
+
|
125
|
+
main_options = {}
|
126
|
+
sub_options = {}
|
127
|
+
|
128
|
+
opts = new_opts main_options, sub_options
|
129
|
+
|
130
|
+
args = opts.parse(arguments)
|
131
|
+
|
132
|
+
unless args.nil? || args.empty?
|
133
|
+
command = args[0]
|
134
|
+
args = args[1..(args.size - 1)] || []
|
135
|
+
end
|
136
|
+
|
137
|
+
command &&= command.to_sym
|
138
|
+
|
139
|
+
command = :help if main_options[:help]
|
140
|
+
|
141
|
+
if !command || command.to_s == ''
|
142
|
+
puts "hey... you didn't tell me what you want to do."
|
143
|
+
puts "Try 'ext help' for a list of commands"
|
144
|
+
exit
|
145
|
+
end
|
146
|
+
|
147
|
+
unless COMMANDS.index command
|
148
|
+
puts "unknown command: #{command}"
|
149
|
+
puts "for a list of commands try 'ext help'"
|
150
|
+
exit
|
151
|
+
end
|
152
|
+
|
153
|
+
Dir.chdir(main_options[:workdir] || ".") do
|
154
|
+
new(main_options).send(command, args, sub_options)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def print_commands(commands)
|
159
|
+
commands.each do |command|
|
160
|
+
puts Command.new(*command)
|
161
|
+
end
|
162
|
+
puts
|
163
|
+
end
|
164
|
+
|
165
|
+
def help(args, options)
|
166
|
+
puts "#{self.class.new_opts({},{}).to_s}\n\n"
|
167
|
+
|
168
|
+
puts "\nCommands that apply to the main project or the .externals file:"
|
169
|
+
puts "#{MAIN_COMMANDS.join(', ')}\n\n"
|
170
|
+
print_commands(MAIN_COMMANDS_HASH)
|
171
|
+
|
172
|
+
puts "\nCommands that apply to the main project and all subprojects:"
|
173
|
+
puts "#{FULL_COMMANDS.join(', ')}\n\n"
|
174
|
+
print_commands(FULL_COMMANDS_HASH)
|
175
|
+
|
176
|
+
puts "\nCommands that only apply to the subprojects:"
|
177
|
+
puts "#{SHORT_COMMANDS.join(', ')}\n\n"
|
178
|
+
print_commands(SHORT_COMMANDS_HASH)
|
179
|
+
end
|
180
|
+
|
181
|
+
def self.registered_scms
|
182
|
+
return @registered_scms if @registered_scms
|
183
|
+
@registered_scms ||= []
|
184
|
+
|
185
|
+
scmdir = File.join(File.dirname(__FILE__), 'scms')
|
186
|
+
|
187
|
+
Dir.entries(scmdir).each do |file|
|
188
|
+
if file =~ /^(.*)_project\.rb$/
|
189
|
+
@registered_scms << $1
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
@registered_scms
|
194
|
+
end
|
195
|
+
|
196
|
+
def projects
|
197
|
+
configuration.projects
|
198
|
+
end
|
199
|
+
|
200
|
+
def subprojects
|
201
|
+
configuration.subprojects
|
202
|
+
end
|
203
|
+
|
204
|
+
def configuration
|
205
|
+
return @configuration if @configuration
|
206
|
+
|
207
|
+
@configuration = Configuration::Configuration.new
|
208
|
+
end
|
209
|
+
|
210
|
+
def reload_configuration
|
211
|
+
@configuration = nil
|
212
|
+
configuration
|
213
|
+
end
|
214
|
+
|
215
|
+
def initialize options
|
216
|
+
super()
|
217
|
+
|
218
|
+
scm = configuration['main']
|
219
|
+
scm = scm['scm'] if scm
|
220
|
+
scm ||= options[:scm]
|
221
|
+
#scm ||= infer_scm(repository)
|
222
|
+
|
223
|
+
type = configuration['main']
|
224
|
+
type = type['type'] if type
|
225
|
+
|
226
|
+
type ||= options[:type]
|
227
|
+
|
228
|
+
if type
|
229
|
+
install_project_type type
|
230
|
+
else
|
231
|
+
possible_project_types = self.class.project_types.select do |project_type|
|
232
|
+
self.class.project_type_detector(project_type).detected?
|
233
|
+
end
|
234
|
+
|
235
|
+
if possible_project_types.size > 1
|
236
|
+
raise "We found multiple project types that this could be: #{possible_project_types.join(',')}
|
237
|
+
Please use
|
238
|
+
the --type option to tell ext which to use."
|
239
|
+
else
|
240
|
+
possible_project_types.each do |project_type|
|
241
|
+
install_project_type project_type
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
def self.project_class(scm)
|
248
|
+
Externals.module_eval("#{scm.to_s.cap_first}Project", __FILE__, __LINE__)
|
249
|
+
end
|
250
|
+
|
251
|
+
def self.project_classes
|
252
|
+
retval = []
|
253
|
+
registered_scms.each do |scm|
|
254
|
+
retval << project_class(scm)
|
255
|
+
end
|
256
|
+
|
257
|
+
retval
|
258
|
+
end
|
259
|
+
|
260
|
+
SHORT_COMMANDS.each do |command_name|
|
261
|
+
define_method command_name do |args, options|
|
262
|
+
project_name_or_path = nil
|
263
|
+
|
264
|
+
if args && !args.empty?
|
265
|
+
project_name_or_path = args.first
|
266
|
+
end
|
267
|
+
|
268
|
+
if project_name_or_path
|
269
|
+
project = subprojects.detect do |project|
|
270
|
+
project.name == project_name_or_path || project.path == project_name_or_path
|
271
|
+
end
|
272
|
+
|
273
|
+
raise "no such project" unless project
|
274
|
+
|
275
|
+
project.send command_name, args, options
|
276
|
+
else
|
277
|
+
subprojects.each {|p| p.send(*([command_name, args, options].flatten))}
|
278
|
+
end
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
def install args, options
|
283
|
+
init args, options unless File.exists? '.externals'
|
284
|
+
row = args.join " "
|
285
|
+
|
286
|
+
orig_options = options.dup
|
287
|
+
|
288
|
+
scm = options[:scm]
|
289
|
+
|
290
|
+
scm ||= infer_scm(row)
|
291
|
+
|
292
|
+
if !configuration[scm]
|
293
|
+
configuration.add_empty_section(scm)
|
294
|
+
end
|
295
|
+
configuration[scm].add_row(row)
|
296
|
+
configuration.write
|
297
|
+
reload_configuration
|
298
|
+
|
299
|
+
project = self.class.project_class(scm).new(row)
|
300
|
+
|
301
|
+
project.co
|
302
|
+
|
303
|
+
update_ignore args, orig_options
|
304
|
+
end
|
305
|
+
|
306
|
+
def update_ignore args, options
|
307
|
+
#path = args[0]
|
308
|
+
|
309
|
+
|
310
|
+
scm = configuration['main']
|
311
|
+
scm = scm['scm'] if scm
|
312
|
+
|
313
|
+
scm ||= options[:scm]
|
314
|
+
|
315
|
+
unless scm
|
316
|
+
raise "You need to either specify the scm as the first line in .externals (for example, scm = git), or use an option to specify it
|
317
|
+
(such as --git or --svn)"
|
318
|
+
end
|
319
|
+
|
320
|
+
project = self.class.project_class(scm).new(".")
|
321
|
+
|
322
|
+
raise "only makes sense for main project" unless project.main?
|
323
|
+
|
324
|
+
subprojects.each do |subproject|
|
325
|
+
puts "about to add #{subproject.path} to ignore"
|
326
|
+
project.update_ignore subproject.path
|
327
|
+
puts "finished adding #{subproject.path}"
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
def touch_emptydirs args, options
|
332
|
+
require 'find'
|
333
|
+
|
334
|
+
excludes = ['.','..','.svn', '.git']
|
335
|
+
|
336
|
+
excludes.dup.each do |exclude|
|
337
|
+
excludes << "./#{exclude}"
|
338
|
+
end
|
339
|
+
|
340
|
+
paths = []
|
341
|
+
|
342
|
+
Find.find('.') do |f|
|
343
|
+
if File.directory?(f)
|
344
|
+
excluded = false
|
345
|
+
File.split(f).each do |part|
|
346
|
+
exclude ||= excludes.index(part)
|
347
|
+
end
|
348
|
+
|
349
|
+
if !excluded && ((Dir.entries(f) - excludes).size == 0)
|
350
|
+
paths << f
|
351
|
+
end
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
paths.each do |p|
|
356
|
+
open(File.join(p,".emptydir"), "w").close
|
357
|
+
end
|
358
|
+
|
359
|
+
end
|
360
|
+
|
361
|
+
|
362
|
+
def status args, options
|
363
|
+
options ||= {}
|
364
|
+
repository = "."
|
365
|
+
path = "."
|
366
|
+
main_project = nil
|
367
|
+
scm = options[:scm]
|
368
|
+
scm ||= infer_scm(repository)
|
369
|
+
|
370
|
+
if !scm
|
371
|
+
scm ||= configuration['main']
|
372
|
+
scm &&= scm['scm']
|
373
|
+
end
|
374
|
+
|
375
|
+
if !scm
|
376
|
+
possible_project_classes = self.class.project_classes.select do |project_class|
|
377
|
+
project_class.detected?
|
378
|
+
end
|
379
|
+
|
380
|
+
raise "Could not determine this projects scm" if possible_project_classes.empty?
|
381
|
+
if possible_project_classes.size > 1
|
382
|
+
raise "This project appears to be managed by multiple SCMs: #{
|
383
|
+
possible_project_classes.map(&:to_s).join(',')}
|
384
|
+
Please explicitly declare the SCM (by using --git or --svn, or,
|
385
|
+
by creating the .externals file manually"
|
386
|
+
end
|
387
|
+
|
388
|
+
scm = possible_project_classes.first.scm
|
389
|
+
end
|
390
|
+
|
391
|
+
unless scm
|
392
|
+
raise "You need to either specify the scm as the first line in .externals, or use an option to specify it
|
393
|
+
(such as --git or --svn)"
|
394
|
+
end
|
395
|
+
|
396
|
+
main_project = self.class.project_class(scm).new("#{repository} #{path}", :is_main)
|
397
|
+
main_project.st
|
398
|
+
|
399
|
+
self.class.new({}).st [], {} #args, options
|
400
|
+
end
|
401
|
+
|
402
|
+
def update args, options
|
403
|
+
options ||= {}
|
404
|
+
repository = args[0]
|
405
|
+
main_project = nil
|
406
|
+
scm = options[:scm]
|
407
|
+
scm ||= infer_scm(repository)
|
408
|
+
|
409
|
+
if !scm
|
410
|
+
scm ||= configuration['main']
|
411
|
+
scm &&= scm['scm']
|
412
|
+
end
|
413
|
+
|
414
|
+
unless scm
|
415
|
+
raise "You need to either specify the scm as the first line in .externals, or use an option to specify it
|
416
|
+
(such as --git or --svn)"
|
417
|
+
end
|
418
|
+
|
419
|
+
main_project = self.class.project_class(scm).new("#{repository} #{path}", :is_main)
|
420
|
+
main_project.up
|
421
|
+
|
422
|
+
self.class.new({}).up [], {} #args, options
|
423
|
+
end
|
424
|
+
|
425
|
+
def checkout args, options
|
426
|
+
options ||= {}
|
427
|
+
|
428
|
+
repository = args[0]
|
429
|
+
path = args[1] || "."
|
430
|
+
|
431
|
+
main_project = do_checkout_or_export repository, path, options, :checkout
|
432
|
+
|
433
|
+
if path == "."
|
434
|
+
path = main_project.name
|
435
|
+
end
|
436
|
+
|
437
|
+
Dir.chdir path do
|
438
|
+
self.class.new({}).co [], {} #args, options
|
439
|
+
end
|
440
|
+
end
|
441
|
+
|
442
|
+
def export args, options
|
443
|
+
options ||= {}
|
444
|
+
|
445
|
+
repository = args[0]
|
446
|
+
path = args[1] || "."
|
447
|
+
|
448
|
+
main_project = do_checkout_or_export repository, path, options, :checkout
|
449
|
+
|
450
|
+
if path == "."
|
451
|
+
path = main_project.name
|
452
|
+
end
|
453
|
+
|
454
|
+
Dir.chdir path do
|
455
|
+
self.class.new({}).ex [], {} #args, options
|
456
|
+
end
|
457
|
+
end
|
458
|
+
|
459
|
+
def init args, options = {}
|
460
|
+
raise ".externals already exists" if File.exists? '.externals'
|
461
|
+
|
462
|
+
scm = options[:scm]
|
463
|
+
type = options[:type]
|
464
|
+
|
465
|
+
if !scm
|
466
|
+
possible_project_classes = self.class.project_classes.select do |project_class|
|
467
|
+
project_class.detected?
|
468
|
+
end
|
469
|
+
|
470
|
+
raise "Could not determine this projects scm" if possible_project_classes.empty?
|
471
|
+
if possible_project_classes.size > 1
|
472
|
+
raise "This project appears to be managed by multiple SCMs: #{
|
473
|
+
possible_project_classes.map(&:to_s).join(',')}
|
474
|
+
Please explicitly declare the SCM (using --git or --svn, or, by creating .externals manually"
|
475
|
+
end
|
476
|
+
|
477
|
+
scm = possible_project_classes.first.scm
|
478
|
+
end
|
479
|
+
|
480
|
+
if !type
|
481
|
+
possible_project_types = self.class.project_types.select do |project_type|
|
482
|
+
self.class.project_type_detector(project_type).detected?
|
483
|
+
end
|
484
|
+
|
485
|
+
if possible_project_types.size > 1
|
486
|
+
raise "We found multiple project types that this could be: #{possible_project_types.join(',')}
|
487
|
+
Please use the --type option to tell ext which to use."
|
488
|
+
elsif possible_project_types.size == 0
|
489
|
+
puts "WARNING: We could not automatically determine the project type.
|
490
|
+
Be sure to specify paths when adding subprojects to your .externals file"
|
491
|
+
else
|
492
|
+
type = possible_project_types.first
|
493
|
+
end
|
494
|
+
end
|
495
|
+
|
496
|
+
config = Configuration::Configuration.new_empty
|
497
|
+
|
498
|
+
config.sections << Configuration::Section.new("[main]\n",
|
499
|
+
"scm = #{scm}\n" +
|
500
|
+
"#{'type = ' + type if type}\n")
|
501
|
+
|
502
|
+
config.write
|
503
|
+
reload_configuration
|
504
|
+
end
|
505
|
+
|
506
|
+
def self.project_type_detector name
|
507
|
+
Externals.module_eval("#{name.classify}Detector", __FILE__, __LINE__)
|
508
|
+
end
|
509
|
+
|
510
|
+
def install_project_type name
|
511
|
+
Externals.module_eval("#{name.classify}ProjectType", __FILE__, __LINE__).install
|
512
|
+
end
|
513
|
+
#
|
514
|
+
#
|
515
|
+
# def self.determine_project_type path = "."
|
516
|
+
# Dir.chdir path do
|
517
|
+
# raise "not done"
|
518
|
+
# end
|
519
|
+
# end
|
520
|
+
|
521
|
+
protected
|
522
|
+
def do_checkout_or_export repository, path, options, sym
|
523
|
+
if File.exists?('.externals')
|
524
|
+
raise "seems main project is already checked out here?"
|
525
|
+
else
|
526
|
+
#We appear to be attempting to checkout/export a main project
|
527
|
+
scm = options[:scm]
|
528
|
+
|
529
|
+
scm ||= infer_scm(repository)
|
530
|
+
|
531
|
+
if !scm
|
532
|
+
scm ||= configuration['main']
|
533
|
+
scm &&= scm['scm']
|
534
|
+
end
|
535
|
+
|
536
|
+
unless scm
|
537
|
+
raise "You need to either specify the scm as the first line in .externals, or use an option to specify it
|
538
|
+
(such as --git or --svn)"
|
539
|
+
end
|
540
|
+
|
541
|
+
main_project = self.class.project_class(scm).new("#{repository} #{path}", :is_main)
|
542
|
+
|
543
|
+
main_project.send(sym)
|
544
|
+
main_project
|
545
|
+
end
|
546
|
+
end
|
547
|
+
|
548
|
+
def infer_scm(path)
|
549
|
+
self.class.registered_scms.each do |scm|
|
550
|
+
return scm if self.class.project_class(scm).scm_path?(path)
|
551
|
+
end
|
552
|
+
nil
|
553
|
+
end
|
554
|
+
end
|
555
|
+
end
|
556
|
+
|
557
|
+
|