git-multirepo 1.0.0.beta70 → 1.0.0.beta71

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 (72) hide show
  1. checksums.yaml +5 -5
  2. data/.gitattributes +4 -4
  3. data/.gitbugtraq +3 -3
  4. data/.gitignore +38 -38
  5. data/.rspec +2 -2
  6. data/.rubocop.yml +79 -79
  7. data/CHANGELOG.md +116 -112
  8. data/Gemfile +4 -4
  9. data/Gemfile.lock +47 -47
  10. data/LICENSE +22 -22
  11. data/README.md +178 -178
  12. data/Rakefile +1 -1
  13. data/bin/multi +11 -11
  14. data/docs/bug-repros/91565510-repro.sh +20 -20
  15. data/git-multirepo.gemspec +31 -31
  16. data/lib/git-multirepo.rb +3 -3
  17. data/lib/multirepo/commands/add-command.rb +55 -55
  18. data/lib/multirepo/commands/branch-command.rb +88 -88
  19. data/lib/multirepo/commands/checkout-command.rb +127 -127
  20. data/lib/multirepo/commands/clone-command.rb +68 -68
  21. data/lib/multirepo/commands/command.rb +87 -87
  22. data/lib/multirepo/commands/commands.rb +14 -14
  23. data/lib/multirepo/commands/do-command.rb +101 -101
  24. data/lib/multirepo/commands/init-command.rb +121 -121
  25. data/lib/multirepo/commands/inspect-command.rb +48 -48
  26. data/lib/multirepo/commands/install-command.rb +170 -170
  27. data/lib/multirepo/commands/merge-command.rb +249 -249
  28. data/lib/multirepo/commands/open-command.rb +55 -55
  29. data/lib/multirepo/commands/remove-command.rb +48 -48
  30. data/lib/multirepo/commands/uninit-command.rb +18 -18
  31. data/lib/multirepo/commands/update-command.rb +112 -112
  32. data/lib/multirepo/config.rb +19 -19
  33. data/lib/multirepo/files/config-entry.rb +39 -39
  34. data/lib/multirepo/files/config-file.rb +52 -52
  35. data/lib/multirepo/files/lock-entry.rb +29 -29
  36. data/lib/multirepo/files/lock-file.rb +62 -62
  37. data/lib/multirepo/files/meta-file.rb +51 -51
  38. data/lib/multirepo/files/tracking-file.rb +9 -9
  39. data/lib/multirepo/files/tracking-files.rb +64 -64
  40. data/lib/multirepo/git/branch.rb +32 -32
  41. data/lib/multirepo/git/change.rb +11 -11
  42. data/lib/multirepo/git/commit.rb +7 -7
  43. data/lib/multirepo/git/git-runner.rb +56 -56
  44. data/lib/multirepo/git/git.rb +10 -10
  45. data/lib/multirepo/git/ref.rb +38 -38
  46. data/lib/multirepo/git/remote.rb +17 -17
  47. data/lib/multirepo/git/repo.rb +131 -131
  48. data/lib/multirepo/hooks/post-commit-hook.rb +23 -23
  49. data/lib/multirepo/hooks/pre-commit-hook.rb +35 -35
  50. data/lib/multirepo/info.rb +5 -5
  51. data/lib/multirepo/logic/dependency.rb +6 -6
  52. data/lib/multirepo/logic/merge-descriptor.rb +95 -95
  53. data/lib/multirepo/logic/node.rb +75 -75
  54. data/lib/multirepo/logic/performer.rb +62 -62
  55. data/lib/multirepo/logic/repo-selection.rb +25 -25
  56. data/lib/multirepo/logic/revision-selection.rb +15 -15
  57. data/lib/multirepo/logic/revision-selector.rb +23 -23
  58. data/lib/multirepo/logic/version-comparer.rb +10 -10
  59. data/lib/multirepo/multirepo-exception.rb +6 -6
  60. data/lib/multirepo/output/extra-output.rb +12 -12
  61. data/lib/multirepo/output/teamcity-extra-output.rb +11 -11
  62. data/lib/multirepo/utility/console.rb +52 -52
  63. data/lib/multirepo/utility/popen-runner.rb +27 -27
  64. data/lib/multirepo/utility/system-runner.rb +14 -14
  65. data/lib/multirepo/utility/utils.rb +107 -107
  66. data/lib/multirepo/utility/verbosity.rb +6 -6
  67. data/resources/.gitconfig +2 -2
  68. data/resources/post-commit +0 -0
  69. data/resources/pre-commit +0 -0
  70. data/spec/integration/init_spec.rb +19 -19
  71. data/spec/spec_helper.rb +89 -89
  72. metadata +3 -3
