on fa2fa13 help sign in
haskell.nix/lib/clean-git.nix
205 lines 8.0 kB history raw
1# From https://github.com/NixOS/nix/issues/2944
2{ lib, runCommand, git, cleanSourceWith }:
3{ name ? null, src, subDir ? "", includeSiblings ? false, keepGitDir ? false }:
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 remove_prefix = prefix: s:
28 builtins.substring
29 (builtins.stringLength prefix)
30 (builtins.stringLength s - builtins.stringLength prefix)
31 s;
33 lines = s: filter (x : x != [] && x != "") (split "\n" s);
35 origSrcSubDir = toString (src.origSrcSubDir or src);
36in
38if builtins.pathExists (origSrcSubDir + "/.git")
39then
40 let
41 hasIndex = builtins.pathExists (origSrcSubDir + "/.git/index");
42 isWorktree = (builtins.readDir origSrcSubDir).".git" == "regular";
44 # Identify the .git directory and filter just the files that we need.
45 gitDir = cleanSourceWith ({
46 caller = "cleanGit";
47 name = (if name == null then "" else name + "-") + "gitFiles";
48 filter = path: type:
49 type == "directory" ||
50 lib.any (i: (lib.hasSuffix i path)) [
51 "/config" "/index" "/HEAD" ] ||
52 (lib.strings.hasInfix "modules/" path &&
53 lib.any (i: (lib.hasSuffix i path)) [
54 "/config" "/index" "/HEAD" "/objects" "/refs" ]);
55 } // (
56 if hasIndex
57 then { inherit src; subDir = ".git"; }
58 else if !isWorktree
59 then abort "cleanGit: ${origSrcSubDir + "/.git"} has no index file"
60 else {
61 # likely a git worktree, so follow the indirection
62 src =
63 let
64 git_content = lines (readFile (origSrcSubDir + "/.git"));
65 first_line = head git_content;
66 prefix = "gitdir: ";
67 ok = length git_content == 1 && lib.hasPrefix prefix first_line;
68 raw_path = remove_prefix prefix first_line;
69 in
70 if ok
71 then
72 if lib.hasPrefix "/" raw_path
73 then /. + raw_path
74 else /. + builtins.toPath (origSrcSubDir + "/" + raw_path)
75 else abort "gitSource.nix: Cannot parse ${origSrcSubDir + "/.git"}";
76 }));
78 # Worktrees have a commondir pointing to the common `.git` dir. We need the
79 # config file from there to get the list of active submodules right.
80 commonConfig = if builtins.pathExists (toString gitDir.origSrc + "/commondir")
81 then
82 # The commondir file contains just the relative path of the
83 # common `.git` dir.
84 let
85 git_content = lines (readFile (toString gitDir.origSrc + "/commondir"));
86 first_line = head git_content;
87 in gitDir.origSrc + ("/" + first_line + "/config")
88 else gitDir + "/config";
90 # We need the .gitmodules file for submoules to work.
91 gitModulesStr = origSrcSubDir + "/.gitmodules";
92 gitModules = builtins.path { name = "gitmodules"; path = gitModulesStr; };
94 # Files from the git repository related knownSubmoduleDirs.
95 gitSubmoduleFiles = knownSubmoduleDirs:
96 let pathsNeeded = map (p: toString (src.origSrcSubDir or src) + "/${p}") (
97 lib.concatMap (x: all_paren_dirs (x + "/.git")) knownSubmoduleDirs);
98 in cleanSourceWith {
99 caller = "cleanGit";
100 name = (if name == null then "" else name + "-") + "gitSubmoduleFiles";
101 inherit src;
102 filter = path: type:
103 elem path pathsNeeded
104 ||
105 lib.any (i: (lib.hasSuffix i path)) [
106 ".gitmodules" ".git/config" ".git/index" ".git/HEAD" ".git/objects" ".git/refs" ]
107 ||
108 (lib.strings.hasInfix ".git/modules/" path &&
109 lib.any (i: (lib.hasSuffix i path)) [
110 "config" "index" "HEAD" "objects" "refs" ]);
111 };
113 hasSubmodules = !isWorktree && builtins.pathExists gitModulesStr;
115 # Make a temporary dir that looks enough like the real thing for
116 # `git ls-files --recurse-submodules` to give us an accurate list
117 # of all the files in the index.
118 whitelist_files = knownSubmoduleDirs:
119 runCommand "git-ls-files" { preferLocalBuild = true; } ''
120 tmp=$(mktemp -d)
121 cd $tmp
122 ${ lib.optionalString hasSubmodules ''
123 cp -ra ${gitSubmoduleFiles knownSubmoduleDirs}/. $tmp
124 chmod +w -R $tmp
125 ''}
126 cp -r ${gitDir} .git
127 chmod +w -R .git
128 mkdir -p .git/objects .git/refs
129 mkdir -p .git-common/objects .git-common/refs
130 cp ${commonConfig} .git-common/config
131 echo ../.git-common > .git/commondir
132 ${ lib.optionalString (isWorktree && builtins.pathExists gitModulesStr) ''
133 cp ${gitModules} ./.gitmodules
134 ''}
135 mkdir $out
136 ${ lib.optionalString hasSubmodules ''
137 ${git}/bin/git submodule status --recursive | awk '{ print $2 }' > $out/submoduleDirs
138 ''}
139 ${git}/bin/git ls-files --recurse-submodules > $out/files
140 '';
142 # Find the submodules
143 whitelist_recursive = knownSubmoduleDirs:
144 let
145 # Get the new list of submoduleDirs and files
146 # (useing the submoduleDirs we already know about)
147 files = whitelist_files knownSubmoduleDirs;
148 new = {
149 submoduleDirs = lines (readFile (files + "/submoduleDirs"));
150 files = lines (readFile (files + "/files"));
151 };
152 in
153 # If we are not expecting submodules or if the new list does not
154 # contain any additional submodules we are done.
155 if !hasSubmodules || lib.all (x: elem x knownSubmoduleDirs) new.submoduleDirs
156 then new
157 else
158 # Look again using what we know now about the submodules
159 whitelist_recursive new.submoduleDirs;
161 # Use empty as a starting point for looking for submodules.
162 # We could allow a list to be passed into cleanGit, but when testing
163 # on the `ghcjs` repo (one with a lot of submodules) it did not
164 # make much of a difference to the speed of `cleanGit`.
165 whitelist = whitelist_recursive [];
167 all_paren_dirs = p:
168 if p == "." || p == "/"
169 then []
170 else [ p ] ++ all_paren_dirs (dirOf p);
172 # All the paths that we need to keep as a set (including parent dirs)
173 whitelist_set = listToAttrs (
174 concatMap (p:
175 # Using `origSrcSubDir` (if present) makes it possible to cleanGit src that
176 # has already been cleaned with cleanSrcWith.
177 let full_path = toString (src.origSrcSubDir or src) + "/${p}"; in
178 map (p': { name = p'; value = true; }) (all_paren_dirs full_path)
179 ) whitelist.files
180 );
182 # Identify files in the `.git` dir
183 isGitDirPath = path:
184 path == origSrcSubDir + "/.git"
185 || lib.hasPrefix (origSrcSubDir + "/.git/") path;
187 filter = path: type:
188 hasAttr (toString path) whitelist_set
189 || (keepGitDir && isGitDirPath path);
190 in
191 cleanSourceWith {
192 caller = "cleanGit";
193 inherit name src subDir includeSiblings filter;
194 } // {
195 # For testing
196 # inherit whitelist whitelist_set;
197 }
199else
200 trace "haskell-nix.haskellLib.cleanGit: ${origSrcSubDir} does not seem to be a git repository,\nassuming it is a clean checkout." (
201 cleanSourceWith {
202 caller = "cleanGit";
203 inherit name src subDir includeSiblings;
204 }
205 )