From 76c46c2cb4d4b55b8b6622e1a7d263ad7fe7ae96 Mon Sep 17 00:00:00 2001
From: Christian Zangl <laktak@cdak.net>
Date: Wed, 21 Aug 2024 21:14:13 +0200
Subject: [PATCH] add --no-recurse flag

---
 README.md           |   1 +
 cmd/chkbit/main.go  |   2 +
 context.go          |  35 +++----
 scripts/run_test.go | 227 +++++++++++++++++++++++++-------------------
 4 files changed, 153 insertions(+), 112 deletions(-)

diff --git a/README.md b/README.md
index d9ac8ce..311af17 100644
--- a/README.md
+++ b/README.md
@@ -90,6 +90,7 @@ Flags:
       --algo="blake3"                  hash algorithm: md5, sha512, blake3 (default: blake3)
   -f, --force                          force update of damaged items
   -s, --skip-symlinks                  do not follow symlinks
+  -R, --no-recurse                     do not recurse into subdirectories
   -D, --no-dir-in-index                do not track directories in the index
   -l, --log-file=STRING                write to a logfile if specified
       --log-verbose                    verbose logging
diff --git a/cmd/chkbit/main.go b/cmd/chkbit/main.go
index 8a9d987..ad8d09c 100644
--- a/cmd/chkbit/main.go
+++ b/cmd/chkbit/main.go
@@ -50,6 +50,7 @@ var cli struct {
 	Algo            string   `default:"blake3" help:"hash algorithm: md5, sha512, blake3 (default: blake3)"`
 	Force           bool     `short:"f" help:"force update of damaged items"`
 	SkipSymlinks    bool     `short:"s" help:"do not follow symlinks"`
+	NoRecurse       bool     `short:"R" help:"do not recurse into subdirectories"`
 	NoDirInIndex    bool     `short:"D" help:"do not track directories in the index"`
 	LogFile         string   `short:"l" help:"write to a logfile if specified"`
 	LogVerbose      bool     `help:"verbose logging"`
@@ -170,6 +171,7 @@ func (m *Main) process() bool {
 	m.context.ShowIgnoredOnly = cli.ShowIgnoredOnly
 	m.context.ShowMissing = cli.ShowMissing
 	m.context.SkipSymlinks = cli.SkipSymlinks
+	m.context.SkipSubdirectories = cli.NoRecurse
 	m.context.TrackDirectories = !cli.NoDirInIndex
 
 	var wg sync.WaitGroup
diff --git a/context.go b/context.go
index a8ed2cb..c9d05b5 100644
--- a/context.go
+++ b/context.go
@@ -8,20 +8,21 @@ import (
 )
 
 type Context struct {
-	NumWorkers       int
-	UpdateIndex      bool
-	ShowIgnoredOnly  bool
-	ShowMissing      bool
-	ForceUpdateDmg   bool
-	HashAlgo         string
-	TrackDirectories bool
-	SkipSymlinks     bool
-	IndexFilename    string
-	IgnoreFilename   string
-	WorkQueue        chan *WorkItem
-	LogQueue         chan *LogEvent
-	PerfQueue        chan *PerfEvent
-	wg               sync.WaitGroup
+	NumWorkers         int
+	UpdateIndex        bool
+	ShowIgnoredOnly    bool
+	ShowMissing        bool
+	ForceUpdateDmg     bool
+	HashAlgo           string
+	TrackDirectories   bool
+	SkipSymlinks       bool
+	SkipSubdirectories bool
+	IndexFilename      string
+	IgnoreFilename     string
+	WorkQueue          chan *WorkItem
+	LogQueue           chan *LogEvent
+	PerfQueue          chan *PerfEvent
+	wg                 sync.WaitGroup
 
 	mutex     sync.Mutex
 	NumTotal  int
@@ -183,7 +184,9 @@ func (context *Context) scanDir(root string, parentIgnore *Ignore) {
 
 	context.addWork(root, filesToIndex, dirList, ignore)
 
-	for _, name := range dirList {
-		context.scanDir(filepath.Join(root, name), ignore)
+	if !context.SkipSubdirectories {
+		for _, name := range dirList {
+			context.scanDir(filepath.Join(root, name), ignore)
+		}
 	}
 }
diff --git a/scripts/run_test.go b/scripts/run_test.go
index 9ae1723..7963dbe 100644
--- a/scripts/run_test.go
+++ b/scripts/run_test.go
@@ -137,60 +137,87 @@ func TestRoot(t *testing.T) {
 	tool := getCmd()
 	root := filepath.Join(testDir, "root")
 
-	// step1: update index
-	cmd := exec.Command(tool, "-um", root)
-	out, err := cmd.Output()
-	if err != nil {
-		t.Fatalf("step1 failed with '%s'\n", err)
-	}
-	sout := string(out)
-	checkOut(t, sout, "67 directories were updated")
-	checkOut(t, sout, "300 file hashes were added")
-	checkNotOut(t, sout, "removed")
-
-	// step2: delete files, check for missing
-	os.RemoveAll(filepath.Join(root, "thing/change"))
-	os.Remove(filepath.Join(root, "time/hour/minute/body-information.csv"))
-
-	cmd = exec.Command(tool, "-m", root)
-	out, err = cmd.Output()
-	if err != nil {
-		t.Fatalf("step2 failed with '%s'\n", err)
-	}
-	sout = string(out)
-	checkOut(t, sout, "del /tmp/chkbit/root/thing/change/")
-	checkOut(t, sout, "2 files/directories would have been removed")
-
-	// step2a: do not report missing without -m
-	cmd = exec.Command(tool, root)
-	out, err = cmd.Output()
-	if err != nil {
-		t.Fatalf("step2a failed with '%s'\n", err)
-	}
-	sout = string(out)
-	checkNotOut(t, sout, "del ")
-	checkNotOut(t, sout, "removed")
-
-	// step3: check for missing and update
-	cmd = exec.Command(tool, "-um", root)
-	out, err = cmd.Output()
-	if err != nil {
-		t.Fatalf("step3 failed with '%s'\n", err)
-	}
-	sout = string(out)
-	checkOut(t, sout, "del /tmp/chkbit/root/thing/change/")
-	checkOut(t, sout, "2 files/directories have been removed")
-
-	// step4: check again
-	for i := 0; i < 10; i++ {
-		cmd = exec.Command(tool, "-u", root)
-		out, err = cmd.Output()
+	// update index, no recourse
+	t.Run("Step1", func(t *testing.T) {
+		cmd := exec.Command(tool, "-umR", filepath.Join(root, "day/office"))
+		out, err := cmd.Output()
 		if err != nil {
-			t.Fatalf("step4 failed with '%s'\n", err)
+			t.Fatalf("failed with '%s'\n", err)
 		}
-		sout = string(out)
-		checkOut(t, sout, "Processed 289 files")
-	}
+		sout := string(out)
+		checkOut(t, sout, "Processed 5 files")
+		checkOut(t, sout, "- 1 directory was updated")
+		checkOut(t, sout, "- 5 file hashes were added")
+		checkOut(t, sout, "- 0 file hashes were updated")
+		checkNotOut(t, sout, "removed")
+	})
+
+	// update remaining index from root
+	t.Run("Step2", func(t *testing.T) {
+		cmd := exec.Command(tool, "-um", root)
+		out, err := cmd.Output()
+		if err != nil {
+			t.Fatalf("failed with '%s'\n", err)
+		}
+		sout := string(out)
+		checkOut(t, sout, "Processed 300 files")
+		checkOut(t, sout, "- 66 directories were updated")
+		checkOut(t, sout, "- 295 file hashes were added")
+		checkOut(t, sout, "- 0 file hashes were updated")
+		checkNotOut(t, sout, "removed")
+	})
+
+	// delete files, check for missing
+	t.Run("Step3", func(t *testing.T) {
+		os.RemoveAll(filepath.Join(root, "thing/change"))
+		os.Remove(filepath.Join(root, "time/hour/minute/body-information.csv"))
+
+		cmd := exec.Command(tool, "-m", root)
+		out, err := cmd.Output()
+		if err != nil {
+			t.Fatalf("failed with '%s'\n", err)
+		}
+		sout := string(out)
+		checkOut(t, sout, "del /tmp/chkbit/root/thing/change/")
+		checkOut(t, sout, "2 files/directories would have been removed")
+	})
+
+	// do not report missing without -m
+	t.Run("Step4", func(t *testing.T) {
+		cmd := exec.Command(tool, root)
+		out, err := cmd.Output()
+		if err != nil {
+			t.Fatalf("failed with '%s'\n", err)
+		}
+		sout := string(out)
+		checkNotOut(t, sout, "del ")
+		checkNotOut(t, sout, "removed")
+	})
+
+	// check for missing and update
+	t.Run("Step5", func(t *testing.T) {
+		cmd := exec.Command(tool, "-um", root)
+		out, err := cmd.Output()
+		if err != nil {
+			t.Fatalf("failed with '%s'\n", err)
+		}
+		sout := string(out)
+		checkOut(t, sout, "del /tmp/chkbit/root/thing/change/")
+		checkOut(t, sout, "2 files/directories have been removed")
+	})
+
+	// check again
+	t.Run("Step6", func(t *testing.T) {
+		for i := 0; i < 10; i++ {
+			cmd := exec.Command(tool, "-u", root)
+			out, err := cmd.Output()
+			if err != nil {
+				t.Fatalf("failed with '%s'\n", err)
+			}
+			sout := string(out)
+			checkOut(t, sout, "Processed 289 files")
+		}
+	})
 }
 
 func TestDMG(t *testing.T) {
@@ -216,50 +243,58 @@ func TestDMG(t *testing.T) {
 	t2, _ := time.Parse(time.RFC3339, "2022-02-01T12:00:00Z")
 	t3, _ := time.Parse(time.RFC3339, "2022-02-01T13:00:00Z")
 
-	// step1: create test and set the modified time"
-	os.WriteFile(testFile, []byte("foo1"), 0644)
-	os.Chtimes(testFile, t2, t2)
+	// create test and set the modified time"
+	t.Run("Step1", func(t *testing.T) {
+		os.WriteFile(testFile, []byte("foo1"), 0644)
+		os.Chtimes(testFile, t2, t2)
 
-	cmd := exec.Command(tool, "-u", ".")
-	if out, err := cmd.Output(); err != nil {
-		t.Fatalf("step1 failed with '%s'\n", err)
-	} else {
-		checkOut(t, string(out), "new test.txt")
-	}
-
-	// step2: update test with different content & old modified (expect 'old')"
-	os.WriteFile(testFile, []byte("foo2"), 0644)
-	os.Chtimes(testFile, t1, t1)
-
-	cmd = exec.Command(tool, "-u", ".")
-	if out, err := cmd.Output(); err != nil {
-		t.Fatalf("step2 failed with '%s'\n", err)
-	} else {
-		checkOut(t, string(out), "old test.txt")
-	}
-
-	// step3: update test & new modified (expect 'upd')"
-	os.WriteFile(testFile, []byte("foo3"), 0644)
-	os.Chtimes(testFile, t3, t3)
-
-	cmd = exec.Command(tool, "-u", ".")
-	if out, err := cmd.Output(); err != nil {
-		t.Fatalf("step3 failed with '%s'\n", err)
-	} else {
-		checkOut(t, string(out), "upd test.txt")
-	}
-
-	// step4: Now update test with the same modified to simulate damage (expect DMG)"
-	os.WriteFile(testFile, []byte("foo4"), 0644)
-	os.Chtimes(testFile, t3, t3)
-
-	cmd = exec.Command(tool, "-u", ".")
-	if out, err := cmd.Output(); err != nil {
-		if cmd.ProcessState.ExitCode() != 1 {
-			t.Fatalf("step4 expected to fail with exit code 1 vs %d!", cmd.ProcessState.ExitCode())
+		cmd := exec.Command(tool, "-u", ".")
+		if out, err := cmd.Output(); err != nil {
+			t.Fatalf("failed with '%s'\n", err)
+		} else {
+			checkOut(t, string(out), "new test.txt")
 		}
-		checkOut(t, string(out), "DMG test.txt")
-	} else {
-		t.Fatal("step4 expected to fail!")
-	}
+	})
+
+	// update test with different content & old modified (expect 'old')"
+	t.Run("Step2", func(t *testing.T) {
+		os.WriteFile(testFile, []byte("foo2"), 0644)
+		os.Chtimes(testFile, t1, t1)
+
+		cmd := exec.Command(tool, "-u", ".")
+		if out, err := cmd.Output(); err != nil {
+			t.Fatalf("failed with '%s'\n", err)
+		} else {
+			checkOut(t, string(out), "old test.txt")
+		}
+	})
+
+	// update test & new modified (expect 'upd')"
+	t.Run("Step3", func(t *testing.T) {
+		os.WriteFile(testFile, []byte("foo3"), 0644)
+		os.Chtimes(testFile, t3, t3)
+
+		cmd := exec.Command(tool, "-u", ".")
+		if out, err := cmd.Output(); err != nil {
+			t.Fatalf("failed with '%s'\n", err)
+		} else {
+			checkOut(t, string(out), "upd test.txt")
+		}
+	})
+
+	// Now update test with the same modified to simulate damage (expect DMG)"
+	t.Run("Step4", func(t *testing.T) {
+		os.WriteFile(testFile, []byte("foo4"), 0644)
+		os.Chtimes(testFile, t3, t3)
+
+		cmd := exec.Command(tool, "-u", ".")
+		if out, err := cmd.Output(); err != nil {
+			if cmd.ProcessState.ExitCode() != 1 {
+				t.Fatalf("expected to fail with exit code 1 vs %d!", cmd.ProcessState.ExitCode())
+			}
+			checkOut(t, string(out), "DMG test.txt")
+		} else {
+			t.Fatal("expected to fail!")
+		}
+	})
 }