prow/tide/status.go
func requirementDiff(pr *PullRequest, q *config.TideQuery, cc contextChecker) (string, int) {
	const maxLabelChars = 50
	var desc string
	var diff int
	// Drops labels if needed to fit the description text area, but keep at least 1.
	truncate := func(labels []string) []string {
		i := 1
		chars := len(labels[0])
		for ; i < len(labels); i++ {
			if chars+len(labels[i]) > maxLabelChars {
				break
			}
			chars += len(labels[i]) + 2 // ", "
		}
		return labels[:i]
	}

	// Weight incorrect branches with very high diff so that we select the query
	// for the correct branch.
	targetBranchBlacklisted := false
	for _, excludedBranch := range q.ExcludedBranches {
		if string(pr.BaseRef.Name) == excludedBranch {
			targetBranchBlacklisted = true
			break
		}
	}
	// if no whitelist is configured, the target is OK by default
	targetBranchWhitelisted := len(q.IncludedBranches) == 0
	for _, includedBranch := range q.IncludedBranches {
		if string(pr.BaseRef.Name) == includedBranch {
			targetBranchWhitelisted = true
			break
		}
	}
	if targetBranchBlacklisted || !targetBranchWhitelisted {
		diff += 1000
		if desc == "" {
			desc = fmt.Sprintf(" Merging to branch %s is forbidden.", pr.BaseRef.Name)
		}
	}

	// Weight incorrect milestone with relatively high diff so that we select the
	// query for the correct milestone (but choose favor query for correct branch).
	if q.Milestone != "" && (pr.Milestone == nil || string(pr.Milestone.Title) != q.Milestone) {
		diff += 100
		if desc == "" {
			desc = fmt.Sprintf(" Must be in milestone %s.", q.Milestone)
		}
	}

	// Weight incorrect labels and statues with low (normal) diff values.
	var missingLabels []string
	for _, l1 := range q.Labels {
		var found bool
		for _, l2 := range pr.Labels.Nodes {
			if string(l2.Name) == l1 {
				found = true
				break
			}
		}
		if !found {
			missingLabels = append(missingLabels, l1)
		}
	}
	diff += len(missingLabels)
	if desc == "" && len(missingLabels) > 0 {
		sort.Strings(missingLabels)
		trunced := truncate(missingLabels)
		if len(trunced) == 1 {
			desc = fmt.Sprintf(" Needs %s label.", trunced[0])
		} else {
			desc = fmt.Sprintf(" Needs %s labels.", strings.Join(trunced, ", "))
		}
	}

	var presentLabels []string
	for _, l1 := range q.MissingLabels {
		for _, l2 := range pr.Labels.Nodes {
			if string(l2.Name) == l1 {
				presentLabels = append(presentLabels, l1)
				break
			}
		}
	}
	diff += len(presentLabels)
	if desc == "" && len(presentLabels) > 0 {
		sort.Strings(presentLabels)
		trunced := truncate(presentLabels)
		if len(trunced) == 1 {
			desc = fmt.Sprintf(" Should not have %s label.", trunced[0])
		} else {
			desc = fmt.Sprintf(" Should not have %s labels.", strings.Join(trunced, ", "))
		}
	}

	// fixing label issues takes precedence over status contexts
	var contexts []string
	for _, commit := range pr.Commits.Nodes {
		if commit.Commit.OID == pr.HeadRefOID {
			for _, ctx := range unsuccessfulContexts(commit.Commit.Status.Contexts, cc, logrus.New().WithFields(pr.logFields())) {
				contexts = append(contexts, string(ctx.Context))
			}
		}
	}
	diff += len(contexts)
	if desc == "" && len(contexts) > 0 {
		sort.Strings(contexts)
		trunced := truncate(contexts)
		if len(trunced) == 1 {
			desc = fmt.Sprintf(" Job %s has not succeeded.", trunced[0])
		} else {
			desc = fmt.Sprintf(" Jobs %s have not succeeded.", strings.Join(trunced, ", "))
		}
	}

	// TODO(cjwagner): List reviews (states:[APPROVED], first: 1) as part of open
	// PR query.

	return desc, diff
}
func requirementDiff(pr *PullRequest, q *config.TideQuery, cc contextChecker) (string, int) {
	const maxLabelChars = 50
	var desc string
	var diff int
	// Drops labels if needed to fit the description text area, but keep at least 1.
	truncate := func(labels []string) []string {
		i := 1
		chars := len(labels[0])
		for ; i < len(labels); i++ {
			if chars+len(labels[i]) > maxLabelChars {
				break
			}
			chars += len(labels[i]) + 2 // ", "
		}
		return labels[:i]
	}

	// Weight incorrect branches with very high diff so that we select the query
	// for the correct branch.
	targetBranchBlacklisted := false
	for _, excludedBranch := range q.ExcludedBranches {
		if string(pr.BaseRef.Name) == excludedBranch {
			targetBranchBlacklisted = true
			break
		}
	}
	// if no whitelist is configured, the target is OK by default
	targetBranchWhitelisted := len(q.IncludedBranches) == 0
	for _, includedBranch := range q.IncludedBranches {
		if string(pr.BaseRef.Name) == includedBranch {
			targetBranchWhitelisted = true
			break
		}
	}
	if targetBranchBlacklisted || !targetBranchWhitelisted {
		diff += 1000
		if desc == "" {
			desc = fmt.Sprintf(" Merging to branch %s is forbidden.", pr.BaseRef.Name)
		}
	}

	// Weight incorrect milestone with relatively high diff so that we select the
	// query for the correct milestone (but choose favor query for correct branch).
	if q.Milestone != "" && (pr.Milestone == nil || string(pr.Milestone.Title) != q.Milestone) {
		diff += 100
		if desc == "" {
			desc = fmt.Sprintf(" Must be in milestone %s.", q.Milestone)
		}
	}

	// Weight incorrect labels and statues with low (normal) diff values.
	var missingLabels []string
	for _, l1 := range q.Labels {
		var found bool
		for _, l2 := range pr.Labels.Nodes {
			if string(l2.Name) == l1 {
				found = true
				break
			}
		}
		if !found {
			missingLabels = append(missingLabels, l1)
		}
	}
	diff += len(missingLabels)
	if desc == "" && len(missingLabels) > 0 {
		sort.Strings(missingLabels)
		trunced := truncate(missingLabels)
		if len(trunced) == 1 {
			desc = fmt.Sprintf(" Needs %s label.", trunced[0])
		} else {
			desc = fmt.Sprintf(" Needs %s labels.", strings.Join(trunced, ", "))
		}
	}

	var presentLabels []string
	for _, l1 := range q.MissingLabels {
		for _, l2 := range pr.Labels.Nodes {
			if string(l2.Name) == l1 {
				presentLabels = append(presentLabels, l1)
				break
			}
		}
	}
	diff += len(presentLabels)
	if desc == "" && len(presentLabels) > 0 {
		sort.Strings(presentLabels)
		trunced := truncate(presentLabels)
		if len(trunced) == 1 {
			desc = fmt.Sprintf(" Should not have %s label.", trunced[0])
		} else {
			desc = fmt.Sprintf(" Should not have %s labels.", strings.Join(trunced, ", "))
		}
	}

	// fixing label issues takes precedence over status contexts
	var contexts []string
	for _, commit := range pr.Commits.Nodes {
		if commit.Commit.OID == pr.HeadRefOID {
			for _, ctx := range unsuccessfulContexts(commit.Commit.Status.Contexts, cc, logrus.New().WithFields(pr.logFields())) {
				contexts = append(contexts, string(ctx.Context))
			}
		}
	}
	diff += len(contexts)
	if desc == "" && len(contexts) > 0 {
		sort.Strings(contexts)
		trunced := truncate(contexts)
		if len(trunced) == 1 {
			desc = fmt.Sprintf(" Job %s has not succeeded.", trunced[0])
		} else {
			desc = fmt.Sprintf(" Jobs %s have not succeeded.", strings.Join(trunced, ", "))
		}
	}

	// TODO(cjwagner): List reviews (states:[APPROVED], first: 1) as part of open
	// PR query.

	return desc, diff
}
func requirementDiff ( pr * PullRequest , q * config . TideQuery , cc contextChecker ) ( string , int ) { const maxLabelChars = 50 var desc string var diff int // Drops labels if needed to fit the description text area, but keep at least 1. truncate := func ( labels [ ] string ) [ ] string { i := 1 chars := len ( labels [ 0 ] ) for ; i < len ( labels ) ; i ++ { if chars + len ( labels [ i ] ) > maxLabelChars { break } chars += len ( labels [ i ] ) + 2 // ", " } return labels [ : i ] } // Weight incorrect branches with very high diff so that we select the query // for the correct branch. targetBranchBlacklisted := false for _ , excludedBranch := range q . ExcludedBranches { if string ( pr . BaseRef . Name ) == excludedBranch { targetBranchBlacklisted = true break } } // if no whitelist is configured, the target is OK by default targetBranchWhitelisted := len ( q . IncludedBranches ) == 0 for _ , includedBranch := range q . IncludedBranches { if string ( pr . BaseRef . Name ) == includedBranch { targetBranchWhitelisted = true break } } if targetBranchBlacklisted || ! targetBranchWhitelisted { diff += 1000 if desc == " " { desc = fmt . Sprintf ( " " , pr . BaseRef . Name ) } } // Weight incorrect milestone with relatively high diff so that we select the // query for the correct milestone (but choose favor query for correct branch). if q . Milestone != " " && ( pr . Milestone == nil || string ( pr . Milestone . Title ) != q . Milestone ) { diff += 100 if desc == " " { desc = fmt . Sprintf ( " " , q . Milestone ) } } // Weight incorrect labels and statues with low (normal) diff values. var missingLabels [ ] string for _ , l1 := range q . Labels { var found bool for _ , l2 := range pr . Labels . Nodes { if string ( l2 . Name ) == l1 { found = true break } } if ! found { missingLabels = append ( missingLabels , l1 ) } } diff += len ( missingLabels ) if desc == " " && len ( missingLabels ) > 0 { sort . Strings ( missingLabels ) trunced := truncate ( missingLabels ) if len ( trunced ) == 1 { desc = fmt . Sprintf ( " " , trunced [ 0 ] ) } else { desc = fmt . Sprintf ( " " , strings . Join ( trunced , " " ) ) } } var presentLabels [ ] string for _ , l1 := range q . MissingLabels { for _ , l2 := range pr . Labels . Nodes { if string ( l2 . Name ) == l1 { presentLabels = append ( presentLabels , l1 ) break } } } diff += len ( presentLabels ) if desc == " " && len ( presentLabels ) > 0 { sort . Strings ( presentLabels ) trunced := truncate ( presentLabels ) if len ( trunced ) == 1 { desc = fmt . Sprintf ( " " , trunced [ 0 ] ) } else { desc = fmt . Sprintf ( " " , strings . Join ( trunced , " " ) ) } } // fixing label issues takes precedence over status contexts var contexts [ ] string for _ , commit := range pr . Commits . Nodes { if commit . Commit . OID == pr . HeadRefOID { for _ , ctx := range unsuccessfulContexts ( commit . Commit . Status . Contexts , cc , logrus . New ( ) . WithFields ( pr . logFields ( ) ) ) { contexts = append ( contexts , string ( ctx . Context ) ) } } } diff += len ( contexts ) if desc == " " && len ( contexts ) > 0 { sort . Strings ( contexts ) trunced := truncate ( contexts ) if len ( trunced ) == 1 { desc = fmt . Sprintf ( " " , trunced [ 0 ] ) } else { desc = fmt . Sprintf ( " " , strings . Join ( trunced , " " ) ) } } // TODO(cjwagner): List reviews (states:[APPROVED], first: 1) as part of open // PR query. return desc , diff }
// requirementDiff calculates the diff between a PR and a TideQuery.
// This diff is defined with a string that describes some subset of the
// differences and an integer counting the total number of differences.
// The diff count should always reflect the scale of the differences between
// the current state of the PR and the query, but the message returned need not
// attempt to convey all of that information if some differences are more severe.
// For instance, we need to convey that a PR is open against a forbidden branch
// more than we need to detail which status contexts are failed against the PR.
// To this end, some differences are given a higher diff weight than others.
// Note: an empty diff can be returned if the reason that the PR does not match
// the TideQuery is unknown. This can happen if this function's logic
// does not match GitHub's and does not indicate that the PR matches the query.
requirementDiff calculates the diff between a PR and a TideQuery . This diff is defined with a string that describes some subset of the differences and an integer counting the total number of differences . The diff count should always reflect the scale of the differences between the current state of the PR and the query but the message returned need not attempt to convey all of that information if some differences are more severe . For instance we need to convey that a PR is open against a forbidden branch more than we need to detail which status contexts are failed against the PR . To this end some differences are given a higher diff weight than others . Note : an empty diff can be returned if the reason that the PR does not match the TideQuery is unknown . This can happen if this function s logic does not match GitHub s and does not indicate that the PR matches the query .