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 }
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 }
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 }
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 }
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 }