ext 1.0.2 → 1.0.3

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG CHANGED
@@ -1,3 +1,8 @@
1
+ September 28, 2011 Version 1.0.3 released
2
+ - "up" now changes branches of subprojects
3
+ - Subversion branch support added
4
+ - "switch" command added.
5
+
1
6
  September 8, 2011 Version 1.0.2 released
2
7
  - For git, fixed bug preventing "ext up" from changing the branches of subprojects as expected.
3
8
  - For git, fixed bug preventing "ext checkout -b <branch_name>" from functioning as expected.
data/README CHANGED
@@ -21,26 +21,35 @@ forms apply the action to all sub projects.
21
21
 
22
22
  The commands and usage are as follows (from 'ext help'):
23
23
  There's a tutorial available at http://nopugs.com/ext-tutorial
24
- There's a tutorial available at http://nopugs.com/ext-tutorial
25
24
 
26
25
  ext [OPTIONS] <command> [repository] [-b <branch>] [path]
27
- -g, --git same as '--scm git' Uses git to checkout/export the main project
28
- -s, --svn, --subversion same as '--scm svn' Uses subversion to checkout/export the main project
29
- -t, --type TYPE The type of project the main project is. For example, 'rails'.
30
- --scm SCM The SCM used to manage the main project. For example, '--scm svn'.
31
- -b, --branch BRANCH The branch you want the subproject to checkout when doing 'ext install'
32
- -f, --force_removal When doing an uninstall of a subproject, remove it's files and subfolders, too.
33
- -w, --workdir DIR The working directory to execute commands from. Use this if for some reason you
34
- cannot execute ext from the main project's directory (or if it's just inconvenient,
35
- such as in a script or in a Capistrano task)
36
- --help does the same as 'ext help' If you use this with a command
37
- it will ignore the command and run help instead.
38
- --version Displays the version number of externals and then exits.
39
- Same as 'ext version'
26
+ -g, --git same as '--scm git' Uses git to checkout/export the
27
+ main project
28
+ --svn, --subversion same as '--scm svn' Uses subversion to
29
+ checkout/export the main project
30
+ -t, --type TYPE The type of project the main project is. For example,
31
+ 'rails'.
32
+ -s, --scm SCM The SCM used to manage the main project. For example,
33
+ '--scm svn'.
34
+ -b, --branch BRANCH The branch you want the subproject to checkout when
35
+ doing 'ext install'
36
+ -f, --force_removal When doing an uninstall of a subproject, remove it's
37
+ files and subfolders, too.
38
+ -w, --workdir DIR The working directory to execute commands from. Use
39
+ this if for some reason you cannot execute ext from
40
+ the main project's directory (or if it's just
41
+ inconvenient, such as in a script or in a Capistrano
42
+ task)
43
+ --help does the same as 'ext help' If you use this with a
44
+ command it will ignore the command and run help
45
+ instead.
46
+ --version Displays the version number of externals and then
47
+ exits. Same as 'ext version'
48
+
40
49
 
41
50
 
42
51
  Commands that apply to the main project or the .externals file:
43
- freeze, help, init, install, touch_emptydirs, uninstall, update_ignore, upgrade_externals_file
52
+ freeze, help, init, install, switch, touch_emptydirs, uninstall, update_ignore, version
44
53
 
45
54
  freeze Usage: ext freeze project [REVISION]
46
55
 
@@ -65,6 +74,11 @@ install Usage: ext install <repository> [-b <branch>] [path]
65
74
  type is not obvious from the repository URL, use the --scm,
66
75
  --git, or --svn flags.
67
76
 
77
+ switch Usage: ext switch <branch_name>
78
+ Changes to the named branch <branch_name> and updates any
79
+ subprojects and applies any changes that have been made to the
80
+ .externals file.
81
+
68
82
  touch_emptydirs Recurses through all directories from the
69
83
  top and adds a .emptydir file to any empty directories it
70
84
  comes across. Useful for dealing with SCMs that refuse to
