1
+ using JetBrains . Annotations ;
2
+ using LibGit2Sharp ;
3
+ using System ;
4
+ using System . Collections . Generic ;
5
+ using System . Linq ;
6
+
7
+ namespace GitVersion
8
+ {
9
+ public class GitRepoMetadataProvider
10
+ {
11
+ private Dictionary < Branch , List < BranchCommit > > mergeBaseCommitsCache ;
12
+ private Dictionary < Tuple < Branch , Branch > , MergeBaseData > mergeBaseCache ;
13
+ private Dictionary < Branch , List < SemanticVersion > > semanticVersionTagsOnBranchCache ;
14
+ private IRepository repository ;
15
+ const string missingTipFormat = "{0} has no tip. Please see http://example.com/docs for information on how to fix this." ;
16
+
17
+
18
+ public GitRepoMetadataProvider ( IRepository repository )
19
+ {
20
+ mergeBaseCache = new Dictionary < Tuple < Branch , Branch > , MergeBaseData > ( ) ;
21
+ mergeBaseCommitsCache = new Dictionary < Branch , List < BranchCommit > > ( ) ;
22
+ semanticVersionTagsOnBranchCache = new Dictionary < Branch , List < SemanticVersion > > ( ) ;
23
+ this . repository = repository ;
24
+ }
25
+
26
+ public IEnumerable < SemanticVersion > GetVersionTagsOnBranch ( Branch branch , IRepository repository , string tagPrefixRegex )
27
+ {
28
+ if ( semanticVersionTagsOnBranchCache . ContainsKey ( branch ) )
29
+ {
30
+ return semanticVersionTagsOnBranchCache [ branch ] ;
31
+ }
32
+ var tags = repository . Tags . Select ( t => t ) . ToList ( ) ;
33
+
34
+ var versionTags = repository . Commits . QueryBy ( new CommitFilter
35
+ {
36
+ IncludeReachableFrom = branch . Tip
37
+ } )
38
+ . SelectMany ( c => tags . Where ( t => c . Sha == t . Target . Sha ) . SelectMany ( t =>
39
+ {
40
+ SemanticVersion semver ;
41
+ if ( SemanticVersion . TryParse ( t . FriendlyName , tagPrefixRegex , out semver ) )
42
+ return new [ ] { semver } ;
43
+ return new SemanticVersion [ 0 ] ;
44
+ } ) ) . ToList ( ) ;
45
+
46
+ semanticVersionTagsOnBranchCache . Add ( branch , versionTags ) ;
47
+ return versionTags ;
48
+ }
49
+
50
+ // TODO Should we cache this?
51
+ public IEnumerable < Branch > GetBranchesContainingCommit ( [ NotNull ] Commit commit , IRepository repository , IList < Branch > branches , bool onlyTrackedBranches )
52
+ {
53
+ if ( commit == null )
54
+ {
55
+ throw new ArgumentNullException ( "commit" ) ;
56
+ }
57
+
58
+ using ( Logger . IndentLog ( string . Format ( "Getting branches containing the commit '{0}'." , commit . Id ) ) )
59
+ {
60
+ var directBranchHasBeenFound = false ;
61
+ Logger . WriteInfo ( "Trying to find direct branches." ) ;
62
+ // TODO: It looks wasteful looping through the branches twice. Can't these loops be merged somehow? @asbjornu
63
+ foreach ( var branch in branches )
64
+ {
65
+ if ( branch . Tip != null && branch . Tip . Sha != commit . Sha || ( onlyTrackedBranches && ! branch . IsTracking ) )
66
+ {
67
+ continue ;
68
+ }
69
+
70
+ directBranchHasBeenFound = true ;
71
+ Logger . WriteInfo ( string . Format ( "Direct branch found: '{0}'." , branch . FriendlyName ) ) ;
72
+ yield return branch ;
73
+ }
74
+
75
+ if ( directBranchHasBeenFound )
76
+ {
77
+ yield break ;
78
+ }
79
+
80
+ Logger . WriteInfo ( string . Format ( "No direct branches found, searching through {0} branches." , onlyTrackedBranches ? "tracked" : "all" ) ) ;
81
+ foreach ( var branch in branches . Where ( b => onlyTrackedBranches && ! b . IsTracking ) )
82
+ {
83
+ Logger . WriteInfo ( string . Format ( "Searching for commits reachable from '{0}'." , branch . FriendlyName ) ) ;
84
+
85
+ var commits = repository . Commits . QueryBy ( new CommitFilter
86
+ {
87
+ IncludeReachableFrom = branch
88
+ } ) . Where ( c => c . Sha == commit . Sha ) ;
89
+
90
+ if ( ! commits . Any ( ) )
91
+ {
92
+ Logger . WriteInfo ( string . Format ( "The branch '{0}' has no matching commits." , branch . FriendlyName ) ) ;
93
+ continue ;
94
+ }
95
+
96
+ Logger . WriteInfo ( string . Format ( "The branch '{0}' has a matching commit." , branch . FriendlyName ) ) ;
97
+ yield return branch ;
98
+ }
99
+ }
100
+ }
101
+
102
+ /// <summary>
103
+ /// Find the merge base of the two branches, i.e. the best common ancestor of the two branches' tips.
104
+ /// </summary>
105
+ public Commit FindMergeBase ( Branch branch , Branch otherBranch , IRepository repository )
106
+ {
107
+ var key = Tuple . Create ( branch , otherBranch ) ;
108
+
109
+ if ( mergeBaseCache . ContainsKey ( key ) )
110
+ {
111
+ return mergeBaseCache [ key ] . MergeBase ;
112
+ }
113
+
114
+ using ( Logger . IndentLog ( string . Format ( "Finding merge base between '{0}' and '{1}'." , branch . FriendlyName , otherBranch . FriendlyName ) ) )
115
+ {
116
+ // Otherbranch tip is a forward merge
117
+ var commitToFindCommonBase = otherBranch . Tip ;
118
+ var commit = branch . Tip ;
119
+ if ( otherBranch . Tip . Parents . Contains ( commit ) )
120
+ {
121
+ commitToFindCommonBase = otherBranch . Tip . Parents . First ( ) ;
122
+ }
123
+
124
+ var findMergeBase = repository . ObjectDatabase . FindMergeBase ( commit , commitToFindCommonBase ) ;
125
+ if ( findMergeBase != null )
126
+ {
127
+ Logger . WriteInfo ( string . Format ( "Found merge base of {0}" , findMergeBase . Sha ) ) ;
128
+ // We do not want to include merge base commits which got forward merged into the other branch
129
+ bool mergeBaseWasForwardMerge ;
130
+ do
131
+ {
132
+ // Now make sure that the merge base is not a forward merge
133
+ mergeBaseWasForwardMerge = otherBranch . Commits
134
+ . SkipWhile ( c => c != commitToFindCommonBase )
135
+ . TakeWhile ( c => c != findMergeBase )
136
+ . Any ( c => c . Parents . Contains ( findMergeBase ) ) ;
137
+ if ( mergeBaseWasForwardMerge )
138
+ {
139
+ var second = commitToFindCommonBase . Parents . First ( ) ;
140
+ var mergeBase = repository . ObjectDatabase . FindMergeBase ( commit , second ) ;
141
+ if ( mergeBase == findMergeBase )
142
+ {
143
+ break ;
144
+ }
145
+ findMergeBase = mergeBase ;
146
+ Logger . WriteInfo ( string . Format ( "Merge base was due to a forward merge, next merge base is {0}" , findMergeBase ) ) ;
147
+ }
148
+ } while ( mergeBaseWasForwardMerge ) ;
149
+ }
150
+
151
+ // Store in cache.
152
+ mergeBaseCache . Add ( key , new MergeBaseData ( branch , otherBranch , repository , findMergeBase ) ) ;
153
+
154
+ return findMergeBase ;
155
+ }
156
+ }
157
+
158
+ /// <summary>
159
+ /// Find the commit where the given branch was branched from another branch.
160
+ /// If there are multiple such commits and branches, returns the newest commit.
161
+ /// </summary>
162
+ public BranchCommit FindCommitBranchWasBranchedFrom ( [ NotNull ] Branch branch , IRepository repository , params Branch [ ] excludedBranches )
163
+ {
164
+ if ( branch == null )
165
+ {
166
+ throw new ArgumentNullException ( "branch" ) ;
167
+ }
168
+
169
+ using ( Logger . IndentLog ( string . Format ( "Finding branch source of '{0}'" , branch . FriendlyName ) ) )
170
+ {
171
+ if ( branch . Tip == null )
172
+ {
173
+ Logger . WriteWarning ( string . Format ( missingTipFormat , branch . FriendlyName ) ) ;
174
+ return BranchCommit . Empty ;
175
+ }
176
+
177
+ return GetMergeCommitsForBranch ( branch ) . ExcludingBranches ( excludedBranches ) . FirstOrDefault ( b => ! branch . IsSameBranch ( b . Branch ) ) ;
178
+ }
179
+ }
180
+
181
+
182
+ List < BranchCommit > GetMergeCommitsForBranch ( Branch branch )
183
+ {
184
+ if ( mergeBaseCommitsCache . ContainsKey ( branch ) )
185
+ {
186
+ return mergeBaseCommitsCache [ branch ] ;
187
+ }
188
+
189
+ var branchMergeBases = repository . Branches . Select ( otherBranch =>
190
+ {
191
+ if ( otherBranch . Tip == null )
192
+ {
193
+ Logger . WriteWarning ( string . Format ( missingTipFormat , otherBranch . FriendlyName ) ) ;
194
+ return BranchCommit . Empty ;
195
+ }
196
+
197
+ var findMergeBase = FindMergeBase ( branch , otherBranch , repository ) ;
198
+ return new BranchCommit ( findMergeBase , otherBranch ) ;
199
+ } ) . Where ( b => b . Commit != null ) . OrderByDescending ( b => b . Commit . Committer . When ) . ToList ( ) ;
200
+ mergeBaseCommitsCache . Add ( branch , branchMergeBases ) ;
201
+
202
+ return branchMergeBases ;
203
+ }
204
+
205
+ private class MergeBaseData
206
+ {
207
+ public Branch Branch { get ; private set ; }
208
+ public Branch OtherBranch { get ; private set ; }
209
+ public IRepository Repository { get ; private set ; }
210
+
211
+ public Commit MergeBase { get ; private set ; }
212
+
213
+ public MergeBaseData ( Branch branch , Branch otherBranch , IRepository repository , Commit mergeBase )
214
+ {
215
+ Branch = branch ;
216
+ OtherBranch = otherBranch ;
217
+ Repository = repository ;
218
+ MergeBase = mergeBase ;
219
+ }
220
+ }
221
+ }
222
+ }
0 commit comments