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