@@ -166,7 +180,33 @@ This will pass "--non-interactive --trust-certid" to all co calls dealing with
166
180
  subversion projects, regardless of which project it is.
167
181
 
168
182
 
183
+ BRANCHES WITH SUBVERSION
184
+ ------------------------
185
+ Subversion does not have a branch feature. Instead, subversion users use the copy
186
+ feature to copy directories within the repository (branch), a switch command to switch the
187
+ URL that the working directory points at (checkout), and a merge feature to merge
188
+ directories (merge).
189
+
190
+ ext handles branches in Subversion by splitting the URL given by 'svn info' into
191
+ a repository part and a branch part.
192
+
193
+ If you find yourself doing this:
194
+
195
+ ext checkout svn+ssh://someserver/somepath/repository/branches/new_feature
196
+
197
+ Then you almost certainly mean to do this:
198
+
199
+ ext checkout svn+ssh://someserver/somepath/repository -b branches/new_feature
200
+
201
+ For ext to use branch-related features with Subversion repositories, the repository
202
+ must be known. For subprojects this is never a problem, but for the main project,
203
+ it's important that there is a repository field under [.] in the .externals file.
204
+ This can be accomplished by using -b with commands like 'ext checkout' and 'ext init'
205
+ or by manually editing the .externals file.
206
+
169
207
 
208
+ Copyright Information
209
+ ---------------------
170
210
  The externals project is copyright 2008 by Miles Georgi, nopugs.com, azimux.com
171
211
  and is released under the MIT license.
172
212
 
@@ -134,6 +134,14 @@ module Externals
134
134
  sections << Section.new("[#{title.to_s}]", "")
135
135
  end
136
136
 
137
+ def removed_project_paths other_config
138
+ all_paths - other_config.all_paths
139
+ end
140
+
141
+ def all_paths
142
+ sections.map(&:title)
143
+ end
144
+
137
145
  def self.new_empty
138
146
  new nil, true
139
147
  end
@@ -5,7 +5,7 @@ require 'externals/command'
5
5
  require 'externals/extensions/symbol'
6
6
 
7
7
  module Externals
8
- VERSION = '1.0.2'
8
+ VERSION = '1.0.3'
9
9
  PROJECT_TYPES_DIRECTORY = File.join(File.dirname(__FILE__), '..', 'externals','project_types')
10
10
 
11
11
  # Full commands operate on the main project as well as the externals
@@ -51,6 +51,10 @@ module Externals
51
51
  feature offered by the SCM of the main project. If the SCM
52
52
  type is not obvious from the repository URL, use the --scm,
53
53
  --git, or --svn flags."],
