tenseleyflow/shithub / f2fb5df

Browse files

Avoid dead submodule tree links

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
f2fb5dfad905624bbb3c58bf07e90b65b00ef993
Parents
9c71d87
Tree
83ae5cf

6 changed files

StatusFile+-
M docs/internal/code-tab.md 6 2
M internal/repos/git/logops.go 20 1
M internal/repos/git/logops_test.go 26 2
M internal/web/handlers/repo/code_tree_rows.go 1 1
M internal/web/handlers/repo/code_tree_rows_test.go 9 0
M internal/web/handlers/repo/submodule_links.go 64 4
docs/internal/code-tab.mdmodified
@@ -67,8 +67,12 @@ directories first, then files alphabetically.
6767
 `commit` entries are git submodule pointers. When `.gitmodules` exists
6868
 on the rendered ref, the Code tab parses it once, matches entries by
6969
 submodule path, and links GitHub or configured shithub clone remotes to
70
-the local `/{owner}/{repo}/tree/{gitlink-oid}` route. Unknown, external,
71
-or malformed remotes stay as plain `name @ shortsha` rows.
70
+the local `/{owner}/{repo}/tree/{gitlink-oid}` route when the target
71
+repo has that commit. If the target repo exists locally but does not
72
+have the pinned commit object, the row links to the target repo's
73
+default Code tab so independently-created mirrors don't produce dead
74
+links. Unknown, external, absent, or malformed remotes stay as plain
75
+`name @ shortsha` rows.
7276
 
7377
 The S17 ship excludes the htmx-driven "last commit per entry" column
7478
 that the spec describes — an extra round-trip we can add later without
internal/repos/git/logops.gomodified
@@ -102,6 +102,19 @@ func CountCommits(ctx context.Context, gitDir, ref string) (int, error) {
102102
 	return count, nil
103103
 }
104104
 
105
+// CommitExists reports whether sha resolves to a commit in this repository.
106
+func CommitExists(ctx context.Context, gitDir, sha string) (bool, error) {
107
+	cmd := exec.CommandContext(ctx, "git", "-C", gitDir, "cat-file", "-e", sha+"^{commit}")
108
+	if _, err := cmd.Output(); err != nil {
109
+		var ee *exec.ExitError
110
+		if errors.As(err, &ee) && isMissingGitObjectError(ee.Stderr) {
111
+			return false, nil
112
+		}
113
+		return false, wrapExecErr(err)
114
+	}
115
+	return true, nil
116
+}
117
+
105118
 // WeeklyCommitActivity counts commits by UTC week, oldest bucket first.
106119
 // It is intentionally small and read-only so list surfaces can render
107120
 // GitHub-style activity sparklines without parsing full commit rows.
