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