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 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
+