Files
gitocean/services/repository/files/cherry_pick.go
T
c956dae73d feat: adds option to force overwrite new branch for /contents route (#12663)
Adds an option "force_overwrite_new_branch" when posting to
/repos/{owner}/{repo}/contents to modify multiple files in a repository.
When user provides both "branch" and "new_branch" options, and
"new_branch" already exists, the "force_overwrite_new_branch" option
allows the user to overwrite the existing branch. Under the hood this
amounts to a "git push --force".

[Issue #12600](https://codeberg.org/forgejo/forgejo/issues/12600)

### Tests for Go changes

- I added test coverage for Go changes...
  - [ ] in their respective `*_test.go` for unit tests.
  - [x] in the `tests/integration` directory if it involves interactions with a live Forgejo server.
- I ran...
  - [x] `make pr-go` before pushing

### Documentation

- [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change.
- [ ] I did not document these changes and I do not expect someone else to do it.
- [x] API swagger docs updated

### Release notes

- [x] This change will be noticed by a Forgejo user or admin (feature, bug fix, performance, etc.). I suggest to include a release note for this change.
- [ ] This change is not visible to a Forgejo user or admin (refactor, dependency upgrade, etc.). I think there is no need to add a release note for this change.

Co-authored-by: Rob Gonnella <rob.gonnella@papayapay.com>
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/12663
Reviewed-by: Mathieu Fenniak <mfenniak@noreply.codeberg.org>
2026-06-06 16:45:57 +02:00

142 lines
3.9 KiB
Go

// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package files
import (
"context"
"errors"
"fmt"
"strings"
"forgejo.org/models"
repo_model "forgejo.org/models/repo"
user_model "forgejo.org/models/user"
"forgejo.org/modules/git"
"forgejo.org/modules/log"
"forgejo.org/modules/structs"
"forgejo.org/services/pull"
)
// CherryPick cherrypicks or reverts a commit to the given repository
func CherryPick(ctx context.Context, repo *repo_model.Repository, doer *user_model.User, revert bool, opts *ApplyDiffPatchOptions) (*structs.FileResponse, error) {
if err := opts.Validate(ctx, repo, doer); err != nil {
return nil, err
}
message := strings.TrimSpace(opts.Message)
author, committer := GetAuthorAndCommitterUsers(opts.Author, opts.Committer, doer)
t, err := NewTemporaryUploadRepository(ctx, repo)
if err != nil {
log.Error("NewTemporaryUploadRepository failed: %v", err)
}
defer t.Close()
if err := t.Clone(opts.OldBranch, false); err != nil {
return nil, err
}
if err := t.SetDefaultIndex(); err != nil {
return nil, err
}
if err := t.RefreshIndex(); err != nil {
return nil, err
}
// Get the commit of the original branch
commit, err := t.GetBranchCommit(opts.OldBranch)
if err != nil {
return nil, err // Couldn't get a commit for the branch
}
// Assigned LastCommitID in opts if it hasn't been set
if opts.LastCommitID == "" {
opts.LastCommitID = commit.ID.String()
} else {
lastCommitID, err := t.gitRepo.ConvertToGitID(opts.LastCommitID)
if err != nil {
return nil, fmt.Errorf("CherryPick: Invalid last commit ID: %w", err)
}
opts.LastCommitID = lastCommitID.String()
if commit.ID.String() != opts.LastCommitID {
return nil, models.ErrCommitIDDoesNotMatch{
GivenCommitID: opts.LastCommitID,
CurrentCommitID: opts.LastCommitID,
}
}
}
commit, err = t.GetCommit(strings.TrimSpace(opts.Content))
if err != nil {
return nil, err
}
parent, err := commit.ParentID(0)
if err != nil {
parent = git.ObjectFormatFromName(repo.ObjectFormatName).EmptyTree()
}
base, right := parent.String(), commit.ID.String()
if revert {
right, base = base, right
}
var treeHash string
if git.SupportGitMergeTree {
var conflict bool
treeHash, conflict, _, err = pull.MergeTree(ctx, t.gitRepo, base, opts.LastCommitID, right, nil)
if err != nil {
return nil, fmt.Errorf("failed to three-way merge %s onto %s: %w", right, opts.OldBranch, err)
}
if conflict {
return nil, errors.New("failed to merge due to conflicts")
}
} else {
description := fmt.Sprintf("CherryPick %s onto %s", right, opts.OldBranch)
conflict, _, err := pull.AttemptThreeWayMerge(ctx, t.gitRepo, base, opts.LastCommitID, right, description)
if err != nil {
return nil, fmt.Errorf("failed to three-way merge %s onto %s: %w", right, opts.OldBranch, err)
}
if conflict {
return nil, errors.New("failed to merge due to conflicts")
}
treeHash, err = t.WriteTree()
if err != nil {
// likely non-sensical tree due to merge conflicts...
return nil, err
}
}
// Now commit the tree
var commitHash string
if opts.Dates != nil {
commitHash, err = t.CommitTreeWithDate("HEAD", author, committer, treeHash, message, opts.Signoff, opts.Dates.Author, opts.Dates.Committer)
} else {
commitHash, err = t.CommitTree("HEAD", author, committer, treeHash, message, opts.Signoff)
}
if err != nil {
return nil, err
}
// Then push this tree to NewBranch
if err := t.Push(doer, commitHash, opts.NewBranch, false); err != nil {
return nil, err
}
commit, err = t.GetCommit(commitHash)
if err != nil {
return nil, err
}
fileCommitResponse, _ := GetFileCommitResponse(repo, commit) // ok if fails, then will be nil
verification := GetPayloadCommitVerification(ctx, commit)
fileResponse := &structs.FileResponse{
Commit: fileCommitResponse,
Verification: verification,
}
return fileResponse, nil
}