[demos] Add local-instance target that uses existing Skia checkout

There is some refactoring here too to clean up things like:
 - Avoiding module-level variables
 - Avoiding packages named "common"
 - Putting main() near the top of the file for easier discoverability.

Change-Id: I3deaa26a9e7437538c1af80e62af4f3f4b5d9f3f
Bug: skia:13456
Reviewed-on: https://skia-review.googlesource.com/c/buildbot/+/552059
Reviewed-by: Chris Mumford <cmumford@google.com>
Auto-Submit: Kevin Lubick <kjlubick@google.com>
Commit-Queue: Chris Mumford <cmumford@google.com>
diff --git a/demos/Makefile b/demos/Makefile
index ca1e067..ea5bc93 100644
--- a/demos/Makefile
+++ b/demos/Makefile
@@ -1,5 +1,22 @@
 include ../make/bazel.mk
 
+local-instance:
+	$(BAZEL) build //demos/pages/...
+	$(BAZEL) run //demos/go/demoserver -- \
+        --resources_dir=../_bazel_bin/demos/pages/development \
+		--unsynced_repo_path=${SKIA_ROOT} \
+		--demos_dir=demos.skia.org/demos \
+		--local
+
+local-instance-with-sync:
+	$(BAZEL) build //demos/pages/...
+	$(BAZEL) run //demos/go/demoserver -- \
+        --resources_dir=../_bazel_bin/demos/pages/development \
+		--repo_url=https://skia.googlesource.com/skia \
+		--repo_default_branch=main \
+		--demos_dir=demos.skia.org/demos \
+		--local
+
 release:
 	$(BAZEL) run //demos:push_demos_container
 
diff --git a/demos/go/common/BUILD.bazel b/demos/go/common/BUILD.bazel
deleted file mode 100644
index 8d7d29a..0000000
--- a/demos/go/common/BUILD.bazel
+++ /dev/null
@@ -1,8 +0,0 @@
-load("@io_bazel_rules_go//go:def.bzl", "go_library")
-
-go_library(
-    name = "common",
-    srcs = ["common.go"],
-    importpath = "go.skia.org/infra/demos/go/common",
-    visibility = ["//visibility:public"],
-)
diff --git a/demos/go/demoserver/BUILD.bazel b/demos/go/demoserver/BUILD.bazel
index 68519d7..7d2a60e 100644
--- a/demos/go/demoserver/BUILD.bazel
+++ b/demos/go/demoserver/BUILD.bazel
@@ -6,7 +6,7 @@
     importpath = "go.skia.org/infra/demos/go/demoserver",
     visibility = ["//visibility:private"],
     deps = [
-        "//demos/go/common",
+        "//demos/go/frontend",
         "//go/auth",
         "//go/common",
         "//go/git",
diff --git a/demos/go/demoserver/main.go b/demos/go/demoserver/main.go
index 5d3e5fc..6d722bc 100644
--- a/demos/go/demoserver/main.go
+++ b/demos/go/demoserver/main.go
@@ -17,7 +17,7 @@
 	"github.com/gorilla/mux"
 	"golang.org/x/oauth2/google"
 
-	demos "go.skia.org/infra/demos/go/common"
+	"go.skia.org/infra/demos/go/frontend"
 	"go.skia.org/infra/go/auth"
 	"go.skia.org/infra/go/common"
 	"go.skia.org/infra/go/git"
@@ -32,19 +32,59 @@
 	repoPollInterval = 3 * time.Minute
 )
 
