|  | @@ -17,13 +17,23 @@ import (
 | 
	
		
			
			| 17 | 17 |  	gitssh "gopkg.in/src-d/go-git.v4/plumbing/transport/ssh"
 | 
	
		
			
			| 18 | 18 |  )
 | 
	
		
			
			| 19 | 19 |  
 | 
	
		
			
			|  | 20 | +// Repository represents a Git repository, as an abstraction layer above the
 | 
	
		
			
			|  | 21 | +// go-git library in order to also store the current configuration and the
 | 
	
		
			
			|  | 22 | +// authentication data needed to talk to the Git remote.
 | 
	
		
			
			| 20 | 23 |  type Repository struct {
 | 
	
		
			
			| 21 | 24 |  	Repo *gogit.Repository
 | 
	
		
			
			| 22 | 25 |  	cfg  config.GitSettings
 | 
	
		
			
			| 23 | 26 |  	auth *gitssh.PublicKeys
 | 
	
		
			
			| 24 | 27 |  }
 | 
	
		
			
			| 25 | 28 |  
 | 
	
		
			
			|  | 29 | +// NewRepository creates a new instance of the Repository structure and fills
 | 
	
		
			
			|  | 30 | +// it accordingly to the current configuration.
 | 
	
		
			
			|  | 31 | +// Returns a boolean if the clone path doesn't contain a valid Git repository
 | 
	
		
			
			|  | 32 | +// and needs the repository to be cloned from remote before it is usable.
 | 
	
		
			
			|  | 33 | +// Returns an error if there was an issue opening the clone path or loading
 | 
	
		
			
			|  | 34 | +// authentication data.
 | 
	
		
			
			| 26 | 35 |  func NewRepository(cfg config.GitSettings) (r *Repository, invalidRepo bool, err error) {
 | 
	
		
			
			|  | 36 | +	// Load the repository.
 | 
	
		
			
			| 27 | 37 |  	repo, err := gogit.PlainOpen(cfg.ClonePath)
 | 
	
		
			
			| 28 | 38 |  	if err != nil {
 | 
	
		
			
			| 29 | 39 |  		if err == gogit.ErrRepositoryNotExists {
 | 
	
	
		
			
			|  | @@ -33,11 +43,14 @@ func NewRepository(cfg config.GitSettings) (r *Repository, invalidRepo bool, err
 | 
	
		
			
			| 33 | 43 |  		}
 | 
	
		
			
			| 34 | 44 |  	}
 | 
	
		
			
			| 35 | 45 |  
 | 
	
		
			
			|  | 46 | +	// Fill the structure instance with the gogit.Repository instance and the
 | 
	
		
			
			|  | 47 | +	// configuration.
 | 
	
		
			
			| 36 | 48 |  	r = &Repository{
 | 
	
		
			
			| 37 | 49 |  		Repo: repo,
 | 
	
		
			
			| 38 | 50 |  		cfg:  cfg,
 | 
	
		
			
			| 39 | 51 |  	}
 | 
	
		
			
			| 40 | 52 |  
 | 
	
		
			
			|  | 53 | +	// Load authentication data in the structure instance.
 | 
	
		
			
			| 41 | 54 |  	err = r.getAuth()
 | 
	
		
			
			| 42 | 55 |  	return
 | 
	
		
			
			| 43 | 56 |  }
 | 
	
	
		
			
			|  | @@ -52,13 +65,13 @@ func NewRepository(cfg config.GitSettings) (r *Repository, invalidRepo bool, err
 | 
	
		
			
			| 52 | 65 |  // whether the clone path already exists, or synchronising the repo with the
 | 
	
		
			
			| 53 | 66 |  // remote.
 | 
	
		
			
			| 54 | 67 |  func (r *Repository) Sync(dontClone bool) (err error) {
 | 
	
		
			
			| 55 |  | -	// Check whether the clone path already exists
 | 
	
		
			
			|  | 68 | +	// Check whether the clone path already exists.
 | 
	
		
			
			| 56 | 69 |  	exists, err := dirExists(r.cfg.ClonePath)
 | 
	
		
			
			| 57 | 70 |  	if err != nil {
 | 
	
		
			
			| 58 | 71 |  		return
 | 
	
		
			
			| 59 | 72 |  	}
 | 
	
		
			
			| 60 | 73 |  
 | 
	
		
			
			| 61 |  | -	// Check whether the clone path is a Git repository
 | 
	
		
			
			|  | 74 | +	// Check whether the clone path is a Git repository.
 | 
	
		
			
			| 62 | 75 |  	var isRepo bool
 | 
	
		
			
			| 63 | 76 |  	if isRepo, err = dirExists(r.cfg.ClonePath + "/.git"); err != nil {
 | 
	
		
			
			| 64 | 77 |  		return
 | 
	
	
		
			
			|  | @@ -99,11 +112,11 @@ func (r *Repository) Push() (err error) {
 | 
	
		
			
			| 99 | 112 |  		"clone_path": r.cfg.ClonePath,
 | 
	
		
			
			| 100 | 113 |  	}).Info("Pushing to the remote")
 | 
	
		
			
			| 101 | 114 |  
 | 
	
		
			
			| 102 |  | -	// Push to remote
 | 
	
		
			
			|  | 115 | +	// Push to remote.
 | 
	
		
			
			| 103 | 116 |  	if err = r.Repo.Push(&gogit.PushOptions{
 | 
	
		
			
			| 104 | 117 |  		Auth: r.auth,
 | 
	
		
			
			| 105 | 118 |  	}); err != nil {
 | 
	
		
			
			| 106 |  | -		// Check error against known non-errors
 | 
	
		
			
			|  | 119 | +		// Check error against known non-errors.
 | 
	
		
			
			| 107 | 120 |  		err = checkRemoteErrors(err, logrus.Fields{
 | 
	
		
			
			| 108 | 121 |  			"repo":       r.cfg.User + "@" + r.cfg.URL,
 | 
	
		
			
			| 109 | 122 |  			"clone_path": r.cfg.ClonePath,
 | 
	
	
		
			
			|  | @@ -114,22 +127,31 @@ func (r *Repository) Push() (err error) {
 | 
	
		
			
			| 114 | 127 |  	return err
 | 
	
		
			
			| 115 | 128 |  }
 | 
	
		
			
			| 116 | 129 |  
 | 
	
		
			
			|  | 130 | +// GetLatestCommit retrieves the latest commit from the local Git repository and
 | 
	
		
			
			|  | 131 | +// returns it.
 | 
	
		
			
			|  | 132 | +// Returns an error if there was an issue fetching the references or loading the
 | 
	
		
			
			|  | 133 | +// latest one.
 | 
	
		
			
			| 117 | 134 |  func (r *Repository) GetLatestCommit() (*object.Commit, error) {
 | 
	
		
			
			| 118 |  | -	// Retrieve latest hash
 | 
	
		
			
			|  | 135 | +	// Retrieve the list of references from the repository.
 | 
	
		
			
			| 119 | 136 |  	refs, err := r.Repo.References()
 | 
	
		
			
			| 120 | 137 |  	if err != nil {
 | 
	
		
			
			| 121 | 138 |  		return nil, err
 | 
	
		
			
			| 122 | 139 |  	}
 | 
	
		
			
			| 123 | 140 |  
 | 
	
		
			
			|  | 141 | +	// Extract the latest reference.
 | 
	
		
			
			| 124 | 142 |  	ref, err := refs.Next()
 | 
	
		
			
			| 125 | 143 |  	if err != nil {
 | 
	
		
			
			| 126 | 144 |  		return nil, err
 | 
	
		
			
			| 127 | 145 |  	}
 | 
	
		
			
			| 128 | 146 |  
 | 
	
		
			
			|  | 147 | +	// Load the commit matching the reference's hash and return it.
 | 
	
		
			
			| 129 | 148 |  	hash := ref.Hash()
 | 
	
		
			
			| 130 | 149 |  	return r.Repo.CommitObject(hash)
 | 
	
		
			
			| 131 | 150 |  }
 | 
	
		
			
			| 132 | 151 |  
 | 
	
		
			
			|  | 152 | +// Log loads the Git repository's log, with the most recent commit having the
 | 
	
		
			
			|  | 153 | +// given hash.
 | 
	
		
			
			|  | 154 | +// Returns an error if the log couldn't be loaded.
 | 
	
		
			
			| 133 | 155 |  func (r *Repository) Log(fromHash string) (object.CommitIter, error) {
 | 
	
		
			
			| 134 | 156 |  	hash := plumbing.NewHash(fromHash)
 | 
	
		
			
			| 135 | 157 |  
 | 
	
	
		
			
			|  | @@ -138,9 +160,20 @@ func (r *Repository) Log(fromHash string) (object.CommitIter, error) {
 | 
	
		
			
			| 138 | 160 |  	})
 | 
	
		
			
			| 139 | 161 |  }
 | 
	
		
			
			| 140 | 162 |  
 | 
	
		
			
			|  | 163 | +// GetModifiedAndRemovedFiles takes to commits and returns the name of files
 | 
	
		
			
			|  | 164 | +// that were added, modified or removed between these two commits. Note that
 | 
	
		
			
			|  | 165 | +// the added/modified files and the removed files are returned in two separated
 | 
	
		
			
			|  | 166 | +// slices, mainly because some features using this function need to load the
 | 
	
		
			
			|  | 167 | +// files' contents afterwards, and this is done differently depending on whether
 | 
	
		
			
			|  | 168 | +// the file was removed or not.
 | 
	
		
			
			|  | 169 | +// "from" refers to the oldest commit of both, and "to" to the latest one.
 | 
	
		
			
			|  | 170 | +// Returns empty slices and no error if both commits have the same hash.
 | 
	
		
			
			|  | 171 | +// Returns an error if there was an issue loading the repository's log, the
 | 
	
		
			
			|  | 172 | +// commits' stats, or retrieving a file from the repository.
 | 
	
		
			
			| 141 | 173 |  func (r *Repository) GetModifiedAndRemovedFiles(
 | 
	
		
			
			| 142 | 174 |  	from *object.Commit, to *object.Commit,
 | 
	
		
			
			| 143 | 175 |  ) (modified []string, removed []string, err error) {
 | 
	
		
			
			|  | 176 | +	// Initialise the slices.
 | 
	
		
			
			| 144 | 177 |  	modified = make([]string, 0)
 | 
	
		
			
			| 145 | 178 |  	removed = make([]string, 0)
 | 
	
		
			
			| 146 | 179 |  
 | 
	
	
		
			
			|  | @@ -153,26 +186,35 @@ func (r *Repository) GetModifiedAndRemovedFiles(
 | 
	
		
			
			| 153 | 186 |  		return
 | 
	
		
			
			| 154 | 187 |  	}
 | 
	
		
			
			| 155 | 188 |  
 | 
	
		
			
			|  | 189 | +	// Iterate over the commits contained in the commit's log.
 | 
	
		
			
			| 156 | 190 |  	err = iter.ForEach(func(commit *object.Commit) error {
 | 
	
		
			
			|  | 191 | +		// If the commit was done by the manager, go to the next iteration.
 | 
	
		
			
			| 157 | 192 |  		if commit.Author.Email == r.cfg.CommitsAuthor.Email {
 | 
	
		
			
			| 158 | 193 |  			return nil
 | 
	
		
			
			| 159 | 194 |  		}
 | 
	
		
			
			| 160 | 195 |  
 | 
	
		
			
			|  | 196 | +		// If the current commit is the oldest one requested, break the loop.
 | 
	
		
			
			| 161 | 197 |  		if commit.Hash.String() == from.Hash.String() {
 | 
	
		
			
			| 162 | 198 |  			return storer.ErrStop
 | 
	
		
			
			| 163 | 199 |  		}
 | 
	
		
			
			| 164 | 200 |  
 | 
	
		
			
			|  | 201 | +		// Load stats from the current commit.
 | 
	
		
			
			| 165 | 202 |  		stats, err := commit.Stats()
 | 
	
		
			
			| 166 | 203 |  		if err != nil {
 | 
	
		
			
			| 167 | 204 |  			return err
 | 
	
		
			
			| 168 | 205 |  		}
 | 
	
		
			
			| 169 | 206 |  
 | 
	
		
			
			|  | 207 | +		// Iterate over the files contained in the commit's stats.
 | 
	
		
			
			| 170 | 208 |  		for _, stat := range stats {
 | 
	
		
			
			|  | 209 | +			// Try to access the file's content.
 | 
	
		
			
			| 171 | 210 |  			_, err := commit.File(stat.Name)
 | 
	
		
			
			| 172 | 211 |  			if err != nil && err != object.ErrFileNotFound {
 | 
	
		
			
			| 173 | 212 |  				return err
 | 
	
		
			
			| 174 | 213 |  			}
 | 
	
		
			
			| 175 | 214 |  
 | 
	
		
			
			|  | 215 | +			// If the content couldn't be retrieved, it means the file was
 | 
	
		
			
			|  | 216 | +			// removed in this commit, else it means that it was either added or
 | 
	
		
			
			|  | 217 | +			// modified.
 | 
	
		
			
			| 176 | 218 |  			if err == object.ErrFileNotFound {
 | 
	
		
			
			| 177 | 219 |  				removed = append(removed, stat.Name)
 | 
	
		
			
			| 178 | 220 |  			} else {
 | 
	
	
		
			
			|  | @@ -186,24 +228,34 @@ func (r *Repository) GetModifiedAndRemovedFiles(
 | 
	
		
			
			| 186 | 228 |  	return
 | 
	
		
			
			| 187 | 229 |  }
 | 
	
		
			
			| 188 | 230 |  
 | 
	
		
			
			|  | 231 | +// GetFilesContentsAtCommit retrieves the state of the repository at a given
 | 
	
		
			
			|  | 232 | +// commit, and returns a map contaning the contents of all files in the repository
 | 
	
		
			
			|  | 233 | +// at this time.
 | 
	
		
			
			|  | 234 | +// Returns an error if there was an issue loading the commit's tree, or loading
 | 
	
		
			
			|  | 235 | +// a file's content.
 | 
	
		
			
			| 189 | 236 |  func (r *Repository) GetFilesContentsAtCommit(commit *object.Commit) (map[string][]byte, error) {
 | 
	
		
			
			| 190 | 237 |  	var content string
 | 
	
		
			
			| 191 | 238 |  
 | 
	
		
			
			|  | 239 | +	// Load the commit's tree.
 | 
	
		
			
			| 192 | 240 |  	tree, err := commit.Tree()
 | 
	
		
			
			| 193 | 241 |  	if err != nil {
 | 
	
		
			
			| 194 | 242 |  		return nil, err
 | 
	
		
			
			| 195 | 243 |  	}
 | 
	
		
			
			| 196 | 244 |  
 | 
	
		
			
			|  | 245 | +	// Initialise the map that will be returned.
 | 
	
		
			
			| 197 | 246 |  	filesContents := make(map[string][]byte)
 | 
	
		
			
			| 198 |  | -
 | 
	
		
			
			|  | 247 | +	// Load the files from the tree.
 | 
	
		
			
			| 199 | 248 |  	files := tree.Files()
 | 
	
		
			
			| 200 | 249 |  
 | 
	
		
			
			|  | 250 | +	// Iterate over the files.
 | 
	
		
			
			| 201 | 251 |  	err = files.ForEach(func(file *object.File) error {
 | 
	
		
			
			|  | 252 | +		// Try to access the file's content at the given commit.
 | 
	
		
			
			| 202 | 253 |  		content, err = file.Contents()
 | 
	
		
			
			| 203 | 254 |  		if err != nil {
 | 
	
		
			
			| 204 | 255 |  			return err
 | 
	
		
			
			| 205 | 256 |  		}
 | 
	
		
			
			| 206 | 257 |  
 | 
	
		
			
			|  | 258 | +		// Append the content to the map.
 | 
	
		
			
			| 207 | 259 |  		filesContents[file.Name] = []byte(content)
 | 
	
		
			
			| 208 | 260 |  
 | 
	
		
			
			| 209 | 261 |  		return nil
 | 
	
	
		
			
			|  | @@ -217,11 +269,13 @@ func (r *Repository) GetFilesContentsAtCommit(commit *object.Commit) (map[string
 | 
	
		
			
			| 217 | 269 |  // Returns an error if there was an issue reading the private key file or
 | 
	
		
			
			| 218 | 270 |  // parsing it.
 | 
	
		
			
			| 219 | 271 |  func (r *Repository) getAuth() error {
 | 
	
		
			
			|  | 272 | +	// Load the private key.
 | 
	
		
			
			| 220 | 273 |  	privateKey, err := ioutil.ReadFile(r.cfg.PrivateKeyPath)
 | 
	
		
			
			| 221 | 274 |  	if err != nil {
 | 
	
		
			
			| 222 | 275 |  		return err
 | 
	
		
			
			| 223 | 276 |  	}
 | 
	
		
			
			| 224 | 277 |  
 | 
	
		
			
			|  | 278 | +	// Parse the private key.
 | 
	
		
			
			| 225 | 279 |  	signer, err := ssh.ParsePrivateKey(privateKey)
 | 
	
		
			
			| 226 | 280 |  	if err != nil {
 | 
	
		
			
			| 227 | 281 |  		return err
 | 
	
	
		
			
			|  | @@ -250,24 +304,24 @@ func (r *Repository) clone() (err error) {
 | 
	
		
			
			| 250 | 304 |  // tree or pulling from the remote. In the latter case, if the error is a known
 | 
	
		
			
			| 251 | 305 |  // non-error, doesn't return any error.
 | 
	
		
			
			| 252 | 306 |  func (r *Repository) pull() error {
 | 
	
		
			
			| 253 |  | -	// Open the repository
 | 
	
		
			
			|  | 307 | +	// Open the repository.
 | 
	
		
			
			| 254 | 308 |  	repo, err := gogit.PlainOpen(r.cfg.ClonePath)
 | 
	
		
			
			| 255 | 309 |  	if err != nil {
 | 
	
		
			
			| 256 | 310 |  		return err
 | 
	
		
			
			| 257 | 311 |  	}
 | 
	
		
			
			| 258 | 312 |  
 | 
	
		
			
			| 259 |  | -	// Get its worktree
 | 
	
		
			
			|  | 313 | +	// Get its worktree.
 | 
	
		
			
			| 260 | 314 |  	w, err := repo.Worktree()
 | 
	
		
			
			| 261 | 315 |  	if err != nil {
 | 
	
		
			
			| 262 | 316 |  		return err
 | 
	
		
			
			| 263 | 317 |  	}
 | 
	
		
			
			| 264 | 318 |  
 | 
	
		
			
			| 265 |  | -	// Pull from remote
 | 
	
		
			
			|  | 319 | +	// Pull from remote.
 | 
	
		
			
			| 266 | 320 |  	if err = w.Pull(&gogit.PullOptions{
 | 
	
		
			
			| 267 | 321 |  		RemoteName: "origin",
 | 
	
		
			
			| 268 | 322 |  		Auth:       r.auth,
 | 
	
		
			
			| 269 | 323 |  	}); err != nil {
 | 
	
		
			
			| 270 |  | -		// Check error against known non-errors
 | 
	
		
			
			|  | 324 | +		// Check error against known non-errors.
 | 
	
		
			
			| 271 | 325 |  		err = checkRemoteErrors(err, logrus.Fields{
 | 
	
		
			
			| 272 | 326 |  			"clone_path": r.cfg.ClonePath,
 | 
	
		
			
			| 273 | 327 |  			"error":      err,
 | 
	
	
		
			
			|  | @@ -301,7 +355,7 @@ func dirExists(path string) (bool, error) {
 | 
	
		
			
			| 301 | 355 |  func checkRemoteErrors(err error, logFields logrus.Fields) error {
 | 
	
		
			
			| 302 | 356 |  	var nonError bool
 | 
	
		
			
			| 303 | 357 |  
 | 
	
		
			
			| 304 |  | -	// Check against known non-errors
 | 
	
		
			
			|  | 358 | +	// Check against known non-errors.
 | 
	
		
			
			| 305 | 359 |  	switch err {
 | 
	
		
			
			| 306 | 360 |  	case gogit.NoErrAlreadyUpToDate:
 | 
	
		
			
			| 307 | 361 |  		nonError = true
 | 
	
	
		
			
			|  | @@ -314,7 +368,7 @@ func checkRemoteErrors(err error, logFields logrus.Fields) error {
 | 
	
		
			
			| 314 | 368 |  		break
 | 
	
		
			
			| 315 | 369 |  	}
 | 
	
		
			
			| 316 | 370 |  
 | 
	
		
			
			| 317 |  | -	// Log non-error
 | 
	
		
			
			|  | 371 | +	// Log non-error.
 | 
	
		
			
			| 318 | 372 |  	if nonError {
 | 
	
		
			
			| 319 | 373 |  		logrus.WithFields(logFields).Warn("Caught specific non-error")
 | 
	
		
			
			| 320 | 374 |  
 |