@@ -1,17 +1,17 @@
1
- require_relative "git-runner"
2
-
3
- module MultiRepo
4
- class Remote
5
- attr_accessor :name
6
-
7
- def initialize(repo, name)
8
- @repo = repo
9
- @name = name
10
- end
11
-
12
- def url
13
- output = GitRunner.run(@repo.path, "config --get remote.#{@name}.url", Verbosity::OUTPUT_NEVER).strip
14
- return output == "" ? nil : output
15
- end
16
- end
17
- end
1
+ require_relative "git-runner"
2
+
3
+ module MultiRepo
4
+ class Remote
5
+ attr_accessor :name
6
+
7
+ def initialize(repo, name)
8
+ @repo = repo
9
+ @name = name
10
+ end
11
+
12
+ def url
13
+ output = GitRunner.run(@repo.path, "config --get remote.#{@name}.url", Verbosity::OUTPUT_NEVER).strip
14
+ return output == "" ? nil : output
15
+ end
16
+ end
17
+ end
@@ -1,131 +1,131 @@
1
- require_relative "branch"
2
- require_relative "remote"
3
- require_relative "commit"
4
- require_relative "change"
5
-
6
- module MultiRepo
7
- class Repo
8
- attr_accessor :path
9
- attr_accessor :basename
10
-
11
- def initialize(path)
12
- @path = path
13
- @basename = Pathname.new(path).basename.to_s
14
- end
15
-
16
- # Inspection
17
-
18
- def exists?
19
- return false unless Dir.exist?("#{@path}/.git")
20
- return GitRunner.run(@path, "rev-parse --is-inside-work-tree", Verbosity::OUTPUT_NEVER).strip == "true"
21
- end
22
-
23
- def head_born?
24
- result = GitRunner.run(@path, "rev-parse HEAD --", Verbosity::OUTPUT_NEVER).strip
25
- return !result.start_with?("fatal: bad revision")
26
- end
27
-
28
- def current_revision
29
- (current_branch || current_commit).name
30
- end
31
-
32
- def clean?
33
- changes.count == 0
34
- end
35
-
36
- def local_branches
37
- branches_by_removing_prefix(%r{^refs/heads/})
38
- end
39
-
40
- def remote_branches
41
- branches_by_removing_prefix(%r{^refs/remotes/})
42
- end
43
-
44
- def changes
45
- output = GitRunner.run(@path, "status --porcelain", Verbosity::OUTPUT_NEVER)
46
- lines = output.split("\n").each(&:strip).delete_if{ |f| f == "" }
47
- lines.map { |l| Change.new(l) }
48
- end
49
-
50
- # Operations
51
-
52
- def fetch
53
- GitRunner.run_as_system(@path, "fetch --prune")
54
- GitRunner.last_command_succeeded
55
- end
56
-
57
- def clone(url, options = nil)
58
- options = {} unless options
59
-
60
- branch = options[:branch]
61
-
62
- command = "clone #{url} #{@path}"
63
- command << " -q" if options[:quiet] || false
64
- command << " -b #{branch}" if branch
65
- command << " --recurse-submodules"
66
- command << " --shallow-submodules" if options[:shallow] || false
67
- command << " --depth 1" if options[:shallow] || false
68
-
69
- GitRunner.run_as_system(".", command)
70
- GitRunner.last_command_succeeded
71
- end
72
-
73
- def checkout(ref_name)
74
- GitRunner.run(@path, "checkout #{ref_name}", Verbosity::OUTPUT_ON_ERROR)
75
- GitRunner.last_command_succeeded
76
- end
77
-
78
- # Current
79
-
80
- def head
81
- return nil unless exists? && head_born?
82
- Ref.new(self, "HEAD")
83
- end
84
-
85
- def current_commit
86
- return nil unless exists? && head_born?
87
- Commit.new(self, head.commit_id)
88
- end
89
-
90
- def current_branch
91
- return nil unless exists? && head_born?
92
- name = GitRunner.run(@path, "rev-parse --abbrev-ref HEAD", Verbosity::OUTPUT_NEVER).strip
93
- return nil if name == "HEAD" # Code assumes that current_branch will be nil when we're in floating HEAD
94
- Branch.new(self, name)
95
- end
96
-
97
- # Factory methods
98
-
99
- def ref(name)
100
- Ref.new(self, name)
101
- end
102
-
103
- def branch(name)
104
- Branch.new(self, name)
105
- end
106
-
107
- def remote(name)
108
- Remote.new(self, name)
109
- end
110
-
111
- def commit(id)
112
- Commit.new(self, id)
113
- end
114
-
115
- # Private helper methods
116
-
117
- private
118
-
119
- def branches_by_removing_prefix(prefix_regex)
120
- output = GitRunner.run(@path, "for-each-ref --format='%(refname)'", Verbosity::OUTPUT_NEVER)
121
- all_refs = output.strip.split("\n")
122
-
123
- # Remove surrounding quotes on Windows
124
- all_refs = all_refs.map { |l| l.sub(/^\'/, "").sub(/\'$/, "") }
125
-
126
- full_names = all_refs.select { |r| r =~ prefix_regex }
127
- names = full_names.map{ |f| f.sub(prefix_regex, "") }.delete_if{ |n| n =~ /HEAD$/ }
128
- names.map { |b| Branch.new(self, b) }
129
- end
130
- end
131
- end
1
+ require_relative "branch"
2
+ require_relative "remote"
3
+ require_relative "commit"
4
+ require_relative "change"
5
+
6
+ module MultiRepo
7
+ class Repo
8
+ attr_accessor :path
9
+ attr_accessor :basename
10
+
11
+ def initialize(path)
12
+ @path = path
13
+ @basename = Pathname.new(path).basename.to_s
14
+ end
15
+
16
+ # Inspection
17
+
18
+ def exists?
19
+ return false unless Dir.exist?("#{@path}/.git")
20
+ return GitRunner.run(@path, "rev-parse --is-inside-work-tree", Verbosity::OUTPUT_NEVER).strip == "true"
21
+ end
22
+
23
+ def head_born?
24
+ result = GitRunner.run(@path, "rev-parse HEAD --", Verbosity::OUTPUT_NEVER).strip
25
+ return !result.start_with?("fatal: bad revision")
26
+ end
27
+
28
+ def current_revision
29
+ (current_branch || current_commit).name
30
+ end
31
+
32
+ def clean?
33
+ changes.count == 0
34
+ end
35
+
36
+ def local_branches
37
+ branches_by_removing_prefix(%r{^refs/heads/})
38
+ end
39
+
40
+ def remote_branches
41
+ branches_by_removing_prefix(%r{^refs/remotes/})
42
+ end
43
+
44
+ def changes
45
+ output = GitRunner.run(@path, "status --porcelain", Verbosity::OUTPUT_NEVER)
46
+ lines = output.split("\n").each(&:strip).delete_if{ |f| f == "" }
47
+ lines.map { |l| Change.new(l) }
48
+ end
49
+
50
+ # Operations
51
+
52
+ def fetch
53
+ GitRunner.run_as_system(@path, "fetch --prune")
54
+ GitRunner.last_command_succeeded
55
+ end
56
+
57
+ def clone(url, options = nil)
58
+ options = {} unless options
59
+
60
+ branch = options[:branch]
61
+
62
+ command = "clone #{url} #{@path}"
63
+ command << " -q" if options[:quiet] || false
64
+ command << " -b #{branch}" if branch
65
+ command << " --recurse-submodules"
66
+ command << " --shallow-submodules" if options[:shallow] || false
67
+ command << " --depth 1" if options[:shallow] || false
68
+
69
+ GitRunner.run_as_system(".", command)
70
+ GitRunner.last_command_succeeded
71
+ end
72
+
73
+ def checkout(ref_name)
74
+ GitRunner.run(@path, "checkout #{ref_name}", Verbosity::OUTPUT_ON_ERROR)
75
+ GitRunner.last_command_succeeded
76
+ end
77
+
78
+ # Current
79
+
80
+ def head
81
+ return nil unless exists? && head_born?
82
+ Ref.new(self, "HEAD")
83
+ end
84
+
85
+ def current_commit
86
+ return nil unless exists? && head_born?
87
+ Commit.new(self, head.commit_id)
88
+ end
89
+
90
+ def current_branch
91
+ return nil unless exists? && head_born?
92
+ name = GitRunner.run(@path, "rev-parse --abbrev-ref HEAD", Verbosity::OUTPUT_NEVER).strip
93
+ return nil if name == "HEAD" # Code assumes that current_branch will be nil when we're in floating HEAD
94
+ Branch.new(self, name)
95
+ end
96
+
97
+ # Factory methods
98
+
99
+ def ref(name)
100
+ Ref.new(self, name)
101
+ end
102
+
103
+ def branch(name)
104
+ Branch.new(self, name)
105
+ end
106
+
107
+ def remote(name)
108
+ Remote.new(self, name)
109
+ end
110
+
111
+ def commit(id)
112
+ Commit.new(self, id)
113
+ end
114
+
115
+ # Private helper methods
116
+
117
+ private
118
+
119
+ def branches_by_removing_prefix(prefix_regex)
120
+ output = GitRunner.run(@path, "for-each-ref --format='%(refname)'", Verbosity::OUTPUT_NEVER)
121
+ all_refs = output.strip.split("\n")
122
+
123
+ # Remove surrounding quotes on Windows
124
+ all_refs = all_refs.map { |l| l.sub(/^\'/, "").sub(/\'$/, "") }
125
+
126
+ full_names = all_refs.select { |r| r =~ prefix_regex }
127
+ names = full_names.map{ |f| f.sub(prefix_regex, "") }.delete_if{ |n| n =~ /HEAD$/ }
128
+ names.map { |b| Branch.new(self, b) }
129
+ end
130
+ end
131
+ end
@@ -1,23 +1,23 @@
1
- require "multirepo/files/config-file"
2
- require "multirepo/files/tracking-files"
3
- require "multirepo/utility/utils"
4
- require "multirepo/utility/console"
5
-
6
- module MultiRepo
7
- class PostCommitHook
8
- def self.run
9
- Config.instance.running_git_hook = true
10
-
11
- Console.log_step("Performing post-commit operations...")
12
-
13
- # Works around bug #91565510 (https://www.pivotaltracker.com/story/show/91565510)
14
- TrackingFiles.new(".").stage
15
- Console.log_info("Cleaned-up staging area")
16
-
17
- exit 0
18
- rescue StandardError => e
19
- Console.log_error("Post-commit hook failed to execute! #{e.message}")
20
- exit 1
21
- end
22
- end
23
- end
1
+ require "multirepo/files/config-file"
2
+ require "multirepo/files/tracking-files"
3
+ require "multirepo/utility/utils"
4
+ require "multirepo/utility/console"
5
+
6
+ module MultiRepo
7
+ class PostCommitHook
8
+ def self.run
9
+ Config.instance.running_git_hook = true
10
+
11
+ Console.log_step("Performing post-commit operations...")
12
+
13
+ # Works around bug #91565510 (https://www.pivotaltracker.com/story/show/91565510)
14
+ TrackingFiles.new(".").stage
15
+ Console.log_info("Cleaned-up staging area")
16
+
17
+ exit 0
18
+ rescue StandardError => e
19
+ Console.log_error("Post-commit hook failed to execute! #{e.message}")
20
+ exit 1
21
+ end
22
+ end
23
+ end
@@ -1,35 +1,35 @@
1
- require "multirepo/files/config-file"
2
- require "multirepo/files/tracking-files"
3
- require "multirepo/utility/utils"
4
- require "multirepo/utility/console"
5
-
6
- module MultiRepo
7
- class PreCommitHook
8
- def self.run
9
- Config.instance.running_git_hook = true
10
-
11
- Console.log_step("Performing pre-commit operations...")
12
-
13
- dependencies_clean = Utils.dependencies_clean?(ConfigFile.new(".").load_entries)
14
-
15
- unless dependencies_clean
16
- Console.log_error("You must commit changes to your dependencies before you can commit this repo")
17
- exit 1
18
- end
19
-
20
- tracking_files = TrackingFiles.new(".")
21
- tracking_files.update
22
- tracking_files.stage
23
-
24
- Console.log_info("Updated and staged tracking files")
25
-
26
- exit 0
27
- rescue MultiRepoException => e
28
- Console.log_error("Can't perform commit. #{e.message}")
29
- exit 1
30
- rescue StandardError => e
31
- Console.log_error("Pre-commit hook failed to execute! #{e.message}")
32
- exit 1
33
- end
34
- end
35
- end
1
+ require "multirepo/files/config-file"
2
+ require "multirepo/files/tracking-files"
3
+ require "multirepo/utility/utils"
4
+ require "multirepo/utility/console"
5
+
6
+ module MultiRepo
7
+ class PreCommitHook
8
+ def self.run
9
+ Config.instance.running_git_hook = true
10
+
11
+ Console.log_step("Performing pre-commit operations...")
12
+
13
+ dependencies_clean = Utils.dependencies_clean?(ConfigFile.new(".").load_entries)
14
+
15
+ unless dependencies_clean
16
+ Console.log_error("You must commit changes to your dependencies before you can commit this repo")
17
+ exit 1
18
+ end
19
+
20
+ tracking_files = TrackingFiles.new(".")
21
+ tracking_files.update
22
+ tracking_files.stage
23
+
24
+ Console.log_info("Updated and staged tracking files")
25
+
26
+ exit 0
27
+ rescue MultiRepoException => e
28
+ Console.log_error("Can't perform commit. #{e.message}")
29
+ exit 1
30
+ rescue StandardError => e
31
+ Console.log_error("Pre-commit hook failed to execute! #{e.message}")
32
+ exit 1
33
+ end
34
+ end
35
+ end
@@ -1,5 +1,5 @@
1
- module MultiRepo
2
- NAME = "git-multirepo"
3
- VERSION = "1.0.0.beta70"
4
- DESCRIPTION = "Track multiple Git repositories side-by-side."
5
- end
1
+ module MultiRepo
2
+ NAME = "git-multirepo"
3
+ VERSION = "1.0.0.beta71"
4
+ DESCRIPTION = "Track multiple Git repositories side-by-side."
5
+ end
@@ -1,6 +1,6 @@
1
- module MultiRepo
2
- class Dependency
3
- attr_accessor :config_entry
4
- attr_accessor :lock_entry
5
- end
6
- end
1
+ module MultiRepo
2
+ class Dependency
3
+ attr_accessor :config_entry
4
+ attr_accessor :lock_entry
5
+ end
6
+ end
@@ -1,95 +1,95 @@
1
- require "colored"
2
- require "multirepo/git/repo"
3
-
4
- module MultiRepo
5
- class TheirState
6
- NON_EXISTENT = 0
7
- EXACT_REF = 1
8
- LOCAL_NO_UPSTREAM = 2
9
- UPSTREAM_NO_LOCAL = 3
10
- LOCAL_UP_TO_DATE = 4
11
- LOCAL_OUTDATED = 5
12
- LOCAL_UPSTREAM_DIVERGED = 6
13
- end
14
-
15
- class MergeDescriptor
16
- attr_accessor :name
17
- attr_accessor :repo
18
- attr_accessor :our_revision
19
- attr_accessor :their_revision
20
- attr_accessor :state
21
-
22
- def initialize(name, repo, our_revision, their_revision)
23
- @name = name
24
- @repo = repo
25
- @our_revision = our_revision
26
- @their_revision = their_revision
27
-
28
- # Revisions can be anything: "feature1", "origin/feature1", "b51f3c0", ...
29
- their_ref = repo.ref(their_revision)
30
-
31
- @short_commit_id = their_ref.short_commit_id
32
-
33
- @state = determine_merge_state(repo, their_ref)
34
- end
35
-
36
- def merge_description
37
- case @state
38
- when TheirState::NON_EXISTENT then "No revision named #{@their_revision}".red
39
- else; "Merge '#{@state == TheirState::EXACT_REF ? @short_commit_id : @their_revision}' into '#{@our_revision}'"
40
- end
41
- end
42
-
43
- def upstream_description
44
- case @state
45
- when TheirState::NON_EXISTENT then "--"
46
- when TheirState::EXACT_REF then "Exact ref".yellow
47
- when TheirState::LOCAL_NO_UPSTREAM then "Not remote-tracking".yellow
48
- when TheirState::UPSTREAM_NO_LOCAL then "Branch is upstream".green
49
- when TheirState::LOCAL_UP_TO_DATE then "Local up-to-date with upstream".green
50
- when TheirState::LOCAL_OUTDATED then "Local outdated compared to upstream".yellow
51
- when TheirState::LOCAL_UPSTREAM_DIVERGED then "Local and upstream have diverged!".red
52
- end
53
- end
54
-
55
- private
56
-
57
- def determine_merge_state(repo, their_ref)
58
- return TheirState::NON_EXISTENT unless their_ref.exists?
59
-
60
- remote_branch = repo.remote_branches.find { |b| b.name == their_ref.name }
61
- local_branch = repo.local_branches.find { |b| b.name == their_ref.name }
62
-
63
- # If no local branch nor remote branch exist for their_ref, this is an exact ref
64
- return TheirState::EXACT_REF unless remote_branch || local_branch
65
-
66
- # If remote exists but local does not, return UPSTREAM_NO_LOCAL
67
- return TheirState::UPSTREAM_NO_LOCAL if remote_branch && !local_branch
68
-
69
- # If there is no upstream, no need to check for differences between local and remote
70
- return TheirState::LOCAL_NO_UPSTREAM unless local_branch.upstream_branch
71
-
72
- # Else check local vs upstream state
73
- return determine_local_upstream_merge_state(repo, their_ref)
74
- end
75
-
76
- def determine_local_upstream_merge_state(repo, their_ref)
77
- # We can assume we're working with a branch at this point
78
- their_branch = repo.branch(their_ref.name)
79
-
80
- their_upstream_branch = their_branch.upstream_branch
81
- local_as_upstream = their_branch.commit_id == their_upstream_branch.commit_id
82
- can_fast_forward_local_to_upstream = their_branch.can_fast_forward_to?(their_upstream_branch)
83
-
84
- state = if local_as_upstream
85
- TheirState::LOCAL_UP_TO_DATE
86
- elsif !local_as_upstream && can_fast_forward_local_to_upstream
87
- TheirState::LOCAL_OUTDATED
88
- else
89
- TheirState::LOCAL_UPSTREAM_DIVERGED
90
- end
91
-
92
- return state
93
- end
94
- end
95
- end
1
+ require "colored"
2
+ require "multirepo/git/repo"
3
+
4
+ module MultiRepo
5
+ class TheirState
6
+ NON_EXISTENT = 0
7
+ EXACT_REF = 1
8
+ LOCAL_NO_UPSTREAM = 2
9
+ UPSTREAM_NO_LOCAL = 3
10
+ LOCAL_UP_TO_DATE = 4
11
+ LOCAL_OUTDATED = 5
12
+ LOCAL_UPSTREAM_DIVERGED = 6
13
+ end
14
+
15
+ class MergeDescriptor
16
+ attr_accessor :name
17
+ attr_accessor :repo
18
+ attr_accessor :our_revision
19
+ attr_accessor :their_revision
20
+ attr_accessor :state
21
+
22
+ def initialize(name, repo, our_revision, their_revision)
23
+ @name = name
24
+ @repo = repo
25
+ @our_revision = our_revision
26
+ @their_revision = their_revision
27
+
28
+ # Revisions can be anything: "feature1", "origin/feature1", "b51f3c0", ...
29
+ their_ref = repo.ref(their_revision)
30
+
31
+ @short_commit_id = their_ref.short_commit_id
32
+
33
+ @state = determine_merge_state(repo, their_ref)
34
+ end
35
+
36
+ def merge_description
37
+ case @state
38
+ when TheirState::NON_EXISTENT then "No revision named #{@their_revision}".red
39
+ else; "Merge '#{@state == TheirState::EXACT_REF ? @short_commit_id : @their_revision}' into '#{@our_revision}'"
40
+ end
41
+ end
42
+
43
+ def upstream_description
44
+ case @state
45
+ when TheirState::NON_EXISTENT then "--"
46
+ when TheirState::EXACT_REF then "Exact ref".yellow
47
+ when TheirState::LOCAL_NO_UPSTREAM then "Not remote-tracking".yellow
48
+ when TheirState::UPSTREAM_NO_LOCAL then "Branch is upstream".green
49
+ when TheirState::LOCAL_UP_TO_DATE then "Local up-to-date with upstream".green
50
+ when TheirState::LOCAL_OUTDATED then "Local outdated compared to upstream".yellow
51
+ when TheirState::LOCAL_UPSTREAM_DIVERGED then "Local and upstream have diverged!".red
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ def determine_merge_state(repo, their_ref)
58
+ return TheirState::NON_EXISTENT unless their_ref.exists?
59
+
60
+ remote_branch = repo.remote_branches.find { |b| b.name == their_ref.name }
61
+ local_branch = repo.local_branches.find { |b| b.name == their_ref.name }
62
+
63
+ # If no local branch nor remote branch exist for their_ref, this is an exact ref
64
+ return TheirState::EXACT_REF unless remote_branch || local_branch
65
+
66
+ # If remote exists but local does not, return UPSTREAM_NO_LOCAL
67
+ return TheirState::UPSTREAM_NO_LOCAL if remote_branch && !local_branch
68
+
69
+ # If there is no upstream, no need to check for differences between local and remote
70
+ return TheirState::LOCAL_NO_UPSTREAM unless local_branch.upstream_branch
71
+
72
+ # Else check local vs upstream state
73
+ return determine_local_upstream_merge_state(repo, their_ref)
74
+ end
75
+
76
+ def determine_local_upstream_merge_state(repo, their_ref)
77
+ # We can assume we're working with a branch at this point
78
+ their_branch = repo.branch(their_ref.name)
79
+
80
+ their_upstream_branch = their_branch.upstream_branch
81
+ local_as_upstream = their_branch.commit_id == their_upstream_branch.commit_id
82
+ can_fast_forward_local_to_upstream = their_branch.can_fast_forward_to?(their_upstream_branch)
83
+
84
+ state = if local_as_upstream
85
+ TheirState::LOCAL_UP_TO_DATE
86
+ elsif !local_as_upstream && can_fast_forward_local_to_upstream
87
+ TheirState::LOCAL_OUTDATED
88
+ else
89
+ TheirState::LOCAL_UPSTREAM_DIVERGED
90
+ end
91
+
92
+ return state
93
+ end
94
+ end
95
+ end