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 }
// EnsureValueFilled returns an error in case the value passed in is not set. // // The function checks structs and slices recursively. func EnsureValueFilled(value interface{}, path string) error { logger := log.V(log.Debug) // Turn the interface into reflect.Value. var ( v = reflect.ValueOf(value) t = v.Type() ) logger.Log(fmt.Sprintf(`config.EnsureValueFilled: Checking "%v" ... `, path)) // Handle pointers in a special way. if kind := v.Kind(); kind == reflect.Ptr || kind == reflect.Slice { if v.IsNil() { logger.NewLine(" ---> Nil") return &ErrKeyNotSet{path} } } // Decide what to do depending on the value kind. iv := reflect.Indirect(v) switch iv.Kind() { case reflect.Struct: return ensureStructFilled(iv, path) case reflect.Slice: return ensureSliceFilled(iv, path) } // In case the value is not valid, return an error. if !v.IsValid() { logger.NewLine(" ---> Invalid") return &ErrKeyNotSet{path} } // In case the field is set to the zero value of the given type, // we return an error since the field is not set. if reflect.DeepEqual(v.Interface(), reflect.Zero(t).Interface()) { logger.NewLine(" ---> Unset") return &ErrKeyNotSet{path} } if logger { logger.NewLine(fmt.Sprintf(" ---> OK (set to '%v')", v.Interface())) } return nil }
// searchIssues can be used to query GitHub for issues matching the given filter. // It handles pagination internally, it fetches all matching issues automatically. func (tracker *issueTracker) searchIssues( queryFormat string, v ...interface{}, ) ([]*github.Issue, error) { logger := log.V(log.Debug) // Format the query. query := fmt.Sprintf(queryFormat, v...) // Since GH API does not allow OR queries, we need to send a concurrent request // for every item in tracker.config.StoryLabels label list. ch := make(chan *searchResult, len(tracker.config.StoryLabels)) for _, label := range tracker.config.StoryLabels { go func(label string) { // We are only interested in issues for the given repository. innerQuery := fmt.Sprintf(`%v type:issue repo:%v/%v label:"%v"`, query, tracker.config.GitHubOwner, tracker.config.GitHubRepository, label) task := "Search GitHub: " + innerQuery if logger { logger.Go(task) } searchOpts := &github.SearchOptions{} searchOpts.Page = 1 searchOpts.PerPage = 50 var ( acc []*github.Issue searched int ) client := tracker.newClient() for { // Fetch another page. var ( result *github.IssuesSearchResult err error ) withRequestAllocated(func() { result, _, err = client.Search.Issues(innerQuery, searchOpts) }) if err != nil { ch <- &searchResult{nil, errs.NewError(task, err)} return } // Check the issues for exact string match. for i := range result.Issues { acc = append(acc, &result.Issues[i]) } // Check whether we have reached the end or not. searched += len(result.Issues) if searched == *result.Total { ch <- &searchResult{acc, nil} return } // Check the next page in the next iteration. searchOpts.Page += 1 } }(label) } // Collect the results. var issues []*github.Issue for i := 0; i < cap(ch); i++ { res := <-ch if err := res.err; err != nil { return nil, err } issues = append(issues, res.issues...) } // Make sure there are no duplicates in the list. return dedupeIssues(issues), nil }
func filterBranches(storyBranches []*git.GitBranch, trunkName string) ([]*gitBranch, error) { // Pair the branches with commit ranges specified by trunk..story task := "Collected commits associated with the story branches" branches := make([]*gitBranch, 0, len(storyBranches)) for _, branch := range storyBranches { var revRange string if branch.BranchName != "" { // Handle branches that exist locally. revRange = fmt.Sprintf("%v..%v", trunkName, branch.BranchName) } else { // Handle branches that exist only in the remote repository. // We can use trunkName here since trunk is up to date. revRange = fmt.Sprintf("%v..%v/%v", trunkName, branch.Remote, branch.RemoteBranchName) } commits, err := git.ShowCommitRange(revRange) if err != nil { return nil, errs.NewError(task, err) } branches = append(branches, &gitBranch{ tip: branch, commits: commits, }) continue } // Collect story tags. task = "Collect affected story tags" tracker, err := modules.GetIssueTracker() if err != nil { return nil, errs.NewError(task, err) } tags := make([]string, 0, len(storyBranches)) BranchLoop: for _, branch := range branches { for _, commit := range branch.commits { commitTag := commit.StoryIdTag // Make sure the tag is not in the list already. for _, tag := range tags { if tag == commitTag { continue BranchLoop } } // Drop tags not recognized by the current issue tracker. _, err := tracker.StoryTagToReadableStoryId(commitTag) if err == nil { tags = append(tags, commitTag) } } } // Fetch the collected stories. task = "Fetch associated stories from the issue tracker" log.Run(task) stories, err := tracker.ListStoriesByTag(tags) if err != nil { return nil, errs.NewError(task, err) } // Filter the branches according to the story state. storyByTag := make(map[string]common.Story, len(stories)) for i, story := range stories { // tags[i] corresponds to stories[i] tag := tags[i] if story != nil { storyByTag[tag] = story } else { log.Warn(fmt.Sprintf("Story for tag '%v' was not found in the issue tracker", tag)) } } allowedStates := allowedStoryStates() // checkCommits returns whether the commits passed in are ok // considering the state of the stories found in these commits, // whether the branch containing these commits can be deleted. checkCommits := func(commits []*git.Commit) (common.StoryState, bool) { var storyFound bool for _, commit := range commits { // Skip commits with empty Story-Id tag. if commit.StoryIdTag == "" { continue } // In case the story is not found, the tag is not recognized // by the current issue tracker. In that case we just skip the commit. story, ok := storyByTag[commit.StoryIdTag] if !ok { continue } // When the story state associated with the commit is not ok, // we can return false here to reject the branch. storyState := story.State() if _, ok := allowedStates[storyState]; !ok { return storyState, false } storyFound = true } // We went through all the commits and they are fine, check passed. return common.StoryStateInvalid, storyFound } // Go through the branches and only return these that // comply with the story state requirements. bs := make([]*gitBranch, 0, len(branches)) for _, branch := range branches { tip := branch.tip logger := log.V(log.Verbose) if logger { logger.Log(fmt.Sprintf("Processing branch %v", tip.CanonicalName())) } // The branch can be for sure deleted in case there are no commits // contained in the commit range. That means the branch is merged into trunk. if len(branch.commits) == 0 { if logger { logger.Log(" Include the branch (reason: merged into trunk)") } branch.reason = "merged" bs = append(bs, branch) continue } // In case the commit check passed, we append the branch. state, ok := checkCommits(branch.commits) if ok { if logger { logger.Log(" Include the branch (reason: branch check passed)") } branch.reason = "check passed" bs = append(bs, branch) continue } // Otherwise we print the skip warning. if logger { if state == common.StoryStateInvalid { logger.Log( " Exclude the branch (reason: no story commits found on the branch)") } else { logger.Log(fmt.Sprintf( " Exclude the branch (reason: story state is '%v')", state)) } } } return bs, nil }
// Log just calls LogWith(err, log.V(log.Info)). func Log(err error) error { return LogWith(err, log.V(log.Info)) }
func postReviewRequestForCommit( ctx *common.ReviewContext, opts map[string]interface{}, ) error { var ( commit = ctx.Commit story = ctx.Story ) // Assert that certain field are set. switch { case commit.SHA == "": panic("SHA not set for the commit being posted") case commit.StoryIdTag == "": panic("story ID not set for the commit being posted") } // Load the RB config. config, err := LoadConfig() if err != nil { return err } // Parse the options. var ( fixes = formatOptInteger(opts["fixes"]) update = formatOptInteger(opts["update"]) open bool ) if _, ok := opts["open"]; ok { open = true } // Post the review request. args := []string{"post", "--server", config.ServerURL().String(), "--guess-fields", "yes", } if story != nil { args = append(args, "--bugs-closed", commit.StoryIdTag) } if fixes != "" { args = append(args, "--depends-on", fixes) } if update != "" { args = append(args, "--review-request-id", update) } if open { args = append(args, "--open") } args = append(args, commit.SHA) var task string if update != "" { task = "Update a Review Board review request with commit " + commit.SHA } else { task = "Create a Review Board review request for commit " + commit.SHA } log.Run(task) stdout, stderr, err := shell.Run("rbt", args...) if err != nil { // rbt is retarded and sometimes prints stderr to stdout. // That is why we return stdout when stderr is empty. if stderr.Len() == 0 { return errs.NewErrorWithHint(task, err, stdout.String()) } else { return errs.NewErrorWithHint(task, err, stderr.String()) } } logger := log.V(log.Info) logger.Lock() logger.UnsafeNewLine("") logger.UnsafeOk(task) fmt.Print(stdout) logger.Unlock() return nil }