piston 1.4.0 → 2.0.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.
Files changed (85) hide show
  1. data/History.txt +24 -0
  2. data/License.txt +20 -0
  3. data/Manifest.txt +109 -0
  4. data/{README → README.txt} +14 -10
  5. data/VERSION.yml +4 -0
  6. data/bin/piston +3 -8
  7. data/lib/piston/cli.rb +121 -0
  8. data/lib/piston/commands/base.rb +44 -0
  9. data/lib/piston/commands/convert.rb +23 -71
  10. data/lib/piston/commands/diff.rb +14 -46
  11. data/lib/piston/commands/import.rb +48 -57
  12. data/lib/piston/commands/info.rb +24 -0
  13. data/lib/piston/commands/lock_unlock.rb +26 -0
  14. data/lib/piston/commands/status.rb +29 -54
  15. data/lib/piston/commands/update.rb +35 -122
  16. data/lib/piston/commands/upgrade.rb +26 -0
  17. data/lib/piston/commands.rb +4 -0
  18. data/lib/piston/git/client.rb +76 -0
  19. data/lib/piston/git/commit.rb +114 -0
  20. data/lib/piston/git/repository.rb +63 -0
  21. data/lib/piston/git/working_copy.rb +145 -0
  22. data/lib/piston/git.rb +13 -0
  23. data/lib/piston/repository.rb +61 -0
  24. data/lib/piston/revision.rb +83 -0
  25. data/lib/piston/svn/client.rb +88 -0
  26. data/lib/piston/svn/repository.rb +67 -0
  27. data/lib/piston/svn/revision.rb +112 -0
  28. data/lib/piston/svn/working_copy.rb +184 -0
  29. data/lib/piston/svn.rb +15 -0
  30. data/lib/piston/version.rb +9 -7
  31. data/lib/piston/working_copy.rb +334 -0
  32. data/lib/piston.rb +13 -64
  33. data/lib/subclass_responsibility_error.rb +2 -0
  34. data/test/integration_helpers.rb +36 -0
  35. data/test/spec_suite.rb +4 -0
  36. data/test/test_helper.rb +92 -0
  37. data/test/unit/git/commit/test_checkout.rb +31 -0
  38. data/test/unit/git/commit/test_each.rb +30 -0
  39. data/test/unit/git/commit/test_rememberance.rb +22 -0
  40. data/test/unit/git/commit/test_validation.rb +34 -0
  41. data/test/unit/git/repository/test_at.rb +23 -0
  42. data/test/unit/git/repository/test_basename.rb +12 -0
  43. data/test/unit/git/repository/test_branchanme.rb +15 -0
  44. data/test/unit/git/repository/test_guessing.rb +32 -0
  45. data/test/unit/git/working_copy/test_copying.rb +25 -0
  46. data/test/unit/git/working_copy/test_creation.rb +22 -0
  47. data/test/unit/git/working_copy/test_existence.rb +18 -0
  48. data/test/unit/git/working_copy/test_finalization.rb +15 -0
  49. data/test/unit/git/working_copy/test_guessing.rb +35 -0
  50. data/test/unit/git/working_copy/test_rememberance.rb +22 -0
  51. data/test/unit/svn/repository/test_at.rb +19 -0
  52. data/test/unit/svn/repository/test_basename.rb +24 -0
  53. data/test/unit/svn/repository/test_guessing.rb +45 -0
  54. data/test/unit/svn/revision/test_checkout.rb +28 -0
  55. data/test/unit/svn/revision/test_each.rb +22 -0
  56. data/test/unit/svn/revision/test_rememberance.rb +38 -0
  57. data/test/unit/svn/revision/test_validation.rb +50 -0
  58. data/test/unit/svn/working_copy/test_copying.rb +26 -0
  59. data/test/unit/svn/working_copy/test_creation.rb +16 -0
  60. data/test/unit/svn/working_copy/test_existence.rb +23 -0
  61. data/test/unit/svn/working_copy/test_externals.rb +56 -0
  62. data/test/unit/svn/working_copy/test_finalization.rb +17 -0
  63. data/test/unit/svn/working_copy/test_guessing.rb +18 -0
  64. data/test/unit/svn/working_copy/test_rememberance.rb +26 -0
  65. data/test/unit/test_info.rb +37 -0
  66. data/test/unit/test_lock_unlock.rb +47 -0
  67. data/test/unit/test_repository.rb +51 -0
  68. data/test/unit/test_revision.rb +31 -0
  69. data/test/unit/working_copy/test_guessing.rb +35 -0
  70. data/test/unit/working_copy/test_info.rb +14 -0
  71. data/test/unit/working_copy/test_rememberance.rb +42 -0
  72. data/test/unit/working_copy/test_validate.rb +63 -0
  73. metadata +132 -31
  74. data/CHANGELOG +0 -81
  75. data/LICENSE +0 -19
  76. data/Rakefile +0 -63
  77. data/contrib/piston +0 -43
  78. data/lib/core_ext/range.rb +0 -5
  79. data/lib/core_ext/string.rb +0 -9
  80. data/lib/piston/command.rb +0 -68
  81. data/lib/piston/command_error.rb +0 -6
  82. data/lib/piston/commands/lock.rb +0 -30
  83. data/lib/piston/commands/switch.rb +0 -139
  84. data/lib/piston/commands/unlock.rb +0 -29
  85. data/lib/transat/parser.rb +0 -189