54
+ [:switch, "ext switch <branch_name>",
55
+ "Changes to the named branch <branch_name> and updates any
56
+ subprojects and applies any changes that have been made to the
57
+ .externals file."],
54
58
  [:touch_emptydirs, "Recurses through all directories from the
55
59
  top and adds a .emptydir file to any empty directories it
56
60
  comes across. Useful for dealing with SCMs that refuse to
@@ -75,20 +79,19 @@ module Externals
75
79
 
76
80
  COMMANDS = FULL_COMMANDS + SHORT_COMMANDS + MAIN_COMMANDS
77
81
 
82
+ Dir.entries(File.join(File.dirname(__FILE__), 'extensions')).each do |extension|
83
+ require "externals/extensions/#{extension}" if extension =~ /.rb$/
84
+ end
78
85
 
79
- class Ext
80
- Dir.entries(File.join(File.dirname(__FILE__), 'extensions')).each do |extension|
81
- require "externals/extensions/#{extension}" if extension =~ /.rb$/
82
- end
83
-
84
- Dir.entries(File.join(File.dirname(__FILE__), '..', 'externals','scms')).each do |project|
85
- require "externals/scms/#{project}" if project =~ /_project.rb$/
86
- end
86
+ Dir.entries(File.join(File.dirname(__FILE__), '..', 'externals','scms')).each do |project|
87
+ require "externals/scms/#{project}" if project =~ /_project.rb$/
88
+ end
87
89
 
88
- Dir.entries(PROJECT_TYPES_DIRECTORY).each do |type|
89
- require File.join(PROJECT_TYPES_DIRECTORY, type) if type =~ /\.rb$/
90
- end
90
+ Dir.entries(PROJECT_TYPES_DIRECTORY).each do |type|
91
+ require File.join(PROJECT_TYPES_DIRECTORY, type) if type =~ /\.rb$/
92
+ end
91
93
 
94
+ class Ext
92
95
  attr_accessor :path_calculator
93
96
 
94
97
  def self.project_types
@@ -114,35 +117,52 @@ module Externals
114
117
  end
115
118
 
116
119
  def self.new_opts main_options, sub_options
117
- opts = OptionParser.new
118
-
119
- opts.banner = "ext [OPTIONS] <command> [repository] [-b <branch>] [path]"
120
+ opts = OptionParser.new(
121
+ "ext [OPTIONS] <command> [repository] [-b <branch>] [path]"
122
+ )
123
+ opts.summary_indent = ' '
124
+ opts.summary_width = 24
125
+ summary_width = 53
120
126
 
121
127
  project_classes.each do |project_class|
122
- project_class.fill_in_opts(opts, main_options, sub_options)
123
- end
124
-
125
- opts.on("--type TYPE", "-t TYPE", "The type of project the main project is. For example, 'rails'.",
126
- String) {|type| sub_options[:scm] = main_options[:type] = type}
127
- opts.on("--scm SCM", "-s SCM", "The SCM used to manage the main project. For example, '--scm svn'.",
128
- String) {|scm| sub_options[:scm] = main_options[:scm] = scm}
129
- opts.on("--branch BRANCH", "-b BRANCH", "The branch you want the subproject to checkout when doing 'ext install'",
130
- String) {|branch| sub_options[:branch] = main_options[:branch] = branch}
131
- opts.on("--force_removal", "-f", "When doing an uninstall of a subproject, remove it's files and subfolders, too.",
132
- String) {|branch| sub_options[:force_removal] = true}
133
- opts.on("--workdir DIR", "-w DIR", "The working directory to execute commands from. Use this if for some reason you
128
+ project_class.fill_in_opts(opts, main_options, sub_options,
129
+ :summary_width => summary_width)
130
+ end
131
+
132
+ opts.on("--type TYPE", "-t TYPE",
133
+ String,
134
+ *"The type of project the main project is.
135
+ For example, 'rails'.".lines_by_width(summary_width)
136
+ ) {|type| sub_options[:scm] = main_options[:type] = type}
137
+ opts.on("--scm SCM", "-s SCM",
138
+ String,
139
+ *"The SCM used to manage the main project. For example, '--scm svn'.".lines_by_width(summary_width)
140
+ ) {|scm| sub_options[:scm] = main_options[:scm] = scm}
141
+ opts.on("--branch BRANCH", "-b BRANCH",
142
+ String,
143
+ *"The branch you want the
144
+ subproject to checkout when doing 'ext install'".lines_by_width(summary_width)
145
+ ) {|branch| sub_options[:branch] = main_options[:branch] = branch}
146
+ opts.on("--force_removal", "-f",
147
+ String,
148
+ *"When doing an uninstall of a subproject,
149
+ remove it's files and subfolders, too.".lines_by_width(summary_width)
150
+ ) {|branch| sub_options[:force_removal] = true}
151
+ opts.on("--workdir DIR", "-w DIR", String, *"The working directory to execute commands from. Use this if for some reason you
134
152
  cannot execute ext from the main project's directory (or if it's just inconvenient, such as in a script
135
- or in a Capistrano task)",
136
- String) {|dir|
153
+ or in a Capistrano task)".lines_by_width(summary_width)) {|dir|
137
154
  raise "No such directory: #{dir}" unless File.exists?(dir) && File.directory?(dir)
138
155
  main_options[:workdir] = dir
139
156
  }
140
- opts.on("--help", "does the same as 'ext help' If you use this with a command
141
- it will ignore the command and run help instead.") {main_options[:help] = true}
142
- opts.on("--version", "Displays the version number of externals and then exits.
143
- Same as 'ext version'") {
157
+ opts.on(
158
+ "--help", *"does the same as 'ext help' If you use this with a command
159
+ it will ignore the command and run help instead.".lines_by_width(summary_width)
160
+ ) {main_options[:help] = true}
161
+ opts.on("--version", *"Displays the version number of externals and then exits.
162
+ Same as 'ext version'".lines_by_width(summary_width)) {
144
163
  main_options[:version] = true
145
164
  }
165
+ opts
146
166
  end
147
167
 
148
168
  def self.run *arguments
@@ -539,6 +559,70 @@ by creating the .externals file manually"
539
559
  self.class.new({}).st [], {} #args, options
540
560
  end
541
561
 
562
+ def switch args, options
563
+ branch = args[0]
564
+
565
+ options ||= {}
566
+ scm = options[:scm]
567
+
568
+ if !scm
569
+ scm ||= configuration['.']
570
+ scm &&= scm['scm']
571
+ end
572
+
573
+ if !scm
574
+ possible_project_classes = self.class.project_classes.select do |project_class|
575
+ project_class.detected?
576
+ end
577
+
578
+ raise "Could not determine this projects scm" if possible_project_classes.empty?
579
+ if possible_project_classes.size > 1
580
+ raise "This project appears to be managed by multiple SCMs: #{
581
+ possible_project_classes.map(&:to_s).join(',')}
582
+ Please explicitly declare the SCM (by using --git or --svn, or,
583
+ by creating the .externals file manually"
584
+ end
585
+
586
+ scm = possible_project_classes.first.scm
587
+ end
588
+
589
+ unless scm
590
+ raise "You need to either specify the scm as the first line in .externals, or use an option to specify it
591
+ (such as --git or --svn)"
592
+ end
593
+
594
+ old_config = configuration
595
+ project = main_project
596
+ project.scm ||= scm
597
+
598
+ if project.current_branch == branch
599
+ puts "Already on branch #{branch}"
600
+ else
601
+ project.switch branch, options
602
+ project.up
603
+
604
+ reload_configuration
605
+
606
+ #update subprojects
607
+ self.class.new({}).up [], {} #args, options
608
+
609
+ removed_project_paths = old_config.removed_project_paths(
610
+ configuration
611
+ ).select{|path| File.exists?(path)}
612
+
613
+ if !removed_project_paths.empty?
614
+ puts "WARNING: The following subprojects are no longer being maintained in the
615
+ .externals file. You might want to remove them. You can copy and paste the
616
+ commands below if you actually wish to delete them."
617
+ removed_project_paths.each do |path|
618
+ if File.exists? path
619
+ puts " rm -r #{path}"
620
+ end
621
+ end
622
+ end
623
+ end
624
+ end
625
+
542
626
  def update args, options
543
627
  options ||= {}
544
628
  #repository = args[0]
@@ -635,11 +719,23 @@ Please use the --type option to tell ext which to use."
635
719
  end
636
720
 
637
721
  config = Configuration::Configuration.new_empty
638
-
639
722
  raise ".externals already exists" if File.exists?('.externals')
640
723
 
641
724
  config.add_empty_section '.'
642
725
 
726
+ # If we are using subversion, we should warn about not setting a branch
727
+ if scm == "svn"
728
+ if options[:branch]
729
+ config['.'][:repository] = SvnProject.extract_repository(
730
+ SvnProject.info_url,
731
+ options[:branch]
732
+ )
733
+ elsif args[0]
734
+ config['.'][:repository] = args[0].strip
735
+ else
736
+ end
737
+ end
738
+
643
739
  config['.'][:scm] = scm
644
740
  config['.'][:type] = type if type
645
741
 
@@ -2,8 +2,38 @@ String.class_eval do
2
2
  def cap_first
3
3
  "#{self[0].chr.upcase}#{self[1..(self.length - 1)]}"
4
4
  end
5
-
5
+
6
6
  def classify
7
7
  split('_').map(&:cap_first).join
8
8
  end
9
+
10
+ #avoid collision
11
+ raise unless instance_methods.include?("lines")
12
+ raise if instance_methods.include?("lines_by_width")
13
+ def lines_by_width(width = 32)
14
+ width ||= 32
15
+ lines = []
16
+ string = gsub(/\s+/, ' ')
17
+ while string.size > 0
18
+ if string.size <= width
19
+ lines << string
20
+ string = ""
21
+ else
22
+ index = string[0, width + 1].rindex(/\s/)
23
+ unless index
24
+ # let's find the first space we can.
25
+ index = string.index(/\s/)
26
+ end
27
+ if index
28
+ lines << string[0, index]
29
+ string = string[(index + 1)..-1]
30
+ else
31
+ lines << string
32
+ string = ""
33
+ end
34
+ end
35
+ end
36
+
37
+ lines
38
+ end
9
39
  end
@@ -56,6 +56,10 @@ module Externals
56
56
  self.class.default_branch
57
57
  end
58
58
 
59
+ def switch branch_name, options = {}
60
+ raise "subclass responsibility"
61
+ end
62
+
59
63
  def initialize hash
60
64
  raise "Abstract class" if self.class == Project
61
65
  raise "expected hash" unless hash.is_a? Hash
@@ -22,6 +22,9 @@ module Externals
22
22
 
23
23
  puts(gitclonecmd = "git #{opts} clone \"#{repository}\" #{dest}")
24
24
  puts `#{gitclonecmd}`
25
+ unless $? == 0
26
+ raise "git clone of #{repository} failed."
27
+ end
25
28
 
26
29
  change_to_branch_revision(command)
27
30
  end
@@ -86,6 +89,33 @@ module Externals
86
89
  end
87
90
  end
88
91
 
92
+ def switch branch_name, options = {}
93
+ cb = current_branch
94
+ if cb == branch_name
95
+ puts "Already on branch #{branch_name}"
96
+ else
97
+ # This allows the main project to be checked out to a directory
98
+ # that doesn't match it's name.
99
+ Dir.chdir path do
100
+ # let's see if the branch exists in the remote repository
101
+ # and if not, fetch it.
102
+ if !branch_exists("origin/#{branch_name}")
103
+ puts `git #{scm_opts} fetch`
104
+ end
105
+
106
+ # if the local branch doens't exist, add --track -b
107
+ if branch_exists(branch_name)
108
+ puts `git #{scm_opts} checkout #{branch_name}`
109
+ else
110
+ puts `git #{resolve_opts("co")} checkout --track -b #{branch_name} origin/#{branch_name}`
111
+ end
112
+ unless $? == 0
113
+ raise "Could not checkout origin/#{branch_name}"
114
+ end
115
+ end
116
+ end
117
+ end
118
+
89
119
  def ex *args
90
120
  if path != '.'
91
121
  (rmdircmd = "rmdir #{path}")
@@ -131,9 +161,12 @@ module Externals
131
161
  path =~ /^git:/ || path =~ /.git$/
132
162
  end
133
163
 
134
- def self.fill_in_opts opts, main_options, sub_options
135
- opts.on("--git", "-g", "same as '--scm git' Uses git to checkout/export the main project",
136
- Integer) {sub_options[:scm] = main_options[:scm] = 'git'}
164
+ def self.fill_in_opts opts, main_options, sub_options, options
165
+ opts.on("--git", "-g",
166
+ Integer,
167
+ *"same as '--scm git' Uses git to
168
+ checkout/export the main project".lines_by_width(options[:summary_width])
169
+ ) {sub_options[:scm] = main_options[:scm] = 'git'}
137
170
  end
138
171
 
139
172
  def self.detected?
@@ -150,7 +183,7 @@ module Externals
150
183
  text.split(/\n/).detect {|r| r.strip == path.strip}
151
184
  end
152
185
 
153
- def ignore_text(path)
186
+ def ignore_text(path = nil)
154
187
  return '' unless File.exists? '.gitignore'
155
188
  retval = ''
156
189
  open('.gitignore') do |f|