func main()

in cmd/git-sync/main.go [244:566]


func main() {
	// In case we come up as pid 1, act as init.
	if os.Getpid() == 1 {
		fmt.Fprintf(os.Stderr, "INFO: detected pid 1, running init handler\n")
		code, err := pid1.ReRun()
		if err == nil {
			os.Exit(code)
		}
		fmt.Fprintf(os.Stderr, "ERROR: unhandled pid1 error: %v\n", err)
		os.Exit(127)
	}

	setFlagDefaults()
	flag.Parse()

	log = logging.New(*flRoot, *flErrorFile)
	cmdRunner = cmd.NewRunner(log)

	if *flVer {
		fmt.Println(version.VERSION)
		os.Exit(0)
	}

	if *flRepo == "" {
		handleError(true, "ERROR: --repo must be specified")
	}

	if *flDepth < 0 { // 0 means "no limit"
		handleError(true, "ERROR: --depth must be greater than or equal to 0")
	}

	switch *flSubmodules {
	case submodulesRecursive, submodulesShallow, submodulesOff:
	default:
		handleError(true, "ERROR: --submodules must be one of %q, %q, or %q", submodulesRecursive, submodulesShallow, submodulesOff)
	}

	if *flRoot == "" {
		handleError(true, "ERROR: --root must be specified")
	}

	if *flDest == "" {
		parts := strings.Split(strings.Trim(*flRepo, "/"), "/")
		*flDest = parts[len(parts)-1]
	}
	if !filepath.IsAbs(*flDest) {
		*flDest = filepath.Join(*flRoot, *flDest)
	}

	if *flWait < 0 {
		handleError(true, "ERROR: --wait must be greater than or equal to 0")
	}

	if *flSyncTimeout < 0 {
		handleError(true, "ERROR: --timeout must be greater than 0")
	}

	if *flWebhookURL != "" {
		if *flWebhookStatusSuccess < -1 {
			handleError(true, "ERROR: --webhook-success-status must be a valid HTTP code or -1")
		}
		if *flWebhookTimeout < time.Second {
			handleError(true, "ERROR: --webhook-timeout must be at least 1s")
		}
		if *flWebhookBackoff < time.Second {
			handleError(true, "ERROR: --webhook-backoff must be at least 1s")
		}
	}

	// Convert deprecated sync-hook-command flag to exechook-command flag
	if *flExechookCommand == "" && *flSyncHookCommand != "" {
		*flExechookCommand = *flSyncHookCommand
		log.Info("--sync-hook-command is deprecated, please use --exechook-command instead")
	}

	if *flExechookCommand != "" {
		if *flExechookTimeout < time.Second {
			handleError(true, "ERROR: --exechook-timeout must be at least 1s")
		}
		if *flExechookBackoff < time.Second {
			handleError(true, "ERROR: --exechook-backoff must be at least 1s")
		}
	}

	if _, err := exec.LookPath(*flGitCmd); err != nil {
		handleError(false, "ERROR: git executable %q not found: %v", *flGitCmd, err)
	}

	if *flPassword != "" && *flPasswordFile != "" {
		handleError(false, "ERROR: only one of --password and --password-file may be specified")
	}
	if *flUsername != "" {
		if *flPassword == "" && *flPasswordFile == "" {
			handleError(true, "ERROR: --password or --password-file must be set when --username is specified")
		}
	}

	if *flSSH {
		if *flUsername != "" {
			handleError(false, "ERROR: only one of --ssh and --username may be specified")
		}
		if *flPassword != "" {
			handleError(false, "ERROR: only one of --ssh and --password may be specified")
		}
		if *flPasswordFile != "" {
			handleError(false, "ERROR: only one of --ssh and --password-file may be specified")
		}
		if *flAskPassURL != "" {
			handleError(false, "ERROR: only one of --ssh and --askpass-url may be specified")
		}
		if *flCookieFile {
			handleError(false, "ERROR: only one of --ssh and --cookie-file may be specified")
		}
		if *flSSHKeyFile == "" {
			handleError(true, "ERROR: --ssh-key-file must be specified when --ssh is specified")
		}
		if *flSSHKnownHosts {
			if *flSSHKnownHostsFile == "" {
				handleError(true, "ERROR: --ssh-known-hosts-file must be specified when --ssh-known-hosts is specified")
			}
		}
	}

	if *flAddUser {
		if err := addUser(); err != nil {
			handleError(false, "ERROR: can't write to /etc/passwd: %v", err)
		}
	}

	// This context is used only for git credentials initialization. There are no long-running operations like
	// `git clone`, so initTimeout set to 30 seconds should be enough.
	ctx, cancel := context.WithTimeout(context.Background(), initTimeout)

	if *flUsername != "" {
		if *flPasswordFile != "" {
			passwordFileBytes, err := ioutil.ReadFile(*flPasswordFile)
			if err != nil {
				log.Error(err, "ERROR: can't read password file")
				os.Exit(1)
			}
			*flPassword = string(passwordFileBytes)
		}
		if err := setupGitAuth(ctx, *flUsername, *flPassword, *flRepo); err != nil {
			handleError(false, "ERROR: can't create .netrc file: %v", err)
		}
	}

	if *flSSH {
		if err := setupGitSSH(*flSSHKnownHosts); err != nil {
			handleError(false, "ERROR: can't configure SSH: %v", err)
		}
	}

	if *flCookieFile {
		if err := setupGitCookieFile(ctx); err != nil {
			handleError(false, "ERROR: can't set git cookie file: %v", err)
		}
	}

	if *flAskPassURL != "" {
		if err := callGitAskPassURL(ctx, *flAskPassURL); err != nil {
			askpassCount.WithLabelValues(metricKeyError).Inc()
			handleError(false, "ERROR: failed to call ASKPASS callback URL: %v", err)
		}
		askpassCount.WithLabelValues(metricKeySuccess).Inc()
	}

	// This needs to be after all other git-related config flags.
	if *flGitConfig != "" {
		if err := setupExtraGitConfigs(ctx, *flGitConfig); err != nil {
			fmt.Fprintf(os.Stderr, "ERROR: can't set additional git configs: %v\n", err)
			os.Exit(1)
		}
	}

	// The scope of the initialization context ends here, so we call cancel to release resources associated with it.
	cancel()

	if *flHTTPBind != "" {
		ln, err := net.Listen("tcp", *flHTTPBind)
		if err != nil {
			handleError(false, "ERROR: unable to bind HTTP endpoint: %v", err)
		}
		mux := http.NewServeMux()
		go func() {
			if *flHTTPMetrics {
				mux.Handle("/metrics", promhttp.Handler())
			}

			if *flHTTPprof {
				mux.HandleFunc("/debug/pprof/", pprof.Index)
				mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
				mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
				mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
				mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
			}

			// This is a dumb liveliness check endpoint. Currently this checks
			// nothing and will always return 200 if the process is live.
			mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
				if !getRepoReady() {
					http.Error(w, "repo is not ready", http.StatusServiceUnavailable)
				}
				// Otherwise success
			})
			http.Serve(ln, mux)
		}()
	}

	// From here on, output goes through logging.
	log.V(0).Info("starting up", "pid", os.Getpid(), "args", os.Args)

	// Startup webhooks goroutine
	var webhookRunner *hook.HookRunner
	if *flWebhookURL != "" {
		webhook := hook.NewWebhook(
			*flWebhookURL,
			*flWebhookMethod,
			*flWebhookStatusSuccess,
			*flWebhookTimeout,
			log,
		)
		webhookRunner = hook.NewHookRunner(
			webhook,
			*flWebhookBackoff,
			hook.NewHookData(),
			log,
			*flOneTime,
		)
		go webhookRunner.Run(context.Background())
	}

	// Startup exechooks goroutine
	var exechookRunner *hook.HookRunner
	if *flExechookCommand != "" {
		exechook := hook.NewExechook(
			cmd.NewRunner(log),
			*flExechookCommand,
			*flRoot,
			[]string{},
			*flExechookTimeout,
			log,
		)
		exechookRunner = hook.NewHookRunner(
			exechook,
			*flExechookBackoff,
			hook.NewHookData(),
			log,
			*flOneTime,
		)
		go exechookRunner.Run(context.Background())
	}

	initialSync := true
	failCount := 0
	for {
		start := time.Now()
		ctx, cancel := context.WithTimeout(context.Background(), time.Second*time.Duration(*flSyncTimeout))
		if changed, hash, err := syncRepo(ctx, *flRepo, *flBranch, *flRev, *flDepth, *flRoot, *flDest, *flAskPassURL, *flSubmodules); err != nil {
			updateSyncMetrics(metricKeyError, start)
			if *flMaxSyncFailures != -1 && failCount >= *flMaxSyncFailures {
				// Exit after too many retries, maybe the error is not recoverable.
				log.Error(err, "too many failures, aborting", "failCount", failCount)
				os.Exit(1)
			}

			failCount++
			log.Error(err, "unexpected error syncing repo, will retry")
			log.V(0).Info("waiting before retrying", "waitTime", waitTime(*flWait))
			cancel()
			time.Sleep(waitTime(*flWait))
			continue
		} else if changed {
			if webhookRunner != nil {
				webhookRunner.Send(hash)
			}
			if exechookRunner != nil {
				exechookRunner.Send(hash)
			}
			updateSyncMetrics(metricKeySuccess, start)
		} else {
			updateSyncMetrics(metricKeyNoOp, start)
		}

		if initialSync {
			// Determine if git-sync should terminate for one of several reasons
			if *flOneTime {
				// Wait for hooks to complete at least once, if not nil, before
				// checking whether to stop program.
				// Assumes that if hook channels are not nil, they will have at
				// least one value before getting closed
				exitCode := 0 // is 0 if all hooks succeed, else is 1
				if exechookRunner != nil {
					if err := exechookRunner.WaitForCompletion(); err != nil {
						exitCode = 1
					}
				}
				if webhookRunner != nil {
					if err := webhookRunner.WaitForCompletion(); err != nil {
						exitCode = 1
					}
				}
				log.DeleteErrorFile()
				os.Exit(exitCode)
			}
			if isHash, err := revIsHash(ctx, *flRev, *flRoot); err != nil {
				log.Error(err, "can't tell if rev is a git hash, exiting", "rev", *flRev)
				os.Exit(1)
			} else if isHash {
				log.V(0).Info("rev appears to be a git hash, no further sync needed", "rev", *flRev)
				log.DeleteErrorFile()
				sleepForever()
			}
			initialSync = false
		}

		failCount = 0
		log.DeleteErrorFile()
		log.V(1).Info("next sync", "wait_time", waitTime(*flWait))
		cancel()
		time.Sleep(waitTime(*flWait))
	}
}