From cb92015dfcbe8e5e2c42e7df174354f33d6cd85a Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 21:34:31 +0000 Subject: [PATCH 1/9] Expose a genuinely flattened file listing (blobs) Both "tree" and "tree_recursive" (at the top-level package scope, and the per-file scope on a directory entry) are misleadingly named - neither actually recurses. "tree" is filtered to top-level entries; "tree_recursive" is the exact same unfiltered top-level list (a no-op filter, since top-level entries never contain a path separator); and per-file `tree`/`tree_recursive` both just return one level of a directory's immediate children (treeFileGetTreeRecursive literally just returns `fs` unchanged). There was no existing way to get a real flat, full-depth listing of a repo's files. Add `flattenFiles` (Types.hs), which actually recurses into FolderContents and collects only file/blob leaves, and expose it as a new top-level `blobs` field in Repositories.hs's `package` scope. Left the existing `tree`/`tree_recursive` fields untouched, since other templates (and the readme/license lookup, which searches `tree`) may depend on their current, if misleadingly-named, behavior. Each entry's `path` in the flattened list is already the full path relative to the repo root - the tree walk in Repositories.hs accumulates this via prependParent as it descends, regardless of how deep a file is nested. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_012RfZTyHvxLyVp4yaWmNvhN --- src/Repositories.hs | 1 + src/Types.hs | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/src/Repositories.hs b/src/Repositories.hs index 2635bff7..68357fcf 100644 --- a/src/Repositories.hs +++ b/src/Repositories.hs @@ -185,6 +185,7 @@ package env repos name description commits tree = , ("commits", toGVal commits) , ("tree", toGVal . filter (notElem FP.pathSeparator . T.unpack . treeFilePath) $ tree) , ("tree_recursive", toGVal tree) + , ("blobs", toGVal . concatMap flattenFiles $ tree) , ("readme", toGVal . findFile "readme" $ tree) , ("license", toGVal . findFile "license" $ tree) ] diff --git a/src/Types.hs b/src/Types.hs index 233a446e..0bb36941 100644 --- a/src/Types.hs +++ b/src/Types.hs @@ -255,6 +255,17 @@ getBlobContents oid = do then return BinaryContents else FileContents <$> Git.catBlob oid +{- +Recursively descend into a tree, discarding directories and returning a flat list of +every file (blob) reachable from it, in depth-first order. Each file's `path` is already +the full path relative to the repo root, since that's accumulated during the tree walk in +Repositories.hs. +-} +flattenFiles :: TreeFile -> [TreeFile] +flattenFiles treefile = case treeFileContents treefile of + FolderContents files -> concatMap flattenFiles files + _ -> [treefile] + {- GVal implementations for data definitions above, allowing commits to be rendered in Ginger templates. From a175f52a54f7515ee7ff9a31a38419546561f85c Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 21:35:55 +0000 Subject: [PATCH 2/9] Actually generate file pages for nested files, not just top-level ones processRepo' only ever called the file-page generator on the top-level tree (in both the force and non-force/getUpdatedFiles branches), never on anything reachable through a directory's FolderContents. So gitja has never actually written a file.html-equivalent page for anything nested in a subdirectory, independent of the "blobs" field or any template - the page itself was never generated, regardless of what a template linked to. Flatten tree with flattenFiles before both generation paths, so a file's depth no longer determines whether it gets a page. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_012RfZTyHvxLyVp4yaWmNvhN --- src/Repositories.hs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Repositories.hs b/src/Repositories.hs index 68357fcf..00c49373 100644 --- a/src/Repositories.hs +++ b/src/Repositories.hs @@ -150,10 +150,14 @@ processRepo' env repos repo = do mapM_ (gen force commitT "commit" commitDir commitHref) newCommits whenJust (envFileTemplate env) \fileT -> do + -- `tree` only holds the top level, with nested files reachable + -- via each directory entry's FolderContents - flatten it so + -- files at any depth actually get a page generated for them. + let allFiles = concatMap flattenFiles tree if force - then mapM_ (gen True fileT "file" fileDir fileHref) tree + then mapM_ (gen True fileT "file" fileDir fileHref) allFiles else - let updatedFiles = getUpdatedFiles tree newCommits + let updatedFiles = getUpdatedFiles allFiles newCommits in mapM_ (gen True fileT "file" fileDir fileHref) updatedFiles -- Copy any static files/folders into the output directory -- From 3764d23d110a9f19ced8f656305ba738de3c44a3 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 21:37:02 +0000 Subject: [PATCH 3/9] Drop comment Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_012RfZTyHvxLyVp4yaWmNvhN --- src/Repositories.hs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Repositories.hs b/src/Repositories.hs index 00c49373..cb146fde 100644 --- a/src/Repositories.hs +++ b/src/Repositories.hs @@ -150,9 +150,6 @@ processRepo' env repos repo = do mapM_ (gen force commitT "commit" commitDir commitHref) newCommits whenJust (envFileTemplate env) \fileT -> do - -- `tree` only holds the top level, with nested files reachable - -- via each directory entry's FolderContents - flatten it so - -- files at any depth actually get a page generated for them. let allFiles = concatMap flattenFiles tree if force then mapM_ (gen True fileT "file" fileDir fileHref) allFiles From e2f66dda15b857bed907c8e05af4eadcaec1d05b Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 21:38:36 +0000 Subject: [PATCH 4/9] Move flattenFiles into Repositories.hs Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_012RfZTyHvxLyVp4yaWmNvhN --- src/Repositories.hs | 5 +++++ src/Types.hs | 11 ----------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/Repositories.hs b/src/Repositories.hs index cb146fde..3c8036ba 100644 --- a/src/Repositories.hs +++ b/src/Repositories.hs @@ -374,6 +374,11 @@ getUpdates directory cs = go cs (return []) ((x :) <$> go xs) +flattenFiles :: TreeFile -> [TreeFile] +flattenFiles treefile = case treeFileContents treefile of + FolderContents files -> concatMap flattenFiles files + _ -> [treefile] + getUpdatedFiles :: [TreeFile] -> [Commit] -> [TreeFile] getUpdatedFiles [] _ = [] getUpdatedFiles _ [] = [] diff --git a/src/Types.hs b/src/Types.hs index 0bb36941..233a446e 100644 --- a/src/Types.hs +++ b/src/Types.hs @@ -255,17 +255,6 @@ getBlobContents oid = do then return BinaryContents else FileContents <$> Git.catBlob oid -{- -Recursively descend into a tree, discarding directories and returning a flat list of -every file (blob) reachable from it, in depth-first order. Each file's `path` is already -the full path relative to the repo root, since that's accumulated during the tree walk in -Repositories.hs. --} -flattenFiles :: TreeFile -> [TreeFile] -flattenFiles treefile = case treeFileContents treefile of - FolderContents files -> concatMap flattenFiles files - _ -> [treefile] - {- GVal implementations for data definitions above, allowing commits to be rendered in Ginger templates. From 1f58d3caf7a21736e3339a84b99987812aab915e Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 22:01:30 +0000 Subject: [PATCH 5/9] Drop tree_recursive - it never actually recursed Confirmed by reading the code: at both scopes (top-level package, and per-file on a directory entry), tree_recursive was identical to tree. Top-level tree's filter (notElem pathSeparator ...) was a no-op, since the list it filters only ever contains top-level entries in the first place - nested entries live inside each directory's own FolderContents, never flattened into it. Per-file, treeFileGetTreeRecursive just returned `fs` unchanged - no recursion despite the name. The docs described tree_recursive as "a list of *all* of the repository's/directory's contents", which was never actually true. "tree" is git's own name for a tree object's entries (a git tree is not itself recursive - that's exactly what it means for git to have separate tree objects per directory), so keeping that name for "this tree's entries" is accurate, not a simplification. Dropped the now-pointless filter/atTop logic along with tree_recursive, and updated DOCUMENTATION.md's Scopes/Attributes tables to match - "blobs" is documented there too, since it existed but wasn't previously listed. This doesn't add per-tree-entry pages for every object type a tree can contain (blob/tree/commit-as-submodule) - just removes a redundant, inaccurately-documented field. That's real future work, not this. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_012RfZTyHvxLyVp4yaWmNvhN --- DOCUMENTATION.md | 5 ++--- src/Repositories.hs | 3 +-- src/Types.hs | 18 ++++++------------ 3 files changed, 9 insertions(+), 17 deletions(-) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 014d31e6..436cabe0 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -145,8 +145,8 @@ The variables available within each scope are listed here for reference: | | name | The repository name, taken from its folder name. | | | description | The repository's description (see below). | | | commits | A list of the repository's commits. | -| | tree | A list of the top-level folder's contents. | -| | tree\_recursive | A list of *all* of the repository's contents. | +| | tree | A list of the repository root tree's entries. | +| | blobs | A flat list of *every* blob (file) in the repository. | | | tags | A list of the refs corresponding to tags. | | | branches | A list of the refs corresponding to branches. | | | readme | The repository's readme file, if it has one. | @@ -180,7 +180,6 @@ Here is the reference of attributes available on the variables that have them: | | is\_directory | A boolean, useful for ginger conditionals. | | | is\_binary | A boolean, tells you if the contents can be rendered. | | | tree | A list of a directory's direct contents. | -| | tree\_recursive | A list of *all* of a directory's contents. | | ref | name | The tag or branch name. | | | commit | The commit pointed to by the tag or branch. | | commit | id | The SHA of the given commit. | diff --git a/src/Repositories.hs b/src/Repositories.hs index 3c8036ba..ac4fceac 100644 --- a/src/Repositories.hs +++ b/src/Repositories.hs @@ -184,8 +184,7 @@ package env repos name description commits tree = , ("name", toGVal . T.pack . init . toFilePath $ name) , ("description", toGVal description) , ("commits", toGVal commits) - , ("tree", toGVal . filter (notElem FP.pathSeparator . T.unpack . treeFilePath) $ tree) - , ("tree_recursive", toGVal tree) + , ("tree", toGVal tree) , ("blobs", toGVal . concatMap flattenFiles $ tree) , ("readme", toGVal . findFile "readme" $ tree) , ("license", toGVal . findFile "license" $ tree) diff --git a/src/Types.hs b/src/Types.hs index 233a446e..f729aa50 100644 --- a/src/Types.hs +++ b/src/Types.hs @@ -14,7 +14,7 @@ import Control.Monad.IO.Class (liftIO) import Control.Monad.Trans.Reader (ReaderT) import Data.ByteString (ByteString) import Data.Default (def) -import Data.Maybe (fromMaybe, listToMaybe) +import Data.Maybe (listToMaybe) import Data.Tagged (untag) import qualified Data.Text as T import Data.Text.Encoding (decodeUtf8With) @@ -286,8 +286,7 @@ treeAsLookup treefile = \case "name" -> Just . toGVal . FP.takeFileName . T.unpack . treeFilePath $ treefile "href" -> Just . toGVal . treePathToHref $ treefile "contents" -> Just . toGVal . treeFileContents $ treefile - "tree" -> Just . toGVal . treeFileGetTree (treeFilePath treefile) . treeFileContents $ treefile - "tree_recursive" -> Just . toGVal . treeFileGetTreeRecursive . treeFileContents $ treefile + "tree" -> Just . toGVal . treeFileGetTree . treeFileContents $ treefile "mode" -> Just . toGVal . drop 4 . show . treeFileMode $ treefile "mode_octal" -> Just . toGVal . modeToOctal . treeFileMode $ treefile "mode_symbolic" -> Just . toGVal . modeToSymbolic . treeFileMode $ treefile @@ -295,15 +294,10 @@ treeAsLookup treefile = \case "is_directory" -> Just . toGVal . treeFileIsDirectory $ treefile _ -> Nothing where - treeFileGetTree :: T.Text -> TreeFileContents -> [TreeFile] - treeFileGetTree parent (FolderContents fs) = filter atTop fs - where - atTop = notElem FP.pathSeparator . drop 1 . T.unpack . fromMaybe "" . T.stripPrefix parent . treeFilePath - treeFileGetTree _ _ = [] - - treeFileGetTreeRecursive :: TreeFileContents -> [TreeFile] - treeFileGetTreeRecursive (FolderContents fs) = fs - treeFileGetTreeRecursive _ = [] + -- The entries of this tree object - i.e. this directory's immediate children. + treeFileGetTree :: TreeFileContents -> [TreeFile] + treeFileGetTree (FolderContents fs) = fs + treeFileGetTree _ = [] treeFileIsBinary :: TreeFile -> Bool treeFileIsBinary treefile' = case treeFileContents treefile' of From 6725451e6d592369202950972951d2d9f2469a46 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 22:08:57 +0000 Subject: [PATCH 6/9] Reinstate tree_recursive, properly recursive this time blobs (leaves only) and tree_recursive (every entry, blobs and trees alike, at any depth) are different things - collapsing tree_recursive into blobs lost that distinction. Add flattenTree (Types.hs), the tree-and-blob equivalent of flattenFiles - keeps every entry including directories themselves as it descends, equivalent to `git ls-tree -r -t`. Used for both the top-level "tree_recursive" (package scope, Repositories.hs) and the per-file "tree_recursive" on a directory entry (treeAsLookup, Types.hs). flattenTree lives in Types.hs rather than alongside flattenFiles in Repositories.hs, since it's needed by treeAsLookup there too and Repositories.hs already depends on Types.hs (the reverse would be a cycle). Updated DOCUMENTATION.md's Scopes/Attributes tables to describe both tree_recursive and blobs accurately. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_012RfZTyHvxLyVp4yaWmNvhN --- DOCUMENTATION.md | 2 ++ src/Repositories.hs | 1 + src/Types.hs | 10 ++++++++++ 3 files changed, 13 insertions(+) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 436cabe0..6caa3cab 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -146,6 +146,7 @@ The variables available within each scope are listed here for reference: | | description | The repository's description (see below). | | | commits | A list of the repository's commits. | | | tree | A list of the repository root tree's entries. | +| | tree\_recursive | A flat list of *every* entry (blob or tree) at any depth in the repository, like `git ls-tree -r -t`. | | | blobs | A flat list of *every* blob (file) in the repository. | | | tags | A list of the refs corresponding to tags. | | | branches | A list of the refs corresponding to branches. | @@ -180,6 +181,7 @@ Here is the reference of attributes available on the variables that have them: | | is\_directory | A boolean, useful for ginger conditionals. | | | is\_binary | A boolean, tells you if the contents can be rendered. | | | tree | A list of a directory's direct contents. | +| | tree\_recursive | A flat list of *every* entry (blob or tree) at any depth under this directory. | | ref | name | The tag or branch name. | | | commit | The commit pointed to by the tag or branch. | | commit | id | The SHA of the given commit. | diff --git a/src/Repositories.hs b/src/Repositories.hs index ac4fceac..5a741201 100644 --- a/src/Repositories.hs +++ b/src/Repositories.hs @@ -185,6 +185,7 @@ package env repos name description commits tree = , ("description", toGVal description) , ("commits", toGVal commits) , ("tree", toGVal tree) + , ("tree_recursive", toGVal . concatMap flattenTree $ tree) , ("blobs", toGVal . concatMap flattenFiles $ tree) , ("readme", toGVal . findFile "readme" $ tree) , ("license", toGVal . findFile "license" $ tree) diff --git a/src/Types.hs b/src/Types.hs index f729aa50..8d89ccb7 100644 --- a/src/Types.hs +++ b/src/Types.hs @@ -280,6 +280,15 @@ instance ToGVal m TreeFileContents where , asList = Just . fmap toGVal $ treeFiles } +{- +Recursively descend into a tree, keeping every entry along the way - blobs and trees +alike - as a single flat list. Equivalent to `git ls-tree -r -t`. +-} +flattenTree :: TreeFile -> [TreeFile] +flattenTree treefile = case treeFileContents treefile of + FolderContents files -> treefile : concatMap flattenTree files + _ -> [treefile] + treeAsLookup :: TreeFile -> T.Text -> Maybe (GVal m) treeAsLookup treefile = \case "path" -> Just . toGVal . treeFilePath $ treefile @@ -287,6 +296,7 @@ treeAsLookup treefile = \case "href" -> Just . toGVal . treePathToHref $ treefile "contents" -> Just . toGVal . treeFileContents $ treefile "tree" -> Just . toGVal . treeFileGetTree . treeFileContents $ treefile + "tree_recursive" -> Just . toGVal . concatMap flattenTree . treeFileGetTree . treeFileContents $ treefile "mode" -> Just . toGVal . drop 4 . show . treeFileMode $ treefile "mode_octal" -> Just . toGVal . modeToOctal . treeFileMode $ treefile "mode_symbolic" -> Just . toGVal . modeToSymbolic . treeFileMode $ treefile From 6332411b29b3da34180e4b4db5eebc7622edd526 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 22:33:01 +0000 Subject: [PATCH 7/9] Add a per-tree page category, and rename file -> blob Third special repo/ template, alongside commit.html and the newly-renamed blob.html: tree.html, generated once per tree (directory) object at any depth, mirroring how blob.html/commit.html already work per blob/commit. Uses flattenTrees (Repositories.hs) to walk every tree object including itself, and treeHref for its output path. Unlike blobs, a tree's own path never appears in a commit diff, so there's no cheap way to tell whether a tree needs regenerating from newCommits alone - tree pages are always regenerated on every run rather than incrementally, which is simpler and correct rather than risking the exact kind of silent staleness bug the blob-generation fix earlier addressed. "file" -> "blob" throughout (envFileTemplate/fileT/fileHref/fileDir -> envBlobTemplate/blobT/blobHref/blobDir, output moves from file/ to blob/), matching git's own object vocabulary and freeing up the name "file" from colliding with anything tree-related. Updated the three bundled templates (base/docs/stagit) so this doesn't silently break them: file.html -> blob.html (dropping their now-truly- unreachable is_directory branches, since blobs are never directories), the existing once-per-repo file-listing tree.html -> files.html (freeing the name for the new per-object template), and a real tree.html per-object page for each, built from what those old is_directory branches were already trying to do. Every href referencing the old file/ output directory is updated to blob/, and files.html/tree.html's child links now route to tree/ or blob/ depending on the entry's own type. Updated DOCUMENTATION.md's folder structure, scopes and attributes tables to describe blob/tree/commit as the three special per-object templates, and the blob/tree attribute table entries as shared (most attributes apply to both, a few are n/a on one side). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_012RfZTyHvxLyVp4yaWmNvhN --- DOCUMENTATION.md | 37 +++++++++------ src/Env.hs | 13 +++-- src/Repositories.hs | 36 ++++++++++---- templates/base/repo/blob.html | 15 ++++++ templates/base/repo/file.html | 35 -------------- templates/base/repo/files.html | 27 +++++++++++ templates/base/repo/tree.html | 16 +++++-- templates/base/template.html.include | 6 +-- templates/docs/repo/blob.html | 15 ++++++ templates/docs/repo/file.html | 47 ------------------- templates/docs/repo/files.html | 27 +++++++++++ templates/docs/repo/tree.html | 14 ++++-- templates/docs/template.html.include | 6 +-- templates/stagit/repo/blob.html | 21 +++++++++ templates/stagit/repo/file.html | 41 ---------------- templates/stagit/repo/files.html | 33 +++++++++++++ .../repo/repo-table-header.html.include | 6 +-- templates/stagit/repo/tree.html | 14 ++++-- 18 files changed, 236 insertions(+), 173 deletions(-) create mode 100644 templates/base/repo/blob.html delete mode 100644 templates/base/repo/file.html create mode 100644 templates/base/repo/files.html create mode 100644 templates/docs/repo/blob.html delete mode 100644 templates/docs/repo/file.html create mode 100644 templates/docs/repo/files.html create mode 100644 templates/stagit/repo/blob.html delete mode 100644 templates/stagit/repo/file.html create mode 100644 templates/stagit/repo/files.html diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 6caa3cab..07cacbc6 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -67,7 +67,7 @@ new to you, it may be enough to skim through some of the examples in the otherwise the ginger docs can be very helpful to see what is supported. Templates are a folder containing a number of ginger template files. There are -4 "scopes", each making available a unique set of variables storing information +5 "scopes", each making available a unique set of variables storing information about the git repositories. Each template file has access to a single one of these scopes. The structure of the template folder determines the scopes of the files contained therein. @@ -82,8 +82,9 @@ To illustrate, this is the expected structure: repo/ inside_this_folder.html two_names_are_special.html - file.html + blob.html commit.html + tree.html The top-level folder, here `template`, is that which is specified in the config file. @@ -98,9 +99,12 @@ The special folder "repo" has access to the *repo scope*, which exposes information pertaining to a single git repository. The template files contained within this folder are parsed and output once per git repository. -The exceptions to this are the two special template files with the names -"file.html" and "commit.html". These have access to the *file scope* and -*commit scope* respectively, and are parsed and output once per file or commit. +The exceptions to this are the three special template files with the names +"blob.html", "commit.html" and "tree.html". These have access to the *blob +scope*, *commit scope* and *tree scope* respectively, and are parsed and +output once per blob, commit or tree found anywhere in the repository, at any +depth (a submodule reference is currently treated as a blob whose content is +its target commit hash, rather than as its own scope). The resulting folder structure found in `output` will look like this (if `repos` only contains gitja): @@ -112,7 +116,7 @@ The resulting folder structure found in `output` will look like this (if gitja/ inside_this_folder.html two_names_are_special.html - file/ + blob/ LICENSE.html Makefile.html ... @@ -120,6 +124,9 @@ The resulting folder structure found in `output` will look like this (if 0a18f38bb5c398bd192a6268281fc6abefaedd63.html 0a7601059956d9c4d395f5d08e8cf48a515d080f.html ... + tree/ + src.html + ... ... ### Static files @@ -152,10 +159,12 @@ The variables available within each scope are listed here for reference: | | branches | A list of the refs corresponding to branches. | | | readme | The repository's readme file, if it has one. | | | license | The repository's license file, if it has one. | -| File | | *In addition to the variables from the Repo scope...* | -| | file | A single file. | +| Blob | | *In addition to the variables from the Repo scope...* | +| | blob | A single blob (file). | | Commit | | *In addition to the variables from the Repo scope...* | | | commit | A single commit. | +| Tree | | *In addition to the variables from the Repo scope...* | +| | tree | A single tree (directory) - shadows the repo-scope `tree`, since a tree page is itself scoped to one tree. | As in [Jinja](https://jinja.palletsprojects.com), a list can be accessed with indexing, and attributes can be accessed using a dot notation. For example, a @@ -171,17 +180,17 @@ Here is the reference of attributes available on the variables that have them: | | description | The repository's description (see below). | | | head | The current git commit. | | | updated | The time when the current commit was committed. | -| file | path | The path the file relative to the repository root. | -| | name | The name of the file. | -| | href | The name of the HTML file for this file. | -| | contents | The file's contents. | +| blob/tree | path | The path relative to the repository root. | +| | name | The name of the blob or tree. | +| | href | The name of the HTML file for this blob or tree. | +| | contents | The blob's contents (n/a for a tree). | | | mode | Directory, Plain, Executable, Symlink or Submodule. | | | mode\_octal | Mode in octal form e.g. "00644" for plain files. | | | mode\_symbolic | Mode in symbolic form e.g. ""-rw-r--r--" for plain files.| | | is\_directory | A boolean, useful for ginger conditionals. | | | is\_binary | A boolean, tells you if the contents can be rendered. | -| | tree | A list of a directory's direct contents. | -| | tree\_recursive | A flat list of *every* entry (blob or tree) at any depth under this directory. | +| | tree | This tree's direct contents (n/a for a blob). | +| | tree\_recursive | A flat list of *every* entry (blob or tree) at any depth under this tree (n/a for a blob). | | ref | name | The tag or branch name. | | | commit | The commit pointed to by the tag or branch. | | commit | id | The SHA of the given commit. | diff --git a/src/Env.hs b/src/Env.hs index e579e43e..a864c9b7 100644 --- a/src/Env.hs +++ b/src/Env.hs @@ -68,7 +68,8 @@ data Env = Env { envConfig :: Config , envIndexTemplates :: [Template] , envCommitTemplate :: Maybe Template - , envFileTemplate :: Maybe Template + , envBlobTemplate :: Maybe Template + , envTreeTemplate :: Maybe Template , envRepoTemplates :: [Template] , envOutput :: Path Abs Dir , envRepos :: [Path Abs Dir] @@ -105,15 +106,16 @@ loadEnv quiet force config = do -- Load files from template directory indexT <- collectTemplates files commitT <- findTemplate "commit.html" filesRepo - fileT <- findTemplate "file.html" filesRepo + blobT <- findTemplate "blob.html" filesRepo + treeT <- findTemplate "tree.html" filesRepo repoT <- collectTemplates - . filter (flip notElem ["commit.html", "file.html"] . toFilePath . filename) + . filter (flip notElem ["commit.html", "blob.html", "tree.html"] . toFilePath . filename) $ filesRepo -- Exit early if we didn't find any templates when - ( all null [indexT, repoT] && all isNothing [commitT, fileT] + ( all null [indexT, repoT] && all isNothing [commitT, blobT, treeT] ) $ die "No templates were found." @@ -123,7 +125,8 @@ loadEnv quiet force config = do { envConfig = config , envIndexTemplates = indexT , envCommitTemplate = commitT - , envFileTemplate = fileT + , envBlobTemplate = blobT + , envTreeTemplate = treeT , envRepoTemplates = repoT , envOutput = output , envRepos = repos diff --git a/src/Repositories.hs b/src/Repositories.hs index 5a741201..fb164aa0 100644 --- a/src/Repositories.hs +++ b/src/Repositories.hs @@ -132,9 +132,11 @@ processRepo' env repos repo = do withRunInIO \runInIO -> do -- Create the destination folders -- commitDir <- (directory ) <$> parseRelDir "commit" - fileDir <- (directory ) <$> parseRelDir "file" + blobDir <- (directory ) <$> parseRelDir "blob" + treeDir <- (directory ) <$> parseRelDir "tree" ensureDir commitDir - ensureDir fileDir + ensureDir blobDir + ensureDir treeDir -- Check which commits are new since the last run -- newCommits <- getUpdates commitDir commits @@ -149,13 +151,21 @@ processRepo' env repos repo = do whenJust (envCommitTemplate env) \commitT -> do mapM_ (gen force commitT "commit" commitDir commitHref) newCommits - whenJust (envFileTemplate env) \fileT -> do - let allFiles = concatMap flattenFiles tree + whenJust (envBlobTemplate env) \blobT -> do + let allBlobs = concatMap flattenFiles tree if force - then mapM_ (gen True fileT "file" fileDir fileHref) allFiles + then mapM_ (gen True blobT "blob" blobDir blobHref) allBlobs else - let updatedFiles = getUpdatedFiles allFiles newCommits - in mapM_ (gen True fileT "file" fileDir fileHref) updatedFiles + let updatedBlobs = getUpdatedFiles allBlobs newCommits + in mapM_ (gen True blobT "blob" blobDir blobHref) updatedBlobs + + whenJust (envTreeTemplate env) \treeT -> do + -- A tree's own path never appears in a commit's diff (only + -- the blobs within it do), so unlike blobs, staleness can't + -- be judged by newCommits/getUpdatedFiles - always + -- regenerate every tree page. + let allTrees = concatMap flattenTrees tree + mapM_ (gen True treeT "tree" treeDir treeHref) allTrees -- Copy any static files/folders into the output directory -- envRepoCopyStatics env directory @@ -379,6 +389,11 @@ flattenFiles treefile = case treeFileContents treefile of FolderContents files -> concatMap flattenFiles files _ -> [treefile] +flattenTrees :: TreeFile -> [TreeFile] +flattenTrees treefile = case treeFileContents treefile of + FolderContents files -> treefile : concatMap flattenTrees files + _ -> [] + getUpdatedFiles :: [TreeFile] -> [Commit] -> [TreeFile] getUpdatedFiles [] _ = [] getUpdatedFiles _ [] = [] @@ -432,8 +447,11 @@ genTarget scope runInIO quiet force template category directory href target = do commitHref :: Commit -> FilePath commitHref = (++ ".html") . commitHash -fileHref :: TreeFile -> FilePath -fileHref = T.unpack . treePathToHref +blobHref :: TreeFile -> FilePath +blobHref = T.unpack . treePathToHref + +treeHref :: TreeFile -> FilePath +treeHref = T.unpack . treePathToHref {- With a dictionary of preloaded values and a function to access additional data, create a diff --git a/templates/base/repo/blob.html b/templates/base/repo/blob.html new file mode 100644 index 00000000..05f08723 --- /dev/null +++ b/templates/base/repo/blob.html @@ -0,0 +1,15 @@ +{% extends "../template.html.include" %} + +{%- block title %}{{ name }} - {{ blob }}{% endblock %} + +{%- block content %} +

{{ blob.mode_symbolic }} {{ blob }}


+ +{% if blob.is_binary %} +(File is binary) +{% else %} +
{% for line in split(blob.contents, '\n') %}
+{{ printf("%5v", loop.index) }}  {{ line }}
+{% endfor %}
+{% endif %} +{% endblock %} diff --git a/templates/base/repo/file.html b/templates/base/repo/file.html deleted file mode 100644 index 6321c443..00000000 --- a/templates/base/repo/file.html +++ /dev/null @@ -1,35 +0,0 @@ -{% extends "../template.html.include" %} - -{%- block title %}{{ name }} - {{ file }}{% endblock %} - -{%- block content %} -

{{ file.mode_symbolic }} {{ file }}


- -{% if file.is_directory %} - - - - - - - - - - {% for child in file.tree %} - - - - - {% endfor %} - -
ModeName
{{ child.mode_symbolic }}{{ child.path }}
-{% else %} - {% if file.is_binary %} - (File is binary) - {% else %} -
{% for line in split(file.contents, '\n') %}
-{{ printf("%5v", loop.index) }}  {{ line }}
-{% endfor %}
-{% endif %} -{% endif %} -{% endblock %} diff --git a/templates/base/repo/files.html b/templates/base/repo/files.html new file mode 100644 index 00000000..77b30c54 --- /dev/null +++ b/templates/base/repo/files.html @@ -0,0 +1,27 @@ +{% extends "../template.html.include" %} + +{%- block title %}{{ name }} - Tree{% endblock -%} + +{% block content %} + + + + + + + + + + {% for file in tree %} + + + {% if file.is_directory %} + + {% else %} + + {% endif %} + + {% endfor %} + +
ModeName
{{ file.mode_symbolic }}{{ file.path }}{{ file.path }}
+{% endblock %} diff --git a/templates/base/repo/tree.html b/templates/base/repo/tree.html index 7e62bcde..af179c48 100644 --- a/templates/base/repo/tree.html +++ b/templates/base/repo/tree.html @@ -1,8 +1,10 @@ {% extends "../template.html.include" %} -{%- block title %}{{ name }} - Tree{% endblock -%} +{%- block title %}{{ name }} - {{ tree }}{% endblock %} + +{%- block content %} +

{{ tree.mode_symbolic }} {{ tree }}


-{% block content %} @@ -12,10 +14,14 @@ - {% for file in tree %} + {% for child in tree.tree %} - - + + {% if child.is_directory %} + + {% else %} + + {% endif %} {% endfor %} diff --git a/templates/base/template.html.include b/templates/base/template.html.include index 8aa1fab6..18407614 100644 --- a/templates/base/template.html.include +++ b/templates/base/template.html.include @@ -19,13 +19,13 @@ git clone {{ host }}/{{ name }}

diff --git a/templates/docs/repo/blob.html b/templates/docs/repo/blob.html new file mode 100644 index 00000000..1d551ac9 --- /dev/null +++ b/templates/docs/repo/blob.html @@ -0,0 +1,15 @@ +{% extends "../template.html.include" %} + +{%- block title %}{{ blob }}{% endblock -%} + +{% block body %} +

{{ blob.mode_symbolic }} {{ blob }}


+ +{% if blob.is_binary %} +(File is binary) +{% else %} +
{% for line in split(blob.contents, '\n') %}
+{{ printf("%5v", loop.index) }}  {{ line }}
+{% endfor %}
+{% endif %} +{% endblock %} diff --git a/templates/docs/repo/file.html b/templates/docs/repo/file.html deleted file mode 100644 index 395f251b..00000000 --- a/templates/docs/repo/file.html +++ /dev/null @@ -1,47 +0,0 @@ -{% extends "../template.html.include" %} - -{%- block title %}{{ file }}{% endblock -%} - -{% block body %} -

{{ file.mode_symbolic }} {{ file }}


- -{% if file.is_directory %} -
{{ file.mode_symbolic }}{{ file.path }}{{ child.mode_symbolic }}{{ child.path }}{{ child.path }}
- - - - - - - - - {% for child in file.tree %} - - - - - {% endfor %} - -
ModeName
{{ child.mode_symbolic }}{{ child.path }}
- - {%- if length(file.contents) == 1 -%} - {%- set child = file.contents[0] -%} - {% if child.is_binary %} - (Child is binary) - {% else %} -
{% for line in split(child.contents, '\n') %}
-{{ printf("%5v", loop.index) }}  {{ line }}
-{% endfor %}
- {% endif %} - {%- endif -%} - -{% else %} - {% if file.is_binary %} - (File is binary) - {% else %} -
{% for line in split(file.contents, '\n') %}
-{{ printf("%5v", loop.index) }}  {{ line }}
-{% endfor %}
- {% endif %} -{% endif %} -{% endblock %} diff --git a/templates/docs/repo/files.html b/templates/docs/repo/files.html new file mode 100644 index 00000000..ce2f5e1c --- /dev/null +++ b/templates/docs/repo/files.html @@ -0,0 +1,27 @@ +{% extends "../template.html.include" %} + +{%- block title %}tree{% endblock -%} + +{% block body %} + + + + + + + + + + {% for file in tree %} + + + {% if file.is_directory %} + + {% else %} + + {% endif %} + + {% endfor %} + +
ModeName
{{ file.mode_symbolic }}{{ file.path }}{{ file.path }}
+{% endblock %} diff --git a/templates/docs/repo/tree.html b/templates/docs/repo/tree.html index 0e468cff..c8e36bdd 100644 --- a/templates/docs/repo/tree.html +++ b/templates/docs/repo/tree.html @@ -1,8 +1,10 @@ {% extends "../template.html.include" %} -{%- block title %}tree{% endblock -%} +{%- block title %}{{ tree }}{% endblock -%} {% block body %} +

{{ tree.mode_symbolic }} {{ tree }}


+ @@ -12,10 +14,14 @@ - {% for file in tree %} + {% for child in tree.tree %} - - + + {% if child.is_directory %} + + {% else %} + + {% endif %} {% endfor %} diff --git a/templates/docs/template.html.include b/templates/docs/template.html.include index 43e3af49..9ac9dfe0 100644 --- a/templates/docs/template.html.include +++ b/templates/docs/template.html.include @@ -23,10 +23,10 @@ Home | Log | - Files | + Files | Refs | - Readme | - License + Readme | + License

diff --git a/templates/stagit/repo/blob.html b/templates/stagit/repo/blob.html new file mode 100644 index 00000000..7b540790 --- /dev/null +++ b/templates/stagit/repo/blob.html @@ -0,0 +1,21 @@ +{% extends "../template.html.include" %} + +{%- block title -%} +{{ name }} - {{ blob }} +{%- endblock -%} + +{% block body %} +{% include "repo-table-header.html.include" %} + +

+

{{ blob.mode_symbolic }} {{ blob }}


+ + {% if blob.is_binary %} + (File is binary) + {% else %} +
{% for line in split(blob.contents, '\n') %}
+{{ printf("%7v", loop.index) }} {{ line }}
+{% endfor %}
+ {% endif %} +
+{% endblock %} diff --git a/templates/stagit/repo/file.html b/templates/stagit/repo/file.html deleted file mode 100644 index 70301d6f..00000000 --- a/templates/stagit/repo/file.html +++ /dev/null @@ -1,41 +0,0 @@ -{% extends "../template.html.include" %} - -{%- block title -%} -{{ name }} - {{ file }} -{%- endblock -%} - -{% block body %} -{% include "repo-table-header.html.include" %} - -
-

{{ file.mode_symbolic }} {{ file }}


- -{% if file.is_directory %} -
{{ file.mode_symbolic }}{{ file.path }}{{ child.mode_symbolic }}{{ child.path }}{{ child.path }}
- - - - - - - - - {% for child in file.contents %} - - - - - {% endfor %} - -
ModeName
{{ child.mode_symbolic }}{{ child.path }}
-{% else %} - {% if file.is_binary %} - (File is binary) - {% else %} -
{% for line in split(file.contents, '\n') %}
-{{ printf("%7v", loop.index) }} {{ line }}
-{% endfor %}
- {% endif %} -{% endif %} - -{% endblock %} diff --git a/templates/stagit/repo/files.html b/templates/stagit/repo/files.html new file mode 100644 index 00000000..176bb51c --- /dev/null +++ b/templates/stagit/repo/files.html @@ -0,0 +1,33 @@ +{% extends "../template.html.include" %} + +{%- block title -%} +{{ name }} - files +{%- endblock -%} + +{% block body %} +{% include "repo-table-header.html.include" %} + +
+ + + + + + + + + + {% for file in tree %} + + + {% if file.is_directory %} + + {% else %} + + {% endif %} + + {% endfor %} + +
ModeName
{{ file.mode_symbolic }}{{ file.path }}{{ file.path }}
+
+{% endblock %} diff --git a/templates/stagit/repo/repo-table-header.html.include b/templates/stagit/repo/repo-table-header.html.include index e713331c..e3069501 100644 --- a/templates/stagit/repo/repo-table-header.html.include +++ b/templates/stagit/repo/repo-table-header.html.include @@ -16,9 +16,9 @@ {{ description }} git clone {{ host }}/{{ name }} - Files | Refs - {% if readme %} | Readme{% endif %} - {% if license %} | License{% endif %} + Files | Refs + {% if readme %} | Readme{% endif %} + {% if license %} | License{% endif %}
diff --git a/templates/stagit/repo/tree.html b/templates/stagit/repo/tree.html index 7d5d1ab9..85d94748 100644 --- a/templates/stagit/repo/tree.html +++ b/templates/stagit/repo/tree.html @@ -1,13 +1,15 @@ {% extends "../template.html.include" %} {%- block title -%} -{{ name }} - target +{{ name }} - {{ tree }} {%- endblock -%} {% block body %} {% include "repo-table-header.html.include" %}
+

{{ tree.mode_symbolic }} {{ tree }}


+ @@ -17,10 +19,14 @@ - {% for file in tree %} + {% for child in tree.tree %} - - + + {% if child.is_directory %} + + {% else %} + + {% endif %} {% endfor %} From 949e1a81c07afa47250a3b1770c644225a7cafe3 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 22:40:37 +0000 Subject: [PATCH 8/9] Fix test.sh and rename special templates to foreach..html Fixes the CI failure on the previous commit: test.sh's golden-file test still expected output under gitja/file/... (the pre-rename output directory), and test/templates/repo/file.html was never updated to match the blob rename. Renamed that test template to foreach.blob.html (see below), updated its dumped "file"/"scope: file" labels to "blob"/"scope: blob" (the values themselves - paths, hrefs, contents - are unchanged, since it's the same underlying object under a renamed scope), moved test/expected/gitja/file/ to test/expected/gitja/blob/ with matching label updates, and fixed test.sh's TESTS array paths. Also rename the three special per-object templates to foreach.blob.html/foreach.commit.html/foreach.tree.html (from blob.html/commit.html/tree.html), across the bundled templates, the test template, and Env.hs's lookups. Makes it visually obvious in a directory listing which templates in repo/ run once per repo vs. once per object - previously "blob.html"/"commit.html"/"tree.html" looked like just three more arbitrarily-named repo templates alongside index.html/refs.html/etc, despite being the special-cased ones. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_012RfZTyHvxLyVp4yaWmNvhN --- DOCUMENTATION.md | 18 ++++++------ src/Env.hs | 8 +++--- .../repo/{blob.html => foreach.blob.html} | 0 .../repo/{commit.html => foreach.commit.html} | 0 .../repo/{tree.html => foreach.tree.html} | 0 .../repo/{blob.html => foreach.blob.html} | 0 .../repo/{commit.html => foreach.commit.html} | 0 .../repo/{tree.html => foreach.tree.html} | 0 .../repo/{blob.html => foreach.blob.html} | 0 .../repo/{commit.html => foreach.commit.html} | 0 .../repo/{tree.html => foreach.tree.html} | 0 .../gitja/blob/github.FUNDING.yml.html | 22 +++++++++++++++ .../test.templates.so_called_binary_file.html | 21 ++++++++++++++ .../gitja/blob/test.templates.style.css.html | 21 ++++++++++++++ .../gitja/file/github.FUNDING.yml.html | 22 --------------- .../test.templates.so_called_binary_file.html | 21 -------------- .../gitja/file/test.templates.style.css.html | 21 -------------- test/templates/repo/file.html | 28 ------------------- test/templates/repo/foreach.blob.html | 28 +++++++++++++++++++ .../repo/{commit.html => foreach.commit.html} | 0 test/test.sh | 6 ++-- 21 files changed, 109 insertions(+), 107 deletions(-) rename templates/base/repo/{blob.html => foreach.blob.html} (100%) rename templates/base/repo/{commit.html => foreach.commit.html} (100%) rename templates/base/repo/{tree.html => foreach.tree.html} (100%) rename templates/docs/repo/{blob.html => foreach.blob.html} (100%) rename templates/docs/repo/{commit.html => foreach.commit.html} (100%) rename templates/docs/repo/{tree.html => foreach.tree.html} (100%) rename templates/stagit/repo/{blob.html => foreach.blob.html} (100%) rename templates/stagit/repo/{commit.html => foreach.commit.html} (100%) rename templates/stagit/repo/{tree.html => foreach.tree.html} (100%) create mode 100644 test/expected/gitja/blob/github.FUNDING.yml.html create mode 100644 test/expected/gitja/blob/test.templates.so_called_binary_file.html create mode 100644 test/expected/gitja/blob/test.templates.style.css.html delete mode 100644 test/expected/gitja/file/github.FUNDING.yml.html delete mode 100644 test/expected/gitja/file/test.templates.so_called_binary_file.html delete mode 100644 test/expected/gitja/file/test.templates.style.css.html delete mode 100644 test/templates/repo/file.html create mode 100644 test/templates/repo/foreach.blob.html rename test/templates/repo/{commit.html => foreach.commit.html} (100%) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 07cacbc6..4ad3921f 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -82,9 +82,9 @@ To illustrate, this is the expected structure: repo/ inside_this_folder.html two_names_are_special.html - blob.html - commit.html - tree.html + foreach.blob.html + foreach.commit.html + foreach.tree.html The top-level folder, here `template`, is that which is specified in the config file. @@ -100,11 +100,13 @@ information pertaining to a single git repository. The template files contained within this folder are parsed and output once per git repository. The exceptions to this are the three special template files with the names -"blob.html", "commit.html" and "tree.html". These have access to the *blob -scope*, *commit scope* and *tree scope* respectively, and are parsed and -output once per blob, commit or tree found anywhere in the repository, at any -depth (a submodule reference is currently treated as a blob whose content is -its target commit hash, rather than as its own scope). +"foreach.blob.html", "foreach.commit.html" and "foreach.tree.html" - named to +make clear that, unlike everything else in "repo/", these are parsed and +output once *per* blob, commit or tree found anywhere in the repository, at +any depth, rather than once per repository. These have access to the *blob +scope*, *commit scope* and *tree scope* respectively (a submodule reference +is currently treated as a blob whose content is its target commit hash, +rather than as its own scope). The resulting folder structure found in `output` will look like this (if `repos` only contains gitja): diff --git a/src/Env.hs b/src/Env.hs index a864c9b7..9aee69f8 100644 --- a/src/Env.hs +++ b/src/Env.hs @@ -105,12 +105,12 @@ loadEnv quiet force config = do -- Load files from template directory indexT <- collectTemplates files - commitT <- findTemplate "commit.html" filesRepo - blobT <- findTemplate "blob.html" filesRepo - treeT <- findTemplate "tree.html" filesRepo + commitT <- findTemplate "foreach.commit.html" filesRepo + blobT <- findTemplate "foreach.blob.html" filesRepo + treeT <- findTemplate "foreach.tree.html" filesRepo repoT <- collectTemplates - . filter (flip notElem ["commit.html", "blob.html", "tree.html"] . toFilePath . filename) + . filter (flip notElem ["foreach.commit.html", "foreach.blob.html", "foreach.tree.html"] . toFilePath . filename) $ filesRepo -- Exit early if we didn't find any templates diff --git a/templates/base/repo/blob.html b/templates/base/repo/foreach.blob.html similarity index 100% rename from templates/base/repo/blob.html rename to templates/base/repo/foreach.blob.html diff --git a/templates/base/repo/commit.html b/templates/base/repo/foreach.commit.html similarity index 100% rename from templates/base/repo/commit.html rename to templates/base/repo/foreach.commit.html diff --git a/templates/base/repo/tree.html b/templates/base/repo/foreach.tree.html similarity index 100% rename from templates/base/repo/tree.html rename to templates/base/repo/foreach.tree.html diff --git a/templates/docs/repo/blob.html b/templates/docs/repo/foreach.blob.html similarity index 100% rename from templates/docs/repo/blob.html rename to templates/docs/repo/foreach.blob.html diff --git a/templates/docs/repo/commit.html b/templates/docs/repo/foreach.commit.html similarity index 100% rename from templates/docs/repo/commit.html rename to templates/docs/repo/foreach.commit.html diff --git a/templates/docs/repo/tree.html b/templates/docs/repo/foreach.tree.html similarity index 100% rename from templates/docs/repo/tree.html rename to templates/docs/repo/foreach.tree.html diff --git a/templates/stagit/repo/blob.html b/templates/stagit/repo/foreach.blob.html similarity index 100% rename from templates/stagit/repo/blob.html rename to templates/stagit/repo/foreach.blob.html diff --git a/templates/stagit/repo/commit.html b/templates/stagit/repo/foreach.commit.html similarity index 100% rename from templates/stagit/repo/commit.html rename to templates/stagit/repo/foreach.commit.html diff --git a/templates/stagit/repo/tree.html b/templates/stagit/repo/foreach.tree.html similarity index 100% rename from templates/stagit/repo/tree.html rename to templates/stagit/repo/foreach.tree.html diff --git a/test/expected/gitja/blob/github.FUNDING.yml.html b/test/expected/gitja/blob/github.FUNDING.yml.html new file mode 100644 index 00000000..98ecfdae --- /dev/null +++ b/test/expected/gitja/blob/github.FUNDING.yml.html @@ -0,0 +1,22 @@ +.github/FUNDING.yml +scope: blob +blob: .github/FUNDING.yml +blob.path: .github/FUNDING.yml +blob.href: github.FUNDING.yml.html +blob.contents: github: ["m-col"] +custom: ["https://liberapay.com/mcol"] +blob.mode: Plain +blob.mode_octal: 00644 +blob.mode_symbolic: -rw-r--r-- +if blob.is_directory then "directory" else "file": "file" + +blob scope also should have data from repo scope available: +host: https://github.com/m-col/gitja +repos: gitja +name: gitja +description: 🐙 Templated web page generator for your git repositories +commits: (skipped) +tree: (skipped) +tags: 1 +readme.path: README.rst +license.path: LICENSE diff --git a/test/expected/gitja/blob/test.templates.so_called_binary_file.html b/test/expected/gitja/blob/test.templates.so_called_binary_file.html new file mode 100644 index 00000000..ed9d43c2 --- /dev/null +++ b/test/expected/gitja/blob/test.templates.so_called_binary_file.html @@ -0,0 +1,21 @@ +test/templates/so_called_binary_file +scope: blob +blob: test/templates/so_called_binary_file +blob.path: test/templates/so_called_binary_file +blob.href: test.templates.so_called_binary_file.html +blob.contents: (binary) +blob.mode: Plain +blob.mode_octal: 00644 +blob.mode_symbolic: -rw-r--r-- +if blob.is_directory then "directory" else "file": "file" + +blob scope also should have data from repo scope available: +host: https://github.com/m-col/gitja +repos: gitja +name: gitja +description: 🐙 Templated web page generator for your git repositories +commits: (skipped) +tree: (skipped) +tags: 1 +readme.path: README.rst +license.path: LICENSE diff --git a/test/expected/gitja/blob/test.templates.style.css.html b/test/expected/gitja/blob/test.templates.style.css.html new file mode 100644 index 00000000..93d8a281 --- /dev/null +++ b/test/expected/gitja/blob/test.templates.style.css.html @@ -0,0 +1,21 @@ +test/templates/style.css +scope: blob +blob: test/templates/style.css +blob.path: test/templates/style.css +blob.href: test.templates.style.css.html +blob.contents: html +blob.mode: Plain +blob.mode_octal: 00644 +blob.mode_symbolic: -rw-r--r-- +if blob.is_directory then "directory" else "file": "file" + +blob scope also should have data from repo scope available: +host: https://github.com/m-col/gitja +repos: gitja +name: gitja +description: 🐙 Templated web page generator for your git repositories +commits: (skipped) +tree: (skipped) +tags: 1 +readme.path: README.rst +license.path: LICENSE diff --git a/test/expected/gitja/file/github.FUNDING.yml.html b/test/expected/gitja/file/github.FUNDING.yml.html deleted file mode 100644 index 95a8659b..00000000 --- a/test/expected/gitja/file/github.FUNDING.yml.html +++ /dev/null @@ -1,22 +0,0 @@ -.github/FUNDING.yml -scope: file -file: .github/FUNDING.yml -file.path: .github/FUNDING.yml -file.href: github.FUNDING.yml.html -file.contents: github: ["m-col"] -custom: ["https://liberapay.com/mcol"] -file.mode: Plain -file.mode_octal: 00644 -file.mode_symbolic: -rw-r--r-- -if file.is_directory then "directory" else "file": "file" - -file scope also should have data from repo scope available: -host: https://github.com/m-col/gitja -repos: gitja -name: gitja -description: 🐙 Templated web page generator for your git repositories -commits: (skipped) -tree: (skipped) -tags: 1 -readme.path: README.rst -license.path: LICENSE diff --git a/test/expected/gitja/file/test.templates.so_called_binary_file.html b/test/expected/gitja/file/test.templates.so_called_binary_file.html deleted file mode 100644 index 2e815d3c..00000000 --- a/test/expected/gitja/file/test.templates.so_called_binary_file.html +++ /dev/null @@ -1,21 +0,0 @@ -test/templates/so_called_binary_file -scope: file -file: test/templates/so_called_binary_file -file.path: test/templates/so_called_binary_file -file.href: test.templates.so_called_binary_file.html -file.contents: (binary) -file.mode: Plain -file.mode_octal: 00644 -file.mode_symbolic: -rw-r--r-- -if file.is_directory then "directory" else "file": "file" - -file scope also should have data from repo scope available: -host: https://github.com/m-col/gitja -repos: gitja -name: gitja -description: 🐙 Templated web page generator for your git repositories -commits: (skipped) -tree: (skipped) -tags: 1 -readme.path: README.rst -license.path: LICENSE diff --git a/test/expected/gitja/file/test.templates.style.css.html b/test/expected/gitja/file/test.templates.style.css.html deleted file mode 100644 index 315187f4..00000000 --- a/test/expected/gitja/file/test.templates.style.css.html +++ /dev/null @@ -1,21 +0,0 @@ -test/templates/style.css -scope: file -file: test/templates/style.css -file.path: test/templates/style.css -file.href: test.templates.style.css.html -file.contents: html -file.mode: Plain -file.mode_octal: 00644 -file.mode_symbolic: -rw-r--r-- -if file.is_directory then "directory" else "file": "file" - -file scope also should have data from repo scope available: -host: https://github.com/m-col/gitja -repos: gitja -name: gitja -description: 🐙 Templated web page generator for your git repositories -commits: (skipped) -tree: (skipped) -tags: 1 -readme.path: README.rst -license.path: LICENSE diff --git a/test/templates/repo/file.html b/test/templates/repo/file.html deleted file mode 100644 index e5b0ede1..00000000 --- a/test/templates/repo/file.html +++ /dev/null @@ -1,28 +0,0 @@ -{% extends "../title.html.include" -%} -{%- block title -%}{{ file }}{%- endblock -%} - -{% block body %} -scope: file -file: {{ file }} -file.path: {{ file.path }} -file.href: {{ file.href }} -file.contents: {% if file.is_binary %}(binary) -{% else %}{{ file.contents }}{% endif %} -file.mode: {{ file.mode }} -file.mode_octal: {{ file.mode_octal }} -file.mode_symbolic: {{ file.mode_symbolic }} -if file.is_directory then "directory" else "file": - {%- if file.is_directory %} "directory"{% else %} "file"{% endif %} - - -file scope also should have data from repo scope available: -host: {{ host }} -repos: {{ repositories }} -name: {{ name }} -description: {{ description }} -commits: (skipped) -tree: (skipped) -tags: {{ length(filter(tags, (t) -> t.name == "0.2.0")) }} -readme.path: {{ readme.path }} -license.path: {{ license.path }} -{% endblock %} diff --git a/test/templates/repo/foreach.blob.html b/test/templates/repo/foreach.blob.html new file mode 100644 index 00000000..9ca72d5b --- /dev/null +++ b/test/templates/repo/foreach.blob.html @@ -0,0 +1,28 @@ +{% extends "../title.html.include" -%} +{%- block title -%}{{ blob }}{%- endblock -%} + +{% block body %} +scope: blob +blob: {{ blob }} +blob.path: {{ blob.path }} +blob.href: {{ blob.href }} +blob.contents: {% if blob.is_binary %}(binary) +{% else %}{{ blob.contents }}{% endif %} +blob.mode: {{ blob.mode }} +blob.mode_octal: {{ blob.mode_octal }} +blob.mode_symbolic: {{ blob.mode_symbolic }} +if blob.is_directory then "directory" else "file": + {%- if blob.is_directory %} "directory"{% else %} "file"{% endif %} + + +blob scope also should have data from repo scope available: +host: {{ host }} +repos: {{ repositories }} +name: {{ name }} +description: {{ description }} +commits: (skipped) +tree: (skipped) +tags: {{ length(filter(tags, (t) -> t.name == "0.2.0")) }} +readme.path: {{ readme.path }} +license.path: {{ license.path }} +{% endblock %} diff --git a/test/templates/repo/commit.html b/test/templates/repo/foreach.commit.html similarity index 100% rename from test/templates/repo/commit.html rename to test/templates/repo/foreach.commit.html diff --git a/test/test.sh b/test/test.sh index b34756ca..35fd9c32 100755 --- a/test/test.sh +++ b/test/test.sh @@ -36,10 +36,10 @@ TESTS=( "static/a_nice_file" # Static folder at top level "gitja/index.html" # Repo scope "gitja/commit/0292014748caae952bbc8dd6225680d83c0a5135.html" # A commit - "gitja/file/test.templates.style.css.html" # A plain text file + "gitja/blob/test.templates.style.css.html" # A plain text file "so_called_binary_file" # Static file at top level that git considers binary - "gitja/file/test.templates.so_called_binary_file.html" # The binary file - "gitja/file/github.FUNDING.yml.html" # HREF drops leading period + "gitja/blob/test.templates.so_called_binary_file.html" # The binary file + "gitja/blob/github.FUNDING.yml.html" # HREF drops leading period "gitja/log.html" # Symbolic link inside repo/ "gitja/static/another_file" # Static folder inside repo/ ) From 24b1cda840d2f623392ef6952235c90e4de1e7f0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 22:49:58 +0000 Subject: [PATCH 9/9] Rename TreeFile to TreeEntry and tree_recursive to entries, add trees field TreeEntry better reflects that these represent any tree object (blob, tree, or submodule), and entries/trees give the flattened lists clearer, distinct names. --- DOCUMENTATION.md | 5 ++- src/Repositories.hs | 37 +++++++++--------- src/Types.hs | 95 +++++++++++++++++++++++---------------------- 3 files changed, 70 insertions(+), 67 deletions(-) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 4ad3921f..ca064c77 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -155,8 +155,9 @@ The variables available within each scope are listed here for reference: | | description | The repository's description (see below). | | | commits | A list of the repository's commits. | | | tree | A list of the repository root tree's entries. | -| | tree\_recursive | A flat list of *every* entry (blob or tree) at any depth in the repository, like `git ls-tree -r -t`. | +| | entries | A flat list of *every* entry (blob or tree) at any depth in the repository, like `git ls-tree -r -t`. | | | blobs | A flat list of *every* blob (file) in the repository. | +| | trees | A flat list of *every* tree (directory) at any depth in the repository. | | | tags | A list of the refs corresponding to tags. | | | branches | A list of the refs corresponding to branches. | | | readme | The repository's readme file, if it has one. | @@ -192,7 +193,7 @@ Here is the reference of attributes available on the variables that have them: | | is\_directory | A boolean, useful for ginger conditionals. | | | is\_binary | A boolean, tells you if the contents can be rendered. | | | tree | This tree's direct contents (n/a for a blob). | -| | tree\_recursive | A flat list of *every* entry (blob or tree) at any depth under this tree (n/a for a blob). | +| | entries | A flat list of *every* entry (blob or tree) at any depth under this tree (n/a for a blob). | | ref | name | The tag or branch name. | | | commit | The commit pointed to by the tag or branch. | | commit | id | The SHA of the given commit. | diff --git a/src/Repositories.hs b/src/Repositories.hs index fb164aa0..5768a9af 100644 --- a/src/Repositories.hs +++ b/src/Repositories.hs @@ -185,7 +185,7 @@ package :: Path Rel Dir -> T.Text -> [Commit] -> - [TreeFile] -> + [TreeEntry] -> HashMap.HashMap T.Text (GVal RunRepo) package env repos name description commits tree = HashMap.fromList @@ -195,16 +195,17 @@ package env repos name description commits tree = , ("description", toGVal description) , ("commits", toGVal commits) , ("tree", toGVal tree) - , ("tree_recursive", toGVal . concatMap flattenTree $ tree) + , ("entries", toGVal . concatMap flattenTree $ tree) , ("blobs", toGVal . concatMap flattenFiles $ tree) + , ("trees", toGVal . concatMap flattenTrees $ tree) , ("readme", toGVal . findFile "readme" $ tree) , ("license", toGVal . findFile "license" $ tree) ] where -- Find a file in the tree starting with the specified prefix. The prefix is looked -- for on the full path, so will only find files in the top level directory. - findFile :: T.Text -> [TreeFile] -> Maybe TreeFile - findFile prefix = find (T.isPrefixOf prefix . T.toLower . treeFilePath) + findFile :: T.Text -> [TreeEntry] -> Maybe TreeEntry + findFile prefix = find (T.isPrefixOf prefix . T.toLower . treeEntryPath) {- Collect commit history up to a head. @@ -310,17 +311,17 @@ loadDiff gitCommit = do Collect tree information for the given commit. Recurses on directories to list their contents. -} -getTree :: Git.CommitOid LgRepo -> ReaderT LgRepo IO [TreeFile] +getTree :: Git.CommitOid LgRepo -> ReaderT LgRepo IO [TreeEntry] getTree = getTree' "" 0 . Git.commitTree <=< Git.lookupCommit where - getTree' :: Git.TreeFilePath -> Int -> Git.TreeOid LgRepo -> ReaderT LgRepo IO [TreeFile] + getTree' :: Git.TreeFilePath -> Int -> Git.TreeOid LgRepo -> ReaderT LgRepo IO [TreeEntry] getTree' parent count toid = do one <- Git.lookupTree toid entries <- Git.listTreeEntries one let entries' = fmap (prependParent parent) entries contents <- mapM (\x -> getEntryContents x (count + 1)) entries' modes <- mapM (getEntryModes . snd) entries' - return $ zipWith3 TreeFile (fmap treePaths entries') contents modes + return $ zipWith3 TreeEntry (fmap treePaths entries') contents modes prependParent :: Git.TreeFilePath -> @@ -329,7 +330,7 @@ getTree = getTree' "" 0 . Git.commitTree <=< Git.lookupCommit prependParent "" pathentry = pathentry prependParent parent (path, entry) = (mconcat [parent, "/", path], entry) - getEntryContents :: (Git.TreeFilePath, Git.TreeEntry LgRepo) -> Int -> ReaderT LgRepo IO TreeFileContents + getEntryContents :: (Git.TreeFilePath, Git.TreeEntry LgRepo) -> Int -> ReaderT LgRepo IO TreeEntryContents getEntryContents (_, Git.BlobEntry oid _) _ = getBlobContents oid getEntryContents (path, Git.TreeEntry oid) count = FolderContents <$> getTree' path count oid getEntryContents (_, Git.CommitEntry oid) _ = return . FileContents . B.fromString . show . untag $ oid @@ -384,20 +385,20 @@ getUpdates directory cs = go cs (return []) ((x :) <$> go xs) -flattenFiles :: TreeFile -> [TreeFile] -flattenFiles treefile = case treeFileContents treefile of +flattenFiles :: TreeEntry -> [TreeEntry] +flattenFiles treeentry = case treeEntryContents treeentry of FolderContents files -> concatMap flattenFiles files - _ -> [treefile] + _ -> [treeentry] -flattenTrees :: TreeFile -> [TreeFile] -flattenTrees treefile = case treeFileContents treefile of - FolderContents files -> treefile : concatMap flattenTrees files +flattenTrees :: TreeEntry -> [TreeEntry] +flattenTrees treeentry = case treeEntryContents treeentry of + FolderContents files -> treeentry : concatMap flattenTrees files _ -> [] -getUpdatedFiles :: [TreeFile] -> [Commit] -> [TreeFile] +getUpdatedFiles :: [TreeEntry] -> [Commit] -> [TreeEntry] getUpdatedFiles [] _ = [] getUpdatedFiles _ [] = [] -getUpdatedFiles files commits = filter ((`elem` updated) . treeFilePath) files +getUpdatedFiles files commits = filter ((`elem` updated) . treeEntryPath) files where updated :: [T.Text] updated = fmap (bsToText . diffNewFile) . concatMap commitDiffs $ commits @@ -447,10 +448,10 @@ genTarget scope runInIO quiet force template category directory href target = do commitHref :: Commit -> FilePath commitHref = (++ ".html") . commitHash -blobHref :: TreeFile -> FilePath +blobHref :: TreeEntry -> FilePath blobHref = T.unpack . treePathToHref -treeHref :: TreeFile -> FilePath +treeHref :: TreeEntry -> FilePath treeHref = T.unpack . treePathToHref {- diff --git a/src/Types.hs b/src/Types.hs index 8d89ccb7..074a1884 100644 --- a/src/Types.hs +++ b/src/Types.hs @@ -214,15 +214,16 @@ lineAsLookup line = \case {- Next we have some data used to represent a repository's tree and the different kinds of -objects contained therein. +objects contained therein. A TreeEntry is any single entry found in a git tree object - +a blob, a nested tree, or a commit (submodule reference). -} -data TreeFile = TreeFile - { treeFilePath :: T.Text - , treeFileContents :: TreeFileContents - , treeFileMode :: TreeEntryMode +data TreeEntry = TreeEntry + { treeEntryPath :: T.Text + , treeEntryContents :: TreeEntryContents + , treeEntryMode :: TreeEntryMode } -data TreeFileContents = BinaryContents | FileContents ByteString | FolderContents [TreeFile] +data TreeEntryContents = BinaryContents | FileContents ByteString | FolderContents [TreeEntry] data TreeEntryMode = ModeDirectory | ModePlain | ModeExecutable | ModeSymlink | ModeSubmodule deriving stock (Show) @@ -240,7 +241,7 @@ blobkindToMode Git.SymlinkBlob = ModeSymlink {- This -} -getBlobContents :: Git.BlobOid LgRepo -> ReaderT LgRepo IO TreeFileContents +getBlobContents :: Git.BlobOid LgRepo -> ReaderT LgRepo IO TreeEntryContents getBlobContents oid = do repo <- Git.getRepository blobPtr <- liftIO mallocForeignPtr @@ -259,63 +260,63 @@ getBlobContents oid = do GVal implementations for data definitions above, allowing commits to be rendered in Ginger templates. -} -instance ToGVal m TreeFile where - toGVal :: TreeFile -> GVal m - toGVal treefile = +instance ToGVal m TreeEntry where + toGVal :: TreeEntry -> GVal m + toGVal treeentry = def - { asHtml = html . treeFilePath $ treefile - , asText = treeFilePath treefile - , asLookup = Just . treeAsLookup $ treefile + { asHtml = html . treeEntryPath $ treeentry + , asText = treeEntryPath treeentry + , asLookup = Just . treeAsLookup $ treeentry , asBoolean = True -- Used for conditionally checking readme/license template variables. } -instance ToGVal m TreeFileContents where - toGVal :: TreeFileContents -> GVal m +instance ToGVal m TreeEntryContents where + toGVal :: TreeEntryContents -> GVal m toGVal BinaryContents = def toGVal (FileContents bytestring) = toGVal bytestring - toGVal (FolderContents treeFiles) = + toGVal (FolderContents treeEntries) = def - { asHtml = html . T.pack . show . fmap treeFilePath $ treeFiles - , asText = T.pack . show . fmap treeFilePath $ treeFiles - , asList = Just . fmap toGVal $ treeFiles + { asHtml = html . T.pack . show . fmap treeEntryPath $ treeEntries + , asText = T.pack . show . fmap treeEntryPath $ treeEntries + , asList = Just . fmap toGVal $ treeEntries } {- Recursively descend into a tree, keeping every entry along the way - blobs and trees alike - as a single flat list. Equivalent to `git ls-tree -r -t`. -} -flattenTree :: TreeFile -> [TreeFile] -flattenTree treefile = case treeFileContents treefile of - FolderContents files -> treefile : concatMap flattenTree files - _ -> [treefile] - -treeAsLookup :: TreeFile -> T.Text -> Maybe (GVal m) -treeAsLookup treefile = \case - "path" -> Just . toGVal . treeFilePath $ treefile - "name" -> Just . toGVal . FP.takeFileName . T.unpack . treeFilePath $ treefile - "href" -> Just . toGVal . treePathToHref $ treefile - "contents" -> Just . toGVal . treeFileContents $ treefile - "tree" -> Just . toGVal . treeFileGetTree . treeFileContents $ treefile - "tree_recursive" -> Just . toGVal . concatMap flattenTree . treeFileGetTree . treeFileContents $ treefile - "mode" -> Just . toGVal . drop 4 . show . treeFileMode $ treefile - "mode_octal" -> Just . toGVal . modeToOctal . treeFileMode $ treefile - "mode_symbolic" -> Just . toGVal . modeToSymbolic . treeFileMode $ treefile - "is_binary" -> Just . toGVal . treeFileIsBinary $ treefile - "is_directory" -> Just . toGVal . treeFileIsDirectory $ treefile +flattenTree :: TreeEntry -> [TreeEntry] +flattenTree treeentry = case treeEntryContents treeentry of + FolderContents entries -> treeentry : concatMap flattenTree entries + _ -> [treeentry] + +treeAsLookup :: TreeEntry -> T.Text -> Maybe (GVal m) +treeAsLookup treeentry = \case + "path" -> Just . toGVal . treeEntryPath $ treeentry + "name" -> Just . toGVal . FP.takeFileName . T.unpack . treeEntryPath $ treeentry + "href" -> Just . toGVal . treePathToHref $ treeentry + "contents" -> Just . toGVal . treeEntryContents $ treeentry + "tree" -> Just . toGVal . treeEntryGetTree . treeEntryContents $ treeentry + "entries" -> Just . toGVal . concatMap flattenTree . treeEntryGetTree . treeEntryContents $ treeentry + "mode" -> Just . toGVal . drop 4 . show . treeEntryMode $ treeentry + "mode_octal" -> Just . toGVal . modeToOctal . treeEntryMode $ treeentry + "mode_symbolic" -> Just . toGVal . modeToSymbolic . treeEntryMode $ treeentry + "is_binary" -> Just . toGVal . treeEntryIsBinary $ treeentry + "is_directory" -> Just . toGVal . treeEntryIsDirectory $ treeentry _ -> Nothing where -- The entries of this tree object - i.e. this directory's immediate children. - treeFileGetTree :: TreeFileContents -> [TreeFile] - treeFileGetTree (FolderContents fs) = fs - treeFileGetTree _ = [] + treeEntryGetTree :: TreeEntryContents -> [TreeEntry] + treeEntryGetTree (FolderContents fs) = fs + treeEntryGetTree _ = [] - treeFileIsBinary :: TreeFile -> Bool - treeFileIsBinary treefile' = case treeFileContents treefile' of + treeEntryIsBinary :: TreeEntry -> Bool + treeEntryIsBinary treeentry' = case treeEntryContents treeentry' of BinaryContents -> True _ -> False - treeFileIsDirectory :: TreeFile -> Bool - treeFileIsDirectory treefile' = case treeFileContents treefile' of + treeEntryIsDirectory :: TreeEntry -> Bool + treeEntryIsDirectory treeentry' = case treeEntryContents treeentry' of FolderContents _ -> True _ -> False @@ -334,10 +335,10 @@ treeAsLookup treefile = \case modeToSymbolic ModeSubmodule = "git-module" {- -Get the name of a tree file path's HTML file. Leading periods are dropped. +Get the name of a tree entry path's HTML file. Leading periods are dropped. -} -treePathToHref :: TreeFile -> T.Text -treePathToHref = T.dropWhile (== '.') . flip T.append ".html" . T.replace "/" "." . treeFilePath +treePathToHref :: TreeEntry -> T.Text +treePathToHref = T.dropWhile (== '.') . flip T.append ".html" . T.replace "/" "." . treeEntryPath {- Data to store information about references: tags and branches.
{{ file.mode_symbolic }}{{ file.path }}{{ child.mode_symbolic }}{{ child.path }}{{ child.path }}