Пример #1
0
func (r *release) EnsureClosable() error {
	// Prepare for API calls.
	_, owner, repo, err := r.prepareForApiCalls()
	if err != nil {
		return err
	}

	// Get the relevant review milestone.
	releaseString := r.v.BaseString()
	task := fmt.Sprintf("Get GitHub review milestone for release %v", releaseString)
	log.Run(task)
	milestone, err := milestoneForVersion(r.tool.config, owner, repo, r.v)
	if err != nil {
		return errs.NewError(task, err)
	}
	if milestone == nil {
		return errs.NewErrorWithHint(task, errors.New("milestone not found"),
			fmt.Sprintf("\nMake sure the review milestone for release %v exists\n\n", r.v))
	}

	// Close the milestone unless there are some issues open.
	task = fmt.Sprintf(
		"Make sure the review milestone for release %v can be closed", releaseString)
	if num := *milestone.OpenIssues; num != 0 {
		hint := fmt.Sprintf(
			"\nreview milestone for release %v cannot be closed: %v issue(s) open\n\n",
			releaseString, num)
		return errs.NewErrorWithHint(task, common.ErrNotClosable, hint)
	}
	r.closingMilestone = milestone
	return nil
}
Пример #2
0
func ensureRbtVersion() error {
	hint := `
You need to install RBTools version 0.7. Please run

  $ pip install rbtools==0.7 --allow-external rbtools --allow-unverified rbtools

to install the correct version.

`

	// Load configuration and check the RBTools version only if Review Board is being used.
	config, err := common.LoadConfig()
	if err != nil {
		return err
	}
	if config.CodeReviewToolId() != Id {
		return nil
	}

	// Check the RBTools version being used.
	task := "Check the RBTools version being used"
	log.Run(task)

	// rbt 0.5.x prints the version string to stdout,
	// rbt 0.6.x prints the version string to stderr.
	stdout, stderr, err := shell.Run("rbt", "--version")
	if err != nil {
		// Return the hint instead of stderr.
		// Failing to run rbt --version probably means that it's not installed.
		return errs.NewErrorWithHint(task, err, hint)
	}

	var outputBuffer *bytes.Buffer
	if stdout.Len() != 0 {
		outputBuffer = stdout
	} else {
		outputBuffer = stderr
	}
	output := outputBuffer.String()

	pattern := regexp.MustCompile("^RBTools (([0-9]+)[.]([0-9]+).*)")
	parts := pattern.FindStringSubmatch(output)
	if len(parts) != 4 {
		err := fmt.Errorf("failed to parse 'rbt --version' output: %v", output)
		return errs.NewError(task, err)
	}
	rbtVersion := parts[1]
	// No need to check errors, we know the format is correct.
	major, _ := strconv.Atoi(parts[2])
	minor, _ := strconv.Atoi(parts[3])

	if !(major == 0 && minor == 7) {
		return errs.NewErrorWithHint(
			task, errors.New("unsupported rbt version detected: "+rbtVersion), hint)
	}

	return nil
}
Пример #3
0
func readAndUnmarshalConfig(absolutePath string, v interface{}) error {
	currentBranch := func() string {
		branch, err := gitutil.CurrentBranch()
		if err != nil {
			return err.Error()
		}
		return branch
	}

	// Read the file.
	task := "Read given configuration file"
	content, err := ioutil.ReadFile(absolutePath)
	if err != nil {
		hint := fmt.Sprintf(`
Failed to read the configuration file expected to be located at

  %v

The Git branch where the error occurred: %v

Make sure the configuration file exists and is committed
at the Git branch mentioned above.

You might want to check 'repo bootstrap' command
to see how the given configuration file can be generated.

`, absolutePath, currentBranch())
		return errs.NewErrorWithHint(task, err, hint)
	}

	// Unmarshall the content.
	task = "Unmarshal given configuration file"
	if err := Unmarshal(content, v); err != nil {
		hint := fmt.Sprintf(`
Failed to parse the configuration file located at

  %v

The Git branch where the error occurred: %v

Make sure the configuration file is valid JSON
that follows the right configuration schema.

You might want to check 'repo bootstrap' command
to see how the given configuration file can be re-generated.

`, absolutePath, currentBranch())

		return errs.NewErrorWithHint(task, err, hint)
	}

	return nil
}
Пример #4
0
func GetCodeReviewTool() (common.CodeReviewTool, error) {
	// Load configuration.
	config, err := common.LoadConfig()
	if err != nil && config == nil {
		return nil, err
	}

	// Choose the code review tool based on the configuration.
	var task = "Instantiate the selected code review plugin"
	id := config.CodeReviewToolId()
	factory, ok := codeReviewToolFactories[id]
	if !ok {
		// Collect the available code review tool ids.
		ids := make([]string, 0, len(codeReviewToolFactories))
		for id := range codeReviewToolFactories {
			ids = append(ids, id)
		}

		hint := fmt.Sprintf("\nAvailable code review tools: %v\n\n", ids)
		return nil, errs.NewErrorWithHint(
			task, fmt.Errorf("unknown code review tool: '%v'", id), hint)
	}

	// Try to instantiate the code review tool.
	tool, err := factory()
	if err != nil {
		return nil, errs.NewError(task, err)
	}

	return tool, nil
}
Пример #5
0
func (release *runningRelease) EnsureStageable() error {
	task := "Make sure the stories can be staged"
	log.Run(task)

	// Load the assigned stories.
	stories, err := release.loadStories()
	if err != nil {
		return errs.NewError(task, err)
	}

	var details bytes.Buffer
	tw := tabwriter.NewWriter(&details, 0, 8, 4, '\t', 0)
	io.WriteString(tw, "\n")
	io.WriteString(tw, "Story URL\tError\n")
	io.WriteString(tw, "=========\t=====\n")

	// For a story to be stageable, it must be in the Finished stage.
	// That by definition means that it has been reviewed and verified.
	for _, story := range stories {
		if !stateAtLeast(story, pivotal.StoryStateFinished) {
			fmt.Fprintf(tw, "%v\t%v\n", story.URL, "story not finished yet")
			err = common.ErrNotStageable
		}
	}
	if err != nil {
		io.WriteString(tw, "\n")
		tw.Flush()
		return errs.NewErrorWithHint(task, err, details.String())
	}
	return nil
}
Пример #6
0
func (release *runningRelease) EnsureStageable() error {
	versionString := release.releaseVersion.BaseString()

	var task = fmt.Sprintf(
		"Make sure that release %v can be staged", versionString)
	log.Run(task)

	var details bytes.Buffer
	tw := tabwriter.NewWriter(&details, 0, 8, 4, '\t', 0)
	io.WriteString(tw, "\n")
	io.WriteString(tw, "Issue Key\tError\n")
	io.WriteString(tw, "=========\t=====\n")

	var err error
	for _, issue := range release.issues {
		if ex := ensureStageableIssue(issue); ex != nil {
			fmt.Fprintf(tw, "%v\t%v\n", issue.Key, ex)
			err = common.ErrNotStageable
		}
	}

	if err != nil {
		io.WriteString(tw, "\n")
		tw.Flush()
		return errs.NewErrorWithHint(task, err, details.String())
	}
	return nil
}
Пример #7
0
func runMain(versionString string) error {
	// Make sure the version string is correct.
	task := "Parse the command line VERSION argument"
	ver, err := version.Parse(versionString)
	if err != nil {
		hint := `
The version string must be in the form of Major.Minor.Patch
and no part of the version string can be omitted.

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

	// In case -commit is set, set and commit the version string.
	if flagCommit {
		currentBranch, err := gitutil.CurrentBranch()
		if err != nil {
			return err
		}

		_, err = version.SetForBranch(ver, currentBranch)
		return err
	}

	// Otherwise just set the version.
	return version.Set(ver)
}
Пример #8
0
func ensureStoryIdTag(commits []*git.Commit) error {
	task := "Make sure all commits contain the Story-Id tag"

	storyIdTag := flagStoryIdTag
	if storyIdTag != "" && storyIdTag != git.StoryIdUnassignedTagValue {
		if err := checkStoryIdTag(storyIdTag); err != nil {
			return errs.NewError(task, err)
		}
	}

	var (
		hint    = bytes.NewBufferString("\n")
		missing bool
	)
	for _, commit := range commits {
		if commit.StoryIdTag == "" {
			if storyIdTag != "" {
				commit.StoryIdTag = storyIdTag
				continue
			}

			fmt.Fprintf(hint, "Commit %v is missing the Story-Id tag\n", commit.SHA)
			missing = true
		}
	}
	fmt.Fprintf(hint, "\n")

	if missing {
		return errs.NewErrorWithHint(
			task, errors.New("Story-Id tag missing"), hint.String())
	}
	return nil
}
Пример #9
0
func GetIssueTracker() (common.IssueTracker, error) {
	// Load configuration.
	config, err := common.LoadConfig()
	if err != nil && config == nil {
		return nil, err
	}

	// Choose the issue tracker based on the configuration.
	var task = "Instantiate the selected issue tracker plugin"
	id := config.IssueTrackerId()
	factory, ok := issueTrackerFactories[id]
	if !ok {
		// Collect the available tracker ids.
		ids := make([]string, 0, len(issueTrackerFactories))
		for id := range issueTrackerFactories {
			ids = append(ids, id)
		}

		hint := fmt.Sprintf("\nAvailable issue trackers: %v\n\n", ids)
		return nil, errs.NewErrorWithHint(
			task, fmt.Errorf("unknown issue tracker: '%v'", id), hint)
	}

	// Try to instantiate the issue tracker.
	tracker, err := factory()
	if err != nil {
		return nil, errs.NewError(task, err)
	}

	return tracker, nil
}
Пример #10
0
func SetConfigString(key string, value string) error {
	task := fmt.Sprintf("Run 'git config %v %v'", key, value)
	_, stderr, err := shell.Run("git", "config", key, value)
	if err != nil {
		return errs.NewErrorWithHint(task, err, stderr.String())
	}
	return nil
}
Пример #11
0
func ensureCommitsPushed(commits []*git.Commit) error {
	task := "Make sure that all commits exist in the upstream repository"

	// Load git-related config.
	gitConfig, err := git.LoadConfig()
	if err != nil {
		return errs.NewError(task, err)
	}
	remoteName := gitConfig.RemoteName
	remotePrefix := remoteName + "/"

	// Check each commit one by one.
	//
	// We run `git branch -r --contains HASH` for each commit,
	// then we check the output. In case there is a branch prefixed
	// with the right upstream name, the commit is treated as pushed.
	var (
		hint    = bytes.NewBufferString("\n")
		missing bool
	)
CommitLoop:
	for _, commit := range commits {
		// Get `git branch -r --contains HASH` output.
		stdout, err := git.Run("branch", "-r", "--contains", commit.SHA)
		if err != nil {
			return errs.NewError(task, err)
		}

		// Parse `git branch` output line by line.
		scanner := bufio.NewScanner(stdout)
		for scanner.Scan() {
			line := scanner.Text()
			if strings.HasPrefix(strings.TrimSpace(line), remotePrefix) {
				// The commit is contained in a remote branch, continue.
				continue CommitLoop
			}
		}
		if err := scanner.Err(); err != nil {
			return errs.NewError(task, err)
		}

		// The commit is not contained in any remote branch, bummer.
		fmt.Fprintf(hint,
			"Commit %v has not been pushed into remote '%v' yet.\n", commit.SHA, remoteName)
		missing = true
	}
	fmt.Fprintf(hint, "\n")
	fmt.Fprintf(hint, "All selected commits need to be pushed into the upstream pository.\n")
	fmt.Fprintf(hint, "Please make sure that is the case before trying again.\n")
	fmt.Fprintf(hint, "\n")

	// Return an error in case there is any commit that is not pushed.
	if missing {
		return errs.NewErrorWithHint(
			task, fmt.Errorf("some commits not found in upstream '%v'", remoteName), hint.String())
	}
	return nil
}
Пример #12
0
func fetchOrUpdateSkeleton(skeleton string) error {
	// Parse the skeleton string.
	parts := strings.SplitN(skeleton, "/", 2)
	if len(parts) != 2 {
		return fmt.Errorf("not a valid repository path string: %v", skeleton)
	}
	owner, repo := parts[0], parts[1]

	// Create the cache directory if necessary.
	task := "Make sure the local cache directory exists"
	cacheDir, err := cacheDirectoryAbsolutePath()
	if err != nil {
		return errs.NewError(task, err)
	}
	if err := os.MkdirAll(cacheDir, 0755); err != nil {
		return errs.NewError(task, err)
	}

	// Pull or close the given skeleton.
	task = "Pull or clone the given skeleton"
	skeletonDir := filepath.Join(cacheDir, "github.com", owner)

	if err := os.MkdirAll(skeletonDir, 0755); err != nil {
		return errs.NewError(task, err)
	}

	skeletonPath := filepath.Join(skeletonDir, repo)
	if _, err := os.Stat(skeletonPath); err != nil {
		if !os.IsNotExist(err) {
			return errs.NewError(task, err)
		}

		// The directory does not exist, hence we clone.
		task := fmt.Sprintf("Clone skeleton '%v'", skeleton)
		log.Run(task)
		args := []string{
			"clone",
			"--single-branch",
			fmt.Sprintf("https://github.com/%v/%v", owner, repo),
			skeletonPath,
		}
		if _, err := git.Run(args...); err != nil {
			return errs.NewError(task, err)
		}
		return nil
	}

	// The skeleton directory exists, hence we pull.
	task = fmt.Sprintf("Pull skeleton '%v'", skeleton)
	log.Run(task)
	cmd, _, stderr := shell.Command("git", "pull")
	cmd.Dir = skeletonPath
	if err := cmd.Run(); err != nil {
		return errs.NewErrorWithHint(task, err, stderr.String())
	}
	return nil
}
Пример #13
0
func RunCommand(command string, args ...string) (stdout *bytes.Buffer, err error) {
	argsList := make([]string, 2, 2+len(args))
	argsList[0], argsList[1] = "--no-pager", command
	argsList = append(argsList, args...)

	task := fmt.Sprintf("Run 'git %v' with args = %#v", command, args)
	log.V(log.Debug).Log(task)
	stdout, stderr, err := shell.Run("git", argsList...)
	if err != nil {
		return nil, errs.NewErrorWithHint(task, err, stderr.String())
	}
	return stdout, nil
}
Пример #14
0
// RefExistsStrict requires the whole ref path to be specified,
// e.g. refs/remotes/origin/master.
func RefExistsStrict(ref string) (exists bool, err error) {
	task := fmt.Sprintf("Run 'git show-ref --quiet --verify %v'", ref)
	_, stderr, err := shell.Run("git", "show-ref", "--verify", "--quiet", ref)
	if err != nil {
		if stderr.Len() != 0 {
			// Non-empty error output means that there was an error.
			return false, errs.NewErrorWithHint(task, err, stderr.String())
		}
		// Otherwise the ref does not exist.
		return false, nil
	}
	// No error means that the ref exists.
	return true, nil
}
Пример #15
0
func loadActiveModule(kind loader.ModuleKind) (loader.Module, error) {
	// Load local configuration.
	localConfig, err := config.ReadLocalConfig()
	if err != nil {
		return nil, err
	}

	// Get the module matching the module kind.
	activeModuleId := loader.ActiveModule(localConfig, kind)
	if activeModuleId == "" {
		task := fmt.Sprintf("Get active module ID for module kind '%v'", kind)
		err := &ErrModuleNotSet{kind}
		hint := "\nMake sure the ID is specified in the local configuration file.\n\n"
		return nil, errs.NewErrorWithHint(task, err, hint)
	}

	// Find the module among the registered modules.
	for _, module := range registeredModules {
		if module.Id() == activeModuleId {
			return module, nil
		}
	}

	task := fmt.Sprintf("Load active module for module kind '%v'", kind)
	err = &ErrModuleNotFound{activeModuleId}
	hint := `
The module for the given module ID was not found.
This can happen for one of the following reasons:

  1. the module ID as stored in the local configuration file is mistyped, or
  2. the module for the given module ID was not linked into your SalsaFlow.

Check the scenarios as mentioned above to fix the issue.

`
	return nil, errs.NewErrorWithHint(task, err, hint)
}
Пример #16
0
func GetConfigString(key string) (value string, err error) {
	task := fmt.Sprintf("Run 'git config %v'", key)
	stdout, stderr, err := shell.Run("git", "config", key)
	if err != nil {
		if stderr.Len() == 0 {
			// git config returns exit code 1 when the key is not set.
			// This can be detected by stderr being of zero length.
			// We treat this as the key being set to "".
			return "", nil
		}
		// Otherwise there is an error.
		return "", errs.NewErrorWithHint(task, err, stderr.String())
	}
	// Just return what was printed to stdout.
	return strings.TrimSpace(stdout.String()), nil
}
Пример #17
0
// LoadConfig can be used to load SalsaFlow configuration
// according to the given configuration specification.
//
// The main difference between BootstrapConfig and LoadConfig is that
// LoadConfig returns an error when the local configuration is not valid.
// While `repo bootstrap` is using BootstrapConfig, all other modules
// and commands should be using LoadConfig. The local configuration file
// is only supposed to be generated once during `repo bootstrap`.
func LoadConfig(spec ConfigSpec) (err error) {
	if spec == nil {
		return errs.NewErrorWithHint(
			"Load configuration according to the specification",
			errors.New("nil configuration specification provided"),
			`
Nil configuration specification provided,
please contact the module author to have it fixed.

`,
		)
	}

	if err := bootstrapGlobalConfig(spec); err != nil {
		return err
	}
	return loadLocalConfig(spec)
}
Пример #18
0
func UnmarshalGlobalConfig(v interface{}) error {
	// Read the global config file into the cache in case it's not there yet.
	if globalContentCache == nil {
		globalContent, err := readGlobalConfig()
		if err != nil {
			return err
		}
		globalContentCache = globalContent.Bytes()
	}

	// Unmarshal the global config file.
	task := "Unmarshal the global configuration file"
	if err := yaml.Unmarshal(globalContentCache, v); err != nil {
		return errs.NewErrorWithHint(
			task, err, "Make sure the configuration file is valid YAML\n")
	}
	return nil
}
Пример #19
0
func ensureNoMergeCommits(commits []*git.Commit) error {
	var (
		task = "Make sure there are no merge commits"
		hint bytes.Buffer
		err  error
	)
	fmt.Fprintln(&hint)
	for _, commit := range commits {
		if commit.Merge != "" {
			fmt.Fprintf(&hint, "Commit %v is a merge commit\n", commit.SHA)
			err = errors.New("merge commit detected")
		}
	}
	fmt.Fprintln(&hint)
	if err != nil {
		return errs.NewErrorWithHint(task, err, hint.String())
	}
	return nil
}
Пример #20
0
func (release *runningRelease) EnsureClosable() error {
	versionString := release.version.BaseString()

	task := fmt.Sprintf(
		"Make sure that the stories associated with release %v can be released", versionString)
	log.Run(task)

	// Make sure the stories are loaded.
	stories, err := release.loadStories()
	if err != nil {
		return errs.NewError(task, err)
	}

	// Make sure all relevant stories are accepted.
	// This includes the stories with SkipCheckLabels.
	notAccepted := make([]*pivotal.Story, 0, len(stories))
	for _, story := range stories {
		if story.State != pivotal.StoryStateAccepted {
			notAccepted = append(notAccepted, story)
		}
	}

	// In case there is no story that is not accepted, we are done.
	if len(notAccepted) == 0 {
		return nil
	}

	// Generate the error hint.
	var hint bytes.Buffer
	tw := tabwriter.NewWriter(&hint, 0, 8, 2, '\t', 0)
	fmt.Fprintf(tw, "\nThe following stories cannot be released:\n\n")
	fmt.Fprintf(tw, "Story URL\tState\n")
	fmt.Fprintf(tw, "=========\t=====\n")
	for _, story := range notAccepted {
		fmt.Fprintf(tw, "%v\t%v\n", story.URL, story.State)
	}
	fmt.Fprintf(tw, "\n")
	tw.Flush()

	return errs.NewErrorWithHint(task, common.ErrNotClosable, hint.String())
}
Пример #21
0
// EnsureClosable is a part of common.RunningRelease interface.
func (release *runningRelease) EnsureClosable() error {
	var (
		config  = release.tracker.config
		vString = release.version.BaseString()
	)

	task := fmt.Sprintf(
		"Make sure that the issues associated with release %v can be released", vString)
	log.Run(task)

	// Make sure the issues are loaded.
	issues, err := release.loadIssues()
	if err != nil {
		return errs.NewError(task, err)
	}

	// Make sure all relevant issues are accepted.
	// This includes the issues with SkipCheckLabels.
	notAccepted := filterIssues(issues, func(issue *github.Issue) bool {
		return abstractState(issue, config) != common.StoryStateAccepted
	})

	// In case there are no issues in a wrong state, we are done.
	if len(notAccepted) == 0 {
		return nil
	}

	// Generate the error hint.
	var hint bytes.Buffer
	tw := tabwriter.NewWriter(&hint, 0, 8, 2, '\t', 0)
	fmt.Fprintf(tw, "\nThe following issues are blocking the release:\n\n")
	fmt.Fprintf(tw, "Issue URL\tState\n")
	fmt.Fprintf(tw, "=========\t=====\n")
	for _, issue := range notAccepted {
		fmt.Fprintf(tw, "%v\t%v\n", *issue.HTMLURL, abstractState(issue, config))
	}
	fmt.Fprintf(tw, "\n")
	tw.Flush()

	return errs.NewErrorWithHint(task, common.ErrNotClosable, hint.String())
}
Пример #22
0
func Run(scriptName string, args ...string) (stdout *bytes.Buffer, err error) {
	task := fmt.Sprintf("Run the %v script", scriptName)

	// Get the command for the given script name and args.
	cmd, err := Command(scriptName, args...)
	if err != nil {
		return nil, errs.NewError(task, err)
	}

	// Run the given script in the repository root.
	var (
		sout bytes.Buffer
		serr bytes.Buffer
	)
	cmd.Stdout = &sout
	cmd.Stderr = &serr
	if err := cmd.Run(); err != nil {
		return nil, errs.NewErrorWithHint(task, err, serr.String())
	}
	return &sout, nil
}
Пример #23
0
func readLocalConfig() (content *bytes.Buffer, err error) {
	// Get the config file absolute path.
	path, err := LocalConfigDirectoryAbsolutePath()
	if err != nil {
		return nil, err
	}
	path = filepath.Join(path, LocalConfigFilename)

	// Read the content and return it.
	task := "Read the local config file"
	contentBytes, err := ioutil.ReadFile(path)
	if err != nil {
		if os.IsNotExist(err) {
			hint := `
The local configuration file was not found.
Check 'repo bootstrap' to see how to generate it.

`
			return nil, errs.NewErrorWithHint(task, err, hint)
		}
		return nil, errs.NewError(task, err)
	}
	return bytes.NewBuffer(contentBytes), nil
}
Пример #24
0
func (release *runningRelease) EnsureReleasable() error {
	// Drop accepted issues.
	var notAccepted []*jira.Issue
IssueLoop:
	for _, issue := range release.issues {
		for _, id := range acceptedStateIds {
			if id == issue.Fields.Status.Id {
				continue IssueLoop
			}
		}
		notAccepted = append(notAccepted, issue)
	}

	// In case there is no open story, we are done.
	if len(notAccepted) == 0 {
		return nil
	}

	// Generate the error hint.
	var hint bytes.Buffer
	tw := tabwriter.NewWriter(&hint, 0, 8, 2, '\t', 0)
	fmt.Fprintf(tw, "\nThe following issues cannot be released:\n\n")
	fmt.Fprintf(tw, "Issue Key\tStatus\n")
	fmt.Fprintf(tw, "=========\t======\n")
	for _, issue := range notAccepted {
		fmt.Fprintf(tw, "%v\t%v\n", issue.Key, issue.Fields.Status.Name)
	}
	fmt.Fprintf(tw, "\n")
	tw.Flush()

	versionString := release.releaseVersion.BaseString()
	return errs.NewErrorWithHint(
		fmt.Sprintf("Make sure release %v can be released", versionString),
		common.ErrNotReleasable,
		hint.String())
}
Пример #25
0
func GetReleaseNotesManager() (common.ReleaseNotesManager, error) {
	// Load configuration.
	config, err := common.LoadConfig()
	if err != nil && config == nil {
		return nil, err
	}

	// Choose the release notes manager based on the configuration.
	var task = "Instantiate the selected release notes manager plugin"
	id := config.ReleaseNotesManagerId()
	// In case the id is not set, we simply return nil.
	// This means that this module is disabled.
	if id == "" {
		return nil, nil
	}
	factory, ok := notesManagerFactories[id]
	if !ok {
		// Collect the available tracker ids.
		ids := make([]string, 0, len(notesManagerFactories))
		for id := range notesManagerFactories {
			ids = append(ids, id)
		}

		hint := fmt.Sprintf("\nAvailable release notes managers: %v\n\n", ids)
		return nil, errs.NewErrorWithHint(
			task, fmt.Errorf("unknown release notes manager: '%v'", id), hint)
	}

	// Try to instantiate the release notes manager.
	rnm, err := factory()
	if err != nil {
		return nil, errs.NewError(task, err)
	}

	return rnm, nil
}
Пример #26
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
}
Пример #27
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
}
Пример #28
0
// updateIssues calls updateFunc on every issue in the list, concurrently.
// It then collects all the results and returns the cumulative result.
func updateIssues(
	api *jira.Client,
	issues []*jira.Issue,
	updateFunc issueUpdateFunc,
	rollbackFunc issueUpdateFunc,
) error {
	// Send all the requests at once.
	retCh := make(chan *issueUpdateResult, len(issues))
	for _, issue := range issues {
		go func(is *jira.Issue) {
			err := updateFunc(api, is)
			retCh <- &issueUpdateResult{is, err}
		}(issue)
	}

	// Wait for the requests to complete.
	var (
		stderr        = bytes.NewBufferString("\nUpdate Errors\n-------------\n")
		updatedIssues = make([]*jira.Issue, 0, len(issues))
		err           error
	)
	for i := 0; i < cap(retCh); i++ {
		if ret := <-retCh; ret.err != nil {
			fmt.Fprintln(stderr, ret.err)
			err = errors.New("failed to update JIRA issues")
		} else {
			updatedIssues = append(updatedIssues, ret.issue)
		}
	}
	fmt.Fprintln(stderr)

	if err != nil {
		if rollbackFunc != nil {
			// Spawn the rollback goroutines.
			retCh := make(chan *issueUpdateResult)
			for _, issue := range updatedIssues {
				go func(is *jira.Issue) {
					err := rollbackFunc(api, is)
					retCh <- &issueUpdateResult{is, err}
				}(issue)
			}

			// Collect the rollback results.
			rollbackStderr := bytes.NewBufferString("Rollback Errors\n---------------\n")
			for _ = range updatedIssues {
				if ret := <-retCh; ret.err != nil {
					fmt.Fprintln(rollbackStderr, ret.err)
				}
			}
			fmt.Fprintln(stderr)

			// Append the rollback error output to the update error output.
			if _, err := io.Copy(stderr, rollbackStderr); err != nil {
				panic(err)
			}
		}
		// Return the aggregate error.
		return errs.NewErrorWithHint("Update JIRA issues", err, stderr.String())
	}
	return nil
}
Пример #29
0
// updateIssues can be used to update multiple issues at once concurrently.
// It basically calls the given update function on all given issues and
// collects the results. In case there is any error, updateIssues tries
// to revert partial changes. The error returned contains the complete list
// of API call errors as the error hint.
func updateIssues(
	client *github.Client,
	owner string,
	repo string,
	issues []*github.Issue,
	updateFunc issueUpdateFunc,
	rollbackFunc issueUpdateFunc,
) ([]*github.Issue, action.Action, error) {

	// Prepare a function that can be used to apply the given updateFunc.
	// It is later used to both update issues and revert changes.
	update := func(
		issues []*github.Issue,
		updateFunc issueUpdateFunc,
	) (newIssues []*github.Issue, errHint string, err error) {

		// Send the requests concurrently.
		retCh := make(chan *issueUpdateResult, len(issues))
		for _, issue := range issues {
			go func(issue *github.Issue) {
				var (
					updatedIssue *github.Issue
					err          error
				)
				withRequestAllocated(func() {
					updatedIssue, err = updateFunc(client, owner, repo, issue)
				})
				if err == nil {
					// On success, return the updated story.
					retCh <- &issueUpdateResult{updatedIssue, nil}
				} else {
					// On error, keep the original story, add the error.
					retCh <- &issueUpdateResult{nil, err}
				}
			}(issue)
		}

		// Wait for the requests to complete.
		var (
			updatedIssues = make([]*github.Issue, 0, len(issues))
			errFailed     = errors.New("failed to update GitHub issues")
			stderr        bytes.Buffer
		)
		for range issues {
			if ret := <-retCh; ret.err != nil {
				fmt.Fprintln(&stderr, ret.err)
				err = errFailed
			} else {
				updatedIssues = append(updatedIssues, ret.issue)
			}
		}

		return updatedIssues, stderr.String(), err
	}

	// Apply the update function.
	updatedIssues, errHint, err := update(issues, updateFunc)
	if err != nil {
		// In case there is an error, generate the error hint.
		var errHintAcc bytes.Buffer
		errHintAcc.WriteString("\nUpdate Errors\n-------------\n")
		errHintAcc.WriteString(errHint)
		errHintAcc.WriteString("\n")

		// Revert the changes.
		_, errHint, ex := update(updatedIssues, rollbackFunc)
		if ex != nil {
			// In case there is an error during rollback, extend the error hint.
			errHintAcc.WriteString("Rollback Errors\n---------------\n")
			errHintAcc.WriteString(errHint)
			errHintAcc.WriteString("\n")
		}

		return nil, nil, errs.NewErrorWithHint("Update GitHub issues", err, errHintAcc.String())
	}

	// On success, return the updated issues and a rollback function.
	act := action.ActionFunc(func() error {
		_, errHint, err := update(updatedIssues, rollbackFunc)
		if err != nil {
			var errHintAcc bytes.Buffer
			errHintAcc.WriteString("\nRollback Errors\n---------------\n")
			errHintAcc.WriteString(errHint)
			errHintAcc.WriteString("\n")
			return errs.NewErrorWithHint("Revert GitHub issue updates", err, errHintAcc.String())
		}
		return nil
	})
	return updatedIssues, act, nil
}
Пример #30
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()
}