Exemple #1
0
func run(cmd *gocli.Command, args []string) {
	if len(args) != 0 {
		cmd.Usage()
		os.Exit(2)
	}

	upgraded, err := pkg.Upgrade(&pkg.InstallOptions{
		GitHubOwner: flagOwner,
		GitHubRepo:  flagRepo,
	})
	if err != nil {
		if err == pkg.ErrAborted {
			fmt.Println("\nYour wish is my command, exiting now!")
			return
		}
		errs.Fatal(err)
	}

	if upgraded {
		log.Log("SalsaFlow was upgraded successfully")
	} else {
		log.Log("SalsaFlow is up to date")
		asciiart.PrintThumbsUp()
		fmt.Println()
	}
}
// extendReviewRequest is a general function that can be used to extend
// the given review issue with the given list of commits.
func extendReviewRequest(
	config Config,
	owner string,
	repo string,
	issue *github.Issue,
	commits []*git.Commit,
	opts map[string]interface{},
) error {

	var (
		issueNum     = *issue.Number
		issueBody    = *issue.Body
		bodyBuffer   = bytes.NewBufferString(issueBody)
		addedCommits = make([]*git.Commit, 0, len(commits))
	)

	for _, commit := range commits {
		// Make sure the commit is not added yet.
		commitString := fmt.Sprintf("] %v: %v", commit.SHA, commit.MessageTitle)
		if strings.Contains(issueBody, commitString) {
			log.Log(fmt.Sprintf("Commit %v already listed in issue #%v", commit.SHA, issueNum))
			continue
		}

		// Extend the issue body.
		addedCommits = append(addedCommits, commit)
		fmt.Fprintf(bodyBuffer, "\n- [ ] %v: %v", commit.SHA, commit.MessageTitle)
	}

	if len(addedCommits) == 0 {
		log.Log(fmt.Sprintf("All commits already listed in issue #%v", issueNum))
		return nil
	}

	// Edit the issue.
	task := fmt.Sprintf("Update GitHub issue #%v", issueNum)
	log.Run(task)

	client := ghutil.NewClient(config.Token())
	newIssue, _, err := client.Issues.Edit(owner, repo, issueNum, &github.IssueRequest{
		Body:  github.String(bodyBuffer.String()),
		State: github.String("open"),
	})
	if err != nil {
		return errs.NewError(task, err)
	}

	// Add the review comment.
	if err := addReviewComment(config, owner, repo, issueNum, addedCommits); err != nil {
		return err
	}

	// Open the issue if requested.
	if _, open := opts["open"]; open {
		return openIssue(newIssue)
	}
	return nil
}
Exemple #3
0
func CreateTrackingBranch(branch, remote string) error {
	if err := Branch(branch, remote+"/"+branch); err != nil {
		return err
	}
	log.Log(fmt.Sprintf("Local branch '%v' created (tracking %v/%v)", branch, remote, branch))
	return nil
}
func (tool *codeReviewTool) InitialiseRelease(v *version.Version) (action.Action, error) {
	// Get necessary config.
	config, err := LoadConfig()
	if err != nil {
		return nil, err
	}

	owner, repo, err := git.ParseUpstreamURL()
	if err != nil {
		return nil, err
	}

	// Check whether the review milestone exists or not.
	// People can create milestones manually, so this makes the thing more robust.
	task := fmt.Sprintf("Check whether GitHub review milestone exists for release %v", v.BaseString())
	log.Run(task)
	milestone, err := milestoneForVersion(config, owner, repo, v)
	if err != nil {
		return nil, errs.NewError(task, err)
	}
	if milestone != nil {
		// Milestone already exists, we are done.
		log.Log(fmt.Sprintf("GitHub review milestone '%v' already exists", milestoneTitle(v)))
		return nil, nil
	}

	// Create the review milestone.
	_, act, err := createMilestone(config, owner, repo, v)
	return act, err
}
Exemple #5
0
// CheckOrCreateTrackingBranch tries to make sure that a local branch
// of the given name exists and is in sync with the given remote.
//
// So, in case the right remote branch exists and the local does not,
// the local tracking branch is created. In case the local branch
// exists already, it is ensured that it is up to date.
func CheckOrCreateTrackingBranch(branch, remote string) error {
	// Check whether the remote counterpart exists.
	exists, err := RemoteBranchExists(branch, remote)
	if err != nil {
		return err
	}
	if !exists {
		return &ErrRefNotFound{remote + "/" + branch}
	}

	// Check whether the local branch exists.
	exists, err = LocalBranchExists(branch)
	if err != nil {
		return err
	}
	if !exists {
		if err := Branch(branch, remote+"/"+branch); err != nil {
			return err
		}
		log.Log(fmt.Sprintf("Local branch '%v' created (tracking %v/%v)", branch, remote, branch))
		return nil
	}

	// In case it exists, make sure that it is up to date.
	return EnsureBranchSynchronized(branch, remote)
}
func (tool *codeReviewTool) PostReviewRequests(
	ctxs []*common.ReviewContext,
	opts map[string]interface{},
) error {
	log.Log("NOOP code review module active, not doing anything")
	return nil
}
func createIssue(
	task string,
	config Config,
	owner string,
	repo string,
	issueTitle string,
	issueBody string,
	milestone *github.Milestone,
) (issue *github.Issue, err error) {

	log.Run(task)
	client := ghutil.NewClient(config.Token())
	labels := []string{config.ReviewLabel()}
	issue, _, err = client.Issues.Create(owner, repo, &github.IssueRequest{
		Title:     github.String(issueTitle),
		Body:      github.String(issueBody),
		Labels:    &labels,
		Milestone: milestone.Number,
	})
	if err != nil {
		return nil, errs.NewError(task, err)
	}

	log.Log(fmt.Sprintf("GitHub issue #%v created", *issue.Number))
	return issue, nil
}
Exemple #8
0
// copyHook installs the SalsaFlow git hook by copying the hook executable
// from the expected absolute path to the git config hook directory.
func copyHook(hookType HookType, hookExecutable, hookDestPath string) error {
	task := fmt.Sprintf("Install the SalsaFlow git %v hook", hookType)
	if err := fileutil.CopyFile(hookExecutable, hookDestPath); err != nil {
		return errs.NewError(task, err)
	}
	if err := os.Chmod(hookDestPath, 0750); err != nil {
		return errs.NewError(task, err)
	}
	log.Log(fmt.Sprintf("SalsaFlow git %v hook installed", hookType))
	return nil
}
Exemple #9
0
func tryToStageRunningRelease() {
	stagingTask := "Try to stage the next release for acceptance"
	log.Run(stagingTask)
	_, err := commands.Stage(&commands.StageOptions{
		SkipFetch: true,
	})
	if err != nil {
		// Not terribly pretty, but it works. We just handle the known errors and continue.
		// It is ok when the release branch does not exist yet or the release cannot be staged
		// in the issue tracker.
		rootCause := errs.RootCause(err)
		if ex, ok := rootCause.(*git.ErrRefNotFound); ok {
			log.Log(fmt.Sprintf("Git reference '%v' not found, not staging any release", ex.Ref))
		} else if rootCause == common.ErrNotStageable {
			log.Log("The next release cannot be staged yet, staging canceled")
		} else {
			errs.LogError(stagingTask, err)
			log.Log("Failed to stage the next release, continuing anyway ...")
		}
	}
}
Exemple #10
0
func ensureStoryId(commits []*git.Commit) ([]*git.Commit, bool, error) {
	task := "Make sure the commits comply with the rules"
	if i, missing := isStoryIdMissing(commits); missing {
		commits, err := rewriteCommits(commits, i)
		if err != nil {
			return nil, false, errs.NewError(task, err)
		}
		return commits, true, nil
	} else {
		log.Log("Commit check passed")
		return commits, false, nil
	}
}
Exemple #11
0
func run(cmd *gocli.Command, args []string) {
	if len(args) != 0 {
		cmd.Usage()
		os.Exit(2)
	}

	if err := app.Init(flagForce); err != nil {
		if errs.RootCause(err) == repo.ErrInitialised {
			log.Log("The repository has been already initialised")
			return
		}
		errs.Fatal(err)
	}
}
Exemple #12
0
func run(cmd *gocli.Command, args []string) {
	if len(args) != 1 {
		cmd.Usage()
		os.Exit(2)
	}

	if err := runMain(args[0]); err != nil {
		if err == pkg.ErrAborted {
			fmt.Println("\nYour wish is my command, exiting now!")
			return
		}
		errs.Fatal(err)
	}

	log.Log("SalsaFlow was installed successfully")
}
Exemple #13
0
func run(cmd *gocli.Command, args []string) {
	if len(args) != 0 {
		cmd.Usage()
		os.Exit(2)
	}

	app.InitOrDie()

	if err := pkg.Upgrade(&pkg.InstallOptions{flagOwner, flagRepo}); err != nil {
		if err == pkg.ErrAborted {
			fmt.Println("\nYour wish is my command, exiting now!")
			return
		}
		errs.Fatal(err)
	}

	log.Log("SalsaFlow was upgraded successfully")
}
Exemple #14
0
func postCommitsForReview(commits []*git.Commit) error {
	// Pick the commits to be posted for review.
	if flagPick {
		task := "Select the commits to be posted for review"
		var err error
		commits, err = selectCommitsForReview(commits)
		if err != nil {
			return errs.NewError(task, err)
		}

		if len(commits) == 0 {
			log.NewLine("")
			log.Log("No commits selected, aborting...")
			prompt.PanicCancel()
		}
	}

	// Print Snoopy.
	asciiart.PrintSnoopy()

	// Turn Commits into ReviewContexts.
	task := "Fetch stories for the commits to be posted for review"
	log.Run(task)
	ctxs, err := commitsToReviewContexts(commits)
	if err != nil {
		return errs.NewError(task, err)
	}

	// Mark the stories as implemented, potentially.
	task = "Mark the stories as implemented, optionally"
	implemented, act, err := implementedDialog(ctxs)
	if err != nil {
		return errs.NewError(task, err)
	}
	defer action.RollbackTaskOnError(&err, task, act)

	// Post the review requests.
	task = "Post the review requests"
	if err := sendReviewRequests(ctxs, implemented); err != nil {
		return errs.NewError(task, err)
	}

	return nil
}
func createIssue(
	task string,
	config *moduleConfig,
	owner string,
	repo string,
	issueTitle string,
	issueBody string,
	assignee string,
	milestone *github.Milestone,
	implemented bool,
) (issue *github.Issue, err error) {

	log.Run(task)
	client := ghutil.NewClient(config.Token)

	var labels []string
	if implemented {
		labels = []string{config.ReviewLabel, config.StoryImplementedLabel}
	} else {
		labels = []string{config.ReviewLabel}
	}

	var assigneePtr *string
	if assignee != "" {
		assigneePtr = &assignee
	}

	issue, _, err = client.Issues.Create(owner, repo, &github.IssueRequest{
		Title:     github.String(issueTitle),
		Body:      github.String(issueBody),
		Labels:    &labels,
		Assignee:  assigneePtr,
		Milestone: milestone.Number,
	})
	if err != nil {
		return nil, errs.NewError(task, err)
	}

	log.Log(fmt.Sprintf("GitHub issue #%v created", *issue.Number))
	return issue, nil
}
Exemple #16
0
func runMain(cmd *gocli.Command) (err error) {
	// Validate CL flags.
	task := "Check the command line flags"
	switch {
	case flagSkeleton == "" && !flagNoSkeleton:
		cmd.Usage()
		return errs.NewError(
			task, errors.New("-no_skeleton must be specified when no skeleton is given"))

	case flagSkeletonOnly && flagSkeleton == "":
		cmd.Usage()
		return errs.NewError(
			task, errors.New("-skeleton must be specified when -skeleton_only is set"))
	}

	// Make sure the local config directory exists.
	act, err := ensureLocalConfigDirectoryExists()
	if err != nil {
		return err
	}
	defer action.RollbackOnError(&err, act)

	// Set up the global and local configuration file unless -skeleton_only.
	if !flagSkeletonOnly {
		if err := assembleAndWriteConfig(); err != nil {
			return err
		}
	}

	// Install the skeleton into the local config directory if desired.
	if skeleton := flagSkeleton; skeleton != "" {
		if err := getAndPourSkeleton(skeleton); err != nil {
			return err
		}
	}

	fmt.Println()
	log.Log("Successfully bootstrapped the repository for SalsaFlow")
	log.NewLine("Do not forget to commit modified configuration files!")
	return nil
}
Exemple #17
0
func GetOrCreateMilestoneForTitle(
	client *github.Client,
	owner string,
	repo string,
	title string,
) (*github.Milestone, action.Action, error) {

	// Try to get the milestone.
	milestone, err := FindMilestoneByTitle(client, owner, repo, title)
	if err != nil {
		return nil, nil, err
	}
	if milestone != nil {
		// Milestone found, return it.
		log.Log(fmt.Sprintf("GitHub milestone '%v' already exists", title))
		return milestone, nil, nil
	}

	// Create the milestone when not found.
	return CreateMilestone(client, owner, repo, title)
}
Exemple #18
0
func push(remote, branch string) error {
	task := fmt.Sprintf("Push branch '%v' to remote '%v'", branch, remote)

	msg := fmt.Sprintf("Pushing branch '%v' to synchronize", branch)
	isCore, err := git.IsCoreBranch(branch)
	if err != nil {
		return errs.NewError(task, err)
	}

	if !isCore {
		msg += " (using force)"
	}
	log.Log(msg)

	if isCore {
		err = git.Push(remote, branch)
	} else {
		err = git.PushForce(remote, branch)
	}
	if err != nil {
		return errs.NewError(task, err)
	}
	return nil
}
Exemple #19
0
func runMain() (err error) {
	// Load repo config.
	gitConfig, err := git.LoadConfig()
	if err != nil {
		return err
	}

	var (
		remoteName    = gitConfig.RemoteName
		stagingBranch = gitConfig.StagingBranchName
		stableBranch  = gitConfig.StableBranchName
	)

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

	// Check branches.
	checkBranch := func(branchName string) error {
		// Make sure the branch exists.
		task := fmt.Sprintf("Make sure that branch '%v' exists and is up to date", branchName)
		log.Run(task)
		if err := git.CheckOrCreateTrackingBranch(branchName, remoteName); err != nil {
			return errs.NewError(task, err)
		}

		// Make sure we are not on the branch.
		task = fmt.Sprintf("Make sure that branch '%v' is not checked out", branchName)
		log.Run(task)
		currentBranch, err := gitutil.CurrentBranch()
		if err != nil {
			return errs.NewError(task, err)
		}
		if currentBranch == branchName {
			err := fmt.Errorf("cannot deploy while on branch '%v'", branchName)
			return errs.NewError(task, err)
		}
		return nil
	}

	for _, branch := range []string{stableBranch, stagingBranch} {
		if err := checkBranch(branch); err != nil {
			return err
		}
	}

	// Make sure the current staging branch can be released.
	task := fmt.Sprintf("Make sure that branch '%v' can be released", stagingBranch)
	log.Run(task)
	tracker, err := modules.GetIssueTracker()
	if err != nil {
		return errs.NewError(task, err)
	}
	codeReviewTool, err := modules.GetCodeReviewTool()
	if err != nil {
		return errs.NewError(task, err)
	}

	stagingVersion, err := version.GetByBranch(stagingBranch)
	if err != nil {
		return errs.NewError(task, err)
	}

	// Make sure the release can be closed in the issue tracker.
	issueTrackerRelease := tracker.RunningRelease(stagingVersion)
	if err := issueTrackerRelease.EnsureClosable(); err != nil {
		return err
	}

	// Make sure the release can be closed in the code review tool.
	codeReviewRelease := codeReviewTool.NewRelease(stagingVersion)
	if err := codeReviewRelease.EnsureClosable(); err != nil {
		return err
	}

	// Reset the stable branch to point to stage.
	task = fmt.Sprintf("Reset branch '%v' to point to branch '%v'", stableBranch, stagingBranch)
	log.Run(task)
	act, err := git.CreateOrResetBranch(stableBranch, stagingBranch)
	if err != nil {
		return errs.NewError(task, err)
	}
	defer action.RollbackTaskOnError(&err, task, act)

	// Bump version for the stable branch.
	stableVersion, err := stagingVersion.ToStableVersion()
	if err != nil {
		return err
	}

	task = fmt.Sprintf("Bump version (branch '%v' -> %v)", stableBranch, stableVersion)
	log.Run(task)
	act, err = version.SetForBranch(stableVersion, stableBranch)
	if err != nil {
		return errs.NewError(task, err)
	}
	defer action.RollbackTaskOnError(&err, task, act)

	// Tag the stable branch.
	tag := stableVersion.ReleaseTagString()
	task = fmt.Sprintf("Tag branch '%v' with tag '%v'", stableBranch, tag)
	log.Run(task)
	if err := git.Tag(tag, stableBranch); err != nil {
		return errs.NewError(task, err)
	}
	defer action.RollbackTaskOnError(&err, task, action.ActionFunc(func() error {
		task := fmt.Sprintf("Delete tag '%v'", tag)
		if err := git.Tag("-d", tag); err != nil {
			return errs.NewError(task, err)
		}
		return nil
	}))

	// Generate the release notes.
	// We try to do as much as possible before pushing.
	task = fmt.Sprintf("Generate release notes for version '%v'", stableVersion)
	log.Run(task)
	rnm, err := modules.GetReleaseNotesManager()
	if err != nil {
		if _, ok := err.(*modules.ErrModuleNotSet); !ok {
			return errs.NewError(task, err)
		}
	}
	var nts *common.ReleaseNotes
	// rnm will be nil in case the module is disabled.
	if rnm != nil {
		// Get the relevant stories.
		stories, err := tracker.ListStoriesByRelease(stableVersion)
		if err != nil {
			return errs.NewError(task, err)
		}
		// Generate the release notes.
		nts = notes.GenerateReleaseNotes(stableVersion, stories)
	} else {
		log.Log("Release notes module disabled, not doing anything")
	}

	// Close the release in the issue tracker.
	act, err = issueTrackerRelease.Close()
	if err != nil {
		return err
	}
	defer action.RollbackOnError(&err, act)

	// Close the release in the code review tool.
	act, err = codeReviewRelease.Close()
	if err != nil {
		return err
	}
	defer action.RollbackOnError(&err, act)

	// Push the changes to the remote repository.
	task = "Push changes to the remote repository"
	log.Run(task)
	toPush := []string{
		"--tags",
		fmt.Sprintf("%v:%v", stableBranch, stableBranch),
	}
	if err := git.PushForce(remoteName, toPush...); err != nil {
		return errs.NewError(task, err)
	}

	// Post the release notes.
	task = fmt.Sprintf("Post the release notes for version '%v'", stableVersion)
	if rnm != nil {
		log.Run(task)
		if _, err := rnm.PostReleaseNotes(nts); err != nil {
			errs.LogError(task, err)
			log.Warn("Failed to post the release notes, continuing anyway ...")
		}
	}

	// Tell the user we succeeded.
	color.Green("\n-----> Release %v deployed successfully!\n\n", stableVersion)
	color.Cyan("Let's check whether the next release branch can be staged already.\n")
	color.Cyan("In other words, we will try to run `release stage` and see what happens.\n\n")

	// Now we proceed to the staging step. We do not roll back
	// the previous changes on error since this is a separate step.
	tryToStageRunningRelease()
	return nil
}
Exemple #20
0
// EnsureBranchSynchronized makes sure the given branch is up to date.
// If that is not the case, *ErrRefNoInSync is returned.
func EnsureBranchSynchronized(branch, remote string) (err error) {
	// Check whether the remote counterpart actually exists.
	exists, err := RemoteBranchExists(branch, remote)
	if err != nil {
		return err
	}
	if !exists {
		return nil
	}

	// Get the data needed.
	var (
		localRef  = fmt.Sprintf("refs/heads/%v", branch)
		remoteRef = fmt.Sprintf("refs/remotes/%v/%v", remote, branch)
	)
	localHexsha, err := Hexsha(localRef)
	if err != nil {
		return err
	}
	remoteHexsha, err := Hexsha(remoteRef)
	if err != nil {
		return err
	}
	if localHexsha == remoteHexsha {
		// The branch is up to date, we are done here.
		return nil
	}

	// Check whether the local branch can be fast-forwarded.
	remoteBranch := fmt.Sprintf("%v/%v", remote, branch)
	_, err = Run("merge-base", "--is-ancestor", branch, remoteBranch)
	if err != nil {
		// --is-ancestor returns exit status 0 on true, 1 on false, some other on error.
		// We cannot check the value in a platform-independent way, but we count on the fact that
		// stderr will be non-empty on error.
		ex, ok := err.(errs.Err)
		if !ok || len(ex.Hint()) != 0 {
			// In case err is not implementing errs.Err or len(stderr) != 0, we return the error.
			return err
		}
		// Otherwise the error means that --is-ancestor returned false,
		// so we cannot fast-forward and we have to return an error.
		return &ErrRefNotInSync{branch}
	}

	// Perform a fast-forward merge.
	// Ask the user before doing so.
	fmt.Println()
	fmt.Printf("Branch '%v' is behind '%v', and can be fast-forwarded.\n", branch, remoteBranch)
	proceed, err := prompt.Confirm("Shall we perform the merge? It's all safe!", true)
	fmt.Println()
	if err != nil {
		return err
	}
	if !proceed {
		return &ErrRefNotInSync{branch}
	}

	// Make sure the right branch is checked out.
	currentBranch, err := gitutil.CurrentBranch()
	if err != nil {
		return err
	}
	if branch != currentBranch {
		// Checkout the branch to be merged.
		task := fmt.Sprintf("Checkout branch '%v'", branch)
		if err := Checkout(branch); err != nil {
			return errs.NewError(task, err)
		}
		defer func() {
			// Checkout the original branch on return.
			task := fmt.Sprintf("Checkout branch '%v'", currentBranch)
			if ex := Checkout(currentBranch); ex != nil {
				if err == nil {
					err = ex
				} else {
					errs.LogError(task, err)
				}
			}
		}()
	}

	// Merge. Use --ff-only, just to be sure.
	// But we have already checked that this will be a fast-forward merge.
	_, err = Run("merge", "--ff-only", remoteBranch)
	if err != nil {
		return err
	}

	log.Log(fmt.Sprintf("Branch '%v' fast-forwarded onto '%v'", branch, remoteBranch))
	return nil
}
Exemple #21
0
func runMain() error {
	// Load config.
	config, err := git.LoadConfig()
	if err != nil {
		return err
	}
	var (
		remoteName = config.RemoteName
		trunkName  = config.TrunkBranchName
	)

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

	// Collect the story branches.
	task = "Collect the story branches"
	log.Run(task)
	storyBranches, err := collectStoryBranches(remoteName)
	if err != nil {
		return errs.NewError(task, err)
	}

	// Split the branches that are not up to date.
	task = "Split the branches that are not up to date"
	log.Run(task)
	storyBranches, err = splitBranchesNotInSync(storyBranches)
	if err != nil {
		return errs.NewError(task, err)
	}

	// Filter branches according to the story state.
	task = "Filter branches according to the story state"
	log.Run(task)
	filteredBranches, err := filterBranches(storyBranches, trunkName)
	if err != nil {
		return errs.NewError(task, err)
	}
	if len(storyBranches) == 0 {
		log.Log("No branches left to be deleted")
		return nil
	}

	// Prompt the user to choose what branches to delete.
	task = "Prompt the user to choose what branches to delete"
	localToDelete, remoteToDelete, err := promptUserToChooseBranches(filteredBranches)
	if err != nil {
		return errs.NewError(task, err)
	}

	// Delete chosen local branches.
	if len(localToDelete) != 0 {
		task := "Delete chosen local branches"
		log.Run(task)
		args := make([]string, 1, 1+len(localToDelete))
		args[0] = "-D"
		args = append(args, localToDelete...)
		if ex := git.Branch(args...); ex != nil {
			errs.LogError(task, ex)
			err = errors.New("failed to delete local branches")
		}
	}

	// Delete chosen remote branches.
	if len(remoteToDelete) != 0 {
		task := "Delete chosen remote branches"
		log.Run(task)
		args := make([]string, 1, 1+len(remoteToDelete))
		args[0] = "--delete"
		args = append(args, remoteToDelete...)
		if ex := git.Push(remoteName, args...); ex != nil {
			errs.LogError(task, ex)
			err = errors.New("failed to delete remote branches")
		}
	}

	return err
}
Exemple #22
0
func load(args *loadArgs) error {
	// Save the args into regular variables.
	var (
		configKind     = args.configKind
		configKey      = args.configKey
		container      = args.configContainer
		readConfig     = args.readConfig
		emptyConfig    = args.emptyConfig
		disallowPrompt = args.disallowPrompt
	)

	// Do nothing in case the container is nil.
	if container == nil {
		return nil
	}

	// Read the configuration file.
	configFile, err := readConfig()
	if err != nil {
		if emptyConfig == nil || !os.IsNotExist(errs.RootCause(err)) {
			return err
		}
		// In case the file is not there, initialise a new one.
		configFile = emptyConfig()
	}

	prompt := func(err error) error {
		if disallowPrompt {
			dialogTask := "Prompt the user for configuration according to the spec"
			dialogErr := errors.New("configuration dialog is disabled")
			dialogHint := `
The configuration dialog for the local configuration file can only be run
during 'repo bootstrap' so that the configuration file is not modified
without anybody noticing in the middle of other work being done.

Please fix the issues manually, either by manually editing the local
configuration file or by re-running 'repo bootstrap' command.

Don't forget to commit the changes.

`
			if ex, ok := err.(errs.Err); ok {
				err := errs.NewErrorWithHint(dialogTask, dialogErr, dialogHint)
				return errs.NewErrorWithHint(ex.Task(), err, ex.Hint())
			}
			return errs.NewErrorWithHint(dialogTask, err, dialogHint)
		}
		return promptAndWrite(configFile, args)
	}

	// Find the config record for the given key.
	// In case there is an error and the prompt is allowed, prompt the user.
	section, err := configFile.ConfigRecord(configKey)
	if err != nil {
		return prompt(err)
	}

	// Unmarshal the record according to the spec.
	// In case there is an error and the prompt is allowed, prompt the user.
	if err := unmarshal(section.RawConfig, container); err != nil {
		fmt.Println()
		log.Log(fmt.Sprintf(
			"Failed to unmarshal %v configuration, will try to run the bootstrap dialog",
			configKind))
		log.NewLine(fmt.Sprintf("(err = %v)", err.Error()))
		return prompt(err)
	}

	// Validate the returned object according to the spec.
	// In case there is an error and the prompt is allowed, prompt the user.
	if err := validate(container, section.Path()); err != nil {
		fmt.Println()
		log.Log(fmt.Sprintf(
			"%v configuration section invalid, will try to run the bootstrap dialog",
			strings.Title(configKind)))
		log.NewLine(fmt.Sprintf("(error = %v)", err.Error()))
		return prompt(err)
	}

	return nil
}
Exemple #23
0
func runMain() (err error) {
	tracker, err := modules.GetIssueTracker()
	if err != nil {
		return err
	}

	// Fetch stories from the issue tracker.
	task := "Fetch stories from the issue tracker"
	log.Run(task)
	stories, err := tracker.StartableStories()
	if err != nil {
		return errs.NewError(task, err)
	}
	if len(stories) == 0 {
		return errs.NewError(task, errors.New("no startable stories found"))
	}

	// Filter out the stories that are not relevant,
	// i.e. not owned by the current user or assigned to someone else.
	task = "Fetch the current user record from the issue tracker"
	user, err := tracker.CurrentUser()
	if err != nil {
		return errs.NewError(task, err)
	}

	var filteredStories []common.Story
StoryLoop:
	for _, story := range stories {
		assignees := story.Assignees()
		// Include the story in case there is no assignee set yet.
		if len(assignees) == 0 {
			filteredStories = append(filteredStories, story)
			continue StoryLoop
		}
		// Include the story in case the current user is assigned.
		for _, assignee := range assignees {
			if assignee.Id() == user.Id() {
				filteredStories = append(filteredStories, story)
				continue StoryLoop
			}
		}
	}
	stories = filteredStories

	// Prompt the user to select a story.
	story, err := dialog(
		"\nYou can start working on one of the following stories:", stories)
	if err != nil {
		switch err {
		case prompt.ErrNoStories:
			return errors.New("no startable stories found")
		case prompt.ErrCanceled:
			prompt.PanicCancel()
		default:
			return err
		}
	}
	fmt.Println()

	// Create the story branch, optionally.
	if flagNoBranch {
		log.Log("Not creating any feature branch")
	} else {
		var act action.Action
		act, err = createBranch()
		if err != nil {
			return err
		}
		// Roll back on error.
		defer action.RollbackTaskOnError(&err, task, act)
	}

	// Add the current user to the list of story assignees.
	task = "Amend the list of story assignees"
	log.Run(task)
	originalAssignees := story.Assignees()
	if err := story.AddAssignee(user); err != nil {
		return errs.NewError(task, err)
	}
	defer action.RollbackTaskOnError(&err, task, action.ActionFunc(func() error {
		task := "Reset the list of story assignees"
		if err := story.SetAssignees(originalAssignees); err != nil {
			return errs.NewError(task, err)
		}
		return nil
	}))

	// Start the selected story. No need to roll back.
	task = "Start the selected story"
	log.Run(task)
	return errs.Wrap(task, story.Start())
}
Exemple #24
0
func Upgrade(opts *InstallOptions) error {
	// Get GitHub owner and repository names.
	var (
		owner = DefaultGitHubOwner
		repo  = DefaultGitHubRepo
	)
	if opts != nil {
		if opts.GitHubOwner != "" {
			owner = opts.GitHubOwner
		}
		if opts.GitHubRepo != "" {
			repo = opts.GitHubRepo
		}
	}

	// Instantiate a GitHub client.
	task := "Instantiate a GitHub client"
	client, err := newGitHubClient()
	if err != nil {
		return errs.NewError(task, err)
	}

	// Fetch the list of available GitHub releases.
	task = fmt.Sprintf("Fetch GitHub releases for %v/%v", owner, repo)
	log.Run(task)
	releases, _, err := client.Repositories.ListReleases(owner, repo, nil)
	if err != nil {
		return errs.NewError(task, err)
	}

	// Sort the releases by version and get the most recent release.
	task = "Select the most suitable GitHub release"
	var rs releaseSlice
	for i, release := range releases {
		// Skip drafts and pre-releases.
		if *release.Draft || *release.Prerelease {
			continue
		}
		// We expect the tag to be "v" + semver version string.
		version, err := version.Parse((*release.TagName)[1:])
		if err != nil {
			log.Warn(fmt.Sprintf("Tag format invalid for '%v', skipping...", release.TagName))
			continue
		}
		// Append the release to the list of releases.
		rs = append(rs, &githubRelease{
			version:  version,
			resource: &releases[i],
		})
	}
	if rs.Len() == 0 {
		return errs.NewError(task, errors.New("no suitable GitHub releases found"))
	}

	sort.Sort(rs)
	release := rs[len(rs)-1]

	// Make sure the selected release is more recent than this executable.
	currentVersion, err := version.Parse(metadata.Version)
	if err != nil {
		panic(err)
	}
	if release.version.String() == metadata.Version || release.version.LT(currentVersion.Version) {
		log.Log("SalsaFlow is up to date")
		asciiart.PrintThumbsUp()
		fmt.Println()
		return nil
	}

	// Prompt the user to confirm the upgrade.
	task = "Prompt the user to confirm upgrade"
	fmt.Println()
	confirmed, err := prompt.Confirm(fmt.Sprintf(
		"SalsaFlow version %v is available. Upgrade now?", release.version))
	if err != nil {
		return errs.NewError(task, err)
	}
	if !confirmed {
		return ErrAborted
	}
	fmt.Println()

	// Proceed to actually install the executables.
	return doInstall(client, owner, repo, release.resource.Assets, release.version.String())
}
Exemple #25
0
func runMain() error {
	// Load git-related config.
	gitConfig, err := git.LoadConfig()
	if err != nil {
		return err
	}
	var (
		remoteName    = gitConfig.RemoteName
		trunkBranch   = gitConfig.TrunkBranchName
		releaseBranch = gitConfig.ReleaseBranchName
	)

	// Fetch the remote repository unless explicitly skipped.
	if !flagNoFetch {
		task := "Fetch the remote repository"
		log.Run(task)
		if err := git.UpdateRemotes(remoteName); err != nil {
			return errs.NewError(task, err)
		}
	}

	log.Run("Make sure all important branches are up to date")

	// Check branches.
	checkBranch := func(branchName string) error {
		task := fmt.Sprintf("Make sure that branch '%v' exists and is up to date", branchName)
		if err := git.CheckOrCreateTrackingBranch(branchName, remoteName); err != nil {
			return errs.NewError(task, err)
		}
		return nil
	}

	for _, branch := range [...]string{releaseBranch, trunkBranch} {
		if err := checkBranch(branch); err != nil {
			return err
		}
	}

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

	// Checkout the release branch.
	task := "Checkout the release branch"
	if err := git.Checkout(releaseBranch); err != nil {
		return errs.NewError(task, err)
	}
	defer func() {
		// Do not checkout the original branch in case the name is empty.
		// This is later used to disable the checkout of the original branch.
		if currentBranch == "" {
			return
		}
		// Otherwise checkout the original branch.
		task := fmt.Sprintf("Checkout the original branch (%v)", currentBranch)
		if err := git.Checkout(currentBranch); err != nil {
			errs.LogError(task, err)
		}
	}()

	// Get the current release version string.
	// It is enough to just call version.Get since
	// we are already on the release branch.
	task = "Get the release branch version string"
	releaseVersion, err := version.Get()
	if err != nil {
		return errs.NewError(task, err)
	}

	// Get the stories associated with the current release.
	task = "Fetch the stories associated with the current release"
	log.Run(task)
	tracker, err := modules.GetIssueTracker()
	if err != nil {
		return errs.NewError(task, err)
	}
	release := tracker.RunningRelease(releaseVersion)
	stories, err := release.Stories()
	if err != nil {
		return errs.NewError(task, err)
	}

	if len(stories) == 0 {
		return errs.NewError(task, errors.New("no relevant stories found"))
	}

	// Get the release changes.
	task = "Collect the release changes"
	log.Run(task)
	groups, err := changes.StoryChanges(stories)
	if err != nil {
		return errs.NewError(task, err)
	}

	// Just return in case there are no relevant commits found.
	if len(groups) == 0 {
		return errs.NewError(task, errors.New("no relevant commits found"))
	}

	// Sort the change groups.
	groups = changes.SortStoryChanges(groups, stories)
	groups, err = releases.StoryChangesToCherryPick(groups)
	if err != nil {
		return errs.NewError(task, err)
	}

	var (
		// Collect the changes not reachable from trunk.
		// In case there are any, we abort the cherry-picking process.
		unreachable = make([]*changes.StoryChangeGroup, 0, len(groups))

		// When we are at iterating, we also collect all release commits
		// so that we know what trunk commits to cherry-pick later.
		releaseCommits = make(map[string]struct{})

		trunkRef = fmt.Sprintf("refs/heads/%v", trunkBranch)
	)
	for _, group := range groups {
		g := &changes.StoryChangeGroup{
			StoryIdTag: group.StoryIdTag,
		}

		for _, ch := range group.Changes {
			var ok bool
			for _, c := range ch.Commits {
				// Add the commit to the map of release commits.
				releaseCommits[c.SHA] = struct{}{}

				// Look for a commit that is on trunk.
				if c.Source == trunkRef {
					ok = true
				}
			}
			if !ok {
				// In case there is none, remember the change.
				g.Changes = append(g.Changes, ch)
			}
		}

		// In case there are some changes not reachable from trunk,
		// add the story change to the list of unreachable story changes.
		if len(g.Changes) != 0 {
			unreachable = append(unreachable, g)
		}
	}

	// In case there are some changes not reachable from the trunk branch,
	// abort the process and tell the user to get the changes into trunk first.
	if len(unreachable) != 0 {
		var details bytes.Buffer
		fmt.Fprint(&details, `
The following story changes are not reachable from the trunk branch:

`)
		changes.DumpStoryChanges(&details, unreachable, tracker, false)
		fmt.Fprint(&details, `
Please cherry-pick these changes onto the trunk branch.
Only then we can proceed and cherry-pick the changes.

`)
		return errs.NewErrorWithHint(
			task, errors.New("commits not reachable from trunk detected"), details.String())
	}

	// Everything seems fine, let's continue with the process
	// by dumping the change details into the console.
	fmt.Println()
	changes.DumpStoryChanges(os.Stdout, groups, tracker, false)

	// Ask the user to confirm before doing any cherry-picking.
	task = "Ask the user to confirm cherry-picking"
	fmt.Println(`
The changes listed above will be cherry-picked into the release branch.`)
	confirmed, err := prompt.Confirm("Are you sure you want to continue?", false)
	if err != nil {
		return errs.NewError(task, err)
	}
	if !confirmed {
		prompt.PanicCancel()
	}
	fmt.Println()

	// Collect the trunk commits that were created since the last release.
	task = "Collect the trunk commits added since the last release"
	trunkCommits, err := releases.ListNewTrunkCommits()
	if err != nil {
		return errs.NewError(task, err)
	}
	// We need the list to start with the oldest commit.
	for i, j := 0, len(trunkCommits)-1; i < j; i, j = i+1, j-1 {
		trunkCommits[i], trunkCommits[j] = trunkCommits[j], trunkCommits[i]
	}

	// Collect the commits to cherry pick. These are the commits
	// that are on trunk and they are associated with the release.
	hashesToCherryPick := make([]string, 0, len(trunkCommits))
	for _, commit := range trunkCommits {
		if _, ok := releaseCommits[commit.SHA]; ok {
			hashesToCherryPick = append(hashesToCherryPick, commit.SHA)
		}
	}

	// Perform the cherry-pick itself.
	task = "Cherry-pick the missing changes into the release branch"
	log.Run(task)
	if err := git.CherryPick(hashesToCherryPick...); err != nil {
		hint := `
It was not possible to cherry-pick the missing changes into the release branch.
The cherry-picking process might be still in progress, though. Please check
the repository status and potentially resolve the cherry-picking manually.

`
		// Do not checkout the original branch.
		currentBranch = ""
		return errs.NewErrorWithHint(task, err, hint)
	}

	log.Log("All missing changes cherry-picked into the release branch")
	fmt.Println(`
  ###################################################################
  # IMPORTANT: The release branch is not being pushed automatically #
  ###################################################################
`)
	return nil
}
Exemple #26
0
func rewriteCommits(commits []*git.Commit, firstMissingOffset int) ([]*git.Commit, error) {
	// Fetch the stories in progress from the issue tracker.
	storiesTask := "Missing Story-Id detected, fetch stories from the issue tracker"
	log.Run(storiesTask)

	tracker, err := modules.GetIssueTracker()
	if err != nil {
		return nil, errs.NewError(storiesTask, err)
	}

	task := "Fetch the user record from the issue tracker"
	me, err := tracker.CurrentUser()
	if err != nil {
		return nil, errs.NewError(task, err)
	}

	stories, err := tracker.ReviewableStories()
	if err != nil {
		return nil, errs.NewError(storiesTask, err)
	}

	reviewedStories, err := tracker.ReviewedStories()
	if err != nil {
		return nil, errs.NewError(storiesTask, err)
	}

	// Show only the stories owned by the current user.
	// Note: Go sucks here, badly.
	filterStories := func(stories []common.Story, filter func(common.Story) bool) []common.Story {
		ss := make([]common.Story, 0, len(stories))
		for _, story := range stories {
			if filter(story) {
				ss = append(ss, story)
			}
		}
		return ss
	}

	mine := func(story common.Story) bool {
		for _, assignee := range story.Assignees() {
			if assignee.Id() == me.Id() {
				return true
			}
		}
		return false
	}

	stories = filterStories(stories, mine)
	reviewedStories = filterStories(reviewedStories, mine)

	// Tell the user what is happening.
	log.Run("Prepare a temporary branch to rewrite commit messages")

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

	// Get the parent of the first commit in the chain.
	task = "Get the parent commit of the commit chain to be posted"
	var parentSHA string
	if firstMissingOffset != 0 {
		// In case there are multiple commits being posted
		// and the first missing offset is not pointing to the first commit,
		// we can easily get the parent SHA by just accessing the commit list.
		parentSHA = commits[firstMissingOffset-1].SHA
	} else {
		// Otherwise we have to ask git for help.
		stdout, err := git.Log("--pretty=%P", "-n", "1", commits[firstMissingOffset].SHA)
		if err != nil {
			return nil, errs.NewError(task, err)
		}
		parentSHA = strings.Fields(stdout.String())[0]
	}

	// Prepare a temporary branch that will be used to amend commit messages.
	task = "Create a temporary branch to rewrite commit messages"
	if err := git.SetBranch(constants.TempBranchName, parentSHA); err != nil {
		return nil, errs.NewError(task, err)
	}
	defer func() {
		// Delete the temporary branch on exit.
		task := "Delete the temporary branch"
		if err := git.Branch("-D", constants.TempBranchName); err != nil {
			errs.LogError(task, err)
		}
	}()

	// Checkout the temporary branch.
	task = "Checkout the temporary branch"
	if err := git.Checkout(constants.TempBranchName); err != nil {
		return nil, errs.NewError(task, err)
	}
	defer func() {
		// Checkout the original branch on exit.
		task := fmt.Sprintf("Checkout branch '%v'", currentBranch)
		if err := git.Checkout(currentBranch); err != nil {
			errs.LogError(task, err)
		}
	}()

	// Loop and rewrite the commit messages.
	var story common.Story
	if flagAskOnce {
		header := `
Some of the commits listed above are not assigned to any story.
Please pick up the story that these commits will be assigned to.
You can also insert 'u' to mark the commits as unassigned:`
		selectedStory, err := promptForStory(header, stories, reviewedStories)
		if err != nil {
			return nil, err
		}
		story = selectedStory
	}

	// The temp branch is pointing to the parent of the first commit missing
	// the Story-Id tag. So we only need to cherry-pick the commits that
	// follow the first commit missing the Story-Id tag.
	commitsToCherryPick := commits[firstMissingOffset:]
	for _, commit := range commitsToCherryPick {
		// Cherry-pick the commit.
		task := fmt.Sprintf("Move commit %v onto the temporary branch", commit.SHA)
		if err := git.CherryPick(commit.SHA); err != nil {
			return nil, errs.NewError(task, err)
		}

		if commit.StoryIdTag == "" {
			if !flagAskOnce {
				commitMessageTitle := prompt.ShortenCommitTitle(commit.MessageTitle)

				// Ask for the story ID for the current commit.
				header := fmt.Sprintf(`
The following commit is not assigned to any story:

  commit hash:  %v
  commit title: %v

Please pick up the story to assign the commit to.
Inserting 'u' will mark the commit as unassigned:`, commit.SHA, commitMessageTitle)
				selectedStory, err := promptForStory(header, stories, reviewedStories)
				if err != nil {
					return nil, err
				}
				story = selectedStory
			}

			// Use the unassigned tag value in case no story is selected.
			storyTag := git.StoryIdUnassignedTagValue
			if story != nil {
				storyTag = story.Tag()
			}

			// Extend the commit message to include Story-Id.
			commitMessage := fmt.Sprintf("%v\nStory-Id: %v\n", commit.Message, storyTag)

			// Amend the cherry-picked commit to include the new commit message.
			task = "Amend the commit message for " + commit.SHA
			stderr := new(bytes.Buffer)
			cmd := exec.Command("git", "commit", "--amend", "-F", "-")
			cmd.Stdin = bytes.NewBufferString(commitMessage)
			cmd.Stderr = stderr
			if err := cmd.Run(); err != nil {
				return nil, errs.NewErrorWithHint(task, err, stderr.String())
			}
		}
	}

	// Reset the current branch to point to the new branch.
	task = "Reset the current branch to point to the temporary branch"
	if err := git.SetBranch(currentBranch, constants.TempBranchName); err != nil {
		return nil, errs.NewError(task, err)
	}

	// Parse the commits again since the commit hashes have changed.
	newCommits, err := git.ShowCommitRange(parentSHA + "..")
	if err != nil {
		return nil, err
	}

	log.NewLine("")
	log.Log("Commit messages amended successfully")

	// And we are done!
	return newCommits, nil
}
Exemple #27
0
func run(remoteName, pushURL string) error {
	// Load the git-related SalsaFlow config.
	gitConfig, err := git.LoadConfig()
	if err != nil {
		return err
	}

	// Load the other necessary SalsaFlow config.
	repoConfig, err := repo.LoadConfig()
	if err != nil {
		return err
	}
	enabledTimestamp := repoConfig.SalsaFlowEnabledTimestamp

	// Only check the project remote.
	if remoteName != gitConfig.RemoteName {
		log.Log(
			fmt.Sprintf(
				"Not pushing to the main project remote (%v), check skipped",
				gitConfig.RemoteName))
		return nil
	}

	// The commits that are being pushed are listed on stdin.
	// The format is <local ref> <local sha1> <remote ref> <remote sha1>,
	// so we parse the input and collect all the local hexshas.
	var coreRefs = []string{
		"refs/heads/" + gitConfig.TrunkBranchName,
		"refs/heads/" + gitConfig.ReleaseBranchName,
		"refs/heads/" + gitConfig.StagingBranchName,
		"refs/heads/" + gitConfig.StableBranchName,
	}

	parseTask := "Parse the hook input"
	var revRanges []*revisionRange
	scanner := bufio.NewScanner(os.Stdin)
	for scanner.Scan() {
		var (
			line  = scanner.Text()
			parts = strings.Split(line, " ")
		)
		if len(parts) != 4 {
			return errs.NewError(parseTask, errors.New("invalid input line: "+line))
		}

		localRef, localSha, remoteRef, remoteSha := parts[0], parts[1], parts[2], parts[3]

		// Skip the refs that are being deleted.
		if localSha == git.ZeroHash {
			continue
		}

		// Check only updates to the core branches,
		// i.e. trunk, release, client or master.
		var isCoreBranch bool
		for _, ref := range coreRefs {
			if remoteRef == ref {
				isCoreBranch = true
			}
		}
		if !isCoreBranch {
			continue
		}

		// Make sure the reference is up to date.
		// In this case the reference is not up to date when
		// the remote hash cannot be found in the local clone.
		if remoteSha != git.ZeroHash {
			task := fmt.Sprintf("Make sure remote ref '%s' is up to date", remoteRef)
			if _, err := git.Run("cat-file", "-t", remoteSha); err != nil {
				hint := fmt.Sprintf(`
Commit %v does not exist locally.
This is probably because '%v' is not up to date.
Please update the reference from the remote repository,
perhaps by executing 'git pull'.

`, remoteSha, remoteRef)
				return errs.NewErrorWithHint(task, err, hint)
			}
		}

		// Append the revision range for this input line.
		var revRange *revisionRange
		if remoteSha == git.ZeroHash {
			// In case we are pushing a new branch, check commits up to trunk.
			// There is probably no better guess that we can do in general.
			revRange = &revisionRange{gitConfig.TrunkBranchName, localRef}
		} else {
			// Otherwise check the commits that are new compared to the remote ref.
			revRange = &revisionRange{remoteSha, localRef}
		}
		revRanges = append(revRanges, revRange)
	}
	if err := scanner.Err(); err != nil {
		return errs.NewError(parseTask, err)
	}

	// Check the missing Story-Id tags.
	var missing []*git.Commit

	for _, revRange := range revRanges {
		// Get the commit objects for the relevant range.
		task := "Get the commit objects to be pushed"
		commits, err := git.ShowCommitRange(fmt.Sprintf("%v..%v", revRange.From, revRange.To))
		if err != nil {
			return errs.NewError(task, err)
		}

		// Check every commit in the range.
		for _, commit := range commits {
			// Do not check merge commits.
			if commit.Merge != "" {
				continue
			}

			// Do not check commits that happened before SalsaFlow.
			if commit.AuthorDate.Before(enabledTimestamp) {
				continue
			}

			// Check the Story-Id tag.
			if commit.StoryIdTag == "" {
				missing = append(missing, commit)
			}
		}
	}

	// Prompt for confirmation in case that is needed.
	if len(missing) != 0 {
		// Fill in the commit sources.
		task := "Fix commit sources"
		if err := git.FixCommitSources(missing); err != nil {
			return errs.NewError(task, err)
		}

		// Prompt the user for confirmation.
		task = "Prompt the user for confirmation"
		confirmed, err := promptUserForConfirmation(missing)
		if err != nil {
			return errs.NewError(task, err)
		}
		if !confirmed {
			return prompt.ErrCanceled
		}
	}

	return nil
}
Exemple #28
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
}
// extendReviewRequest is a general function that can be used to extend
// the given review issue with the given list of commits.
func extendReviewRequest(
	config *moduleConfig,
	owner string,
	repo string,
	issue *github.Issue,
	commits []*git.Commit,
	opts map[string]interface{},
) (*github.Issue, []*git.Commit, error) {

	issueNum := *issue.Number

	// Parse the issue.
	task := fmt.Sprintf("Parse review issue #%v", issueNum)
	reviewIssue, err := ghissues.ParseReviewIssue(issue)
	if err != nil {
		return nil, nil, errs.NewError(task, err)
	}

	// Add the commits.
	newCommits := make([]*git.Commit, 0, len(commits))
	for _, commit := range commits {
		if reviewIssue.AddCommit(false, commit.SHA, commit.MessageTitle) {
			newCommits = append(newCommits, commit)
		}
	}
	if len(newCommits) == 0 {
		log.Log(fmt.Sprintf("All commits already listed in issue #%v", issueNum))
		return issue, nil, nil
	}

	// Add the implemented label if necessary.
	var (
		implemented      bool
		implementedLabel = config.StoryImplementedLabel
		labelsPtr        *[]string
	)
	implementedOpt, ok := opts["implemented"]
	if ok {
		implemented = implementedOpt.(bool)
	}
	if implemented {
		labels := make([]string, 0, len(issue.Labels)+1)
		labelsPtr = &labels

		for _, label := range issue.Labels {
			if *label.Name == implementedLabel {
				// The label is already there, for some reason.
				// Set the pointer to nil so that we don't update labels.
				labelsPtr = nil
				break
			}
			labels = append(labels, *label.Name)
		}
		if labelsPtr != nil {
			labels = append(labels, implementedLabel)
		}
	}

	// Edit the issue.
	task = fmt.Sprintf("Update GitHub issue #%v", issueNum)
	log.Run(task)

	client := ghutil.NewClient(config.Token)
	updatedIssue, _, err := client.Issues.Edit(owner, repo, issueNum, &github.IssueRequest{
		Body:   github.String(reviewIssue.FormatBody()),
		State:  github.String("open"),
		Labels: labelsPtr,
	})
	if err != nil {
		return nil, nil, errs.NewError(task, err)
	}

	// Add the review comment.
	if err := addReviewComment(config, owner, repo, issueNum, newCommits); err != nil {
		return nil, nil, err
	}

	return updatedIssue, newCommits, nil
}
Exemple #30
0
func Init(force bool) error {
	// Check whether the repository has been initialised yet.
	task := "Check whether the repository has been initialised"
	versionString, err := git.GetConfigString("salsaflow.initialised")
	if err != nil {
		return errs.NewError(task, err)
	}
	if versionString == metadata.Version && !force {
		return errs.NewError(task, ErrInitialised)
	}

	log.Log("Initialising the repository for SalsaFlow")

	// Make sure the user is using the right version of Git.
	//
	// The check is here and not in app.Init because it is highly improbable
	// that the check would pass once and then fail later. Once the right
	// version of git is installed, it most probably stays.
	task = "Check the git version being used"
	log.Run(task)
	stdout, err := git.Run("--version")
	if err != nil {
		return errs.NewError(task, err)
	}
	pattern := regexp.MustCompile("^git version (([0-9]+)[.]([0-9]+).*)")
	parts := pattern.FindStringSubmatch(stdout.String())
	if len(parts) != 4 {
		return errs.NewError(task, errors.New("unexpected git --version output"))
	}
	gitVersion := parts[1]
	// This cannot fail since we matched the regexp.
	major, _ := strconv.Atoi(parts[2])
	minor, _ := strconv.Atoi(parts[3])
	// We need Git version 1.8.5.4+, so let's require 1.9+.
	switch {
	case major >= 2:
		// OK
	case major == 1 && minor >= 9:
		// OK
	default:
		hint := `
You need Git version 1.9.0 or newer.

`
		return errs.NewErrorWithHint(
			task,
			errors.New("unsupported git version detected: "+gitVersion),
			hint)
	}

	// Get hold of a git config instance.
	gitConfig, err := git.LoadConfig()
	if err != nil {
		return err
	}
	var (
		remoteName   = gitConfig.RemoteName
		trunkBranch  = gitConfig.TrunkBranchName
		stableBranch = gitConfig.StableBranchName
	)

	// Make sure that the stable branch exists.
	task = fmt.Sprintf("Make sure branch '%v' exists", stableBranch)
	log.Run(task)
	err = git.CheckOrCreateTrackingBranch(stableBranch, remoteName)
	if err != nil {
		if ex, ok := err.(*git.ErrRefNotFound); ok {
			hint := fmt.Sprintf(
				"Make sure that branch '%v' exists and run init again.\n", ex.Ref)
			return errs.NewErrorWithHint(task, err, hint)
		} else if _, ok := err.(*git.ErrRefNotInSync); !ok {
			// We ignore ErrRefNotInSync here, so return the error
			// in case it is of some other kind.
			return errs.NewError(task, err)
		}
	}

	// Make sure that the trunk branch exists.
	task = fmt.Sprintf("Make sure branch '%v' exists", trunkBranch)
	log.Run(task)
	err = git.CheckOrCreateTrackingBranch(trunkBranch, remoteName)
	if err != nil {
		if _, ok := err.(*git.ErrRefNotFound); ok {
			task := fmt.Sprintf("Create branch '%v'", trunkBranch)
			log.Log(fmt.Sprintf(
				"Branch '%v' not found. Will create one for you for free!", trunkBranch))
			if err := git.Branch(trunkBranch, stableBranch); err != nil {
				return errs.NewError(task, err)
			}
			log.NewLine(fmt.Sprintf(
				"The newly created branch is pointing to '%v'.", stableBranch))

			task = fmt.Sprintf("Push branch '%v' to remote '%v'", trunkBranch, remoteName)
			log.Run(task)
			if err := git.Push(remoteName, trunkBranch+":"+trunkBranch); err != nil {
				return errs.NewError(task, err)
			}
		} else if _, ok := err.(*git.ErrRefNotInSync); !ok {
			// We ignore ErrRefNotInSync here, so return the error
			// in case it is of some other kind.
			return errs.NewError(task, err)
		}
	}

	// Verify our git hooks are installed and used.
	for _, kind := range hooks.HookTypes {
		task := fmt.Sprintf("Check the current git %v hook", kind)
		log.Run(task)
		if err := hooks.CheckAndUpsert(kind, force); err != nil {
			return errs.NewError(task, err)
		}
	}

	// Run other registered init hooks.
	task = "Running the registered repository init hooks"
	log.Log(task)
	if err := executeInitHooks(); err != nil {
		return errs.NewError(task, err)
	}

	// Success! Mark the repository as initialised in git config.
	task = "Mark the repository as initialised"
	if err := git.SetConfigString("salsaflow.initialised", metadata.Version); err != nil {
		return err
	}
	asciiart.PrintThumbsUp()
	fmt.Println()
	log.Log("The repository is initialised")

	return nil
}