@@ -1,74 +1,65 @@
1
- require "piston"
2
- require "piston/command"
1
+ require "piston/commands/base"
3
2
 
4
3
  module Piston
5
4
  module Commands
6
- class Import < Piston::Command
7
- def run
8
- raise Piston::CommandError, "Missing REPOS_URL argument" if args.empty?
5
+ class Import < Piston::Commands::Base
6
+ attr_reader :options
9
7
 
10
- repos, dir = args.shift, args.shift
11
- raise Piston::CommandError, "Too many arguments" unless args.empty?
12
- dir = File.basename(URI.parse(repos).path) unless dir
13
-
14
- if File.exists?(dir) then
15
- raise Piston::CommandError, "Target folder already exists" unless force
16
- svn :revert, '--recursive', dir
17
- FileUtils.rm_rf(dir)
18
- end
19
-
20
- my_info = YAML::load(svn(:info, File.join(dir, '..')))
21
- my_revision = YAML::load(svn(:info, my_info['URL']))['Revision']
22
- raise Piston::CommandError, "#{File.expand_path(File.join(dir, '..'))} is out of date - run svn update" unless my_info['Revision'] == my_revision
8
+ def repository_type
9
+ options[:repository_type]
10
+ end
23
11
 
24
- info = YAML::load(svn(:info, repos))
25
- his_revision = revision || info['Revision']
26
- options = [:export]
27
- options << ['--revision', his_revision]
28
- options << '--quiet'
29
- options << repos
30
- options << dir
31
- export = svn options
32
- export.each_line do |line|
33
- next unless line =~ /Exported revision (\d+)./i
34
- @revision = $1
35
- break
12
+ def select_repository(repository_url)
13
+ if repository_type then
14
+ logger.info {"Forced repository type to #{repository_type}"}
15
+ repository_class_name = "Piston::#{repository_type.downcase.capitalize}::Repository"
16
+ repository_class = repository_class_name.constantize
17
+ repository_class.new(repository_url)
18
+ else
19
+ logger.info {"Guessing the repository type"}
20
+ Piston::Repository.guess(repository_url)
36
21
  end
22
+ end
37
23
 
