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 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
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Invoke TextMate Bundle Manager
4
+
5
+ require File.dirname(__FILE__) + '/../lib/textmate.rb'
6
+
7
+ TextMateBundleManager.start
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,13 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # TextMateBundleManager Bundle class
4
+
5
+ class TextMateBundleManager
6
+ class Bundle
7
+ attr_accessor :name, :repository
8
+ def initialize(options={})
9
+ @name=options[:name]
10
+ @repository = options[:repository]
11
+ end
12
+ end
13
+ end
@@ -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
+