-var (
-	port                   = flag.String("port", ":8000", "HTTP service address (e.g., ':8000')")
-	local                  = flag.Bool("local", false, "Is this running locally for development (use gcloud for auth)")
-	resourcesDir           = flag.String("resources_dir", "./dist", "The directory to find templates, JS, and CSS files. If blank ./dist will be used.")
-	demosRepo              = flag.String("repo_url", "https://skia.googlesource.com/infra-internal", "The repo from where to fetch the demos. Defaults to https://skia.googlesource.com/infra-internal")
-	demosRepoPath          = flag.String("demos_dir", "demos/internal", "The top level directory in the repo that holds the demos.")
-	demosRepoDefaultBranch = flag.String("repo_default_branch", git.MasterBranch, "The default branch of the repo (ie. master or main).")
-)
+func main() {
+	var (
+		port         = flag.String("port", ":8000", "HTTP service address (e.g., ':8000')")
+		local        = flag.Bool("local", false, "Is this running locally for development (use gcloud for auth)")
+		resourcesDir = flag.String("resources_dir", "./dist", "The directory to find templates, JS, and CSS files. If blank ./dist will be used.")
+		repoURL      = flag.String("repo_url", "https://skia.googlesource.com/infra-internal", "The repo from where to fetch the demos. Defaults to https://skia.googlesource.com/infra-internal")
+		demosDir     = flag.String("demos_dir", "demos/internal", "The top level directory in the repo that holds the demos.")
+		branch       = flag.String("repo_default_branch", git.MainBranch, "The branch of the repo to sync (ie. master or main).")
+
+		unsyncedRepoPath = flag.String("unsynced_repo_path", "unset", "If set, will use an already existing Skia checkout and not sync it.")
+	)
+	common.InitWithMust(
+		"demos",
+	)
+
+	ctx := context.Background()
+	if err := setupGit(ctx, *local); err != nil {
+		sklog.Fatalf("Failed to setup git: %s", err)
+	}
+	// Create a threadsafe checkout to serve from.
+	checkoutDir, err := ioutil.TempDir("", "demos_repo")
+	if err != nil {
+		sklog.Fatalf("Unable to create temporary directory for demos checkout: %s", err)
+	}
+	var demos *syncedDemos
+	if *unsyncedRepoPath != "unset" {
+		if empty, err := util.IsDirEmpty(*unsyncedRepoPath); err != nil || empty {
+			sklog.Fatalf("If unsynced_repo_path is specified, it cannot be empty. Do you have the environment variable SKIA_ROOT set?")
+		}
+		demos = newUnsyncedDemos(*unsyncedRepoPath, *demosDir)
+	} else {
+		demos = newSyncedDemos(ctx, *repoURL, *branch, checkoutDir, *demosDir)
+	}
+
+	r := mux.NewRouter()
+	r.PathPrefix("/demo/").HandlerFunc(demos.demoHandler())
+	r.PathPrefix("/dist/").Handler(http.StripPrefix("/dist/", http.FileServer(http.Dir(*resourcesDir))))
+	r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+		http.ServeFile(w, r, filepath.Join(*resourcesDir, "main.html"))
+	})
+
+	h := httputils.LoggingGzipRequestResponse(r)
+	h = httputils.HealthzAndHTTPS(h)
+	http.Handle("/", h)
+	sklog.Info("Ready to serve on http://localhost" + *port)
+	sklog.Fatal(http.ListenAndServe(*port, nil))
+}
 
 type syncedDemos struct {
 	sync.RWMutex
 	repo    *git.Checkout
 	repoURL string
+	branch  string
 	// Absolute path where demos are located.
 	demoPath string
 }
@@ -54,15 +94,17 @@
 // It periodically updates the repo under a lock, and writes a metadata file to demoPath listing
 // the available subdirectories and other repo information. The repo files can be safely served
 // by exposing DemoHandler().
