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