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