rleber-textmate 0.9.7.1
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README.markdown +47 -0
- data/bin/textmate +7 -0
- data/lib/textmate.rb +14 -0
- data/lib/textmate/bundle.rb +13 -0
- data/lib/textmate/commands.rb +280 -0
- data/lib/textmate/main.rb +511 -0
- data/lib/textmate/repository.rb +29 -0
- data/spec/list_spec.rb +526 -0
- data/spec/locations_spec.rb +91 -0
- data/spec/spec_helper.rb +394 -0
- metadata +94 -0
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2008 Yehuda Katz
|
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.markdown
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
# textmate
|
2
|
+
|
3
|
+
A binary that provides package management for TextMate.
|
4
|
+
|
5
|
+
# Usage
|
6
|
+
|
7
|
+
`textmate [COMMAND] [*PARAMS]`
|
8
|
+
|
9
|
+
Textmate bundles are automatically reloaded after install or uninstall operations.
|
10
|
+
|
11
|
+
## List available remote bundles
|
12
|
+
|
13
|
+
`textmate remote [SEARCH]`
|
14
|
+
|
15
|
+
List all of the available bundles in the remote repository, optionally filtering by `search`.
|
16
|
+
|
17
|
+
## List installed bundles
|
18
|
+
|
19
|
+
`textmate list [SEARCH]`
|
20
|
+
|
21
|
+
List all of the bundles that are installed on the local system, optionally filtering by `search`.
|
22
|
+
|
23
|
+
## Installing new bundles
|
24
|
+
|
25
|
+
`textmate install NAME [SOURCE]`
|
26
|
+
|
27
|
+
Installs a bundle from the remote repository. SOURCE filters known remote bundle locations.
|
28
|
+
For example, if you want to install the "Ruby on Rails" bundle off GitHub, you'd type the following:
|
29
|
+
|
30
|
+
`textmate install "Ruby on Rails" GitHub`
|
31
|
+
|
32
|
+
Available remote bundle locations are:
|
33
|
+
* Macromates Trunk
|
34
|
+
* Macromates Review
|
35
|
+
* GitHub
|
36
|
+
|
37
|
+
## Updateing bundles
|
38
|
+
|
39
|
+
`textmate update`
|
40
|
+
|
41
|
+
Tries to update all installed bundles.
|
42
|
+
|
43
|
+
## Uninstalling bundles
|
44
|
+
|
45
|
+
`textmate uninstall NAME`
|
46
|
+
|
47
|
+
Uninstalls a bundle from the local repository.
|
data/bin/textmate
ADDED
data/lib/textmate.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "fileutils"
|
4
|
+
require "rubygems"
|
5
|
+
require "thor"
|
6
|
+
require "open-uri"
|
7
|
+
require "yaml"
|
8
|
+
require "net/http"
|
9
|
+
require "cgi"
|
10
|
+
require "pp"
|
11
|
+
|
12
|
+
dir = File.dirname(__FILE__) + "/textmate/"
|
13
|
+
require dir + "main.rb"
|
14
|
+
require dir + 'commands.rb'
|
@@ -0,0 +1,280 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# TextMateBundleManager commands
|
4
|
+
|
5
|
+
# TODO "Move" task
|
6
|
+
# TODO "Copy" task?
|
7
|
+
# TODO "Rename" task?
|
8
|
+
# TODO "Shell" or "Find" task
|
9
|
+
# TODO "Finder" task -- open relevant directory in the Finder
|
10
|
+
# TODO "Where" or "Which" task
|
11
|
+
# TODO "Push" task?
|
12
|
+
# TODO "Pull" task? Is this different from update?
|
13
|
+
# TODO "Edit" task
|
14
|
+
# TODO "Hide" task
|
15
|
+
# TODO "Show" task (i.e. reverse of Hide)
|
16
|
+
# TODO "Duplicates" task
|
17
|
+
# TODO "Find" task -- e.g. find all bundles linked to svn, find all dirty, etc. (or options to list command?)
|
18
|
+
|
19
|
+
class TextMateBundleManager < Thor
|
20
|
+
|
21
|
+
# CHANGED: renamed list to remote. Could there be a better name?
|
22
|
+
desc "search [SEARCH]", "Lists all the matching remote bundles"
|
23
|
+
def search(search_term = "")
|
24
|
+
search_term = Regexp.new(".*#{search_term}.*", "i")
|
25
|
+
|
26
|
+
bundles_by_source = {}
|
27
|
+
each_remote_bundle(search_term) do |location|
|
28
|
+
name = location[:name]
|
29
|
+
bundles_by_source[name] ||= {:name=>name, :path=>location[:path], :bundles=>[]}
|
30
|
+
bundles_by_source[name][:bundles] << location
|
31
|
+
end
|
32
|
+
|
33
|
+
each_remote_source do |source|
|
34
|
+
name = source[:name]
|
35
|
+
next unless v=bundles_by_source[name]
|
36
|
+
banner = name.to_s << " Remote Bundles [#{v[:path]}]"
|
37
|
+
puts "\n" << banner << "\n" << banner.to_s.gsub(/./,'-')
|
38
|
+
v[:bundles].each do |b|
|
39
|
+
s = b[:file].sub(/[-\.]?tmbundle\/?$/i,'')
|
40
|
+
s << " (by #{b[:username]})" if b[:username]
|
41
|
+
puts s
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
desc "list [SEARCH]", "lists all the bundles installed locally"
|
47
|
+
method_options :status=>false, :flat=>false, :exact=>false
|
48
|
+
def list(search_term = "")
|
49
|
+
search_term = options[:exact] ? Regexp.new("^#{Regexp.escape(search_term)}$") : Regexp.new(search_term, "i")
|
50
|
+
rows = []
|
51
|
+
rows << (options[:status] ? %w{Location Bundle SCM Status Source} : %w{Location Bundle}) if options[:flat]
|
52
|
+
local_locations.each do |bundles_definition|
|
53
|
+
name = bundles_definition[:name]
|
54
|
+
bundles_path = bundles_definition[:path]
|
55
|
+
installed_bundles = get_local_bundles(bundles_path).map {|x| x.split("/").last.split(".").first}.
|
56
|
+
select {|x| x =~ search_term}
|
57
|
+
if installed_bundles.size > 0
|
58
|
+
unless options[:flat]
|
59
|
+
banner = name.to_s << " Bundles [#{bundles_path}]"
|
60
|
+
puts "\n" << banner << "\n" << banner.to_s.gsub(/./,'-')
|
61
|
+
end
|
62
|
+
if options[:status]
|
63
|
+
table = []
|
64
|
+
table << %w{Name SCM Status Source} unless options[:flat]
|
65
|
+
table += installed_bundles.map do |bundle|
|
66
|
+
path = File.join(bundles_path, bundle + '.tmbundle')
|
67
|
+
status = bundle_status(path)
|
68
|
+
row = [bundle, status[:scm] ? status[:scm].to_s : '-', status[:status] ? status[:status].to_s : '-',
|
69
|
+
status[:source] ? status[:source].to_s : '-' ]
|
70
|
+
row.unshift name.to_s if options[:flat]
|
71
|
+
row
|
72
|
+
end
|
73
|
+
if options[:flat]
|
74
|
+
rows += table
|
75
|
+
else
|
76
|
+
print_table table
|
77
|
+
end
|
78
|
+
elsif options[:flat]
|
79
|
+
rows += installed_bundles.map {|b| [name.to_s, b]}
|
80
|
+
else
|
81
|
+
puts installed_bundles.join("\n")
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
print_table rows, :separator=>" | " if options[:flat] && rows.size > 1
|
86
|
+
end
|
87
|
+
|
88
|
+
# TODO Allow install to system folders
|
89
|
+
# TODO Install to User/System, with copy to Pristine
|
90
|
+
desc "install NAME", "Install a bundle. Source must be one of trunk, review, github, or personal. \n" \
|
91
|
+
"If multiple gems with the same name exist, you will be prompted to \n" \
|
92
|
+
"choose from the available list.\n"
|
93
|
+
method_options :source=>:string
|
94
|
+
def install(bundle_name)
|
95
|
+
FileUtils.mkdir_p install_bundles_path
|
96
|
+
puts "Checking out #{bundle_name}..."
|
97
|
+
|
98
|
+
# CHANGED: It's faster to just try and fail for each repo than to search them all first
|
99
|
+
installed=false
|
100
|
+
remote_locations.each do |location|
|
101
|
+
next unless remote_location(options["source"]) == location if options.key?("source")
|
102
|
+
name = location[:name]
|
103
|
+
cmd = case location[:scm]
|
104
|
+
when :git
|
105
|
+
'echo "git remotes not implemented yet"'
|
106
|
+
when :svn
|
107
|
+
%[svn co "#{location[:path]}/#{url_escape bundle_name}.tmbundle" #{e_sh install_bundles_path}/#{e_sh bundle_name}.tmbundle 2>&1]
|
108
|
+
when :github
|
109
|
+
repos = find_github_bundles(location, denormalize_github_repo_name(bundle_name))
|
110
|
+
|
111
|
+
# Handle possible multiple Repos with the same name
|
112
|
+
case repos.size
|
113
|
+
when 0
|
114
|
+
'echo "Sorry, no such bundle found"'
|
115
|
+
when 1
|
116
|
+
git_clone(repos.first[:username], repos.first[:name])
|
117
|
+
else
|
118
|
+
puts "Multiple bundles with that name found. Please choose which one you want to install:"
|
119
|
+
repos.each_with_index {|repo, idx|
|
120
|
+
puts "%d: %s by %s" %
|
121
|
+
[
|
122
|
+
idx + 1,
|
123
|
+
normalize_github_repo_name(repo[:name]),
|
124
|
+
repo[:username]
|
125
|
+
]
|
126
|
+
}
|
127
|
+
print "Your choice: "
|
128
|
+
|
129
|
+
# Since to_i defaults to 0, we have to use Integer
|
130
|
+
choice = Integer(STDIN.gets.chomp) rescue nil
|
131
|
+
until choice && (0...repos.size).include?( choice - 1 ) do
|
132
|
+
print "Sorry, invalid choice. Please enter a valid number or Ctrl+C to stop: "
|
133
|
+
choice = Integer(STDIN.gets.chomp) rescue nil
|
134
|
+
end
|
135
|
+
|
136
|
+
git_clone(repos[choice - 1][:username], repos[choice - 1][:name])
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
res = %x{#{cmd}}
|
141
|
+
|
142
|
+
puts cmd, res.gsub(/^/,' ')
|
143
|
+
|
144
|
+
installed=true and break if res =~ /Checked out revision|Initialized empty Git repository/
|
145
|
+
end
|
146
|
+
abort 'Not Installed' unless installed
|
147
|
+
|
148
|
+
reload :verbose => true
|
149
|
+
end
|
150
|
+
|
151
|
+
# TODO select which folders to uninstall from
|
152
|
+
desc "uninstall NAME", "uninstall a bundle"
|
153
|
+
def uninstall(bundle_name)
|
154
|
+
removed = false
|
155
|
+
|
156
|
+
puts "Removing bundle..."
|
157
|
+
# When moving to the trash, maybe move the bundle into a trash/disabled_bundles subfolder
|
158
|
+
# named as the bundles_path key. Just in case there are multiple versions of
|
159
|
+
# the same bundle in multiple bundle paths
|
160
|
+
each_local_bundle do |bundle_path|
|
161
|
+
next unless File.basename(bundle_path, '.*') == bundle_name
|
162
|
+
removed = true
|
163
|
+
%x[osascript -e 'tell application "Finder" to move the POSIX file "#{bundle_path}" to trash']
|
164
|
+
break
|
165
|
+
end
|
166
|
+
|
167
|
+
unless removed
|
168
|
+
say "There is no bundle #{bundle_name} in the system", :red
|
169
|
+
exit
|
170
|
+
else
|
171
|
+
reload :verbose => true
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
desc "update", "updates all installed bundles"
|
176
|
+
def update
|
177
|
+
each_local_bundle do |path|
|
178
|
+
svn_path = File.join(path, ".svn")
|
179
|
+
github_path = File.join(path, ".git")
|
180
|
+
if File.exist?(github_path)
|
181
|
+
puts "Updating #{path}"
|
182
|
+
%x[git --git-dir=#{e_sh github_path} --work-tree=#{e_sh path}pull]
|
183
|
+
elsif File.exist?(svn_path)
|
184
|
+
puts "Updating #{path}"
|
185
|
+
%x[svn up #{e_sh path}]
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
VALID_SCM_TYPES = %w{git svn github}
|
191
|
+
|
192
|
+
desc "register NAME URL", "Register a remote Bundle repository"
|
193
|
+
method_options :short=>:string, :scm=>:string
|
194
|
+
def register(name, path)
|
195
|
+
short = options[:short] || name.to_s.downcase.gsub(/\s/,'_')
|
196
|
+
scm = options[:scm] || :git
|
197
|
+
abort "Unknown Bundle repository type #{options[:scm]}. Should be #{VALID_SCM_TYPES.join(', ')}" unless
|
198
|
+
VALID_SCM_TYPES.include?(scm.to_s)
|
199
|
+
add_remote_location(name, short, scm, path)
|
200
|
+
save_remote_locations
|
201
|
+
end
|
202
|
+
|
203
|
+
desc "unregister", "Unregister a remote Bundle repository"
|
204
|
+
def unregister(name)
|
205
|
+
if remove_remote_location(name)
|
206
|
+
save_remote_locations
|
207
|
+
else
|
208
|
+
abort "Location #{name} not found"
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
desc "reload", "Reloads TextMate Bundles"
|
213
|
+
method_options :verbose => :boolean
|
214
|
+
def reload(opts = {})
|
215
|
+
puts "Reloading bundles..." if opts[:verbose]
|
216
|
+
reload_bundles
|
217
|
+
puts "Done." if opts[:verbose]
|
218
|
+
end
|
219
|
+
|
220
|
+
desc "locations", "List TextMate Bundle locations"
|
221
|
+
def locations
|
222
|
+
remote_repos = remote_locations.map {|l| ['Remote', l[:short].to_s, l[:name].to_s, l[:path]]}
|
223
|
+
local_repos = local_locations.map {|l| ['Local', l[:short].to_s, l[:name].to_s, l[:path]]}
|
224
|
+
print_table([%w{Zone Short Name Location}] + local_repos + remote_repos)
|
225
|
+
end
|
226
|
+
|
227
|
+
desc "get", "Get repository locations for local bundles"
|
228
|
+
method_options :fork=>true, :system=>false, :exact=>false, :force=>false
|
229
|
+
def get(bundle_name=nil, repo_path=nil)
|
230
|
+
replacements = 0
|
231
|
+
if options[:exact]
|
232
|
+
bundle_name = bundle_name.to_s
|
233
|
+
bundle_name += '.tmbundle' unless bundle_name =~ /\.tmbundle$/
|
234
|
+
search_pattern =/^#{Regexp.escape(bundle_name.to_s)}$/
|
235
|
+
else
|
236
|
+
search_pattern = /#{Regexp.escape(bundle_name.to_s)}[^\/]*\.tmbundle$/i
|
237
|
+
end
|
238
|
+
fixed_source = if repo_path
|
239
|
+
{
|
240
|
+
:scm => repo_path =~ /github/ ? :github : (repo_path =~ /git/ ? :git : :svn),
|
241
|
+
:repo => repo_path,
|
242
|
+
:file => File.basename(repo_path),
|
243
|
+
:short => :manual,
|
244
|
+
:name => :'Manually Provided',
|
245
|
+
}
|
246
|
+
else
|
247
|
+
nil
|
248
|
+
end
|
249
|
+
|
250
|
+
each_local_bundle do |path|
|
251
|
+
next if !bundle_name.nil? && File.basename(path) !~ search_pattern # Find matching bundles; nil == all
|
252
|
+
install_bundle_name = File.basename(path)
|
253
|
+
found_bundle_name = File.basename(path, '.*')
|
254
|
+
install_location = options[:system] ? 'System' : 'User'
|
255
|
+
source = nil
|
256
|
+
[install_location.to_sym, "#{install_location} Pristine".to_sym].each do |loc|
|
257
|
+
location = local_location(loc)
|
258
|
+
install_path = File.join(location[:path],install_bundle_name)
|
259
|
+
next if !options[:force] && bundle_name.nil? && bundle_source(install_path) # Skip bundles which are already linked to a source
|
260
|
+
source ||= fixed_source || get_remote_source(found_bundle_name)
|
261
|
+
if source.nil?
|
262
|
+
puts "No matching bundle found for #{found_bundle_name.inspect}"
|
263
|
+
break
|
264
|
+
end
|
265
|
+
if options[:fork] && source[:scm] == :github && github_owner(source) != current_github_user
|
266
|
+
puts "Forking #{source[:repo]} to #{github_path(current_github_user, source[:file])}"
|
267
|
+
github_fork source
|
268
|
+
source[:username] = current_github_user
|
269
|
+
source[:repo] = github_path(current_github_user, source[:file])
|
270
|
+
end
|
271
|
+
puts "Installing #{source[:repo]} to #{install_bundle_name} in #{location[:name]}"
|
272
|
+
install_bundle(source, install_path)
|
273
|
+
replacements += 1
|
274
|
+
end
|
275
|
+
break unless bundle_name.nil? # Save needless repetition if a single matching bundle was found
|
276
|
+
end
|
277
|
+
ensure
|
278
|
+
reload_bundles if replacements > 0
|
279
|
+
end
|
280
|
+
end
|
@@ -0,0 +1,511 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# TextMateBundleManager Main file
|
4
|
+
|
5
|
+
# TODO Refactor repositories, bundles, bundle lists as first-class objects
|
6
|
+
# TODO Refactor search, list and install using iterators
|
7
|
+
# TODO Treat "name:" as a location, not a bundle
|
8
|
+
# TODO Add tests
|
9
|
+
|
10
|
+
class TextMateBundleManager < Thor
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def current_github_user
|
15
|
+
user = `git config --global github.user`.chomp
|
16
|
+
return nil unless user.size > 0
|
17
|
+
user
|
18
|
+
end
|
19
|
+
|
20
|
+
def current_github_token
|
21
|
+
token = `git config --global github.token`.chomp
|
22
|
+
return nil unless token.size > 0
|
23
|
+
token
|
24
|
+
end
|
25
|
+
|
26
|
+
# In order of precedence, from highest to lowest
|
27
|
+
DEFAULT_REMOTE_LOCATIONS = [
|
28
|
+
{:name => :'GitHub', :short => :github, :scm => :github, :path => 'http://github.com/api/v2/yaml/repos/search/tmbundle'},
|
29
|
+
{:name => :'Macromates Trunk', :short => :trunk, :scm => :svn, :path => 'http://svn.textmate.org/trunk/Bundles'},
|
30
|
+
{:name => :'Macromates Review', :short => :review, :scm => :svn, :path => 'http://svn.textmate.org/trunk/Review/Bundles'},
|
31
|
+
]
|
32
|
+
|
33
|
+
def default_remote_locations
|
34
|
+
res = DEFAULT_REMOTE_LOCATIONS
|
35
|
+
if current_github_user
|
36
|
+
res = res.dup.unshift(
|
37
|
+
{:name => :'Personal', :short => :personal, :scm => :github, :path => "http://github.com/api/v2/yaml/repos/show/#{current_github_user}", :single_page=>true}
|
38
|
+
)
|
39
|
+
end
|
40
|
+
res
|
41
|
+
end
|
42
|
+
|
43
|
+
def remote_locations
|
44
|
+
@remote_locations ||= load_remote_locations
|
45
|
+
@remote_locations ||= default_remote_locations
|
46
|
+
end
|
47
|
+
|
48
|
+
def add_remote_location(position, name, short_name, scm, path)
|
49
|
+
name=name.to_sym
|
50
|
+
remote_locations.insert(position, {:name=>name, :short=>short_name.to_sym, :scm=>scm.to_sym, :path=>path})
|
51
|
+
name
|
52
|
+
end
|
53
|
+
|
54
|
+
def remove_remote_location(name)
|
55
|
+
index = find_remote_location_index(name)
|
56
|
+
remote_locations.delete_at(index) if index
|
57
|
+
name
|
58
|
+
end
|
59
|
+
|
60
|
+
def get_remote_source(bundle_name)
|
61
|
+
choices = get_remote_sources(bundle_name)
|
62
|
+
case choices.size
|
63
|
+
when 0
|
64
|
+
return nil
|
65
|
+
when 1
|
66
|
+
choices.first
|
67
|
+
else
|
68
|
+
puts "Please choose one bundle for #{bundle_name.inspect}:"
|
69
|
+
choices.each_with_index do |repo, idx|
|
70
|
+
puts " #{idx + 1}) #{repo[:short]} #{repo[:file]} #{repo[:username] ? 'by ' + repo[:username] : ''} "
|
71
|
+
end
|
72
|
+
input_choices = "1..#{choices.size}, q"
|
73
|
+
prompt = " Your choice (#{input_choices})? "
|
74
|
+
choice = nil
|
75
|
+
loop do
|
76
|
+
print prompt
|
77
|
+
case choice = $stdin.gets.chomp
|
78
|
+
when 'q'
|
79
|
+
abort "Quit"
|
80
|
+
when /\d+/
|
81
|
+
choice = choice.to_i
|
82
|
+
break if (1..choices.size).include?(choice)
|
83
|
+
end
|
84
|
+
prompt = " Sorry, invalid choice. Please enter #{input_choices}: "
|
85
|
+
end
|
86
|
+
choices[choice-1]
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def get_remote_sources(bundle_name)
|
91
|
+
res = []
|
92
|
+
each_remote_bundle(bundle_name) do |location|
|
93
|
+
res << location
|
94
|
+
end
|
95
|
+
res
|
96
|
+
end
|
97
|
+
|
98
|
+
def each_remote_source(source=nil)
|
99
|
+
remote_locations.each do |location|
|
100
|
+
next unless remote_location(source) == location if source
|
101
|
+
yield location
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def each_remote_bundle(bundle=nil, source=nil, &blk)
|
106
|
+
each_remote_source(source) do |location|
|
107
|
+
case location[:scm]
|
108
|
+
when :git
|
109
|
+
abort "Git remote repositories not implemented"
|
110
|
+
when :svn
|
111
|
+
each_svn_bundle(location, bundle, &blk)
|
112
|
+
when :github
|
113
|
+
each_github_bundle(location, bundle, &blk)
|
114
|
+
else
|
115
|
+
abort "Unknown remote repository type #{location[:scm]}"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def remote_bundles(bundle=nil, source=nil)
|
121
|
+
bundles = []
|
122
|
+
each_remote_bundle(bundle, source) {|b| bundles << b}
|
123
|
+
bundles
|
124
|
+
end
|
125
|
+
|
126
|
+
def each_svn_bundle(location=nil, bundle=nil, &blk)
|
127
|
+
search_term = /#{bundle.to_s}.*tmbundle/i
|
128
|
+
%x[svn list #{e_sh location[:path]}].split("\n").select {|x| x =~ search_term}.each do |file|
|
129
|
+
yield(location.merge(:file=>file, :repo=>"#{location[:path]}/#{file}"))
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def each_github_bundle(location=nil, bundle=nil, &blk)
|
134
|
+
search_term = /#{bundle.to_s}.*tmbundle/i
|
135
|
+
gh_bundles = find_github_bundles(location, search_term)
|
136
|
+
gh_bundles.each do |specification|
|
137
|
+
yield(location.merge(
|
138
|
+
:file=>specification[:name],
|
139
|
+
:username=>specification[:username],
|
140
|
+
:repo=>github_path(specification[:username], specification[:name])))
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
# TODO Maybe we should never install into System or other bundles? Install to System or User, with copy to Pristine?
|
145
|
+
def install_bundle(bundle, to)
|
146
|
+
FileUtils.mkdir_p File.dirname(to)
|
147
|
+
if File.exists?(to)
|
148
|
+
saved_bundle = to.sub(/\.tmbundle/,'.orig.tmbundle')
|
149
|
+
FileUtils.mv(to, saved_bundle) # Save the bundle, in case we fail
|
150
|
+
end
|
151
|
+
success = false
|
152
|
+
begin
|
153
|
+
case bundle[:scm]
|
154
|
+
when :git
|
155
|
+
abort "Installation of git bundles not implemented yet"
|
156
|
+
when :svn
|
157
|
+
command = %Q[svn co "#{bundle[:repo]}" #{e_sh to} 2>&1]
|
158
|
+
res = %x[#{command}]
|
159
|
+
success = res =~ /Checked out revision/
|
160
|
+
when :github
|
161
|
+
command = %Q[git clone "#{bundle[:repo]}" #{e_sh to} 2>&1]
|
162
|
+
res = %x[#{command}]
|
163
|
+
success = res =~ /Cloning into/
|
164
|
+
end
|
165
|
+
abort "Unable to install to #{to} from #{bundle[:repo]}:\n#{command} => #{res}" unless success
|
166
|
+
ensure
|
167
|
+
if success
|
168
|
+
FileUtils.rm_rf(saved_bundle) if saved_bundle && File.exists?(saved_bundle) # Clean up saved bundle, if any
|
169
|
+
else
|
170
|
+
FileUtils.mv(saved_bundle, to) if saved_bundle && File.exists?(saved_bundle) # Restore the bundle on failure
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
CONFIG_FILE = File.join(ENV['HOME'], '.textmate.yml')
|
176
|
+
def config
|
177
|
+
@config ||= load_config
|
178
|
+
end
|
179
|
+
|
180
|
+
def load_config
|
181
|
+
return {} unless File.exists?(CONFIG_FILE)
|
182
|
+
YAML.load(File.read(CONFIG_FILE)) rescue {}
|
183
|
+
end
|
184
|
+
|
185
|
+
def save_config
|
186
|
+
File.open(CONFIG_FILE, 'w') {|f| f.write(YAML.dump(@config))}
|
187
|
+
end
|
188
|
+
|
189
|
+
def load_remote_locations
|
190
|
+
config[:remote_locations]
|
191
|
+
end
|
192
|
+
|
193
|
+
def save_remote_locations
|
194
|
+
config[:remote_locations] = @remote_locations
|
195
|
+
save_config
|
196
|
+
end
|
197
|
+
|
198
|
+
def remote_location(name)
|
199
|
+
find_remote_location_by(:name, name) || find_remote_location_by(:short, name)
|
200
|
+
end
|
201
|
+
|
202
|
+
def find_remote_location_by(field, name)
|
203
|
+
remote_locations.find {|l| l[field] == name }
|
204
|
+
end
|
205
|
+
|
206
|
+
def find_remote_location_index(name)
|
207
|
+
find_remote_location_index_by(:name, name) || find_remote_location_index_by(:short, name)
|
208
|
+
end
|
209
|
+
|
210
|
+
def find_remote_location_index_by(field, name)
|
211
|
+
remote_locations.each_with_index do |l, i|
|
212
|
+
return i if l[field] == name
|
213
|
+
end
|
214
|
+
return nil
|
215
|
+
end
|
216
|
+
|
217
|
+
def long_remote_location_name(name)
|
218
|
+
l = remote_location(name)
|
219
|
+
return nil unless l
|
220
|
+
l[:name]
|
221
|
+
end
|
222
|
+
|
223
|
+
def local_locations
|
224
|
+
@local_locations ||= [
|
225
|
+
# From highest precedence to lowest
|
226
|
+
{:name => :User, :short=>:user, :path=>"#{ENV["HOME"]}/Library/Application Support/TextMate/Bundles"},
|
227
|
+
{:name => :'User Pristine', :short=>:user_p, :path=>"#{ENV["HOME"]}/Library/Application Support/TextMate/Pristine Copy/Bundles"},
|
228
|
+
{:name => :System, :short=>:system, :path=>'/Library/Application Support/TextMate/Bundles'},
|
229
|
+
{:name => :'System Pristine', :short=>:system_p, :path=>'/Library/Application Support/TextMate/Pristine Copy/Bundles'},
|
230
|
+
{:name => :Application, :short=>:app, :path=>"#{textmate_app}/Contents/SharedSupport/Bundles"},
|
231
|
+
]
|
232
|
+
end
|
233
|
+
|
234
|
+
def local_location(name)
|
235
|
+
l = find_local_location_by(:name, name)
|
236
|
+
return l if l
|
237
|
+
find_local_location_by(:short, name)
|
238
|
+
end
|
239
|
+
|
240
|
+
def find_local_location_by(field, name)
|
241
|
+
local_locations.find {|l| l[field] == name }
|
242
|
+
end
|
243
|
+
|
244
|
+
def long_local_location_name(name)
|
245
|
+
l = local_location(name)
|
246
|
+
return nil unless l
|
247
|
+
l[:name]
|
248
|
+
end
|
249
|
+
|
250
|
+
def each_local_bundle(&blk)
|
251
|
+
local_locations.each do |name, bundles_definition|
|
252
|
+
bundles_path = bundles_definition[:path]
|
253
|
+
get_local_bundles(bundles_path).each do |file|
|
254
|
+
yield(File.join(bundles_path, file))
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
def get_local_bundles(path)
|
260
|
+
Dir["#{e_sh path}/*.tmbundle"]
|
261
|
+
end
|
262
|
+
|
263
|
+
def textmate_app
|
264
|
+
@textmate_app ||= `mdfind -name "TextMate" | grep "/TextMate.app\$"`.chomp
|
265
|
+
end
|
266
|
+
|
267
|
+
def reload_bundles
|
268
|
+
%x[osascript -e 'tell app "TextMate" to reload bundles']
|
269
|
+
end
|
270
|
+
|
271
|
+
def install_bundles_path
|
272
|
+
local_location(:'User Pristine')[:path]
|
273
|
+
end
|
274
|
+
|
275
|
+
# Retrieve the status of a Bundle.
|
276
|
+
#
|
277
|
+
# ==== Parameters
|
278
|
+
# String
|
279
|
+
# String
|
280
|
+
#
|
281
|
+
# ==== Returns
|
282
|
+
# {:name=>bundle_name, :scm=>scm_type, :source=>source_repo}
|
283
|
+
def bundle_status(bundle_path)
|
284
|
+
scm = bundle_scm(bundle_path)
|
285
|
+
case scm
|
286
|
+
when :git
|
287
|
+
branch = `cd #{e_sh bundle_path}; git branch 2>&1`.split("\n").select {|line| line =~ /^\s*\*/}[0]
|
288
|
+
branch = branch[/^\s*\*\s*(.*?)\s*$/,1] if branch
|
289
|
+
origin = `cd #{e_sh bundle_path}; git config branch.#{branch}.remote 2>&1`.chomp
|
290
|
+
if origin
|
291
|
+
source = `cd #{e_sh bundle_path}; git remote -v 2>&1`.split("\n").select {|line| line =~ /\b#{Regexp.escape(origin)}\b.*\bfetch\b/ }[0]
|
292
|
+
source = source[/#{Regexp.escape(origin)}\s+(.*?)\s*\(fetch/,1] if source
|
293
|
+
status = nil
|
294
|
+
`cd #{e_sh bundle_path}; git fetch #{origin} 2>&1`
|
295
|
+
status = $1 if `cd #{e_sh bundle_path}; git branch -v -v 2>&1`.chomp =~ /(\b(?:ahead|behind)\s+\d+(?:\s*,\s*behind\s+\d+)?)/
|
296
|
+
else
|
297
|
+
status = nil
|
298
|
+
end
|
299
|
+
when :svn
|
300
|
+
source = `cd #{e_sh bundle_path}; svn info 2>&1`.split("\n").select{|line| line =~ /URL/ }[0]
|
301
|
+
source = source[/URL:\s*(.*?)\s*$/,1] if source
|
302
|
+
status = `cd #{e_sh bundle_path}; svn st 2>&1`.split("\n").size>0 && "dirty"
|
303
|
+
else
|
304
|
+
source = status = nil
|
305
|
+
end
|
306
|
+
{:name=>File.basename(bundle_path, '.*'), :scm=>scm, :status=>status, :source=>source}
|
307
|
+
end
|
308
|
+
|
309
|
+
def bundle_source(path)
|
310
|
+
bundle_status(path)[:source]
|
311
|
+
end
|
312
|
+
|
313
|
+
def bundle_scm(path)
|
314
|
+
if File.exists?(File.join(path, '.git'))
|
315
|
+
:git
|
316
|
+
elsif File.exists?(File.join(path, '.svn'))
|
317
|
+
:svn
|
318
|
+
else
|
319
|
+
nil
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
323
|
+
# Prints a table.
|
324
|
+
# Basic code borrowed from Thor: https://github.com/wycats/thor
|
325
|
+
#
|
326
|
+
# ==== Parameters
|
327
|
+
# Array[Array[String, String, ...]]
|
328
|
+
#
|
329
|
+
# ==== Options
|
330
|
+
# ident<Integer>:: Indent the first column by ident value.
|
331
|
+
# colwidth<Integer>:: Force the first column to colwidth spaces wide.
|
332
|
+
#
|
333
|
+
def print_table(table, options={})
|
334
|
+
return if table.empty?
|
335
|
+
|
336
|
+
formats, ident, colwidth, separator = [], options[:ident].to_i, options[:colwidth], options[:separator]
|
337
|
+
separator ||= " "
|
338
|
+
# options[:truncate] = terminal_width if options[:truncate] == true
|
339
|
+
|
340
|
+
formats << "%-#{colwidth}s#{separator}" if colwidth
|
341
|
+
start = colwidth ? 1 : 0
|
342
|
+
|
343
|
+
start.upto(table.first.length - 2) do |i|
|
344
|
+
maxima ||= table.max{|a,b| a[i].size <=> b[i].size }[i].size
|
345
|
+
formats << "%-#{maxima}s#{separator}"
|
346
|
+
end
|
347
|
+
|
348
|
+
formats[0] = formats[0].insert(0, " " * ident)
|
349
|
+
formats << "%s"
|
350
|
+
|
351
|
+
table.each do |row|
|
352
|
+
sentence = ""
|
353
|
+
|
354
|
+
row.each_with_index do |column, i|
|
355
|
+
sentence << formats[i] % column.to_s
|
356
|
+
end
|
357
|
+
|
358
|
+
sentence = truncate(sentence, options[:truncate]) if options[:truncate]
|
359
|
+
stdout.puts sentence
|
360
|
+
end
|
361
|
+
end
|
362
|
+
|
363
|
+
# For compatibility with print_table
|
364
|
+
def stdout
|
365
|
+
$stdout
|
366
|
+
end
|
367
|
+
|
368
|
+
# Copied from http://macromates.com/svn/Bundles/trunk/Support/lib/escape.rb
|
369
|
+
# escape text to make it useable in a shell script as one “word” (string)
|
370
|
+
def e_sh(str)
|
371
|
+
str.to_s.gsub(/(?=[^a-zA-Z0-9_.\/\-\x7F-\xFF\n])/, '\\').gsub(/\n/, "'\n'").sub(/^$/, "''")
|
372
|
+
end
|
373
|
+
|
374
|
+
def url_escape(str)
|
375
|
+
chars = ((33...47).to_a + (94...96).to_a + (123...126).to_a).map {|c| c.chr }.join + "\\[\\]\\\\"
|
376
|
+
str = str.to_s.gsub(%r{[#{chars}]}) {|m| CGI.escape(m) }
|
377
|
+
end
|
378
|
+
|
379
|
+
CAPITALIZATION_EXCEPTIONS = %w[tmbundle on]
|
380
|
+
# Convert a GitHub repo name into a "normal" TM bundle name
|
381
|
+
# e.g. ruby-on-rails-tmbundle => Ruby on Rails.tmbundle
|
382
|
+
def normalize_github_repo_name(name)
|
383
|
+
name = name.gsub(/[\-\.]/, " ").split.each{|part| part.capitalize! unless CAPITALIZATION_EXCEPTIONS.include? part}.join(" ")
|
384
|
+
name[-9] = ?. if name =~ / tmbundle$/
|
385
|
+
name
|
386
|
+
end
|
387
|
+
|
388
|
+
# Does the opposite of normalize_github_repo_name
|
389
|
+
def denormalize_github_repo_name(name)
|
390
|
+
name = name.split(' ').each{|part| part.downcase!}.join(' ').gsub(' ', '-')
|
391
|
+
name += ".tmbundle" unless name =~ /.tmbundle$/
|
392
|
+
name
|
393
|
+
end
|
394
|
+
|
395
|
+
def find_github_bundles(location, search_term)
|
396
|
+
# Until GitHub fixes http://support.github.com/discussions/feature-requests/11-api-search-results,
|
397
|
+
# we need to account for multiple pages of results:
|
398
|
+
page = 1
|
399
|
+
path = location[:path]
|
400
|
+
repositories = YAML.load(open("#{path}?start_page=#{page}"))['repositories'] rescue []
|
401
|
+
results = []
|
402
|
+
until repositories.empty?
|
403
|
+
results += repositories.find_all do |result|
|
404
|
+
result[:name] = result.delete('name') if result['name']
|
405
|
+
result[:username] = result.delete('username') if result['username']
|
406
|
+
name = result[:name]
|
407
|
+
name.match(search_term) && name=~/tmbundle$/
|
408
|
+
end
|
409
|
+
break if location[:single_page]
|
410
|
+
page += 1
|
411
|
+
repositories = YAML.load(open("#{path}?start_page=#{page}"))['repositories'] rescue []
|
412
|
+
end
|
413
|
+
results.each do |result|
|
414
|
+
result[:username] ||= current_github_user
|
415
|
+
end
|
416
|
+
results.sort_by {|r| r[:name] }
|
417
|
+
end
|
418
|
+
|
419
|
+
def git_clone(username, name)
|
420
|
+
bundle_name = normalize_github_repo_name(name)
|
421
|
+
|
422
|
+
path = "#{install_bundles_path}/#{bundle_name}"
|
423
|
+
escaped_path = "#{e_sh(install_bundles_path)}/#{e_sh(bundle_name)}"
|
424
|
+
|
425
|
+
if File.directory?(path)
|
426
|
+
say "Sorry, that bundle is already installed. Please uninstall it first.", :red
|
427
|
+
exit
|
428
|
+
end
|
429
|
+
|
430
|
+
%[git clone "#{github_path(username, name)}" #{escaped_path} 2>&1]
|
431
|
+
end
|
432
|
+
|
433
|
+
GITHUB_API_REPO = 'http://github.com/api/v2/yaml/repos/show/%s/%s'
|
434
|
+
GITHUB_API_FORK = 'http://github.com/api/v2/yaml/repos/fork/%s/%s'
|
435
|
+
|
436
|
+
# ... hardcore forking action ...
|
437
|
+
# > git remote add -f YOUR_USER git@github.com:YOUR_USER/CURRENT_REPO.git
|
438
|
+
# Code borrowed (with thanks!) from Chris Wanstrath https://github.com/defunkt/hub/tree/master/lib/hub
|
439
|
+
def github_fork(from_repo)
|
440
|
+
# can't do anything without token and original owner name
|
441
|
+
repo_name = from_repo[:file]
|
442
|
+
abort "git config must include github.user and github.token. See http://help.github.com/git-email-settings/ for details" \
|
443
|
+
unless current_github_user && current_github_token
|
444
|
+
abort "#{current_github_user}/#{repo_name} already exists on GitHub" \
|
445
|
+
if github_repo_exists?(current_github_user, repo_name)
|
446
|
+
fork_github_repo(from_repo)
|
447
|
+
end
|
448
|
+
|
449
|
+
def fork_github_repo(from_repo)
|
450
|
+
repo_owner = github_owner(from_repo)
|
451
|
+
repo_name = from_repo[:file]
|
452
|
+
path = GITHUB_API_FORK % [repo_owner, repo_name]
|
453
|
+
Net::HTTP.post_form(URI(path), 'login' => current_github_user, 'token' => current_github_token)
|
454
|
+
end
|
455
|
+
|
456
|
+
def github_repo_exists?(user, repo_name)
|
457
|
+
path = GITHUB_API_REPO % [user, repo_name]
|
458
|
+
Net::HTTPSuccess === Net::HTTP.get_response(URI(path))
|
459
|
+
end
|
460
|
+
|
461
|
+
def github_owner(repo)
|
462
|
+
res = File.basename(File.dirname(repo[:repo]))
|
463
|
+
res = $1 if res =~ /^git@github.com:(.*)/
|
464
|
+
res
|
465
|
+
end
|
466
|
+
|
467
|
+
def github_path(username, name)
|
468
|
+
if username == current_github_user
|
469
|
+
"git@github.com:#{url_escape(username)}/#{url_escape(name)}.git"
|
470
|
+
else
|
471
|
+
"git://github.com/#{url_escape(username)}/#{url_escape(name)}.git"
|
472
|
+
end
|
473
|
+
end
|
474
|
+
|
475
|
+
end
|
476
|
+
|
477
|
+
# TODO: create a "monument to personal cleverness" by class-izing everything?
|
478
|
+
# class TextMateBundle
|
479
|
+
# def self.find_local(bundle_name)
|
480
|
+
#
|
481
|
+
# end
|
482
|
+
#
|
483
|
+
# def self.find_remote(bundle_name)
|
484
|
+
#
|
485
|
+
# end
|
486
|
+
# attr_reader :name
|
487
|
+
# attr_reader :location
|
488
|
+
# attr_reader :scm
|
489
|
+
# def initialize(name, location, scm)
|
490
|
+
# @name = name
|
491
|
+
# @location = location
|
492
|
+
# @scm = scm
|
493
|
+
# end
|
494
|
+
#
|
495
|
+
# def install!
|
496
|
+
#
|
497
|
+
# end
|
498
|
+
#
|
499
|
+
# def uninstall!
|
500
|
+
#
|
501
|
+
# end
|
502
|
+
#
|
503
|
+
#
|
504
|
+
# def installed?
|
505
|
+
# # List all the installed versions, and where they're at
|
506
|
+
# end
|
507
|
+
#
|
508
|
+
# # TODO: dirty? method to show if there are any deltas
|
509
|
+
# end
|
510
|
+
|
511
|
+
|