Ejemplo n.º 1
0
func rebase(currentBranch, parentBranch string) ([]*git.Commit, error) {
	// Tell the user what is happening.
	task := fmt.Sprintf("Rebase branch '%v' onto '%v'", currentBranch, parentBranch)
	log.Run(task)

	// Do the rebase.
	if err := git.Rebase(parentBranch); err != nil {
		ex := errs.Log(errs.NewError(task, err))
		asciiart.PrintGrimReaper("GIT REBASE FAILED")
		fmt.Printf(`Git failed to rebase your branch onto '%v'.

The repository might have been left in the middle of the rebase process.
In case you do not know how to handle this, just execute

  $ git rebase --abort

to make your repository clean again.

In any case, you have to rebase your current branch onto '%v'
if you want to continue and post a review request. In the edge cases
you can as well use -no_rebase to skip this step, but try not to do it.
`, parentBranch, parentBranch)
		return nil, ex
	}

	// Reload the commits.
	task = "Get the commits to be posted for code review, again"
	commits, err := git.ShowCommitRange(parentBranch + "..")
	if err != nil {
		return nil, errs.NewError(task, err)
	}

	// Return new commits.
	return commits, nil
}
Ejemplo n.º 2
0
func main() {
	// Set up the identification command line flag.
	hooks.IdentifyYourself()

	// Tell the user what is happening.
	fmt.Println("---> Running SalsaFlow pre-push hook")

	// The hook is always invoked as `pre-push <remote-name> <push-url>`.
	if len(os.Args) != 3 {
		fmt.Fprintf(os.Stderr, "Usage: %v <remote-name> <push-url>\n", os.Args[0])
		errs.Fatal(fmt.Errorf("invalid arguments: %#v\n", os.Args[1:]))
	}

	// Run the main function.
	if err := run(os.Args[1], os.Args[2]); err != nil {
		if err != prompt.ErrCanceled {
			fmt.Println()
			errs.Log(err)
		}
		asciiart.PrintGrimReaper("PUSH ABORTED")
		os.Exit(1)
	}

	// Insert an empty line before git push output.
	fmt.Println()
}
Ejemplo n.º 3
0
func GetByBranch(branch string) (ver *Version, err error) {
	// Remember the current branch.
	currentBranch, err := git.CurrentBranch()
	if err != nil {
		return nil, err
	}

	// Checkout the target branch.
	if err := git.Checkout(branch); err != nil {
		return nil, err
	}
	defer func() {
		// Checkout the original branch on return.
		if ex := git.Checkout(currentBranch); ex != nil {
			if err == nil {
				err = ex
			} else {
				errs.Log(ex)
			}
		}
	}()

	// Get the version.
	v, err := Get()
	if err != nil {
		if ex, ok := err.(*scripts.ErrNotFound); ok {
			return nil, fmt.Errorf(
				"custom SalsaFlow script '%v' not found on branch '%v'", ex.ScriptName(), branch)
		}
		return nil, err
	}
	return v, nil
}
Ejemplo n.º 4
0
func ResetKeep(branch, ref string) (err error) {
	// Remember the current branch.
	currentBranch, err := CurrentBranch()
	if err != nil {
		return err
	}

	// Checkout the branch to be reset.
	if err := Checkout(branch); err != nil {
		return err
	}
	defer func() {
		// Checkout the original branch on return.
		if ex := Checkout(currentBranch); ex != nil {
			if err == nil {
				err = ex
			} else {
				errs.Log(ex)
			}
		}
	}()

	// Reset the branch.
	_, err = Run("reset", "--keep", ref)
	return err
}
Ejemplo n.º 5
0
func (tool *codeReviewTool) PostReviewRequests(
	ctxs []*common.ReviewContext,
	opts map[string]interface{},
) (err error) {

	// Use PostReviewRequestForCommit for every commit on the branch.
	// Try to post a review request for every commit and keep printing the errors.
	// Return a common error in case there is any partial error encountered.
	for _, ctx := range ctxs {
		if ex := postReviewRequestForCommit(ctx, opts); ex != nil {
			log.NewLine("")
			errs.Log(ex)
			err = errors.New("failed to post a review request")
		}
	}
	return
}
Ejemplo n.º 6
0
func (chain *ActionChain) Rollback() error {
	var ex error
	for i := range chain.actions {
		act := chain.actions[len(chain.actions)-1-i]

		// Inform the user what is happening.
		if task := act.task; task != "" {
			log.Rollback(task)
		}

		// Run the rollback function registered.
		if err := act.action.Rollback(); err != nil {
			errs.Log(err)
			ex = ErrRollbackFailed
		}
	}
	return ex
}
Ejemplo n.º 7
0
func implementedDialog(ctxs []*common.ReviewContext) (implemented bool, act action.Action, err error) {
	// Collect the affected stories.
	var (
		stories  = make([]common.Story, 0, len(ctxs))
		storySet = make(map[string]struct{}, len(ctxs))
	)
	for _, ctx := range ctxs {
		story := ctx.Story
		// Skip unassigned commits.
		if story == nil {
			continue
		}
		rid := story.ReadableId()
		if _, ok := storySet[rid]; ok {
			continue
		}
		// Collect only the stories that are Being Implemented.
		// The transition doesn't make sense for other story states.
		if story.State() != common.StoryStateBeingImplemented {
			continue
		}
		storySet[rid] = struct{}{}
		stories = append(stories, story)
	}
	// Do nothing in case there are no stories left.
	if len(stories) == 0 {
		return false, nil, nil
	}

	// Prompt the user for confirmation.
	fmt.Println("\nIt is possible to mark the affected stories as implemented.")
	fmt.Println("The following stories were associated with one or more commits:\n")
	storyprompt.ListStories(stories, os.Stdout)
	fmt.Println()
	confirmed, err := prompt.Confirm(
		"Do you wish to mark these stories as implemented?", false)
	if err != nil {
		return false, nil, err
	}
	fmt.Println()
	if !confirmed {
		return false, nil, nil
	}

	// Always update as many stories as possible.
	var (
		chain           = action.NewActionChain()
		errUpdateFailed = errors.New("failed to update stories in the issue tracker")
		ex              error
	)
	for _, story := range stories {
		task := fmt.Sprintf("Mark story %v as implemented", story.ReadableId())
		log.Run(task)
		act, err := story.MarkAsImplemented()
		if err != nil {
			errs.Log(errs.NewError(task, err))
			ex = errUpdateFailed
			continue
		}
		chain.PushTask(task, act)
	}
	if ex != nil {
		if err := chain.Rollback(); err != nil {
			errs.Log(err)
		}
		return false, nil, ex
	}

	return true, chain, nil
}
Ejemplo n.º 8
0
func (tool *codeReviewTool) PostReviewRequests(
	ctxs []*common.ReviewContext,
	opts map[string]interface{},
) (err error) {

	// Load the GitHub config.
	config, err := LoadConfig()
	if err != nil {
		return err
	}

	// Get the GitHub owner and repository from the upstream URL.
	owner, repo, err := git.ParseUpstreamURL()
	if err != nil {
		return err
	}

	// Group commits by story ID.
	//
	// In case the commit is associated with a story, we add it to the relevant story group.
	// Otherwise the commit is marked as unassigned and added to the relevant list.
	var (
		ctxsByStoryId     = make(map[string][]*common.ReviewContext, 1)
		unassignedCommits = make([]*git.Commit, 0, 1)
	)
	for _, ctx := range ctxs {
		story := ctx.Story
		if story != nil {
			sid := story.ReadableId()
			rcs, ok := ctxsByStoryId[sid]
			if ok {
				rcs = append(rcs, ctx)
			} else {
				rcs = []*common.ReviewContext{ctx}
			}
			ctxsByStoryId[sid] = rcs
		} else {
			unassignedCommits = append(unassignedCommits, ctx.Commit)
		}
	}

	// Post the assigned commits.
	for _, ctxs := range ctxsByStoryId {
		var (
			story   = ctxs[0].Story
			commits = make([]*git.Commit, 0, len(ctxs))
		)
		for _, ctx := range ctxs {
			commits = append(commits, ctx.Commit)
		}
		if ex := postAssignedReviewRequest(config, owner, repo, story, commits, opts); ex != nil {
			errs.Log(ex)
			err = errPostReviewRequest
		}
	}

	// Post the unassigned commits.
	for _, commit := range unassignedCommits {
		if ex := postUnassignedReviewRequest(config, owner, repo, commit, opts); ex != nil {
			errs.Log(ex)
			err = errPostReviewRequest
		}
	}

	return
}
Ejemplo n.º 9
0
func merge(mergeTask, current, parent string) (act action.Action, err error) {
	// Remember the current branch hash.
	currentSHA, err := git.BranchHexsha(current)
	if err != nil {
		return nil, err
	}

	// Checkout the parent branch so that we can perform the merge.
	if err := git.Checkout(parent); err != nil {
		return nil, err
	}
	// Checkout the current branch on return to be consistent.
	defer func() {
		if ex := git.Checkout(current); ex != nil {
			if err == nil {
				err = ex
			} else {
				errs.Log(ex)
			}
		}
	}()

	// Perform the merge.
	// Use --no-ff in case -merge_no_ff is set.
	if flagMergeNoFF {
		err = git.Merge(current, "--no-ff")
	} else {
		err = git.Merge(current)
	}
	if err != nil {
		return nil, err
	}

	// Return a rollback action.
	return action.ActionFunc(func() (err error) {
		log.Rollback(mergeTask)
		task := fmt.Sprintf("Reset branch '%v' to the original position", current)

		// Get the branch is the current branch now.
		currentNow, err := gitutil.CurrentBranch()
		if err != nil {
			return errs.NewError(task, err)
		}

		// Checkout current in case it is not the same as the current branch now.
		if currentNow != current {
			if err := git.Checkout(current); err != nil {
				return errs.NewError(task, err)
			}
			defer func() {
				if ex := git.Checkout(currentNow); ex != nil {
					if err == nil {
						err = ex
					} else {
						errs.Log(ex)
					}
				}
			}()
		}

		// Reset the branch to the original position.
		if err := git.Reset("--keep", currentSHA); err != nil {
			return errs.NewError(task, err)
		}
		return nil
	}), nil
}
Ejemplo n.º 10
0
func postBranch(parentBranch string) (err error) {
	// Load the git-related config.
	gitConfig, err := git.LoadConfig()
	if err != nil {
		return err
	}
	var (
		remoteName = gitConfig.RemoteName
	)

	// Get the current branch name.
	currentBranch, err := gitutil.CurrentBranch()
	if err != nil {
		return err
	}

	if !flagNoFetch {
		// Fetch the remote repository.
		task := "Fetch the remote repository"
		log.Run(task)

		if err := git.UpdateRemotes(remoteName); err != nil {
			return errs.NewError(task, err)
		}
	}

	// Make sure the parent branch is up to date.
	task := fmt.Sprintf("Make sure reference '%v' is up to date", parentBranch)
	log.Run(task)
	if err := git.EnsureBranchSynchronized(parentBranch, remoteName); err != nil {
		return errs.NewError(task, err)
	}

	// Make sure the current branch is up to date.
	task = fmt.Sprintf("Make sure branch '%v' is up to date", currentBranch)
	log.Run(task)
	if err = git.EnsureBranchSynchronized(currentBranch, remoteName); err != nil {
		return errs.NewError(task, err)
	}

	// Get the commits to be posted
	task = "Get the commits to be posted for code review"
	commits, err := git.ShowCommitRange(parentBranch + "..")
	if err != nil {
		return errs.NewError(task, err)
	}

	// Make sure there are no merge commits.
	if err := ensureNoMergeCommits(commits); err != nil {
		return err
	}

	// Prompt the user to confirm.
	if err := promptUserToConfirmCommits(commits); err != nil {
		return err
	}

	// Rebase the current branch on top the parent branch.
	if !flagNoRebase {
		commits, err = rebase(currentBranch, parentBranch)
		if err != nil {
			return err
		}
	}

	// Ensure the Story-Id tag is there.
	commits, _, err = ensureStoryId(commits)
	if err != nil {
		return err
	}

	// Get data on the current branch.
	task = fmt.Sprintf("Get data on branch '%v'", currentBranch)
	remoteCurrentExists, err := git.RemoteBranchExists(currentBranch, remoteName)
	if err != nil {
		return errs.NewError(task, err)
	}
	currentUpToDate, err := git.IsBranchSynchronized(currentBranch, remoteName)
	if err != nil {
		return errs.NewError(task, err)
	}

	// Merge the current branch into the parent branch unless -no_merge.
	pushTask := "Push the current branch"
	if flagNoMerge {
		// In case the user doesn't want to merge,
		// we need to push the current branch.
		if !remoteCurrentExists || !currentUpToDate {
			if err := push(remoteName, currentBranch); err != nil {
				return errs.NewError(pushTask, err)
			}
		}
	} else {
		// Still push the current branch if necessary.
		if remoteCurrentExists && !currentUpToDate {
			if err := push(remoteName, currentBranch); err != nil {
				return errs.NewError(pushTask, err)
			}
		}

		// Merge the branch into the parent branch
		mergeTask := fmt.Sprintf("Merge branch '%v' into branch '%v'", currentBranch, parentBranch)
		log.Run(mergeTask)
		act, err := merge(mergeTask, currentBranch, parentBranch)
		if err != nil {
			return err
		}

		// Push the parent branch.
		if err := push(remoteName, parentBranch); err != nil {
			// In case the push fails, we revert the merge as well.
			if err := act.Rollback(); err != nil {
				errs.Log(err)
			}
			return errs.NewError(mergeTask, err)
		}

		// Register a rollback function that just says that
		// a pushed merge cannot be reverted.
		defer action.RollbackOnError(&err, action.ActionFunc(func() error {
			log.Rollback(mergeTask)
			hint := "\nCannot revert merge that has already been pushed.\n"
			return errs.NewErrorWithHint(
				"Revert the merge", errors.New("merge commit already pushed"), hint)
		}))
	}

	// Post the review requests.
	if err := postCommitsForReview(commits); err != nil {
		return err
	}

	// In case there is no error, tell the user they can do next.
	return printFollowup()
}
Ejemplo n.º 11
0
func (tool *codeReviewTool) PostReviewRequests(
	ctxs []*common.ReviewContext,
	opts map[string]interface{},
) (err error) {

	// Get the GitHub owner and repository from the upstream URL.
	owner, repo, err := ghutil.ParseUpstreamURL()
	if err != nil {
		return err
	}

	// Group commits by story ID.
	//
	// In case the commit is associated with a story, we add it to the relevant story group.
	// Otherwise the commit is marked as unassigned and added to the relevant list.
	var (
		ctxsByStoryId     = make(map[string][]*common.ReviewContext, 1)
		unassignedCommits = make([]*git.Commit, 0, 1)
	)
	for _, ctx := range ctxs {
		story := ctx.Story
		if story != nil {
			sid := story.ReadableId()
			rcs, ok := ctxsByStoryId[sid]
			if ok {
				rcs = append(rcs, ctx)
			} else {
				rcs = []*common.ReviewContext{ctx}
			}
			ctxsByStoryId[sid] = rcs
		} else {
			unassignedCommits = append(unassignedCommits, ctx.Commit)
		}
	}

	// Post the assigned commits.
	_, open := opts["open"]
	for _, ctxs := range ctxsByStoryId {
		var (
			story   = ctxs[0].Story
			commits = make([]*git.Commit, 0, len(ctxs))
		)
		for _, ctx := range ctxs {
			commits = append(commits, ctx.Commit)
		}
		// Create/update the review issue.
		issue, postedCommits, ex := postAssignedReviewRequest(
			tool.config, owner, repo, story, commits, opts)
		if ex != nil {
			errs.Log(ex)
			err = errPostReviewRequest
			continue
		}

		// Add comments to the commits posted for review.
		linkCommitsToReviewIssue(tool.config, owner, repo, *issue.Number, postedCommits)

		// Open the review issue in the browser if requested.
		if open {
			openIssue(issue)
		}
	}

	// Get the value of -fixes.
	var fixes int
	flagFixes, ok := opts["fixes"]
	if ok {
		if v, ok := flagFixes.(uint); ok && v != 0 {
			fixes = int(v)
		}
	}

	// Post the unassigned commits.
	for _, commit := range unassignedCommits {
		var (
			issue         *github.Issue
			postedCommits []*git.Commit
			ex            error
		)
		if fixes != 0 {
			// Extend the specified review issue.
			issue, postedCommits, ex = extendUnassignedReviewRequest(
				tool.config, owner, repo, fixes, commit, opts)
		} else {
			// Create/update the review issue.
			issue, postedCommits, ex = postUnassignedReviewRequest(
				tool.config, owner, repo, commit, opts)
		}
		if ex != nil {
			errs.Log(ex)
			err = errPostReviewRequest
			continue
		}

		// Add comments to the commits posted for review.
		linkCommitsToReviewIssue(tool.config, owner, repo, *issue.Number, postedCommits)

		// Open the review issue in the browser if requested.
		if open {
			openIssue(issue)
		}
	}

	return
}
Ejemplo n.º 12
0
func (manager *releaseNotesManager) PostReleaseNotes(
	releaseNotes *common.ReleaseNotes,
) (action.Action, error) {

	// Get the GitHub owner and repository from the upstream URL.
	owner, repo, err := github.ParseUpstreamURL()
	if err != nil {
		return nil, err
	}

	// Instantiate the API client.
	client := github.NewClient(manager.config.Token)

	// Format the release notes.
	task := "Format the release notes"
	body := bytes.NewBufferString(`
## Summary ##

**PLEASE FILL IN THE RELEASE SUMMARY**
`)
	encoder, err := notes.NewEncoder(notes.EncodingMarkdown, body)
	if err != nil {
		return nil, errs.NewError(task, err)
	}
	if err := encoder.Encode(releaseNotes, nil); err != nil {
		return nil, errs.NewError(task, err)
	}
	bodyString := body.String()

	// Create GitHub release for the given version.
	tag := releaseNotes.Version.ReleaseTagString()
	releaseTask := fmt.Sprintf("Create GitHub release for tag '%v'", tag)
	log.Run(releaseTask)
	release, _, err := client.Repositories.CreateRelease(owner, repo, &gh.RepositoryRelease{
		TagName: gh.String(tag),
		Name:    gh.String("Release " + releaseNotes.Version.BaseString()),
		Body:    &bodyString,
		Draft:   gh.Bool(true),
	})
	if err != nil {
		return nil, err
	}

	// Delete the GitHub release on rollback.
	rollback := func() error {
		log.Rollback(releaseTask)
		task := fmt.Sprintf("Delete GitHub release for tag '%v'", tag)
		_, err := client.Repositories.DeleteRelease(owner, repo, *release.ID)
		if err != nil {
			return errs.NewError(task, err)
		}
		return nil
	}

	// Open the release in the browser so that the user can fill in the details.
	task = "Open the release notes in the browser"
	if err := webbrowser.Open(*release.HTMLURL); err != nil {
		if ex := rollback(); ex != nil {
			errs.Log(ex)
		}
		return nil, errs.NewError(task, err)
	}

	// Return the rollback function.
	return action.ActionFunc(rollback), nil
}
Ejemplo n.º 13
0
func createBranch() (action.Action, error) {
	// Get the current branch name.
	originalBranch, err := gitutil.CurrentBranch()
	if err != nil {
		return nil, err
	}

	// Fetch the remote repository.
	task := "Fetch the remote repository"
	log.Run(task)

	gitConfig, err := git.LoadConfig()
	if err != nil {
		return nil, errs.NewError(task, err)
	}

	var (
		remoteName = gitConfig.RemoteName
		baseBranch = gitConfig.TrunkBranchName
	)
	if flagBase != "" {
		baseBranch = flagBase
	}

	// Fetch the remote repository.
	if err := git.UpdateRemotes(remoteName); err != nil {
		return nil, errs.NewError(task, err)
	}

	// Make sure the trunk branch is up to date.
	task = fmt.Sprintf("Make sure branch '%v' is up to date", baseBranch)
	log.Run(task)
	if err := git.CheckOrCreateTrackingBranch(baseBranch, remoteName); err != nil {
		return nil, errs.NewError(task, err)
	}

	// Prompt the user for the branch name.
	task = "Prompt the user for the branch name"
	line, err := prompt.Prompt(`
Please insert the branch slug now.
Insert an empty string to skip the branch creation step: `)
	if err != nil && err != prompt.ErrCanceled {
		return nil, errs.NewError(task, err)
	}

	sluggedLine := slug.Slug(line)
	if sluggedLine == "" {
		fmt.Println()
		log.Log("Not creating any feature branch")
		return nil, nil
	}

	branchName := "story/" + sluggedLine
	ok, err := prompt.Confirm(
		fmt.Sprintf(
			"\nThe branch that is going to be created will be called '%s'.\nIs that alright?",
			branchName),
		true)
	if err != nil {
		return nil, errs.NewError(task, err)
	}
	if !ok {
		panic(prompt.ErrCanceled)
	}
	fmt.Println()

	createTask := fmt.Sprintf(
		"Create branch '%v' on top of branch '%v'", branchName, baseBranch)
	log.Run(createTask)
	if err := git.Branch(branchName, baseBranch); err != nil {
		return nil, errs.NewError(createTask, err)
	}

	deleteTask := fmt.Sprintf("Delete branch '%v'", branchName)
	deleteBranch := func() error {
		// Roll back and delete the newly created branch.
		log.Rollback(createTask)
		if err := git.Branch("-D", branchName); err != nil {
			return errs.NewError(deleteTask, err)
		}
		return nil
	}

	// Checkout the newly created branch.
	checkoutTask := fmt.Sprintf("Checkout branch '%v'", branchName)
	log.Run(checkoutTask)
	if err := git.Checkout(branchName); err != nil {
		if err := deleteBranch(); err != nil {
			errs.Log(err)
		}
		return nil, errs.NewError(checkoutTask, err)
	}

	// Push the newly created branch unless -no_push.
	pushTask := fmt.Sprintf("Push branch '%v' to remote '%v'", branchName, remoteName)
	if flagPush {
		log.Run(pushTask)
		if err := git.Push(remoteName, branchName); err != nil {
			if err := deleteBranch(); err != nil {
				errs.Log(err)
			}
			return nil, errs.NewError(pushTask, err)
		}
	}

	return action.ActionFunc(func() error {
		// Checkout the original branch.
		log.Rollback(checkoutTask)
		if err := git.Checkout(originalBranch); err != nil {
			return errs.NewError(
				fmt.Sprintf("Checkout the original branch '%v'", originalBranch), err)
		}

		// Delete the newly created branch.
		deleteErr := deleteBranch()

		// In case we haven't pushed anything, we are done.
		if !flagPush {
			return deleteErr
		}

		// Delete the branch from the remote repository.
		log.Rollback(pushTask)
		if _, err := git.Run("push", "--delete", remoteName, branchName); err != nil {
			// In case deleteBranch failed, tell the user now
			// since we are not going to return that error.
			if deleteErr != nil {
				errs.Log(deleteErr)
			}

			return errs.NewError(
				fmt.Sprintf("Delete branch '%v' from remote '%v'", branchName, remoteName), err)
		}

		// Return deleteErr to make sure it propagates up.
		return deleteErr
	}), nil
}
Ejemplo n.º 14
0
func downloadAndInstallAsset(assetName, assetURL, dstDir string) error {
	// Download the asset.
	task := "Download " + assetName
	log.Run(task)
	resp, err := http.Get(assetURL)
	if err != nil {
		return errs.NewError(task, err)
	}
	defer resp.Body.Close()

	// Unpack the asset (in-memory).
	// We keep the asset in the memory since it is never going to be that big.
	task = "Read the asset into an internal buffer"
	var capacity = resp.ContentLength
	if capacity == -1 {
		capacity = 0
	}
	bodyBuffer := bytes.NewBuffer(make([]byte, 0, capacity))
	_, err = io.Copy(bodyBuffer, resp.Body)
	if err != nil {
		return errs.NewError(task, err)
	}

	task = "Replace SalsaFlow executables"
	archive, err := zip.NewReader(bytes.NewReader(bodyBuffer.Bytes()), int64(bodyBuffer.Len()))
	if err != nil {
		return errs.NewError(task, err)
	}

	var numThreads int
	errCh := make(chan errs.Err, len(archive.File))

	// Uncompress all the executables in the archive and move them into place.
	// This part replaces the current executables with new ones just downloaded.
	for _, file := range archive.File {
		if file.CompressedSize64 == 0 {
			continue
		}

		numThreads++

		go func(file *zip.File) {
			baseName := filepath.Base(file.Name)
			task := fmt.Sprintf("Uncompress executable '%v'", baseName)
			log.Go(task)

			src, err := file.Open()
			if err != nil {
				errCh <- errs.NewError(task, err)
				return
			}

			task = fmt.Sprintf("Move executable '%v' into place", baseName)
			log.Go(task)
			if err := replaceExecutable(src, dstDir, baseName); err != nil {
				src.Close()
				errCh <- errs.NewError(task, err)
				return
			}

			src.Close()
			errCh <- nil
		}(file)
	}

	task = "install given SalsaFlow package"
	var ex error
	for i := 0; i < numThreads; i++ {
		if err := <-errCh; err != nil {
			errs.Log(err)
			ex = errs.NewError(task, ErrInstallationFailed)
		}
	}
	return ex
}