@@ -225,7 +238,7 @@ func GetCommit(ctx context.Context, gitDir, sha string) (CommitDetail, error) {
225238
 	out, err := cmd.Output()
226239
 	if err != nil {
227240
 		var ee *exec.ExitError
228
-		if errors.As(err, &ee) && bytes.Contains(ee.Stderr, []byte("unknown revision")) {
241
+		if errors.As(err, &ee) && isMissingGitObjectError(ee.Stderr) {
229242
 			return CommitDetail{}, ErrCommitNotFound
230243
 		}
231244
 		return CommitDetail{}, wrapExecErr(err)
@@ -274,6 +287,12 @@ func GetCommit(ctx context.Context, gitDir, sha string) (CommitDetail, error) {
274287
 // resolve to a commit on this repo.
275288
 var ErrCommitNotFound = errors.New("git: commit not found")
276289
 
290
+func isMissingGitObjectError(stderr []byte) bool {
291
+	return bytes.Contains(stderr, []byte("unknown revision")) ||
292
+		bytes.Contains(stderr, []byte("Not a valid object name")) ||
293
+		bytes.Contains(stderr, []byte("bad object"))
294
+}
295
+
277296
 // DiffStat returns the per-file change list for a SHA. We run two
278297
 // commands: --name-status for the letter and rename pairs, --numstat
279298
 // for +/- counts. Two passes is two forks, but the parsing stays
internal/repos/git/logops_test.gomodified
@@ -4,6 +4,7 @@ package git_test
44
 
55
 import (
66
 	"context"
7
+	"errors"
78
 	"os/exec"
89
 	"strings"
910
 	"testing"
@@ -69,8 +70,31 @@ func TestGetCommit_ReturnsErrCommitNotFound(t *testing.T) {
6970
 	t.Parallel()
7071
 	gitDir := buildSeedRepo(t)
7172
 	_, err := gitops.GetCommit(context.Background(), gitDir, strings.Repeat("0", 40))
72
-	if err == nil {
73
-		t.Fatalf("expected error for unknown sha")
73
+	if !errors.Is(err, gitops.ErrCommitNotFound) {
74
+		t.Fatalf("GetCommit error = %v, want commit not found", err)
75
+	}
76
+}
77
+
78
+func TestCommitExists(t *testing.T) {
79
+	t.Parallel()
80
+	gitDir := buildSeedRepo(t)
81
+	commits, err := gitops.Log(context.Background(), gitDir, gitops.LogOptions{Ref: "trunk"})
82
+	if err != nil {
83
+		t.Fatalf("Log: %v", err)
84
+	}
85
+	exists, err := gitops.CommitExists(context.Background(), gitDir, commits[0].OID)
86
+	if err != nil {
87
+		t.Fatalf("CommitExists(existing): %v", err)
88
+	}
89
+	if !exists {
90
+		t.Fatal("CommitExists(existing) = false, want true")
91
+	}
92
+	exists, err = gitops.CommitExists(context.Background(), gitDir, strings.Repeat("0", 40))
93
+	if err != nil {
94
+		t.Fatalf("CommitExists(missing): %v", err)
95
+	}
96
+	if exists {
97
+		t.Fatal("CommitExists(missing) = true, want false")
7498
 	}
7599
 }
76100
 
internal/web/handlers/repo/code_tree_rows.gomodified
@@ -36,7 +36,7 @@ func (h *Handlers) codeTreeEntryRows(ctx context.Context, cc *codeContext, entri
3636
 		if e.Kind == repogit.EntrySubmod {
3737
 			entryURL = ""
3838
 			if sm, ok := submodules[fullPath]; ok {
39
-				entryURL = h.submoduleTreeURL(cc, sm.URL, e.OID)
39
+				entryURL = h.submoduleTreeURL(ctx, cc, sm.URL, e.OID)
4040
 			}
4141
 		}
4242
 		row := codeTreeEntryRow{
internal/web/handlers/repo/code_tree_rows_test.gomodified
@@ -3,6 +3,7 @@
33
 package repo
44
 
55
 import (
6
+	"strings"
67
 	"testing"
78
 
89
 	repogit "github.com/tenseleyFlow/shithub/internal/repos/git"
@@ -79,6 +80,14 @@ func TestSubmoduleRouteURL_GitHubRemotesBecomeLocalTreeLinks(t *testing.T) {
7980
 			if got != tt.want {
8081
 				t.Fatalf("submoduleRouteURL(%q) = %q, want %q", tt.remote, got, tt.want)
8182
 			}
83
+			route, ok := submoduleRouteForRemote(cfg, tt.remote, oid)
84
+			if !ok {
85
+				t.Fatalf("submoduleRouteForRemote(%q) ok = false", tt.remote)
86
+			}
87
+			wantRepoURL := strings.TrimSuffix(tt.want, "/tree/"+oid)
88
+			if route.RepoURL != wantRepoURL {
89
+				t.Fatalf("RepoURL = %q, want %q", route.RepoURL, wantRepoURL)
90
+			}
8291
 		})
8392
 	}
8493
 }