piston 1.4.0 → 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
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