38
- # Add so we can set properties
39
- svn :add, '--non-recursive', '--force', '--quiet', dir
40
-
41
- # Set the properties
42
- svn :propset, Piston::ROOT, repos, dir
43
- svn :propset, Piston::UUID, info['Repository UUID'], dir
44
- svn :propset, Piston::REMOTE_REV, his_revision, dir
45
- svn :propset, Piston::LOCAL_REV, my_revision, dir
46
- svn :propset, Piston::LOCKED, revision, dir if lock
47
-
48
- # Finish adding. If we get an error, at least the properties will be
49
- # set and the user can handle the rest
50
- svn :add, '--force', '--quiet', dir
24
+ def run(repository_url, target_revision, wcdir)
25
+ repository = select_repository(repository_url)
26
+ revision = repository.at(target_revision)
51
27
 
52
- logging_stream.puts "Exported r#{his_revision} from '#{repos}' to '#{dir}'"
53
- end
28
+ wcdir = File.expand_path(wcdir.nil? ? repository.basename : wcdir)
29
+ logger.info {"Guessing the working copy type"}
30
+ logger.debug {"repository_url: #{repository_url.inspect}, target_revision: #{target_revision.inspect}, wcdir: #{wcdir.inspect}"}
31
+ working_copy = guess_wc(wcdir)
54
32
 
55
- def self.help
56
- "Prepares a folder for merge tracking"
33
+ if working_copy.exist? && !force then
34
+ logger.fatal "Path #{working_copy} already exists and --force not given. Aborting..."
35
+ abort
36
+ end
37
+
38
+ working_copy.import(revision, options[:lock])
39
+ logger.info {"Imported #{revision} from #{repository}"}
57
40
  end
58
41
 
59
- def self.detailed_help
60
- <<EOF
61
- usage: import REPOS_URL [DIR]
42
+ def start(*args)
43
+ repository_url = args.shift
44
+ wcdir = args.shift
62
45
 
63
- Exports the specified REPOS_URL (which must be a Subversion repository) to
64
- DIR, defaulting to the last component of REPOS_URL if DIR is not present.
46
+ raise ArgumentError, "Required REPOSITORY argument missing" if repository_url.blank?
65
47
 
66
- If the local folder already exists, this command will abort with an error.
67
- EOF
68
- end
48
+ begin
49
+ self.run(repository_url, options[:revision] || options[:commit] || :head, wcdir)
50
+ rescue Piston::Repository::UnhandledUrl => e
51
+ supported_types = Piston::Repository.handlers.collect do |handler|
52
+ handler.repository_type
53
+ end
54
+ puts "Unsure how to handle:"
55
+ puts "\t#{repository_url.inspect}."
56
+ puts "You should try using --repository-type. Supported types are:"
57
+ supported_types.each do |type|
58
+ puts "\t#{type}"
59
+ end
69
60
 
70
- def self.aliases
71
- %w(init)
61
+ exit 1
62
+ end
72
63
  end
73
64
  end
74
65
  end
@@ -0,0 +1,24 @@
1
+ require "piston/commands/base"
2
+
3
+ module Piston
4
+ module Commands
5
+ class Info < Piston::Commands::Base
6
+ attr_reader :options
7
+
8
+ def run(wcdir)
9
+ working_copy = working_copy!(wcdir)
10
+ working_copy.info.to_yaml
11
+ end
12
+
13
+ def start(*args)
14
+ args.flatten.map {|d| Pathname.new(d).expand_path}.each do |wcdir|
15
+ begin
16
+ run(wcdir)
17
+ rescue Piston::WorkingCopy::NotWorkingCopy
18
+ puts "#{wcdir} is not a working copy"
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,26 @@
1
+ require "piston/commands/base"
2
+
3
+ module Piston
4
+ module Commands
5
+ class LockUnlock < Piston::Commands::Base
6
+ attr_reader :options
7
+
8
+ def run(lock)
9
+ working_copy = working_copy!(File.expand_path(options[:wcdir]))
10
+
11
+ values = working_copy.recall
12
+ values["lock"] = lock
13
+ working_copy.remember(values, values["handler"])
14
+ working_copy.finalize
15
+
16
+ text = lock ? "Locked" : "Unlocked"
17
+ logger.info "#{text} #{working_copy} against automatic updates"
18
+ end
19
+
20
+ def start(*args)
21
+ options[:wcdir] = args.first
22
+ run(true)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -1,75 +1,50 @@
1
- require "piston"
2
- require "piston/command"
3
- require 'pp'
1
+ require "piston/commands/base"
4
2
 
