Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 64 additions & 3 deletions dev/backports/apply/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -305,9 +305,12 @@ func (a applier) prepareWorkingBranch(remote, branchName, workingBranch string)
// cherryPickOrConflict attempts the cherry-pick. changelog.yml is always
// restored to HEAD afterwards — its entries are fully regenerated by this
// pipeline (see extractChangelogFields/InsertEntry), so cherry-picked changes
// to it are redundant. manifest.yml is left as cherry-pick merges it: any
// legitimate, non-version content change is preserved, and a conflict that is
// purely a "version:" line difference (expected, since each backport branch
// to it are redundant. All files outside the target package directory are also
// reset to HEAD unconditionally, scoping the backport to the target package
// only: changes (or conflicts) the source commit introduced in other packages
// are irrelevant and discarded. manifest.yml is left as cherry-pick merges it:
// any legitimate, non-version content change is preserved, and a conflict that
// is purely a "version:" line difference (expected, since each backport branch
// bumps its own version independently) is auto-resolved in favor of the
// current branch — bumpPatchVersion recomputes the version afterwards. A
// manifest.yml conflict block containing anything else is left as a genuine
Expand Down Expand Up @@ -340,6 +343,21 @@ func (a applier) cherryPickOrConflict(sha, branchName, pkg, changelogPath, manif
return nil, fmt.Errorf("restoring changelog after cherry-pick: %w", err)
}

// Scope the cherry-pick to the target package: reset every file outside
// pkgDir to HEAD, discarding both clean changes and conflicts that belong to
// other packages. This also resolves modify/delete conflicts in other
// packages (e.g. a package absent from this backport branch) in favor of
// the branch state, so conflictingFiles() below only surfaces issues that
// actually require the contributor's attention.
pkgDir := filepath.Dir(manifestPath)
relPkgDir, err := filepath.Rel(a.workDir, pkgDir)
if err != nil {
a.abortCherryPick()
return nil, fmt.Errorf("computing relative package dir: %w", err)
}
relPkgDir = filepath.ToSlash(relPkgDir)
a.resetNonPackageChanges(relPkgDir)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a Pull Request contains changes in two different packages and it is selected to backport to one of those specific packages, this function removes the files that are not related to the selected package.

For instance, a pull request updates kubernetes and security_detection_engine packages. The user selects to backport just the kubernetes package. Then this process ensures that the changes about security_detection_engine are not part of the backport process.


if conflict := a.manifestMissingConflict(sha, branchName, pkg, manifestPath); conflict != nil {
return conflict, nil
}
Expand Down Expand Up @@ -394,6 +412,49 @@ func (a applier) abortCherryPick() {
_ = a.git.RunToStderr("reset", "--hard", "HEAD")
}

// resetNonPackageChanges resets all index and working-tree changes outside
// relPkgDir to HEAD. For each such file it first tries "git checkout HEAD --
// <file>", which resolves both clean staged changes and most conflict types.
// When that fails (the file does not exist in HEAD — i.e. a modify/delete
// conflict where HEAD removed the file), it falls back to "git rm --force",
// which removes the file from both the index and the working tree. Errors from
// both commands are non-fatal: conflictingFiles() is authoritative about what
// remains unresolved.
func (a applier) resetNonPackageChanges(relPkgDir string) {
out, err := a.git.Output("status", "--porcelain")
if err != nil {
return
}
var outside []string
for _, line := range strings.Split(out, "\n") {
if len(line) < 4 {
continue
}
file := strings.TrimSpace(line[3:])
// Renamed entries look like "old -> new"; use only the new path.
if idx := strings.Index(file, " -> "); idx != -1 {
file = file[idx+4:]
}
fileFwd := filepath.ToSlash(file)
pkgPrefix := relPkgDir + "/"
if fileFwd == relPkgDir || strings.HasPrefix(fileFwd, pkgPrefix) {
continue
}
outside = append(outside, file)
}
if len(outside) == 0 {
return
}
fmt.Fprintf(os.Stderr, "note: cherry-pick touched %d file(s) outside %s — resetting to HEAD to scope backport to target package\n", len(outside), relPkgDir)
for _, file := range outside {
if err := a.git.RunToStderr("checkout", "HEAD", "--", file); err != nil {
// HEAD does not have this file (e.g. modify/delete conflict where the
// backport branch removed it): remove it from index and working tree.
_ = a.git.RunToStderr("rm", "--force", "--", file)
}
}
}

// manifestMissingConflict reports a conflict Result if manifestPath does not
// exist in the working tree. Called both before the cherry-pick (the
// package doesn't exist on the target backport branch at all yet) and after
Expand Down
Loading
Loading