11package changelog
22
33import (
4+ "fmt"
45 "io/ioutil"
56 "path/filepath"
67 "sort"
8+ "sync"
9+ "time"
10+
11+ "golang.org/x/sync/errgroup"
712
813 "github.com/go-git/go-billy/v5/memfs"
914 "github.com/go-git/go-git/v5"
@@ -24,9 +29,92 @@ var TypeValues = []string{"enhancement",
2429type Entry struct {
2530 Issue string
2631 Body string
32+ Date time.Time
33+ Hash string
34+ }
35+
36+ // EntryList provides thread-safe operations on a list of Entry values
37+ type EntryList struct {
38+ mu sync.RWMutex
39+ es []* Entry
40+ }
41+
42+ // NewEntryList returns an EntryList with capacity c
43+ func NewEntryList (c int ) * EntryList {
44+ return & EntryList {
45+ es : make ([]* Entry , 0 , c ),
46+ }
47+ }
48+
49+ // Append appends entries to the EntryList
50+ func (el * EntryList ) Append (entries ... * Entry ) {
51+ el .mu .Lock ()
52+ defer el .mu .Unlock ()
53+ el .es = append (el .es , entries ... )
54+ }
55+
56+ // Get returns the Entry at index i
57+ func (el * EntryList ) Get (i int ) * Entry {
58+ el .mu .RLock ()
59+ defer el .mu .RUnlock ()
60+ if i >= len (el .es ) || i < 0 {
61+ return nil
62+ }
63+ return el .es [i ]
64+ }
65+
66+ // Set sets the Entry at index i. The list will be resized if i is larger than
67+ // the current list capacity.
68+ func (el * EntryList ) Set (i int , e * Entry ) {
69+ if i < 0 {
70+ panic ("invalid slice index" )
71+ }
72+ el .mu .Lock ()
73+ defer el .mu .Unlock ()
74+
75+ if i > (cap (el .es ) - 1 ) {
76+ // resize the slice
77+ newEntries := make ([]* Entry , i )
78+ copy (newEntries , el .es )
79+ el .es = newEntries
80+ }
81+ el .es [i ] = e
82+ }
83+
84+ // Len returns the number of items in the EntryList
85+ func (el * EntryList ) Len () int {
86+ el .mu .RLock ()
87+ defer el .mu .RUnlock ()
88+ return len (el .es )
89+ }
90+
91+ // SortByIssue does an in-place sort of the entries by their issue number.
92+ func (el * EntryList ) SortByIssue () {
93+ el .mu .Lock ()
94+ defer el .mu .Unlock ()
95+ sort .Slice (el .es , func (i , j int ) bool {
96+ return el .es [i ].Issue < el .es [j ].Issue
97+ })
2798}
2899
29- func Diff (repo , ref1 , ref2 , dir string ) ([]Entry , error ) {
100+ type changelog struct {
101+ content []byte
102+ hash string
103+ date time.Time
104+ }
105+
106+ // Diff returns the slice of Entry values that represent the difference of
107+ // entries in the dir directory within repo from ref1 revision to ref2 revision.
108+ // ref1 and ref2 should be valid git refs as strings and dir should be a valid
109+ // directory path in the repository.
110+ //
111+ // The function calculates the diff by first checking out ref2 and collecting
112+ // the set of all entries in dir. It then checks out ref1 and subtracts the
113+ // entries found in dir. The resulting set of entries is then filtered to
114+ // exclude any entries that came before the commit date of ref1.
115+ //
116+ // Along the way, if any git or filesystem interactions fail, an error is returned.
117+ func Diff (repo , ref1 , ref2 , dir string ) (* EntryList , error ) {
30118 r , err := git .Clone (memory .NewStorage (), memfs .New (), & git.CloneOptions {
31119 URL : repo ,
32120 })
@@ -35,42 +123,34 @@ func Diff(repo, ref1, ref2, dir string) ([]Entry, error) {
35123 }
36124 rev2 , err := r .ResolveRevision (plumbing .Revision (ref2 ))
37125 if err != nil {
38- return nil , err
126+ return nil , fmt . Errorf ( "could not resolve revision %s: %w" , ref2 , err )
39127 }
40128 var rev1 * plumbing.Hash
41129 if ref1 != "-" {
42130 rev1 , err = r .ResolveRevision (plumbing .Revision (ref1 ))
43131 if err != nil {
44- return nil , err
132+ return nil , fmt . Errorf ( "could not resolve revision %s: %w" , ref1 , err )
45133 }
46134 }
47135 wt , err := r .Worktree ()
48136 if err != nil {
49137 return nil , err
50138 }
51- err = wt .Checkout (& git.CheckoutOptions {
139+ if err : = wt .Checkout (& git.CheckoutOptions {
52140 Hash : * rev2 ,
53141 Force : true ,
54- })
55- if err != nil {
56- return nil , err
142+ }); err != nil {
143+ return nil , fmt .Errorf ("could not checkout repository at %s: %w" , ref2 , err )
57144 }
58145 entriesAfterFI , err := wt .Filesystem .ReadDir (dir )
59146 if err != nil {
60- return nil , err
147+ return nil , fmt . Errorf ( "could not read repository directory %s: %w" , dir , err )
61148 }
62- entriesAfter := make (map [string ][]byte , len (entriesAfterFI ))
149+ // a set of all entries at rev2 (this release); the set of entries at ref1
150+ // will then be subtracted from it to arrive at a set of 'candidate' entries.
151+ entryCandidates := make (map [string ]bool , len (entriesAfterFI ))
63152 for _ , i := range entriesAfterFI {
64- f , err := wt .Filesystem .Open (filepath .Join (dir , i .Name ()))
65- if err != nil {
66- return nil , err
67- }
68- contents , err := ioutil .ReadAll (f )
69- f .Close ()
70- if err != nil {
71- return nil , err
72- }
73- entriesAfter [i .Name ()] = contents
153+ entryCandidates [i .Name ()] = true
74154 }
75155 if rev1 != nil {
76156 err = wt .Checkout (& git.CheckoutOptions {
@@ -82,22 +162,56 @@ func Diff(repo, ref1, ref2, dir string) ([]Entry, error) {
82162 }
83163 entriesBeforeFI , err := wt .Filesystem .ReadDir (dir )
84164 if err != nil {
85- return nil , err
165+ return nil , fmt . Errorf ( "could not read repository directory %s: %w" , dir , err )
86166 }
87167 for _ , i := range entriesBeforeFI {
88- delete (entriesAfter , i .Name ())
168+ delete (entryCandidates , i .Name ())
169+ }
170+ // checkout rev2 so that we can read files later
171+ if err := wt .Checkout (& git.CheckoutOptions {
172+ Hash : * rev2 ,
173+ Force : true ,
174+ }); err != nil {
175+ return nil , fmt .Errorf ("could not checkout repository at %s: %w" , ref2 , err )
89176 }
90177 }
91- entries := make ([]Entry , 0 , len (entriesAfter ))
92- for name , contents := range entriesAfter {
93- entries = append (entries , Entry {
94- Issue : name ,
95- Body : string (contents ),
178+
179+ entries := NewEntryList (len (entryCandidates ))
180+ errg := new (errgroup.Group )
181+ for name := range entryCandidates {
182+ name := name // https://golang.org/doc/faq#closures_and_goroutines
183+ errg .Go (func () error {
184+ fp := filepath .Join (dir , name )
185+ f , err := wt .Filesystem .Open (fp )
186+ if err != nil {
187+ return fmt .Errorf ("error opening file at %s: %w" , name , err )
188+ }
189+ contents , err := ioutil .ReadAll (f )
190+ f .Close ()
191+ if err != nil {
192+ return fmt .Errorf ("error reading file at %s: %w" , name , err )
193+ }
194+ log , err := r .Log (& git.LogOptions {FileName : & fp })
195+ if err != nil {
196+ return fmt .Errorf ("error fetching git log for %s: %w" , name , err )
197+ }
198+ lastChange , err := log .Next ()
199+ if err != nil {
200+ return fmt .Errorf ("error fetching next git log: %w" , err )
201+ }
202+ entries .Append (& Entry {
203+ Issue : name ,
204+ Body : string (contents ),
205+ Date : lastChange .Author .When ,
206+ Hash : lastChange .Hash .String (),
207+ })
208+ return nil
96209 })
97210 }
98- sort .Slice (entries , func (i , j int ) bool {
99- return entries [i ].Issue < entries [j ].Issue
100- })
211+ if err := errg .Wait (); err != nil {
212+ return nil , err
213+ }
214+ entries .SortByIssue ()
101215 return entries , nil
102216}
103217
0 commit comments