Also provide more context in comment
Unified · Split
| @@ -14,11 +14,34 @@ pub struct Args { · 13 unmodified lines | |||
| 14 | 14 | ||
| 15 | #[derive(Debug, clap::Subcommand)] | 15 | #[derive(Debug, clap::Subcommand)] |
| 16 | pub enum Subcommands { | 16 | pub enum Subcommands { |
| 17 | /// Extract a file’s history so that its blame shows the same characteristics, in particular | 17 | /// Generate a shell script that creates a git repository containing all commits that are |
| 18 | /// bugs, as the original, but in a way that can't be traced back uniquely to its source. | 18 | /// traversed when a blame is generated. |
| 19 | /// | 19 | /// |
| 20 | /// The idea is that we don't want to deal with licensing, it's more about patterns in order to | 20 | /// This command extracts the file’s history so that blame, when run on the repository created |
| 21 | /// reproduce cases for tests. | 21 | /// by the script, shows the same characteristics, in particular bugs, as the original, but in |
| 22 | /// a way that the original source file's content cannot be reconstructed. | ||
| 23 | /// | ||
| 24 | /// The idea is that by obfuscating the file's content we make it easier for people to share | ||
| 25 | /// the subset of data that's required for debugging purposes from repositories that are not | ||
| 26 | /// public. | ||
| 27 | /// | ||
| 28 | /// Note that the obfuscation leaves certain properties of the source intact, so they can still | ||
| 29 | /// be inferred from the extracted history. Among these properties are directory structure | ||
| 30 | /// (though not the directories' names), renames, number of lines, and whitespace. | ||
| 31 | /// | ||
| 32 | /// This command can also be helpful in debugging the blame algorithm itself. | ||
| 33 | /// | ||
| 34 | /// ### Terminology | ||
| 35 | /// | ||
| 36 | /// A **blame history** is the set of commits that the blame algorithm, at some point, treated | ||
| 37 | /// as potential suspects for any line in a file. It is a subset of all commits that ever | ||
| 38 | /// changed a file in its history. | ||
| 39 | /// | ||
| 40 | /// With respect to branches and merge commits, the **blame history** will not necessarily be | ||
| 41 | /// identical to the file's history in the source repository. This is because the blame | ||
| 42 | /// algorithm will stop following a file's history for branches that only touch lines for which | ||
| 43 | /// the source has already been found. The **blame history**, thus, looks likely "cleaner" and | ||
| 44 | /// "simpler" than the source history. | ||
| 22 | #[clap(visible_alias = "bcr")] | 45 | #[clap(visible_alias = "bcr")] |
| 23 | BlameCopyRoyal { | 46 | BlameCopyRoyal { |
| 24 | /// Don't really copy anything. | 47 | /// Don't really copy anything. |
| @@ -32,9 +55,8 @@ pub enum Subcommands { · 7 unmodified lines | |||
| 32 | file: std::ffi::OsString, | 55 | file: std::ffi::OsString, |
| 33 | /// Do not use `copy-royal` to obfuscate the content of blobs, but copy it verbatim. | 56 | /// Do not use `copy-royal` to obfuscate the content of blobs, but copy it verbatim. |
| 34 | /// | 57 | /// |
| 35 | /// Note that this should only be done if the source repository only contains information | 58 | /// Note that this should only be done if the source history does not contain information |
| 36 | /// you’re willing to share. Also note that the obfuscation leaves the structure of the | 59 | /// you're not willing to share. |
| 37 | /// source intact, so a few of its properties can still be inferred. | ||
| 38 | #[clap(long)] | 60 | #[clap(long)] |
| 39 | verbatim: bool, | 61 | verbatim: bool, |
| 40 | }, | 62 | }, |
| @@ -6,7 +6,7 @@ pub(super) mod function { · 5 unmodified lines | |||
| 6 | use anyhow::Context; | 6 | use anyhow::Context; |
| 7 | use gix::{ | 7 | use gix::{ |
| 8 | blame::BlamePathEntry, | 8 | blame::BlamePathEntry, |
| 9 | bstr::{BStr, ByteSlice}, | 9 | bstr::{BStr, BString, ByteSlice}, |
| 10 | objs::FindExt, | 10 | objs::FindExt, |
| 11 | ObjectId, | 11 | ObjectId, |
| 12 | }; | 12 | }; |
| @@ -130,25 +130,31 @@ pub(super) mod function { · 117 unmodified lines | |||
| 130 | ); | 130 | ); |
| 131 | 131 | ||
| 132 | if !dry_run { | 132 | if !dry_run { |
| 133 | std::fs::write(script_file, blame_script.script)?; | 133 | let blocks: Vec<_> = blame_script |
| 134 | .script | ||
| 135 | .iter() | ||
| 136 | .map(|operation| operation.to_string()) | ||
| 137 | .collect(); | ||
| 138 | |||
| 139 | std::fs::write(script_file, blocks.join(""))?; | ||
| 134 | } | 140 | } |
| 135 | 141 | ||
| 136 | Ok(()) | 142 | Ok(()) |
| 137 | } | 143 | } |
| 138 | 144 | ||
| 139 | struct BlameScript { | 145 | enum BlameScriptOperation { |
| 140 | blame_path: Vec<BlamePathEntry>, | 146 | InitRepository, |
| 141 | seen: BTreeSet<ObjectId>, | 147 | RemoveFile(String), |
| 142 | script: String, | 148 | CommitFile(BString, ObjectId), |
| 143 | options: Options, | 149 | CheckoutTag(ObjectId), |
| 150 | PrepareMerge(Vec<ObjectId>), | ||
| 151 | CreateTag(ObjectId), | ||
| 144 | } | 152 | } |
| 145 | 153 | ||
| 146 | impl BlameScript { | 154 | impl BlameScriptOperation { |
| 147 | fn new(blame_path: Vec<BlamePathEntry>, options: Options) -> Self { | 155 | fn to_string(&self) -> String { |
| 148 | let mut script = String::new(); | 156 | match self { |
| 149 | 157 | BlameScriptOperation::InitRepository => r"#!/bin/sh | |
| 150 | script.push_str( | ||
| 151 | r"#!/bin/sh | ||
| 152 | 158 | ||
| 153 | set -e | 159 | set -e |
| 154 | 160 | ||
| @@ -157,8 +163,46 @@ echo .gitignore >> .gitignore · 2 unmodified lines | |||
| 157 | echo assets/ >> .gitignore | 163 | echo assets/ >> .gitignore |
| 158 | echo create-history.sh >> .gitignore | 164 | echo create-history.sh >> .gitignore |
| 159 | 165 | ||
| 160 | ", | 166 | " |
| 161 | ); | 167 | .into(), |
| 168 | BlameScriptOperation::RemoveFile(src) => format!( | ||
| 169 | r"# delete previous version of file | ||
| 170 | git rm {src} | ||
| 171 | " | ||
| 172 | ), | ||
| 173 | BlameScriptOperation::CommitFile(src, commit_id) => format!( | ||
| 174 | r"# make file {src} contain content at commit {commit_id} | ||
| 175 | mkdir -p $(dirname {src}) | ||
| 176 | cp ./assets/{commit_id}.commit ./{src} | ||
| 177 | # create commit | ||
| 178 | git add {src} | ||
| 179 | git commit -m {commit_id} | ||
| 180 | " | ||
| 181 | ), | ||
| 182 | BlameScriptOperation::CheckoutTag(commit_id) => format!("git checkout tag-{}\n", commit_id), | ||
| 183 | BlameScriptOperation::PrepareMerge(commit_ids) => format!( | ||
| 184 | "git merge --no-commit {} || true\n", | ||
| 185 | commit_ids | ||
| 186 | .iter() | ||
| 187 | .map(|commit_id| format!("tag-{commit_id}")) | ||
| 188 | .collect::<Vec<_>>() | ||
| 189 | .join(" ") | ||
| 190 | ), | ||
| 191 | BlameScriptOperation::CreateTag(commit_id) => format!("git tag tag-{commit_id}\n\n"), | ||
| 192 | } | ||
| 193 | } | ||
| 194 | } | ||
| 195 | |||
| 196 | struct BlameScript { | ||
| 197 | blame_path: Vec<BlamePathEntry>, | ||
| 198 | seen: BTreeSet<ObjectId>, | ||
| 199 | script: Vec<BlameScriptOperation>, | ||
| 200 | options: Options, | ||
| 201 | } | ||
| 202 | |||
| 203 | impl BlameScript { | ||
| 204 | fn new(blame_path: Vec<BlamePathEntry>, options: Options) -> Self { | ||
| 205 | let script = vec![BlameScriptOperation::InitRepository]; | ||
| 162 | 206 | ||
| 163 | Self { | 207 | Self { |
| 164 | blame_path, | 208 | blame_path, |
| @@ -202,7 +246,7 @@ echo create-history.sh >> .gitignore · 37 unmodified lines | |||
| 202 | }; | 246 | }; |
| 203 | let commit_id = blame_path_entry.commit_id; | 247 | let commit_id = blame_path_entry.commit_id; |
| 204 | 248 | ||
| 205 | let delete_previous_file_script = match &blame_path_entry.previous_source_file_path { | 249 | let delete_previous_file_operation = match &blame_path_entry.previous_source_file_path { |
| 206 | Some(previous_source_file_path) if source_file_path != *previous_source_file_path => { | 250 | Some(previous_source_file_path) if source_file_path != *previous_source_file_path => { |
| 207 | let src = if self.options.verbatim { | 251 | let src = if self.options.verbatim { |
| 208 | previous_source_file_path.to_string() | 252 | previous_source_file_path.to_string() |
| @@ -215,57 +259,41 @@ echo create-history.sh >> .gitignore · 6 unmodified lines | |||
| 215 | crate::commands::copy_royal::remapped(source_file_path) | 259 | crate::commands::copy_royal::remapped(source_file_path) |
| 216 | }; | 260 | }; |
| 217 | 261 | ||
| 218 | format!( | 262 | Some(BlameScriptOperation::RemoveFile(src)) |
| 219 | r"# delete previous version of file | ||
| 220 | git rm {src} | ||
| 221 | " | ||
| 222 | ) | ||
| 223 | } | 263 | } |
| 224 | _ => String::new(), | 264 | _ => None, |
| 225 | }; | 265 | }; |
| 226 | 266 | ||
| 227 | let script = format!( | ||
| 228 | r"# make file {src} contain content at commit {commit_id} | ||
| 229 | mkdir -p $(dirname {src}) | ||
| 230 | cp ./assets/{commit_id}.commit ./{src} | ||
| 231 | # create commit | ||
| 232 | git add {src} | ||
| 233 | git commit -m {commit_id} | ||
| 234 | " | ||
| 235 | ); | ||
| 236 | |||
| 237 | if parents.is_empty() { | 267 | if parents.is_empty() { |
| 238 | self.script.push_str(delete_previous_file_script.as_str()); | 268 | if let Some(delete_previous_file_operation) = delete_previous_file_operation { |
| 239 | self.script.push_str(script.as_str()); | 269 | self.script.push(delete_previous_file_operation); |
| 270 | } | ||
| 271 | self.script.push(BlameScriptOperation::CommitFile(src, commit_id)); | ||
| 240 | } else { | 272 | } else { |
| 241 | let ([first], rest) = parents.split_at(1) else { | 273 | let ([first], rest) = parents.split_at(1) else { |
| 242 | unreachable!(); | 274 | unreachable!(); |
| 243 | }; | 275 | }; |
| 244 | 276 | ||
| 245 | self.script | 277 | self.script.push(BlameScriptOperation::CheckoutTag(first.commit_id)); |
| 246 | .push_str(format!("git checkout tag-{}\n", first.commit_id).as_str()); | ||
| 247 | 278 | ||
| 248 | if rest.is_empty() { | 279 | if rest.is_empty() { |
| 249 | self.script.push_str(delete_previous_file_script.as_str()); | 280 | if let Some(delete_previous_file_operation) = delete_previous_file_operation { |
| 250 | self.script.push_str(script.as_str()); | 281 | self.script.push(delete_previous_file_operation); |
| 282 | } | ||
| 283 | self.script.push(BlameScriptOperation::CommitFile(src, commit_id)); | ||
| 251 | } else { | 284 | } else { |
| 252 | self.script.push_str( | 285 | self.script.push(BlameScriptOperation::PrepareMerge( |
| 253 | format!( | 286 | rest.iter().map(|blame_path_entry| blame_path_entry.commit_id).collect(), |
| 254 | "git merge --no-commit {} || true\n", | 287 | )); |
| 255 | rest.iter() | 288 | |
| 256 | .map(|blame_path_entry| format!("tag-{}", blame_path_entry.commit_id)) | 289 | if let Some(delete_previous_file_operation) = delete_previous_file_operation { |
| 257 | .collect::<Vec<_>>() | 290 | self.script.push(delete_previous_file_operation); |
| 258 | .join(" ") | 291 | } |
| 259 | ) | 292 | self.script.push(BlameScriptOperation::CommitFile(src, commit_id)); |
| 260 | .as_str(), | ||
| 261 | ); | ||
| 262 | |||
| 263 | self.script.push_str(delete_previous_file_script.as_str()); | ||
| 264 | self.script.push_str(script.as_str()); | ||
| 265 | } | 293 | } |
| 266 | } | 294 | } |
| 267 | 295 | ||
| 268 | self.script.push_str(format!("git tag tag-{commit_id}\n\n").as_str()); | 296 | self.script.push(BlameScriptOperation::CreateTag(commit_id)); |
| 269 | 297 | ||
| 270 | Ok(()) | 298 | Ok(()) |
| 271 | } | 299 | } |