示例#1
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
}
示例#2
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
}
示例#3
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
}
示例#4
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
}
示例#5
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
}