rleber-textmate 0.9.7.1
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/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
|
+
|