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