History

Use structured format for blame script

Also provide more context in comment

Sebastian Thiel committed 1 year ago

Commit c6149701 parent 5d748af2 files · +109 −59Browse files

Unified · Split

2 files
  1. tests/it/src/args.rs +29 −7
  2. tests/it/src/commands/blame_copy_royal.rs +80 −52
tests/it/src/args.rs+29−7
@@ -14,11 +14,34 @@ pub struct Args { · 13 unmodified lines
14 14
15#[derive(Debug, clap::Subcommand)]15#[derive(Debug, clap::Subcommand)]
16pub enum Subcommands {16pub enum Subcommands {
17 /// Extract a file’s history so that its blame shows the same characteristics, in particular17 /// 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 to20 /// 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 information58 /// 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 the59 /// 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 },
tests/it/src/commands/blame_copy_royal.rs+80−52
@@ -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
153set -e159set -e
154 160
@@ -157,8 +163,46 @@ echo .gitignore >> .gitignore · 2 unmodified lines
157echo assets/ >> .gitignore163echo assets/ >> .gitignore
158echo create-history.sh >> .gitignore164echo create-history.sh >> .gitignore
159 165
160",166"
161 );167 .into(),
168 BlameScriptOperation::RemoveFile(src) => format!(
169 r"# delete previous version of file
170git rm {src}
171"
172 ),
173 BlameScriptOperation::CommitFile(src, commit_id) => format!(
174 r"# make file {src} contain content at commit {commit_id}
175mkdir -p $(dirname {src})
176cp ./assets/{commit_id}.commit ./{src}
177# create commit
178git add {src}
179git 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
220git 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}
229mkdir -p $(dirname {src})
230cp ./assets/{commit_id}.commit ./{src}
231# create commit
232git add {src}
233git 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.script277 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 }