##// END OF EJS Templates
copies-rust: parse the changed-file sidedata directly in rust...
marmoute -
r46674:e0313b0a default
parent child Browse files
Show More
@@ -25,7 +25,10 b' from . import ('
25
25
26 from .utils import stringutil
26 from .utils import stringutil
27
27
28 from .revlogutils import flagutil
28 from .revlogutils import (
29 flagutil,
30 sidedata as sidedatamod,
31 )
29
32
30 rustmod = policy.importrust("copy_tracing")
33 rustmod = policy.importrust("copy_tracing")
31
34
@@ -175,7 +178,7 b' def _committedforwardcopies(a, b, base, '
175 return cm
178 return cm
176
179
177
180
178 def _revinfo_getter(repo):
181 def _revinfo_getter(repo, match):
179 """returns a function that returns the following data given a <rev>"
182 """returns a function that returns the following data given a <rev>"
180
183
181 * p1: revision number of first parent
184 * p1: revision number of first parent
@@ -215,20 +218,42 b' def _revinfo_getter(repo):'
215 # time to save memory.
218 # time to save memory.
216 merge_caches = {}
219 merge_caches = {}
217
220
218 def revinfo(rev):
221 alwaysmatch = match.always()
219 p1, p2 = parents(rev)
222
220 value = None
223 if rustmod is not None and alwaysmatch:
221 e = merge_caches.pop(rev, None)
224
222 if e is not None:
225 def revinfo(rev):
223 return e
226 p1, p2 = parents(rev)
224 changes = None
227 value = None
225 if flags(rev) & HASCOPIESINFO:
228 e = merge_caches.pop(rev, None)
226 changes = changelogrevision(rev).changes
229 if e is not None:
227 value = (p1, p2, changes)
230 return e
228 if p1 != node.nullrev and p2 != node.nullrev:
231 if flags(rev) & HASCOPIESINFO:
229 # XXX some case we over cache, IGNORE
232 raw = changelogrevision(rev)._sidedata.get(sidedatamod.SD_FILES)
230 merge_caches[rev] = value
233 else:
231 return value
234 raw = None
235 value = (p1, p2, raw)
236 if p1 != node.nullrev and p2 != node.nullrev:
237 # XXX some case we over cache, IGNORE
238 merge_caches[rev] = value
239 return value
240
241 else:
242
243 def revinfo(rev):
244 p1, p2 = parents(rev)
245 value = None
246 e = merge_caches.pop(rev, None)
247 if e is not None:
248 return e
249 changes = None
250 if flags(rev) & HASCOPIESINFO:
251 changes = changelogrevision(rev).changes
252 value = (p1, p2, changes)
253 if p1 != node.nullrev and p2 != node.nullrev:
254 # XXX some case we over cache, IGNORE
255 merge_caches[rev] = value
256 return value
232
257
233 return revinfo
258 return revinfo
234
259
@@ -289,7 +314,7 b' def _changesetforwardcopies(a, b, match)'
289 revs = sorted(iterrevs)
314 revs = sorted(iterrevs)
290
315
291 if repo.filecopiesmode == b'changeset-sidedata':
316 if repo.filecopiesmode == b'changeset-sidedata':
292 revinfo = _revinfo_getter(repo)
317 revinfo = _revinfo_getter(repo, match)
293 return _combine_changeset_copies(
318 return _combine_changeset_copies(
294 revs, children, b.rev(), revinfo, match, isancestor
319 revs, children, b.rev(), revinfo, match, isancestor
295 )
320 )
@@ -5,8 +5,9 b' use crate::Revision;'
5 use im_rc::ordmap::DiffItem;
5 use im_rc::ordmap::DiffItem;
6 use im_rc::ordmap::OrdMap;
6 use im_rc::ordmap::OrdMap;
7
7
8 use std::cmp::Ordering;
8 use std::collections::HashMap;
9 use std::collections::HashMap;
9 use std::collections::HashSet;
10 use std::convert::TryInto;
10
11
11 pub type PathCopies = HashMap<HgPathBuf, HgPathBuf>;
12 pub type PathCopies = HashMap<HgPathBuf, HgPathBuf>;
12
13
@@ -23,18 +24,18 b' struct TimeStampedPathCopy {'
23 type TimeStampedPathCopies = OrdMap<HgPathBuf, TimeStampedPathCopy>;
24 type TimeStampedPathCopies = OrdMap<HgPathBuf, TimeStampedPathCopy>;
24
25
25 /// hold parent 1, parent 2 and relevant files actions.
26 /// hold parent 1, parent 2 and relevant files actions.
26 pub type RevInfo = (Revision, Revision, ChangedFiles);
27 pub type RevInfo<'a> = (Revision, Revision, ChangedFiles<'a>);
27
28
28 /// represent the files affected by a changesets
29 /// represent the files affected by a changesets
29 ///
30 ///
30 /// This hold a subset of mercurial.metadata.ChangingFiles as we do not need
31 /// This hold a subset of mercurial.metadata.ChangingFiles as we do not need
31 /// all the data categories tracked by it.
32 /// all the data categories tracked by it.
32 pub struct ChangedFiles {
33 /// This hold a subset of mercurial.metadata.ChangingFiles as we do not need
33 removed: HashSet<HgPathBuf>,
34 /// all the data categories tracked by it.
34 merged: HashSet<HgPathBuf>,
35 pub struct ChangedFiles<'a> {
35 salvaged: HashSet<HgPathBuf>,
36 nb_items: u32,
36 copied_from_p1: PathCopies,
37 index: &'a [u8],
37 copied_from_p2: PathCopies,
38 data: &'a [u8],
38 }
39 }
39
40
40 /// Represent active changes that affect the copy tracing.
41 /// Represent active changes that affect the copy tracing.
@@ -62,55 +63,161 b' enum MergeCase {'
62 Normal,
63 Normal,
63 }
64 }
64
65
65 impl ChangedFiles {
66 type FileChange<'a> = (u8, &'a HgPath, &'a HgPath);
66 pub fn new(
67
67 removed: HashSet<HgPathBuf>,
68 const EMPTY: &[u8] = b"";
68 merged: HashSet<HgPathBuf>,
69 const COPY_MASK: u8 = 3;
69 salvaged: HashSet<HgPathBuf>,
70 const P1_COPY: u8 = 2;
70 copied_from_p1: PathCopies,
71 const P2_COPY: u8 = 3;
71 copied_from_p2: PathCopies,
72 const ACTION_MASK: u8 = 28;
72 ) -> Self {
73 const REMOVED: u8 = 12;
73 ChangedFiles {
74 const MERGED: u8 = 8;
74 removed,
75 const SALVAGED: u8 = 16;
75 merged,
76
76 salvaged,
77 impl<'a> ChangedFiles<'a> {
77 copied_from_p1,
78 const INDEX_START: usize = 4;
78 copied_from_p2,
79 const ENTRY_SIZE: u32 = 9;
79 }
80 const FILENAME_START: u32 = 1;
81 const COPY_SOURCE_START: u32 = 5;
82
83 pub fn new(data: &'a [u8]) -> Self {
84 assert!(
85 data.len() >= 4,
86 "data size ({}) is too small to contain the header (4)",
87 data.len()
88 );
89 let nb_items_raw: [u8; 4] = (&data[0..=3])
90 .try_into()
91 .expect("failed to turn 4 bytes into 4 bytes");
92 let nb_items = u32::from_be_bytes(nb_items_raw);
93
94 let index_size = (nb_items * Self::ENTRY_SIZE) as usize;
95 let index_end = Self::INDEX_START + index_size;
96
97 assert!(
98 data.len() >= index_end,
99 "data size ({}) is too small to fit the index_data ({})",
100 data.len(),
101 index_end
102 );
103
104 let ret = ChangedFiles {
105 nb_items,
106 index: &data[Self::INDEX_START..index_end],
107 data: &data[index_end..],
108 };
109 let max_data = ret.filename_end(nb_items - 1) as usize;
110 assert!(
111 ret.data.len() >= max_data,
112 "data size ({}) is too small to fit all data ({})",
113 data.len(),
114 index_end + max_data
115 );
116 ret
80 }
117 }
81
118
82 pub fn new_empty() -> Self {
119 pub fn new_empty() -> Self {
83 ChangedFiles {
120 ChangedFiles {
84 removed: HashSet::new(),
121 nb_items: 0,
85 merged: HashSet::new(),
122 index: EMPTY,
86 salvaged: HashSet::new(),
123 data: EMPTY,
87 copied_from_p1: PathCopies::new(),
124 }
88 copied_from_p2: PathCopies::new(),
125 }
126
127 /// internal function to return an individual entry at a given index
128 fn entry(&'a self, idx: u32) -> FileChange<'a> {
129 if idx >= self.nb_items {
130 panic!(
131 "index for entry is higher that the number of file {} >= {}",
132 idx, self.nb_items
133 )
134 }
135 let flags = self.flags(idx);
136 let filename = self.filename(idx);
137 let copy_idx = self.copy_idx(idx);
138 let copy_source = self.filename(copy_idx);
139 (flags, filename, copy_source)
140 }
141
142 /// internal function to return the filename of the entry at a given index
143 fn filename(&self, idx: u32) -> &HgPath {
144 let filename_start;
145 if idx == 0 {
146 filename_start = 0;
147 } else {
148 filename_start = self.filename_end(idx - 1)
89 }
149 }
150 let filename_end = self.filename_end(idx);
151 let filename_start = filename_start as usize;
152 let filename_end = filename_end as usize;
153 HgPath::new(&self.data[filename_start..filename_end])
154 }
155
156 /// internal function to return the flag field of the entry at a given
157 /// index
158 fn flags(&self, idx: u32) -> u8 {
159 let idx = idx as usize;
160 self.index[idx * (Self::ENTRY_SIZE as usize)]
161 }
162
163 /// internal function to return the end of a filename part at a given index
164 fn filename_end(&self, idx: u32) -> u32 {
165 let start = (idx * Self::ENTRY_SIZE) + Self::FILENAME_START;
166 let end = (idx * Self::ENTRY_SIZE) + Self::COPY_SOURCE_START;
167 let start = start as usize;
168 let end = end as usize;
169 let raw = (&self.index[start..end])
170 .try_into()
171 .expect("failed to turn 4 bytes into 4 bytes");
172 u32::from_be_bytes(raw)
173 }
174
175 /// internal function to return index of the copy source of the entry at a
176 /// given index
177 fn copy_idx(&self, idx: u32) -> u32 {
178 let start = (idx * Self::ENTRY_SIZE) + Self::COPY_SOURCE_START;
179 let end = (idx + 1) * Self::ENTRY_SIZE;
180 let start = start as usize;
181 let end = end as usize;
182 let raw = (&self.index[start..end])
183 .try_into()
184 .expect("failed to turn 4 bytes into 4 bytes");
185 u32::from_be_bytes(raw)
90 }
186 }
91
187
92 /// Return an iterator over all the `Action` in this instance.
188 /// Return an iterator over all the `Action` in this instance.
93 fn iter_actions(&self, parent: usize) -> impl Iterator<Item = Action> {
189 fn iter_actions(&self, parent: usize) -> ActionsIterator {
94 let copies_iter = match parent {
190 ActionsIterator {
95 1 => self.copied_from_p1.iter(),
191 changes: &self,
96 2 => self.copied_from_p2.iter(),
192 parent: parent,
97 _ => unreachable!(),
193 current: 0,
98 };
194 }
99 let remove_iter = self.removed.iter();
100 let copies_iter = copies_iter.map(|(x, y)| Action::Copied(x, y));
101 let remove_iter = remove_iter.map(|x| Action::Removed(x));
102 copies_iter.chain(remove_iter)
103 }
195 }
104
196
105 /// return the MergeCase value associated with a filename
197 /// return the MergeCase value associated with a filename
106 fn get_merge_case(&self, path: &HgPath) -> MergeCase {
198 fn get_merge_case(&self, path: &HgPath) -> MergeCase {
107 if self.salvaged.contains(path) {
199 if self.nb_items == 0 {
108 return MergeCase::Salvaged;
109 } else if self.merged.contains(path) {
110 return MergeCase::Merged;
111 } else {
112 return MergeCase::Normal;
200 return MergeCase::Normal;
113 }
201 }
202 let mut low_part = 0;
203 let mut high_part = self.nb_items;
204
205 while low_part < high_part {
206 let cursor = (low_part + high_part - 1) / 2;
207 let (flags, filename, _source) = self.entry(cursor);
208 match path.cmp(filename) {
209 Ordering::Less => low_part = cursor + 1,
210 Ordering::Greater => high_part = cursor,
211 Ordering::Equal => {
212 return match flags & ACTION_MASK {
213 MERGED => MergeCase::Merged,
214 SALVAGED => MergeCase::Salvaged,
215 _ => MergeCase::Normal,
216 };
217 }
218 }
219 }
220 MergeCase::Normal
114 }
221 }
115 }
222 }
116
223
@@ -150,6 +257,50 b" impl<'a, A: Fn(Revision, Revision) -> bo"
150 }
257 }
151 }
258 }
152
259
260 struct ActionsIterator<'a> {
261 changes: &'a ChangedFiles<'a>,
262 parent: usize,
263 current: u32,
264 }
265
266 impl<'a> Iterator for ActionsIterator<'a> {
267 type Item = Action<'a>;
268
269 fn next(&mut self) -> Option<Action<'a>> {
270 while self.current < self.changes.nb_items {
271 let (flags, file, source) = self.changes.entry(self.current);
272 self.current += 1;
273 if (flags & ACTION_MASK) == REMOVED {
274 return Some(Action::Removed(file));
275 }
276 let copy = flags & COPY_MASK;
277 if self.parent == 1 && copy == P1_COPY {
278 return Some(Action::Copied(file, source));
279 }
280 if self.parent == 2 && copy == P2_COPY {
281 return Some(Action::Copied(file, source));
282 }
283 }
284 return None;
285 }
286 }
287
288 /// A small struct whose purpose is to ensure lifetime of bytes referenced in
289 /// ChangedFiles
290 ///
291 /// It is passed to the RevInfoMaker callback who can assign any necessary
292 /// content to the `data` attribute. The copy tracing code is responsible for
293 /// keeping the DataHolder alive at least as long as the ChangedFiles object.
294 pub struct DataHolder<D> {
295 /// RevInfoMaker callback should assign data referenced by the
296 /// ChangedFiles struct it return to this attribute. The DataHolder
297 /// lifetime will be at least as long as the ChangedFiles one.
298 pub data: Option<D>,
299 }
300
301 pub type RevInfoMaker<'a, D> =
302 Box<dyn for<'r> Fn(Revision, &'r mut DataHolder<D>) -> RevInfo<'r> + 'a>;
303
153 /// Same as mercurial.copies._combine_changeset_copies, but in Rust.
304 /// Same as mercurial.copies._combine_changeset_copies, but in Rust.
154 ///
305 ///
155 /// Arguments are:
306 /// Arguments are:
@@ -163,11 +314,11 b" impl<'a, A: Fn(Revision, Revision) -> bo"
163 /// * ChangedFiles
314 /// * ChangedFiles
164 /// isancestors(low_rev, high_rev): callback to check if a revision is an
315 /// isancestors(low_rev, high_rev): callback to check if a revision is an
165 /// ancestor of another
316 /// ancestor of another
166 pub fn combine_changeset_copies<A: Fn(Revision, Revision) -> bool>(
317 pub fn combine_changeset_copies<A: Fn(Revision, Revision) -> bool, D>(
167 revs: Vec<Revision>,
318 revs: Vec<Revision>,
168 children: HashMap<Revision, Vec<Revision>>,
319 children: HashMap<Revision, Vec<Revision>>,
169 target_rev: Revision,
320 target_rev: Revision,
170 rev_info: &impl Fn(Revision) -> RevInfo,
321 rev_info: RevInfoMaker<D>,
171 is_ancestor: &A,
322 is_ancestor: &A,
172 ) -> PathCopies {
323 ) -> PathCopies {
173 let mut all_copies = HashMap::new();
324 let mut all_copies = HashMap::new();
@@ -189,8 +340,9 b' pub fn combine_changeset_copies<A: Fn(Re'
189 for child in current_children {
340 for child in current_children {
190 // We will chain the copies information accumulated for `rev` with
341 // We will chain the copies information accumulated for `rev` with
191 // the individual copies information for each of its children.
342 // the individual copies information for each of its children.
192 // Creating a new PathCopies for each `rev` ? `children` vertex.
343 // Creating a new PathCopies for each `rev` `children` vertex.
193 let (p1, p2, changes) = rev_info(*child);
344 let mut d: DataHolder<D> = DataHolder { data: None };
345 let (p1, p2, changes) = rev_info(*child, &mut d);
194
346
195 let parent = if rev == p1 {
347 let parent = if rev == p1 {
196 1
348 1
@@ -11,8 +11,9 b' use cpython::Python;'
11
11
12 use hg::copy_tracing::combine_changeset_copies;
12 use hg::copy_tracing::combine_changeset_copies;
13 use hg::copy_tracing::ChangedFiles;
13 use hg::copy_tracing::ChangedFiles;
14 use hg::copy_tracing::DataHolder;
14 use hg::copy_tracing::RevInfo;
15 use hg::copy_tracing::RevInfo;
15 use hg::utils::hg_path::HgPathBuf;
16 use hg::copy_tracing::RevInfoMaker;
16 use hg::Revision;
17 use hg::Revision;
17
18
18 /// Combines copies information contained into revision `revs` to build a copy
19 /// Combines copies information contained into revision `revs` to build a copy
@@ -57,184 +58,41 b' pub fn combine_changeset_copies_wrapper('
57 // happens in case of programing error or severe data corruption. Such
58 // happens in case of programing error or severe data corruption. Such
58 // errors will raise panic and the rust-cpython harness will turn them into
59 // errors will raise panic and the rust-cpython harness will turn them into
59 // Python exception.
60 // Python exception.
60 let rev_info_maker = |rev: Revision| -> RevInfo {
61 let rev_info_maker: RevInfoMaker<PyBytes> =
61 let res: PyTuple = rev_info
62 Box::new(|rev: Revision, d: &mut DataHolder<PyBytes>| -> RevInfo {
62 .call(py, (rev,), None)
63 let res: PyTuple = rev_info
63 .expect("rust-copy-tracing: python call to `rev_info` failed")
64 .call(py, (rev,), None)
64 .cast_into(py)
65 .expect("rust-copy-tracing: python call to `rev_info` failed")
65 .expect(
66 "rust-copy_tracing: python call to `rev_info` returned \
67 unexpected non-Tuple value",
68 );
69 let p1 = res.get_item(py, 0).extract(py).expect(
70 "rust-copy-tracing: \
71 rev_info return is invalid, first item is a not a revision",
72 );
73 let p2 = res.get_item(py, 1).extract(py).expect(
74 "rust-copy-tracing: \
75 rev_info return is invalid, second item is a not a revision",
76 );
77
78 let changes = res.get_item(py, 2);
79
80 let files;
81 if !changes
82 .hasattr(py, "copied_from_p1")
83 .expect("rust-copy-tracing: python call to `hasattr` failed")
84 {
85 files = ChangedFiles::new_empty();
86 } else {
87 let p1_copies: PyDict = changes
88 .getattr(py, "copied_from_p1")
89 .expect(
90 "rust-copy-tracing: retrieval of python attribute \
91 `copied_from_p1` failed",
92 )
93 .cast_into(py)
94 .expect(
95 "rust-copy-tracing: failed to convert `copied_from_p1` \
96 to PyDict",
97 );
98 let p1_copies: PyResult<_> = p1_copies
99 .items(py)
100 .iter()
101 .map(|(key, value)| {
102 let key = key.extract::<PyBytes>(py).expect(
103 "rust-copy-tracing: conversion of copy destination to\
104 PyBytes failed",
105 );
106 let key = key.data(py);
107 let value = value.extract::<PyBytes>(py).expect(
108 "rust-copy-tracing: conversion of copy source to \
109 PyBytes failed",
110 );
111 let value = value.data(py);
112 Ok((
113 HgPathBuf::from_bytes(key),
114 HgPathBuf::from_bytes(value),
115 ))
116 })
117 .collect();
118
119 let p2_copies: PyDict = changes
120 .getattr(py, "copied_from_p2")
121 .expect(
122 "rust-copy-tracing: retrieval of python attribute \
123 `copied_from_p2` failed",
124 )
125 .cast_into(py)
66 .cast_into(py)
126 .expect(
67 .expect(
127 "rust-copy-tracing: failed to convert `copied_from_p2` \
68 "rust-copy_tracing: python call to `rev_info` returned \
128 to PyDict",
69 unexpected non-Tuple value",
129 );
70 );
130 let p2_copies: PyResult<_> = p2_copies
71 let p1 = res.get_item(py, 0).extract(py).expect(
131 .items(py)
72 "rust-copy-tracing: rev_info return is invalid, first item \
132 .iter()
73 is a not a revision",
133 .map(|(key, value)| {
134 let key = key.extract::<PyBytes>(py).expect(
135 "rust-copy-tracing: conversion of copy destination to \
136 PyBytes failed");
137 let key = key.data(py);
138 let value = value.extract::<PyBytes>(py).expect(
139 "rust-copy-tracing: conversion of copy source to \
140 PyBytes failed",
141 );
142 let value = value.data(py);
143 Ok((
144 HgPathBuf::from_bytes(key),
145 HgPathBuf::from_bytes(value),
146 ))
147 })
148 .collect();
149
150 let removed: PyObject = changes.getattr(py, "removed").expect(
151 "rust-copy-tracing: retrieval of python attribute \
152 `removed` failed",
153 );
74 );
154 let removed: PyResult<_> = removed
75 let p2 = res.get_item(py, 1).extract(py).expect(
155 .iter(py)
76 "rust-copy-tracing: rev_info return is invalid, first item \
156 .expect(
77 is a not a revision",
157 "rust-copy-tracing: getting a python iterator over the \
158 `removed` set failed",
159 )
160 .map(|filename| {
161 let filename = filename
162 .expect(
163 "rust-copy-tracing: python iteration over the \
164 `removed` set failed",
165 )
166 .extract::<PyBytes>(py)
167 .expect(
168 "rust-copy-tracing: \
169 conversion of `removed` item to PyBytes failed",
170 );
171 let filename = filename.data(py);
172 Ok(HgPathBuf::from_bytes(filename))
173 })
174 .collect();
175
176 let merged: PyObject = changes.getattr(py, "merged").expect(
177 "rust-copy-tracing: retrieval of python attribute \
178 `merged` failed",
179 );
78 );
180 let merged: PyResult<_> = merged
181 .iter(py)
182 .expect(
183 "rust-copy-tracing: getting a python iterator over the \
184 `merged` set failed",
185 )
186 .map(|filename| {
187 let filename = filename
188 .expect(
189 "rust-copy-tracing: python iteration over the \
190 `merged` set failed",
191 )
192 .extract::<PyBytes>(py)
193 .expect(
194 "rust-copy-tracing: \
195 conversion of `merged` item to PyBytes failed",
196 );
197 let filename = filename.data(py);
198 Ok(HgPathBuf::from_bytes(filename))
199 })
200 .collect();
201
79
202 let salvaged: PyObject = changes.getattr(py, "salvaged").expect(
80 let files = match res.get_item(py, 2).extract::<PyBytes>(py) {
203 "rust-copy-tracing: retrieval of python attribute \
81 Ok(raw) => {
204 `salvaged` failed",
82 // Give responsability for the raw bytes lifetime to
205 );
83 // hg-core
206 let salvaged: PyResult<_> = salvaged
84 d.data = Some(raw);
207 .iter(py)
85 let addrs = d.data.as_ref().expect(
208 .expect(
86 "rust-copy-tracing: failed to get a reference to the \
209 "rust-copy-tracing: getting a python iterator over the \
87 raw bytes for copy data").data(py);
210 `salvaged` set failed",
88 ChangedFiles::new(addrs)
211 )
89 }
212 .map(|filename| {
90 // value was presumably None, meaning they was no copy data.
213 let filename = filename
91 Err(_) => ChangedFiles::new_empty(),
214 .expect(
92 };
215 "rust-copy-tracing: python iteration over the \
216 `salvaged` set failed",
217 )
218 .extract::<PyBytes>(py)
219 .expect(
220 "rust-copy-tracing: \
221 conversion of `salvaged` item to PyBytes failed",
222 );
223 let filename = filename.data(py);
224 Ok(HgPathBuf::from_bytes(filename))
225 })
226 .collect();
227 files = ChangedFiles::new(
228 removed.unwrap(),
229 merged.unwrap(),
230 salvaged.unwrap(),
231 p1_copies.unwrap(),
232 p2_copies.unwrap(),
233 );
234 }
235
93
236 (p1, p2, files)
94 (p1, p2, files)
237 };
95 });
238 let children: PyResult<_> = children
96 let children: PyResult<_> = children
239 .items(py)
97 .items(py)
240 .iter()
98 .iter()
@@ -250,7 +108,7 b' pub fn combine_changeset_copies_wrapper('
250 revs?,
108 revs?,
251 children?,
109 children?,
252 target_rev,
110 target_rev,
253 &rev_info_maker,
111 rev_info_maker,
254 &is_ancestor_wrap,
112 &is_ancestor_wrap,
255 );
113 );
256 let out = PyDict::new(py);
114 let out = PyDict::new(py);
General Comments 0
You need to be logged in to leave comments. Login now