-func newSyncedDemos(ctx context.Context, repoURL, checkoutDir, demoPath string) *syncedDemos {
+func newSyncedDemos(ctx context.Context, repoURL, branch, checkoutDir, demoPath string) *syncedDemos {
 	sklog.Infof("Creating new syncedDemos for %s at %s", repoURL, checkoutDir)
-	s := new(syncedDemos)
+	s := &syncedDemos{
+		branch:  branch,
+		repoURL: repoURL,
+	}
 	var err error
 	s.repo, err = git.NewCheckout(ctx, repoURL, checkoutDir)
 	if err != nil {
 		sklog.Fatal(err)
 	}
-	s.repoURL = repoURL
 	s.demoPath = filepath.Join(s.repo.Dir(), demoPath)
 	sklog.Infof("Serving demos out of '%s'", s.demoPath)
 
@@ -70,18 +112,26 @@
 	return s
 }
 
+// newUnsyncedDemos creates a *syncedDemos pointing to an existing Skia checkout.
+// It does not periodically update anything; this is meant to expedite local development.
+func newUnsyncedDemos(checkoutDir, demoPath string) *syncedDemos {
+	return &syncedDemos{
+		demoPath: filepath.Join(checkoutDir, demoPath),
+	}
+}
+
 // writeMetadata writes a json file containing the list of subdirectories in s.demoPath as well as
 // the hash and URL of the current s.repo revision.
 //
 // This is used to generate a list of demo links on the skia-demos main page.
-func (s *syncedDemos) writeMetadata(ctx context.Context, rev string) error {
+func (s *syncedDemos) writeMetadata(rev string) error {
 	file, err := os.Open(s.demoPath)
 	if err != nil {
 		return skerr.Wrapf(err, "Failed to Open '%s'.", s.demoPath)
 	}
 	defer file.Close()
-	metadata := demos.Metadata{
-		Rev: demos.Revision{
+	metadata := frontend.Metadata{
+		Rev: frontend.Revision{
 			Hash: rev,
 			URL:  fmt.Sprintf("%s/+/%s", s.repoURL, rev),
 		},
@@ -112,7 +162,7 @@
 	s.Lock()
 	defer s.Unlock()
 
-	if err := s.repo.UpdateBranch(ctx, *demosRepoDefaultBranch); err != nil {
+	if err := s.repo.UpdateBranch(ctx, s.branch); err != nil {
 		sklog.Errorf("Failed to update repo: %s", err)
 	}
 
@@ -123,7 +173,7 @@
 
 	sklog.Infof("Checkout at %s.", hash)
 
-	if err = s.writeMetadata(ctx, hash); err != nil {
+	if err = s.writeMetadata(hash); err != nil {
 		sklog.Fatalf("Unable to write metadata: %s", err)
 	}
 }
@@ -140,46 +190,16 @@
 }
 
 // setupGit acquires necessary credentials to clone the repo.
-func setupGit(ctx context.Context) error {
+func setupGit(ctx context.Context, local bool) error {
 	// Start the gitauth package because we will need to read from infra-internal.
 	ts, err := google.DefaultTokenSource(ctx, auth.ScopeUserinfoEmail, auth.ScopeGerrit)
 	if err != nil {
 		return err
 	}
-	if !*local {
+	if !local {
 		if _, err := gitauth.New(ts, filepath.Join(os.TempDir(), "gitcookies"), true, ""); err != nil {
 			return skerr.Wrapf(err, "Failed to create git cookie updater")
 		}
 	}
 	return nil
 }
-
-func main() {
-	common.InitWithMust(
-		"demos",
-	)
-
-	ctx := context.Background()
-	if err := setupGit(ctx); err != nil {
-		sklog.Fatalf("Failed to setup git: %s", err)
-	}
-	// Create a threadsafe checkout to serve from.
-	checkoutDir, err := ioutil.TempDir("", "demos_repo")
-	if err != nil {
-		sklog.Fatalf("Unable to create temporary directory for demos checkout: %s", err)
-	}
-	syncedDemos := newSyncedDemos(ctx, *demosRepo, checkoutDir, *demosRepoPath)
-
-	r := mux.NewRouter()
-	r.PathPrefix("/demo/").HandlerFunc(syncedDemos.demoHandler())
-	r.PathPrefix("/dist/").Handler(http.StripPrefix("/dist/", http.FileServer(http.Dir(*resourcesDir))))
-	r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
-		http.ServeFile(w, r, filepath.Join(*resourcesDir, "main.html"))
-	})
-
-	h := httputils.LoggingGzipRequestResponse(r)
-	h = httputils.HealthzAndHTTPS(h)
-	http.Handle("/", h)
-	sklog.Info("Ready to serve on http://localhost" + *port)
-	sklog.Fatal(http.ListenAndServe(*port, nil))
-}
diff --git a/demos/go/frontend/BUILD.bazel b/demos/go/frontend/BUILD.bazel
new file mode 100644
index 0000000..c072ad9
--- /dev/null
+++ b/demos/go/frontend/BUILD.bazel
@@ -0,0 +1,8 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+    name = "frontend",
+    srcs = ["frontend.go"],
+    importpath = "go.skia.org/infra/demos/go/frontend",
+    visibility = ["//visibility:public"],
+)
diff --git a/demos/go/common/common.go b/demos/go/frontend/frontend.go
similarity index 95%
rename from demos/go/common/common.go
rename to demos/go/frontend/frontend.go
index e108a14..2ff374b 100644
--- a/demos/go/common/common.go
+++ b/demos/go/frontend/frontend.go
@@ -1,4 +1,4 @@
-package common
+package frontend
 
 // Revision represents repo HEAD info for storage as json.
 type Revision struct {
diff --git a/demos/go/generate/BUILD.bazel b/demos/go/generate/BUILD.bazel
index 296a336..4895732 100644
--- a/demos/go/generate/BUILD.bazel
+++ b/demos/go/generate/BUILD.bazel
@@ -6,7 +6,7 @@
     importpath = "go.skia.org/infra/demos/go/generate",
     visibility = ["//visibility:private"],
     deps = [
-        "//demos/go/common",
+        "//demos/go/frontend",
         "//go/sklog",
         "//go/util",
         "@com_github_skia_dev_go2ts//:go2ts",
diff --git a/demos/go/generate/main.go b/demos/go/generate/main.go
index 52504ae..efb71b6 100644
--- a/demos/go/generate/main.go
+++ b/demos/go/generate/main.go
@@ -8,7 +8,7 @@
 
 	"github.com/skia-dev/go2ts"
 
-	"go.skia.org/infra/demos/go/common"
+	"go.skia.org/infra/demos/go/frontend"
 	"go.skia.org/infra/go/sklog"
 	"go.skia.org/infra/go/util"
 )
@@ -29,5 +29,5 @@
 }
 
 func addTypes(generator *go2ts.Go2TS) {
-	generator.Add(common.Metadata{})
+	generator.Add(frontend.Metadata{})
 }