5
3
  module Piston
6
4
  module Commands
7
- class Status < Piston::Command
8
- def run
9
- # First, find the list of pistoned folders
10
- folders = svn(:propget, '--recursive', Piston::ROOT, *args)
5
+ class Status < Piston::Commands::Base
6
+ def run(wcdir)
7
+ # Get the working copy handler to search pistonized folders inside it
8
+ handler = guess_wc(wcdir).class
9
+
10
+ # First, find the list of pistonized folders
11
11
  repos = Hash.new
12
- repo = nil
13
- folders.each_line do |line|
14
- next unless line =~ /(\w.*) - /
15
- repos[$1] = Hash.new
12
+ Pathname.glob(wcdir + '**/.piston.yml') do |path|
13
+ repos[path.dirname] = Hash.new
16
14
  end
17
15
 
18
16
  # Then, get their properties
19
- repo = nil
20
- svn(:proplist, '--verbose', *repos.keys).each_line do |line|
21
- case line
22
- when /'([^']+)'/
23
- repo = repos[$1]
24
- when /(piston:[-\w]+)\s*:\s*(.*)$/
25
- repo[$1] = $2
26
- end
27
- end
28
-
29
- # Determine their local status
30
17
  repos.each_pair do |path, props|
31
- log = svn(:log, '--revision', "#{props[Piston::LOCAL_REV]}:HEAD", '--quiet', '--limit', '2', path)
32
- props[:locally_modified] = 'M' if log.count("\n") > 3
18
+ logger.debug {"Get info of #{path}"}
19
+ working_copy = handler.new(path)
20
+ working_copy.validate!
21
+ props.update(working_copy.info)
22
+ props[:locally_modified] = 'M' if working_copy.locally_modified
23
+ props[:remotely_modified] = 'M' if show_updates and working_copy.remotely_modified
33
24
  end
34
25
 
35
- # And their remote status, if required
36
- repos.each_pair do |path, props|
37
- log = svn(:log, '--revision', "#{props[Piston::REMOTE_REV]}:HEAD", '--quiet', '--limit', '2', props[Piston::ROOT])
38
- props[:remotely_modified] = 'M' if log.count("\n") > 3
39
- end if show_updates
40
-
41
26
  # Display the results
42
27
  repos.each_pair do |path, props|
43
- logging_stream.printf "%1s%1s %5s %s (%s)\n", props[:locally_modified],
44
- props[:remotely_modified], props[Piston::LOCKED], path, props[Piston::ROOT]
28
+ printf "%1s%1s %6s %s (%s)\n", props[:locally_modified],
29
+ props[:remotely_modified], props["lock"] ? 'locked' : '', path, props["repository_url"]
45
30
  end
46
31
 
47
- logging_stream.puts "No pistoned folders found" if repos.empty?
48
- end
49
-
50
- def self.help
51
- "Determines the current status of each pistoned directory"
32
+ puts "No pistonized folders found in #{wcdir}" if repos.empty?
52
33
  end
53
34
 
54
- def self.detailed_help
55
- <<EOF
56
- usage: status [DIR [DIR...]]
57
-
58
- Shows the status of one, many or all pistoned folders. The status is
59
- returned in columns.
60
-
61
- The first column's values are:
62
- : Locally unchanged (space)
63
- M: Locally modified since importing
64
-
65
- The second column's values are blanks, unless the --show-updates is passed.
66
- M: Remotely modified since importing
67
- EOF
35
+ def show_updates
36
+ options[:show_updates]
68
37
  end
69
38
 
