diff --git a/.ruby-version b/.ruby-version deleted file mode 100644 index ef538c2..0000000 --- a/.ruby-version +++ /dev/null @@ -1 +0,0 @@ -3.1.2 diff --git a/Gemfile b/Gemfile deleted file mode 100644 index 80c7112..0000000 --- a/Gemfile +++ /dev/null @@ -1,3 +0,0 @@ -source "https://rubygems.org" - -gem "async", "~> 2.0" diff --git a/Gemfile.lock b/Gemfile.lock deleted file mode 100644 index 83aa7d5..0000000 --- a/Gemfile.lock +++ /dev/null @@ -1,21 +0,0 @@ -GEM - remote: https://rubygems.org/ - specs: - async (2.0.3) - console (~> 1.10) - io-event (~> 1.0.0) - timers (~> 4.1) - console (1.15.3) - fiber-local - fiber-local (1.0.0) - io-event (1.0.9) - timers (4.3.3) - -PLATFORMS - x86_64-darwin-20 - -DEPENDENCIES - async (~> 2.0) - -BUNDLED WITH - 2.3.7 diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..fa2a903 --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module github.com/broothie/self-referential-commit + +go 1.18 + +require ( + github.com/pkg/errors v0.9.1 + golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..33c820c --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f h1:Ax0t5p6N38Ga0dThY21weqDEyz2oklo4IvDkpigvkD8= +golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..b4771e5 --- /dev/null +++ b/main.go @@ -0,0 +1,145 @@ +package main + +import ( + "context" + "flag" + "fmt" + "math/rand" + "os" + "os/exec" + "path" + "strconv" + "strings" + "time" + + "github.com/pkg/errors" + "golang.org/x/sync/errgroup" +) + +func init() { + rand.Seed(time.Now().UnixNano()) +} + +func main() { + start := time.Now() + if err := os.RemoveAll("clones"); err != nil { + panic(err) + } + + if err := os.RemoveAll("short.sha"); err != nil { + panic(err) + } + + // Args + workers := flag.Int("w", 1, "number of workers") + flag.Parse() + + // Async + ctx, cancel := context.WithCancel(context.Background()) + group, ctx := errgroup.WithContext(ctx) + shaChan := make(chan string) + + // Start workers + for worker := 0; worker < *workers; worker++ { + group.Go(findLuckySHA(ctx, start, worker, shaChan)) + } + + sha := <-shaChan + fmt.Printf("success! the lucky sha was %q\n", sha) + + cancel() + if err := group.Wait(); err != nil { + panic(err) + } +} + +func findLuckySHA(ctx context.Context, start time.Time, worker int, shaChan chan string) func() error { + return func() error { + repoPath := path.Join("clones", strconv.Itoa(int(start.Unix())), fmt.Sprintf("%d-self-referential-commit", worker)) + if err := gitInit(ctx, repoPath); err != nil { + return errors.Wrapf(err, "failed to git init %q", repoPath) + } + + for { + select { + case <-ctx.Done(): + return nil + default: + shortSha := randomShortSHA() + message := fmt.Sprintf("short sha: %s", shortSha) + output, err := gitCommit(ctx, repoPath, message) + if err != nil { + return err + } + + if strings.TrimSpace(output) == fmt.Sprintf("[main %s] %s", shortSha, message) { + shaChan <- shortSha + close(shaChan) + + if err := os.WriteFile("short.sha", []byte(shortSha), 0666); err != nil { + return errors.Wrapf(err, "failed to write file under repo %q", repoPath) + } + } else { + if err := gitReset(ctx, repoPath); err != nil { + return err + } + + if err := gitGC(ctx, repoPath); err != nil { + return err + } + } + } + } + } +} + +func gitInit(ctx context.Context, repoPath string) error { + output, err := exec.CommandContext(ctx, "git", "clone", "https://github.com/broothie/self-referential-commit.git", repoPath).CombinedOutput() + if err != nil { + fmt.Println(string(output)) + return errors.Wrapf(err, "failed to init git repo at %q", repoPath) + } + + return nil +} + +func gitCommit(ctx context.Context, repoPath, message string) (string, error) { + output, err := exec.CommandContext(ctx, "git", "-C", repoPath, "commit", "--allow-empty", "-m", message).CombinedOutput() + if err != nil { + fmt.Println(string(output)) + return "", errors.Wrapf(err, "failed to commit to repo at %q", repoPath) + } + + return string(output), nil +} + +func gitReset(ctx context.Context, repoPath string) error { + output, err := exec.CommandContext(ctx, "git", "-C", repoPath, "reset", "--hard", "HEAD~").CombinedOutput() + if err != nil { + fmt.Println(string(output)) + return errors.Wrapf(err, "failed to reset repo at %q", repoPath) + } + + return nil +} + +func gitGC(ctx context.Context, repoPath string) error { + output, err := exec.CommandContext(ctx, "git", "-C", repoPath, "gc").CombinedOutput() + if err != nil { + fmt.Println(string(output)) + return errors.Wrapf(err, "failed to gc repo at %q", repoPath) + } + + return nil +} + +func randomShortSHA() string { + const hexRunes = "0123456789abcdef" + + runes := make([]rune, 7) + for i := range runes { + runes[i] = rune(hexRunes[rand.Intn(len(hexRunes))]) + } + + return string(runes) +} diff --git a/script.rb b/script.rb deleted file mode 100644 index b505732..0000000 --- a/script.rb +++ /dev/null @@ -1,35 +0,0 @@ -require 'async' -require 'fileutils' - -NUM_WORKERS = ENV.fetch('NUM_WORKERS', 1) -CHARACTERS = (('0'..'9').to_a + ('a'..'f').to_a).freeze - -start_time = Time.now - -Async do |task| - NUM_WORKERS.times do |worker| - task.async do - repo_path = File.join('clones', start_time.to_i.to_s, "#{worker}-self-referential-commit") - FileUtils.mkdir_p(repo_path) - - puts `git clone https://github.com/broothie/self-referential-commit.git #{repo_path}` - - loop do - short_sha = Array.new(7) { CHARACTERS.sample }.join - puts "attempt with short sha #{short_sha}" - - message = "short sha: #{short_sha}" - puts output = `git -C #{repo_path} commit --allow-empty -m '#{message}'`.chomp - - if output == "[main #{short_sha}] #{message}" - File.write('short.sha', short_sha) - puts "success! the lucky short sha is #{short_sha}, under #{repo_path}" - task.stop - else - puts `git -C #{repo_path} reset --hard HEAD~` - puts `git -C #{repo_path} gc` - end - end - end - end -end