mirror of
https://github.com/juanfont/headscale.git
synced 2026-10-27 16:56:49 +09:00
Open PR with commits by others: rebase onto base, build on top, push with lease. Rebase conflict pauses and comments once.
173 lines
4.8 KiB
Go
173 lines
4.8 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// changedFiles lists paths that differ from HEAD, tracked or not.
|
|
func changedFiles(ctx context.Context, r *repo) ([]string, error) {
|
|
out, err := r.run(ctx, "git", "status", "--porcelain", "--untracked-files=all")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var files []string
|
|
|
|
for line := range strings.Lines(out) {
|
|
line = strings.TrimRight(line, "\n")
|
|
if len(line) < 4 {
|
|
continue
|
|
}
|
|
|
|
path := strings.TrimSpace(line[3:])
|
|
// Renames read as "old -> new"; the destination is what matters.
|
|
if _, dst, ok := strings.Cut(path, " -> "); ok {
|
|
path = dst
|
|
}
|
|
|
|
files = append(files, strings.Trim(path, `"`))
|
|
}
|
|
|
|
return files, nil
|
|
}
|
|
|
|
func headSHA(ctx context.Context, r *repo) (string, error) {
|
|
out, err := r.run(ctx, "git", "rev-parse", "HEAD")
|
|
|
|
return strings.TrimSpace(out), err
|
|
}
|
|
|
|
// treeSHA identifies the content of the working commit, independent of message
|
|
// or parentage. It is what the pull request marker records so a rerun can tell
|
|
// "nothing new" from "same change, previously rejected".
|
|
func treeSHA(ctx context.Context, r *repo) (string, error) {
|
|
out, err := r.run(ctx, "git", "rev-parse", "HEAD^{tree}")
|
|
|
|
return strings.TrimSpace(out), err
|
|
}
|
|
|
|
// resetTo rewinds the worktree to sha, leaving nothing behind. Every dropped
|
|
// area goes through here, which is why a drop cannot leave a partial edit in
|
|
// the pull request.
|
|
func resetTo(ctx context.Context, r *repo, sha string) error {
|
|
if _, err := r.run(ctx, "git", "reset", "--hard", sha); err != nil { //nolint:noinlineerr
|
|
return err
|
|
}
|
|
|
|
_, err := r.run(ctx, "git", "clean", "-fd")
|
|
|
|
return err
|
|
}
|
|
|
|
func commitAll(ctx context.Context, r *repo, message string) error {
|
|
if _, err := r.run(ctx, "git", "add", "--all"); err != nil { //nolint:noinlineerr
|
|
return err
|
|
}
|
|
|
|
// The bot's own commits are machine-generated and already gated; the
|
|
// hooks re-run the same checks far more slowly.
|
|
_, err := r.run(ctx, "git", "commit", "--no-verify", "--message", message)
|
|
|
|
return err
|
|
}
|
|
|
|
var errRebaseConflict = errors.New("hand-pushed commits do not rebase cleanly")
|
|
|
|
// startBranch rebuilds the working branch from the base every run, so it never
|
|
// accumulates history and the bot keeps no state on disk between runs. The
|
|
// exception is an open pull request holding commits someone pushed by hand:
|
|
// rebuilding would erase them, so the branch is rebased onto the base and the
|
|
// run builds on top. It returns the subjects of the commits it carried.
|
|
func startBranch(ctx context.Context, r *repo, remote, base, branch string, carry bool) ([]string, error) {
|
|
if _, err := r.run(ctx, "git", "fetch", remote, base); err != nil { //nolint:noinlineerr
|
|
return nil, err
|
|
}
|
|
|
|
var hand []string
|
|
|
|
if carry {
|
|
var err error
|
|
|
|
hand, err = handCommits(ctx, r, remote, base, branch)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
start := remote + "/" + base
|
|
if len(hand) > 0 {
|
|
start = remote + "/" + branch
|
|
}
|
|
|
|
if _, err := r.run(ctx, "git", "checkout", "-B", branch, start); err != nil { //nolint:noinlineerr
|
|
return nil, err
|
|
}
|
|
|
|
if len(hand) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
if _, err := r.run(ctx, "git", "rebase", remote+"/"+base); err != nil { //nolint:noinlineerr
|
|
_, _ = r.run(ctx, "git", "rebase", "--abort")
|
|
|
|
return nil, fmt.Errorf("%w onto %s: %w", errRebaseConflict, base, err)
|
|
}
|
|
|
|
return hand, nil
|
|
}
|
|
|
|
// handCommits lists, oldest first, the subjects of commits on the remote branch
|
|
// authored by anyone but the configured committer.
|
|
func handCommits(ctx context.Context, r *repo, remote, base, branch string) ([]string, error) {
|
|
if _, err := r.run(ctx, "git", "fetch", remote, branch); err != nil { //nolint:noinlineerr
|
|
return nil, err
|
|
}
|
|
|
|
self, err := r.run(ctx, "git", "config", "user.email")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
out, err := r.run(ctx, "git", "log", "--reverse", "--format=%ae%x00%s",
|
|
remote+"/"+base+".."+remote+"/"+branch)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return notAuthoredBy(out, strings.TrimSpace(self)), nil
|
|
}
|
|
|
|
// notAuthoredBy filters `git log --format=%ae%x00%s` output to the subjects of
|
|
// commits whose author is not email.
|
|
func notAuthoredBy(log, email string) []string {
|
|
var subjects []string
|
|
|
|
for line := range strings.Lines(log) {
|
|
author, subject, _ := strings.Cut(strings.TrimRight(line, "\n"), "\x00")
|
|
if author != email {
|
|
subjects = append(subjects, subject)
|
|
}
|
|
}
|
|
|
|
return subjects
|
|
}
|
|
|
|
// forcePush replaces branch on remote. A non-empty lease is the tip the run
|
|
// started from; the push is refused if anyone moved the branch since.
|
|
func forcePush(ctx context.Context, r *repo, remote, branch, lease string) error {
|
|
force := "--force"
|
|
if lease != "" {
|
|
force = "--force-with-lease=refs/heads/" + branch + ":" + lease
|
|
}
|
|
|
|
_, err := r.run(ctx, "git", "push", force, remote, "HEAD:refs/heads/"+branch)
|
|
if err != nil {
|
|
return fmt.Errorf("pushing %s: %w", branch, err)
|
|
}
|
|
|
|
return nil
|
|
}
|