git-multirepo 1.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +38 -0
  3. data/.rspec +2 -0
  4. data/Gemfile +4 -0
  5. data/Gemfile.lock +37 -0
  6. data/LICENSE +22 -0
  7. data/README.md +132 -0
  8. data/Rakefile +2 -0
  9. data/bin/multi +6 -0
  10. data/git-multirepo.gemspec +29 -0
  11. data/lib/commands.rb +11 -0
  12. data/lib/git-multirepo.rb +1 -0
  13. data/lib/info.rb +5 -0
  14. data/lib/multirepo/commands/add.rb +41 -0
  15. data/lib/multirepo/commands/checkout.rb +59 -0
  16. data/lib/multirepo/commands/command.rb +41 -0
  17. data/lib/multirepo/commands/edit.rb +22 -0
  18. data/lib/multirepo/commands/fetch.rb +24 -0
  19. data/lib/multirepo/commands/init.rb +54 -0
  20. data/lib/multirepo/commands/install.rb +61 -0
  21. data/lib/multirepo/commands/open.rb +26 -0
  22. data/lib/multirepo/commands/remove.rb +42 -0
  23. data/lib/multirepo/commands/uninit.rb +21 -0
  24. data/lib/multirepo/commands/update.rb +19 -0
  25. data/lib/multirepo/config.rb +10 -0
  26. data/lib/multirepo/files/config-entry.rb +38 -0
  27. data/lib/multirepo/files/config-file.rb +38 -0
  28. data/lib/multirepo/files/lock-entry.rb +26 -0
  29. data/lib/multirepo/files/lock-file.rb +35 -0
  30. data/lib/multirepo/git/branch.rb +17 -0
  31. data/lib/multirepo/git/change.rb +11 -0
  32. data/lib/multirepo/git/git.rb +33 -0
  33. data/lib/multirepo/git/remote.rb +16 -0
  34. data/lib/multirepo/git/repo.rb +67 -0
  35. data/lib/multirepo/hooks/pre-commit-hook.rb +23 -0
  36. data/lib/multirepo/multirepo-exception.rb +6 -0
  37. data/lib/multirepo/utility/console.rb +52 -0
  38. data/lib/multirepo/utility/runner.rb +21 -0
  39. data/lib/multirepo/utility/utils.rb +37 -0
  40. data/resources/pre-commit +6 -0
  41. data/spec/integration/init-spec.rb +23 -0
  42. data/spec/spec_helper.rb +89 -0
  43. metadata +178 -0
