on e94de40 help sign in
haskell.nix/lib/clean-git.nix
164 lines 6.1 kB history raw
1# From https://github.com/NixOS/nix/issues/2944
2{ lib, runCommand, git, cleanSourceWith }:
3{ name ? null, src, subDir ? "" }:
5# The function call
6#
7# gitSource ./toplevel subpath
8#
9# creates a Nix store path of ./toplevel/subpath that includes only those files
10# tracked by git. More precisely: mentioned in the git index (i.e. git add is enough
11# to get them to be included, you do not have to commit).
13# This is a whitelist-based alternative to manually listing files or using
14# nix-gitignore.
16# Internally, it works by calling git ls-files at evaluation time. To
17# avoid copying all of `.git` to the git store, it only copies the least amount
18# of files necessary for `git ls-files` to work; this is a bit fragile, but
19# very fast.
21with builtins;
23# We read the git index once, before getting the subdir parameter, so that it
24# is shared among multiple invocations of gitSource:
26let
27 filter_from_list = root: files:
28 let
29 all_paren_dirs = p:
30 if p == "." || p == "/"
31 then []
32 else [ p ] ++ all_paren_dirs (dirOf p);
34 whitelist_set = listToAttrs (
35 concatMap (p:
36 # Using `origSrcSubDir` (if present) makes it possible to cleanGit src that
37 # has already been cleaned with cleanSrcWith.
38 let full_path = root.origSrcSubDir or (toString root) + "/${p}"; in
39 map (p': { name = p'; value = true; }) (all_paren_dirs full_path)
40 ) files
41 );
42 in
43 p: t: hasAttr (toString p) whitelist_set;
45 has_prefix = prefix: s:
46 prefix == builtins.substring 0 (builtins.stringLength prefix) s;
47 remove_prefix = prefix: s:
48 builtins.substring
49 (builtins.stringLength prefix)
50 (builtins.stringLength s - builtins.stringLength prefix)
51 s;
53 lines = s: filter (x : x != [] && x != "") (split "\n" s);
55 origSrcSubDir = toString (src.origSrcSubDir or src);
56in
58if builtins.pathExists (origSrcSubDir + "/.git")
59then
60 let
61 hasIndex = builtins.pathExists (origSrcSubDir + "/.git/index");
62 isWorktree = (builtins.readDir origSrcSubDir).".git" == "regular";
64 # Identify the .git directory and filter just the files that we need.
65 gitDir = cleanSourceWith ({
66 caller = "cleanGit";
67 name = (if name == null then "" else name + "-") + "gitFiles";
68 filter = path: type:
69 type == "directory" ||
70 lib.any (i: (lib.hasSuffix i path)) [
71 "/config" "/index" "/HEAD" ] ||
72 (lib.strings.hasInfix "modules/" path &&
73 lib.any (i: (lib.hasSuffix i path)) [
74 "/config" "/index" "/HEAD" "/objects" "/refs" ]);
75 } // (
76 if hasIndex
77 then { inherit src; subDir = ".git"; }
78 else if !isWorktree
79 then abort "cleanGit: ${origSrcSubDir + "/.git"} has no index file"
80 else {
81 # likely a git worktree, so follow the indirection
82 src =
83 let
84 git_content = lines (readFile (origSrcSubDir + "/.git"));
85 first_line = head git_content;
86 prefix = "gitdir: ";
87 ok = length git_content == 1 && has_prefix prefix first_line;
88 in
89 if ok
90 then /. + remove_prefix prefix first_line
91 else abort "gitSource.nix: Cannot parse ${origSrcSubDir + "/.git"}";
92 }));
94 # Worktrees have a commondir pointing to the common `.git` dir. We need the
95 # config file from there to get the list of active submodules right.
96 commonConfig = if builtins.pathExists (toString gitDir.origSrc + "/commondir")
97 then
98 # The commondir file contains just the relative path of the
99 # common `.git` dir.
100 let
101 git_content = lines (readFile (toString gitDir.origSrc + "/commondir"));
102 first_line = head git_content;
103 in gitDir.origSrc + ("/" + first_line + "/config")
104 else gitDir + "/config";
106 # We need the .gitmodules file for submoules to work.
107 gitModulesStr = origSrcSubDir + "/.gitmodules";
108 gitModules = builtins.path { name = "gitmodules"; path = gitModulesStr; };
110 gitSubmoduleFiles = cleanSourceWith {
111 caller = "cleanGit";
112 name = (if name == null then "" else name + "-") + "gitSubmoduleFiles";
113 inherit src;
114 filter = path: type:
115 type == "directory" # TODO get sudmodule directories from `.gitmodules`
116 # and use that to filter directory tree here
117 ||
118 lib.any (i: (lib.hasSuffix i path)) [
119 "/.git" ".gitmodules" ".git/config" ".git/index" ".git/HEAD" ".git/objects" ".git/refs" ] ||
120 (lib.strings.hasInfix ".git/modules/" path &&
121 lib.any (i: (lib.hasSuffix i path)) [
122 "config" "index" "HEAD" "objects" "refs" ]);
123 };
125 # Make a temporary dir that looks enough like the real thing for
126 # `git ls-files --recurse-submodules` to give us an accurate list
127 # of all the files in the index.
128 whitelist_file =
129 runCommand "git-ls-files" {envVariable = true;} ''
130 tmp=$(mktemp -d)
131 cd $tmp
132 ${ lib.optionalString (!isWorktree && builtins.pathExists gitModulesStr) ''
133 cp -ra ${gitSubmoduleFiles}/. $tmp
134 chmod +w -R $tmp
135 rm -rf .git
136 ''}
137 cp -r ${gitDir} .git
138 chmod +w -R .git
139 mkdir -p .git/objects .git/refs
140 mkdir -p .git-common/objects .git-common/refs
141 cp ${commonConfig} .git-common/config
142 echo ../.git-common > .git/commondir
143 ${ lib.optionalString (isWorktree && builtins.pathExists gitModulesStr) ''
144 cp ${gitModules} ./.gitmodules
145 ''}
146 ${git}/bin/git ls-files --recurse-submodules > $out
147 '';
149 whitelist = lines (readFile (whitelist_file.out));
151 filter = filter_from_list src whitelist;
152 in
153 cleanSourceWith {
154 caller = "cleanGit";
155 inherit name src subDir filter;
156 }
158else
159 trace "gitSource.nix: ${origSrcSubDir} does not seem to be a git repository,\nassuming it is a clean checkout." (
160 cleanSourceWith {
161 caller = "cleanGit";
162 inherit name src subDir;
163 }
164 )