70
- def self.aliases
71
- %w(st)
72
- end
39
+ def start(*args)
40
+ args.flatten.map {|d| Pathname.new(d).expand_path}.each do |wcdir|
41
+ begin
42
+ run(wcdir)
43
+ rescue Piston::WorkingCopy::NotWorkingCopy
44
+ puts "#{wcdir} is not a working copy"
45
+ end
46
+ end
47
+ end
73
48
  end
74
49
  end
75
50
  end
@@ -1,131 +1,44 @@
1
- require "piston"
2
- require "piston/command"
3
- require 'find'
1
+ require "piston/commands/base"
4
2
 
5
3
  module Piston
6
4
  module Commands
7
- class Update < Piston::Command
8
- def run
9
- (args.empty? ? find_targets : args).each do |dir|
10
- update dir
5
+ class Update < Piston::Commands::Base
6
+ # +wcdir+ is the working copy we're going to change.
7
+ # +to+ is the new target revision we want to be at after update returns.
8
+ def run(wcdir, to)
9
+ working_copy = working_copy!(wcdir)
10
+
11
+ logger.debug {"Recalling previously saved values"}
12
+ values = working_copy.recall
13
+ return "#{wcdir} is locked: not updating" if values["lock"]
14
+
15
+ repository = working_copy.repository
16
+ from_revision = repository.at(values["handler"])
17
+ to_revision = repository.at(to)
18
+ to_revision.resolve!
19
+
20
+ logger.debug {"Validating that #{from_revision} exists and is capable of performing the update"}
21
+ from_revision.validate!
22
+
23
+ logger.info {"Updating from #{from_revision} to #{to_revision}"}
24
+
25
+ changed = working_copy.update(from_revision, to_revision, options[:lock])
26
+ if changed then
27
+ logger.info {"Updated #{wcdir} to #{to_revision}"}
28
+ else
29
+ logger.info {"Upstream #{repository} was unchanged from #{from_revision}"}
11
30
  end
12
31
  end
13
32
 