@@ -0,0 +1,22 @@
1
+ require "os"
2
+
3
+ module MultiRepo
4
+ class Edit < Command
5
+ self.command = "edit"
6
+ self.summary = "Opens the .multirepo file in the default text editor."
7
+
8
+ def run
9
+ super
10
+ ensure_multirepo_initialized
11
+
12
+ if OS.posix?
13
+ editor = `echo ${FCEDIT:-${VISUAL:-${EDITOR:-vi}}}`.strip
14
+ system(editor, ".multirepo")
15
+ elsif OS.windows?
16
+ raise "The edit command is not implemented on Window yet."
17
+ end
18
+ rescue MultiRepoException => e
19
+ Console.log_error(e.message)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,24 @@
1
+ require "multirepo/utility/console"
2
+
3
+ module MultiRepo
4
+ class Fetch < Command
5
+ self.command = "fetch"
6
+ self.summary = "Performs a git fetch on all dependencies."
7
+
8
+ def run
9
+ super
10
+ ensure_multirepo_initialized
11
+
12
+ Console.log_step("Fetching dependencies...")
13
+
14
+ ConfigFile.load.each do |entry|
15
+ Console.log_substep("Fetching from #{entry.repo.remote('origin').url}...")
16
+ entry.repo.fetch
17
+ end
18
+
19
+ Console.log_step("Done!")
20
+ rescue MultiRepoException => e
21
+ Console.log_error(e.message)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,54 @@
1
+ require "multirepo/utility/console"
2
+ require "multirepo/utility/utils"
3
+ require "multirepo/files/config-file"
4
+ require "multirepo/files/lock-file"
5
+ require "multirepo/commands/command"
6
+
7
+ module MultiRepo
8
+ class Init < Command
9
+ self.command = "init"
10
+ self.summary = "Initialize the current repository as a multirepo project."
11
+
12
+ def run
13
+ super
14
+ Console.log_step("Initializing new multirepo config...")
15
+
16
+ if ConfigFile.exists?
17
+ return unless Console.ask_yes_no(".multirepo file already exists. Reinitialize?")
18
+ end
19
+
20
+ sibling_repos = Utils.sibling_repos
21
+
22
+ if sibling_repos.any?
23
+ entries = []
24
+ sibling_repos.each do |repo|
25
+ if Console.ask_yes_no("Do you want to add '#{repo.path}' (#{repo.remote('origin').url} #{repo.current_branch}) as a dependency?")
26
+ entries.push(ConfigEntry.new(repo))
27
+ Console.log_substep("Added the repository '#{repo.path}' to the .multirepo file")
28
+ end
29
+ end
30
+ ConfigFile.save(entries)
31
+
32
+ ConfigFile.stage
33
+
34
+ uncommitted = Utils.warn_of_uncommitted_changes(entries)
35
+ raise MultiRepoException, "Can't finish initialization!" if uncommitted
36
+
37
+ self.update_lock_file
38
+ else
39
+ Console.log_info("There are no sibling repositories to add")
40
+ end
41
+
42
+ self.install_pre_commit_hook
43
+
44
+ Console.log_step("Done!")
45
+ rescue MultiRepoException => e
46
+ Console.log_error(e.message)
47
+ end
48
+
49
+ def check_repo_exists
50
+ if !Dir.exists?(@repo.path) then raise MultiRepoException, "There is no folder at path '#{@repo.path}'" end
51
+ if !@repo.exists? then raise MultiRepoException, "'#{@repo.path}' is not a repository" end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,61 @@
1
+ require "multirepo/utility/console"
2
+ require "multirepo/utility/utils"
3
+ require "multirepo/git/repo"
4
+
5
+ module MultiRepo
6
+ class Setup < Command
7
+ self.command = "install"
8
+ self.summary = "Clones and checks out dependencies as defined in the .multirepo file, and installs git-multirepo's local pre-commit hook."
9
+
10
+ def run
11
+ super
12
+ ensure_multirepo_initialized
13
+
14
+ Console.log_step("Cloning dependencies and installing hook...")
15
+
16
+ ConfigFile.load.each { |e| install(e) }
17
+
18
+ self.install_pre_commit_hook
19
+
20
+ Console.log_step("Done!")
21
+ rescue MultiRepoException => e
22
+ Console.log_error(e.message)
23
+ end
24
+
25
+ def install(entry)
26
+ if entry.repo.exists?
27
+ check_repo_validity(entry)
28
+ fetch_repo(entry)
29
+ else
30
+ clone_repo(entry)
31
+ end
32
+ checkout_branch(entry)
33
+ end
34
+
35
+ # Repo operations
36
+
37
+ def fetch_repo(entry)
38
+ Console.log_substep("Working copy '#{entry.repo.path}' already exists, fetching instead...")
39
+ raise MultiRepoException, "Could not fetch from remote #{entry.repo.remote('origin').url}" unless entry.repo.fetch
40
+ end
41
+
42
+ def clone_repo(entry)
43
+ Console.log_substep("Cloning '#{entry.url} to #{entry.repo.path}'")
44
+ raise MultiRepoException, "Could not clone remote #{entry.url}" unless entry.repo.clone(entry.url)
45
+ end
46
+
47
+ def checkout_branch(entry)
48
+ branch = entry.repo.branch(entry.branch);
49
+ raise MultiRepoException, "Could not checkout branch #{branch.name}" unless branch.checkout
50
+ Console.log_substep("Checked out branch #{branch.name} -> origin/#{branch.name}")
51
+ end
52
+
53
+ # Validation
54
+
55
+ def check_repo_validity(entry)
56
+ unless entry.repo.remote("origin").url == entry.url
57
+ raise MultiRepoException, "'#{entry.path}' origin URL (#{entry.repo.remote('origin').url}) does not match entry (#{entry.url})!"
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,26 @@
1
+ require "os"
2
+
3
+ require "multirepo/utility/console"
4
+ require "multirepo/utility/utils"
5
+
6
+ module MultiRepo
7
+ class Open < Command
8
+ self.command = "open"
9
+ self.summary = "Opens all dependencies in the current OS's file explorer."
10
+
11
+ def run
12
+ super
13
+ ensure_multirepo_initialized
14
+
15
+ ConfigFile.load.each do |entry|
16
+ if OS.osx?
17
+ `open "#{entry.repo.path}"`
18
+ elsif OS.windows?
19
+ `explorer "#{Utils.convert_to_windows_path(entry.repo.path)}"`
20
+ end
21
+ end
22
+ rescue MultiRepoException => e
23
+ Console.log_error(e.message)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,42 @@
1
+ require "multirepo/utility/console"
2
+ require "multirepo/files/config-file"
3
+
4
+ module MultiRepo
5
+ class Remove < Command
6
+ self.command = "remove"
7
+ self.summary = "Removes the specified dependency from multirepo."
8
+
9
+ def initialize(argv)
10
+ @path = argv.shift_argument
11
+ @delete = argv.flag?("delete")
12
+ super
13
+ end
14
+
15
+ def validate!
16
+ super
17
+ help! "You must specify a dependency repository to remove" unless @path
18
+ end
19
+
20
+ def run
21
+ super
22
+ ensure_multirepo_initialized
23
+
24
+ repo = Repo.new(@path)
25
+ entry = ConfigEntry.new(repo)
26
+
27
+ if ConfigFile.entry_exists?(entry)
28
+ ConfigFile.remove_entry(entry)
29
+ Console.log_step("Removed '#{@path}' from the .multirepo file")
30
+
31
+ if @delete
32
+ FileUtils.rm_rf(@path)
33
+ Console.log_step("Deleted '#{@path}' from disk")
34
+ end
35
+ else
36
+ raise MultiRepoException, "'#{@path}' isn't tracked by multirepo"
37
+ end
38
+ rescue MultiRepoException => e
39
+ Console.log_error(e.message)
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,21 @@
1
+ require "multirepo/utility/console"
2
+
3
+ module MultiRepo
4
+ class Uninit < Command
5
+ self.command = "uninit"
6
+ self.summary = "Removes all traces of multirepo in the current multirepo repository."
7
+
8
+ def run
9
+ super
10
+ ensure_multirepo_initialized
11
+
12
+ File.delete(".multirepo")
13
+ File.delete(".multirepo.lock")
14
+ File.delete(".git/hooks/pre-commit")
15
+
16
+ Console.log_step("All traces of multirepo have been removed from this repository")
17
+ rescue MultiRepoException => e
18
+ Console.log_error(e.message)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,19 @@
1
+ require "multirepo/utility/console"
2
+
3
+ module MultiRepo
4
+ class Update < Command
5
+ self.command = "update"
6
+ self.summary = "Force-updates the multirepo lock file."
7
+
8
+ def run
9
+ super
10
+ ensure_multirepo_initialized
11
+
12
+ LockFile.update
13
+
14
+ Console.log_step("Lock file updated")
15
+ rescue MultiRepoException => e
16
+ Console.log_error(e.message)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,10 @@
1
+ require "singleton"
2
+
3
+ module MultiRepo
4
+ class Config
5
+ include Singleton
6
+
7
+ attr_accessor :verbose
8
+ @verbose = false
9
+ end
10
+ end
@@ -0,0 +1,38 @@
1
+ require "securerandom"
2
+
3
+ require "multirepo/utility/console"
4
+ require "multirepo/git/repo"
5
+
6
+ module MultiRepo
7
+ class ConfigEntry
8
+ attr_accessor :id
9
+ attr_accessor :path
10
+ attr_accessor :url
11
+ attr_accessor :branch
12
+ attr_accessor :repo
13
+
14
+ def encode_with(coder)
15
+ coder["id"] = @id
16
+ coder["path"] = @path
17
+ coder["url"] = @url
18
+ coder["branch"] = @branch
19
+ end
20
+
21
+ def ==(entry)
22
+ entry_path = Pathname.new(entry.path)
23
+ self_path = Pathname.new(self.path)
24
+ entry_path.exist? && self_path.exist? && entry_path.realpath == self_path.realpath
25
+ end
26
+
27
+ def initialize(repo)
28
+ @id = SecureRandom.uuid
29
+ @path = repo.path
30
+ @url = repo.exists? ? repo.remote('origin').url : nil
31
+ @branch = repo.exists? ? repo.current_branch : nil
32
+ end
33
+
34
+ def repo
35
+ Repo.new(path)
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,38 @@
1
+ require "fileutils"
2
+ require "pathname"
3
+
4
+ require_relative "config-entry"
5
+
6
+ module MultiRepo
7
+ class ConfigFile
8
+ FILE = Pathname.new(".multirepo")
9
+
10
+ def self.exists?
11
+ FILE.exist?
12
+ end
13
+
14
+ def self.load
15
+ Psych.load(FILE.read)
16
+ end
17
+
18
+ def self.save(entries)
19
+ File.write(FILE.to_s, Psych.dump(entries))
20
+ end
21
+
22
+ def self.entry_exists?(entry)
23
+ load.any? { |e| e == entry }
24
+ end
25
+
26
+ def self.add_entry(entry)
27
+ save(load.push(entry))
28
+ end
29
+
30
+ def self.remove_entry(entry)
31
+ save(load.delete_if { |e| e == entry })
32
+ end
33
+
34
+ def self.stage
35
+ Git.run_in_current_dir("add -A -f #{FILE.to_s}", false)
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,26 @@
1
+ require "multirepo/utility/console"
2
+ require "multirepo/git/repo"
3
+
4
+ module MultiRepo
5
+ class LockEntry
6
+ attr_accessor :config_entry
7
+ attr_accessor :name
8
+ attr_accessor :id
9
+ attr_accessor :head
10
+ attr_accessor :branch
11
+
12
+ def encode_with(coder)
13
+ coder["name"] = @name
14
+ coder["id"] = @id
15
+ coder["head"] = @head
16
+ coder["branch"] = @branch
17
+ end
18
+
19
+ def initialize(config_entry)
20
+ @name = config_entry.repo.basename
21
+ @id = config_entry.id
22
+ @head = config_entry.repo.head_hash
23
+ @branch = config_entry.repo.current_branch
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,35 @@
1
+ require "pathname"
2
+ require "psych"
3
+
4
+ require "multirepo/git/git"
5
+ require_relative "lock-entry"
6
+ require_relative "config-file"
7
+
8
+ module MultiRepo
9
+ class LockFile
10
+ FILE = Pathname.new(".multirepo.lock")
11
+
12
+ def self.exists?
13
+ FILE.exist?
14
+ end
15
+
16
+ def self.load
17
+ Psych.load(FILE.read)
18
+ end
19
+
20
+ def self.update
21
+ config_entries = ConfigFile.load
22
+ lock_entries = config_entries.map { |c| LockEntry.new(c) }
23
+
24
+ File.write(FILE.to_s, Psych.dump(lock_entries))
25
+
26
+ Git.run_in_current_dir("add -A -f #{FILE.to_s}", false)
27
+ end
28
+
29
+ def self.validate_components(line, components)
30
+ unless components.count == 2
31
+ raise MultiRepoException, "Wrong entry format in .multirepo.lock file: #{line}"
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,17 @@
1
+ require_relative "git"
2
+
3
+ module MultiRepo
4
+ class Branch
5
+ attr_accessor :name
6
+
7
+ def initialize(repo, name)
8
+ @repo = repo
9
+ @name = name
10
+ end
11
+
12
+ def checkout
13
+ Git.run_in_working_dir(@repo.path, "checkout #{@name}", false)
14
+ Git.last_command_succeeded
15
+ end
16
+ end
17
+ end