14
- def update(dir)
15
- return unless File.directory?(dir)
16
- return skip(dir, "locked") unless svn(:propget, Piston::LOCKED, dir) == ''
17
- status = svn(:status, '--show-updates', dir)
18
- new_local_rev = nil
19
- new_status = Array.new
20
- status.each_line do |line|
21
- if line =~ /status.+\s(\d+)$/i then
22
- new_local_rev = $1.to_i
23
- else
24
- new_status << line unless line =~ /^\?/
25
- end
26
- end
27
- raise "Unable to parse status\n#{status}" unless new_local_rev
28
- return skip(dir, "pending updates -- run \"svn update #{dir}\"\n#{new_status}") if new_status.size > 0
29
-
30
- logging_stream.puts "Processing '#{dir}'..."
31
- repos = svn(:propget, Piston::ROOT, dir).chomp
32
- uuid = svn(:propget, Piston::UUID, dir).chomp
33
- remote_revision = svn(:propget, Piston::REMOTE_REV, dir).chomp.to_i
34
- local_revision = svn(:propget, Piston::LOCAL_REV, dir).chomp.to_i
35
- local_revision = local_revision.succ
36
-
37
- logging_stream.puts " Fetching remote repository's latest revision and UUID"
38
- info = YAML::load(svn(:info, repos))
39
- return skip(dir, "Repository UUID changed\n Expected #{uuid}\n Found #{info['Repository UUID']}\n Repository: #{repos}") unless uuid == info['Repository UUID']
40
-
41
- new_remote_rev = info['Last Changed Rev'].to_i
42
- return skip(dir, "unchanged from revision #{remote_revision}", false) if remote_revision == new_remote_rev
43
-
44
- revisions = (remote_revision .. (revision || new_remote_rev))
45
-
46
- logging_stream.puts " Restoring remote repository to known state at r#{revisions.first}"
47
- svn :checkout, '--ignore-externals', '--quiet', '--revision', revisions.first, repos, dir.tmp
48
-
49
- logging_stream.puts " Updating remote repository to r#{revisions.last}"
50
- updates = svn :update, '--ignore-externals', '--revision', revisions.last, dir.tmp
51
-
52
- logging_stream.puts " Processing adds/deletes"
53
- merges = Array.new
54
- changes = 0
55
- updates.each_line do |line|
56
- next unless line =~ %r{^([A-Z]).*\s+#{Regexp.escape(dir.tmp)}[\\/](.+)$}
57
- op, file = $1, $2
58
- changes += 1
59
-
60
- case op
61
- when 'A'
62
- if File.directory?(File.join(dir.tmp, file)) then
63
- svn :mkdir, '--quiet', File.join(dir, file)
64
- else
65
- copy(dir, file)
66
- svn :add, '--quiet', '--force', File.join(dir, file)
67
- end
68
- when 'D'
69
- svn :remove, '--quiet', '--force', File.join(dir, file)
70
- else
71
- copy(dir, file)
72
- merges << file
73
- end
74
- end
75
-
76
- # Determine if there are any local changes in the pistoned directory
77
- log = svn(:log, '--quiet', '--revision', (local_revision .. new_local_rev).to_svn, '--limit', '2', dir)
78
-
79
- # If none, we skip the merge process
80
- if local_revision < new_local_rev && log.count("\n") > 3 then
81
- logging_stream.puts " Merging local changes back in"
82
- merges.each do |file|
83
- begin
84
- svn(:merge, '--quiet', '--revision', (local_revision .. new_local_rev).to_svn,
85
- File.join(dir, file), File.join(dir, file))
86
- rescue RuntimeError
87
- next if $!.message =~ /Unable to find repository location for/
88
- end
89
- end
90
- end
91
-
92
- logging_stream.puts " Removing temporary files / folders"
93
- FileUtils.rm_rf dir.tmp
94
-
95
- logging_stream.puts " Updating Piston properties"
96
- svn :propset, Piston::REMOTE_REV, revisions.last, dir
97
- svn :propset, Piston::LOCAL_REV, new_local_rev, dir
98
- svn :propset, Piston::LOCKED, revisions.last, dir if lock
99
-
100
- logging_stream.puts " Updated to r#{revisions.last} (#{changes} changes)"
101
- end
102
-
103
- def copy(dir, file)
104
- FileUtils.cp(File.join(dir.tmp, file), File.join(dir, file))
105
- end
106
-
107
- def self.help
108
- "Updates all or specified folders to the latest revision"
109
- end
110
-
111
- def self.detailed_help
112
- <<EOF
113
- usage: update [DIR [...]]
114
-
115
- This operation has the effect of downloading all remote changes back to our
116
- working copy. If any local modifications were done, they will be preserved.
117
- If merge conflicts occur, they will not be taken care of, and your subsequent
118
- commit will fail.
119
-
120
- Piston will refuse to update a folder if it has pending updates. Run
121
- 'svn update' on the target folder to update it before running Piston
122
- again.
123
- EOF
124
- end
125
-
126
- def self.aliases
127
- %w(up)
128
- end
33
+ def start(*args)
34
+ args.flatten.map {|d| Pathname.new(d).expand_path}.each do |wcdir|
35
+ begin
36
+ run(wcdir, options[:revision] || options[:commit] || :head)
37
+ rescue Piston::WorkingCopy::NotWorkingCopy
38
+ puts "#{wcdir} is not a working copy"
39
+ end
40
+ end
41
+ end
129
42
  end
130
43
  end
131
44
  end
@@ -0,0 +1,26 @@
1
+ require "piston/commands/base"
2
+
3
+ module Piston
4
+ module Commands
5
+ class Upgrade < Piston::Commands::Base
6
+ def run(*directories)
7
+ # piston 1.x managed only subversion repositories
8
+ directories = directories.select { |dir| Piston::Svn::WorkingCopy.understands_dir? dir }
9
+
10
+ repositories = Piston::Svn::WorkingCopy.old_repositories(*directories)
11
+ repositories.each do |repository|
12
+ logger.debug {"Upgrading repository #{repository}"}
13
+ Piston::Svn::WorkingCopy.new(repository).upgrade
14
+ end
15
+
16
+ repositories
17
+ end
18
+
19
+ def start(*args)
20
+ targets = args.flatten.map {|d| Pathname.new(d).expand_path}
21
+ run(targets)
22
+ puts "#{targets.length} directories upgraded"
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,4 @@
1
+ dir = File.dirname(__FILE__)
2
+ Dir["#{dir}/commands/**/*.rb"].each do |f|
3
+ require f.gsub("#{File.expand_path("#{File.dirname(f)}/../..")}/", "")
4
+ end
@@ -0,0 +1,76 @@
1
+ require "singleton"
2
+
3
+ module Piston
4
+ module Git
5
+ class Client
6
+ include Singleton
7
+
8
+ class CommandError < RuntimeError; end
9
+ class Failed < CommandError; end
10
+ class BadCommand < CommandError; end
11
+
12
+ def logger
13
+ @logger ||= Log4r::Logger["handler::client"]
14
+ end
15
+
16
+ def out_logger
17
+ @out_logger ||= Log4r::Logger["handler::client::out"]
18
+ end
19
+
20
+ def git(*args)
21
+ run_cmd :git, *args
22
+ end
23
+
24
+ private
25
+ def run_cmd(executable, *args)
26
+ args.collect! {|arg| arg.to_s =~ /\s|\*|\?|"|\n|\r/ ? %Q('#{arg}') : arg}
27
+ args.collect! {|arg| arg ? arg : '""'}
28
+ cmd = %Q|#{executable} #{args.join(' ')}|
29
+ logger.debug {"> " + cmd}
30
+
31
+ original_language = ENV["LANGUAGE"]
32
+ begin
33
+ ENV["LANGUAGE"] = "C"
34
+ value = run_real(cmd)
35
+ out_logger.info {"< " + value} unless (value || "").strip.empty?
36
+ return value
37
+ ensure
38
+ ENV["LANGUAGE"] = original_language
39
+ end
40
+ end
41
+
42
+ begin
43
+ raise LoadError, "Not implemented on Win32 machines" if RUBY_PLATFORM =~ /mswin32/
44
+
45
+ begin
46
+ require "rubygems"
47
+ rescue LoadError
48
+ # NOP -- attempt to load without Rubygems
49
+ end
50
+
51
+ require "open4"
52
+
53
+ def run_real(cmd)
54
+ begin
55
+ pid, stdin, stdout, stderr = Open4::popen4(cmd)
56
+ _, cmdstatus = Process.waitpid2(pid)
57
+ raise CommandError, "#{cmd.inspect} exited with status: #{cmdstatus.exitstatus}\n#{stderr.read}" unless cmdstatus.success? || cmdstatus.exitstatus == 1
58
+ return stdout.read
59
+ rescue Errno::ENOENT
60
+ raise BadCommand, cmd.inspect
61
+ end
62
+ end
63
+
64
+ rescue LoadError
65
+ # On platforms where open4 is unavailable, we fallback to running using
66
+ # the backtick method of Kernel.
67
+ def run_real(cmd)
68
+ out = `#{cmd}`
69
+ raise BadCommand, cmd.inspect if $?.exitstatus == 127
70
+ raise Failed, "#{cmd.inspect} exited with status: #{$?.exitstatus}" unless $?.success? || $?.exitstatus == 1
71
+ out
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,114 @@
1
+ require "piston/git/client"
2
+ require "piston/revision"
3
+ require "fileutils"
4
+
5
+ module Piston
6
+ module Git
7
+ class Commit < Piston::Revision
8
+ class InvalidCommit < RuntimeError; end
9
+ class Gone < InvalidCommit; end
10
+
11
+ alias_method :commit, :revision
12
+ attr_reader :sha1
13
+
14
+ def initialize(repository, revision, recalled_values={})
15
+ super
16
+ @revision = 'master' if @revision.upcase == 'HEAD'
17
+ end
18
+
19
+ def client
20
+ @client ||= Piston::Git::Client.instance
21
+ end
22
+
23
+ def git(*args)
24
+ client.git(*args)
25
+ end
26
+
27
+ def recalled_commit_id
28
+ recalled_values[Piston::Git::COMMIT]
29
+ end
30
+
31
+ def validate!
32
+ begin
33
+ data = git("ls-remote", @repository.url)
34
+ self
35
+ rescue Piston::Git::Client::CommandError
36
+ raise Piston::Git::Commit::Gone, "Repository at #{@repository.url} does not exist anymore"
37
+ end
38
+ end
39
+
40
+ def name
41
+ commit[0,7]
42
+ end
43
+
44
+ def branch_name
45
+ "my-#{commit}"
46
+ end
47
+
48
+ def checkout_to(dir)
49
+ super
50
+ git(:clone, repository.url, @dir)
51
+ Dir.chdir(@dir) do
52
+ git(:checkout, "-b", branch_name, commit)
53
+ response = git(:log, "-n", "1")
54
+ @sha1 = $1 if response =~ /commit\s+([a-f\d]{40})/i
55
+ end
56
+ end
57
+
58
+ def update_to(commit)
59
+ raise ArgumentError, "Commit #{self.commit} of #{repository.url} was never checked out -- can't update" unless @dir
60
+
61
+ Dir.chdir(@dir) do
62
+ logger.debug {"Saving old changes before updating"}
63
+ git(:commit, '-a', '-m', 'old changes')
64
+ logger.debug {"Merging old changes with #{commit}"}
65
+ git(:merge, '--squash', commit)
66
+ output = git(:status)
67
+ added = output.scan(/new file:\s+(.*)$/).flatten
68
+ deleted = output.scan(/deleted:\s+(.*)$/).flatten
69
+ renamed = output.scan(/renamed:\s+(.+) -> (.+)$/)
70
+ [added, deleted, renamed]
71
+ end
72
+ end
73
+
74
+ def remember_values
75
+ # find last commit for +commit+ if it wasn't checked out
76
+ @sha1 = git('ls-remote', repository.url, commit).match(/\w+/)[0] unless @sha1
77
+ # if ls-remote returns nothing, +commit+ must be a commit, not a branch
78
+ @sha1 = commit unless @sha1
79
+ { Piston::Git::COMMIT => @sha1, Piston::Git::BRANCH => commit }
80
+ end
81
+
82
+ def each
83
+ raise ArgumentError, "Never cloned + checked out" if @dir.nil?
84
+ @dir.find do |path|
85
+ Find.prune if path.to_s =~ %r{/[.]git}
86
+ next if @dir == path
87
+ next if File.directory?(path)
88
+ yield path.relative_path_from(@dir)
89
+ end
90
+ end
91
+
92
+ def remotely_modified
93
+ branch = recalled_values[Piston::Git::BRANCH]
94
+ logger.debug {"Get last commit in #{branch} of #{repository.url}"}
95
+ commit = git('ls-remote', repository.url, branch).match(/\w+/)
96
+ # when we update to a commit, instead latest commit of a branch, +branch+ will be a commit, and ls-remote can return nil
97
+ commit = commit[0] unless commit.nil?
98
+ commit != self.commit
99
+ end
100
+
101
+ def exclude_for_diff
102
+ Piston::Git::EXCLUDE
103
+ end
104
+
105
+ def to_s
106
+ "commit #{sha1}"
107
+ end
108
+
109
+ def resolve!
110
+ # NOP, because @sha1 is what we want
111
+ end
112
+ end
113
+ end
114
+ end